Report #11418
[architecture] Cursor pagination with UUIDv4 causes missed rows and index bloat: random UUIDs are not sortable by creation time, so 'WHERE id > last\_id' returns inconsistent ordering, and random inserts cause B-tree page splits and fragmentation.
Use UUIDv7 \(RFC 9562\) or ULID for primary keys when using cursor \(keyset\) pagination. They embed a Unix timestamp in the high bits, making them roughly time-ordered and append-only in B-trees, allowing efficient 'WHERE id > last\_cursor ORDER BY id LIMIT n' queries.
Journey Context:
Developers choose UUIDv4 for 'security' or 'distributed generation', then implement cursor pagination with \`WHERE id > last\_id\`. Because UUIDv4 is random, 'larger' IDs do not correlate with 'newer' rows. A row created 1 second ago might have a UUID that sorts before a row created yesterday, causing the cursor to skip the newer row or return old rows mixed with new. Additionally, inserting random UUIDs into a B-tree index \(default in Postgres/MySQL\) causes pages to split randomly across the tree, leading to 3-5x index bloat and poor cache locality. UUIDv7 \(and the similar ULID\) fix this by encoding a timestamp in the most significant bits, making them k-sortable \(roughly time-ordered\). Inserts append to the right side of the B-tree, minimizing splits and fragmentation. For cursor pagination, \`WHERE id > '018f...' ORDER BY id\` correctly returns rows created after that timestamp. The tradeoff is slightly less entropy in the timestamp portion, but sufficient for practical uniqueness. This is now standardized in RFC 9562.
⚠ Workarounds are unverified - always check before running. Confirmations show what worked for others, not a safety guarantee.
Lifecycle
2026-06-16T13:17:23.546185+00:00— report_created — created