Posts tagged as ‘mysql’

Generar sitemaps de millones de páginas

Escrito en Tutoriales

Los creadores de sortea2 siempre hemos tenido que tratar con el mismo problema en todas las webs que hemos tenido que realizar: generar los sitemaps.

En qué consiste un sitemap

Un sitemap no es más que un fichero XML en el que ponemos todas las URLs que tiene nuestro sitio web con el fin de que Google, Bing o el robot que quiera lo lea y sea capaz de indexar todo el sitio mucho más fácilmente de lo que lo haría normalmente.

A estas URLs les podemos dar además una prioridad, que es un número decimal de entre 0 y 1 que puede servirle como dato orientativo para el motor de búsqueda. No tiene lógica ninguna darle prioridad 1 a todas las páginas con la idea de que nuestras páginas indexen mejor porque el sistema no va así. Está hecho para diferenciar importancias entre diversas secciones del sitio web y cosas así. Luego será el propio motor de búsqueda el que determine qué vale la pena más o menos.

Un sitemap en su forma más simple es algo como esto:

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.google.com/schemas/sitemap/0.84">
<url>
  <loc>http://www.sortea2.com</loc>
  <priority>0.6</priority>
</url>
</urlset>

 

Dentro de cada <url> meteremos en <loc> el link en sí y luego su prioridad. Dentro del urlset irán todas las URLs.

Un sitemap así será fácil de generar con cualquier lenguaje que queramos (PHP, python, perl, etc.), puesto que simplemente es meter en un bucle todas las URLs e ir poniéndolas en ese formato XML.

El problema viene en que existe un límite de URLs que puede tener un sitemap. El límite está en 50.000 páginas y 10MB como máximo.

En este caso habrá que dividir los sitemaps en el máximo e ir separándolos mediante Sitemap indexs. Esto no es más que otro XML con la ruta de cada sitemap al que habrá que acceder.

Un Sitemap Index tiene la siguiente estructura:

<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.google.com/schemas/sitemap/0.84">
<sitemap>
<loc>http://www.sortea2.com/sitemap/sitemap1.xml</loc>
</sitemap>
<sitemap>
<loc>http://www.sortea2.com/sitemap/sitemap2.xml</loc>
</sitemap>
</sitemapindex>

 

Hasta aquí la breve explicación de en qué consisten los sitemaps.

Proceso habitual para generarlos

Habitualmente los generaremos mediante una tarea programada a una hora a la que veamos que nuestro tráfico baja considerablemente y ésta se encargará de escribir todo lo que sea necesario.

En PHP tenemos el problema de que por defecto los servidores suelen capar el máximo tiempo de ejecución del script a 30, 60 o 90 segundos, y si vamos a generar una cantidad grande de sitemaps implicando consultas de MySQL complicadas lo más normal es que nos quedemos cortos.

Para este problema habría que poner al principio del todo del script: set_time_limit(0); para que se le permita tardar todo lo que necesite. Aún así habrán servidores que tendrán fijado un tiempo máximo y no dejará de ninguna de las maneras cambiarlo.

Otra opción bastante viable puede ser usar Python o Perl; lenguajes mucho más veloces que PHP y que suelen venir instalados en hostings compartidos.

Primero se generarían los distintos sitemaps, cortando cada archivo cuando se llegue al límite y al final del todo se creará un índice de sitemaps conteniendo las URLs de todos los que acabamos de generar.

La manera de implementarlo ya variará dependiendo de cómo lo vayamos a hacer.

Consejos para generar miles de ellos

