Paginación de registros V 1.0

Published Fri, Aug 6 2004 13:42

Ayer jueves 5 de agosto, luego de una decepcionante charla que presenciamos junto a unos amigos (Leonardo Garcés y Luís Hereira), decidimos ir a pasar los malos ratos vividos al lugar tradicionalmente utilizado para celebrar, el ShopDog de Pedro de Valdivia con Providencia u 11 de septiembre.

La conversación derivó en variados temas, y después de algunas discusiones bizantinas, caímos en la eterna discusión sobre métodos de paginación. Yo presenté el esquema que actualmente utilizamos en IConstruye. 

Nuestro sistema de paginación funciona de la siguiente forma. Es necesario tener un campo identity y utilizar cache. Una consulta a paginar típica tiene los siguientes elementos

Select
      campos
From
      tablas
Where
      cláusulas

La idea consiste en dividir esta consulta en dos consultas, una que ejecute principalmente la parte del where y la segunda que ejecute principalmente la parte del select y los campos.

Con la primera consulta acotamos a una lista de ids (identitys) de los documentos que cumplen con la restricción where (por eso digo que principalmente ejecuta el where), y almacenamos nuestros N ids en un caché. Si consideramos que cada int pesa 4bytes y almacenamos 500 elementos, tenemos 2kilos (mas algunos bytes de estructura y otros) en un caché. No es mucho. Es mejor que almacenar todo en un caché.

Con la segunda consulta, le pedimos una página cualquiera de x registros. Con esto obtengo los x ids moviéndome sobre el arreglo de enteros almacenado en el cache. Con los ids, ejecuto la segunda consulta, que es mucho mas pesada en el select (y liviana en el where), pero como el resultado son sólo los documentos que quiero, se minimiza bastante el trafico de información que no va a ser considerada después. Traigo así SOLO los x registros de la página solicitada. Para cualquier otra página, ya tengo los ids almacenados en el cache así que no es necesario ejecutar la primera consulta.

Luís menciona que mi sistema está representado por la ecuación Y = m*X + b, es decir una recta con pendiente m y que parte en b. Pueden ver la discusión escrita en la servilleta en la imagen asociada. El costo b es el de la consulta 1 (la de los ids) y por cada pagina X se le pondera por un costo m. Yo argumento que m tiende a cero ya que no hay diferencia significante entre la primera y la última página.


Servilleta de la discusión 

Cuando la discusión se torna inútil nuevamente, Luís propone su método de paginación, obtenido desde http://www.winnetmag.com/Article/ArticleID/40505/40505.html.

Este método se basa en dos consultas, una de ellas anidada en la otra. Un seudo SQL sería algo como

SELECT TOP page_size *
FROM table
WHERE primary_key NOT IN
      (SELECT TOP page_size * (page_number - 1) primary_key FROM table WHERE filter_conditions ORDER BY sort_field)
AND filter_criteria
ORDER BY sort_field

El argumento mío no se deja esperar. Siempre haces dos consultas. Su respuesta, buena en todo caso, es que las primeras páginas son muy rápidas de cargar (por que el select anidado da Top 0 para el primer caso) y que el usuario nunca avanza muchas páginas. Es cierto, pero también ocurre a veces que usuarios llegan hasta altas páginas y ya deja de funcionar bien. En IConstruye utilizamos ese mismo sistema durante un tiempo y habían usuarios que llegaban hasta la página 27.

Cuando la página es muy alta, se pone cada vez mas lento. Este sistema es mas parecido a una exponencial. Verán en la imagen la exponencial y su ecuación ex. Interesante es encontrar el punto donde ex se junta con m*x+b.

¿Cual es mejor?. Dependerá de los requerimientos. Probablemente el sistema perfecto sea una mezcla de ambos, tal como existe el HybridDictionary, que tiene un comportamiento dual dependiendo de la cantidad de elementos. También recuerdo dentro de los algoritmos de ordenamiento, donde el QuickSort era muy rápido, pero para pocos elementos era muy lento. Si se hacia un algoritmo hibrido entre QuickSort y BubbleSort (creo que era el otro), para pocos elementos funcionaba bien el Bubble y para muchos, el Quick.

by pmackay

Comments

# pmackay said on Wednesday, August 11, 2004 7:37 PM

Me sumo a tu idea y además me parece que sería bueno incluso si uno la implementa utilizando el patrón Strategy, se aplica cundo se tiene un conjunto de algoritmos para solucionar un mismo problema de esa forma entonces alguien podría utilizar sin mucho esfuerzo distintas variantes de paginación de hecho podría incluso hasta generar una clase colocada en la UIP que se especialice en la paginación y determine cual criterio aplicar.

Esto teniendo en cuenta que Patrick habla de un algoritmo yo menciono otro pero en realidad existen algunos más que recuerde en éste momento he visto uno basado en tablas temporales, otro en RowCount, etc. Cada uno tiene distintos niveles de respuesta y posibilidad de aplicación en dependencia de la solución y la necesidad planteada.

# pmackay said on Wednesday, August 11, 2004 7:57 PM

Luisón, me aparecen las siguientes dudas:

¿Es la UIP la encargada de determinar el criterio?
¿Eso implicaría que la UIP debe conocer todos los criterios?. ¿No es responsabilidad de datos tomar esa decisión?

De los algoritmos, por supuesto que hay mas.

Lo que a mi no me gusta de las tablas temporales es que producen un efecto similar a la segunda metodología ya que hay mucha copia de datos.

# pmackay said on Friday, August 27, 2004 1:55 PM

