VACUUM, autovacuum y bloat de tablas

PostgreSQL nunca actualiza una fila en su sitio. Cada UPDATE escribe una nueva versión de la fila y marca la antigua como muerta; cada DELETE se limita a marcarla. El espacio se recupera después, de forma asíncrona, mediante VACUUM. Cuando ese ciclo funciona, nunca piensas en él. Cuando se queda atrás, las tablas y los índices crecen en silencio —eso es el bloat— y cada consulta lo paga.

Por qué existen las filas muertas: MVCC

Bajo MVCC (control de concurrencia multiversión), los lectores nunca bloquean a los escritores ni viceversa, porque cada transacción ve una instantánea consistente: las versiones antiguas de las filas se conservan mientras alguna transacción pueda seguir necesitándolas. En el momento en que ninguna transacción activa puede ver una versión muerta, esta se convierte en basura, pero PostgreSQL no la recupera de forma inline. Ese es el trabajo de VACUUM.

Dos consecuencias inmediatas:

Qué hace realmente VACUUM (y qué no)

Cómo decide autovacuum cuándo ejecutarse

El launcher de autovacuum comprueba periódicamente cada tabla contra un umbral:

vacuum when:  n_dead_tup  >  autovacuum_vacuum_threshold
                             + autovacuum_vacuum_scale_factor × reltuples
-- defaults:  50 + 0.20 × reltuples  (20% of the table)

El scale factor por defecto del 20% está bien para tablas pequeñas y es terrible para las grandes: una tabla de 100 millones de filas acumula 20 millones de filas muertas antes de que autovacuum siquiera arranque. Para tablas grandes y calientes, establece un override por tabla:

ALTER TABLE orders SET (
  autovacuum_vacuum_scale_factor = 0.01,
  autovacuum_vacuum_threshold    = 10000
);

Autovacuum también está deliberadamente frenado por retardos basados en coste (autovacuum_vacuum_cost_delay / autovacuum_vacuum_cost_limit) para que no sature la E/S. En el hardware moderno, los valores por defecto son conservadores; si autovacuum se ejecuta constantemente pero nunca alcanza el ritmo, subir autovacuum_vacuum_cost_limit suele ser la primera palanca.

Detectar el bloat antes de que duela

SELECT relname,
       n_live_tup,
       n_dead_tup,
       last_autovacuum,
       last_autoanalyze
FROM   pg_stat_user_tables
ORDER  BY n_dead_tup DESC
LIMIT  20;

Actualizaciones HOT: el almuerzo gratis por el que vale la pena diseñar

Si un UPDATE cambia solo columnas que no están indexadas, y la nueva versión cabe en la misma página, PostgreSQL realiza una actualización HOT (heap-only tuple): no se escribe ninguna entrada de índice en absoluto, y la versión muerta se puede limpiar de forma barata dentro de la página. Por eso añadir "solo un índice más" en una columna que se actualiza con frecuencia puede arruinar el rendimiento de escritura: convierte cada actualización HOT en una completa. Un fillfactor más bajo (p. ej. 80–90 para tablas con muchas actualizaciones) deja espacio en cada página y aumenta la tasa de HOT.

Reglas prácticas

¿Sospechas que el bloat está ralentizando una consulta? Ejecútala con EXPLAIN (ANALYZE, BUFFERS) y vuelca la salida en el EXPLAIN Visualizer para ver qué nodo está haciendo las lecturas de más.
🧯 Errores relacionados: cuando vacuum se queda atrás lo suficiente, obtienes "not accepting commands to avoid wraparound data loss"; el crecimiento descontrolado termina en No space left on device; y en las réplicas, la limpieza provoca conflictos con la recuperación.