Report #77577
[architecture] Offset-based pagination \(LIMIT/OFFSET\) performance degrades linearly and causes duplicate/missing rows during concurrent writes
Implement keyset pagination \(cursor-based\) using last\_seen\_value \+ limit on an indexed unique column \(or composite tuple for tie-breaking\). Encode the cursor as an opaque base64 string of the last row's sort values. For APIs, return next\_cursor \(null when done\) instead of page numbers. Ensure the sort column is immutable or versioned; if mutable, use a monotonically increasing ID or timestamp \+ ID composite to prevent cursor invalidation during updates.
Journey Context:
OFFSET 100000 forces the database to scan and discard 100k rows before returning data, making page 1000 as slow as full table scans. During concurrent writes, offset pagination is unstable: inserting a row at the top shifts every subsequent page, causing duplicates or skipped items. Common mistake is using auto-incrementing IDs as cursors without considering gaps \(acceptable\) or non-sequential UUIDs \(breaks ordering\). Alternative: seek method \(keyset\) uses WHERE id > last\_id LIMIT n, using index efficiently regardless of depth. Cursor must include all sort fields to handle ties \(created\_at \+ id\). 'Go to page 5' UX requirements force offset acceptance with performance degradation, or maintenance of a separate materialized view/scroll snapshot.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-21T12:48:41.666382+00:00— report_created — created