Ilustración editorial: Paginación con cursor vs paginación con offset: cuándo aplica cada patrón en APIs REST

Diseño de API · Paginación · REST · Rendimiento · Mejores Prácticas

Paginación con cursor vs paginación con offset: cuándo aplica cada patrón en APIs REST

Una comparación práctica de paginación basada en cursor, offset y keyset para APIs REST: compensaciones, guía de implementación y cuándo elegir cada enfoque para conjuntos de datos en crecimiento.

Publicado:

La respuesta corta

La paginación con offset es adecuada para conjuntos de datos pequeños y estáticos donde los usuarios necesitan saltar a páginas arbitrarias. La paginación con cursor es el valor por defecto correcto para colecciones en crecimiento o actualizadas frecuentemente, porque evita la degradación de rendimiento y los problemas de deriva de datos que introducen los enfoques basados en offset a escala. La paginación con keyset es un punto medio que ofrece navegación estable sin la complejidad de codificación del cursor.

Por qué importa la elección del patrón de paginación

Cuando tu API sirve miles o millones de registros, devolver todo en una sola respuesta no es viable. La paginación divide los resultados en fragmentos manejables, pero el método que elijas afecta directamente el rendimiento de las consultas, la complejidad del cliente y la consistencia de los datos a medida que crece tu conjunto de datos.

Los tres patrones que encontrarás en el diseño de APIs REST son la paginación basada en offset, la basada en cursor y la basada en keyset. Cada una tiene compensaciones distintas.

Paginación basada en offset

La paginación con offset utiliza dos parámetros: un offset (el número de fila inicial) y un límite (cuántas filas devolver). Esto se mapea directamente a las cláusulas LIMIT y OFFSET de SQL que la mayoría de los desarrolladores ya conocen.

Cómo funciona:

GET /api/productos?offset=0&limit=10
GET /api/productos?offset=10&limit=10
GET /api/productos?offset=20&limit=10

Cuándo funciona bien:

Cuándo falla:

El problema fundamental es que la base de datos debe escanear y descartar cada fila antes del offset. Si solicitas offset 50.000 con un límite de 10, la base de datos lee 50.010 filas y descarta las primeras 50.000. A medida que crece el offset, el tiempo de consulta crece proporcionalmente. Esto no es una preocupación teórica: es una consecuencia directa de cómo funcionan los escaneos en B-tree y heap en bases de datos relacionales.

Un segundo problema es la deriva de datos. Si se insertan o eliminan filas entre solicitudes paginadas, el usuario ve registros duplicados o huecos. Contentful documentó este problema exacto cuando migraron sus APIs de contenido lejos de la paginación con offset: el contenido agregado o eliminado entre solicitudes de página causaba que los resultados se desplazaran de forma impredecible.

Paginación basada en cursor

La paginación con cursor reemplaza el offset numérico por un valor de cursor, típicamente un token opaco que codifica la posición en el conjunto de resultados. El cliente pasa el cursor de la respuesta anterior para obtener la siguiente página.

Cómo funciona:

GET /api/productos?limit=10
→ la respuesta incluye next_cursor: "eyJpZCI6MTIzNH0="

GET /api/productos?limit=10&cursor=eyJpZCI6MTIzNH0=
→ la respuesta incluye next_cursor: "eyJpZCI6MTIzNX0="

Cuándo funciona bien:

Cómo implementarla correctamente:

El cursor debe codificar los valores usados en la cláusula ORDER BY, no solo un ID de fila. Un cursor basado solo en un ID autoincremental falla cuando las filas comparten la misma clave de ordenamiento o cuando la columna de orden no es única. Una implementación robusta codifica las columnas de ordenamiento más el ID como desempate:

SELECT * FROM productos
WHERE (fecha_creacion, id) > ('2024-01-15T10:30:00Z', 1234)
ORDER BY fecha_creacion ASC, id ASC
LIMIT 10;

El token del cursor codifica fecha_creacion e id de la última fila. Esto asegura un ordenamiento determinista incluso cuando varias filas comparten la misma marca de tiempo.

La compensación:

La paginación con cursor no soporta acceso aleatorio. No puedes saltar a la página 50. Debes recorrer el cursor secuencialmente. Esta es una decisión de diseño deliberada, no una limitación que debas superar. Si tu caso de uso requiere números de página, la paginación con offset o keyset es más apropiada.

Incluir un conteo total:

