# Paginación eficiente en bases de datos

**Categoría:** Bases de datos · **Nivel:** Avanzado · **Lectura:** 20 min
**Publicada:** 2026-07-28 · **Actualizada:** 2026-07-28 · **Autoría:** Underc0de
**Versión HTML (canónica):** https://underc0de.org/guias/bases-de-datos/paginacion-eficiente-en-bases-de-datos/

## Respuesta rápida

Mostrar resultados **de a páginas** (paginar) es habitual, y la forma clásica —`LIMIT` para decir cuántas filas y `OFFSET` para saltar las anteriores— funciona bien… **hasta que la tabla crece**. El problema del **OFFSET** es sutil: para darte la página 5000, la base de datos tiene que **recorrer y descartar todas las filas anteriores** —las primeras 5000 × N filas— antes de devolverte las que querés. Cuanto más lejos está la página, **más filas descarta en balde**, así que la página 1 vuela pero la página 5000 se arrastra: el coste **crece con el número de página**. Es trabajo desperdiciado que empeora sin límite. La solución es la **paginación por cursor** (o *keyset*): en vez de decir «saltá las primeras 50 000 filas», usa un **puntero al último elemento visto** y pide «dame las siguientes filas *a partir de este valor*». Por ejemplo, en lugar de «página 5000», dice «los siguientes 20 productos *con id mayor que el último que te di*». Como la condición es una **comparación** (mayor que un valor) y no un salto, la base de datos —**con un índice** sobre la columna de orden— va **directo** al punto de partida sin recorrer nada, y el coste es **constante**: la página 5000 es tan rápida como la 1. La contrapartida: la paginación por cursor solo permite ir **hacia adelante (o atrás) secuencialmente** —«siguiente» y «anterior»—, no **saltar a una página arbitraria** («ir a la página 5000»). ¿Cuándo usar cada una? **OFFSET** sirve para tablas pequeñas o cuando necesitás saltar a páginas concretas y numeradas (y el conjunto no es enorme). La **paginación por cursor** es la correcta para conjuntos grandes y para el patrón de *scroll infinito* o «cargar más», donde solo se avanza. El factor que hace rápida a cualquiera de las dos —y crítico para el keyset— es el [índice](../indices-en-bases-de-datos-como-aceleran-las-consultas/index.md) sobre la columna por la que se ordena y pagina: sin él, ni el cursor salva el rendimiento. Entender *por qué* el OFFSET se degrada es lo que lleva a elegir bien.

## OFFSET y su problema

La forma clásica de paginar en SQL combina dos cláusulas: **`LIMIT`**, que dice *cuántas* filas devolver (el tamaño de página), y **`OFFSET`**, que dice *cuántas saltar* antes de empezar. Para la página 3 con páginas de 20, sería «saltá 40, dame 20». Es intuitivo y funciona perfectamente en tablas pequeñas o en las primeras páginas. El problema aparece con **tablas grandes y páginas lejanas**, y es un problema de **eficiencia** que mucha gente no ve venir hasta que su aplicación se ralentiza en producción.

> **El OFFSET descarta trabajo en balde**
>
> La clave está en *cómo* la base de datos ejecuta un `OFFSET`. Para darte la fila número 100 001, no puede «ir directo» ahí: tiene que **generar y recorrer las 100 000 filas anteriores** en orden, **descartarlas una a una**, y recién entonces empezar a devolverte las que pediste. Es decir, hace **todo el trabajo de las páginas anteriores** solo para tirarlo. Cuanto más grande es el `OFFSET`, más filas recorre y descarta inútilmente, así que el coste **crece linealmente con el número de página**: la página 1 es instantánea, la página 100 va bien, la página 10 000 se arrastra, y la página 100 000 puede tardar segundos. Es un rendimiento que **se degrada sin límite** a medida que el usuario avanza —o, peor, a medida que la tabla crece—. Este comportamiento es el mismo en todos los motores y es inherente a cómo funciona el `OFFSET`: no es un fallo, es su naturaleza.

## Paginación por cursor

