Vacuum, autovacuum y gestión de bloat

Avanzado
SQL
SQL
Actualizado: 19/04/2026

PostgreSQL crea versiones nuevas en cada UPDATE y DELETE, dejando filas viejas marcadas como muertas pero ocupando disco. El proceso VACUUM las recoge y devuelve el espacio al pool reutilizable. Autovacuum lo automatiza, pero sus parámetros por defecto están pensados para casos genéricos y suelen quedarse cortos en bases de datos con escritura intensa.

Por qué hay bloat: MVCC en acción

Recordando la lección de MVCC: cuando se ejecuta un UPDATE, PostgreSQL inserta una nueva versión y marca la antigua con xmax. La antigua no se borra inmediatamente porque puede ser visible para transacciones activas con snapshot anterior. El espacio se libera solo cuando ya nadie podría verla.

-- Antes del update: 1 fila viva
-- UPDATE clientes SET activo=false WHERE id=1;
-- Despues: 1 fila viva + 1 fila muerta ocupando disco

En tablas con muchos UPDATE/DELETE, las filas muertas se acumulan y la tabla crece físicamente aunque el número lógico no cambie. Esto se llama bloat y degrada el rendimiento: el motor lee más bloques para encontrar las filas vivas, los índices crecen, las cachés se llenan de versiones obsoletas.

VACUUM: limpieza manual

VACUUM recorre la tabla, identifica las filas muertas no visibles para ninguna transacción y libera su espacio. No devuelve el espacio al sistema operativo, solo al pool interno de la tabla.

-- Vacuum estandar
VACUUM clientes;

-- Vacuum con estadisticas analizadas
VACUUM ANALYZE clientes;

-- Vacuum verbose para ver el progreso
VACUUM VERBOSE clientes;

-- Vacuum FULL: reconstruye la tabla, devuelve espacio al SO,
-- requiere ACCESS EXCLUSIVE lock (peligroso)
VACUUM FULL clientes;

VACUUM FULL adquiere un lock exclusivo y bloquea cualquier acceso a la tabla durante la operación. Evítalo en producción salvo en ventanas de mantenimiento programadas.

Autovacuum: el proceso de fondo

PostgreSQL ejecuta autovacuum automáticamente. El daemon se despierta cada autovacuum_naptime (default 1 minuto) y revisa qué tablas necesitan VACUUM o ANALYZE.

Los parámetros principales (en postgresql.conf):

autovacuum = on
autovacuum_max_workers = 3
autovacuum_naptime = 1min
autovacuum_vacuum_threshold = 50
autovacuum_vacuum_scale_factor = 0.2
autovacuum_analyze_threshold = 50
autovacuum_analyze_scale_factor = 0.1
autovacuum_vacuum_cost_limit = 200
autovacuum_vacuum_cost_delay = 2ms

La fórmula que decide si una tabla necesita vacuum:

autovacuum_vacuum_threshold + autovacuum_vacuum_scale_factor * pg_class.reltuples

Con los defaults, una tabla de 10 millones de filas necesita acumular 2 millones de filas muertas (20%) antes de que se dispare. Para tablas calientes esto es demasiado tarde.

Ajustes por tabla

Lo correcto es ajustar autovacuum por tabla, no a nivel global. Una tabla de pedidos con muchas escrituras requiere thresholds más agresivos:

ALTER TABLE pedidos SET (
    autovacuum_vacuum_scale_factor = 0.05,         -- 5% en lugar de 20%
    autovacuum_vacuum_cost_limit = 1000,           -- mas trabajo por ronda
    autovacuum_vacuum_cost_delay = 0,              -- sin pausas
    autovacuum_analyze_scale_factor = 0.02         -- estadisticas mas frescas
);

Para una tabla pequeña pero crítica con cambios frecuentes:

ALTER TABLE configuracion SET (
    autovacuum_vacuum_threshold = 10,              -- vacuum tras 10 dead tuples
    autovacuum_vacuum_scale_factor = 0.0,
    autovacuum_analyze_threshold = 10,
    autovacuum_analyze_scale_factor = 0.0
);

El parámetro autovacuum_vacuum_cost_limit controla cuánto trabajo hace cada ronda antes de pausarse. Con valor alto (1000-2000) las rondas son más rápidas pero impactan más a las queries activas.

Monitorización del estado

La vista pg_stat_user_tables muestra estadísticas de cada tabla:

