Marcio Cunha

Migraciones de Bases de Datos sin Interrupción Usando el Patrón Expand and Contract

Aprende cómo modificar esquemas de bases de datos relacionales en producción sin interrumpir el servicio aplicando la técnica paso a paso de expansión y contracción.

Marcio Cunha12 min
También disponible en:EnglishPortuguês
Resumen
  • Las alteraciones estructurales directas en bases de datos grandes causan bloqueos que tumban sistemas enteros durante horas.
  • El método de expandir y contraer desacopla el cambio de base de datos en etapas aisladas que conviven pacíficamente con código antiguo y nuevo.
  • La creación de columnas intermedias permite que versiones antiguas y actualizadas de la aplicación lean y escriban datos simultáneamente sin corrupción.
  • La eliminación definitiva de columnas heredadas ocurre solo después de ciclos completos de publicación y validación en el entorno de producción.
  • La disciplina rigurosa de retrocompatibilidad elimina el riesgo de interrupciones y devuelve la previsibilidad a los ciclos de entrega de software.

El desafío invisible de los cambios estructurales en bases de datos

Cuando necesitamos alterar el formato de los datos en un sistema a gran escala, el primer instinto suele ser sencillo: alterar la tabla y desplegar el código nuevo. En la práctica, los sistemas en producción procesan miles de solicitudes por segundo, y cualquier comando que bloquee el acceso a la tabla principal causa fallas en cascada, frustrando a usuarios y sobrecargando equipos de soporte.

En bases de datos relacionales como PostgreSQL o MySQL, alterar una columna o agregar restricciones complejas a menudo requiere que el sistema reescriba por completo el archivo de datos en el disco. Durante esta reescritura, la base de datos bloquea la tabla para evitar inconsistencias, convirtiendo una simple actualización de rutina en un apagón generalizado.

Para resolver este dilema sin interrumpir el tráfico de usuarios, la ingeniería de software adopta el patrón conocido como Expand and Contract, o expansión y contracción. En términos prácticos, este enfoque divide un cambio riesgoso en varios pasos incrementales y seguros, permitiendo que la aplicación evolucione de forma continua sin que nadie note la transición tras bambalinas.

Comprendiendo el ciclo de vida de expansión y contracción

El concepto fundamental detrás de Expand and Contract es garantizar que los cambios en la base de datos ocurran siempre de manera aditiva antes de volverse destructivos. En lugar de modificar lo que ya existe, crecemos creando nuevas estructuras junto a las viejas, poblando ambas temporalmente hasta que todo el sistema dependa exclusivamente de la novedad.

Este ciclo suele dividirse en tres fases distintas: expansión, migración y contracción. En la expansión, preparamos la base de datos para aceptar el nuevo formato sin romper el código heredado que sigue ejecutándose en los servidores de producción. En la migración, movemos los datos antiguos al nuevo lugar y actualizamos gradualmente el código de la aplicación.

En la última fase, contracción, eliminamos con seguridad todo lo que se ha vuelto obsoleto, como columnas antiguas o tablas duplicadas. Este cuidado quirúrgico evita el temido momento de inactividad planeada, asegurando que el servicio permanezca accesible las veinticuatro horas del día.

Escenario práctico: dividiendo el nombre completo en nombre y apellido

Imaginemos que tenemos una tabla llamada clientes con una sola columna llamada nombre_completo, que contiene el nombre y el apellido juntos. La nueva regla de negocio requiere que el sistema almacene estos datos por separado en dos columnas distintas: primer_nombre y apellido.

Si alteráramos la tabla de un solo golpe para borrar la columna antigua y crear las dos nuevas, todas las consultas y registros en curso fallarían instantáneamente. El código actual de la aplicación intentará escribir datos en la columna antigua que ya no existe, generando errores críticos de ejecución que tumban el flujo de pagos o registros.

Para evitar este colapso, aplicamos el primer paso del patrón: agregamos las nuevas columnas primer_nombre y apellido a la tabla, dejando la columna antigua nombre_completo intacta. En este preciso momento, la base de datos tiene tanto la estructura vieja como la nueva coexistiendo en armonía, abriendo camino para la transición del código.

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