La **paginación por cursor** (o *keyset*) resuelve el problema cambiando la pregunta. Comparación de las dos formas:

| Aspecto | OFFSET | Cursor (keyset) |
|---|---|---|
| Cómo pide la página | «Saltá N filas y dame las siguientes» | «Dame las siguientes a partir de este valor» |
| Coste según la página | Crece con el número de página | Constante: siempre igual de rápido |
| Navegación | Salto a cualquier página numerada | Solo siguiente / anterior (secuencial) |
| Depende de índice | Ayuda, pero no evita el recorrido | Crítico: es lo que lo hace rápido |

> **Atención**
>
> La idea del keyset es reemplazar el *salto* por un *puntero*. En vez de «saltá las primeras 100 000 filas», el cursor recuerda el **valor del último elemento entregado** (por ejemplo, el `id` o la fecha de la última fila de la página anterior) y pide «dame las siguientes filas **cuyo valor sea mayor que ese**». La diferencia de rendimiento viene de que esa condición es una **comparación** (`WHERE id > :ultimo`), y una comparación sobre una columna **indexada** permite a la base de datos **ir directamente** al punto de partida usando el índice, como quien abre un diccionario por la letra correcta en vez de leerlo desde la A. No recorre ni descarta nada: salta al sitio y devuelve las filas. Por eso el coste es **constante** —la página que está «5000 pantallas más adelante» se sirve igual de rápido que la primera—. El requisito imprescindible es un [índice](../indices-en-bases-de-datos-como-aceleran-las-consultas/index.md) sobre la columna por la que se ordena y compara; sin él, la base tendría que recorrer igualmente, y perderíamos la ventaja. También hace falta que el orden sea **estable y único** (a menudo se combina la columna de orden con el identificador para desempatar), o algunas filas podrían repetirse o saltarse entre páginas.

## Cuándo cada una

Ninguna es «mejor» en absoluto: cada una encaja en un caso. La decisión depende del **tamaño** y del **tipo de navegación**:

| Situación | Paginación recomendada |
|---|---|
| Tabla pequeña o pocas páginas | OFFSET: simple y suficiente |
| Necesitás saltar a páginas numeradas concretas | OFFSET (si el conjunto no es enorme) |
| Conjunto grande, solo se avanza | Cursor (keyset) |
| Scroll infinito / «cargar más» | Cursor (keyset): encaja perfecto |
| API que sirve muchos datos secuenciales | Cursor (keyset) |

> **Atención**
>
> La paginación por cursor tiene una **limitación** que decide cuándo *no* usarla: solo permite moverse **secuencialmente** —«siguiente» y «anterior»—, porque su puntero apunta al *final de la página actual*. No puede responder a «llevame directo a la página 5000», porque no sabe cuál es el valor de puntero de esa página sin haber pasado por las anteriores. Por eso, si tu interfaz necesita **numeración de páginas** con salto directo («1, 2, 3 … 5000»), el keyset no encaja de forma natural, y ahí el OFFSET —con sus límites— sigue siendo la opción, siempre que el conjunto no sea gigantesco. La buena noticia es que el patrón dominante en la web moderna —el **scroll infinito** y el botón «cargar más»— es exactamente *avance secuencial*, así que el keyset encaja de maravilla y es la elección correcta para APIs y listados grandes. La regla práctica: si solo se avanza (o se avanza y retrocede) por un conjunto grande, **cursor**; si se necesita saltar a páginas concretas de un conjunto manejable, **OFFSET**. Y en ambos casos, el [índice](../indices-en-bases-de-datos-como-aceleran-las-consultas/index.md) sobre la columna de orden es lo que marca la diferencia de rendimiento.

## Errores frecuentes

