TL;DR — Resumen Rápido
Guía completa de VACUUM y autoVACUUM en PostgreSQL: trabajadores, umbrales, costo de retraso, monitoreo de bloat, prevención de wraparound y uso de pg_repack.
El modelo de control de concurrencia multiversión (MVCC) de PostgreSQL mantiene vivas las versiones antiguas de filas para servir a los lectores concurrentes. Sin una limpieza regular, esas tuplas muertas se acumulan, inflan el tamaño de las tablas, ralentizan los escaneos secuenciales y eventualmente amenazan la disponibilidad de la base de datos a través del wraparound del Transaction ID. Esta guía cubre todos los controles que puede usar un DBA: desde los parámetros globales de autovacuum hasta las anulaciones por tabla, desde las consultas de monitoreo de bloat hasta la reorganización en línea con pg_repack.
Prerrequisitos
Antes de comenzar, asegúrate de tener:
- PostgreSQL 13 o posterior (la mayoría de los ejemplos funcionan desde la versión 12+; algunas sintaxis requieren la 14+).
- Acceso a psql o pgAdmin con el rol superuser o pg_monitor.
- Extensión pg_repack instalada si planeas reorganización en línea (se cubre más adelante).
- Familiaridad con
postgresql.confy la capacidad de recargar la configuración.
MVCC y Tuplas Muertas
PostgreSQL nunca modifica una fila en su lugar. Un UPDATE escribe una nueva versión de la tupla y marca la antigua como muerta. Un DELETE también marca la fila como muerta sin eliminarla. Este diseño permite a los lectores ver una instantánea consistente sin tomar bloqueos, pero el tradeoff es la acumulación de tuplas muertas en el disco.
Las tuplas muertas consumen espacio en disco, inflan el tamaño físico de la tabla y ralentizan los escaneos secuenciales porque PostgreSQL debe leer y saltar cada página aunque la mayoría de sus filas estén muertas. La vista pg_stat_user_tables rastrea el conteo:
SELECT
relname,
n_live_tup,
n_dead_tup,
round(n_dead_tup::numeric / nullif(n_live_tup + n_dead_tup, 0) * 100, 1) AS dead_pct,
last_autovacuum,
last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;
Una tabla con dead_pct superior al 10–15% es candidata a atención inmediata.
VACUUM, VACUUM FULL y VACUUM ANALYZE
PostgreSQL incluye tres variantes de VACUUM con comportamientos muy diferentes:
| Comando | ¿Bloquea la tabla? | ¿Devuelve espacio al SO? | ¿Actualiza estadísticas? | ¿Seguro en producción? |
|---|---|---|---|---|
VACUUM | No (ShareUpdateExclusiveLock) | No — marca espacio reutilizable | No | Sí |
VACUUM ANALYZE | No | No | Sí | Sí |
VACUUM FULL | Sí (AccessExclusiveLock) | Sí | No | Con precaución |
VACUUM marca las tuplas muertas como disponibles para reutilización. El archivo físico no se reduce — las páginas liberadas se añaden al Free Space Map (FSM) para futuras inserciones. Este es el comando en el que debes confiar principalmente.
VACUUM FULL reescribe la tabla completa en un nuevo archivo heap, luego descarta el antiguo. Devuelve espacio en disco al sistema operativo y elimina el bloat de índices reconstruyendo todos los índices, pero adquiere un bloqueo exclusivo durante toda su ejecución. En una tabla de 100 GB, eso podría significar 30 minutos o más de indisponibilidad total. Nunca ejecutes VACUUM FULL en una tabla de producción activa sin programar una ventana de mantenimiento — o mejor, usa pg_repack en su lugar.
VACUUM ANALYZE combina la limpieza de tuplas muertas con una actualización de estadísticas, que el planificador de consultas usa para elegir planes de ejecución óptimos.
Configuración del Autovacuum
El autovacuum ejecuta workers en segundo plano que llaman a VACUUM y ANALYZE automáticamente. Los parámetros clave están en postgresql.conf:
# Número de workers concurrentes de autovacuum (predeterminado: 3)
autovacuum_max_workers = 5
# Frecuencia con la que el lanzador de autovacuum verifica las tablas (predeterminado: 1min)
autovacuum_naptime = 30s
# Mínimo de tuplas muertas antes de activar VACUUM (predeterminado: 50)
autovacuum_vacuum_threshold = 50
# Fracción de filas vivas que pueden estar muertas antes de activar VACUUM (predeterminado: 0.2 = 20%)
autovacuum_vacuum_scale_factor = 0.05
# Mínimo de cambios de filas antes de activar ANALYZE (predeterminado: 50)
autovacuum_analyze_threshold = 50
# Fracción de filas cambiadas antes de activar ANALYZE (predeterminado: 0.1 = 10%)
autovacuum_analyze_scale_factor = 0.02
# Limitación de E/S: retraso entre operaciones de buffer de vacuum en milisegundos (predeterminado: 2)
autovacuum_vacuum_cost_delay = 2ms
# Máximo de unidades de costo gastadas antes de un retraso (predeterminado: -1 = usa vacuum_cost_limit = 200)
autovacuum_vacuum_cost_limit = 400
Comprendiendo la Fórmula de Activación
El autovacuum se activa en una tabla cuando:
n_dead_tup > autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * n_live_tup
Para una tabla de 10 millones de filas con los valores predeterminados:
umbral = 50 + 0.20 × 10,000,000 = 2,000,050 tuplas muertas
Eso significa que el 20% de la tabla debe estar muerta antes de que se active el autovacuum. Para una tabla de alta escritura, eso es demasiado tarde. Reducir autovacuum_vacuum_scale_factor a 0.01 o incluso 0.005 es apropiado para tablas grandes con actualizaciones frecuentes.
autovacuum_vacuum_cost_delay
Este parámetro limita el autovacuum para evitar privar de E/S a las aplicaciones. Cada buffer que el autovacuum lee o ensucia cuesta un cierto número de “unidades de costo”. Cuando el costo acumulado alcanza autovacuum_vacuum_cost_limit, el worker duerme durante autovacuum_vacuum_cost_delay milisegundos.
En SSD y matrices NVMe, puedes configurar autovacuum_vacuum_cost_delay = 0 o 2ms y aumentar autovacuum_vacuum_cost_limit = 800. Esto hace que el autovacuum sea más rápido sin afectar significativamente las IOPS en almacenamiento rápido.
Después de cambiar postgresql.conf, recarga la configuración (no es necesario reiniciar):
SELECT pg_reload_conf();
Monitoreo del Bloat
Las vistas pg_stat_user_tables y pg_stat_activity son tus principales herramientas de monitoreo.
Consulta de Panel de Tuplas Muertas
SELECT
schemaname,
relname,
n_dead_tup,
n_live_tup,
last_autovacuum,
last_autoanalyze,
autovacuum_count,
pg_size_pretty(pg_total_relation_size(schemaname || '.' || relname)) AS total_size
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC;
Encontrar Workers de Autovacuum en Ejecución
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE query LIKE 'autovacuum:%'
ORDER BY duration DESC;
Si nunca ves workers de autovacuum pero las tablas tienen tuplas muertas altas, el autovacuum puede tener recursos insuficientes (muy pocos workers o limitación de costo demasiado agresiva).
Anulaciones de Autovacuum por Tabla
Los parámetros globales afectan a todas las tablas. Las tablas grandes con actualizaciones frecuentes necesitan umbrales más ajustados. Aplica parámetros de almacenamiento por tabla con ALTER TABLE:
-- Activar vacuum cuando el 1% de las filas estén muertas (en lugar del 20% predeterminado)
ALTER TABLE orders SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_vacuum_threshold = 100,
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_cost_delay = 0
);
-- Verificar las anulaciones
SELECT relname, reloptions
FROM pg_class
WHERE relname = 'orders';
Este es el cambio más impactante que puedes hacer para una tabla activa. El factor de escala global permanece conservador para tablas pequeñas, mientras que la tabla activa obtiene un vacuum agresivo.
Para restablecer a los valores globales predeterminados:
ALTER TABLE orders RESET (autovacuum_vacuum_scale_factor);
Prevención del Wraparound de Transaction ID
PostgreSQL usa IDs de transacción (XIDs) de 32 bits. Después de aproximadamente 2.100 millones de transacciones, el contador se reinicia. Las tuplas más antiguas que el horizonte de wraparound podrían volverse visibles nuevamente o invisibles, causando corrupción de datos. PostgreSQL se protege forzando un freeze de autovacuum antes de que se alcance el horizonte.
Verificar la Edad del XID
SELECT
relname,
age(relfrozenxid) AS xid_age,
pg_size_pretty(pg_total_relation_size(oid)) AS size
FROM pg_class
WHERE relkind = 'r'
ORDER BY age(relfrozenxid) DESC
LIMIT 20;
Cuando xid_age supera autovacuum_freeze_max_age (predeterminado: 200 millones), el autovacuum forzará un freeze vacuum en esa tabla, independientemente del conteo de tuplas muertas. Cuando alcanza los 2.000 millones, PostgreSQL se cierra con:
ERROR: database is not accepting commands to avoid wraparound data loss
Monitoreo a Nivel de Base de Datos
SELECT
datname,
age(datfrozenxid) AS db_xid_age
FROM pg_database
ORDER BY age(datfrozenxid) DESC;
Configura una alerta cuando la edad del XID de cualquier base de datos supere los 1.500 millones.
pg_repack: Reorganización en Línea de Tablas
Cuando VACUUM no puede recuperar espacio físico y VACUUM FULL es inaceptable por el bloqueo, pg_repack es la solución. Reconstruye la tabla usando una tabla sombra y triggers, luego los intercambia atómicamente — todo mientras la tabla original permanece completamente disponible.
Instalación de pg_repack
# Ubuntu/Debian
sudo apt install postgresql-15-repack
-- En la base de datos objetivo
CREATE EXTENSION pg_repack;
Reorganización de una Tabla en Línea
# Reorganizar una sola tabla con bloat
pg_repack -d mydb -t orders
# Reorganizar todas las tablas en una base de datos
pg_repack -d mydb
# Simulación (mostrar qué pasaría)
pg_repack -d mydb -t orders --dry-run
pg_repack requiere que la tabla tenga una clave primaria o un índice único NOT NULL. No aplica ningún bloqueo exclusivo hasta el intercambio final, que tarda milisegundos.
Comparación pg_repack vs VACUUM FULL:
| Aspecto | pg_repack | VACUUM FULL |
|---|---|---|
| Bloqueo durante la reconstrucción | Ninguno | AccessExclusiveLock (duración completa) |
| Espacio en disco requerido | 2× tamaño de tabla temporalmente | 2× tamaño de tabla temporalmente |
| Reconstrucción de índices | Sí (concurrente) | Sí (bloqueante) |
| Requiere clave primaria | Sí | No |
| Seguro en producción | Sí | Solo en ventana de mantenimiento |
Bloat de Índices y REINDEX CONCURRENTLY
Los índices acumulan bloat de forma independiente a la tabla. Los índices B-tree tienen un factor de llenado interno y no siempre recuperan espacio de las entradas eliminadas.
Detección de Bloat de Índices
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 20;
Reconstrucción de Índices Sin Tiempo de Inactividad
-- Reconstruir un solo índice de forma concurrente
REINDEX INDEX CONCURRENTLY orders_created_at_idx;
-- Reconstruir todos los índices de una tabla de forma concurrente (PostgreSQL 14+)
REINDEX TABLE CONCURRENTLY orders;
REINDEX CONCURRENTLY construye un nuevo índice junto al existente sin bloquear lecturas ni escrituras.
Interacción con PgBouncer y Autovacuum
PgBouncer en modo de pooling de transacciones crea un problema sutil: las conexiones de aplicaciones inactivas pueden mantener transacciones abiertas que fijan el xmin más antiguo, impidiendo que VACUUM elimine tuplas muertas.
Verifica el xmin fijado:
SELECT
client_addr,
state,
backend_xmin,
now() - state_change AS state_age
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY backend_xmin;
Si ves backends con valores backend_xmin muy antiguos que nunca cambian, pueden ser conexiones de PgBouncer que iniciaron una transacción y quedaron inactivas. Establece idle_in_transaction_session_timeout = '5min' en PostgreSQL para terminar automáticamente las transacciones obsoletas.
Errores Comunes
1. Establecer autovacuum = off globalmente. Nunca hagas esto en una base de datos de producción. Sin autovacuum, el cierre por wraparound es inevitable.
2. Ejecutar VACUUM FULL en horas pico. El bloqueo exclusivo bloquea todo. Prográmalo durante ventanas de mantenimiento o usa pg_repack.
3. Ignorar el oldest xmin en VACUUM VERBOSE. Si una transacción de larga duración o un slot de replicación mantiene un xmin antiguo, VACUUM no puede eliminar tuplas muertas. Verifica también los slots de replicación:
SELECT slot_name, xmin, catalog_xmin, age(xmin) FROM pg_replication_slots;
4. Configurar autovacuum_max_workers demasiado alto. Cada worker consume conexiones y E/S. Más de 6–8 workers en la mayoría de los sistemas causa contención. Ajusta cost_delay y cost_limit primero.
5. Usar pg_repack en tablas con triggers diferidos. pg_repack es incompatible con triggers diferidos. Verifica con \d+ tablename en psql antes de ejecutar.
Resumen
El rendimiento saludable de PostgreSQL depende de que VACUUM pueda mantener el ritmo de tu carga de escritura:
- Reduce
autovacuum_vacuum_scale_factorpara tablas grandes con muchas escrituras — el 20% predeterminado es demasiado permisivo. - Monitorea
n_dead_tupylast_autovacuumcontinuamente a través depg_stat_user_tables. - Verifica
xid_ageregularmente; alerta cuando supere los 1.500 millones para prevenir emergencias de wraparound. - Usa anulaciones por tabla (
ALTER TABLE SET) en lugar de relajar la configuración global para todas las tablas. - Reemplaza VACUUM FULL con pg_repack para reorganización en línea sin tiempo de inactividad.
- Reconstruye índices con
REINDEX CONCURRENTLYcuando el bloat de índices afecte el rendimiento de las consultas. - Investiga transacciones largas y slots de replicación cuando VACUUM se ejecuta pero las tuplas muertas persisten.