Con las nuevas columnas creadas en la base de datos, el siguiente paso es actualizar la aplicación para soportar un estado de transición conocido como escritura dual. Durante este período, el código de la aplicación se ajusta para continuar escribiendo el nombre completo en la columna antigua, pero también poblando las columnas nuevas recién creadas.

Para consultas y lecturas, el sistema puede seguir utilizando la columna antigua mientras el proceso de copia en segundo plano no concluye. Esta redundancia momentánea es el secreto para evitar cualquier interrupción, asegurando que, si ocurre un error en la nueva lógica, el flujo principal continúe operando sin interrupciones.

A continuación tenemos un ejemplo ilustrativo en código que demuestra cómo la aplicación maneja esta escritura dual durante el período de transición estructural:

def actualizar_cliente(conexion, cliente_id, nombre_completo):
partes = nombre_completo.split(' ', 1)
primero = partes[0]
apellido = partes[1] if len(partes) > 1 else ''

# Escritura dual: mantenemos la columna antigua y poblamos las nuevas
cursor = conexion.cursor()
cursor.execute(
"UPDATE clientes SET nombre_completo = %s, primer_nombre = %s, apellido = %s WHERE id = %s",
(nombre_completo, primero, apellido, cliente_id)
)
conexion.commit()

Este fragmento sencillo de código ilustra perfectamente cómo el sistema actúa como un puente entre el pasado y el futuro, garantizando que ningún dato se pierda o sea interpretado incorrectamente por las rutinas de negocio.

Sincronizando el histórico de datos existentes

Agregar columnas y actualizar el código para nuevos registros resuelve solo la mitad del problema, porque los registros antiguos que ya estaban en la base de datos siguen con las columnas nuevas vacías. Para llenar este vacío, necesitamos ejecutar un proceso auxiliar en lotes conocido como migración de datos históricos.

Este proceso lee los registros antiguos en pequeños bloques, por ejemplo, mil filas a la vez, y actualiza las columnas nuevas correspondientes sin sobrecargar la memoria del servidor de base de datos. Hacer esto en lotes controlados evita picos repentinos de uso de CPU y disco que podrían volver lento el sistema para los usuarios finales.

Tras completar este rastreo, todas las filas de la tabla pasan a tener los datos duplicados correctamente entre la estructura antigua y la nueva. En este punto, la aplicación ya está lista para cambiar su fuente principal de lectura hacia las columnas recién creadas.

Ajustando consultas y preparando la contracción

Con los datos históricos totalmente sincronizados y el código adaptado para escribir en ambos lugares, el paso siguiente consiste en alterar las consultas de la aplicación para que lean exclusivamente de las columnas nuevas. Esto garantiza que la lógica de negocio esté validada y operando con el nuevo esquema estructural.

Después de desplegar este cambio y monitorear el sistema durante unos días para asegurar que no hay errores ocultos, entramos en la fase final del patrón conocida como contracción. En esta etapa, removemos el código que escribía en la columna antigua y, finalmente, eliminamos la columna nombre_completo de la tabla de la base de datos.

La eliminación de una columna antigua en sistemas modernos debe hacerse con precaución, pero como la aplicación ya dejó totalmente de usarla, esta operación ocurre sin ningún riesgo de ruptura o pérdida de datos en producción.

Consideraciones finales sobre la evolución segura de esquemas

Implementar cambios estructurales sin interrumpir el funcionamiento del sistema exige disciplina, planificación y la descomposición de tareas complejas en pasos más pequeños y reversibles. El patrón Expand and Contract transforma una operación estresante de base de datos en una rutina predecible y segura de ingeniería.

Aunque este enfoque requiere más líneas de código temporal y un mayor esfuerzo de planificación inicial, el retorno de inversión es invaluable. Garantizar alta disponibilidad y confianza operacional consolida la madurez técnica del equipo y preserva la mejor experiencia posible para quienes utilizan el sistema todos los días.