- **Usar OFFSET en tablas enormes con páginas lejanas.** El coste crece con el número de página; para conjuntos grandes, keyset.
- **Creer que un índice arregla el OFFSET.** Ayuda, pero el OFFSET sigue recorriendo y descartando filas.
- **Paginar por cursor sin índice sobre la columna de orden.** Sin él, se pierde toda la ventaja de rendimiento.
- **Ordenar por una columna no única sin desempate.** Filas repetidas o saltadas entre páginas; combinar con el identificador.
- **Intentar salto a página arbitraria con keyset.** No lo soporta; para eso está el OFFSET.
- **Paginar sin un ORDER BY estable.** Sin orden fijo, las páginas pueden mezclarse; el orden debe ser determinista.
- **No medir dónde empieza el problema.** Conviene ver a partir de qué página el OFFSET se degrada en tu caso.

## Preguntas frecuentes

**¿Por qué la paginación con OFFSET se vuelve lenta?**
La paginación con desplazamiento u offset se vuelve lenta en tablas grandes y en páginas lejanas porque, para devolver una página determinada, la base de datos tiene que recorrer y descartar todas las filas anteriores a esa página antes de empezar a entregar las que se piden, de modo que el coste crece a medida que aumenta el número de página. Para entenderlo hay que ver cómo funciona el desplazamiento. La paginación clásica combina una cláusula que indica cuántas filas devolver, que es el tamaño de la página, con una cláusula de desplazamiento que indica cuántas filas saltar antes de empezar a devolver resultados. Por ejemplo, para obtener la tercera página con páginas de veinte filas, se indica que se salten cuarenta filas y se devuelvan las siguientes veinte. El problema está en cómo ejecuta la base de datos ese salto. Para poder saltar las primeras filas y llegar a la página solicitada, la base de datos no puede ir directamente al punto de partida, sino que tiene que generar y recorrer en orden todas las filas anteriores, descartándolas una a una, y solo cuando ha descartado tantas filas como indica el desplazamiento empieza a devolver las filas de la página pedida. Esto significa que hace todo el trabajo correspondiente a las páginas anteriores únicamente para tirarlo. La consecuencia es que el coste de obtener una página crece linealmente con el número de página, ya que cuanto más lejana es la página, mayor es el desplazamiento y más filas hay que recorrer y descartar inútilmente. Así, la primera página se obtiene de forma prácticamente instantánea, las primeras páginas van bien, pero a medida que se avanza el rendimiento se degrada, hasta el punto de que una página muy lejana, como la número diez mil o cien mil, puede tardar segundos en obtenerse. Este rendimiento se degrada sin límite a medida que el usuario avanza en la paginación o a medida que la tabla crece en tamaño. Es importante entender que este comportamiento no es un fallo ni un problema de configuración, sino que es inherente a cómo funciona el desplazamiento en todos los motores de bases de datos, y por tanto no se soluciona simplemente añadiendo un índice, ya que aunque el índice pueda ayudar, el desplazamiento sigue requiriendo recorrer y descartar las filas anteriores. La solución a este problema es cambiar el enfoque de la paginación, utilizando la paginación por cursor, que evita ese recorrido innecesario.

**¿Qué es la paginación por cursor o keyset?**
La paginación por cursor, también conocida como paginación por keyset, es una técnica de paginación que, en lugar de saltar un número de filas para llegar a una página, utiliza un puntero al último elemento entregado y pide las siguientes filas a partir de ese valor, lo que le permite ir directamente al punto de partida sin recorrer ni descartar las filas anteriores. La idea central de esta técnica es reemplazar el salto propio del desplazamiento por un puntero. En la paginación por desplazamiento, para obtener una página lejana hay que saltar todas las filas anteriores, lo que obliga a recorrerlas y descartarlas. En la paginación por cursor, en cambio, se recuerda el valor del último elemento que se entregó en la página anterior, por ejemplo el identificador o la fecha de la última fila, y para obtener la siguiente página se piden las filas cuyo valor sea mayor que ese último valor recordado, junto con el límite de cuántas filas devolver. De este modo, en lugar de decir que se salten muchas filas, se dice que se devuelvan las siguientes filas a partir de un determinado punto. La ventaja de rendimiento proviene de que la condición utilizada es una comparación, es decir, pedir las filas cuyo valor sea mayor que uno dado, y una comparación sobre una columna que esté indexada permite a la base de datos ir directamente al punto de partida usando el índice, del mismo modo que se abre un diccionario por la letra correcta en lugar de leerlo desde el principio. Así, la base de datos no recorre ni descarta ninguna fila anterior, sino que salta directamente al lugar y devuelve las filas solicitadas, con un coste que es constante e independiente de lo avanzada que esté la página, de manera que una página muy lejana se sirve tan rápido como la primera. Para que esta técnica funcione correctamente y con buen rendimiento son necesarias dos condiciones. La primera es que exista un índice sobre la columna por la que se ordena y compara, ya que sin él la base de datos tendría que recorrer las filas igualmente y se perdería la ventaja. La segunda es que el orden sea estable y único, para lo cual a menudo se combina la columna de orden con el identificador de la fila para desempatar, ya que si el orden no fuera único, algunas filas podrían repetirse o saltarse entre páginas. La paginación por cursor tiene la limitación de que solo permite avanzar y retroceder de forma secuencial, pero es la solución idónea para conjuntos grandes y para patrones como el desplazamiento infinito.

