Bases de Datos 4 min de lectura 851 palabras

REPACK en Postgres 19: recuperar el espacio de una tabla sin bloquearla

EN
REPACK en Postgres 19: recuperar el espacio de una tabla sin bloquearla

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ónPega
VACUUM FULLBloquea la tabla por completo (ni lecturas ni escrituras) mientras dura
CLUSTERLo mismo, con el bloqueo, aunque además ordena las filas por un índice
pg_repackExtensión aparte con su propio binario cliente; captura los cambios con triggers, que cargan las escrituras
pg_squeezeExtensió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étodoTiempoTamaño final¿Tabla disponible?
VACUUM FULL2,7 s228 MB (desde 493 MB)No
REPACK5,9 s228 MBNo
REPACK (CONCURRENTLY)9,4 s228 MBSí, 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.