GH GambleHub

ページネーションとカーソル

1)ペジネーションが必要な理由

ページネーションは、クライアントが送信およびレンダリングするデータの量を制限し、ストレージ/ネットワークへの負荷を軽減し、コレクションを「歩く」決定的な方法を設定します。実際のシステムでは、ページネーションは'page=1&limit=50'だけでなく、一連のプロトコル契約と整合性の不変量である。

典型的な目的:
  • リクエストごとのレイテンシとメモリ制御。
  • データセットを変更するときの安定したナビゲーション(挿入/削除)。
  • 場所から再開する能力(再開)。
  • キャッシュとプリロード(プリフェッチ)。
  • 乱用に対する保護(レート制限、背圧)。

2)ペジネーションモデル

2.1オフセット/リミット(ページング)

アイデア: 「N行をスキップし、Mを返します。」

長所:シンプルさ、ほぼすべてのSQL/NoSQLと互換性があります。

短所:
  • 線形劣化:大きなOFFSETにより、フルスキャン/スキップコストが発生します。
  • リクエスト間の挿入/削除中の不安定さ(オフセット「float」)。
  • 正確な「再生可能性」を確保することは困難です。
SQLの例:
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;

2.2カーソル/キーセット/シークページネーション

アイデア:"Kキーで乗ります。"カーソルはソートされたセット内の位置です。

長所:
  • O (1)インデックスを続行するアクセス。
  • コレクション変更時の安定性。
  • ディープな「ページ」で最高のレイテンシ。
短所:
  • 厳密に定義されたユニークで単調なソートキーが必要です。
  • 実装とデバッグがより困難です。
SQLの例(seek):
sql
-- Resumption after steam (created_at, id) = (:last_ts,:last_id)
SELECT
FROM orders
WHERE (created_at, id) < (:last_ts,:last_id)
ORDER BY created_at DESC, id DESC
LIMIT 50;

2.3継続トークン

アイデア:サーバーは、「position」がエンコードされている不透明なトークンを返します(おそらくシャード/フィルタの状態)。クライアントは内部を理解せず、次のページのトークンを返すだけです。
長所:柔軟性、APIを壊すことなくスキームを変更する機能。
短所:トークン寿命管理、預金との互換性。

2.4時間とロジックカーソル

タイムベース:「すべてのレコードをTまで」、カーソル-タイムスタンプ(追加のみのスレッドに適しています)。
Log-sequence/offset-based: cursor-ログ内のオフセット(Kafka offset、 journal seq)。
グローバルな単調ID:安定したseekのためのsortableキーとしてSnowflake/UUIDv7して下さい。

3)コースとトークンの設計

3.1良いカーソルのプロパティ

不透明-クライアントはフォーマットに依存しません。
著者/完全性:なりすまし/操作を防ぐHMACの署名。
コンテキスト:ソート、フィルタ、スキーマバージョン、テナント/シャードが含まれます。
Lifetime:インデックス/アクセス権を変更する際のTTLと「非リプレイ」。
サイズ:URLに適したコンパクト(<=1-2 KB)。

3.2トークン形式

推奨スタック:JSON→圧縮(zstd/deflate)→Base64URL→HMAC。

ペイロード構造(例):
json
{
"v": 3 ,//token version
"sort": ["created_at:desc","id:desc"],
"pos": {"created_at":"2025-10-30T12:34:56. 789Z","id":"987654321"},
"filters": {"user_id":"42","status":["paid","shipped"]},
"tenant": "eu-west-1",
"shards": [{"s ": "a, "" hi":"..."}] ,//optional: cross-shard context
"issued_at": "2025-10-31T14:00:00Z",
"ttl_sec": 3600
}

'mac=HMAC (secret、 payload)'が上部に追加され、すべてが1つの文字列トークンにエンコードされます。

3.3安全性について

標識(HMAC/SHA-256)。
必要に応じて、機密値(PII)の存在下で暗号化(AES-GCM)。
サーバー検証:バージョン、TTL、ユーザー権限(RBAC/ABAC)。

4)一貫性および不変性

4.1安定した選別

完全な決定:'ORDER BY ts DESC、 id DESC'を使用します。
ソートキーは一意でなければなりません(tiebreakerとして'id'を追加してください)。
インデックスはカバーインデックスと一致している必要があります。

4.2スナップショットと分離

ジャンボページ以外の場合は、読み取り整合スナップショット(MVCC/txid)を使用します。
スナップショットが非現実的(高価/大量のデータ)の場合、コントラクトを定式化します。"カーソルは位置の前に要素を厳密に返します。"これはニュースフィードにとって自然なことです。

4.3ページ間の挿入/削除

Seek-modelは「重複/省略」を最小限に抑えます。
ドキュメントの削除/変更の動作:ページ間のまれな「穴」は許可されていますが「、時間内に戻る」ことはできません。

5)インデックス作成とIDスキーム

