Autovacuum, bloat y tuplas muertas en Postgres en operaciones
Postgres nunca elimina filas in place — updates y deletes dejan tuplas muertas que autovacuum debe reclamar. Cuando vacuum se queda atrás, las tablas se hinchan, los sequential scans se ralentizan, los índices crecen y el wraparound de transaction ID se vuelve una amenaza existencial.
La latencia de consultas se duplicó. EXPLAIN mostró Index Scan — el mismo plan que el mes pasado. El ratio de acierto del buffer cache se veía bien. La tabla tenía cuatro millones de filas vivas y treinta y ocho millones de tuplas muertas que autovacuum no había reclamado porque una consulta analítica de larga duración bloqueó la limpieza durante once días. Autovacuum, bloat y gestión de tuplas muertas en Postgres no son trivia de DBA — son operaciones de producción para cualquier equipo que ejecute cargas OLTP con updates, soft deletes o alto churn. Vacuum es cómo Postgres reclama espacio, actualiza visibility maps y previene wraparound de transaction ID. Cuando se queda atrás, el rendimiento degrada gradualmente hasta degradar de repente.
El control de concurrencia multi-versión (MVCC) de Postgres mantiene versiones antiguas de filas visibles para transacciones en curso. Un UPDATE crea una nueva versión de fila y marca la antigua como muerta. Un DELETE marca filas muertas sin eliminarlas de inmediato. Las tuplas muertas ocupan disco, llenan buffers en sequential scans e inflan índices hasta que vacuum las reclama. Autovacuum es el worker en background que debería mantener baja la fracción de tuplas muertas sin intervención manual — cuando está tuneado y desbloqueado.
How dead tuples accumulate and why autovacuum exists
Cada sentencia mutante produce tuplas muertas:
| Operation | Live rows | Dead tuples after |
|---|---|---|
| INSERT | +1 nueva | 0 |
| UPDATE | +1 nueva versión | 1 muerta (versión antigua) |
| DELETE | 0 | 1 muerta |
| Soft delete (UPDATE flag) | 1 actualizada | 1 muerta (versión antigua) |
Las tablas de alto churn — sesiones, notificaciones, colas de jobs, staging de auditoría — generan tuplas muertas más rápido que tablas mayormente append-only. Sin vacuum, el tamaño del archivo de tabla crece monótonamente mientras el conteo de filas vivas se mantiene plano. Las consultas escanean tuplas muertas aunque las cláusulas WHERE las filtren — las comprobaciones de visibilidad cuestan CPU e I/O.
Autovacuum lanza workers cuando las tablas superan umbrales de tuplas muertas:
autovacuum threshold = autovacuum_vacuum_threshold
+ autovacuum_vacuum_scale_factor * reltuples
El scale factor por defecto de 0.2 significa que vacuum se dispara tras 20% de filas vivas estimadas muertas — en una tabla de diez millones de filas, dos millones de tuplas muertas antes de que empiece la limpieza. Las tablas OLTP de alto churn suelen necesitar scale factors más bajos.
Autovacuum no es mantenimiento opcional. Es parte de la higiene del write path de Postgres.
Vacuum también congela transaction IDs en filas antiguas, marcándolas seguras contra wraparound de transaction ID — un modo de fallo que puede forzar shutdown para proteger integridad de datos cuando el cluster se acerca al límite de dos mil millones de transacciones.
Bloat: table heap, indexes, and visibility maps
Heap bloat — archivos de tabla más grandes de lo que requieren los datos vivos. Medible vía extensiones o consultas de estimación comparando pg_relation_size con la huella esperada de filas vivas. El bloat provoca más páginas leídas por consulta, menor eficiencia de caché y ventanas de backup más largas.
Index bloat — los índices retienen punteros a tuplas muertas del heap hasta que vacuum reclama ambos. Índices hinchados aumentan I/O de scan; los patrones de índice compuesto aportan menos beneficio cuando las páginas están dispersas con entradas muertas.
Visibility map — cuando está actualizado, los index-only scans omiten fetches del heap. Vacuum rezagado reduce oportunidades de index-only scan.
El bloat no se corrige solo sin vacuum. INSERT solo no reclama espacio de tuplas muertas en medio de páginas — solo vacuum marca espacio reutilizable. Bloat extremo puede requerir VACUUM FULL o reescritura estilo pg_repack — ambos con costo operativo e implicaciones de lock discutidas abajo.
Autovacuum tuning for production OLTP
Los ajustes por defecto de autovacuum sirven para bases de desarrollo de bajo churn. OLTP de producción con updates necesita tuning global o por tabla:
-- Aggressive tuning for high-churn table
ALTER TABLE notification_events SET (
autovacuum_vacuum_scale_factor = 0.02,
autovacuum_vacuum_threshold = 1000,
autovacuum_analyze_scale_factor = 0.01,
autovacuum_vacuum_cost_delay = 2
);Parámetros clave:
| Parameter | Purpose | Tuning direction for churn |
|---|---|---|
autovacuum_vacuum_scale_factor | % de tuplas muertas que dispara | Más bajo (0.01–0.05) en tablas calientes |
autovacuum_vacuum_threshold | Mínimo de tuplas muertas antes del trigger | Subir ligeramente en tablas pequeñas para evitar vacuum constante |
autovacuum_vacuum_cost_delay | Limita I/O durante vacuum | Menor delay = vacuum más rápido, más impacto en I/O |
autovacuum_max_workers | Workers de vacuum en paralelo | Aumentar en multi-core con muchas tablas hinchadas |
autovacuum_naptime | Intervalo de poll del launcher | Más bajo (10s) cuando muchas tablas necesitan atención |
Cambios globales vía postgresql.conf o ALTER SYSTEM; por tabla vía ALTER TABLE ... SET para outliers sin afectar todo el cluster.
Analyze corre junto a vacuum — estadísticas obsoletas causan malos planes que parecen índices faltantes. Tablas con churn pesado necesitan scale factors de analyze agresivos también.
Monitorear actividad de autovacuum:
SELECT relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze,
n_live_tup, n_dead_tup,
round(n_dead_tup::numeric / NULLIF(n_live_tup + n_dead_tup, 0) * 100, 2) AS dead_pct
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC
LIMIT 20;Alertar cuando dead_pct supere umbral (comúnmente 10–20% sostenido) o last_autovacuum sea más antiguo de lo esperado para la tasa de churn.
What blocks vacuum and how to unblock it
Autovacuum no puede reclamar tuplas muertas aún visibles para cualquier transacción abierta. Bloqueadores:
Long-running transactions. Consulta analítica abierta horas mantiene horizonte xmin — todas las tuplas muertas desde el inicio de la consulta permanecen. Solución: idle_in_transaction_session_timeout, timeouts de sentencia en roles OLTP, enrutar consultas largas a réplicas.
Replication slots. Los slots de replicación lógica impiden que vacuum elimine tuplas que las réplicas no han consumido. Slot rezagado → vacuum bloqueado → bloat crece en publisher. Monitorear lag en bytes y antigüedad de pg_replication_slots.
Prepared transactions. Restos de commit en dos fases bloquean limpieza. Auditar pg_prepared_xacts.
Lock contention. Autovacuum espera locks de tabla retenidos por DDL o DML largo. Raro en OLTP pero común durante migraciones zero-downtime si las migraciones retienen locks fuertes.
Consulta de diagnóstico para bloqueadores:
SELECT pid, state, query_start, xact_start, query
FROM pg_stat_activity
WHERE state != 'idle'
AND backend_xmin IS NOT NULL
ORDER BY xact_start
LIMIT 10;El xact_start más antiguo entre transacciones activas define hasta dónde puede avanzar vacuum. Los runbooks de operaciones deben incluir matar sesiones idle-in-transaction descontroladas y escalar lag de replication slot antes de que el bloat se vuelva latencia visible para el cliente.
Manual vacuum, VACUUM FULL, and pg_repack tradeoffs
Routine VACUUM — seguro, concurrente, reclama tuplas muertas para reutilización dentro del archivo de tabla, no devuelve espacio al SO en la mayoría de casos. Ejecutar manualmente cuando autovacuum se quedó atrás tras recuperación de incidente:
VACUUM (VERBOSE, ANALYZE) notification_events;VACUUM FULL — reescribe tabla entera, lock exclusivo, devuelve espacio al SO. Riesgo de downtime en tablas grandes. Reservar para ventanas de mantenimiento o tablas pequeñas para lockear brevemente. No es respuesta rutinaria a bloat.
pg_repack / pg_squeeze — reescritura online con locking mínimo. Overhead operativo de instalar extensión y programar. Justificado cuando heap bloat supera 50% y VACUUM no puede reducir archivo porque las páginas están dispersas pero no vacías bastante para reutilización.
La prevención supera la cura: autovacuum tuneado, sin transacciones largas en primary, monitoreo de replication slot. Reescritura manual es recuperación costosa, no estado estable.
Wraparound vacuum and the emergency tier
Los transaction IDs de Postgres son de 32 bits y hacen wrap. Filas más antiguas que autovacuum_freeze_max_age transacciones deben congelarse. Autovacuum lanza vacuum anti-wraparound agresivamente cuando tablas se acercan al límite de edad — visible en logs como autovacuum: vacuum en tablas con alta edad de relfrozenxid.
Si el vacuum anti-wraparound no puede seguir el ritmo — bloqueado por transacciones largas, throttling de I/O o workers insuficientes — el cluster entra en advertencia y eventualmente transaction ID wraparound protection shutdown para prevenir corrupción de datos. Es emergencia de parada total, no degradación gradual.
Monitorear:
SELECT datname, age(datfrozenxid) AS db_age,
current_setting('autovacuum_freeze_max_age')::bigint AS max_age
FROM pg_database
ORDER BY age(datfrozenxid) DESC;Alertar al 50% y 75% de autovacuum_freeze_max_age. Los incidentes de wraparound se remontan a señales de lag de autovacuum ignoradas semanas antes.
Operations runbook: daily signals and incident response
Integrar salud de vacuum en monitoreo estándar de base de datos:
Daily dashboard
- Tablas top por
n_dead_tupy porcentaje muerto - Tiempo desde
last_autovacuumen tablas de mayor churn - Edad de transacción activa más antigua
- Lag de replication slot (si replicación lógica habilitada)
- Edad de
frozenxidde base de datos y tabla
Incident response: comprobar ratio de tuplas muertas y transacciones bloqueadoras primero; ejecutar VACUUM ANALYZE manual si autovacuum se estancó; programar pg_repack solo cuando bloat supere 2× huella de datos vivos. Preventive: autovacuum por tabla en tablas calientes al definir esquema, idle_in_transaction_session_timeout en roles de app, analytics en réplicas no en primary.
How should teams manage Postgres vacuum in production?
Decisiones operativas que evitan que el bloat se convierta en incidentes de latencia.
When should autovacuum settings be per-table vs global?
Por tabla para outliers — colas de notificaciones, almacenes de sesión, tablas de estado de jobs con patrones update-heavy. Defaults globales para esquema de churn moderado. Documentar ajustes por tabla en comentarios de migración para que futuros ingenieros sepan por qué existen.
Is dead tuple percentage or absolute count more useful for alerting?
Ambos. Alto porcentaje en tablas pequeñas importa menos que porcentaje moderado en tablas de mil millones de filas. Alertar sobre n_dead_tup absoluto sobre umbral Y porcentaje sobre umbral para tablas sobre cutoff de tamaño.
Can autovacuum be too aggressive?
Sí. cost_delay muy bajo en instancias saturadas de I/O compite con consultas OLTP. Ajustar durante headroom de I/O observado — aumentar delay si vacuum causa picos de latencia, disminuir si se acumulan tuplas muertas. Medir, no adivinar.
When is bloat bad enough to require rewrite?
Cuando el tamaño de tabla supera 1.5–2× tamaño estimado de datos vivos Y vacuum no reduce tamaño de archivo Y I/O de consultas correlaciona con métricas de bloat de tabla. Por debajo de ese umbral, autovacuum tuneado suele bastar.
A common argument runs the other way
La postura opuesta sostiene que el almacenamiento SSD moderno es barato — dejar crecer tablas, añadir disco cuando haga falta y omitir complejidad de tuning de vacuum.
El costo de disco es la partida más pequeña. La contaminación del buffer cache no es barata — tablas hinchadas desalojan páginas útiles, elevando I/O de lectura en toda la carga. El bloat de índices multiplica amplificación de escritura en cada update. Las ventanas de backup y restore crecen con tamaño de archivo independientemente del conteo de filas vivas. El shutdown de protección wraparound no se resuelve aprovisionando más terabytes. El tuning de vacuum es higiene operativa que preserva latencia predecible — escalar almacenamiento es control compensatorio que llega después de que el rendimiento ya degradó.
Key takeaways
- Updates y deletes crean tuplas muertas; autovacuum las reclama y congela transaction IDs.
- El scale factor por defecto de autovacuum (20%) es demasiado perezoso para OLTP de alto churn — tune por tabla.
- Transacciones largas y replication slots rezagados bloquean vacuum — monitorear xact más antiguo y lag de slot.
- El bloat degrada eficiencia de heap e índice; index-only scans sufren cuando visibility maps se retrasan.
VACUUM FULLlockea en exclusiva — prevención vía autovacuum supera reescritura de emergencia.- Monitoreo de edad wraparound previene el único modo de fallo que apaga Postgres por completo.
- Métricas de tuplas muertas pertenecen al mismo dashboard que latencia de consultas y lag de replicación.
Conclusion
Autovacuum es invisible cuando funciona y llamativo cuando no — usualmente como crecimiento inexplicable de latencia en tablas cuyos planes de consulta nunca cambiaron. La madurez operativa de Postgres incluye monitoreo de tuplas muertas, tuning de autovacuum por tabla para hotspots de churn y pasos de runbook para vacuum bloqueado antes de que el bloat requiera cirugía de reescritura.
La primera auditoría: diez tablas top por conteo de tuplas muertas, transacción activa más antigua en primary, lag de replication slot si aplica, edad frozenxid por base de datos. Hallazgos no cero en múltiples columnas indican deuda de vacuum acumulándose más rápido de lo que autovacuum puede pagar — tune antes de que advertencias de wraparound se conviertan en páginas de outage.