Contenido de esta guía
Respuesta rápida
La paginación keyset —también llamada por cursor— pide «las siguientes filas después de esta clave». Necesita un ORDER BY estable y reutiliza los valores de la última fila de la página anterior dentro de WHERE.
SELECT id, fecha, total
FROM pedidos
WHERE fecha < :ultima_fecha
OR (fecha = :ultima_fecha AND id < :ultimo_id)
ORDER BY fecha DESC, id DESC
LIMIT 5;El cursor contiene dos datos de la última fila: ultima_fecha y ultimo_id. La condición debe avanzar en la misma dirección lógica que el orden.
Modelo mental: deja de pensar en «página 37»
Con OFFSET una aplicación suele pedir «omite 180 filas y dame las 20 siguientes». Con keyset pide «continúa después del último pedido que ya mostré». Esa diferencia evita convertir la posición absoluta de una fila en el punto de partida de cada consulta.
El patrón encaja especialmente bien con feeds, historiales, listados cronológicos y botones «cargar más». No es igual de cómodo si necesitas saltar directamente a una página numerada arbitraria.
El cursor no es una fila oculta. Es el conjunto mínimo de valores necesarios para continuar el mismo orden sin ambigüedad. Si ordenas por fecha DESC, id DESC, conserva ambos.
Ejemplo con la base educativa
La primera consulta no necesita cursor: solo fija un orden total y limita el bloque.
SELECT id, fecha, total
FROM pedidos
ORDER BY fecha DESC, id DESC
LIMIT 5;| id | fecha | total |
|---|---|---|
| 118 | 2025-06-16 | 179,00 |
| 117 | 2025-06-14 | 148,90 |
| 116 | 2025-06-12 | 999,00 |
| 115 | 2025-06-10 | 24,00 |
| 114 | 2025-06-08 | 203,90 |
El cursor para continuar es fecha = '2025-06-08' e id = 114.
SELECT id, fecha, total
FROM pedidos
WHERE fecha < '2025-06-08'
OR (fecha = '2025-06-08' AND id < 114)
ORDER BY fecha DESC, id DESC
LIMIT 5;| id | fecha | total |
|---|---|---|
| 113 | 2025-06-06 | 98,00 |
| 112 | 2025-06-03 | 54,00 |
| 111 | 2025-06-01 | 249,00 |
| 110 | 2025-05-28 | 61,50 |
| 109 | 2025-05-25 | 89,00 |
Por qué necesitas un desempate único
Si ordenas solo por fecha y dos pedidos comparten la misma fecha, el cursor no sabe qué filas de ese empate ya aparecieron. Añadir id —si es único— crea un orden total y permite continuar dentro de la misma fecha.
Cambiar el ORDER BY entre páginas
El filtro del cursor y el ORDER BY describen el mismo recorrido. Si cambias columnas, dirección o colación sin regenerar el cursor, puedes repetir o saltar filas.
Qué mejora frente a OFFSET y qué no
PostgreSQL documenta que las filas descartadas por un OFFSET grande siguen teniendo que calcularse dentro del servidor. Keyset evita basarse en una cantidad creciente de filas que deben saltarse y permite que un índice alineado con el filtro y el orden sea una buena candidata para continuar el recorrido.
También reduce el desplazamiento posicional causado por inserciones anteriores al cursor. Sin embargo, no crea por sí sola una fotografía inmutable de los datos: cambios posteriores todavía pueden alterar qué filas existen o qué valores tienen. Si necesitas consistencia de snapshot entre muchas peticiones, eso pertenece al diseño transaccional y de la aplicación.
Índice candidato y comprobación
CREATE INDEX idx_pedidos_fecha_id
ON pedidos (fecha DESC, id DESC);Es una hipótesis razonable para este patrón, no una promesa de rendimiento. Comprueba el plan con la herramienta de tu motor y mide con datos representativos. El tratamiento de direcciones de índice y el plan concreto varían entre motores.
Compatibilidad entre motores
Keyset no es una cláusula SQL especial: combina comparaciones, WHERE, ORDER BY y una forma de limitar filas. PostgreSQL, MySQL y SQLite permiten LIMIT; SQL Server puede limitar con TOP o con OFFSET/FETCH según el diseño. La forma de los parámetros (:nombre, ?, @nombre) pertenece al driver o framework.
Algunos motores admiten comparar valores de fila, por ejemplo (fecha, id) < (?, ?). La forma explícita con OR de esta guía muestra la lógica sin depender de ese atajo y facilita adaptarla a dialectos distintos.
Práctica: continúa después del pedido 111
Devuelve las tres filas siguientes
El último pedido visible tiene fecha = '2025-06-01' e id = 111. Continúa el orden fecha DESC, id DESC y devuelve tres filas.
ORDER BY y usa LIMIT 3.Solución razonada
SELECT id, fecha, total
FROM pedidos
WHERE fecha < '2025-06-01'
OR (fecha = '2025-06-01' AND id < 111)
ORDER BY fecha DESC, id DESC
LIMIT 3;El resultado comprobado es 110 (2025-05-28), 109 (2025-05-25) y 108 (2025-05-23). El filtro avanza desde el cursor, no desde una posición numérica.
Qué debes recordar
- Keyset continúa desde los valores de la última fila vista.
- El orden debe ser total y estable; añade una clave única para desempatar.
- La condición del cursor debe avanzar en la misma dirección que
ORDER BY. - Evita offsets crecientes, pero no sustituye una estrategia de consistencia transaccional.
Conceptos relacionados
Fuentes técnicas consultadas
Las referencias oficiales se usaron para verificar el coste conceptual de OFFSET, el orden y las variantes de límite.