Yeda AI Tips · #134

Español

Swap Offset Pagination for Keyset Pagination

On a large, changing table, offset pagination is a bug waiting to happen. LIMIT 20 OFFSET 100 looks harmless in a code review. It isn't. On a big, mutable table it does two things nobody wants: it gets slower every page, and it silently skips and repeats rows while people are still inserting and deleting underneath you. Keyset pagination fixes both — and it's the pattern reviewers expect once your table stops being small.

Why offset drifts

OFFSET N tells the database: fetch the rows in order, then throw the first N away. Two consequences fall out of that one sentence.

Page by key, not by position

Keyset pagination (also called seek or cursor pagination) never counts positions. It remembers the sort key of the last row on the page and asks for rows after that key:

-- Page 1
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- Next page: pass the last row's (created_at, id) back in
SELECT id, title, created_at
FROM articles
WHERE (created_at, id) < (:last_created_at, :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;

The WHERE clause anchors to a value, not a count, so inserts and deletes elsewhere in the table can't shift your boundary. And with an index on (created_at, id), the database seeks straight to the anchor instead of walking past everything before it — page 5,000 costs the same as page 1.

The one rule that makes or breaks it

Keyset needs an explicit, unique, stable sort order. Never rely on implicit ordering — a query with no ORDER BY returns rows in whatever physical order the engine likes, and that order can change under you.

Sort columnProblemFix
created_at aloneTimestamps collide; ties get skipped or repeated at page edgesAdd a unique tie-breaker: ORDER BY created_at DESC, id DESC
name, price, any non-unique fieldSame collision at boundariesAppend the primary key to break ties
No ORDER BYOrder is undefined and unstableAlways sort explicitly

The whole guarantee rests on the cursor pointing at exactly one row. Any non-unique sort column needs the primary key appended so the (sort_key, id) tuple is unique.

Power user notes

Resources

Shipping data-heavy features? Yeda AI designs, audits, and ships production backends that stay correct at scale.

Talk to us · Read the blog