Una migración de esquema puede fallar aunque el cambio de código haya superado las pruebas. En producción, una aplicación no suele cambiar de una sola vez: pueden coexistir procesos web, workers de cola, tareas programadas y réplicas que ejecutan versiones distintas. Si una versión nueva elimina una columna que un worker antiguo todavía lee, o si una columna pasa a ser obligatoria antes de que todos los escritores la rellenen, el despliegue deja de ser compatible.
Las migraciones de base de datos sin interrupciones en PHP tratan el esquema y los datos como componentes de un contrato operativo. El objetivo no es sólo ejecutar una sentencia DDL correcta, sino mantener disponibles las lecturas y escrituras mientras las versiones antigua y nueva conviven, y conservar una vía de recuperación realista.
Por qué el esquema puede romper código ya probado

Las pruebas locales suelen partir de una base de datos creada desde cero o actualizada de forma instantánea. Ese escenario omite la transición: datos históricos incompletos, millones de filas, bloqueos, conexiones persistentes y consumidores asíncronos. Una modificación aparentemente menor puede causar errores o degradación.
- Renombrar o eliminar una columna rompe consultas, mapeadores ORM, informes y procesos que aún usan el nombre anterior.
- Añadir una restricción
NOT NULLfalla si existen filas antiguas sin valor o si un escritor todavía no conoce el nuevo campo. - Cambiar un tipo puede truncar valores, alterar comparaciones, invalidar índices o provocar conversiones costosas.
- Crear un índice o reescribir una tabla grande puede retener bloqueos y aumentar la latencia de operaciones normales.
- Una actualización masiva en una única transacción puede agotar el registro transaccional, competir por recursos o dificultar la réplica.
La pregunta relevante es: ¿qué versiones del código pueden leer y escribir cada representación de un dato durante toda la ventana de despliegue? La respuesta debe incluir los ejecutables que no se reinician automáticamente, no sólo las peticiones HTTP.
Compatibilidad temporal entre código, datos y procesos
Durante un despliegue gradual hay al menos tres estados que deben ser compatibles: código antiguo, código nuevo y datos con formatos antiguo, nuevo o parcialmente transformado. La compatibilidad no consiste necesariamente en que todo consumidor entienda todos los formatos para siempre; consiste en definir una ventana acotada donde las combinaciones previsibles funcionen.
Por ejemplo, para sustituir full_name por first_name y last_name, no conviene borrar el campo original al principio. La versión nueva puede escribir ambos formatos y leer primero los campos nuevos cuando estén completos, con una alternativa explícita al valor antiguo. La versión previa sigue operando con full_name. Una vez transformado el histórico y retirados los consumidores antiguos, la lectura puede depender sólo de la nueva estructura.
Evite que la compatibilidad temporal quede dispersa en controladores. Centralice la lectura, escritura y normalización en un servicio de dominio o repositorio. Así se puede auditar qué versión del formato se produce, qué valor tiene prioridad y cuándo retirar la lógica transitoria. Una plantilla de migración no sustituye este modelo de compatibilidad: la plantilla ejecuta cambios; el modelo define cómo se comporta la aplicación durante la transición.
El patrón expandir, migrar y retirar
1. Expandir sin invalidar a los consumidores actuales
La primera fase añade capacidades sin quitar las existentes: una columna nullable, una tabla nueva, un índice adicional o una estructura paralela. Debe evitar cambios destructivos y, cuando el motor lo requiera, planificar el método de creación para reducir bloqueos. Añadir una columna no implica que sea seguro imponer de inmediato un valor por defecto, recalcular todas las filas o declararla obligatoria.
Antes de ejecutar la operación, revise el tamaño de la tabla, las consultas más frecuentes, claves foráneas, espacio disponible, carga de réplica y comportamiento específico del motor de base de datos. Ensaye sobre una copia representativa o un entorno con volumen y concurrencia comparables. También defina límites observables: duración, latencia admisible, tasa de errores y condición de cancelación.
2. Publicar escritores y lectores compatibles
Después se despliega código que entiende las dos representaciones. Los escritores nuevos pueden realizar escritura dual si el coste y la consistencia lo permiten. Los lectores deben establecer una precedencia inequívoca: leer el valor nuevo si está validado; de lo contrario, usar el antiguo. No use una excepción como mecanismo de fallback, porque oculta defectos de datos y añade trabajo innecesario a la ruta crítica.
La escritura dual exige decisiones explícitas. Si una actualización afecta a ambas estructuras, determine si debe realizarse en la misma transacción. Si no es posible, diseñe una reconciliación idempotente y métricas para detectar divergencias. Los eventos, caches, APIs y exportaciones también son consumidores: modificar sólo el repositorio PHP no garantiza compatibilidad de extremo a extremo.
3. Migrar el histórico de forma reanudable
Tras habilitar el código compatible, transforme los registros existentes en lotes pequeños. Cada lote debe poder repetirse sin duplicar efectos ni corromper datos. Use una clave estable o un cursor persistente, límites de tamaño, registro del progreso y reintentos controlados. Evite paginar con desplazamientos sobre conjuntos que cambian, ya que puede saltar o reprocesar filas.
$lastId = 0; // Para una clave primaria positiva y creciente.
while (true) {
$rows = $repository->findPendingAfterId($lastId, 500);
if ($rows === []) {
break;
}
foreach ($rows as $row) {
$repository->migrateIfNeeded($row);
$lastId = $row->id;
}
}Este patrón requiere que findPendingAfterId() devuelva filas ordenadas de forma ascendente por la misma clave usada como cursor. El cursor comienza en un valor anterior al primer identificador válido y avanza sólo después de procesar cada fila; la terminación depende de que la consulta no devuelva ningún lote. En una ejecución reanudada, el valor confirmado de $lastId debe persistirse. migrateIfNeeded() debe verificar el estado actual y producir el mismo resultado si se ejecuta de nuevo.
Mida filas pendientes, filas transformadas, errores de validación y diferencias entre formatos. No declare terminada la fase por haber recorrido la tabla: compruebe también integridad referencial, unicidad, totales de negocio y muestras de registros críticos.
4. Cambiar lecturas, observar y retirar
Cuando el histórico esté completo y los procesos antiguos hayan dejado de ejecutarse, cambie las lecturas para usar exclusivamente la estructura nueva. Esta activación puede ser gradual mediante una configuración controlada, pero no debe confundirse con el despliegue: desplegar pone código disponible; activar modifica qué ruta usa el tráfico.
Observe errores de consulta, campos nulos inesperados, discrepancias funcionales, tiempos de respuesta y salud de workers. Sólo tras una ventana de observación definida retire la escritura dual, dependencias transitorias y, finalmente, la columna, índice o tabla antigua. Conservar estructuras obsoletas indefinidamente aumenta ambigüedad y coste; eliminarlas antes de tiempo elimina la recuperación sencilla.
Nulos, tipos, restricciones e índices sin detener la operación
Una columna nueva suele empezar como nullable porque los registros históricos aún no la tienen. La aplicación debe tratar la ausencia como un estado previsto, no como un caso imposible. Después de completar y validar el backfill, puede imponerse una restricción, siempre que todos los escritores activos proporcionen un valor válido.
Para cambios de tipo, cree una columna nueva y convierta los valores de manera explícita. Esto permite detectar valores no convertibles, aplicar reglas de redondeo o normalización y comparar ambos resultados antes de reemplazar la columna anterior. Cambiar directamente el tipo puede ser adecuado en casos limitados, pero debe justificarse con el comportamiento del motor, el volumen y la compatibilidad de las consultas.
Los índices requieren un análisis equivalente. Un índice nuevo puede mejorar lecturas, pero su construcción consume recursos y una estrategia de creación inadecuada puede bloquear escrituras. Valide el plan de ejecución de la consulta que lo necesita; no añada índices por intuición. Si el motor ofrece modalidades de creación con menor bloqueo, comprenda sus requisitos y limitaciones antes de incorporarlas al plan.
Rollback: reversión de código no siempre implica reversión de datos
Una reversión operativa debe separarse en decisiones. Mientras existe la estructura antigua y la escritura dual, suele ser posible volver al código previo. Pero si el formato nuevo ha aceptado información que el modelo antiguo no puede representar, deshacer el esquema no recupera semánticamente esos datos.
- Reversible: desactivar una lectura nueva y volver al fallback, manteniendo ambas estructuras.
- Compensable: corregir o reconstruir datos desde una fuente definida, con un proceso auditado.
- Irreversible: borrar una estructura o aceptar transformaciones que pierden precisión sin conservar el original.
Documente el punto de no retorno, el responsable de autorizarlo, las copias o exportaciones necesarias y el procedimiento para pausar workers. Un método down() en una herramienta de migraciones no es por sí mismo un plan de rollback: puede revertir DDL, pero no garantiza la validez de los datos escritos durante la transición.
Pruebas, evidencias y lista de comprobación

Pruebe una matriz de compatibilidad: código antiguo con esquema expandido, código nuevo con datos aún no migrados, código nuevo con datos transformados y procesos asíncronos en versiones mezcladas. Incluya migraciones interrumpidas y reanudadas, registros inválidos, concurrencia de escritura y restauración de una versión anterior cuando sea aplicable.
- Inventariar tablas, consultas, workers, integraciones y reportes afectados.
- Definir el contrato temporal de lectura y escritura, incluidos valores nulos y prioridades.
- Separar expansión, despliegue compatible, backfill, activación y retirada en pasos independientes.
- Estimar impacto de DDL, índices y lotes con datos representativos.
- Hacer el proceso de datos idempotente, reanudable y medible.
- Establecer validaciones de integridad y umbrales de observación posteriores.
- Documentar rollback, compensaciones y el punto de no retorno.
- Retirar la compatibilidad y estructura antiguas sólo con evidencias de que no quedan consumidores.
Aplicado con disciplina, este patrón convierte un cambio de base de datos de alto riesgo en una secuencia verificable. La clave es diseñar la convivencia como parte del producto y de la operación, no como un detalle oculto dentro de una migración.



