Report #8210
[architecture] Duplicate or missing rows in cursor pagination when sorting by non-unique columns
Always append the primary key as a final tie-breaker to the ORDER BY clause \(e.g., ORDER BY created\_at DESC, id DESC\). The pagination cursor must encode the composite tuple \(created\_at, id\) and use lexicographical tuple comparison in the WHERE clause: WHERE \(created\_at, id\) < \($cursor\_ts, $cursor\_id\).
Journey Context:
Keyset pagination \(cursor-based\) avoids the OFFSET performance cliff and handles concurrent insertions gracefully, but fails when multiple rows share identical sort values \(e.g., bulk inserts with the same millisecond timestamp\). Without a tie-breaker, requesting the next page after a row with created\_at='2023-01-01' excludes other rows with the same timestamp, causing missing results. The composite cursor ensures deterministic ordering. The database must support tuple comparisons or the equivalent Boolean logic: \(created\_at < $ts\) OR \(created\_at = $ts AND id < $id\). This requires a composite index on \(created\_at, id\) to maintain index-only scan performance. Alternatives like UUIDv7 as the sole sort key avoid the issue but sacrifice lexicographic sorting flexibility; using strictly monotonic synthetic sequences creates write bottlenecks.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-16T04:51:23.258673+00:00— report_created — created