Si tienes que enfrentarte a la generación de una cantidad enorme de sitemaps, te convendrá tener en cuenta algunos consejos:

  • Escribe de golpe en los archivos con fwrite() pero sin usar flush(). De esta manera solo utilizaremos la memoria indispensable en cada momento.
  • Utiliza tablas temporales de MySQL cuando haya que hacer consultas complejas: una buena práctica que podemos necesitar es crear una tabla temporal. Si la consulta que va a generar el sitemap implica a muchas tablas, agrupaciones, eliminación de duplicados, etc. te puede convenir crear una tabla temporal que contenga simplemente los ids de los elementos a los que acceder de tal manera que luego las consultas que se hagan sean directas.

    Con este sistema el mal trago solo pasará una vez al crear la tabla temporal y aún así se ejecutará mucho más rápido que la consulta en sí porque no necesita generar ningún cursor. Las consultas directas sobre esa tabla serán tan simples que serán inmediatas.

    Lo óptimo es borrar la tabla temporal tan pronto como se pueda.

  • Parte en trozos la consulta que vas a tener que hacer: no es recomendable coger todos los datos de golpe e introducirlos en un cursor. Si tienes un millón de registros esa operación es inviable, porque tendrá que gastar una cantidad brutal de memoria (dependiendo de los datos) que realmente no va a valer para nada, porque cada vez solo vamos a leer un dato. Además, si tenemos que realizar esta consulta inmensa junto con algún tipo de ORDER BY, DISTINCT o algo así, es muy probable que agotemos la memoria interna de MySQL y nos dé un error interno grave de Incorrect key file for table ‘/tmp/#sql_7cd7_0.MYI’; try to repair it que quiere decir que no se ha podido generar la tabla temporal necesaria.

    La manera de partir en cachos el resultado de la consulta es meterla dentro de un while() que vaya cogiendo trozos del tamaño que veamos (50.000, 100.000 o lo que sea) con un LIMIT y que pare cuando ya no haya más datos.

    Así se irán escribiendo las URLs usando no demasiada memoria y se leerán de inmediato gracias a la tabla temporal.

  • Escribe todos los sitemaps olvidando saltos de línea y tabulaciones: Google no te va a premiar por escribir tus sitemaps completamente bien estructurados y legibles. Escríbelo todo en una sola línea continua con el menor número de espacios posible y así ahorrarás espacio en disco.

    Estamos hablando de miles de sitemaps, por lo que la diferencia si puede ser notable. En pocos sitemaps esto sería absurdo e innecesario.

  • Se pueden comprimir usando GZIP: si lo consideras oportuno puedes comprimir tus sitemaps usando GZIP, de esta manera ocuparán mucho menos y ahorrarás ancho de banda.

    Es de libre elección hacerlo o no. Por un lado tiene la ventaja de que ahorra disco duro y ancho de banda pero por otro tiene la desventaja de que te costará muchísimo más el generarlos porque la compresión tardará también lo suyo.

    Se pueden generar todos normalmente y luego lanzar un proceso que vaya comprimiendo uno a uno. No habría problema ninguno y serían perfectamente válidos.


Etiquetas: , ,
Escrito por .

10 consejos para optimizar MySQL

Escrito en Tutoriales

En un proyecto web medianamente ambicioso llega siempre el momento en que la parte «sagrada» de la aplicación es la Base de Datos. El problema reside cuando las tablas de nuestra Base de Datos empiezan a tener más de 10.000 registros. A partir de ese momento debemos pensar de diferente manera: los accesos deben optimizarse, el sistema de búsqueda ya es demasiado lento y a veces llega incluso al límite de tiempo de ejecución, ya no se puede hacer un ALTER TABLE sin tener que tener la tabla bloqueada unos segundos, etc. Cuando nos empezamos a encontrar con estos problemas es cuando no hemos hecho una predicción suficientemente buena y debemos actuar rápidamente para remediarlo.

Optimizar MySQL es una tarea que debemos tener en mente desde el minuto cero, aunque sin obsesionarse. Siempre es mejor avanzar que quedarse atascado.

