Por qué PostgreSQL se vuelve lento con el tiempo y cómo solucionarlo
Las bases de datos PostgreSQL acumulan problemas con el tiempo. Aprenda a identificar y resolver los cuellos de botella más comunes sin cambiar hardware.
Por qué una base de datos que funcionaba bien empieza a degradarse
Es un patrón familiar: la aplicación funcionaba bien al principio, pero con el paso del tiempo las operaciones se vuelven progresivamente más lentas. El primer instinto es buscar la causa en el servidor (más RAM, más CPU, más disco), pero en la mayoría de los casos el problema está en la base de datos misma.
PostgreSQL acumula varios tipos de degradación a lo largo del tiempo, y cada uno tiene causas y soluciones específicas. Conocerlos permite diagnosticar rápidamente en lugar de adivinar.
Causa 1: acumulación de dead tuples (bloat)
PostgreSQL usa un mecanismo de control de concurrencia multiversión (MVCC) que no elimina inmediatamente las filas actualizadas o borradas. En cambio, las marca como "muertas" y las deja en su lugar hasta que el proceso autovacuum las limpie.
Si el autovacuum no está correctamente configurado para el volumen de cambios de la tabla, las filas muertas se acumulan. El resultado es una tabla mucho más grande de lo necesario, con datos válidos dispersos entre filas muertas. Cualquier escaneo de la tabla tiene que procesar más datos de los necesarios.
Verificar dead tuples en tablas principales:
SELECT relname, n_live_tup, n_dead_tup, round(100 * n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0), 2) AS dead_pct FROM pg_stat_user_tables WHERE n_dead_tup > 1000 ORDER BY n_dead_tup DESC;
Solución: ajustar el autovacuum para las tablas con alta frecuencia de cambios, y ejecutar VACUUM ANALYZE inmediatamente en tablas con bloat significativo.
Causa 2: estadísticas desactualizadas
El planificador de consultas de PostgreSQL usa estadísticas sobre la distribución de datos en cada tabla para elegir el plan de ejecución más eficiente. Cuando esas estadísticas están desactualizadas (porque los datos crecieron mucho sin que el autovacuum actualizara las estadísticas), el planificador puede tomar decisiones incorrectas.
El caso más común: el planificador elige un escaneo secuencial cuando un index scan sería 100 veces más rápido, porque las estadísticas dicen que la tabla tiene 10.000 filas cuando en realidad tiene 5 millones.
Verificar cuándo fue el último analyze por tabla:
SELECT relname, last_analyze, last_autoanalyze, n_live_tup FROM pg_stat_user_tables ORDER BY last_autoanalyze ASC NULLS FIRST;
Solución: ANALYZE <tabla> en las tablas con estadísticas viejas. A largo plazo, ajustar los parámetros de autovacuum para que el analyze se ejecute más frecuentemente.
Causa 3: índices ineficientes o faltantes
Las aplicaciones evolucionan. Las consultas que eran marginales al principio se vuelven críticas. Las tablas que tenían 10.000 filas ahora tienen 10 millones. Los índices que se crearon al principio ya no son suficientes para las consultas actuales.
La extensión pg_stat_statements permite identificar exactamente cuáles consultas consumen más tiempo:
Consultas más costosas por tiempo total:
SELECT query, calls, total_exec_time, mean_exec_time, round(100 * total_exec_time / sum(total_exec_time) OVER (), 2) AS pct_total FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 20;
Una vez identificadas las consultas costosas, EXPLAIN (ANALYZE, BUFFERS) muestra el plan de ejecución real y revela si se está usando un índice o no.
Solución: crear índices compuestos en las columnas del filtro de las consultas más ejecutadas. Usar CREATE INDEX CONCURRENTLY para no bloquear la tabla en producción.
Causa 4: configuración de memoria incorrecta
Los valores por defecto de PostgreSQL están pensados para sistemas con muy poca RAM. En un servidor moderno con 16 o 32 GB de RAM, los defaults resultan en un servidor que usa apenas una fracción de la memoria disponible para caché.
Parámetros clave de memoria (postgresql.conf):
shared_buffers = 4GB # 25% de la RAM total como punto de partida effective_cache_size = 12GB # 75% de la RAM total work_mem = 256MB # Por operación de ordenamiento (ajustar con cuidado) maintenance_work_mem = 1GB # Para VACUUM, índices, etc.
Advertencia: work_mem se multiplica por el número de operaciones concurrentes. Valores demasiado altos pueden agotar la RAM con muchas conexiones simultáneas.
Causa 5: saturación de conexiones
PostgreSQL crea un proceso del sistema operativo por cada conexión de cliente. Con muchas conexiones simultáneas, el overhead de gestión de procesos puede ser significativo. Además, si se alcanza el límite de max_connections, las nuevas conexiones empiezan a fallar.
La solución estándar es un pooler de conexiones como PgBouncer, que mantiene un pool de conexiones persistentes al servidor y sirve múltiples clientes de aplicación desde ese pool.
Por dónde empezar el diagnóstico
El diagnóstico de rendimiento en PostgreSQL sigue un orden lógico:
- 01.Habilitar
pg_stat_statementssi no está activo. - 02.Identificar las 10 consultas con mayor tiempo total acumulado.
- 03.Analizar el plan de ejecución de cada una con
EXPLAIN (ANALYZE, BUFFERS). - 04.Verificar bloat en tablas principales con
pg_stat_user_tables. - 05.Revisar la configuración de memoria en postgresql.conf.
En la mayoría de los casos, los pasos 2 y 3 identifican la causa principal. Ver un ejemplo concreto: nuestro caso de optimización de PostgreSQL donde una consulta pasó de 3,6 segundos promedio a menos de 2 ms.
Servicios relacionados