複合インデックスは厳密にソート順です:'(created_at DESC、 id DESC)'。
モノトーンID: Snowflake/UUIDv7は時間の順序を与える→seekをスピードアップします。
ホットキー:shard-key ('tenant_id'、 'region'など)で配布し、shard内でソートします。
IDジェネレータ:衝突を避け「、クロックスキュー」-NTPジャンプ中の時間同期、「回帰」。

6)クロスシャードページネーション

6.1スキーム

Scatter-Gather:すべてのシャードへの並列リクエスト、ローカルシークコース、アグリゲータへのk-wayマージ。
シャードカーソルごと:トークンには、各シャードの位置が含まれます。
バウンディングファンアウト-ステップごとのシャード数を制限します(レート制限/タイムアウトの予算)。

6.2マルチシャード用トークン

配列'{shard_id、 last_pos}'を格納します。次のステップでは、アクティブなシャードごとに再開して再度保持し、グローバルにソートされたページを与えます。

7)プロトコル契約

7.1 REST

リクエスト:

GET /v1/orders? limit=50&cursor=eyJ2IjoiMyIsInNvcnQiOiJjcmVh... (opaque)
答え:
json
{
"items": [ /... / ],
"page": {
"limit": 50,
"next_cursor": "eyJ2IjozLCJwb3MiOiJjcmVh...==",
"has_more": true
}
}
推奨事項:
  • 'limit'は上限(例えばmax=200)である。
  • 'has_more=false'の場合、'next_cursor'が見つかりません。
  • GET idempotence、 'next_cursor'なしの応答のキャッシュ可能性(固定フィルタとスナップショット付きの最初のページ)。

7.2 GraphQL(リレーアプローチ)

典型的な「接続」契約:
graphql type Query {
orders(first: Int, after: String, filter: OrderFilter): OrderConnection!
}

type OrderConnection {
edges: [OrderEdge!]!
pageInfo: PageInfo!
}

type OrderEdge {
node: Order!
cursor: String! // opaque
}

type PageInfo {
hasNextPage: Boolean!
endCursor: String
}

'cursor'は不透明で署名されています。HMACなしで「raw Base64 (id)」を使用しないでください。

7.3 gRPC

'page_size'と'page_token'を使う:
proto message ListOrdersRequest {
string filter = 1;
int32 page_size = 2;
string page_token = 3; // opaque
}

message ListOrdersResponse {
repeated Order items = 1;
string next_page_token = 2; // opaque bool has_more = 3;
}

7.4スレッドとWebSocket

連続テープの場合:カーソルを"last seeed offset/ts'とします。

再接続中に'resume_from'をサポート:
json
{ "action":"subscribe", "topic":"orders", "resume_from":"2025-10-31T12:00:00Z#987654321" }

8)キャッシング、プリロード、CDN

安定したフィルタを持つ最初のページのETag/If-None-Match。
パブリックリストのための短いTTL(例えば、5-30秒)を持つキャッシュコントロール。
Prefetch: 'next_cursor'とヒント('Link: rel=「next」')を返すと、クライアントは次のページをプリロードできます。
バリエーション:'filter/sort/locale/tenant'をキャッシュ部分のキーとして考えます。

9)負荷管理および制限

上限'limit'、例: 200.

サーバー側のバックプレッシャー:リクエスト時間が>予算の場合、レスポンスの'limit'を減らします(そしてクライアントに実際のページサイズを明示的に伝えます)。
ユーザー/トークン/テナントごとのレート制限。
Timeout/Retry:指数休止、idempotent requests。

10) UXの側面

番号付けに対するスクロール: 無限スクロール→カーソル;number pages→offset(データ更新時の不正確さを説明する)

placeボタンに戻る:クライアントカーソルスタックを保存します。
空白のページ:'has_more=false'の場合、Moreボタンは表示されません。
安定した境界:安価な場合にのみ正確な'total'を表示します(そうでなければ、おおよその'approach_total'です)。

11)テストおよび端の場合

チェックリスト:
  • 安定したソート:同じ't'の項目は「点滅」しません。
  • 「挿入/削除」(Inserts/Deletes)-重複はページの交差点に表示されません。
  • ページ間の変更フィルタ:トークンは廃止/非互換性として拒否される必要があります。
  • Token TTL:有効期限切れ後のエラー。
  • 偉大な深さ:レイテンシーは直線的に成長しません。
  • Multishard:正しいマージ順序、飢餓「スロー」シャードがない。
プロパティベースのテスト例(擬似コード):
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch

12)観察可能性およびSLO

メトリクス:
  • 'list_request_latency_ms' (P50/P95/P99)をページ長で指定します。
  • 'search_index_hit_ratio'(カバーインデックスが残したリクエストの割合)
  • 'next_cursor_invalid_rate' (検証/TTL/署名エラー)。
  • 'merge_fanout' (1ページあたりのシャード数)
  • 'duplicates_on_boundary'および'gaps_on_boundary'(クライアントテレメトリでの検出)。
