べき等性 (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)
-- 非べき等: 重複実行で行が増える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;-- 標準 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 モデルでのべき等性
{{ 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 タスク) でべき等にする
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)
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. 関連記事
- dbt ハンドブック EP.05 マテリアライゼーション — incremental の詳細
- データ基盤トラブル事件簿 — べき等欠如事故の事例
- 壊れないデータ基盤の作り方 EP.11 災害復旧 — リトライ前提の設計
本 EP は読者リアクションに応じて、「分散トランザクション (SAGA / 2PC)」、「Outbox パターンの実装」、「Kafka exactly-once の実態」 などの続編を追加していきます。
この記事の感想を教えてください
あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。