ふくふくHukuhuku Inc.
EP.13Foundation 12分公開: 2026-05-10

べき等性 (Idempotency):「同じ処理を 2 回実行しても結果が変わらない」を保証する

ジョブが失敗してリトライ → データが二重計上された。バッチが重複実行 → 売上が 2 倍に。データ基盤の悲劇の多くはべき等性の欠如が原因。Idempotency Key・MERGE・upsert・冪等な API 設計まで Python / SQL サンプル付き。

#べき等性#idempotency#データパイプライン#API
CO📔 Google Colab で開く(上から順にセルを実行)
シェア

べき等性 (idempotency) は、データ基盤・API・分散システムの信頼性の核となる概念。本記事では「同じ処理を何度実行しても結果が変わらない」をどう設計・実装するかを、データパイプライン・SQL・API の各層で扱います。

1. なぜべき等性が必要か

  • ジョブのリトライ: ネットワーク失敗・タイムアウトで自動再実行
  • デプロイの重複: CI/CD の二重実行、手動オペレーションミス
  • メッセージキューの重複配信: SQS / Kafka は at-least-once が基本
  • ネットワーク分断: クライアントが「失敗」と判定したが実は成功していたケース
  • 人為的な再実行: 「とりあえず再起動」「もう一度走らせて」
実例: 二重課金事故

Stripe API への課金リクエスト がネットワーク timeout → クライアントがリトライ → 実は最初のリクエストも成功していた → 同じ顧客に 2 回課金。Stripe は Idempotency-Key ヘッダーでこれを防げる仕組みを提供しているが、使い忘れると事故る。

2. べき等な操作 vs 非べき等な操作

操作べき等?理由
SET x = 5 (代入)何度やっても x = 5
x = x + 1 (インクリメント)実行回数で値が変わる
DELETE FROM users WHERE id=1あっても無くても結果は「無い」
INSERT INTO orders ...毎回新しい行が増える
UPSERT (MERGE)存在すれば更新、無ければ挿入
DROP TABLE IF EXISTS存在チェック付き
HTTP GET同じ URL は同じレスポンス
HTTP DELETE削除済みでも 404 を返すだけ
HTTP POST (素朴)毎回新しいリソース作成
HTTP PUTリソース全体を上書き

3. SQL でべき等にする (UPSERT / MERGE)

PostgreSQL: ON CONFLICT
SQL
-- 非べき等: 重複実行で行が増えるINSERT INTO users (id, name, email)VALUES (1, '佐藤', 'sato@example.com');
-- べき等: 重複時は何もしない (or 更新)INSERT INTO users (id, name, email)VALUES (1, '佐藤', 'sato@example.com')ON CONFLICT (id) DO NOTHING;
-- べき等: 存在すれば更新 (UPSERT)INSERT INTO users (id, name, email)VALUES (1, '佐藤', 'sato@example.com')ON CONFLICT (id) DO UPDATESET name = EXCLUDED.name,    email = EXCLUDED.email;
BigQuery / Snowflake: MERGE
SQL
-- 標準 SQL の MERGE 文MERGE INTO target_table TUSING source_table S  ON T.id = S.idWHEN MATCHED THEN  UPDATE SET T.value = S.value, T.updated_at = CURRENT_TIMESTAMP()WHEN NOT MATCHED THEN  INSERT (id, value, created_at)  VALUES (S.id, S.value, CURRENT_TIMESTAMP());
-- これを 100 回実行しても結果は同じ

4. dbt incremental モデルでのべき等性

dbt: unique_key を指定して merge 化
SQL
{{    config(        materialized='incremental',        unique_key='order_id',        on_schema_change='sync_all_columns'    )}}
SELECT    order_id,    user_id,    amount,    created_atFROM {{ source('raw', 'orders') }}{% if is_incremental() %}  WHERE created_at >= (SELECT MAX(created_at) FROM {{ this }})        - INTERVAL 1 DAY  -- オーバーラップで取りこぼし防止{% endif %}-- unique_key='order_id' を指定すると、dbt が MERGE 文を生成-- 重複実行しても行は重複しない