Me parece que si es la UIP pero la parte de presentación directamente, la parte de proceso de presentación, en la capa de datos no creo pues no sabe para que se quieren los datos y puede ser un elemento importante a tener en cuenta a la hora de decidir el algoritmo.

# pmackay said on Wednesday, April 27, 2005 3:54 PM

Hola Patrick, en relación al artículo Paginación de registros V2.0, se me presenta la duda de como haces cuando un usuario visita más páginas y no tienes las claves cargadas en la cache. Es decir en el artículo hablas de tener 501 claves e ir cargando registros de 50 en 50 a cada petición, pero que ocurre si un usuario solicita la página 11, 12, etc. como actualizas la cache de claves?

Escribí un correo a través de la página preguntando lo mismo.
Disculpa las molestias.
Sergio

# pmackay said on Wednesday, April 27, 2005 4:38 PM

Sergio,

había visto tu mensaje, pero no habia tenido oportunidad de contestarlo.

C/R a tu pregunta, no tengo un respuesta probada funcionando, pero podrías complementarlo con un sistema de bloques de páginas, en donde almacenas en caché los 500 ids de cada bloque de 10 páginas. Si el usuario llega a la página 11 podrías buscar los nuevos 500 ids. Otra forma es obtener solo 250 ids nuevos y descartar los 250 de las primeras 5 paginas, quedandote con 10 paginas, de la 6 a las 15.

Esto puede funcionar sin problemas para las primeras páginas, pero a medida que avanzas hacia adelante irremediablemente se pondrá mas lento el obtener estas "super paginas" ya que tendrías que utilizar un algoritmo TSQL del tipo "select where not in (select)", el cual es muy lento.

Te puedo preguntar, que necesidad hay de llegar mas allá de 500 registros?. Nosotros no lo hemos necesitado. Por eso quiero conocer tu necesidad.

Saludos.
Patrick.

# pmackay said on Thursday, April 28, 2005 3:41 AM

Hola Patrick

El problema se plantea para cualquier aplicación windows de gestión. El usuario está acostumbrado a tener la típica botonera de Primero, Anterior, Siguiente y Ultimo para navegar por los registros.
El volumen máximo de registros para ciertas tablas (pedidos, albaranes, ...) será >50000, clasificadas por empresa, ejercicio, serie... Donde en un momento dado un usuario al entrar a ver un listado puede trabajar con 2000-8000 registros para luego como bien indicas mirar los 40 o 50 primeros registros y salir, pero aun así debo tener la posibilidad de que quiera posicionarse en cualquier registro que se encuentre fuera de los límites de la caché, por ejemplo por que anda buscando alguno en concreto que no está entre los que busca.

No se me ocurre ninguna forma eficiente que solucione el problema. Ahora que estoy haciendo pruebas la cache de Ids es del total de registros (3000-8000) y no va mal. Como ventaja al tener en cache todos los Id's siempre puedo cargar la página que contiene el registro que anda buscando sin ningún problema. No obstante no termina de convencerme.

Gracias por contestar
Sergio

# pmackay said on Thursday, April 28, 2005 10:45 AM

Voy a tomar una frase tuya y mostrarte un camino que a lo mejor no has notado. Tu dices "por ejemplo por que anda buscando alguno en concreto que no está entre los que busca".

Por que no entonces le das opciones de filtro, varias opciones para que pueda acotar sus resultados?.

Nosotros tenermos entre 6 y 10 filtros por busqueda, entonces, mi defensa es que con ese nivel de filtrado y los 500 registros que retorna, DEBE poder encontrar cualquier cosa. Sinó es así, el usuario no sabe lo que anda buscando y ahi entramos en otro problema.

Patrick.

# pmackay said on Tuesday, May 03, 2005 6:16 AM

Hola Patrick

Ante la respuesta a mi pregunta no tengo ninguna duda de que tienes razón, muchas veces (por no decir siempre) el usuario no sabe bien lo que anda buscando. Así que lo enfocaré de la forma que me indicas.

Gracias.

Otra cosa, tengo dudas de cómo debo descargar de memoria un dataset que hayamos cargado (Por ejemplo el primero con 25 registros) al releer otra página de datos (otros 25 registros). Lo comento porque aunque haga lo siguiente:

objdataset.clear()
objdataset.dispose

previamente antes de recargar de nuevo con los siguientes 25 registros el consumo de memoria sube, y a cada nueva página o registro que me desplace sube y sube, nunca baja o se mantiene. Este comportamiento de la memoria es normal o estoy haciendo algo mal. He leido algunos artículos sobre este tema pero al probarlos ninguno hace que la memoria se libere. Entiendo que el GC no actua en el mismo instante en el que el objeto ya ha salido de ámbito, pero aun así me parece excesivo el consumo de memoria.

En VB6 cuando cargabas un objeto en memoria y después lo liberabas el consumo bajaba, pero en .NET no da esa sensación.

Algún consejo?


Gracias de antemano.

# pmackay said on Tuesday, May 03, 2005 10:39 AM

Sergio,

la utilización de memoria en .net sigue una regla muy simple. Si el equipo tiene memoria disponible todavia, la usa. El framework no libera memoria cuando no es necesario. El garbage collector no limpia inmediatamente los registros marcados a nothing, de hecho, no necesario hacerlos = nothing.

Tu debieras ver que la memoria utilizada sube, pero en algun momento dejará de subir y se mantendrá estable. Rara vez verás que baje, a menos que tengas otro proceso corriendo que la necesite. Es una lucha entre las aplicaciones y el sistema operativo.

Patrick.