Métodos de paginación desde backend
Publicado por javier el jueves, 14 de julio de 2022Preludio
Durante el desarrollo de un nuevo proyecto, cualquier desarrollador fullstack se ha enfrentado a la misma pregunta. ¿Cómo afrontamos la paginación?. En la mayoría de los casos la respuesta nos viene ya definida desde el framework que estemos utilizando. Sin embargo, las soluciones implementadas no son siempre las más eficientes, y establecen una serie de trade-offs que debemos saber. En este artículo repasaremos los diferentes métodos de paginación posibles en una base de datos relacional tradicional y cuando utilizar cada uno.
Métodos de paginación
Paginación Límite - Sesgo
Este método es el más común y el que tienen implementado por defecto frameworks conocidos de backend como Spring framework o Laravel. También es el más sencillo de implementar.
Este método consiste en utilizar dos parámetros para acotar la paginación, límite y sesgo. Con el límite indicamos el número de elementos a mostrar en la página. Con el sesgo, indicamos a partir de que registro de la tabla queremos empezar a devolver elementos.
La implementación más común se materializa en número de página y tamaño de página. El sesgo entonces es calculado como num_pagina * tam_pagina
y el límite es el tamaño de página.
Una petición que implemente este método tendría esta forma:
GET api/v1/resources?page=2&page_size=10
En SQL la consulta generada sería:
SELECT * FROM resources ORDER BY id LIMIT 10 OFFSET 20
Donde 10
sería el tamaño de página y 20
el sesgo generado de la tercera página 2 * 10 = 20
. En esta implementación concreta la primera página tendría numero 0.
Aunque esta es la implementación más sencilla y la más usada en soluciones genéricas, presenta un dos importantes desventajas que la hacen inviable para determinados casos.
-
Bajo rendimiento en sesgos elevados. A la hora de aplicar la directiva OFFSET, el sistema de gestión de bases de datos que utilicemos (MySQL, Oracle, Postgres, …) debe recorrer las primeros registros de la tabla una por una, contando hasta llegar al número indicado. Por lo que se vuelve una operación muy costosa a medida que se van peticionando páginas cercanas al final de la tabla.
-
Deriva de página. Supongamos el siguiente caso: un usuario ha cargado la página 3 y, mientras está visualizándola, un segundo usuario ha dado de alta un elemento que por id se añade al principio de la tabla, en la página 0. Cuando el primer usuario pase a la página 4, los elementos habrán quedado desplazados y este verá el último elemento de la página 3 repetido como el primer elemento de la página 4.
Este problema no se da si el id o la columna por la cual ordenamos es autoIncremental y tenemos la certeza de que todos los elementos generados se añadirán al final de la consulta. Este no es el caso si utilizamos UUIDs.
Paginación por clave
La paginación por clave utiliza campos de la último registro de la página para obtener el siguiente de forma más eficiente a nivel de base de datos.
Un campo que se suele utilizar mucho es la fecha, ya sea de creación o edición. No obstante sirve cualquier campo por el cual queramos ordenar nuestra consulta de elementos. Un ejemplo de petición sería:
GET api/v1/resources?startDate=2018-01-03&limit=10
Que se trasladaría a la una consulta SQL de la siguiente manera:
SELECT * FROM resources
WHERE created_at <= '2018-01-03'
ORDER BY created_at, id
LIMIT 10
Este diseño no sufre del problema de consistencia que tiene la paginación Límite - Sesgo discutida anteriormente y posee un rendimiento constante sin importar el número de página solicitado. No obstante, se presentan otras desventajas a tener en cuenta.
-
Se produce un alto acoplamiento entre el mecanismo de paginación y el filtrado. El usuario de la api deberá utilizar siempre filtros para obtener una respuesta.
-
Solo podremos acceder a páginas contiguas, puesto que deberemos tener la información del último elemento de la página para pasar a la siguiente, o del primero para pasar a la anterior.
Seek Method o paginación por búsqueda
Siguiendo con la misma técnica de paginación por clave, esta variante solventa algunos problemas de acoplamiento que tiene esta. En este caso, la API siempre espera un Id para después buscar y extraer los campos necesarios para paginar.
Para una petición como la siguiente
GET api/v1/resources?startId=5&limit=10
Si utilizasemos solo el Id quedaría la siguiente consulta.
SELECT * FROM resources
WHERE id > 5
ORDER BY id
LIMIT 10
En caso de querer ordenar la lista por otro campo deberemos realizar dos consultas.
SELECT @start_date = created_at FROM resources
WHERE id = 5;
SELECT * FROM resources
WHERE created_at >= @start_date
ORDER BY created_at, id
LIMIT 10;
Este método es el más complejo de implementar, pero mantiene la eficiencia constante a la vez que resulta más intuitivo para el consumidor de la API. Aún así, mantenemos la limitación de poder acceder únicamente a páginas contiguas.
¿Cuándo utilizar cada uno?
No existe una solución óptima a este problema, cada método de acceso a la información proporciona unas ventajas sobre otras.
La más común para colecciones de tamaño pequeño - medio es la paginación Límite - Sesgo. Esta solución permite obtener páginas arbitrarias sin tener que empezar por el principio o el final, y en la mayoría de los casos, tanto la deriva de página como el bajo rendimiento no son perceptibles o relevantes para proporcionar una buena experiencia de usuario. Si una colección se muestra al usuario y crece a partir de cierto límite es común que la aplicación requiera de filtros adicionales que acotarán la colección amortiguando el impacto de rendimiento, ya que para el usuario también es complicado manejar o navegar por una gran cantidad de páginas.
Sin embargo, la paginación límite sesgo no siempre es adecuada. Un ejemplo es un proceso que requiera cargar para procesar de forma secuencial una gran cantidad de datos, es posible que necesitemos paginar la información para procesarla por lotes. Si cargamos la información mediante paginación Límite - Sesgo, cada petición de página será más costosa de la anterior, aumentando el tiempo de respuesta hasta niveles inaceptables. Un proceso de integración de datos puede pasar de durar horas a minutos utilizando una paginación por clave en este caso.
También debemos tener en cuenta la naturaleza del dominio de nuestra aplicación, para un catálogo de productos que no cambia demasiado, la deriva de página es tolerable y el poder saltar entre páginas no contiguas es una característica deseable. Por lo que una paginación Límite - Sesgo sería la mejor opción.
Si tu aplicación maneja información generada de forma más frecuente, y dicha información pierde importancia conforme pasa el tiempo, probablemente la paginación por búsqueda sea la mejor opción, ya que será suficientementemente eficiente para mostrar grandes cantidades de elementos y mantendrá la consistencia entre páginas a pesar de las frecuentes actualizaciones. Un ejemplo de este caso son la time line que encontramos en twitter o las publicaciones de linkedin.
Conclusiones
Tras repasar los diferentes algoritmos de paginación, queda patente de que no todo se reduce a una única solución correcta.
Cada herramienta a nuestra disposición nos proporcionará una serie de características, ventajas y desventajas. Conocer estas características es importante a la hora elegir la mejor herramienta para conseguir nuestro objetivo.
También debemos tener en cuenta que si bien no siempre la solución más sencilla es la más adecuada, tampoco lo es la más compleja.
Como ingenieros software gran parte de nuestro trabajo consiste en lidiar con la creciente complejidad. A la hora de diseñar un sistema debemos tener claro cuando es necesario implementar una lógica más compleja y cuando mantener la lógica sencilla nos será suficiente. Para ello es crucial tener en mente las necesidades del producto.