これは私が二つの異なるエンジンで二度見た失敗です。エクスポートは 10:00 に実行され、見た中で最も高い最終更新タイムスタンプを記録します。09:58 に始まったトランザクションはまだ開いており、10:03 にコミットし、開始時のタイムスタンプを保持しています。次のエクスポートは 10:00 より新しい行だけを要求するため、その行を決して見ません。転送量は申し分なく、データは間違っていました。

一行を失ったエクスポートをたどる

SQL ではなく出来事の順序をたどります。アプリケーションは行の変更時刻をトランザクションの開始時に書き込み、コミット時には書き込みません。エクスポーターはトランザクションがまだ開いている間に壁時計のウォーターマークを読みます。その後トランザクションは、保存されたウォーターマークより古い変更時刻でコミットします。コンシューマーの次のバッチはウォーターマークの後に始まるので、その行はカーソルの後ろに残り、二度と選択されません。

何もエラーを記録しません。何も失敗していないからです。これがこの種の不具合を高くつくものにします。デルタフィードは速く、行数はもっともらしく見え、欠けた行は数週間後に誰も再現できない照合チケットとして浮上します。

シーケンス番号にも別の理由で同じ形の欠陥があります。単調 ID は値が割り当てられた順序を教えるのであって、トランザクションがコミットした順序ではありません。低い番号を取ったトランザクションが高い番号を取ったものより後にコミットすることがあるため、割り当て順に基づくカーソルも穴を残し得ます。

フィードが果たすべき五つの約束

転送方式を選ぶ前に、ソースシステムへ五つの条項からなる契約を求めます。どれかが欠けていれば、それは変更フィードではなく経験則です。

  • カバレッジ:挿入、更新、削除。それぞれ適用に必要な安定したキーを伴います。
  • 境界:カーソル値が何を意味し、一貫したバッチが何を含むか。
  • 一貫性:互換性のないソース状態を混ぜずに関連テーブルをどう読むか。
  • 保持:切断されたコンシューマーが保存位置から再開できる期間。
  • 再初期化:その位置が無効になったときの、サポートされた完全スナップショット。

サポートされた仕組みなら五つすべてを満たせます。SQL Server の変更追跡のドキュメントはその具体的な公開例です。最小の有効バージョンを定義し、変更とともに削除も記録し、検証・バージョン取得・関連する変更クエリをまたぐスナップショットトランザクションを説明しています。これはソースに求めるべきものの例であり、別のエンジンに貼り付けられる構文ではありません。

チェックポイントをパイプラインの一部として設計する

コンシューマーの保存位置こそが本当の状態機械なので、そう書きます。カーソルは、バッチをすでに適用し対象トランザクションをコミットしたワークフローを通じてのみ前進できます。

cursor  = durable_checkpoint()          # last fully applied position
batch   = extract(cursor, upper_bound)  # one consistent batch
apply(batch)                            # idempotent at the target
commit()
checkpoint(batch.upper_bound)           # only now

配送が途中で失敗したら、新しいソース位置をでっち上げるのではなく同じバッチを照合します。より新しい上限で再抽出することが、部分適用を恒久的な穴に変える道です。鏡の規則は反対側にも当てはまります。対象側の冪等な適用は重複を防ぎますが、抽出が見なかった変更には何もしません。

削除には記録が必要で、推測では足りない

まだ存在する行を比べても、どのキーが消えたかをコンシューマーに伝えられません。削除を記録しないソースでは、二つの不完全な選択肢が残ります。比較用に十分長くキー履歴を保つか、スケジュールで明示的な完全照合を走らせるかです。どちらもコストのかかる設計判断で、誰かが担わなければ存在しません。

保持も同じ種類の判断です。サポートされた変更フィードにはすべて窓があり、契約に明記し、コンシューマーのチェックポイントが近づいたら警告します。二週間の停止後に目覚めたコンシューマーは、黙って部分バッチを受け取るのではなく、「再初期化が必要」という明確なエラーを読むべきです。

そしてデルタが割に合ったかを測る

すべてのキーとハッシュを比較する方式に戻るのが正しいこともあり、そのコストを知る価値はあります。どの行についても情報を転送し続け、ハッシュはテーブルをまたぐ一貫したスナップショットを与えず、ハッシュ方式は表現と衝突について独自の問いを持ち込みます。検証可能な境界を伴うより少ないバイトが目標です。コミット済みの変更を黙って落とす少ないバイトは、デルタですらないのです。

最終更新の列は手がかりにすぎません。遅いコミット、削除、長い停止を生き延びるカーソルこそ契約です。

関連記事