პაგინაცია და კურსორები
1) რატომ არის საჭირო პაგინაცია
პაგინაცია ზღუდავს კლიენტის მიერ გადაცემული და გადაკეთებული მონაცემების მოცულობას, ამცირებს დატვირთვას შენახვის/ქსელებზე და ადგენს დეტერმინის მეთოდს „გასეირნება“ კოლექციის მიხედვით. რეალურ სისტემებში პაგინაცია არ არის მხოლოდ 'page = 1 & limit = 50', არამედ პროტოკოლის ხელშეკრულებების ერთობლიობა და თანმიმდევრულობის ინვარიანტები.
ტიპიური მიზნები:- ლატენტობის კონტროლი და მოთხოვნის მეხსიერება.
- სტაბილური ნავიგაცია მონაცემთა ნაკრების შეცვლისას (ჩანართი/წაშლა).
- ადგილიდან განახლების შესაძლებლობა.
- კეშინგი და წინასწარი დატვირთვა.
- ბოროტად გამოყენებისგან დაცვა.
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: Snowflake/UUIDv7, როგორც სტაბილური seek- ის გასაღებები.
3) კურსებისა და ნიშნების დიზაინი
3. 1 კარგი კურსორის თვისებები
გაუმჭვირვალე: კლიენტი არ არის დამოკიდებული ფორმატით.
ავტორიტეტი/მთლიანობა: HMAC ხელმოწერა ჩანაცვლების/მანიპულირების თავიდან ასაცილებლად.
კონტექსტი: მოიცავს დახარისხებას, ფილტრებს, სქემის ვერსიას, tenant/shard.
სიცოცხლის ხანგრძლივობა: TTL და „შეუსაბამობა“ ინდექსების/წვდომის უფლებების შეცვლისას.
ზომა: კომპაქტური (<= 1-2 KB), შესაფერისი URL- სთვის.
3. 2 ნიშნის ფორმატი
რეკომენდებული დასტის: JSON - შეკუმშვა (zstd/deflate) - BaseeuRL URL 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).
სურვილისამებრ დაშიფვრა (AES-GCM) მგრძნობიარე მნიშვნელობებით (PII).
სერვერზე შესაბამისობა: ვერსია, TTL, მომხმარებლის უფლებამოსილება (RBAC/ABAC).
4) კოორდინაცია და ინვარიანტები
4. 1 სტაბილური დახარისხება
გამოიყენეთ სრული დეტერმინიზმი: 'ORDER BY ts DESC, id DESC'.
დახარისხების გასაღები უნდა იყოს უნიკალური (დაამატეთ 'id' როგორც tiebreaker).
ინდექსი უნდა შეესაბამებოდეს დახარისხების ინდექსს.
4. 2 სურათები (snapshot) და იზოლაცია
„ღარიბი“ გვერდებისთვის გამოიყენეთ read-consistent snapshot (MVCC/txid).
თუ snapshot არაპრაქტიკულია (ძვირი/ბევრი მონაცემი), ჩამოაყალიბეთ კონტრაქტი: „კურსორი უბრუნებს ელემენტებს მკაცრად ადრე“. ეს ბუნებრივია ახალი ამბების ფილმებისთვის.
4. 3 ჩანართები/წაშლა გვერდებს შორის
Seek მოდელი მინიმუმამდე ამცირებს „დუბლიკატებს/უღელტეხილებს“.
ამოიღეთ ქცევა წაშლის/შეცვლის დროს: იშვიათი „ხვრელები“ დაიშვება გვერდებს შორის, მაგრამ არა „დროულად უკან“.
5) იდენტიფიკატორის ინდექსირება და სქემები
კომპოზიციური ინდექსები მკაცრად დალაგების თანმიმდევრობით: '(created _ at DESC, id DESC)'.
მონოტონური ID: Snowflake/UUIDv7 დროულად ბრძანებს და აჩქარებს seek- ს.
ცხელი გასაღებები: განაწილეთ shard-key (მაგალითად, 'tenant _ id', 'region') და მოათავსეთ shard შიგნით.
ID გენერატორები: თავიდან აიცილეთ კონფლიქტები და „საათების მომავლიდან“ - დროის სინქრონიზაცია, NTP ნახტომი „რეგრესია“.
6) ჯვარედინი შარდის პაგინაცია
6. 1 სქემები
Scatter-Gather: პარალელური მოთხოვნები ყველა შარდში, ადგილობრივი seek კურსები, შემდეგ k-way merge აგრეგატორზე.
Per-Shard Cursors: ნიშანი შეიცავს პოზიციებს თითოეულ ხიბლზე.
Bounded fan-out: შეზღუდეთ ხარდიების რაოდენობა ერთი ნაბიჯით.
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
}
}
რეკომენდაციები:
- „ლიმიტი“ ზედა ზღვარით (მაგალითად, max = 200).
- 'შემდეგი _ cursor' არ არსებობს, თუ 'has _ more = false'.
- GET idempotention, პასუხის ქეშირება 'შემდეგი _ cursor' გარეშე (პირველი გვერდი ფიქსირებული ფილტრებით და snapshot).
7. 2 GraphQL (Relay მიდგომა)
ტიპიური კონტრაქტი „კავშირი“: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' უნდა იყოს გაუმჭვირვალე და ხელმოწერილი; არ გამოიყენოთ „ნედლეული Base64 (id)“ HMAC- ის გარეშე.
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 პირველი გვერდისთვის სტაბილური ფილტრებით.
Cache Control მოკლე TTL (მაგალითად, 5-30 გვ) საზოგადოებრივი სიებისთვის.
Prefetch: დაუბრუნეთ 'შემდეგი _ cursor' და რჩევები ('Link: rel = „შემდეგი“), კლიენტს შეუძლია შემდეგი გვერდის გადატვირთვა.
ვარიაციები: გაითვალისწინეთ 'filter/sort/locale/tenant', როგორც ქეშის ნაწილის გასაღები.
9) ტვირთის მართვა და შეზღუდვა
„ლიმიტის“ ზედა საზღვარი, მაგალითად 200.
Server side backpressure: თუ მოთხოვნის დრო> budget, შეამცირეთ 'limit' საპასუხოდ (და აშკარად აცნობეთ კლიენტს გვერდის ფაქტობრივი ზომა).
მომხმარებლის ლიმიტები/ნიშანი/ტენანტი.
Timeout/Retry: ექსპონენციალური პაუზა, immpotent განმეორებითი მოთხოვნები.
10) UX ასპექტები
ნაკაწრი ნუმერაციის საწინააღმდეგოდ: გაუთავებელი გადახრა, კურსორები; offset სალიცენზიო გვერდები (მაგრამ ახსენით უზუსტობა მონაცემთა განახლებისას).
ღილაკი „დაბრუნდით ადგილზე“: შეინახეთ კლიენტის კურსორების დასტური.
ცარიელი გვერდები: თუ 'has _ more = false', არ აჩვენოთ ღილაკი „სხვა“.
სტაბილური საზღვრები: აჩვენეთ ზუსტი 'ტოტალური' მხოლოდ იმ შემთხვევაში, თუ ის იაფია (წინააღმდეგ შემთხვევაში - სავარაუდო 'approx _ total').
11) ტესტირება და edge შემთხვევები
ჩეკის ფურცლები:- სტაბილური დახარისხება: იგივე 'ts' ელემენტები არ არის „მოციმციმე“.
- ჩანართები/წაშლა: განმეორებები არ ჩანს გვერდების კვანძზე.
- ფილტრების შეცვლა გვერდებს შორის: ნიშანი უნდა გადახრა, როგორც „მოძველებული/შეუთავსებელი“.
- TTL ნიშანი: სწორი შეცდომა ვადის გასვლის შემდეგ.
- დიდი სიღრმე: ლატენტობა ხაზოვანი არ იზრდება.
- მულტიშარდი: სწორი მერჯიანი წესრიგი, „ნელი“ ხარდიების არარსებობა.
python
Generate N entries with random inserts between calls
Verify that all pages are merged = = whole ordered fetch
12) დაკვირვება და SLO
მეტრიკა:- 'list _ request _ latence _ ms' (P50/P95/P99) გვერდის სიგრძით.
- 'seek _ index _ hit _ ratio' (მოთხოვნების წილი, რომლებიც ტოვებენ ინდექსს).
- 'შემდეგი _ cursor _ invalid _ rate' (ვალიდაციის შეცდომები/TTL/ხელმოწერები).
- 'merge _ fanout' (გვერდზე ჩართული ხუმრობების რაოდენობა).
- 'duplicates _ on _ boundary' და 'gaps _ on _ boundary' (დეტალი მომხმარებლის ტელემეტრიის შესახებ).
- დაუკავშირეთ 'cursor _ id' ლოგებში, შენიღბეთ payload.
- Spage spans: 'page _ size', 'source _ shards', 'db _ index _ used'.
- წვდომა: 99. 9% 'List' მეთოდებზე.
- ლატენტობა: P95 <200 ms '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.
შეეცადეთ გააკეთოთ ნიშნები მომხმარებლებს შორის შეუწყნარებელი (შეიტანეთ 'sub/tenant/roles' payload- ში და შეამოწმეთ ვალიდაციის დროს).
გააფართოვეთ მხოლოდ ტოქსინების ჰაშები.
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', 'შემდეგი _ cursor'.
7. იფიქრეთ ჯვარედინი შარდის სქემაზე (საჭიროების შემთხვევაში) და k-way მერჯზე.
8. დაამატეთ მეტრიკები, ალერტები და SLO.
9. დაფარეთ გვერდის საზღვრის ტესტები.
10. აღწერეთ ტოქსინების მიგრაციის სტრატეგია.
18) მოკლე რეკომენდაციები მიდგომის არჩევის შესახებ
კატალოგები/ძებნა, სადაც მნიშვნელოვანია „გვერდის ნომერი“ და სავარაუდო ტოტალი: მაგალითად 'OFFSET/LIMIT' + ქეში; აცნობეთ, რომ ტოტალური სავარაუდოა.
ფირები, ანალიტიკა, ღრმა სიები, მაღალი 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'.
ეს სტატია იძლევა არქიტექტურულ პრინციპებს და მზა ნიმუშებს პაგინაციის შესაქმნელად, რომელიც რჩება სწრაფი, პროგნოზირებადი და უსაფრთხო, თუნდაც დიდი მონაცემების პირობებში, შარდვა და ჩანაწერების აქტიურად ცვალებადი ნაკრები.