Migración de Bases de Datos en 2026: Zero Downtime

Profile picture of Equipo Arvucore

Equipo Arvucore

September 22, 2025 · Actualizado el August 26, 2026

16 min read

Una migración de base de datos con zero downtime es un cambio en el esquema o en los datos dividido en pasos lo bastante pequeños como para que la aplicación en ejecución nunca se rompa: añade la estructura nueva, haz que el código funcione con ambas formas, mueve los datos y luego elimina la estructura antigua. El patrón se llama expand-and-contract y es el núcleo de toda estrategia de migración en producción. Todo lo demás en esta guía (DDL online, procesamiento por lotes, herramientas, rollback) existe para que esos pasos sean seguros en tablas grandes con tráfico real.

Tres tipos de migración de bases de datos

La palabra "migración" cubre tres problemas distintos. Necesitan planes distintos.

Tipo Qué cambia Riesgo típico Técnica principal
Migración de esquema Tablas, columnas, índices, constraints Bloqueos, DDL largo, rotura del código antiguo Expand-and-contract, DDL online
Migración de datos Las propias filas: backfills, reformateo, mover valores entre columnas Transacciones largas, lag de replicación, estado parcial Lotes, jobs idempotentes, reconciliación
Migración de plataforma El motor o el host: MySQL a PostgreSQL, on-prem a nube gestionada, una versión major a otra Diferencias semánticas, cutover, pérdida de datos Replicación/CDC, dual write, cutover ensayado

La mayoría de las releases solo necesitan las dos primeras. Una migración de plataforma es un proyecto, no un paso del deploy, y normalmente forma parte de un esfuerzo más amplio, como migrar sistemas heredados a arquitecturas modernas. Este artículo se centra en las migraciones de esquema y de datos, que son las que los equipos entregan cada semana.

Una regla aplica a las tres: la base de datos nunca debe depender de que una versión concreta de la aplicación esté activa. El código antiguo y el nuevo se solaparán durante cualquier rolling deploy, y una migración que asuma lo contrario provoca una caída justo en el momento en que no puedes hacer rollback limpio.

Deploys retrocompatibles: la restricción de la que deriva todo

Durante un deploy rolling o blue-green hay una ventana en la que la versión N y la versión N+1 de la aplicación hablan con la misma base de datos. Si usas deploys canary o rolling, esa ventana puede durar horas. La migración tiene que ser compatible con ambas.

En la práctica, eso significa:

  • Los cambios aditivos van primero, en su propia migración, antes del código que los usa. Columnas nuevas nullable, tablas nuevas, índices nuevos.
  • Los cambios destructivos van al final, cuando el código antiguo ya no existe. Drops, renombrados, NOT NULL en una columna existente, cambios de tipo.
  • Ninguna migración renombra ni borra algo que la versión actual lee. Renombrar es el error clásico: ALTER TABLE ... RENAME COLUMN es instantáneo, y rompe todas las consultas de la versión antigua al instante.
  • La aplicación tolera ambas formas durante toda la transición: escribe en ambas columnas, o lee la nueva con fallback a la antigua.

Si tu ORM genera un rename o un drop automáticamente, tómalo como señal para parar y dividir el cambio. Un feature flag permite cambiar las lecturas de la antigua a la nueva sin deploy, lo que hace reversibles los pasos intermedios.

Expand-and-contract, paso a paso

El patrón tiene cuatro fases. Cada una es un deploy separado o una migración separada, nunca agrupadas.

  1. Expand: añade la estructura nueva sin tocar la antigua.
  2. Migrar el código: la aplicación escribe en ambas, lee de la nueva (con fallback).
  3. Backfill: copia los datos existentes a la estructura nueva, por lotes.
  4. Contract: cambia las lecturas por completo, añade constraints, elimina la estructura antigua.

Ejemplo 1: renombrar una columna

Objetivo: renombrar users.fullname a users.display_name en una tabla con millones de filas.

Paso 1, expand (migración, antes del deploy):

ALTER TABLE users ADD COLUMN display_name text;

