Marcio Cunha

Cómo migrar bases de datos en producción sin interrupciones

Descubre cómo alterar esquemas de bases de datos en producción sin interrumpir la disponibilidad de servicios usando el patrón expand contract paso a paso.

Marcio Cunha•5 min
También disponible en:PortuguêsEnglish
Resumen
  • El patrón expand contract divide cambios complejos de base de datos en fases independientes y seguras para producción
  • Sistemas heredados y aplicaciones modernas deben coexistir temporalmente con columnas duplicadas durante la transición
  • La fase de expansión prepara el terreno agregando nuevos campos sin romper el código antiguo que sigue en ejecución
  • La fase de contrato asegura estabilidad retroactiva antes de eliminar código obsoleto y campos antiguos
  • El monitoreo continuo de registros y métricas evita sorpresas desagradables durante el giro definitivo de llaves

Por qué cambiar bases de datos en producción suele causar dolores de cabeza

Quien trabaja en ingeniería de software sabe que alterar el esquema de una base de datos en producción suele ser una tarea rodeada de aprensión. En sistemas de alta disponibilidad, detener el servicio por unos minutos para ejecutar un comando de alteración de tabla significa perder ingresos, frustrar usuarios y generar tickets de soporte urgente. En la práctica, esto ocurre porque las aplicaciones y las bases de datos hablan un idioma rígido, donde cualquier cambio estructural repentino hace que el código antiguo arroje errores en cascada.

Para sortear este problema sin mostrar pantallas de mantenimiento de madrugada, los arquitectos de software adoptan estrategias basadas en versionado gradual y compatibilidad retroactiva. Este cuidado es similar al que aplicamos al planificar cómo migrar bases de datos de un monolito sin downtime, asegurando que el tráfico real siga fluyendo mientras las bases del sistema se reemplazan por debajo. El secreto consiste en separar los cambios físicos de datos de las modificaciones lógicas del código de la aplicación.

Qué es y cómo funciona el patrón expand contract en la práctica

El patrón expand contract consiste en dividir cualquier cambio complejo de base de datos en tres etapas distintas: expansión, transición y contrato. En la primera fase, llamada expansión, alteras la base de datos únicamente para agregar nuevos elementos, como una nueva columna o tabla, sin eliminar ni modificar lo existente. Así, la aplicación actual sigue funcionando perfectamente porque nada de lo que usa fue roto o cambiado de lugar.

En la fase intermedia, actualizas la aplicación para comenzar a escribir datos tanto en el formato antiguo como en el nuevo formato simultáneamente. Finalmente, en la fase de contrato, después de que todo el código ha sido actualizado y los datos antiguos se han migrado en segundo plano, eliminas el esquema antiguo con total seguridad. Este flujo garantiza que el sistema nunca quede inactivo, ya que en ningún momento la base de datos deja de atender las solicitudes que la aplicación espera recibir.

Paso a paso para aplicar la expansión sin interrumpir el servicio

El primer paso práctico para aplicar esta técnica es crear la nueva estructura en la base de datos sin tocar el código existente. Imagina que necesitas renombrar la columna full_name a first_name y last_name en una tabla de usuarios muy accedida. Si simplemente alteras la columna directamente, el sistema actual fallará al intentar buscar un campo que desapareció.

Para resolver esto, ejecutas un comando SQL simple para agregar las nuevas columnas permitiendo valores nulos, como se muestra en el ejemplo a continuación:

ALTER TABLE users ADD COLUMN first_name VARCHAR(100);
ALTER TABLE users ADD COLUMN last_name VARCHAR(100);

Una vez ejecutado, la base de datos gana nuevo espacio de trabajo, pero la aplicación continúa usando el campo antiguo full_name sin notar ningún cambio en el entorno de producción.

Actualizando el código de la aplicación con escritura dual

Con las nuevas columnas disponibles en la base de datos, el segundo paso exige actualizar el código de la aplicación para adoptar la estrategia de escritura dual. En la práctica, cada vez que un usuario se registra o actualiza su perfil, el sistema completa tanto la columna antigua como las nuevas columnas recién creadas. Este comportamiento garantiza que la información permanezca redundante y segura en ambos lugares durante el periodo de transición.

Para asegurar que las consultas complejas sigan siendo performantes durante esta fase de duplicación, vale la pena revisar la indexación de las tablas afectadas, siguiendo preceptos similares a los discutidos al analizar cómo optimizar consultas con índices compuestos. Un buen índice evita que la escritura dual genere cuellos de botella de rendimiento y mantiene la respuesta de la API rápida para el usuario final.

A continuación tenemos un ejemplo ilustrativo en código que demuestra cómo realizar esta inserción dual de forma segura en la capa de servicio de tu aplicación:

async function updateUserProfile(userId, fullName) {
const parts = fullName.split(' ');
const firstName = parts[0];
const lastName = parts.slice(1).join(' ');

await db.query(
'UPDATE users SET full_name = $1, first_name = $2, last_name = $3 WHERE id = $4',
[fullName, firstName, lastName, userId]
);
}

Migrando el historial de datos legados en segundo plano

Mientras la aplicación nueva escribe en ambos lugares, los registros antiguos que ya estaban en la base de datos continúan solo con el dato completo en la columna heredada. Para resolver esta brecha sin congelar el sistema, debes ejecutar un script de migración en segundo plano, conocido como backfill. Este script lee los registros en lotes pequeños y va rellenando las nuevas columnas poco a poco, respetando los límites de consumo de CPU y disco del servidor.

Este enfoque evita que la base de datos se bloquee por exceso de candados en tablas gigantescas. Cuando el script termina de recorrer toda la tabla, significa que el 100% de los datos históricos ya tienen el nuevo formato poblado y listo para ser consumido por las próximas versiones del software.

Contratando y eliminando el código antiguo con seguridad

La última etapa del proceso es el contrato, momento en que limpias la suciedad dejada por las fases anteriores. Después de que el código de la aplicación ha sido actualizado para leer exclusivamente de las nuevas columnas first_name y last_name, y el script de migración ha terminado su trabajo, la columna antigua full_name deja de tener utilidad práctica.

En este punto, puedes realizar una nueva alteración en la base de datos para eliminar la columna antigua y limpiar el código heredado de la aplicación con total tranquilidad. Esta disciplina operativa transforma una tarea estresante en una rutina predecible de ingeniería, permitiendo entregas continuas de valor sin sacrificar la estabilidad del negocio. Para profundizar en estos conceptos arquitectónicos en producción, consulte también nuestro análisis técnico sobre Cómo monitorizar contenedores en tiempo real con eBPF en Linux.

Consideraciones finales sobre la migración sin paradas

Adoptar el patrón expand contract exige disciplina por parte del equipo de ingeniería, ya que transformamos una tarea que se haría en un solo comando riesgoso en un proceso dividido en etapas controladas. Aunque requiere más líneas de código temporal, el ganado en términos de resiliencia y tranquilidad compensa ampliamente el esfuerzo adicional.

Al dominar esta técnica, tu equipo gana autonomía para evolucionar el modelo de datos de sistemas complejos en cualquier momento del día, sin depender de ventanas de mantenimiento nocturnas o fines de semana prolongados. El resultado es un producto más ágil, confiable y preparado para crecer de forma sostenible.