ログ/トレース:
  • ログ内の'cursor_id'とマスクペイロードを関連付けます。
  • タグの範囲:'page_size'、 'source_shards'、 'db_index_used'。
SLOの例:
  • 空室状況:99。9% on 'List'メソッド。
  • 遅延:ローカル充電で'page_size<=50'に対して200ミリ秒をP95します。
  • トークンエラー:<0。通話総数の1%

13)移行と相互運用性

トークンで'v'を有効にし、N週間の古いバージョンをサポートします。
ソートキーを変更する場合-"soft'エラー'409 Conflict'をカーソルなしで新しいリストを実行するプロンプトで送信します。
壊滅的なケース(すべてのトークンの轟音):'signing_key_id'を変更し、古いものを拒否します。

14)実装例

14.1トークン生成(擬似コード)

python payload = json. dumps({...}). encode()
compressed = zlib. compress(payload)
mac = hmac_sha256(signing_key, compressed)
token = base64url_encode(mac + compressed)

14.2トークン検証

python raw = base64url_decode(token)
mac, compressed = raw[:32], raw[32:]
assert mac == hmac_sha256(signing_key, compressed)
payload = json. loads(zlib. decompress(compressed))
assert now() - payload["issued_at"] < payload["ttl_sec"]
assert payload["filters"] == req. filters

14.3複合キーでクエリを探す

sql
-- Page # 1
SELECT FROM feed
WHERE tenant_id =:t
ORDER BY ts DESC, id DESC
LIMIT:limit;

-- Next pages, continued after (ts0, id0)
SELECT FROM feed
WHERE tenant_id =:t
AND (ts <:ts0 OR (ts =:ts0 AND id <:id0))
ORDER BY ts DESC, id DESC
LIMIT:limit;

15)安全性とコンプライアンス

PIIを派生できるトークンに生のフィールドを含めないでください。
TTLに署名して制限します。
トークンをユーザー間で耐えられないようにしてください(ペイロードに「sub/tenant/roles」と書いて、検証中にチェックしてください)。
トークンハッシュだけを記録します。

16)頻繁なエラーとアンチパターン

カーソルとしてのBase64 (id):偽装/ピックアップが簡単で、ソートを変更するときに契約を破棄します。
タイブレーカーなし:'id'→重複/ジャンプなしの'ORDER BY ts DESC'。
トークンを無効にせずにページ間のフィルタを変更します。
深いオフセット:遅く、予測不可能。
バージョンとTTLのないトークン。

17)ミニチェックリストの実装

1.ソートを定義し、ユニークなタイブレーカーを追加します。
2.この注文のスパニングインデックスを作成します。
3.モデルを選択:seek+opaqueトークン。
4.トークン署名(および必要に応じて暗号化)を実装します。
5.TTLとバージョン管理を置きます。
6.'has_more'、 'next_cursor'のコントラクトを整形して文書化します。
7.クロスシャードスキーム(必要に応じて)とk-wayマージを考慮してください。
8.メトリクス、アラート、SLOを追加します。
9.プロパティベースのページ境界をテストでカバーします。
10.トークンの移行戦略について説明します。

18)アプローチを選択するための簡単な推奨事項

「ページ番号」とおおよその合計が重要なディレクトリ/検索:'OFFSET/LIMIT'+cacheとしましょう。合計がおおよそのものであることを報告します。
フィード、アナリティクス、ディープリスト、高いRPS:カーソル/シークのみ。
シャーディ/分散コレクション:シャードカーソルごと+マージトークン。
スレッド/CDC:履歴書とオフセット/tsとしてカーソル。

19) API契約例(要約)

'GET/v1/items?limit=50&cursor=……'

答えには常に'ページが含まれます。limit'、'ページ。has_more'、オプションの'ページ。 。 。
カーソルは不透明で、TTLで署名されています。
ソートは決定論的な'ORDER BY created_at DESC、 id DESC'です。
変更の振る舞いを設定する-アイテムはカーソルから「戻る」ことはありません。
メトリックとエラーは標準化されています:'invalid_cursor'、 'expired_cursor'、 'mismatch_filters'。

この記事では、ビッグデータ、シャーディ、および積極的に変更されたレコードセットでさえも、高速で予測可能で安全なページネーションを設計するためのアーキテクチャの原則と既製のパターンを提供します。

Contact

お問い合わせ

ご質問やサポートが必要な場合はお気軽にご連絡ください。いつでもお手伝いします!

統合を開始

Email は 必須。Telegram または WhatsApp は 任意

お名前 任意
Email 任意
件名 任意
メッセージ 任意
Telegram 任意
@
Telegram を入力いただいた場合、Email に加えてそちらにもご連絡します。
WhatsApp 任意
形式:+国番号と電話番号(例:+81XXXXXXXXX)。

ボタンを押すことで、データ処理に同意したものとみなされます。