プロンプトとコンテキスト
この質問は、バックエンドエンジニアがページネーションを安定した読み取りプロトコルとして扱っているかをテストします。リクエストの合間に注文、コメント、ログが挿入、削除、または更新される可能性があります。単純な OFFSET は変動する位置に依存し、深いページでは多数の行を無駄にスキャンします。順序付け、カーソルエンコーディング、フィルターのバインド、一貫性境界、インデックス、および前方/後方セマンティクスについて網羅してください。
面接官がテストするポイント
優れた回答では、プロダクトがページジャンプ、件数カウント、リアルタイムな結果のどれを必要としているかを明確にした上で、キーセット/カーソルまたはオフセットを選択します。カーソルは順序付けとフィルターをバインドし、そのソートキーは一意で安定しており、インデックスが作成されています。サーバーはカーソルに署名し、有効期限を設定します。回答では、新しい行が次のページで重複しない理由、削除が結果にどのように影響するか、そして next_cursor とリスト末尾の状態がどのように返されるかを説明します。
確認すべき質問
- ソート順序はどうなっていますか?ソートフィールドは変更される可能性がありますか、また一意なタイブレーカー(順位決定要素)は何ですか?
- 前のページへのナビゲーション、任意のページジャンプ、正確な件数カウントが必要ですか、それとも前方向の無限スクロールのみですか?
- 読み取りは固定されたスナップショットを使用すべきですか、それとも結果整合性のあるライブリストを許容すべきですか?
- フィルター、テナントスコープ、権限、およびソート順序をカーソルにバインドする必要がありますか?有効期間はどれくらいですか?
- 削除、論理削除、権限変更、およびクロスシャード読み取りはどのように処理されますか?
30秒の回答フレームワーク
「降順のキーセットクエリには (created_at, id) のような安定した複合キーを使用します。カーソルは、最後のソート値、フィルターハッシュ、方向、バージョンを含む署名付きの不透明なトークンです。サーバーはこれを検証し、一致する複合インデックスに対して WHERE (created_at,id) < (:time,:id) を実行します。新しい行は、既に読み取られたウィンドウに挿入されるのではなく、再読み込み(リフレッシュ)時に表示されます。削除によってページが短くなる可能性はありますが、重複は発生しません。絶対的な一貫性が必要な場合は、スナップショット境界を追加し、そのコストについて説明します。」
ステップバイステップの詳細な回答
ステップ 1: リストのセマンティクスを定義する
これが監査履歴、ライブフィード、管理テーブルのいずれであるかを決定します。履歴には通常、安定した境界が必要です。ライブフィードでは、更新後にのみ新しい行が表示される場合があります。リアルタイム更新、任意のジャンプ、正確な件数カウント、低コストを同時に約束してはいけません。
ステップ 2: 安定したソートキーを選択する
タイムスタンプは重複したり編集されたりする可能性があるため、タイブレーカーとして一意の id を追加します。表示用フィールドは避けてください。更新によってレコードが移動する可能性がある場合は、イミュータブルな作成シーケンスを使用するか、行がページ間を移動する可能性があることを明確に文書化します。
ステップ 3: 不透明なカーソルを設計する
ソート値、方向、フィルターハッシュ、API バージョン、および有効期限を含めます。署名するかサーバー側で保存します。Base64 はエンコーディングであり、セキュリティではありません。無関係なページを暗黙的に返すのではなく、フィルターが変更された場合はカーソルを拒否します。
ステップ 4: キーセットクエリを作成する
降順の (created_at,id) の場合、次のページでは同じ複合インデックスを使用して created_at < t OR (created_at = t AND id < id0) を使用します。クエリをパラメータ化し、limit を制限し、カーソルの値を SQL に直接連結しないでください。
ステップ 5: 書き込みと削除を処理する
ページ 1 の後に挿入された行はページ 2 に表示されるべきではありません。これらは再読み込み後に表示されます。削除された行によってページが短くなる場合がありますが、これは許容される宣言済みのセマンティクスです。欠落が許容されない場合は、スナップショット境界またはバージョニングされた読み取りを使用します。
ステップ 6: 権限とフィルターをバインドする
カーソルのフィルターハッシュ、テナント、および認可スコープはリクエストと一致する必要があります。権限が厳格化された場合は結果を再計算します。古いカーソルがアクセス制御をバイパスしてはなりません。クロスシャードコーディネーターはローカルカーソルをマージできますが、回答では増幅コストを明記する必要があります。
ステップ 7: レスポンスとエラーを定義する
items、next_cursor、has_more、およびオプションでスナップショット id を返します。期限切れまたは無効なカーソル、および変更されたフィルターには安定したビジネスエラーを使用します。クライアントはカーソルをクリアし、ページ 1 から再開します。SQL や内部データベースのエラーを公開してはいけません。
ステップ 8: 並行性テストで検証する
リクエスト間での挿入、削除、同一タイムスタンプ、フィルター変更、改ざんされたカーソル、および深いページをテストします。1 つのセッションで重複がないこと、インデックスシークが行われていること、レイテンシがページ数に比例して線形に増加しないことを検証します。重複率、スキップ率、p95 レイテンシ、カーソルエラーを追跡します。
クエリの擬似コード
SELECT id, created_at, total
FROM orders
WHERE tenant_id = :tenant
AND (created_at, id) < (:cursor_time, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT :page_size;トレードオフと境界
| 要件 | 選択 | コスト |
|---|---|---|
| 大規模な無限スクロール | キーセットカーソル | 任意のページジャンプ不可 |
| 小規模な管理テーブル | オフセット | 深いページが遅く、書き込みに対して不安定 |
| 絶対的な一貫性 | スナップショット境界 | スナップショットのストレージとクリーンアップ |
| 正確なカウント | 個別または非同期カウント | 追加の処理とデータが古くなる可能性 |
カーソルページネーションは位置の安定性とクエリ効率を解決しますが、ページをまたぐビジネス上の重複排除、権限変更、または行を移動させる更新を自動的に解決するわけではありません。検索結果にはクエリバージョンも必要であり、集計には特定時点の読み取り(point-in-time read)が必要になる場合があります。
ロールアウト計画と根拠
読み取り頻度の高いリストを選択し、現在の重複、スキップ、深いページのレイテンシ、カウントのコストを測定します。カバリングインデックスを追加し、バージョニングされたカーソルをリリースし、並行書き込みのテストとメトリクスを追加します。Django REST framework はカーソルページネーションを不透明なカーソルとして文書化しています。Hello Interview や TechInterview の資料でも、変化するデータセットにおけるカーソル/キーセットの安定性が強調されています。
パイロット終了基準
並行挿入/削除テストが宣言された重複および欠落のセマンティクスを満たしていること、深いページの p95 が安定していること、改ざんやフィルターの変更が拒否されること、クライアントが安全にページ 1 に回復できること、および認可レビューでトークンがデータを漏洩しないことが確認されていること。
成果が本物であることを証明する方法
同一のデータサイズと書き込みレートで、オフセットとカーソルの p95/p99 レイテンシ、スキャンされた行数、重複率、欠落率、データベース CPU を比較します。単一のコールドクエリが結果を左右しないよう、キャッシュの影響を分離します。
よくある間違いとフォローアップ
Base64 を安全なカーソルとして扱う
Base64 はエンコーディングです。クライアントは id やテナントを改ざんできます。トークンに署名するかサーバーに保存し、フィルター、バージョン、有効期限をバインドしてください。
タイムスタンプのみでソートする
多くの行が同一のミリ秒を共有する可能性があり、境界が曖昧になります。一意なタイブレーカーと一致する複合インデックスを追加してください。
カーソルで任意のページにジャンプできますか?
標準的なカーソルは任意のジャンプ向けには設計されていません。制限付きオフセット、事前計算されたアンカー、または一貫性とコストを明示した検索エンジンページネーションを提供してください。
更新によって行が重複することはありますか?
ソートフィールドが変更されると、行がページ間を移動する可能性があります。イミュータブルな作成シーケンスを使用するか、スナップショット/バージョンセマンティクスを使用して、その挙動を文書化してください。
「前のページ」ボタンはどのように実装しますか?
以前のカーソルのスタックを保持するか、逆方向のクエリを実行してサーバー側で結果を反転させます。クライアントがカーソルの内部構造を推測すべきではありません。
正確な総件数を返す必要がありますか?
いいえ。無限スクロールでは通常 has_more のみが必要です。各ページでテーブル全体をスキャンしないように、正確なカウントは非同期または別処理にすることができます。