Añadir una columna nullable sin default es un cambio solo de metadatos en PostgreSQL y en MySQL moderno (InnoDB instant ADD COLUMN). No reescribe la tabla.

Paso 2, dual write (deploy de la aplicación):

El código escribe en ambas columnas en cada insert y update, y lee display_name con fallback a fullname. Si no puedes cambiar todas las rutas de escritura (jobs heredados, otros servicios), un trigger las mantiene sincronizadas hasta que puedas:

CREATE OR REPLACE FUNCTION sync_display_name() RETURNS trigger AS $$
BEGIN
  IF NEW.display_name IS NULL THEN NEW.display_name := NEW.fullname; END IF;
  IF NEW.fullname IS NULL THEN NEW.fullname := NEW.display_name; END IF;
  RETURN NEW;
END $$ LANGUAGE plpgsql;

CREATE TRIGGER users_sync_display_name
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_display_name();

Paso 3, backfill (job por lotes, no un único UPDATE):

UPDATE users
SET display_name = fullname
WHERE id IN (
  SELECT id FROM users
  WHERE display_name IS NULL AND fullname IS NOT NULL
  ORDER BY id
  LIMIT 5000
);

Ejecútalo en bucle hasta que afecte a cero filas, con un sleep corto entre lotes. Cada lote es su propia transacción, así que los bloqueos son cortos y la replicación sigue el ritmo.

Paso 4, contract (después de retirar por completo el código antiguo):

ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;
DROP TRIGGER users_sync_display_name ON users;
DROP FUNCTION sync_display_name();
ALTER TABLE users DROP COLUMN fullname;

En PostgreSQL, SET NOT NULL recorre la tabla. En una tabla grande, añade primero CHECK (display_name IS NOT NULL) NOT VALID, luego VALIDATE CONSTRAINT (que solo toma un bloqueo débil), y entonces SET NOT NULL usa la check para saltarse el recorrido.

Cuatro pasos en al menos dos releases para un rename. Ese es el coste de no caerse.

Ejemplo 2: dividir una tabla

Objetivo: sacar los campos de dirección de customers a una nueva tabla customer_addresses, para que un cliente pueda tener varias direcciones.

Expand:

CREATE TABLE customer_addresses (
  id          bigserial PRIMARY KEY,
  customer_id bigint NOT NULL REFERENCES customers(id),
  kind        text NOT NULL DEFAULT 'primary',
  street      text,
  city        text,
  postal_code text,
  country     text,
  created_at  timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX CONCURRENTLY customer_addresses_customer_id_idx
  ON customer_addresses (customer_id);

Dual write: la aplicación escribe la dirección tanto en las columnas antiguas como en la tabla nueva. Las lecturas vienen de customer_addresses cuando existe una fila, y si no, de las columnas antiguas.

Backfill, por lotes según rango de id de cliente:

INSERT INTO customer_addresses (customer_id, kind, street, city, postal_code, country)
SELECT id, 'primary', street, city, postal_code, country
FROM customers
WHERE id > :last_id AND id <= :last_id + 5000
  AND street IS NOT NULL
  AND NOT EXISTS (
    SELECT 1 FROM customer_addresses a
    WHERE a.customer_id = customers.id AND a.kind = 'primary'
  );

El NOT EXISTS hace idempotente el job: si muere a mitad, volver a ejecutarlo no duplica filas.

Verifica, antes de contraer: cuenta los clientes con dirección en las columnas antiguas pero sin fila primary en la tabla nueva. Debe ser cero.

Contract: cambia las lecturas solo a la tabla nueva, elimina el dual write y luego borra las columnas antiguas en una release posterior.

Migraciones largas sin bloquear producción

La fase de expand es barata cuando el DDL es solo de metadatos. Deja de serlo cuando el motor tiene que reescribir o recorrer la tabla, o cuando un bloqueo se queda en cola detrás de una transacción larga. Estas son las operaciones a vigilar.

PostgreSQL

  • CREATE INDEX CONCURRENTLY y DROP INDEX CONCURRENTLY evitan el bloqueo de escritura. No pueden ejecutarse dentro de una transacción, así que la mayoría de herramientas necesitan un flag por migración para desactivar el wrapper transaccional (-- atlas:txmode none, autocommit_block() en Alembic, disable_ddl_transaction! en Rails). Si se interrumpe, el índice queda INVALID; comprueba pg_index.indisvalid y reconstrúyelo.
  • Añadir una columna con un default volátil (p. ej. DEFAULT now()) reescribe la tabla; un default constante no.
  • Foreign keys y check constraints: añádelas con NOT VALID y luego VALIDATE CONSTRAINT por separado.
  • Define siempre lock_timeout (unos segundos) en la sesión de la migración. Un ALTER TABLE esperando un bloqueo ACCESS EXCLUSIVE bloquea todas las consultas que vienen detrás; un timeout hace que falle rápido en lugar de congelar la aplicación. Reintenta en bucle.
  • Cambiar el tipo de una columna generalmente reescribe la tabla. Prefiere añadir una columna nueva y pasar por expand-and-contract.

MySQL / MariaDB

  • InnoDB soporta ALGORITHM=INSTANT para añadir columnas y ALGORITHM=INPLACE, LOCK=NONE para muchas operaciones de índice. Especifícalos explícitamente para que la sentencia falle si el motor fuera a recurrir a una copia.
  • Para cualquier cosa que aún requiera copiar la tabla, usa una herramienta externa de online schema change: gh-ost (basada en binlog, con throttling, sin triggers) o pt-online-schema-change (basada en triggers). Ambas crean una tabla sombra, copian las filas por bloques y hacen el intercambio al final. Cuestan disco extra y añaden carga de replicación, así que ejecútalas fuera de las horas pico y vigila el lag.

Cualquier motor

  • Procesa las migraciones de datos en bloques de unos pocos miles de filas, por rango de clave primaria, una transacción por bloque.
  • Haz que los jobs de backfill sean reanudables e idempotentes. Guarda el último id procesado.
  • Aplica throttling según el lag de replicación, no con un sleep fijo. Pausa cuando el lag supere tu umbral.
  • Nunca ejecutes un backfill dentro de la transacción de la herramienta de migración de esquema. Entrégalo como un job de la aplicación o un script aparte con su propia monitorización.

Comparativa de herramientas de migración

Las herramientas hacen dos cosas: mantienen un historial ordenado y versionado de cambios y aplican los que una base de datos concreta aún no ha visto. Más allá de eso, difieren en cómo se escriben las migraciones, en si detectan drift entre el esquema declarado y la base de datos real, y en cómo encajan en CI.

Herramienta Lenguaje / ecosistema Modelo de versionado Soporte de rollback Detección de drift Integración con CI
Flyway CLI Java, SQL-first, cualquier stack Archivos SQL versionados, checksums Scripts de undo (plan de pago) validate comprueba checksums aplicados, no el esquema real CLI, Maven/Gradle, imagen Docker
Liquibase CLI Java, changesets XML/YAML/JSON/SQL Changelog con changesets, checksums Rollback integrado por changeset, tags diff contra una BD de referencia CLI, Maven/Gradle, imagen Docker
Alembic Python / SQLAlchemy Grafo de revisiones (DAG), ramas downgrade por revisión autogenerate compara modelos vs BD Python, corre en cualquier pipeline
Prisma Migrate TypeScript / Node schema.prisma declarativo, historial SQL generado Ninguno integrado (escribe una nueva migración forward) Detecta drift en migrate dev y migrate diff migrate deploy en CI
Rails Active Record Ruby DSL Ruby con timestamp, snapshot schema.rb down por migración, DSL reversible Compara schema.rb; la gem strong_migrations señala DDL inseguro Tareas Rake
Django migrations Python Grafo de dependencias por app, autogenerado Operaciones inversas, migrate app 000N makemigrations --check falla el CI si faltan migraciones manage.py en el pipeline
golang-migrate CLI y librería Go, SQL-first Pares SQL up/down numerados Archivos down Ninguna Binario estático pequeño, imagen Docker
Atlas CLI Go, cualquier stack, HCL/SQL/ORM como fuente Declarativo o versionado, directorio con checksum Planificado vía migrate down (consciente del estado) Función central: schema diff, schema inspect, linting de cambios inseguros GitHub Action, gate de lint, imagen Docker

Cómo leer la tabla:

  • SQL-first vs declarativo. Flyway, golang-migrate y Liquibase (en modo SQL) guardan lo que escribiste. Prisma y Atlas guardan el estado final deseado y generan el diff. Lo declarativo es más rápido de escribir, pero genera un DROP COLUMN sin pestañear, así que necesita un gate de lint.
  • El soporte de rollback es menos útil de lo que parece. Una down migration para ADD COLUMN está bien. Una down migration para DROP COLUMN no puede restaurar datos. Ver la siguiente sección.
  • La detección de drift es la función que más equipos no tienen y en la que más incidentes se ven implicados: un hotfix aplicado a mano en producción que ningún archivo de migración conoce. Atlas, diff de Liquibase y autogenerate de Alembic lo detectan; Flyway y golang-migrate, no.
  • El linting de DDL inseguro (Atlas lint, strong_migrations para Rails, squawk para archivos SQL de PostgreSQL) convierte las reglas de la sección anterior en fallos de CI. Añade uno, sea cual sea la herramienta que uses.

Estrategia de rollback y de pruebas

La postura honesta sobre el rollback: los cambios de esquema aditivos se pueden revertir, los cambios de datos normalmente no. Planifica en torno a esa asimetría.

Prefiere roll-forward. Como expand-and-contract nunca rompe la versión antigua, "rollback" en la fase de expand significa volver a desplegar la versión anterior de la aplicación y dejar la columna nueva en su sitio. Es inofensivo. En la fase de contract, el rollback es una restauración desde backup, y por eso contract se ejecuta al final y solo tras la verificación.

Ensaya contra una copia similar a producción. Restaura el backup de anoche (anonimizado si hace falta) en una instancia desechable y ejecuta el conjunto completo de migraciones. Mide el tiempo de reloj por migración y anota las que superen unos segundos. Los fixtures sintéticos con cien filas no mostrarán un problema de bloqueo; una copia con el número real de filas y la cardinalidad real de los índices sí. Esto sirve además como la prueba de restauración de backup que la mayoría de equipos nunca hace.

Automatiza las comprobaciones en CI, junto con tu pipeline de CI/CD habitual:

  • Base de datos limpia: aplica todas las migraciones desde cero y luego ejecuta la suite de pruebas.
  • Base de datos existente: aplica solo las migraciones nuevas sobre un snapshot del esquema actual de producción.
  • Down y up de nuevo para cada migración nueva, para demostrar que el script de down al menos se ejecuta.
  • Lint de operaciones inseguras (renames, drops, NOT NULL sin default, CONCURRENTLY ausente).
  • Haz fallar el build si el modelo del ORM y el historial de migraciones no coinciden (makemigrations --check, prisma migrate diff, atlas migrate lint).

Verifica los datos, no solo el DDL. Tras un backfill, ejecuta consultas de reconciliación: recuento de filas por cada lado, nulls en la columna nueva, foreign keys sin padre. Guarda las consultas en el repositorio junto a la migración.

Vigila las señales correctas durante la aplicación: esperas de bloqueo, lag de replicación, latencia p99 de los endpoints más calientes, tasa de errores. Conecta el ejecutor de migraciones al mismo logging y alertas que la aplicación, para que abortar sea una decisión y no una suposición.

Checklist de migración en producción

Antes del merge:

  • El cambio está dividido en expand, código, backfill y contract, cada uno en su propia migración o release.
  • Ningún rename, drop, cambio de tipo o NOT NULL en una columna que la versión actual lee.
  • Toda migración es idempotente o está protegida (IF NOT EXISTS, NOT EXISTS en los backfills).
  • Los índices en tablas grandes usan CONCURRENTLY (PostgreSQL) o INPLACE, LOCK=NONE / gh-ost (MySQL).
  • Las constraints se añaden como NOT VALID y se validan por separado.
  • lock_timeout y statement_timeout definidos para la sesión de la migración.
  • Los backfills van por lotes, son reanudables y se ejecutan fuera de la transacción de la migración.
  • Revisado por alguien responsable de la base de datos, no solo de la funcionalidad.

Antes de aplicar en producción:

  • Ensayado en una copia del tamaño de producción, con duración medida.
  • Backup verificado y point-in-time recovery confirmado para la ventana.
  • Ruta de rollback por escrito: qué versión de la aplicación redesplegar, qué migración revertir o qué restauración ejecutar.
  • Aplicado en el orden correcto respecto al deploy: expand antes, contract después.
  • El runbook tiene los comandos exactos, los criterios de aborto y quién está de guardia.

Después de aplicar:

  • Las consultas de reconciliación pasan.
  • No quedan índices inválidos ni constraints sin validar.
  • Migración de contract programada para una release posterior, con ticket, para que la columna antigua no viva para siempre.

Cuándo elegir cada enfoque

  • Tabla pequeña, poco tráfico, ventana de mantenimiento corta aceptable: una única migración con bloqueo suele ser más barata que cuatro pasos. Mide primero en una copia; "pequeña" significa que el DDL termina muy por debajo de tu lock_timeout.
  • Tabla grande o SLA estricto: expand-and-contract con backfill por lotes, siempre. La release extra es el precio de seguir en pie.
  • MySQL con una copia de tabla que no puedes evitar: gh-ost o pt-online-schema-change, fuera de las horas pico, vigilando el lag.
  • Índice o constraint de PostgreSQL en una tabla caliente: CONCURRENTLY y NOT VALID / VALIDATE, con el wrapper transaccional desactivado para esa migración.
  • Muchos servicios sobre un mismo esquema: triggers para el dual write durante la transición, porque no puedes coordinar el deploy de todos los escritores.
  • Cambiar el motor o el host: replicación lógica o CDC hacia el nuevo destino, dual write para verificación, cutover ensayado con un go/no-go firme. Eso es una migración de plataforma y merece su propio plan.

Recomendación

Adopta expand-and-contract como el estándar para todo cambio de esquema, no como un procedimiento especial para los grandes. Usa la herramienta de migración nativa de tu framework si la tiene, y añade a CI un linter de DDL inseguro y una comprobación de drift, sea cual sea la herramienta. Ensaya las migraciones de cada release en una copia del tamaño de producción; es la única práctica que detecta problemas de bloqueo y de duración antes que los usuarios. Trata el rollback como roll-forward más backups verificados, y mantén el paso de contract como un seguimiento con ticket para que los esquemas no acumulen columnas muertas. En Arvucore solemos recomendar empezar por el linter y el entorno de ensayo: cuestan un día de configuración y eliminan la mayor parte del riesgo de cada migración posterior.

¿Listo para Transformar tu Negocio?

Hablemos sobre cómo nuestras soluciones pueden ayudarte a alcanzar tus objetivos. Ponte en contacto con nuestros expertos hoy mismo.

Hablar con un Experto

Tags:

migraciones de bases de datosmigración de bases de datosversionado de esquemaszero downtimeexpand and contract
Equipo Arvucore

Equipo Arvucore

El equipo editorial de Arvucore está formado por profesionales experimentados en desarrollo de software. Estamos dedicados a producir y mantener contenido de alta calidad que refleja las mejores prácticas de la industria e insights confiables.

Preguntas frecuentes

¿Qué es una migración de base de datos con zero downtime?
Es un cambio de esquema o de datos que se aplica mientras la aplicación sigue atendiendo tráfico. Funciona dividiendo el cambio en pasos pequeños y retrocompatibles, de modo que la versión antigua y la nueva de la aplicación puedan ejecutarse contra la misma base de datos en todo momento.
¿Qué es el patrón expand-and-contract?
Expand añade la estructura nueva junto a la antigua, la aplicación se actualiza para escribir en ambas y leer de la nueva, se rellenan los datos (backfill) y contract elimina la estructura antigua. Ningún paso por sí solo rompe la versión en ejecución.
¿Las migraciones deben ejecutarse antes o después del deploy de la aplicación?
Las migraciones aditivas (expand) se ejecutan antes del deploy, para que el código nuevo encuentre las columnas que necesita. Las migraciones destructivas (contract) se ejecutan después de retirar por completo el código antiguo, normalmente en una release posterior.
¿Se puede revertir una migración de base de datos?
Los cambios de esquema puramente aditivos se pueden deshacer con una down migration. Nada que haya borrado datos o reescrito filas puede revertirse de verdad; en esos casos, apóyate en backups, point-in-time recovery y una corrección roll-forward. Diseña las migraciones para que el rollback rara vez haga falta.
¿Qué herramienta de migración de bases de datos debería usar?
Usa la nativa de tu stack cuando exista (Rails, Django, Prisma, Alembic). Para equipos políglotas o SQL-first, Flyway y golang-migrate son simples y predecibles; Liquibase añade funciones de gobernanza; Atlas añade esquemas declarativos, detección de drift y linting.
¿Cómo añado un índice a una tabla grande de PostgreSQL sin bloquearla?
Usa CREATE INDEX CONCURRENTLY fuera de una transacción. Tarda más y puede dejar un índice inválido si se interrumpe, así que comprueba pg_index.indisvalid después y reconstrúyelo si hace falta.

Artículos relacionados

Arquitectura basada en eventos: sistemas resilientes y escalables

Arquitectura basada en eventos: sistemas resilientes y escalables

En Arvucore, exploramos cómo la arquitectura basada en eventos transforma los sistemas distribuidos modernos, permitiendo aplicaciones ágiles, resilientes y escalables. Este artículo examina los principios fundamentales, los patrones de diseño prácticos y las consideraciones operativas para la implementación de sistemas basados en eventos en entornos empresariales. Los lectores encontrarán orientación sobre opciones de arquitectura, estrategias de integración y beneficios mensurables para impulsar la agilidad empresarial y la solidez técnica en las implementaciones de producción.

Micro Frontends: Arquitectura Escalable para Grandes Aplicaciones

Micro Frontends: Arquitectura Escalable para Grandes Aplicaciones

En Arvucore, exploramos cómo los micro frontends permiten una arquitectura frontend resiliente y modular para aplicaciones a gran escala. Al descomponer interfaces de usuario monolíticas en partes implementables de forma independiente, los equipos ganan autonomía, lanzamientos más rápidos y una propiedad más clara. Este artículo describe patrones de diseño, estrategias de integración, ventajas y desventajas en términos de rendimiento y enfoques de gobernanza para ayudar a los líderes empresariales y equipos técnicos europeos a evaluar los micro frontends para su próximo gran proyecto.

Arquitectura hexagonal en 2026: puertos, adaptadores y Clean

Arquitectura hexagonal en 2026: puertos, adaptadores y Clean

Hexagonal vs Clean vs Onion explicadas con un ejemplo real en TypeScript, estructura de carpetas, estrategia de tests y los errores que hunden a la mayoría de equipos.

CMS sin cabeza vs. CMS tradicional: Desarrollo de contenido moderno

CMS sin cabeza vs. CMS tradicional: Desarrollo de contenido moderno

En Arvucore, comparamos el desarrollo de CMS headless y los enfoques tradicionales de CMS con la gestión de contenido moderna, ayudando a los equipos técnicos y a los responsables de la toma de decisiones a elegir la plataforma adecuada. Este artículo describe las diferencias arquitectónicas, las implicaciones empresariales y las consideraciones prácticas de migración, buscando un equilibrio entre el rendimiento, la agilidad del desarrollador y los flujos de trabajo editoriales para guiar a las empresas europeas hacia soluciones que satisfagan las demandas multicanal y estrategias de contenido digital con visión de futuro.