Пагинация және курсорлар
1) Пагинация не үшін қажет
Пагинация клиент беретін және ренделетін деректер көлемін шектейді, сақтау қоймаларына/желілеріне жүктемені азайтады және коллекция бойынша «серуендеудің» детерминирленген тәсілін орнатады. Нақты жүйелерде пагинация - бұл 'page = 1 & limit = 50' ғана емес, хаттамалық келісімшарттар мен келісімділік инварианттарының жиынтығы.
Үлгілік мақсаттар:- Сұраудың латенттілігін және жадын бақылау.
- Деректер жиынын өзгерту кезіндегі тұрақты навигация (кірістіру/жою).
- Орнынан қайта бастау мүмкіндігі (resumption).
- Кешіктіру және алдын ала жүктеу (prefetch).
- Теріс пайдаланудан қорғау (rate limiting, backpressure).
2) Пагинация модельдері
2. 1 OFFSET/LIMIT (беттік)
Идея: «N жолды өткізіп ал, M жолды қайтарып ал».
Артықшылықтары: қарапайымдылық, кез келген SQL/NoSQL-мен сыйысымды.
- Сызықтық деградация: үлкен OFFSET толық сканерлеуге/skip-cost әкеледі.
- Сұраулар арасындағы кірістіру/жою кезіндегі тұрақсыздық (ығысу «жүзеді»).
- Нақты «жаңартуды» қамтамасыз ету қиын.
sql
SELECT
FROM orders
ORDER BY created_at DESC, id DESC
OFFSET 1000 LIMIT 50;
2. 2 Cursor/Keyset/Seek-пагинация
Идея: «K кілтінен жалғастыру». Меңзер - сұрыпталған жиындағы орын.
Артықшылықтары:- O (1) индекс болған кезде жалғастыруға қол жеткізу.
- Коллекция өзгерген кездегі тұрақтылық.
- Терең «беттердегі» ең жақсы жасырындылық.
- Қатаң белгіленген, бірегей және монотонды сұрыптау кілттері қажет.
- Іске асыру және жөндеу қиынырақ.
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 Continuation tokens (мөлдір емес токендер)
Идея: сервер «позиция» (және мүмкін шард/сүзгі күйі) кодталған opaque-токенді қайтарады. Клиент ішін түсінбейді және келесі бет үшін токенді қайтарады.
Артықшылықтары: икемділігі, API-ны бұзбай схеманы өзгерту мүмкіндігі.
Кемшіліктері: токендердің өмір сүру мерзімін басқару, деплоялар кезіндегі үйлесімділік.
2. 4 Уақытша және логикалық меңзерлер
Time-based: «T дейінгі барлық жазбалар», курсор - уақыт белгісі (append-only ағындарында сәйкес келеді).
Log-sequence/offset-based: меңзер - логдағы ығысу (Kafka offset, journal seq).
Global monotonic IDs: тұрақты seek үшін сұрыпталатын кілттер ретінде Snowflake/UUIDv7.
3) Курстар мен токендерді жобалау
3. 1 Жақсы меңзердің қасиеттері
Мөлдірлік (opaque): клиент пішімге тәуелді емес.
Авторлық/тұтастық: ауыстыруды/манипуляцияны болдырмау үшін HMAC қолтаңбасы.
Контексті: сұрыптауды, сүзгілерді, схеманың нұсқасын, tenant/shard қамтиды.
Өмір сүру мерзімі: TTL және индекстер/қол жеткізу құқықтары ауысқан кездегі «жатпаушылық» (non-replay).
Өлшемі: ықшам (<= 1-2 KB), URL үшін жарамды.
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)' қосылады және барлығы бір жолды токенге кодталады.
3. 3 Қауіпсіздік
Қол қою (HMAC/SHA-256).
Сезімтал мәндер (PII) болғанда (AES-GCM) қосымша шифрлау.
Серверде валидация: нұсқа, TTL, пайдаланушы өкілеттіктері (RBAC/ABAC).
4) Келісімділік және инварианттар
4. 1 Тұрақты сұрыптау
Толық детерминизмді пайдаланыңыз: 'ORDER BY ts DESC, id DESC'.
Сұрыптау кілті бірегей болуы керек ('id' дегенді tiebreaker деп қосыңыз).
Индекс сұрыптауға (covering index) сәйкес келуі тиіс.
4. 2 Түсірілімдер (snapshot) және оқшаулау
«Айдалмайтын» беттер үшін read-consistent snapshot (MVCC/txid) пайдаланыңыз.
Егер снапшот орынсыз болса (қымбат/көп деректер), келісімшартты тұжырымдаңыз: «курсор позициядан бұрын элементтерді қайтарады». Бұл жаңалықтар үшін табиғи нәрсе.
4. 3 Беттер арасында кірістіру/жою
Seek моделі «көшірмелерді/рұқсаттарды» барынша азайтады.
Жою/өзгерту кезіндегі мінез-құлықты құжаттаңыз: беттер арасында сирек «тесіктерге» жол беріледі, бірақ «уақыт бойынша артқа» болмайды.
5) Индекстеу және сәйкестендіргіштердің схемалары
Композициялық индекстер қатаң сұрыптау тәртібімен: '(created_at DESC, id DESC)'.
Монотонды ID: Snowflake/UUIDv7 уақыт тәртібін береді → seek жылдамдатады.
Ыстық кілттер: shard-key бойынша таратыңыз (мысалы, 'tenant _ id', 'region') және ішін сұрыптаңыз.
ID генераторлары: «болашақтағы сағаттар» (clock skew) - уақытты үндестіру, NTP секірулері кезінде «регрессия».
6) Кросс-шард пагинациясы
6. 1 Сұлбалар
Scatter-Gather: барлық шардарға параллель сұраулар, жергілікті seek-курстар, содан кейін агрегаторда k-way merge.
Per-Shard Cursors: токенінде әрбір шард бойынша позициялар бар.
Bounded fan-out: бір қадамда шард санын шектеңіз (rate limiting/timeout budget).
6. 2 Multi-shard үшін токендер
{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).
- 'next _ cursor' жоқ, егер 'has _ more = false'.
- GET теңсіздігі, 'next _ cursor' жоқ жауаптардың кэшталуы (бекітілген сүзгілер мен snapshot кезінде бірінші бет).
7. 2 GraphQL (Relay-тәсіл)
Типтік 'connection' келісімшарты: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 жоқ «шикі 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 Ағындар және WebSockets
Үздіксіз таспалар үшін: меңзер «соңғы көрінген 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 с) бар Cache-Control.
Prefetch: 'next _ cursor' және кеңестер ('Link: rel =' next '') қайтарыңыз, клиент келесі бетті алдын ала жүктей алады.
Вариациялар: 'filter/sort/locale/tenant' кэш бөлігінің кілті ретінде ескеріңіз.
9) Жүктемені басқару және лимиттеу
Жоғарғы шегі 'limit', мысалы 200.
Server-side backpressure: егер сұрау уақыты> budget болса, жауапта 'limit' дегенді азайтыңыз (және клиентке нақты бет өлшемін анық хабарлаңыз).
Rate limits/token/tenant.
Timeout/Retry: экспоненциалды үзіліс, демпотенттік қайталама сұраулар.
10) UX-аспектілері
Скролл қарсы нөмірлеу: шексіз айналдыру → меңзерлер; нөмірлік беттер → offset (бірақ деректерді жаңарту кезіндегі дәлсіздікті түсіндіріңіз).
«Орнына оралу» түймешігі: клиенттің курсор стегін сақтаңыз.
Бос беттер: егер 'has _ more = false' болса, «Тағы» түймешігін көрсетпеңіз.
Тұрақты шектер: егер ол арзан болса ғана дәл 'total' дегенді көрсетіңіз (әйтпесе - шамамен 'approx _ total').
11) Тестілеу және edge-кейстер
Чек парақтары:- Тұрақты сұрыптау: бірдей 'ts' элементтері «жыпылықтамайды».
- Кірістіру/жою: беттердің түйісуінде қайталаулар пайда болмайды.
- Беттер арасындағы сүзгілерді өзгерту: «ескірген/сыйыспайтын» деп ауытқуы керек.
- TTL белгісі: мерзімі өткен соң дұрыс қате.
- Үлкен тереңдігі: жасырындылық сызықтық түрде өспейді.
- Мультишард: дұрыс merge-тәртіп, starvation «баяу» шардтардың болмауы.
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) беттің ұзындығы бойынша.
- 'seek _ index _ hit _ ratio' (жабатын индекс бойынша кеткен сұраулардың үлесі).
- 'next _ cursor _ invalid _ rate' (валидация қателері/TTL/қолтаңбалар).
- 'merge _ fanout' (бір бетке шаққандағы шардтардың саны).
- 'duplicates _ on _ boundary' және 'gaps _ on _ boundary' (клиенттік телеметриядағы жоба).
- 'cursor _ id' дегенді логтарда байланыстырып, payload жасырыңыз.
- Сипаттарды тегтеңіз: 'page _ size', 'source _ shards', 'db _ index _ used'.
- Қол жетімділік: 99. 'List' әдісіне 9%.
- Жасырындылығы: P95 <200 мс үшін 'page _ size <= 50' жергілікті шарда.
- Белгі қатесі: <0. Шақырулардың жалпы санынан 1%.
13) Көші-қон және үйлесімділік
'v' дегенді токенге қосып, N аптаның ескі нұсқаларын қолдаңыз.
Сұрыптау кілттерін ауыстырғанда, '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 Композитті кілтпен Seek-сұрау
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 қол қою және шектеу.
Белгілерді пайдаланушылар арасында төзімсіз етуге тырысыңыз (payload бағдарламасына 'sub/tenant/roles' жазыңыз және валидация кезінде салыстырыңыз).
Тек токендер хештеріне логин жасаңыз.
16) Жиі қателер және қарсы үлгілер
Base64 (id) курсор ретінде: қолдан жасау/таңдау оңай, сұрыптауды ауыстырған кезде келісімшартты бұзады.
tie-breaker болмауы: 'ORDER BY ts DESC' id 'жоқ → телнұсқалар/секірулер.
Беттер арасындағы сүзгілерді токенді мүгедектендірмей ауыстыру.
Терең OFFSET: баяу және болжанбайтын.
Нұсқасы және TTL жоқ токендер.
17) Енгізудің шағын чеклисті
1. Сұрыптауды анықтаңыз және бірегей tie-breaker қосыңыз.
2. Осы тәртіппен жабатын индекс жасаңыз.
3. Үлгіні таңдаңыз: seek + мөлдір емес токен.
4. Токеннің қолтаңбасын (және қажет болған жағдайда шифрлауды) іске асырыңыз.
5. TTL және нұсқалауды орнатыңыз.
6. 'has _ more', 'next _ cursor' келісімшарттарын жасаңыз және құжаттаңыз.
7. Кросс-шард схемасын (қажет болса) және k-way merge.
8. Өлшемдер, алерттар және SLO қосыңыз.
9. property-based тесттерін бет жиектерімен жабыңыз.
10. Токендердің көші-қон стратегиясын сипаттаңыз.
18) Тәсілді таңдау бойынша қысқаша ұсынымдар
«Бет нөмірі» және шамамен total маңызды каталогтар/іздеулер: мысалы 'OFFSET/LIMIT' + кэш; total шамамен екенін хабарлаңыз.
Таспалар, талдау, терең тізімдер, жоғары RPS: тек cursor/seek.
Шардталған/бөлінген коллекциялар: per-shard cursors + merge токені.
/ CDC ағындары: жаңартылған offsets/ts ретінде меңзерлер.
19) API уағдаластығының мысалы (түйіндеме)
`GET /v1/items? limit=50&cursor=...`
Жауап әрқашан 'page' дегенді қамтиды. limit`, `page. has_more' 'page. next_cursor`.
TTL қол қойған мөлдір емес меңзер.
Сұрыптау анықталған: 'ORDER BY created_at DESC, id DESC'.
Теру өзгерістері кезіндегі мінез-құлық: меңзерге қатысты элементтер «кері қайтарылмайды».
Өлшемдері мен қателері стандартталған: 'invalid _ cursor', 'expired _ cursor', 'mismatch _ filters'.
Бұл мақала пагинацияны жобалау үшін архитектуралық қағидаттар мен дайын паттерндерді береді, ол тіпті үлкен деректер, шардалау және белсенді түрде өзгеріп отыратын жазбалар жиынтығы жағдайында да жылдам, болжамды және қауіпсіз болып қалады.