Загрузка…
Загрузка…
Offset медленный и unstable на scale; cursor (keyset) seeks via index, stable under concurrent writes. Tiebreaker на primary key обязателен.
-- ❌ OFFSET: re-scans and discards OFFSET rows every time. Page 5000 is painfully slow,
-- and a concurrent insert shifts every subsequent page → duplicates.
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20 OFFSET 100000;
-- ✅ CURSOR (keyset): seeks via index, same speed at any depth, stable under writes.
-- Tiebreak on id because created_at is not unique — WITHOUT this, rows sharing a
-- timestamp at the page boundary get skipped.
SELECT * FROM posts
WHERE (created_at, id) < ('2026-07-01T10:00:00Z', 8423) -- the decoded cursor
ORDER BY created_at DESC, id DESC
LIMIT 20;// API response: hand back an OPAQUE next-cursor; the client never parses it.
{
"data": [ /* 20 posts */ ],
"page_info": {
"end_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNy0wMSIsImlkIjo4NDIzfQ==",
"has_next_page": true // fetch LIMIT+1 and check for the extra row
}
}
// Client just echoes the token back — no page numbers, no arithmetic.
fetch(`/api/posts?after=${encodeURIComponent(endCursor)}&limit=20`);Opaque cursor скрывает sort key. LIMIT+1 для has_next_page без COUNT(*). Client не парсит token — только echo back.
Keyset pagination с (created_at, id) tiebreaker; opaque cursor в API; offset только для small admin tables.
