Paginación optimizada en PHP

Leo en SentidoWeb un artículo sobre optimización de paginadores. Mi opinion es que este artículo es un poco exagerado, al decir que el paginado de una busqueda puede consumir más recursos que el resto del sitio. En mi experiencia, un buen paginador no es problema para nada. De hecho, un sitio que mantengo en mi trabajo, cuenta con 2.5 millones de usuarios, tiene millones de mensajes, y la paginación anda perfecto. No es problema de rendimiento.

El problema con los paginadores es que suelen estar mal implementados. En mis comienzos como programador yo hacia algo así:
<?php
mysql_connect('localhost', 'user', 'pass');
mysql_select_db('midb');
function paginar($sql, $res_por_pag, $pag_actual){
// ejecuto el query original, para ver cuantos resultados tengo en total.
// en base a este numero puedo saber la cantidad de paginas.
$rs = mysql_query($sql);
$tot_rows = mysql_num_rows($rs);
// Calculo la cantidad de paginas. Lo hago dividiendo la cantidad de resultados por la cantidad
// de resultados que quiero mostrar por pagina. la funcion ceil redondea hacia arriba
$paginas = ceil($tot_rows/$res_por_pag);
// Calculo los limits. A la pagina actual le resto uno para que cuando se muestre
// en la URL la primera pagina sea la 1 y no la 0.
$inicio = ($pag_actual - 1)*$res_por_pag;
$final = $res_por_pag;
$sql .= " LIMIT $inicio, $final";
return array('sql' => $sql, 'totalPages' => $paginas);
}

// Este ejemplo sería un listado de noticias.

$sql = “SELECT * FROM Noticias”;
// Me fijo que la varibale pag sea un numero valido. sino la pagina actual es la primera
$pag_actual = (isset($_GET[‘pag’]) && is_numeric($_GET[‘pag’]) && $_GET[‘pag’] > 0) ? $_GET[‘pag’]: 1;
// le paso el sql a mi funcion paginadora, le pido que me muestre 20 resultados por pagina,
// y le digo en que pagina estamos ahora
$paginacion = paginar($sql, 20, $pag_actual);

// ahora teniendo esta informacion y el sql con limits, puedo traer solo los que quiero
$rs = mysql_query($paginacion[‘sql’]);
while ($row = mysql_fetch_assoc($rs)){
//muestro las noticias
}
?>

En este caso estoy desperdiciando recursos. ¿Cómo?:

“SELECT * FROM Noticias”

Al hacer ese query le estaría pidiendo a la base de datos (mysql en este ejemplo) que me traiga absolutamente todos los campos de todos los registros de la tabla Noticias. En la función Paginar, pueden ver que lo primero que hago es ejecutar esa consulta para saber la cantidad de registros, y de esa forma saber cuantas paginas voy a tener.

En un sitio que tenga menos de 5.000 noticias no sería problema. Ahora, si Google, que tiene una base de datos con TeraBytes de información, podria tardar días en hacer esa consulta. Esto claramente es un desperdicio de recursos. En este caso si estoy de acuerdo con lo que dice el artículo que menciono arriba. No tendría sentido gastar tantos recursos para mostrar las páginas, cuando podría simplemente poner links de Proximo y Anterior.

Esto es muy eficiente cuando hacemos un sitio para nosotros. Pero la realidad es que la mayoría de los sitios que hacemos son para los clientes. Y a los clientes les gusta que diga cuantas páginas de resultados hay. Personalmente estoy de acuerdo, pero tambien coincido en que no se pueden tirar los recursos. Entonces, ¿qué solución nos queda?

Bueno, esto, como todos los problemas de programación, puede tener varias soluciones, algunas más simples, otras más complejas, con mayor o menor rendimiento. La solución que encontre yo fue la siguiente:

function paginar($sql, $res_por_pag, $pag_actual){
$sqlCount = preg_replace(‘/(^SELECT [\’\”\.\w\d\*\ \,\-\(\)]* FROM)/ims’, ‘SELECT COUNT(*) as total FROM’, $sql);
$sqlCount = preg_replace(‘/(ORDER BY [\’\”\.\w\d\*\s\,\`\-\(\)]* [DESC|ASC]*)/ims’, ”, $sqlCount);
$rs = mysql_query($sqlCount);
$tot_rows = mysql_result($rs, 0, ‘total’);
$pages = ceil($tot_rows / $res_por_pag);
$inicio = ($pag_actual -1)* $res_por_pag;
$final = $res_por_pag;
$sql .= ” LIMIT $inicio, $final”;
return array(‘sql’ => $sql, ‘totalPages’ => $pages);
}

Este reemplazo de la funcion paginar es mucho más eficiente, ya que en vez de traer todos los resultados, solamente traigo el count. En una tabla con una clave primaria, como puede ser un ID, al estar indexado, la función count puede devolver la cantidad de filas en un instante. Ademas, al no traer los resultados, se ahorra tiempo de red y memoria, ya que en el primer ejemplo, al traer todos los datos, estos quedaban guardados en memoria, aunque no los usaba.

Paso a explicar como funciona esto. Lo que hago ees basicamente sacar cualquier campo que se trate de seleccionar, y lo reemplazo por COUNT(*). Tambien saco el ORDER BY, porque podria referirse a alguna función, por ejemplo:

“SELECT ID, CONCAT(Name, LastName) as FullName FROM User ORDER BY FullName”

Despues del primer preg_replace, quedaría asi:

“SELECT COUNT(*) as total FROM User ORDER BY FullName”

FullName, en este caso no es un campo de la tabla, sino que es un campo generado dinámicamente en la consulta. Si dejase este ORDER BY, el servidor devolvería un error diciendo que estoy tratando de ordenar por un campo que no existe. Y ya que el orden no nos importa en este momento (porque esta consulta devuelve un solo registro con un solo campo), lo más simple es sacarlo.

Ese cambio que parece tan simple, duplico el rendimiento del buscador de mensajes.

Sienanse libres de usar este script, distribuirlo y modificarlo a gusto. Agradezco sugerencias para mejorarlo

Etiquetas: , , , , ,

7 comentarios to “Paginación optimizada en PHP”

  1. Wakkos Says:

    Wow!! interesante. Siempre me he hecho un lio con las paginaciones =( algunos diseñadores tenemos que apañarnos con trozos de códigos de aqui y de allá. Mientras mas la gente nos ayude, mejor!!!

    saludos!

  2. matcod86 Says:

    Es curioso como muchos utilizamos la consulta “SELECT * FROM XXX” para contar la cantidad de registros, casi automáticamente y sin pensar en lo que estamos haciendo. Muchas gracias por la función!

  3. Ricardo Lopez Says:

    PAra que no tengas que hacer el count php maneja la instruccion mysql_num_rows()

  4. seba Says:

    Igual tendria que hacer 2 queries. 1 para saber el total de filas, y otra para traer las paginas. Si hago el mysql_num_rows() sobre el query con limit, no me va a dar el total de registros sino el total de registros para esa pagina. Si hago el query sin limit para hacer el mysql_num_rows(), estoy trayendo TODOS los registros con todos los campos. Es mucho mas liviano tirar el COUNT(*), porque traes menos datos y lo resuelve solo la base de datos. Ademas, no dependes de que sea MySQL

  5. topito411 Says:

    Yo tengo una página bastante parecida a la tuya con la diferencia que hice dos funciones para que haga un slide de las flechas, si le das al 1 primer registro y retrocedes vas al último.

    /*—————————————————-Funcion resto——————————————-*/

    function resta($max,$page)//tomo numero maximo de registros y el numero de pagina
    {
    $page1=$page-1; //resto
    if ($page1==0)//comparo numero de pagina no igual a cero porque el minimo es 1 si es 0 retorno el max.
    {
    return $max;
    }
    else
    {
    return $page1;
    }
    }

    /*———————————————————-Funcion Sumo—————————————*/
    function suma($max,$page)//tomo numero maximo de registros y el numero de pagina
    {
    $page1=$page+1; //sumo
    if ($page1>$max)//comparo que el numero de pagina no sea superior al maximo sino lo pongo en 1
    {
    return 1;
    }
    else
    {
    return $page1;
    }
    }
    /*————————————————————————————————————*/

  6. topito411 Says:

    desde el principio el programa quedaría así
    //primero obtenemos el parametro que nos dice en que pagina estamos
    $page = 1; //inicializamos la variable $page a 1 por default
    if(array_key_exists(‘pg’, $_GET)){
    $page = $_GET[‘pg’];
    //si el valor pg existe en nuestra url, significa que estamos en una pagina en especifico.
    }
    //ahora que tenemos, en que pagina estamos obtengamos los resultados:
    // a) el numero de registros en la tabla
    $mysqli = new mysqli(“localhost”,”root”,””,”noticias”);
    if ($mysqli->connect_errno) {
    printf(“Connect failed: %s\n”, $mysqli->connect_error);
    exit();
    }

    $conteo_query = $mysqli->query(“SELECT COUNT(titulo) as conteo FROM noticia WHERE titulo != ””);
    $conteo = “”;
    if($conteo_query){
    while($obj = $conteo_query->fetch_object()){
    $conteo =$obj->conteo;
    }
    }
    $conteo_query->close();
    unset($obj);

    $limite=2;
    /*————————————————————————————————————-
    Ahora dividimos el conteo por el numero de registros que queremos por pagina.
    Ponemos ceil a diferencia de int porque el ceil redondea para arriba, en caso del que sea 3/2 el cociente es 1,5 si dejamos el entero el número nos dá 1, esto hace que quede en el registro que ya imprimió y lo traslade a la segunda página cuando en realidad tiene que mostrar otros registros diferentes motivo por el cual seutiliza el ceil, porque tiene que pasar al limite inmediatamente superior, para que esto no ocurra.
    Recordemos que la función ceil redondea hacia arriba.
    Que cáculo utilizo el de la cuenta del total de registros encontrados divido el límite
    ————————————————————————————————————-*/

    $max_num_paginas=ceil($conteo/$limite);

    $segmento = $mysqli->query(“SELECT titulo, categoria, autor,fecha,noticia FROM noticias.noticia WHERE titulo != ” ORDER BY fecha ASC LIMIT “.(($page-1)*$limite).”, $limite “);

    //ya tenemos el segmento, ahora le damos output.

    if($segmento){

    echo “”;
    echo ‘
    Titulo
    Categoria
    Autor
    Fecha.
    Noticia.
    ‘;

    while($obj2 = $segmento->fetch_object())
    {
    echo ‘
    ‘.$obj2->titulo.’
    ‘.$obj2->categoria.’
    ‘.$obj2->autor.”;
    echo ”.$obj2->fecha.”;
    echo ”.”.$obj2->noticia.”.”;
    echo “”;
    }
    echo ”;
    }

  7. topito411 Says:

    se aprovecha de la recursivad del propio php que puede llamarse así mismo por eso suena medio raro la primera parte donde esta el isset de una variable que no existe por eso la pagina se inicializa en uno, se aprovecha el mysqli porque esta orientado a objetos directamente por eso después las variables son tratadas como objetos y se cierra la base
    $limite es el número de la cantidad de registro que le queremos poner, si quieren 10 $limite=10, yo le puse dos porque hice esto para probar si funcionaba la base, después con css se puede modificar la presentación de letras y la cantidad, también al tener el número de la pagina se puede optimizar poniendo el numero de pagina, hay otros que lo han hecho con Java o con AJAX esto es php puro, sin necesidad de ningun plugin o script aparte del uso del php, desde ya gracias, y si alguno puede encontrar una mejora con gusto acepto críticas,
    En cuanto a los registros que muestro es de una base de datos alternativa que hice para probar los registros, esto se puede hacer en forma de clases o de una funcion especifica que serviría para cualquier base de datos que es lo que hace el ajax, en cuanto a los campos de texto largo se utilizó un textarea redonly para mostrar los datos de la base.

Deja un comentario

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s


Seguir

Recibe cada nueva publicación en tu buzón de correo electrónico.

A %d blogueros les gusta esto: