🗺️ Contexto#
Una migración sobre una tabla caliente —ALTER TABLE users ADD COLUMN ...— toma el lock exclusivo y no lo suelta hasta terminar. Durante veinte minutos, ningún read y ningún write entran. La aplicación devuelve 503 y el negocio pierde dinero por hora.
Justificación#
Lo que hace incómodo este caso es que el trabajo total no cambia. Rellenar dos millones de filas cuesta lo que cuesta, se haga de una vez o en mil lotes. Lo que cambia es cómo se reparte: un lock de veinte minutos contra mil locks de un segundo.
Y hay un detalle que lo vuelve difícil de detectar antes de que ocurra: el proceso sigue vivo. El healthcheck responde, el contenedor no se reinicia, ninguna alerta de disponibilidad de proceso dispara. Lo único que falla son las peticiones — y en muchos sistemas eso se ve como «latencia alta», no como caída.
La solución tiene cuatro fases y un orden que no es negociable:
- Expand — agregar la columna nullable. Es metadata: instantáneo.
- Backfill — rellenar por lotes, soltando el lock entre cada uno.
- Switch — un feature flag cambia lecturas y escrituras a la columna nueva.
- Contract — recién ahora, en una migración posterior, se borra la vieja.
El switch va antes del contract porque el flag es lo único reversible en un segundo. Si se borra la columna vieja primero, volver atrás requiere otra migración — y a esa altura ya no hay a dónde volver.
📇 Ficha del caso#
| Categoría | Entrega |
| Estado | OPERATIVO |
| Stacks operativos | 7 de 7 |
Un ALTER TABLE sobre una tabla caliente bloquea la aplicación entera; expand-contract reparte el mismo trabajo en lotes que nadie nota.
🧱 Dónde correrlo#
| Stack | Versión | URL en el hub | Implementación |
|---|---|---|---|
| 🐘 PHP | PHP 8.3 | http://localhost:8100/17/ | README |
| 🐍 Python | Python 3.12 | http://localhost:8200/17/ | README |
| 🟢 Node.js | Node.js 22 | http://localhost:8300/17/ | README |
| ☕ Java | Java 21 | http://localhost:8400/17/ | README |
| 🔵 .NET | .NET 8 | http://localhost:8500/17/ | README |
| 🐹 Go | Go 1.23 | http://localhost:8600/17/ | README |
| 🦀 Rust | Rust 1.83 | http://localhost:8700/17/ | README |
⚠️ Nota de honestidad del caso: no hay PostgreSQL detrás. El lock de la tabla se modela con el read-write lock de cada runtime — que es honesto, porque el mecanismo es el mismo: un escritor excluye a todos los lectores. La excepción es PHP, donde
flocksí es un lock del sistema operativo entre procesos. El tiempo de migración es una espera, no CPU: unALTER TABLEse demora esperando I/O del motor.
Caso 17 · Migración de esquema sin downtime — ⬅️ README del caso · ⚖️ Comparativa de los 7 stacks
🗺️ Contexto · 🩺 Síntomas · 🔍 Diagnóstico · 🧠 Causas raíz · 🛠️ Opciones de solución · ⚖️ Trade-offs · 💼 Valor de negocio · 🚨 Postmortem