DEV Community

Cover image for Paginación con cursor
Gaston Duarte
Gaston Duarte

Posted on

Paginación con cursor

¿Cuando utilizar paginación?

Al desarrollar aplicaciones que muestran grandes volúmenes de datos, es fundamental implementar técnicas de paginación para mejorar el rendimiento y la experiencia del usuario. En un e-commerce, por ejemplo, podría haber miles de productos en la base de datos, pero el frontend solo necesita mostrar 10 a la vez.

Enviar todos los registros al frontend para que los almacene en memoria no es una solución eficiente. En su lugar, el backend debe gestionar la paginación de manera efectiva.

Tipos de paginación

Existen dos enfoques principales para paginar datos: offset-based pagination y cursor-based pagination.

offset-based pagination

Este método utiliza un offset para determinar desde qué registro comenzar la consulta.

Veamos un ejemplo simple:

Supongamos que tenemos 25 registros en nuestra base de datos, y tenemos una vista que quiere mostrar esos registros de a 10.

Entonces el frontend nos enviara:

{
  "limit": 10,
  "offset": 0
}
Enter fullscreen mode Exit fullscreen mode

El backend hará una consulta a la base de datos:

SELECT * FROM nombre_tabla LIMIT 10 OFFSET 0

Es decir, mostrar 10 registros desde el registro 0 (del 0..9).
Para obtener los siguientes 10 registros:

{
  "limit": 10,
  "offset": 10
}
Enter fullscreen mode Exit fullscreen mode

Consulta SQL:

SELECT * FROM nombre_tabla LIMIT 10 OFFSET 10

Eso mostrara desde el registro 10 al 19 y así sucesivamente.

Problemas de offset-based pagination

  1. Baja eficiencia en grandes volúmenes de datos: Consultas con OFFSET grande pueden volverse costosas, ya que la base de datos debe recorrer muchos registros antes de devolver los deseados.

  2. Inconsistencias en datos dinámicos: Si se agregan o eliminan registros, la paginación puede omitir o duplicar registros.

Cursor-based pagination

Aquí es donde aparece la paginación con cursor, la cual hace que nuestras consulta a la base de datos sean mucho mas performantes.

¿Cómo funciona?

En vez de enviar el campo offset, utilizaremos un campo cursor.

¿Y como definimos cual es nuestro cursor?

Bien, la respuesta es depende. Depende de qué datos tenemos guardados en nuestro registro, vayamos al caso mas simple, tener un identificador único auto-incremental.

Supongamos que nuestro registro tiene estos datos:

{
  "id": 1,
  "nombre": "Juan"
},
{
  "id": 2,
  "nombre": "Patricia"
},
...
Enter fullscreen mode Exit fullscreen mode

El frontend solicita el primer registro con limit=1 (sin cursor) y el backend responderá ademas del registro a mostrar cual es el cursor que debe enviar en la siguiente request:

{
  "limit": 1,
  "cursor": 2
  "user":{
       "id": 1,
       "nombre": "Juan"
  }
}
Enter fullscreen mode Exit fullscreen mode

Consulta a la base de datos:

SELECT * FROM nombre_tabla LIMIT 1

En la siguiente request que haga el frontend enviara, limit=1 y cursor=2, entonces desde el backend podremos hacer una consulta a la base de datos de este estilo:

SELECT * FROM nombre_tabla WHERE id >= 2 ORDER BY id LIMIT 1

Lo cual traerá a partir del registro que contenta id >= 2 y solamente 1.

¿Cual es la ventaja sobre offset?

  1. Mejor rendimiento: No se recorren registros innecesarios. Simplemente lo limitamos en el where.

  2. Mayor consistencia: No se ven afectados registros por insert o delete.

Ahora, ¿Qué sucede si no tenemos un id auto-incremental y ordenado?

Si los registros tienen un UUID en lugar de un ID incremental, se puede utilizar otro campo como cursor, por ejemplo, fecha_nacimiento:

Por ejemplo, supongamos registros de este estilo:

{
  "uuid": "asdn1029nc",
  "nombre": "Juan",
  "fecha_nacimiento": "2003-02-21"
},
{
  "uuid": "sap0238gh",
  "nombre": "Patricia",
  "fecha_nacimiento": "2002-11-04"
},
...
Enter fullscreen mode Exit fullscreen mode

Para utilizar cursor debemos ordenar nuestros registros por algún campo, en este caso la fecha de nacimiento, entonces ahora si el frontend nos pide un registro en la primer request, desde el backend devolveremos:

{
  "limit": 1,
  "cursor": "2002-11-04"
  "user":{
       "uuid": "sap0238gh",
       "nombre": "Patricia",
       "fecha_nacimiento": "2002-11-04"
  }
},
...
Enter fullscreen mode Exit fullscreen mode

Nuestra consulta a la base de datos será:

SELECT * FROM nombre_tabla WHERE fecha_nacimiento > '2002-11-04' ORDER BY fecha_nacimiento LIMIT 1

¿Y que sucede si hay dos registros con la misma fecha, vamos a perder registros?

Bueno, ahi es donde podemos concatenar dos campos del registro para utilizarlo como cursor para asegurarnos de no perder registros, por ejemplo: cursor=fecha_nacimiento+uuid. Importante siempre en la consulta hacer un order by cursor, fecha_nacimiento.

Seguridad: Encodear el cursor

Es importante utilizar un encode en Base64 de nuestro cursor para evitar un sql injection.
Este puede ser un ejemplo de código en Go para encodear el cursor:

func DecodeCursor(encoded string) (string, string, error) {
    data, err := base64.StdEncoding.DecodeString(encoded)
    if err != nil {
        return "", "", err
    }
    parts := strings.Split(string(data), "|")
    if len(parts) != 2 {
        return "", "", fmt.Errorf("cursor inválido")
    }
    return parts[0], parts[1], nil
}

func EncodeCursor(creationDate string, reportID string) string {
    cursorData := fmt.Sprintf("%s|%s", creationDate, reportID)
    return base64.StdEncoding.EncodeToString([]byte(cursorData))
}
Enter fullscreen mode Exit fullscreen mode

Conclusión

Si bien offset-based pagination es sencilla y funciona bien con pocos registros, cursor-based pagination es mucho más eficiente para grandes volúmenes de datos y evita inconsistencias. Dependiendo del caso, se puede utilizar un ID incremental o una combinación de campos como cursor para garantizar un correcto orden y rendimiento.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay