Posts Tagged ‘optimizaciones’

Paginación optimizada en PHP

septiembre 26, 2008

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