Skip to content

Paginar con keyset (seek) en lugar de OFFSET snippet

La paginación con OFFSET recorre N filas en cada página — la página 1000 relee 999 páginas de datos, y cualquier inserción entre peticiones desplaza filas de modo que los ítems se repiten o desaparecen.

La paginación con OFFSET recorre N filas en cada página — la página 1000 relee 999 páginas de datos, y cualquier inserción entre peticiones desplaza filas de modo que los ítems se repiten o desaparecen. La paginación keyset (seek) en cambio pregunta: dame las filas estrictamente posteriores a la última que vi. La clave de ordenación debe ser única y estable — un timestamp por sí solo no lo es, las filas empatadas se reordenan de forma impredecible — así que el id acompaña como desempate, y el índice compuesto (created_at, id) sirve al escaneo. La sintaxis de comparación de filas (a, b) > (x, y) es la grafía concisa de Postgres.

Receta ejecutable · 1 lenguajes
Files & Datasqlpaginationkeysetseekperformance

Every language

1 lenguajes, 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 →