5. Python (Airflow タスク) でべき等にする

「処理済みリスト」テーブルでチェック
Python
def process_order(order_id, db):    # 既に処理済みかチェック    cursor = db.execute(        "SELECT 1 FROM processed_orders WHERE order_id = %s",        (order_id,)    )    if cursor.fetchone():        print(f"order {order_id} は処理済み、skip")        return
    # トランザクション内で「処理 + 処理済みマーク」を一括    with db.transaction():        # 実際の処理 (例: 在庫減らす、メール送る)        db.execute(            "UPDATE inventory SET stock = stock - 1 WHERE id = %s",            (order_id,)        )        # 処理済みマーク        db.execute(            "INSERT INTO processed_orders (order_id, processed_at) VALUES (%s, NOW())",            (order_id,)        )

6. API のべき等性 (Idempotency-Key)

Stripe ライクな Idempotency-Key 実装
Python
from flask import Flask, request, jsonifyimport hashlib, json
app = Flask(__name__)idempotency_cache = {}  # 本番は Redis 等
@app.post('/charge')def charge():    key = request.headers.get('Idempotency-Key')    if not key:        return jsonify({'error': 'Idempotency-Key required'}), 400
    # 同じキーで過去に処理した結果をキャッシュから返す    if key in idempotency_cache:        return jsonify(idempotency_cache[key]), 200
    # 実際の課金処理    body = request.json    result = process_charge(body['amount'], body['customer_id'])
    # 結果をキャッシュ (24h 等)    idempotency_cache[key] = result    return jsonify(result), 201
# クライアント側import requests, uuidkey = str(uuid.uuid4())  # クライアントが生成r = requests.post('/charge',    headers={'Idempotency-Key': key},    json={'amount': 1000, 'customer_id': 'c1'})# リトライ時も同じ key を送る → 2 回目以降はキャッシュ返答

7. メリットとデメリット

観点べき等化のメリットコスト/デメリット
信頼性✅ 二重実行事故が消える
運用✅ 「とりあえず再実行」で安全
実装コスト❌ unique key 設計、idempotency table 管理が増える
書込み速度❌ MERGE は INSERT より遅い (重複チェック分)
ストレージ❌ Idempotency-Key の保管 (24h 程度) が必要
デバッグ❌ 「実行されたか」の判定が unique key 経由に

8. べき等にできない処理の対策

  • メール送信: 送信前に sent_log テーブルでチェック (「user A に件名 X を当日送ったか」)
  • 外部 API 課金: Idempotency-Key 必須、API 側が対応していなければ自前で記録
  • プッシュ通知: 多少の重複は許容するか、message_id でクライアント側で deduplication
  • Webhook 受信: at-least-once 前提で受信側で重複検出 (event_id 記録)
  • ファイルアップロード: ファイルハッシュで重複チェック

9. べき等性の「落とし穴」

  • MERGE で updated_at = NOW() にすると、毎回更新時刻が変わる (べき等の定義から外れる)
  • AUTO_INCREMENT のみで重複検出 → ON CONFLICT が効かない、別の unique 制約必要
  • Idempotency-Key の TTL が短すぎる → リトライ時に既にキャッシュが消えていて二重実行
  • トランザクションを跨いだ複数処理 → 部分実行のリスク、SAGA パターンや outbox で対応
  • テストでべき等性を確認しない → 本番で初めて気づく

10. ふくふくの推奨

「全パイプライン処理にべき等性チェック」をルール化

ジョブごとに「リトライしたらどうなる?」を必ず PR レビューで確認。Airflow タスク・dbt incremental・API ハンドラの 3 つは、特にべき等性必須。`describe_idempotency.md` のような設計書テンプレを社内標準にすると守りやすい。

11. 関連記事

本 EP は読者リアクションに応じて、「分散トランザクション (SAGA / 2PC)」「Outbox パターンの実装」「Kafka exactly-once の実態」 などの続編を追加していきます。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp