¿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
}
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
}
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
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.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"
},
...
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"
}
}
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?
Mejor rendimiento: No se recorren registros innecesarios. Simplemente lo limitamos en el
where
.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"
},
...
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"
}
},
...
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))
}
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.
Top comments (0)