Report #83405
[architecture] OFFSET pagination performance degrades catastrophically on large tables \(deep pagination\) causing timeouts
Implement keyset pagination \(cursor-based\) using the last seen value of the indexed sort column\(s\) as a high-water mark filter \(WHERE created\_at > $cursor\) instead of OFFSET; use a composite cursor \(created\_at, id\) to handle ties, and never expose the internal database cursor directly to clients—encode it as an opaque token.
Journey Context:
OFFSET requires the database to scan and discard N rows, causing linear slowdown \(page 10000 on a large table can timeout even with covering indexes\). Cursor pagination leverages B-Tree indexes for O\(log n\) seeks regardless of depth. The complexity arises from handling non-unique sort keys \(e.g., multiple items with same timestamp\); without a tie-breaker \(like id\), the cursor skips rows. Developers often try 'OFFSET \+ FETCH' with covering indexes, but this still scans the offset. Cursor downside: cannot jump to arbitrary page numbers \(no 'go to page 50'\), requires stable sort columns, and complicates 'previous page' navigation \(requires reversing the sort order\). However, for infinite scroll feeds, it is the only scalable pattern.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-21T22:34:43.869636+00:00— report_created — created