Skip to content

Paginer par keyset (seek) plutôt qu'avec OFFSET snippet

La pagination OFFSET repasse devant N lignes à chaque page — la page 1000 relit 999 pages de données, et toute insertion entre deux requêtes décale les lignes, si bien que des éléments se répètent ou disparaissent.

La pagination OFFSET repasse devant N lignes à chaque page — la page 1000 relit 999 pages de données, et toute insertion entre deux requêtes décale les lignes, si bien que des éléments se répètent ou disparaissent. La pagination keyset (seek) demande plutôt : donne-moi les lignes strictement après la dernière que j'ai vue. La clé de tri doit être unique et stable — un timestamp seul ne l'est pas, les lignes à égalité se réordonnent imprévisiblement — donc l'id s'ajoute comme départage, et l'index composite (created_at, id) sert le scan. La syntaxe de comparaison de lignes (a, b) > (x, y) est l'orthographe concise de Postgres.

Recette exécutable · 1 langages
Files & Datasqlpaginationkeysetseekperformance

Every language

1 langages, copy-ready. One at a time with syntax highlighting, or all inline.

SQLSQLrunnable
-- page 1
SELECT id, created_at, title
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 20;

-- next page: feed the LAST row's keys back in
SELECT id, created_at, title
FROM posts
WHERE (created_at, id) < ('2026-08-24 18:02:00+00', 9014)
ORDER BY created_at DESC, id DESC
LIMIT 20;

The tuple comparison (created_at, id) < (last_seen_at, last_seen_id) is lexicographic — exactly the ORDER BY in reverse. Needs the composite index CREATE INDEX ON posts (created_at DESC, id DESC) to stay an index scan at depth. Run both queries in the playground.

Run in the SQL playground →

Keep going

Read the sql-indexing cheatsheet →