Migraciones de base de datos sin downtime
Una migración que toma un bloqueo exclusivo en el momento del deploy es una caída programada con distintivo de deploy. Las migraciones Postgres sin downtime usan expand/contract: agregar esquema antes de eliminarlo, hacer backfill antes de aplicar restricciones y nunca bloquear lecturas en el hot path.
Deploy el viernes. Corre la migración. ALTER TABLE orders ADD COLUMN status_v2 VARCHAR(20) NOT NULL DEFAULT 'pending' — salvo que la tabla tiene cuarenta millones de filas y Postgres reescribe el heap bajo un bloqueo ACCESS EXCLUSIVE. El checkout se detiene once minutos. El equipo lo llama deploy, no caída. Los clientes lo llaman de otra forma. La migración Postgres sin downtime no es una feature de herramienta: es una disciplina — expandir el esquema junto al viejo, migrar datos de forma asíncrona, contraer solo tras la prueba y tratar la duración del bloqueo como bloqueante de release.
La producción no se pausa por cambios de esquema. Las aplicaciones se despliegan de forma continua. Versiones viejas y nuevas del código corren en simultáneo durante los deploys. Las migraciones deben funcionar mientras ambas versiones están activas — compatibles hacia atrás en la subida, compatibles hacia adelante hasta que termine la fase de contract.
Los modos de bloqueo deciden si el deploy es migración o caída
El DDL de Postgres adquiere niveles de bloqueo que impiden acceso concurrente:
| Lock level | Blocks reads? | Blocks writes? | Typical DDL |
|---|---|---|---|
ACCESS SHARE | No | No | inicio de CREATE INDEX CONCURRENTLY |
ROW EXCLUSIVE | No | No | INSERT, UPDATE, DELETE |
SHARE UPDATE EXCLUSIVE | No | Bloqueo breve de escritura | VACUUM, CREATE INDEX CONCURRENTLY |
ACCESS EXCLUSIVE | Sí | Sí | la mayoría de formas de ALTER TABLE, DROP |
Cualquier migración que requiera ACCESS EXCLUSIVE en una tabla caliente en hora pico es un plan de caída. Medir la espera de bloqueo en staging con conteos de filas al estilo producción — no tablas vacías.
Una columna NOT NULL agregada en una transacción sobre una tabla grande es un bloqueo, no una estrategia de migración.
Expand/contract es el patrón de migración sin downtime
Tres fases, a menudo en varios deploys:
Expand. Agregar esquema nuevo sin romper el código viejo.
- Agregar columna nullable — el código viejo la ignora.
- Agregar tabla nueva junto a la vieja — dual-write o sync asíncrono.
- Agregar índice nuevo
CONCURRENTLY— sin bloqueo de tabla para lecturas/escrituras. - Agregar trigger o columna generada para el período de transición.
Migrate. Mover datos y tráfico.
- Hacer backfill de la columna nueva por lotes —
UPDATE ... WHERE id BETWEEN ? AND ? LIMIT 1000con pausa entre lotes. - Dual-write: la aplicación escribe en columnas/tablas viejas y nuevas.
- Cambiar lecturas al camino nuevo vía feature flag cuando el backfill termine.
- Validar con consultas de reconciliación — conteos, checksums, diffs por muestra.
Contract. Quitar esquema viejo solo tras la prueba.
- Eliminar columna vieja — solo cuando ningún código la lee o escribe.
- Eliminar tabla vieja — solo tras tráfico en cero por un período sostenido.
- Agregar restricción
NOT NULL— solo tras confirmar con backfill que no hay nulls.
Deploy 1 (expand): ADD COLUMN email_normalized TEXT NULL
Deploy 2 (migrate): backfill job + app dual-write
Deploy 3 (migrate): app reads email_normalized; flag on
Deploy 4 (contract): DROP COLUMN email_legacy — after N days zero traffic
El artículo sobre estrategia de rollback para deploys cubre qué pasa cuando fallan deploys en fase migrate — el rollback en fase expand suele ser revertir la app mientras el esquema queda; el rollback en fase contract puede ser imposible sin un forward-fix.
Operaciones de alto riesgo y sus alternativas seguras
| Risky migration | Why it locks or blocks | Safe pattern |
|---|---|---|
ADD COLUMN ... NOT NULL en tabla grande | Reescribe la tabla, bloqueo exclusivo | Agregar nullable → backfill → SET NOT NULL con check constraint por pasos |
CREATE INDEX (no concurrent) | Bloqueo SHARE impide escrituras | CREATE INDEX CONCURRENTLY |
| Cambio de tipo de columna | Reescritura, bloqueo exclusivo | Columna nueva + backfill + swap de rename en contract |
ADD FOREIGN KEY | Valida todas las filas, bloqueo fuerte | Restricción NOT VALID primero → VALIDATE CONSTRAINT por separado |
Reescritura de tabla (ALTER ... TYPE) | Bloqueo total | Tabla nueva + copia + swap de vistas o routing |
| Renombrar columna | Rompe el código viejo al instante | Agregar nueva, dual-write, deprecar nombre viejo en código primero |
CREATE INDEX CONCURRENTLY no puede ejecutarse dentro de un bloque de transacción. Las herramientas de migración que envuelven todo en transacciones necesitan manejo especial para creación concurrente de índices.
Backfill sin derretir la base de datos
Los backfills por lotes protegen la producción:
-- Repeat until zero rows affected
UPDATE orders
SET status_v2 = map_status(status_legacy)
WHERE id IN (
SELECT id FROM orders
WHERE status_v2 IS NULL
LIMIT 5000
FOR UPDATE SKIP LOCKED
);Patrones:
- Tamaño de lote ajustado al I/O — monitorear replication lag en réplicas durante el backfill.
SKIP LOCKED— evitar pelear con escrituras en vivo por las mismas filas.- Programación fuera de pico — el backfill es carga de producción.
- Seguimiento de progreso — filas restantes, ETA, interruptor de pausa para incidentes.
- Backfill idempotente — seguro de reiniciar; guarda
WHERE new_col IS NULL.
Los jobs de backfill compiten con el tráfico en vivo por I/O y slots del connection pool. Coordinar con connection pooling en serverless — backfills de larga duración por el mismo pool que los request handlers ahogan las consultas de la aplicación.
Matriz de compatibilidad de aplicación por deploy
Cada deploy de migración necesita una matriz escrita:
| Deploy phase | Old app (N) | New app (N+1) | Schema state |
|---|---|---|---|
| Expand | Works | Works | Existen columnas nuevas nullable |
| Dual-write | Escribe solo col vieja | Escribe ambas | Ambas columnas se pueblan con el tiempo |
| Read switch | Lee vieja | Lee nueva | Backfill completo |
| Contract | Debe estar fuera | Works | Columna vieja eliminada |
Si N y N+1 deben correr en simultáneo — siempre cierto en rolling deploys — los cambios de esquema no deben romper N. La fase contract exige prueba de que N ya no está en ejecución.
¿Cómo deben ejecutar los equipos migraciones Postgres sin downtime?
Estas prácticas separan la disciplina expand/contract de los deploys de bloquear-y-rezar.
¿Pueden ser todas las migraciones sin downtime?
No. Algunos cambios — dividir una tabla muy contenciosa, reescrituras fundamentales del modelo — requieren ventanas de mantenimiento o modo solo lectura. El objetivo es sin downtime por defecto, ventana de mantenimiento por decisión explícita con aviso a stakeholders — no por accidente cuando un bloqueo golpea producción.
¿Cuántos deploys debe tomar expand/contract?
Los que hagan falta por seguridad. Secuencias de cuatro deploys son normales. Apurar contract antes de que termine el backfill crea corrupción de datos, no velocidad.
¿Cuándo es aceptable una ventana de mantenimiento?
Períodos de bajo tráfico para operaciones que genuinamente no pueden hacerse concurrentes — raro con el tooling de Postgres. Documentar RTO, avisar a clientes, ensayar. No usar ventanas de mantenimiento para evitar aprender CREATE INDEX CONCURRENTLY.
Una visión opuesta
Un argumento frecuente sostiene que las migraciones sin downtime añaden complejidad desproporcionada al riesgo de caída — que equipos pequeños deberían aceptar bloqueos breves y enviar más rápido.
Bloqueos breves en tablas pequeñas están bien. Bloqueos breves en tablas calientes de millones de filas no son breves. La complejidad de expand/contract se carga al inicio y queda documentada; la complejidad de caídas no planificadas de once minutos es reactiva y visible para el cliente. Los equipos que miden duración de bloqueo en staging rara vez eligen el bloqueo.
Key takeaways
- Las migraciones sin downtime usan expand/contract en varios deploys — nunca DDL big-bang en tablas calientes.
- Los bloqueos
ACCESS EXCLUSIVEen tablas de producción son planes de caída — medir en staging con conteos reales de filas. - Agregar columnas nullable primero; backfill por lotes; forzar NOT NULL solo tras completar el backfill.
CREATE INDEX CONCURRENTLYy foreign keysNOT VALIDevitan bloqueos de validación.- Escribir matriz de compatibilidad de app por deploy — código viejo y nuevo corren en simultáneo en rolling deploys.
- El backfill es carga de producción — lotear, monitorear replication lag en réplicas, coordinar con connection pools.
Conclusion
Las migraciones de base de datos sin downtime son un contrato con la producción en ejecución: los cambios de esquema llegan compatibles con el código que sirve tráfico ahora, los datos se mueven en cronogramas que respetan límites de I/O y los pasos destructivos de contract esperan la prueba.
La próxima migración debe clasificarse antes de que alguien escriba SQL: ¿expand, migrate o contract? Si la respuesta es «los tres en una transacción», devolverla a rediseño. La producción no se pausa — las migraciones no deberían pedírselo.