La Basura Oculta en tu Base de Datos
El misterio de la web que no debería ser lenta
Escenario: Tienes un servidor decente (8GB RAM, SSD NVMe), un CDN configurado, imágenes optimizadas, caché activado, lazy loading implementado, y tu WordPress está actualizado. Has hecho todo “bien”.
Y aún así, tu web tarda 4-6 segundos en cargar el backend. El Time to First Byte (TTFB) es deplorable. Los usuarios se quejan. Google PageSpeed te da un 45 en mobile.
Has revisado todo. Dos veces. El problema persiste.
Bienvenido al infierno silencioso de la tabla wp_options.
La autopsia técnica: Un caso real
Recientemente diagnosticamos una web de e-commerce que cumplía este perfil exacto. Cliente frustrado, tres agencias anteriores sin solución, hosting premium de €180/mes, y tiempos de carga inaceptables.
# La consulta que reveló el horror
SELECT COUNT(*) FROM wp_options;
# Resultado: 847,392 filas
Casi 850,000 registros en una tabla que debería tener entre 200-400 filas.
El siguiente comando fue aún más revelador:
SELECT
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS "Size (MB)"
FROM information_schema.TABLES
WHERE table_name = "wp_options";
# Resultado: 1,847 MB (1.8 GB)
Una sola tabla pesaba más que toda la instalación de WordPress.
¿Qué demonios es wp_options?
La tabla wp_options es el corazón de configuración de WordPress. Almacena:
- Configuraciones del sitio (URL, título, tagline)
- Opciones de plugins y temas
- Widgets activos
- Configuraciones de caché
- Transients (aquí está el problema)
Es una tabla de autoload, lo que significa que WordPress carga su contenido en memoria en CADA página cargada, incluso en el backend.
La anatomía de un registro normal:
option_id | option_name | option_value | autoload
----------|--------------------------|--------------|----------
1 | siteurl | https://... | yes
2 | home | https://... | yes
45 | active_plugins | a:12:{...} | yes
Simple, limpio, necesario.
La anatomía del caos:
option_id | option_name | option_value | autoload
----------|------------------------------------------------|---------------------|----------
125847 | _transient_timeout_feed_8a7b2... | 1698234567 | yes
125848 | _transient_feed_8a7b2... | a:4:{s:5:"child"... | yes
125849 | _transient_timeout_wc_report_customers_... | 1698234890 | yes
125850 | _transient_wc_report_customers_... | a:23:{...} | yes
[... 846,988 registros más ...]
Qué son los Transients y por qué son el enemigo silencioso
Los transients son datos temporales que WordPress y los plugins almacenan para acelerar operaciones. Funcionan como una caché interna:
// Ejemplo de cómo se crean
set_transient('mi_dato_temporal', $datos_pesados, 12 * HOUR_IN_SECONDS);
El problema: Se supone que expiran automáticamente. En teoría.
La realidad: WordPress solo elimina transients expirados cuando intentas acceder a ellos. Si nadie los solicita, se quedan ahí. Para siempre.
Los peores culpables:
- WooCommerce: Transients de reportes, sesiones, productos relacionados
- Plugins de caché: Ironía máxima, guardan datos de “optimización”
- Plugins de analytics: Estadísticas que ya nadie consulta
- RSS Feeds: Caché de feeds externos
- Plugins desinstalados: Dejan su basura eternamente
La cascada del desastre: Cómo 850k filas te destruyen
1. Consulta inicial (cada página cargada):
SELECT option_name, option_value
FROM wp_options
WHERE autoload = 'yes';
Con una tabla normal: 0.003 segundos
Con 850k filas: 2.8 segundos
2.8 segundos solo para cargar las opciones. Antes de hacer cualquier otra cosa.
2. Impacto en memoria:
Una tabla wp_options saludable ocupa ~3-5 MB en memoria.
Nuestra tabla monstruo: 487 MB cargados en cada request.
Con un límite de memoria PHP de 512 MB, estás consumiendo el 95% solo en opciones antes de ejecutar tu theme o plugins.
3. Impacto en I/O del disco:
Cada consulta lee 1.8 GB de datos de disco, aunque solo necesite 2 MB. El resto son transients expirados que nadie pidió pero que MySQL debe leer para filtrar.
4. Fragmentación de índices:
SHOW TABLE STATUS WHERE Name = 'wp_options'\G
# Fragmentación: 87%
# Data_free: 1,204 MB (espacio desperdiciado)
Tu base de datos está leyendo aire.
El análisis forense: Qué encontramos
Después de investigar los 847,392 registros:
-- Transients expirados hace más de 1 año
SELECT COUNT(*) FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 YEAR));
# Resultado: 412,847 transients zombie
-- Transients de plugins desinstalados hace 2 años
SELECT COUNT(*) FROM wp_options
WHERE option_name LIKE '%_transient_%revslider%';
# Resultado: 89,234 (el plugin fue borrado en 2022)
-- Sesiones de WooCommerce de usuarios que nunca compraron
SELECT COUNT(*) FROM wp_options
WHERE option_name LIKE '_wc_session_%';
# Resultado: 156,893 sesiones fantasma
El 94.7% de la tabla era basura pura.
La limpieza: Procedimiento quirúrgico
⚠️ ADVERTENCIA CRÍTICA
NUNCA hagas esto sin backup completo de la base de datos.
Un error aquí puede destruir tu sitio irreversiblemente.
Paso 1: Backup
wp db export backup-antes-limpieza-$(date +%Y%m%d-%H%M%S).sql
Paso 2: Análisis pre-limpieza
-- Ver tamaño actual
SELECT
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS "Size (MB)",
table_rows AS "Rows"
FROM information_schema.TABLES
WHERE table_name = "wp_options";
-- Contar transients expirados
SELECT COUNT(*) FROM wp_options
WHERE option_name LIKE '%_transient_%'
AND (
(option_name LIKE '%_transient_timeout_%' AND option_value < UNIX_TIMESTAMP())
OR
(option_name LIKE '%_transient_%' AND option_name NOT LIKE '%_transient_timeout_%')
);
Paso 3: Limpieza con WP-CLI (método recomendado)
# Eliminar transients expirados
wp transient delete --expired
# Eliminar TODOS los transients (se regenerarán los necesarios)
wp transient delete --all
# Limpiar opciones huérfanas
wp db query "DELETE FROM wp_options WHERE option_name LIKE '%_transient_%'"
Paso 4: Limpieza SQL directa (método avanzado)
-- Eliminar timeouts de transients expirados
DELETE FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP();
-- Eliminar los transients correspondientes
DELETE FROM wp_options
WHERE option_name LIKE '_transient_%'
AND option_name NOT LIKE '_transient_timeout_%'
AND option_name NOT IN (
SELECT REPLACE(option_name, '_transient_timeout_', '_transient_')
FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
);
-- Limpiar sesiones de WooCommerce antiguas (>1 semana)
DELETE FROM wp_options
WHERE option_name LIKE '_wc_session_%'
AND option_name NOT LIKE '_wc_session_expires_%';
-- Eliminar autoloads innecesarios grandes (>100KB)
UPDATE wp_options
SET autoload = 'no'
WHERE autoload = 'yes'
AND LENGTH(option_value) > 102400
AND option_name NOT IN ('active_plugins', 'siteurl', 'home');
Paso 5: Optimizar la tabla
OPTIMIZE TABLE wp_options;
Los resultados: Antes y después
Antes de la limpieza:
- Filas: 847,392
- Tamaño: 1,847 MB
- Autoload size: 487 MB
- TTFB: 2.8-3.4s
- Admin load time: 5.2s
- Consultas por página: 247 queries
- Memoria PHP usada: 498 MB
Después de la limpieza:
- Filas: 1,247
- Tamaño: 4.2 MB
- Autoload size: 2.1 MB
- TTFB: 0.18-0.24s (mejora del 92%)
- Admin load time: 0.8s (mejora del 85%)
- Consultas por página: 89 queries
- Memoria PHP usada: 67 MB
PageSpeed score: De 45 a 87 en mobile. Sin cambiar una línea de código.
Prevención: El mantenimiento que tu agencia no hace
1. Monitoreo automatizado
# Script para monitorear wp_options (añadir a cron)
#!/bin/bash
COUNT=$(wp db query "SELECT COUNT(*) as count FROM wp_options" --skip-column-names)
SIZE=$(wp db query "SELECT ROUND(((data_length + index_length) / 1024 / 1024), 2) FROM information_schema.TABLES WHERE table_name = 'wp_options'" --skip-column-names)
if [ $COUNT -gt 5000 ]; then
echo "ALERTA: wp_options tiene $COUNT filas (SIZE: ${SIZE}MB)"
# Enviar notificación
fi
2. Limpieza programada
// Añadir a functions.php o mu-plugin
add_action('wp_scheduled_delete', function() {
global $wpdb;
// Limpiar transients expirados semanalmente
$wpdb->query("DELETE FROM $wpdb->options
WHERE option_name LIKE '_transient_timeout_%'
AND option_value < UNIX_TIMESTAMP()");
// Limpiar sesiones WC antiguas
$wpdb->query("DELETE FROM $wpdb->options
WHERE option_name LIKE '_wc_session_%'
AND option_name NOT LIKE '_wc_session_expires_%'");
});
3. Configurar transients externos
// wp-config.php - Usar Redis/Memcached para transients
define('WP_CACHE', true);
// Los transients se guardarán en Redis, no en la BD
4. Auditoría trimestral
- Revisar tamaño de wp_options mensualmente
- Limpieza profunda cada 3 meses
- Desactivar plugins que abusan de transients
Los culpables habituales: Plugins que destrozan wp_options
Hall of Shame:
- Slider Revolution: Guarda cada slide como transient
- Visual Composer: Cache de elementos desmesurado
- WPML: Traducciones en transients sin expiración
- Rank Math/Yoast SEO: Análisis que nunca se borran
- Social sharing plugins: Contadores de shares obsoletos
- Broken Link Checker: Resultados de checks eternos
Cómo identificar al culpable:
SELECT
SUBSTRING_INDEX(option_name, '_', 2) as plugin_prefix,
COUNT(*) as total,
ROUND(SUM(LENGTH(option_value))/1024/1024, 2) as size_mb
FROM wp_options
WHERE option_name LIKE '_transient_%'
GROUP BY plugin_prefix
ORDER BY total DESC
LIMIT 20;
La pregunta incómoda para tu actual proveedor
Si pagas por mantenimiento WordPress y nunca han mencionado wp_options o transients, hay dos posibilidades:
- No saben que existe el problema
- Saben pero no lo resuelven porque “no está en el checklist”
Ninguna de las dos es buena señal.
El costo real de la ignorancia
Calculemos el impacto económico de ese sitio con 850k filas:
- Servidor sobredimensionado: €180/mes que no necesitarías
- CDN con tráfico extra: €45/mes compensando la lentitud
- Conversiones perdidas: 2.7% menos por cada segundo extra (Google)
- Con 10,000 visitas/mes y conversión del 3% = 300 conversiones
- Pérdida de 2.8s × 2.7% = 7.56% menos conversiones
- 23 conversiones perdidas/mes
- A €50 ticket medio = €1,150/mes en ventas perdidas
Total anual: €14,700 tirados por una tabla sucia.
Y la limpieza toma 30 minutos.
Checklist técnico: ¿Tu web está enferma?
Ejecuta estos comandos:
# 1. Contar filas en wp_options
wp db query "SELECT COUNT(*) FROM wp_options"
# 🚨 Si >5,000: Problema moderado
# 🚨 Si >20,000: Problema grave
# 🚨 Si >100,000: Urgencia crítica
# 2. Tamaño de wp_options
wp db query "SELECT ROUND(((data_length + index_length) / 1024 / 1024), 2) as 'Size MB' FROM information_schema.TABLES WHERE table_name = 'wp_options'"
# 🚨 Si >50 MB: Problema grave
# 🚨 Si >200 MB: Crítico
# 3. Tamaño de autoload
wp db query "SELECT ROUND(SUM(LENGTH(option_value))/1024/1024, 2) as 'Autoload MB' FROM wp_options WHERE autoload='yes'"
# 🚨 Si >3 MB: Revisar
# 🚨 Si >10 MB: Problema serio
# 🚨 Si >50 MB: Desastre inminente
# 4. Contar transients expirados
wp transient list --expired | wc -l
# 🚨 Si >1,000: Necesitas limpieza YA
La verdad que nadie te dice
El 80% de las webs “lentas inexplicables” que diagnosticamos tienen este problema.
No es tu hosting. No es tu tema. No es que “WordPress sea lento”.
Es basura acumulada que nadie limpia porque nadie la ve.
Es como tener un coche de alta gama conduciendo con el freno de mano puesto. El coche es bueno, el conductor es competente, pero algo invisible está saboteando el rendimiento.
Conclusión: El mantenimiento invisible
La optimización web no es solo minificar CSS y comprimir imágenes. Eso es lo visible, lo marketeable, lo que todos ofrecen.
El verdadero mantenimiento profesional ocurre donde nadie mira: en las tripas de la base de datos, en los procesos cron que se acumulan, en las tablas que crecen sin control.
Una web optimizada no se construye. Se mantiene.
¿Quieres saber si tu web tiene este problema?
Te hacemos una auditoría técnica gratuita de tu base de datos. Te diremos exactamente:
- Cuántas filas tiene tu wp_options
- Cuánto espacio ocupa tu autoload
- Qué plugins son los culpables
- Cuánto tiempo de carga estás perdiendo
- Qué mejoraría si lo limpiáramos
Sin letra pequeña. Sin compromiso. Solo datos técnicos reales.
Porque el primer paso para solucionar un problema es saber que existe.
Escrito por
ximo