**¿Cuándo debo usar OFFSET y cuándo paginación por cursor?**
Se debe usar la paginación por desplazamiento cuando la tabla es pequeña o se necesita saltar a páginas numeradas concretas en conjuntos que no sean enormes, y la paginación por cursor cuando el conjunto de datos es grande y la navegación es secuencial, es decir, cuando solo se avanza o se avanza y retrocede, como en el desplazamiento infinito o el botón de cargar más. Ninguna de las dos técnicas es mejor que la otra en términos absolutos, sino que cada una encaja mejor en unos casos según dos factores principales, que son el tamaño del conjunto de datos y el tipo de navegación que se necesita. La paginación por desplazamiento es adecuada en varios casos. Cuando la tabla es pequeña o el número de páginas es reducido, su problema de rendimiento no llega a manifestarse, por lo que es una opción simple y suficiente. Y cuando se necesita permitir saltar directamente a páginas numeradas concretas, por ejemplo ofrecer una interfaz con números de página que permita ir directamente a la página cinco mil, el desplazamiento es la opción natural, siempre que el conjunto de datos no sea gigantesco, ya que en ese caso el problema de rendimiento sería inevitable. La paginación por cursor es adecuada en otros casos. Cuando el conjunto de datos es grande y la navegación consiste en avanzar secuencialmente, la paginación por cursor mantiene un rendimiento constante independientemente de lo avanzada que esté la posición, evitando la degradación del desplazamiento. Y cuando el patrón de navegación es el desplazamiento infinito o el botón de cargar más, tan habitual en las aplicaciones web y móviles modernas, la paginación por cursor encaja perfectamente, ya que ese patrón consiste precisamente en avanzar de forma secuencial cargando más resultados a partir del último visto, que es justo lo que hace el cursor. También es la opción idónea para las interfaces de programación que sirven grandes cantidades de datos de forma secuencial. La diferencia clave que determina la elección es que la paginación por cursor solo permite moverse secuencialmente, con siguiente y anterior, pero no saltar a una página arbitraria numerada, ya que su puntero apunta al final de la página actual y no sabe cuál sería el puntero de una página lejana sin haber pasado por las anteriores. Por ello, si la interfaz necesita numeración de páginas con salto directo, hay que recurrir al desplazamiento, mientras que si solo se avanza por un conjunto grande, la opción correcta es el cursor. En ambos casos, además, el índice sobre la columna por la que se ordena y pagina es fundamental para el rendimiento.

