ALTER TABLE en una tabla de millones de filas detiene la producción: locks, fila de queries, timeout en cascada. El patrón expand-contract divide todo cambio de schema en fases compatibles con versiones antiguas y nuevas del código conviviendo. La regla viene antes de las fases: cambio de schema y código dependiente nunca entran en el mismo release. El código viejo necesita sobrevivir al schema nuevo, y el código nuevo necesita sobrevivir al schema viejo durante toda la transición.

Fase 1: expand

Solo cambios aditivos entran en esta fase. Agrega columna nullable, crea tabla nueva, construye índices con CREATE INDEX CONCURRENTLY para no bloquear escrituras en Postgres. El backfill de los datos existentes corre en lotes paginados por clave primaria, 5 mil rows por lote; una transacción larga genera replication lag y bloat, así que lote pequeño con pausa entre lotes mantiene el cluster saludable. Un build de índice que falla deja atrás un invalid index; la limpieza exige DROP INDEX CONCURRENTLY antes del nuevo intento.

Fase 2: transición con dual-write

El código nuevo escribe en los dos formatos durante la convivencia: cada operación graba en la estructura antigua y en la nueva. El camino de lectura migra poco a poco detrás de feature flag, tabla por tabla. Un consistency checker corre de noche comparando las dos representaciones y reporta divergencias mientras todavía hay tiempo de corregir. La fase dura días o semanas, lo suficiente para cubrir rollbacks, workers atrasados y cache viejo.

Fase 3: contract

¿La telemetría probó cero lecturas en la estructura antigua? Remueve columnas deprecadas y caminos de escritura viejos. El drop de constraint pide pasos escalonados entre deploys: primero sale el código que lee, después la constraint, cada etapa en un deploy propio con rollback trivial.

¿Qué operaciones piden coreografía propia?

  • Cambio de tipo de columna: columna nueva, dual-write, backfill, switch de lectura, drop de la original
  • Rename de columna: misma secuencia, con alias via view cubriendo la ventana de transición
  • Unique constraint nueva: dedupe de los datos antes, índice único CONCURRENTLY después

Herramientas que sostienen la operación

Squawk hace lint de migrations SQL en el CI y señala operaciones que bloquean tabla. Las apps Rails instalan strong_migrations, que bloquea cambios peligrosos en runtime y sugiere la alternativa segura. MySQL a gran escala corre gh-ost o pt-online-schema-change: ambos copian la tabla con triggers y hacen swap atómico al final. Cierra el ciclo corriendo migrations en el CI contra snapshot de tamaño productivo; el ALTER que pasa en dev con 100 rows explota en staging con 50 millones.