Este sábado a última hora de la tarde me avisa un cliente al que le llevo el mantenimiento que su web va muy lenta. Sábado, última hora… WordPress lento: Murphy siempre velando por nosotros 😉
Lo primero que hago es verificarlo y efectivamente veo que los tiempos de carga son de hasta 15 segundos… y mirando los posibles problemas veo que la página de inicio pesa más de 15Mb., al final iré a esa parte, el caso es que yendo a otras páginas como por ejemplo la de tienda (es un WooCommerce), con un peso dentro de los normal, el problema sigue igual.
En la web no se había cambiado nada, desde luego no había actualizado a WordPress 5, si había ejecutado algunas actualizaciones de plugins, pero nada que me pareciese significativo, la parte de administración también iba lentísima.
Me pongo en contacto con el hosting, como parece que había saturación en la línea telefónica (Murphy, sigues ahí?), acabo poniendo un ticket.
La primera respuesta de soporte es que hay demasiados IOPs, que active caché (ya hay varias en uso) y que me ponga en contacto con un especialista experimentado en WordPress para optimizar el sitio web y usar menos IOPs (el ticket lo escribo en nombre del cliente).
En este punto y ya pasada la una de la mañana, vuelvo a llamar por teléfono, si, las webs no tienen horario, y desde el servicio técnico del hosting toman nota cuidadosamente de todo lo que les indico y me dicen que pronto me responderán en el ticket.
Finalmente me contestan que el alto consumo de IOPs es debido a consultas a la base de datos, como:
SELECT
t.*, tt.*, tr.object_id, tm.meta_value
FROM
wp_terms AS t
INNER JOIN
wp_term_taxonomy AS tt ON t.term_id = tt.term_id
INNER JOIN
wp_term_relationships AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id
LEFT JOIN
wp_termmeta AS tm ON (t.term_id = tm.term_id
AND tm.meta_key = 'order')
WHERE
tt.taxonomy IN ('product_type' , 'product_visibility',
'product_cat',
'product_tag',
'product_shipping_class',
'pa_color',
'pa_talla',
'pa_talla-calzado',
'yith_product_brand')
AND tr.object_id;
Bien, lo dejamos para la mañana del domingo,que ya son más de las 3…
Ejecutamos la consulta y no veo problema alguno, al ejecutarse en 0.063 segundos. Hora de pasar a la acción con la base de datos.
Optimizando la base de datos
Lo primero que hago después de conectarme al phpMyAdmin es optimizar todas la tablas para eliminar residuos y ver si mejoran los tiempos.
Después desde la opción de MySQL remoto del cPanel añado mi IP pública para conectarme directamente a la base de datos. En esta ocasión utilizo MySQL Workbench y una vez conectado, lo primero que hago es realizar un backup de la base de datos.
A continuación vuelvo a ejecutar la consulta con los mismos tiempos como resultado. Si a la consulta anteponemos EXPLAIN
nos explicará cada uno de los pasos dados para poder ver los posibles problemas, indices a mejorar, etc., pero además podemos decirle que vuelque dichos resultados como JSON con EXPLAIN FORMAT= JSON
Al conectarnos con MySQL Workbench, en la ventana de ejecutar la consulta tenemos un icono para ejecutarla con EXPLAIN
y verlo de forma tabular o gráfica (mediante el EXPLAIN FORMAT= JSON
), en esta última nos marcará en rojo los pasos lentos, naranja los pasos a mejorar (coste de ejecución medio), verde los optimizados, entre otras opciones.
En la consulta mencionada vemos que se podría mejorar, pero no tiene un mal rendimiento (0.063 segundos) como para ralentizar el sitio web.
Bien, vamos a ir viendo otras opciones y después comprobaremos si podemos optimizar más.
Vemos que la tabla wp_term_relationships tiene 42.791 entradas, seguro que hay muchas no utilizados que han ido quedando a lo largo del tiempo, por lo que ejecutamos:
SELECT COUNT(*) FROM wp_term_relationships
LEFT JOIN wp_posts ON wp_term_relationships.object_id = wp_posts.ID
WHERE wp_posts.ID is NULL;
Y vemos, que efectivamente, hay 3.377 entradas huérfanas, así que vamos a eliminarlas (recuerda, copia de seguridad previa de la base de datos y también recuerda poner el prefijo de tablas adecuado):
DELETE wp_term_relationships FROM wp_term_relationships
LEFT JOIN wp_posts ON wp_term_relationships.object_id = wp_posts.ID
WHERE wp_posts.ID is NULL;
Y la consulta debería indicarnos que efectivamente se han eliminado 3.377 filas, así que optimizamos la tabla de nuevo:
OPTIMIZE TABLE wp_term_relationships;
Además observo que hay tablas MyISAM conviviendo con InnoDB, quizás las primeras de la instalación inicial, por cierto, el motor de base de datos es Percona Server. El siguiente paso es convertirlas a InnoDB con el comando para cada tabla:
ALTER TABLE nombretabla ENGINE = InnoDB ;
* Nota del 11/03/2022: podemos obtener el nombre de todas las tablas en formato MyISAM con la siguiente consulta:
SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'nombre_base_de_datos' AND ENGINE = 'MyISAM';
Con el resultado podemos después crear la consulta ALTER TABLE
para convertir todas las tablas en una sola sentencia.
Inspeccionando post_meta
Comenzaremos por una consulta a la tabla de post_meta, esa tabla en la que se guarda información sobre las páginas, posts, productos, CPTs, etc… y vamos a ver que podemos hacer con la misma:
SELECT * FROM wp_postmeta pm LEFT JOIN wp_posts wp ON wp.ID = pm.post_id WHERE wp.ID IS NULL;
Y resulta que nos devuelve nada menos que 7.147 filas de datos post_meta que no corresponden a entradas de la tabla posts, es decir, datos huérfanos, así que vamos a eliminarlos:
DELETE pm FROM wp_postmeta pm LEFT JOIN wp_posts wp ON wp.ID = pm.post_id WHERE wp.ID IS NULL;
Y tenemos el resultado: 7.147 filas afectadas. (La consulta tardó 1,8589 segundos.). ¿Qué borró más filas de las mostradas como vacías? ¿qué falló todo y te quedaste sin base de datos?. No pasa nada, recupera la copia de seguridad de la base de datos y restaura ¿porque tendrás un backup de antes de haber hecho esto, verdad? no será que no te he avisado…
Acordaos de optimizar siempre después de eliminar registros en vuestra tablas.
Ahora vamos a ver una consulta que nos puede brindar una información muy interesante:
SELECT MAX(t1.post_id) AS `post_id`, MAX(t2.post_date) AS `Date`, SUBSTRING_INDEX(t1.meta_key, '_', 1) AS `Meta`, (SUM(LENGTH(meta_id)+LENGTH(post_id)+LENGTH(meta_key)+LENGTH(meta_value)))/1048576 AS `Size MB`, COUNT(*) AS `Count`
FROM wp_postmeta AS t1
JOIN wp_posts AS t2
ON t1.post_id = t2.ID
WHERE meta_key NOT LIKE '\_%'
GROUP BY `Meta`
ORDER BY `Size MB` DESC, `Date` DESC;
Con el siguiente resultado en nuestra base de datos:
Como podemos ver en el resultado, obtenemos datos muy relevantes como los Meta que más entradas tienen, los que más ocupan en la base de datos o los que se han utilizado por última vez.
Vamos con el SQL. En primer lugar vemos que con MAX
seleccionamos el Id más alto de dicho meta, así como la última fecha (MAX
), al final de la consulta si cambiamos el ORDER BY
podemos ordenarlos por dicha fecha, por el número de entradas o por el tamaño (como está en este caso).
A continuación seleccionamos los meta_key
a los que les eliminamos la barra baja _
, esa que añadimos para que no le aparezca el meta al usuario y este será el campo por el agruparemos con el GROUP BY
para poder hacer los cálculos de cada Meta.
Y ahora viene una de las partes más interesantes: hallamos la longitud de los campos meta_id
, meta_key
y meta_value
y los sumamos con SUM
y el resultado lo pasamos a Mb. dividiendo entre 1.048.576 (1024 x 1024). Así sabemos lo que ocupa cada grupo de Meta (no, no estamos hablando de Walter White en Breaking Bad).
Y por último contamos cuantos metas tenemos de cada tipo con COUNT
. Como ya dijimos, agrupamos por los Meta y ordenamos por el campo o campos que queramos, evidentemente realizamos un JOIN
(un INNER JOIN
) entre las tablas posts
y postmeta
(acordaos de los oportunos prefijos de las tablas).
Ahora nos toca a nosotros interpretar correctamente este resultado y ver lo que más ocupa y en ocasiones ya no se utiliza, sopesar si por ejemplo en nuestro caso nos compensa tener Smush que optimiza nuestras imágenes pero nos ha generado más de 24Mb. de datos en nuestra tabla 🙁
Aquí un tirón de orejas para sus desarrolladores, en este caso crearos una tablita para vuestro plugin era una buena opción y no ralentizar una tabla general tan importante al que todos llevan su basura. Los desarrolladores tenemos una gran responsabilidad con como se va a utilizar nuestro código y optimizar para el peor de los casos.
Bueno, lo dicho, con estos resultados ya podemos decidir si borrar entradas con un DELETE
(siempre previo backup y consulta igual con SELECT
para ver cuantas filas se eliminarían).
Plugin para las consultas
Un plugin sumamente útil es Query Monitor que nos indicará las consultas lentas de cada página, número de consultas, quien las realiza, entre otra información de gran interés… pero no os recomiendo ponerlo en producción.
En este caso, como el hosting tiene dicha función, he creado una copia de staging idéntica, instalé el plugin y a partir de ahí localicé algunas consultas problemáticas con un plugin en concreto, que además como ahora no se utilizaba, pasamos inmediatamente a desactivar. Esta acción habría sido sumamente complicada en Live ya que estaba produciendo errores 503 y el aumento de carga aún empeoraría las cosas.
Siempre nos queda la opción de desactivar plugins e irlos activando poco a poco y comprobando en cada paso, pero si ya me pasé unas cuantas horas nocturnas y gran parte del domingo con esto… con la opción de desactivar/activar aún irían unas cuantas horas más.
Una opción sumamente buena, de las mejores que puedes encontrar, para detectar consultas lentas y todo tipo de pruebas es con New Relic, que ya he utilizado con éxito en otras ocasiones.
Podría realizar un clonado de la web a una de prueba de Pantheon que utilizo en ocasiones y desde ahí inspeccionar con la integración de New Relic que ofrecen, pero en esta ocasión no ha sido necesario.
Es una pena que no tuviese acceso al slow log de la base de datos ya que el hosting no da acceso al mismo (supongo que al ser compartido). En un servidor dedicado también podría ir retocando los parámetros de caché de la base de datos, pero esto ya se sale del tema de este post. Eso sí, hay que agradecer al personal del hosting que me hayan copiado las 10 consultas más lentas para que les diese un vistazo.
Las imágenes
Esto es algo que no me canso de repetir… optimizar las fotos, no pongáis una foto de 3000 pixels para mostrarla a 200 px, una foto en png que en jpg ocuparía mucho menos, o ese logo en jpg, que con un png de 32 colores (o incluso de 16) ocuparía 3 veces menos… cada formato tiene unas características y es ideal para un tipo de imagen, este no es el post indicado para deciros el ideal de cada imagen pero si que os voy a dar una regla general:
Que vuestras imágenes NUNCA pesen más de 100 kb. Si pesan más hay que optimizarlas y si hay que romper la regla (en ocasiones hay que hacer excepciones), que no pase de 150 kb., si llega a 200 kb. que sea por un buen motivo y más sólo si es para descarga en alta resolución o algún otro caso fuera de lo común.
Pero además, nunca. nunCA, NUNCA uséis imágenes enlazadas a webs de terceros ya que estaréis consumiendo su ancho de banda, en cualquier momento pueden eliminarla y os estará produciendo un 404 que ralentizará vuestra web o lo que es peor, que la cambien por otra que pese mucho más (varios megas y os pueda tirar vuestra web) o una imagen totalmente inadecuada (me viene a la memoria un caso con un periódico local).
En este caso en la página de inicio se mostraba un slider al final de la misma con las últimas entradas del blog y con su miniatura de imágenes usadas de cerca de 2Mb., además de alguna externa de 2Mb. que para más INRI cargaba muy lentamente.
Optimizar una web lenta, hacer WPO o una intervención puntual no es seguir un manual, sino experiencia, tiempo, paciencia (mucha), conocimientos en diversos campos (SQL, servidores, PHP, HTML, CSS.. si, el CSS puede ralentizar una web más de lo que podamos pensar, javascript…).
Desde luego que tenemos muchísimas más opciones de optimización, comprobar todas las opciones en autoload (programadores, no siempre hay que marcar las opciones como autoload, después todo el mundo dice «¿he sido yo?»), entre otras muchas más opciones, pero para eso siempre podéis buscar un buen profesional de WordPress que lo haga por vosotros, ¿no?
Espero que estas pequeñas pinceladas de una actuación del amigo Murphy os valgan en vuestro día a día.
P.D. (06-02-2022): Para saber el tamaño de las opciones que se cargan en autoload (en MB):
SELECT SUM(LENGTH(option_value))/1048576 AS autoload_size_MB FROM wp_options WHERE autoload='yes';
P.D2. (26-03-2024): Para saber el tamaño de las opciones que se cargan en autoload por orden de tamaño en KB:
SELECT option_name, ROUND((LENGTH(option_value)/1024), 2) AS autoload_size_KB FROM wp_options WHERE autoload='yes' ORDER BY autoload_size_KB DESC;
soy nuevo en eso =( como elimino por ejemplo los registros de wp-smpro-
smush
Hola Marco Antonio, si no tienes unos conocimientos de bases de datos mínimos no te recomiendo que modifiques nada, ya que puedes estropear completamente tu instalación de WordPress.
Si te manejas un poco con las bases de datos, deberás entrar en tu phpMyAdmin, tu hosting te deberá indicar como acceder. Una vez dentro, buscas en la tabla post_meta todas las entradas que contengan una referencia al plugin y marcando dichas filas podrás eliminarlas. Eso si, siempre previa copia de la Base de Datos.
Saludos.