**¿Por qué son tan importantes los índices al paginar?**
Los índices son fundamentales al paginar, y especialmente en la paginación por cursor, porque son lo que permite a la base de datos ir directamente al punto de partida de la página sin tener que recorrer las filas una a una, siendo por tanto la clave del rendimiento, hasta el punto de que sin un índice adecuado la paginación por cursor pierde su ventaja. Un índice es una estructura que la base de datos mantiene sobre una o varias columnas y que le permite localizar rápidamente las filas según los valores de esas columnas, de forma análoga a como el índice de un libro permite encontrar directamente la página donde se trata un tema sin leer todo el libro. En el contexto de la paginación por cursor, la técnica consiste en pedir las filas cuyo valor en la columna de orden sea mayor que el último valor entregado, lo que es una comparación. Cuando existe un índice sobre esa columna de orden, la base de datos puede usar ese índice para ir directamente a la posición correspondiente al valor buscado y empezar a devolver las filas desde ahí, sin necesidad de recorrer ni examinar las filas anteriores, lo que da lugar al rendimiento constante que caracteriza a esta técnica. En cambio, si no existe un índice sobre la columna de orden, la base de datos no puede localizar directamente el punto de partida y se ve obligada a recorrer las filas para encontrar dónde empezar, lo que reintroduce el mismo tipo de recorrido costoso que se pretendía evitar, anulando la ventaja de la paginación por cursor. Por ello, la existencia de un índice apropiado sobre la columna por la que se ordena y pagina no es un detalle opcional, sino un requisito imprescindible para que la paginación por cursor funcione con el rendimiento esperado. En el caso de la paginación por desplazamiento, los índices también ayudan, ya que pueden acelerar la ordenación y el acceso a los datos, pero no resuelven su problema fundamental, que es la necesidad de recorrer y descartar las filas anteriores, por lo que un índice no evita la degradación del rendimiento del desplazamiento en páginas lejanas. Además, para que la paginación por cursor sea correcta, el índice y el orden deben garantizar un orden estable y único, a menudo combinando la columna de orden con el identificador de la fila, para evitar que filas con el mismo valor se repitan o se salten entre páginas. En resumen, los índices son la pieza que hace posible el rendimiento eficiente de la paginación, especialmente de la paginación por cursor, y diseñarlos correctamente sobre las columnas de ordenación es esencial para que la paginación de grandes conjuntos de datos sea rápida.

**¿La paginación por cursor permite ir a una página concreta?**
No, la paginación por cursor no permite ir directamente a una página concreta o arbitraria, ya que solo permite moverse de forma secuencial, avanzando a la página siguiente o retrocediendo a la anterior, y esta es precisamente su principal limitación y el factor que determina cuándo no conviene usarla. La razón de esta limitación está en cómo funciona la técnica. La paginación por cursor se basa en un puntero que apunta al último elemento entregado en la página actual, de modo que para obtener la siguiente página pide las filas a partir de ese valor. Esto significa que el cursor conoce el punto de partida de la siguiente página porque es el final de la actual, y de forma análoga puede conocer el de la anterior, permitiendo así avanzar y retroceder. Sin embargo, el cursor no puede saber cuál sería el valor del puntero correspondiente a una página lejana y arbitraria, como la página cinco mil, sin haber pasado antes por todas las páginas intermedias, ya que ese valor depende de los datos concretos que hay en las páginas anteriores. Por tanto, la paginación por cursor no puede responder a una petición de ir directamente a una página numerada concreta, porque carece de la información necesaria para situarse en ella sin recorrer las anteriores, lo que contradiría su propia ventaja de no recorrer filas. Esta limitación tiene una consecuencia práctica clara a la hora de elegir la técnica de paginación. Si la interfaz de la aplicación necesita ofrecer una navegación con números de página que permita saltar directamente a cualquier página, como una barra de paginación tradicional con los números uno, dos, tres y así hasta la última página, la paginación por cursor no encaja de forma natural, y en ese caso hay que recurrir a la paginación por desplazamiento, asumiendo sus limitaciones de rendimiento, siempre que el conjunto de datos no sea gigantesco. En cambio, la buena noticia es que el patrón de navegación dominante en las aplicaciones web y móviles modernas es el desplazamiento infinito o el botón de cargar más, que consiste precisamente en avanzar de forma secuencial cargando más resultados, sin necesidad de saltar a páginas concretas. Este patrón encaja perfectamente con la paginación por cursor, que es por tanto la elección idónea para las interfaces modernas de listados grandes y para las interfaces de programación que sirven datos secuenciales. En resumen, la imposibilidad de saltar a páginas arbitrarias es el precio que se paga por el rendimiento constante de la paginación por cursor, y determina que se use cuando la navegación es secuencial y no cuando se requiere salto directo a páginas numeradas.

