Hace un par de días contaba que Postgres 19 llega tarde y que por el camino se han quedado fuera tres funcionalidades grandes. Por suerte, entre lo que sí llega hay algo que mucha gente que administra Postgres lleva años esperando: el comando REPACK, que Craig Kerstiens explica con pruebas en el blog de ingeniería de Snowflake.
El problema: el espacio que no vuelve
Ya hablé de esto en el artículo sobre DELETE en Postgres a gran escala: por cómo funciona MVCC, cada UPDATE y cada DELETE deja una tupla muerta. El autovacuum las limpia y deja ese hueco libre para nuevas filas, pero no reduce el tamaño del fichero. Si una tabla de 500 MB pierde la mitad de sus filas, sigue ocupando 500 MB en disco. Eso es el famoso bloat.
Para devolver ese espacio al sistema hay que reescribir la tabla, y hasta ahora las opciones tenían todas su pega:
| Opción | Pega |
|---|---|
VACUUM FULL | Bloquea la tabla por completo (ni lecturas ni escrituras) mientras dura |
CLUSTER | Lo mismo, con el bloqueo, aunque además ordena las filas por un índice |
pg_repack | Extensión aparte con su propio binario cliente; captura los cambios con triggers, que cargan las escrituras |
pg_squeeze | Extensión que necesita shared_preload_libraries, wal_level = logical y un slot de replicación |
En una tabla pequeña, el bloqueo de VACUUM FULL no importa. En una tabla de producción con cientos de gigas, significa parar el servicio, así que lo habitual era tirar de pg_repack y cruzar los dedos.
Qué trae REPACK
REPACK es un comando SQL normal, sin extensiones, con tres formas:
-- Reescribe la tabla con bloqueo exclusivo, como VACUUM FULL
REPACK mi_tabla;
-- Reescribe la tabla mientras sigue disponible para lectura y escritura
REPACK (CONCURRENTLY) mi_tabla;
-- Igual, pero además ordena físicamente las filas según un índice
REPACK (CONCURRENTLY) mi_tabla USING INDEX mi_indice;
La forma interesante es la segunda. Con CONCURRENTLY, Postgres copia la tabla a una nueva mientras la aplicación sigue trabajando, va recogiendo los cambios que se producen durante la copia mediante decodificación lógica (no con triggers, como pg_repack) y sólo al final toma un bloqueo breve para aplicar lo pendiente e intercambiar una tabla por otra. Según el artículo, buena parte de este trabajo viene de CYBERTEC, el equipo detrás de pg_squeeze, así que es esa misma idea llevada al núcleo de Postgres.
Se pueden añadir opciones como VERBOSE y ANALYZE, y el progreso se sigue en la vista pg_stat_progress_repack:
SELECT pid, relid::regclass, phase,
heap_blks_scanned, heap_blks_total
FROM pg_stat_progress_repack;
Las cifras
Kerstiens prueba con una tabla de 2 millones de filas a la que le borra la mitad. Las tres opciones dejan la tabla en el mismo tamaño, pero con costes distintos:
| Método | Tiempo | Tamaño final | ¿Tabla disponible? |
|---|---|---|---|
VACUUM FULL | 2,7 s | 228 MB (desde 493 MB) | No |
REPACK | 5,9 s | 228 MB | No |
REPACK (CONCURRENTLY) | 9,4 s | 228 MB | Sí, salvo un bloqueo breve al final |
La versión concurrente es la más lenta, lo que tiene sentido porque hace trabajo extra para recoger y aplicar los cambios. Pero lo que importa en producción no es cuánto tarda, sino cuánto tiempo deja la tabla inaccesible, y ahí gana con diferencia. En la prueba con USING INDEX, una tabla de 618 MB bajó a 320 MB en 15,2 segundos.
Las limitaciones
No vale para todo. Para usar CONCURRENTLY, la tabla tiene que:
- Tener una clave primaria indexable.
- No estar particionada.
- Estar registrada en el WAL (no puede ser
UNLOGGED) y usar el método de acceso heap normal. - No ser una tabla del catálogo ni una tabla TOAST.
Además, necesita aproximadamente el doble del espacio de la tabla y sus índices mientras trabaja, y sólo puede haber un REPACK concurrente a la vez por base de datos. Lo del espacio es importante: si estás haciendo esto precisamente porque te quedas sin disco, puede que no tengas margen para hacerlo.
Y hay un detalle que conviene tener muy presente: REPACK CONCURRENTLY no es totalmente seguro con MVCC. Una transacción que tomó su snapshot antes de que empezara el proceso puede ver la tabla vacía después del intercambio. Es la misma limitación que ya tenía pg_repack y está documentada en las notas de la versión, pero en un sistema con transacciones largas (informes, exportaciones, réplicas lógicas lentas) merece la pena programarlo cuando no haya ninguna en marcha.
Lo que me llevo
REPACK no sustituye al autovacuum, que sigue siendo lo que mantiene una tabla sana en el día a día. Es una herramienta para casos concretos: recuperar espacio en disco después de un borrado masivo o reordenar físicamente una tabla para que las consultas por rango lean menos páginas.
Lo que me gusta es que convierte en un comando estándar algo que hasta ahora dependía de extensiones externas, con su instalación, sus versiones y sus permisos. Es de esas mejoras poco vistosas que no salen en los titulares de una versión, pero que acaban usándose mucho. Para quien ya tenga pg_repack funcionando no hay prisa en cambiar, pero en instalaciones nuevas con Postgres 19 ya no hará falta añadirlo.
Fuente: Postgres 19 REPACK: Faster, Built-In Table Bloat Cleanup, Craig Kerstiens, Snowflake.