A continuación os mostraré 10 consejos que pueden acelerar los accesos a vuestras tablas:

  1. Usa índices:

    Un índice es un sistema informático que permite al gestor de base de datos (MySQL) acceder a la información (registros de una tabla) de una forma más directa que si tuviera que ir dato a dato comprobando si cumple las condiciones.

    Los campos que solamos incluir dentro del «WHERE» de las consultas más comunes deben estar indexadas o bien controladas.

    Los accesos vía PRIMARY KEY son, con diferencia los más veloces. De hecho, cuando utilizamos el motor InnoDB, los datos se encuentran físicamente ordenados por el valor de la clave primaria. Esto hace que acceder a los datos sea trivial.

    Si sabemos que un determinado registro no va a poderse repetir, debemos incluirle un índice UNIQUE y si vamos a realizar búsquedas sobre un campo de fecha por ejemplo, le podemos añadir un INDEX simple.

    Siempre que indexamos un campo disminuimos la velocidad de escritura en la tabla porque cada vez que la manipulamos MySQL tiene que asegurarse de actualizar el índice. Hay que encontrar el equilibrio que permita a la aplicación funcionar rápidamente. Si una tabla recibe muchas modificaciones quizás haya que sacrificar algo de velocidad en las consultas.

    Otro dato a tener en cuenta es que MySQL utiliza mayoritariamente índices B-Tree, en el cual se va creando un árbol con las posibles combinaciones. Por ejemplo, en un índice compuesto por los campos (nombre, apellido1, apellido2) será agrupado primero por nombre, luego por apellido1 y luego por apellido2. Una consulta que busque por apellido2 únicamente no utilizaría el índice. En cambio, una consulta por nombre sería rápida. Una consulta consultando por nombre, apellido1 y apellido2 sería bastante óptima también porque podríamos recorrer el índice de arriba a abajo para encontrar el dato.

  2. Utilización de campos binarios

    Los tipos de dato binario son BINARY, VARBINARY y BLOB que van en consonancia con CHAR, VARCHAR y TEXT. Si el texto que vas a almacenar no va a tener una codificación como UTF-8 es mucho más conveniente utilizar campos binarios, puesto que ocupan mucho menos espacio y las comparaciones se hacen como datos numéricos internamente.

    Por ejemplo, campos que guarden cadenas codificadas como MD5, campos de permalink (URL amigable para buscadores), nombres de usuario que sepamos que no permiten caracteres extraños, etc. pueden ser puestos como binarios. Por cada unidad de información utilizaremos menos bytes y por lo tanto el motor de almacenamiento de MySQL podrá manejar los datos con más soltura.

  3. Utiliza UNSIGNED

    Si al crear una tabla, sabes que un campo solamente contendrá valores numéricos positivos, utiliza UNSIGNED. Esto aumenta al doble la capacidad máxima de los campos sin alterar el número de bytes necesarios para almacenar cada dato. Por ejemplo, todos los campos que utilicen un AUTO_INCREMENT deben utilizarlo, puesto que de la otra manera tendremos un rango enorme de números que jamás utilizaremos.

    En aplicaciones que manejen gran cantidad de datos es posible que un campo INT se nos quede corto para almacenar un AUTO_INCREMENT. Si estuviera UNSIGNED habríamos tenido un campo con el doble de vida útil.

  4. Guarda información redundante

    Con esto nos estaríamos saltando la normalización de las tablas. Pero la realidad es que en las aplicaciones web lo más común es que por cada inserción de datos tengamos muchas más consultas de datos. Lo habitual es que la información cambie cada cierto tiempo pero que sea consultada a todas horas.

    Puede ser útil tener campos calculados. Por ejemplo, total de comentarios en una entrada, número de opiniones sobre un producto, ID del último comentario, etc.  Cada vez que se realice una modificación en esas tablas tendremos que hacer más trabajo, pero normalmente compensa.

  5. No uses tipos de datos gigantes

    Me atrevería a decir que el 99% de las personas jamás necesitarán un campo LONGTEXT a no ser que se trate de un gestor de contenidos o algo por el estilo; incluso habrá muy pocas que necesiten un campo BIGINT para uso normal. Un BIGINT ocupa cada número 8 bytes y puede almacenar un número de un 9 seguido de 18 ceros (aproximadamente). El campo LONGTEXT almacena hasta 4 billones y pico de caracteres, que pueden ser mucho más de 50 páginas de Word por ejemplo.

    Los tipos de dato exageradamente grandes ocupan más espacio en disco y rara vez se utilizan por completo. Es por eso que es más recomendable usar los campos adecuados sin pasarse demasiado.

  6. Trata de usar los simplificadores del lenguaje

    MySQL proporciona algunas maneras que ayudan a hacer las consultas más cortas y más fáciles de entender. Estos métodos no solo nos ayudan a nosotros, sino que internamente son mucho más eficientes. Por ejemplo, usar la expresion " WHERE sorteo = 1 OR sorteo = 2 OR sorteo = 5" es mucho menos eficiente que la siguiente: " WHERE sorteo IN(1,2,5)".

    Algunas de estas expresiones pueden ser BETWEEN, IN, LEAST, GREATEST o INTERVAL.

    Ten en cuenta además de que cuanto más cortas sean las consultas, menos bytes ocuparán y menos tardará el servidor de MySQL en recibirlas. Sobre todo en conexiones remotas.

  7. Intenta unificar consultas

    Intenta siempre hacer una consulta cuando normalmente lo harías con varias. INSERT puede insertar varios registros al mismo tiempo si se separan los «VALUES» por paréntesis y comas, por ejemplo: INSERT INTO sorteo VALUES (1,2, 3),(4,5,6),(7,8,9).

    DELETE múltiples se pueden construir en vez de en bucle insertando los valores a borrar dentro de un IN() de MySQL.

    También es posible modificar varios registros desde una consulta. Para ello tendremos que utilizar una sintaxis un tanto peculiar, pero que puede ser útil. Podéis ver esa sintaxis en el siguiente enlace: http://stackoverflow.com/a/84111

    Es útil sobre todo para modificaciones masivas a través de un script.  Por ejemplo, un script que descargue imágenes para cada producto y luego actualice el campo «imagen» de la tabla «producto». Ir uno a uno modificando cada registro sería muy lento e intensivo. Podríamos agrupar las modificaciones de 10 en 10 registros para aliviar la carga de la base de datos.

    Cada vez que mandamos una consulta tenemos que contar con el tiempo de ping, que es lo que tarda el cliente de mysql en «llegar» al servidor. Si tenemos un ping de 10ms ninguna consulta tardará menos de 10ms porque es el tiempo mínimo. Si unificamos 100 consultas en una, esos 10ms solo los tenemos que perder una vez.

  8. Evita operaciones matemáticas

    No es buena idea hacer cálculos complejos en una SELECT. Siempre que sea posible hay que traer los datos al lenguaje que utilices (PHP por ejemplo) y hacer estos cálculos. Por ejemplo, realizar redondeos, medias, divisiones y demás puede resultar problemático y hay que tener siempre en mente que cuanto menos trabajo haga la Base de Datos, más rápida será.

    Normalmente las únicas operaciones matemáticas que compensan son las asociadas a un GROUP BY.

  9. Utiliza MyISAM si es posible

    Un denominador común en las aplicaciones web es que casi siempre se hacen más selects que inserts/updates/deletes. Si este es nuestro caso, podemos renunciar a las posibilidades relacionales que permite InnoDB para tener un gestor más rápido en las consultas.

    Si recibimos muchas consultas de manipulación de datos MyISAM es insostenible y deja corruptas las tablas con frecuencia. Debemos encontrar el equilibrio. Para un blog MyISAM es ideal. Para un bróker online en tiempo real no lo es.

  10. Utiliza el PROCEDURE ANALYSE() de MySQL

    Esta es una característica muy desconocida de MySQL pero de gran utilidad. Se añade tras una consulta y lo que hace es procesarla y devolver estadísticas sobre cada campo seleccionado. Dice máximos, mínimos, medias y lo más importante, propone el tipo de campo óptimo para cada columna.

    Pruébalo tú mismo, ejecuta SELECT * FROM mytabla PROCEDURE ANALYSE(); y comprueba los resultados.

    Ten en cuenta que a nada que haya menos de 100 posibles valores para un campo ya va a proponerte que utilices un ENUM. No siempre esto será aplicable a tu aplicación.

    Este procedimiento es bastante desconocido pero es muy útil sobre todo cuando tenemos que empezar a dar soporte a tablas creadas por otras personas. Es posible que así identifiquemos campos que en realidad no se usan, tipos de datos que son erróneos, etc.

En definitiva todos los puntos se refieren a que hay que tener control total de los datos que tenemos almacenados, siempre teniendo en cuenta las limitaciones y tratando de mantener una integridad de los datos.

Fuentes: MySQL Certification Study Guide, Building Scalabe Web Sites (The Flickr way).


Etiquetas: , , ,
Escrito por .