Una solicitud común del cliente es mostrar “Página 3 de 47” con paginación por cursor. Esto requiere una consulta COUNT(*) separada, lo que añade latencia. La librería fastapi-pagination aborda esto permitiendo un tipo de página de cursor personalizado que incluye el total, pero viene con el costo de una consulta adicional a la base de datos en cada respuesta. Decide si el beneficio de UX justifica el costo de rendimiento para tu API específica.

Paginación con keyset

La paginación con keyset está estrechamente relacionada con la paginación por cursor pero usa valores de columna explícitos en lugar de un token opaco. El cliente pasa el último valor visto de la columna de ordenamiento directamente.

Cómo funciona:

GET /api/productos?limit=10&after_id=1234
GET /api/productos?limit=10&after_id=1235

Cuándo funciona bien:

La compensación:

La paginación con keyset expone tu modelo de datos al cliente. La columna de ordenamiento se convierte en parte del contrato de la API pública. Si necesitas cambiar el orden de ordenamiento o los nombres de las columnas, rompes a los clientes existentes. La paginación con cursor oculta este detalle detrás de un token codificado, haciendo que los cambios de esquema sean más seguros.

Resumen de comparación

Aspecto Offset Cursor Keyset
Acceso a página arbitraria No No
Rendimiento en offsets profundos Degrada Estable Estable
Resistencia a deriva de datos Pobre Fuerte Fuerte
Complejidad de implementación Baja Media Baja
Resiliencia a cambios de esquema N/A Buena Pobre
Soporte de conteo total Nativo Requiere consulta extra Requiere consulta extra

Cuándo elegir cada patrón

Elige paginación con offset cuando:

Elige paginación con cursor cuando:

Elige paginación con keyset cuando:

Lista de verificación para la implementación

Independientemente del patrón que elijas, estas prácticas aplican:

  1. Ordena explícitamente siempre. Nunca confíes en el ordenamiento implícito de la base de datos. Una cláusula ORDER BY sin índice escaneará toda la tabla.

  2. Indexa tus columnas de ordenamiento. La paginación con offset en profundidad requiere un índice en la columna de offset. La paginación con cursor y keyset requiere un índice en la clave de ordenamiento más cualquier columna de desempate.

  3. Devuelve metadatos de navegación. Incluye next_cursor o equivalente en cada respuesta. La API REST de GitHub usa encabezados Link con rel="next" y rel="prev" para este propósito. Las APIs basadas en cursor deben incluir ambos cursores next y previous cuando corresponda.

  4. Documenta el formato del cursor. Ya sea opaco o explícito, los clientes necesitan saber cómo extraer y pasar el valor del cursor. Documenta si el cursor es URL-safe, codificado en base64 o un valor crudo.

  5. Establece un tamaño máximo de página. Permite que los clientes soliciten un límite, pero aplica un límite superior. Esto evita que una sola solicitud sobrecargue tu base de datos o la memoria de tu cliente.

  6. Prueba con escrituras concurrentes. Si tus datos cambian mientras un cliente navega por las páginas, verifica que los resultados sean consistentes. Los enfoques de cursor y keyset manejan esto mejor que offset, pero aún debes validar el comportamiento para tu carga de trabajo específica.

Preguntas frecuentes

¿Puedo combinar paginación con offset y cursor en la misma API?

Sí. Algunas APIs ofrecen paginación con offset para casos simples y paginación con cursor para conjuntos grandes. La API REST de GitHub usa paginación basada en offset con números de página. Si ofreces ambos, documenta qué endpoints usan cuál patrón y haz que la distinción sea clara en tu especificación OpenAPI.

¿Funciona la paginación con cursor con filtrado?

Sí, pero el cursor debe codificar las condiciones de filtro además de la clave de ordenamiento. Un cursor que solo codifica la posición de ordenamiento producirá resultados incorrectos cuando los filtros cambien el conjunto de resultados. Incluye los parámetros de filtro en el token del cursor o requiere que los clientes pasen los mismos filtros con cada solicitud paginada.

¿Qué pasa con la paginación en GraphQL?

El spec de conexiones de Relay de GraphQL usa paginación basada en cursor por diseño. El cursor en GraphQL típicamente codifica el ID del nodo y la posición, haciéndolo compatible con el enfoque de cursor descrito aquí. Si estás construyendo una API GraphQL, sigue el spec de Relay en lugar de inventar un modelo de paginación personalizado.

¿Es la paginación con keyset lo mismo que la paginación con cursor?

Resuelven el mismo problema con contratos diferentes para el cliente. La paginación con keyset expone el valor de orden; la paginación con cursor lo oculta detrás de un token. Desde la perspectiva del rendimiento de la base de datos, son equivalentes cuando se implementan correctamente.

Fuentes