Patrones de Paginación en MongoDB
Estás construyendo un feed, una lista de productos o una página de resultados de búsqueda. La colección ha crecido a millones de documentos y, de repente, tus consultas de paginación tardan segundos en lugar de milisegundos. El culpable es casi siempre el mismo: estás usando skip y limit a gran escala.
Este artículo cubre los patrones principales de paginación en MongoDB—basados en offset y basados en cursor/keyset—explicando cuándo funciona bien cada uno y cuándo no. Sin consejos específicos de frameworks, solo patrones que permanecerán válidos a medida que tus datos crezcan.
Puntos Clave
- La paginación por offset con
skip()ylimit()es intuitiva pero se degrada linealmente a medida que aumentan los valores de offset—resérvala para paginación superficial o conjuntos de datos pequeños. - La paginación por keyset (basada en cursor) utiliza consultas de rango para obtener un rendimiento consistente independientemente de la posición, lo que la hace ideal para grandes conjuntos de datos.
- Siempre usa un campo de desempate único (típicamente
_id) con la paginación por keyset para prevenir duplicados o documentos omitidos. - Elige tu patrón de paginación según el tamaño de los datos, los patrones de acceso y si los usuarios necesitan navegación secuencial o saltos a páginas arbitrarias.
Paginación MongoDB Skip Limit: El Enfoque Familiar
La paginación basada en offset utiliza skip() y limit() para dividir los resultados en páginas. Es intuitiva y se mapea directamente a patrones de UI como “página 1, página 2, página 3”.
db.posts.find()
.sort({ createdAt: -1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
Este patrón funciona bien para paginación superficial—las primeras páginas de resultados, paneles de administración con datos limitados o herramientas internas donde el rendimiento no es crítico.
Por Qué Skip se Degrada a Gran Escala
Los índices de MongoDB son estructuras B-tree, no arrays. Aunque la base de datos puede buscar eficientemente dentro de un índice, aún tiene que avanzar a través de las entradas omitidas para alcanzar el offset solicitado. Omitir 10 documentos avanza pasando 10 entradas de índice. Omitir 100,000 avanza pasando 100,000 entradas.
Esto significa que la página 1 es rápida, la página 100 es más lenta, y la página 10,000 es significativamente más lenta aún. El uso de CPU aumenta linealmente con el valor del offset, independientemente del tamaño de tu página.
Usa paginación por offset cuando:
- Los usuarios rara vez navegan más allá de las primeras páginas
- El conjunto de datos total es pequeño (menos de 10,000 documentos)
- Necesitas funcionalidad de “saltar a la página X” y aceptas el compromiso
- Estás construyendo herramientas internas donde el tiempo de consulta es menos crítico
Paginación Basada en Cursor de MongoDB: Rendimiento Consistente
La paginación por keyset de MongoDB (también llamada paginación basada en cursor) utiliza consultas de rango en lugar de offsets posicionales. En lugar de decir “omite 1,000 documentos”, dices “dame documentos después de este punto específico”.
db.posts.find({ createdAt: { $lt: lastSeenDate } })
.sort({ createdAt: -1 })
.limit(pageSize)
La base de datos realiza una búsqueda eficiente en el índice para localizar el punto de inicio, luego lee solo los documentos que necesitas. La página 1 y la página 10,000 tienen características de rendimiento idénticas.
El Requisito del Desempate
Un solo campo de ordenamiento crea problemas cuando los valores no son únicos. Si múltiples documentos comparten la misma marca de tiempo createdAt, algunos pueden ser omitidos o duplicados entre páginas.
La solución es un ordenamiento compuesto con un desempate único—típicamente _id:
db.posts.find({
$or: [
{ createdAt: { $lt: lastDate } },
{ createdAt: lastDate, _id: { $lt: lastId } }
]
})
.sort({ createdAt: -1, _id: -1 })
.limit(pageSize)
Esto requiere un índice compuesto correspondiente:
db.posts.createIndex({ createdAt: -1, _id: -1 })
El orden de los campos del índice debe coincidir con tu orden de clasificación para un rendimiento óptimo.
Discover how at OpenReplay.com.
Advertencias sobre Consistencia
La paginación basada en cursor es más estable que la paginación por offset, pero no es mágicamente consistente. Si se insertan o eliminan documentos, o si los campos de ordenamiento mutables cambian entre solicitudes, los usuarios aún pueden ver duplicados o perder elementos. La diferencia es que la paginación por keyset se ancla a valores específicos en lugar de posiciones, por lo que las escrituras concurrentes típicamente causan menos anomalías visibles.
Para feeds y listas donde no se requiere consistencia perfecta, este compromiso es generalmente aceptable.
Paginación en Atlas Search: Un Mecanismo Diferente
Si estás usando MongoDB Atlas Search con la etapa $search, la paginación funciona de manera diferente. Atlas Search utiliza su propio sistema basado en tokens—a través de searchSequenceToken internamente—con parámetros searchAfter y searchBefore en lugar de cursores _id. No mezcles estos enfoques—usa el método de paginación que coincida con tu tipo de consulta.
Elegir el Patrón Correcto
| Escenario | Patrón Recomendado |
|---|---|
| Feeds de scroll infinito | Paginación por keyset |
| Botones de “Cargar más” | Paginación por keyset |
| Tablas de administración con números de página | Offset (solo páginas superficiales) |
| Grandes conjuntos de datos con navegación secuencial | Paginación por keyset |
| Conjuntos de datos pequeños y estáticos | Cualquiera funciona |
La decisión a menudo se reduce a los requisitos de la interfaz de usuario. Si los usuarios necesitan saltar a páginas arbitrarias, estás limitado a paginación por offset o enfoques híbridos. Si la navegación secuencial es suficiente—siguiente, anterior, cargar más—la paginación por keyset escala mejor.
Conclusión
La paginación por offset con skip y limit es simple pero se degrada linealmente con el tamaño del offset. Resérvala para paginación superficial o conjuntos de datos pequeños.
La paginación por keyset mantiene un rendimiento consistente independientemente de la posición, pero requiere ordenamiento determinístico con un desempate único. Es la mejor opción para feeds, listas y cualquier interfaz donde los usuarios naveguen secuencialmente a través de grandes conjuntos de resultados.
Ningún patrón es universalmente incorrecto. Elige según el tamaño de tus datos, los patrones de acceso y los requisitos de tu interfaz de usuario.
Preguntas Frecuentes
Sí, puedes usar múltiples campos de ordenamiento con paginación por keyset. El requisito clave es que tu campo final debe ser único para servir como desempate. Tu índice compuesto debe coincidir con el orden exacto y la dirección de todos los campos de ordenamiento. La lógica de la consulta se vuelve más compleja con cada campo adicional, ya que necesitas condiciones OR anidadas para manejar casos de igualdad para cada campo precedente.
Para la paginación hacia atrás, un enfoque común es invertir tus operadores de comparación y la dirección de ordenamiento. Si hacia adelante usa $lt con ordenamiento descendente, hacia atrás usa $gt con ordenamiento ascendente. Obtén los resultados y luego inviértelos en tu aplicación para mantener el orden de visualización. Almacena los cursores tanto del primer como del último documento de cada página para habilitar la navegación bidireccional.
Si el campo de ordenamiento de un documento cambia mientras un usuario está paginando, puede ver ese documento dos veces u omitirlo completamente dependiendo de si se movió hacia adelante o hacia atrás en relación con su posición de cursor. Para campos actualizados frecuentemente, considera usar valores inmutables como _id como tu clave de ordenamiento principal, o acepta esto como una limitación inherente de los datos en tiempo real.
La paginación por keyset no soporta naturalmente conteos totales porque no rastrea la posición. Puedes ejecutar una consulta de conteo separada, pero esto añade sobrecarga en colecciones grandes. Considera si los usuarios realmente necesitan conteos exactos—a menudo los conteos aproximados o simplemente indicar que existen más resultados proporciona suficiente contexto sin el costo de rendimiento.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.