**¿Necesito un orden estable para paginar correctamente?**
Sí, es imprescindible un orden estable y determinista para paginar correctamente, tanto con desplazamiento como con cursor, porque si el orden de las filas no es fijo y único, las páginas pueden mezclarse y algunas filas pueden repetirse o saltarse entre una página y otra, dando resultados incorrectos. La paginación consiste en dividir un conjunto ordenado de resultados en páginas sucesivas, lo que presupone que existe un orden claro y consistente entre las filas. Si no se especifica un orden mediante la cláusula correspondiente, la base de datos puede devolver las filas en un orden no garantizado, que además podría variar entre distintas ejecuciones de la consulta, lo que hace que la paginación no tenga sentido, ya que no habría una noción fija de qué fila va antes o después y, por tanto, de qué filas corresponden a cada página. Por ello, siempre hay que paginar sobre un resultado explícitamente ordenado. Pero además de estar ordenado, el orden debe ser estable y único, es decir, determinista, lo que significa que ante valores iguales en la columna de ordenación debe existir un criterio de desempate que garantice un orden inequívoco. El problema surge cuando se ordena por una columna que puede tener valores repetidos, como una fecha en la que varias filas comparten el mismo valor. En ese caso, el orden entre las filas con el mismo valor no está determinado y puede variar, lo que provoca que al paginar algunas de esas filas puedan aparecer en dos páginas distintas, repitiéndose, o no aparecer en ninguna, saltándose, ya que la frontera entre páginas cae en medio de un grupo de filas con el mismo valor cuyo orden interno no es fijo. La solución a este problema es garantizar un orden único añadiendo un criterio de desempate, lo que habitualmente se consigue combinando la columna de ordenación principal con el identificador único de la fila, de modo que cuando dos filas tienen el mismo valor en la columna principal, el identificador determina cuál va antes, asegurando así un orden totalmente determinista. Esto es especialmente crítico en la paginación por cursor, ya que el puntero se basa en el valor de ordenación, y si ese valor no es único, el cursor no puede situar correctamente el punto de partida de la siguiente página. Por tanto, para paginar correctamente hay que ordenar siempre de forma explícita y garantizar que ese orden sea estable y único, combinando la columna de orden con el identificador cuando la primera pueda tener valores repetidos, lo que evita las repeticiones y los saltos de filas entre páginas y asegura una paginación consistente y correcta.

## Fuentes

Documentación oficial y material de la comunidad consultados para esta guía. Fecha de consulta: 28 de julio de 2026.

### Aportes de la comunidad Underc0de

1. **Underc0de, foro.** [Bases de datos](https://underc0de.org/foro/bases-de-datos/). Rendimiento de consultas.
2. **Underc0de, blog.** [Blog de Underc0de](https://blog.underc0de.org/). Optimización de bases de datos.

### Documentación oficial

1. **PostgreSQL.** [LIMIT y OFFSET](https://www.postgresql.org/docs/current/queries-limit.html). Paginación de resultados.
2. **Use The Index, Luke.** [No Offset](https://use-the-index-luke.com/no-offset). Paginación por keyset.
3. **Oracle (MySQL).** [LIMIT Optimization](https://dev.mysql.com/doc/refman/8.0/en/limit-optimization.html). Optimización de LIMIT.
4. **ISO/IEC.** [SQL Standard](https://www.iso.org/standard/76583.html). El estándar del lenguaje SQL.

## Guías relacionadas

- [Índices en bases de datos](../indices-en-bases-de-datos-como-aceleran-las-consultas/index.md)
- [Consultas SQL avanzadas](../consultas-sql-avanzadas-group-by-y-agregaciones/index.md)
- [Introducción a SQL](../introduccion-a-sql-desde-cero/index.md)
- [Claves primarias](../claves-primarias-y-foraneas-en-bases-de-datos/index.md)
- [Índice de Bases de datos](../index.md)
