Volver a las notas

Cambiar el esquema de PostgreSQL sin parar producción: expand y contract

16 de septiembre de 2026·
postgresqlmigracionesbackenddespliegue

Nota revisada y reescrita el 18 de octubre de 2026: la versión original tenía errores y datos sin fuente.

Un ALTER TABLE ... RENAME COLUMN tarda milisegundos, y aun así puede tirar la web: la versión de la aplicación que sigue en marcha busca la columna con el nombre viejo. Y otros ALTER TABLE reescriben la tabla entera con un bloqueo exclusivo. Cambiar un esquema en producción sin parar el servicio se reduce a dos ideas: nunca romper la versión del código que está corriendo, y no tener la tabla bloqueada mucho tiempo.

El patrón expand / contract

Ejemplo: renombrar users.name a users.full_name. En lugar de un paso, cuatro despliegues:

  1. Expand. Añade la columna nueva. El código viejo la ignora.
    ALTER TABLE users ADD COLUMN full_name text;
    
  2. Escribir en las dos. Despliega código que escribe en name y en full_name, y sigue leyendo de name.
  3. Rellenar y cambiar la lectura. Copia los datos antiguos (por lotes, ver abajo) y despliega código que lee de full_name.
  4. Contract. Cuando ninguna versión desplegada use name, bórrala.
    ALTER TABLE users DROP COLUMN name;
    

En cada paso, la versión anterior del código sigue funcionando. Si algo va mal puedes volver atrás sin restaurar una copia de seguridad.

Qué es rápido y qué reescribe la tabla

Casi todos los ALTER TABLE piden un bloqueo ACCESS EXCLUSIVE, que impide incluso leer. La diferencia está en cuánto tiempo lo mantienen:

  • Rápidos (solo catálogo): añadir una columna que admite nulos, añadir una columna con un DEFAULT constante (desde PostgreSQL 11 no reescribe la tabla) y borrar una columna.
  • Lentos (reescriben o recorren la tabla): cambiar el tipo de una columna (salvo conversiones compatibles como ampliar un varchar(n)), añadir un DEFAULT volátil como random(), y SET NOT NULL, que recorre la tabla para comprobarlo.

Para NOT NULL y claves foráneas hay un camino sin bloqueo largo: crear la restricción como NOT VALID y validarla después. VALIDATE CONSTRAINT usa un bloqueo más débil, que permite seguir leyendo y escribiendo.

ALTER TABLE users ADD CONSTRAINT full_name_not_null
  CHECK (full_name IS NOT NULL) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT full_name_not_null;
-- desde PG 12, SET NOT NULL aprovecha ese CHECK válido y no recorre la tabla
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;

La cola de bloqueos: lock_timeout

Incluso un ALTER instantáneo puede parar la web. Si hay una consulta larga leyendo la tabla, el ALTER espera su bloqueo, y todas las consultas que llegan detrás esperan al ALTER. Pon siempre un límite y reintenta:

SET lock_timeout = '3s';
ALTER TABLE users ADD COLUMN full_name text;

Si no consigue el bloqueo en 3 segundos, falla y no deja la cola atascada.

Rellenar por lotes

Un único UPDATE users SET full_name = name sobre millones de filas es una transacción enorme: bloquea esas filas y genera mucho WAL de golpe. Mejor por tramos de clave primaria, cada uno en su propia transacción:

UPDATE users SET full_name = name
WHERE id >= $1 AND id < $1 + 10000 AND full_name IS NULL;

Avanza $1 desde un script, con una pausa corta entre lotes si la base de datos va justa.

Índices

CREATE INDEX normal bloquea las escrituras mientras se construye. Usa CREATE INDEX CONCURRENTLY. No puede ir dentro de una transacción, así que muchas herramientas de migraciones necesitan marcar ese paso como "sin transacción".

Fuentes