SELECT
    schemaname,
    relname,
    n_live_tup,
    n_dead_tup,
    round(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_pct,
    last_vacuum,
    last_autovacuum,
    autovacuum_count
FROM pg_stat_user_tables
WHERE n_dead_tup > 1000
ORDER BY n_dead_tup DESC
LIMIT 20;

Si dead_pct supera 20-30 % de forma persistente, el autovacuum no llega a tiempo y hay que ajustar.

Medir bloat real con pgstattuple

n_dead_tup es una estimación. Para medir el bloat con precisión existe la extensión pgstattuple:

CREATE EXTENSION IF NOT EXISTS pgstattuple;

SELECT * FROM pgstattuple('pedidos');
-- table_len: 1.5 GB
-- tuple_count: 8000000
-- tuple_len: 800 MB
-- dead_tuple_count: 2000000
-- dead_tuple_len: 200 MB
-- dead_tuple_percent: 20%
-- free_space: 350 MB

dead_tuple_percent por encima del 20 % indica que las queries leen 20 % de bloques inútiles. Acción correctiva: ajustar autovacuum, ejecutar VACUUM manual, o reorganizar con pg_repack.

pg_repack: reorganización online

VACUUM FULL libera el espacio al SO pero bloquea la tabla. pg_repack hace lo mismo sin bloqueos mediante triggers y un swap atómico al final:

# Reorganizar una tabla completa
pg_repack -d produccion -t pedidos

# Reorganizar todos los indices de una tabla
pg_repack -d produccion -t pedidos --only-indexes

# Reorganizar toda la base de datos
pg_repack -d produccion -a

Internamente:

  • 1. Crea una tabla nueva con la estructura.
  • 2. Copia las filas vivas.
  • 3. Mantiene un log de cambios mientras copia.
  • 4. Aplica el log y hace un swap rápido con un lock breve.

pg_repack requiere espacio en disco equivalente a la tabla. Si la base de datos está al 90 % de capacidad, no es viable.

Transacciones largas: el enemigo silencioso

Si una transacción se queda abierta durante horas (un report, un cliente colgado, un script roto), bloquea el cleanup: las filas muertas creadas durante ese tiempo no pueden recogerse porque la transacción podría verlas. Resultado: bloat masivo aunque autovacuum esté bien configurado.

Detectarlas es esencial:

SELECT pid, usename, application_name,
       state, xact_start, query_start, query
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'idle in transaction (aborted)')
   OR (state = 'active' AND query_start < NOW() - INTERVAL '30 minutes')
ORDER BY xact_start;

Acción: identificar la aplicación responsable, configurar idle_in_transaction_session_timeout para terminar automáticamente sesiones colgadas:

ALTER SYSTEM SET idle_in_transaction_session_timeout = '15min';
SELECT pg_reload_conf();

A nivel de aplicación: usar transacciones cortas, hacer COMMIT explícito incluso en operaciones de solo lectura, configurar timeouts en el cliente.

Síntomas de autovacuum mal configurado

Banderas rojas que sugieren ajustar:

  • Tablas que crecen sin parar aunque las filas no aumenten.
  • Queries que se ralentizan progresivamente y se aceleran tras VACUUM manual.
  • pg_stat_user_tables.last_autovacuum antiguo en tablas calientes.
  • pg_stat_progress_vacuum casi siempre activo con poca cobertura.
  • Wraparound risk warnings (texto sobre frozen tuples) en logs.
-- Ver autovacuum corriendo ahora
SELECT pid, datname, relid::regclass, phase,
       heap_blks_total, heap_blks_scanned
FROM pg_stat_progress_vacuum;

Estrategia para tablas particionadas

En tablas particionadas, autovacuum trabaja por partición individual, no por la tabla padre. Esto permite ajustes finos:

  • Particiones calientes (mes actual): autovacuum agresivo, threshold bajo.
  • Particiones viejas y comprimidas: autovacuum apenas necesario.
  • Particiones que se desadjuntan periódicamente: limpieza tras detach.
ALTER TABLE pedidos_2026_04 SET (
    autovacuum_vacuum_scale_factor = 0.05,
    autovacuum_analyze_scale_factor = 0.02
);

ALTER TABLE pedidos_2024_06 SET (
    autovacuum_enabled = false  -- partición vieja, ya no se modifica
);
flowchart LR
    A[UPDATE/DELETE genera<br/>filas muertas] --> B[Acumulacion en tabla]
    B --> C{autovacuum thresholds}
    C -->|Excedido| D[VACUUM marca espacio<br/>como reutilizable]
    C -->|Por debajo| E[Bloat sigue creciendo]
    E --> F[Queries lentas]
    D --> G[Tabla en buen estado]
    H[pg_stat_user_tables] --> I[Monitor dead_tup]
    I --> C

El vacuum bien configurado es invisible: nadie habla de él porque todo va bien. El vacuum mal configurado es el origen de la mayoría de problemas de rendimiento progresivo en bases de datos PostgreSQL veteranas. Auditarlo cada pocos meses y ajustar las tablas más activas evita la mayor parte de incidentes operacionales.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SQL es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de SQL

Explora más contenido relacionado con SQL y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Comprender por qué MVCC genera bloat y cómo VACUUM lo libera. Ajustar parámetros de autovacuum por tabla con autovacuum_vacuum_scale_factor y autovacuum_vacuum_cost_limit. Monitorizar bloat con pgstattuple y pg_stat_user_tables. Usar pg_repack para reorganizar tablas sin bloqueo. Diagnosticar transacciones largas que bloquean cleanup.

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje