プロンプトとスコープ
このサービスには大規模な orders テーブル、多数の子テーブル、パブリックURL、イベントペイロード、およびレプリカが存在します。PostgreSQL 18は、タイムスタンプ順のUUID向けにネイティブな uuidv7() ジェネレータを提供します。既存のUUIDv4の規約を維持し、テーブルの書き換えや長時間のブロッキングロックを回避しながら、新規レコードをUUIDv7へと移行してください。
これは、API、ストレージ、および非同期コンシューマにまたがるオンラインでの識別子とスキーマの移行が中心的なスキルとなるため、backend の問題です。
面接官が評価するポイント
第一に、識別子のフォーマットと時系列的な事実を区別できるかという点です。UUIDv7には時間順序が組み込まれていますが、厳密なグローバルコミット順序ではないため、明示的なビジネス用タイムスタンプの代わりにするべきではありません。
第二に、パブリックな識別子を維持できるかという点です。既存のURL、キャッシュ、冪等性キー、イベント、および外部キーは、移行中も引き続き解決可能でなければなりません。
第三に、明確な信頼できる唯一の情報源(Source of Truth)を持ったデュアルリードおよびデュアルライトのフェーズを設計できるかという点です。
第四に、書き込み増幅(Write Amplification)やレプリケーション遅延を引き起こすことなく、レプリカやリージョン全体に安全にバックフィルできるかという点です。
第五に、あらゆるワークロードでUUIDv7が自動的に高速化されると思い込むのではなく、局所性とレイテンシを検証できるかという点です。
最初に明確にすべき質問
- UUIDは外部に公開されているのか、主キーとして使用されているのか、あるいはその両方か?
- クライアントは新しい識別子を受け入れることができるか、それとも古いUUIDが正規(canonical)のままである必要があるか?
- そのキーを参照している子テーブル、インデックス、イベント、および検索ドキュメントはいくつあるか?
- 各ライターおよびレプリカには、どのPostgreSQLのバージョンと拡張機能が存在するか?
- 書き込みレート、レプリケーション遅延のSLO、およびロールバックの期限はどのくらいか?
- 順序付けはページネーション、監査ログの表示に必要なのか、それともインデックスの局所性のためだけに必要なのか?
30秒で答えるフレームワーク
「UUIDv4を安定したパブリック識別子として維持し、UUIDv7のサロゲートまたはマッピングカラムを追加して、バックフィルの前にデュアルライトを展開します。マッピングが完了するまで読み取りはどちらのキーも受け入れ、子参照とイベントには互換性を保ちます。遅延とロックを監視しながら、制限された主キー範囲でバックフィルを行い、整合性チェックの後に内部インデックスとクエリパスを切り替えます。UUIDv7は局所性と時間指向のスキャンに役立ちますが、タイムスタンプは明示的なまま保持します。ロールバックは、すべてのコンシューマで検証が完了するまで、フィーチャーフラグによる切り戻しで対応します。」
ステップごとの回答
ステップ 1: 互換性の形式を選択する
既存のUUIDの意味を暗黙のうちに変更してはなりません。order_id_v4 を外部向けの規約として維持し、order_id_v7 とユニーク制約を追加するか、永続的なマッピングを持つ別の内部キーを導入します。各フェーズで子テーブルとイベントがどのキーを使用するかを決定します。
ALTER TABLE orders ADD COLUMN order_id_v7 uuid;
CREATE UNIQUE INDEX CONCURRENTLY orders_order_id_v7_uq
ON orders(order_id_v7) WHERE order_id_v7 IS NOT NULL;uuidv7() は、それを提供しているPostgreSQLバージョンでのみ使用してください。それ以外の場合は、そのパスを有効にする前に同等のジェネレータを一貫してデプロイします。
ステップ 2: まず書き込みを展開する
新規の書き込みでは、1つのトランザクション内で両方の識別子を生成してマッピングを記録します。新しいカラムを設定できない既存の書き込みも有効なまま残します。コマンドの再試行によって2つのマッピングが作成されないよう、リトライは冪等にします。
ステップ 3: デュアルリードの解決を追加する
APIの検索はいずれの識別子も受け入れ、1つの正規の順序に解決し、古いレスポンスではレガシーIDを返します。新しいエンドポイントでは、バージョン管理された規約のもとでUUIDv7を公開しても構いません。両方の形式が乖離しないよう、キャッシュキーには解決された正規の識別子を含める必要があります。
ステップ 4: 制限されたバッチでバックフィルを行う
安定したカーソル、小さなトランザクションを使用し、レプリケーション遅延、ロック待機、または書き込みレイテンシが制限を超えた場合には一時停止しながら行をバックフィルします。十分なデータが存在するようになった後、サポートされている環境では同時実行操作(concurrent operations)を使用して新しいカラムにインデックスを作成します。親のマッピングが永続化されるまで、子テーブルを更新してはなりません。
ステップ 5: 参照とイベントを移行する
重複期間中は、子テーブルの外部キーとイベントスキーマの互換性を維持します。v7フィールドをオプショナルとして追加し、両方の値を発行して、v7を必須にする前にコンシューマを更新します。制約を切り替える前に、欠落しているマッピングや重複しているマッピングを照合して解決します。
ステップ 6: 内部のアクセスパスを切り替える
整合性チェックの完了後、局所性や時間指向のスキャンが重要な箇所で、内部の結合(JOIN)やページネーションをUUIDv7にルーティングします。ビジネス上の順序付けには明示的な created_at を維持し、タイブレーカーを使用します。UUIDv7の順序付けはアプリケーションレベルでは近似的なものです。
ステップ 7: 検証とオブザーバビリティ
インデックスサイズ、ページスプリット、キャッシュの挙動、インサートのレイテンシ、範囲スキャンのレイテンシ、レプリケーション遅延、およびエラー率を、適合するワークロードと比較します。v4とv7の検索が同一の行を返し、イベントコンシューマの冪等性が維持されていることを確認します。
ステップ 8: ロールバック期間が経過した後にのみ廃止する
すべてのクライアント、リプレイヤーツール、エクスポート、およびレプリカが移行期間を通過するまで、マッピング、古いインデックス、およびデュアルリードのパスを保持します。明示的なロールバック計画を用意した上で、個別のデプロイにより古い制約を削除します。
模範解答
「パブリックなUUIDの規約をインプレースで書き換えることはしません。UUIDv7カラムとユニークマッピングを追加し、デュアルライトをデプロイしてから、どちらの形式も同じ順序に解決するデュアルリードをデプロイします。ロック、レプリケーション遅延、書き込みレイテンシを監視しながら、制限されたトランザクションでバックフィルを行います。すべての子参照とイベントは、全コンシューマがv7を理解できるようになるまで両方のIDを保持します。整合性と局所性の測定後、ビジネス上の順序付けフィールドとして created_at を維持しつつ、内部の結合とページネーションを切り替えます。古いパスが廃止されるまでは、フィーチャーフラグにより読み取りと書き込みを切り戻すことができます。」
よくある間違い
- パブリックUUIDを即座に置き換えてしまう → リンクやイベントが壊れる → 正規の規約とマッピングを保持する。
- UUIDv7が厳密な時間順序であると想定してしまう → ページネーションでレコードがスキップされたり順序が入れ替わったりする → 明示的なタイムスタンプとタイブレーカーを使用する。
- 1つの巨大なトランザクションでバックフィルしてしまう → ロックとレプリケーション遅延が急増する → 制限されたバッチを使用する。
- マッピングが存在する前に外部キーを追加してしまう → 重複期間中に書き込みが失敗する → 親、子、制約の順に移行する。
- サポートされていない混在バージョンでIDを生成してしまう → セマンティクスが乖離する → バージョンを固定するか、単一の一貫したジェネレータを使用する。
- リトライを無視してしまう → 重複マッピングが発生する → デュアルライトを冪等にする。
- v4をあまりにも早く削除してしまう → 古いエクスポートやリプレイが失敗する → ロールバック期間が経過するまで待つ。
フォローアップ質問
フォローアップ 1: UUIDv7は created_at の代替になりますか?
いいえ。局所性を改善し、タイムスタンプ指向のビットを提供することはできますが、ビジネス上の順序付けには明示的なタイムスタンプと決定論的なタイブレーカーが必要です。
フォローアップ 2: v4とv7で1つのUUIDカラムを共有できますか?
はい、UUID型は両方のフォーマットを保持できますが、移行のメタデータ、外部向けの規約、および順序付けのセマンティクスについては依然として明示的な計画が必要です。
フォローアップ 3: なぜ書き込みを切り替える前にデュアルリードを行うのですか?
バックフィルやコンシューマの展開が完了していない間でも、古いレコードと新しいレコードを一貫して解決できるようにするためです。
フォローアップ 4: バックフィルのスロットリングはどのように行いますか?
制限されたトランザクションを使用し、レプリケーション遅延、ロック待機、CPU、または書き込みレイテンシのしきい値に達したときに一時停止します。その後、永続化されたカーソルから再開します。
フォローアップ 5: イベントには何を含める必要がありますか?
重複期間中は、両方のIDまたは安定したマッピング参照を発行し、スキーマをバージョン管理して、v7を必須にする前にコンシューマを更新します。
フォローアップ 6: 古いカラムはいつ削除できますか?
クライアント、エクスポート、リプレイ、レプリカ、およびロールバックチェックが合意された期間を通過した後にのみ削除します。元に戻すことが可能な個別のデプロイにより、制約とインデックスを削除します。