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:
- Una carga intensiva en
UPDATEescribe aproximadamente tanto como una intensiva enINSERT, más el churn de índices. - Una única transacción de larga duración frena la limpieza de toda la base de datos: VACUUM no puede eliminar ninguna versión de fila que la instantánea de esa transacción todavía pueda ver, independientemente de las tablas que tocara. Una sesión
idle in transactionolvidada el viernes significa una base de datos hinchada el lunes.
Qué hace realmente VACUUM (y qué no)
- Un
VACUUMnormal busca tuplas muertas, hace que su espacio sea reutilizable para futuras escrituras en la misma tabla, actualiza el free space map y el visibility map, y congela las tuplas antiguas para evitar el transaction-ID wraparound. Se ejecuta junto al tráfico normal. - No reduce el fichero en disco (salvo el caso especial de páginas completamente vacías al final de la tabla). Una tabla que una vez se hinchó hasta 50 GB se queda en 50 GB en disco aunque por dentro esté vacía en un 90%.
VACUUM FULLreescribe la tabla en un nuevo fichero compacto y devuelve espacio al sistema operativo, pero toma un lockACCESS EXCLUSIVEdurante toda la reescritura. En una tabla grande y con mucho tráfico, eso es una caída de servicio; mira pg_repack para reconstrucciones online.ANALYZE(que suele ejecutarse junto) actualiza las estadísticas del planificador, un trabajo distinto que también importa; consulta cómo leer EXPLAIN ANALYZE.
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;
n_dead_tupgrande y creciendo, conlast_autovacuumantiguo o NULL → autovacuum no está siguiendo el ritmo (o una transacción larga está fijando el horizonte: comprueba enpg_stat_activitylosxact_startantiguos).- Síntoma del lado de la consulta: recuentos de buffers desproporcionados respecto a las filas devueltas en
EXPLAIN (ANALYZE, BUFFERS): leer 40.000 páginas para 500 filas significa escanear mayoritariamente espacio muerto. - Comprobación del lado del tamaño: compara
pg_total_relation_size()a lo largo del tiempo, o usa la extensiónpgstattuplepara obtener un porcentaje exacto de espacio muerto en una tabla sospechosa.
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
- Nunca desactives autovacuum. Si molesta, está mal ajustado, no es innecesario; y además te protege del transaction-ID wraparound, que es un evento capaz de detener la base de datos.
- Mata o pon timeout a las transacciones idle:
idle_in_transaction_session_timeoutes un seguro barato. - Ajusta por tabla, no globalmente: un puñado de tablas calientes suele causar la mayor parte del dolor.
- El bloat que ya tienes no desaparecerá solo: un VACUUM normal detiene el crecimiento; recuperar disco requiere
VACUUM FULLo una reconstrucción online.
EXPLAIN (ANALYZE, BUFFERS) y vuelca la salida en el EXPLAIN Visualizer para ver qué nodo está haciendo las lecturas de más.
