Gestionar Migraciones de Base de Datos de Forma Segura
Cómo versionar, aplicar y hacer rollback de cambios de schema de base de datos usando herramientas como Flyway, Alembic y Liquibase en entornos de producción.
Visión general
Las migraciones de base de datos rastrean, versionan y aplican cambios de schema a lo largo del tiempo. Sin un sistema de migración, los cambios de schema se aplican manualmente a través de scripts SQL ad-hoc, sesiones SSH y oración. Esto lleva a entornos que divergen, fallas de deployment y outages en producción causados por índices olvidados o columnas faltantes.
Una herramienta de migración convierte los cambios de schema en scripts versionados, repetibles y reversibles. Cada migración está numerada o timestamped, trackeada en una tabla de historial dedicada, y aplicada automáticamente durante el deployment. Los rollbacks están scripteados y testeados, no improvisados. La solucion a continuacion cubre las tres herramientas más ampliamente adoptadas: Flyway (JVM), Alembic (Python) y Liquibase (multi-lenguaje).
Cuándo usarlo
Usa esta receta cuando:
- Gestionando la evolución de schema a través de bases de datos de desarrollo, staging y producción. Consulta Safe Migrations para estrategias sin downtime.
- Agregando tablas, columnas, índices o constraints como parte de un release de feature. Consulta Database Transactions para consistencia durante despliegues.
- Coordinando cambios de schema con deployments de código de aplicación. Consulta Clean Code Guide para patrones mantenibles.
- Haciendo rollback de cambios de schema después de deployments fallidos. Consulta Retry Logic para estrategias de recuperación.
- Auditando quién cambió qué en la base de datos y cuándo
Solución
Flyway (JVM/SQL)
-- V1__create_users_table.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT NOW()
);
-- V2__add_user_status.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'active';
-- V3__create_user_index.sql
CREATE INDEX idx_users_email ON users(email);
flyway -url=jdbc:postgresql://db:5432/app -locations=filesystem:db/migration migrate
Alembic (Python/SQLAlchemy)
# alembic/versions/20250613_add_user_status.py
from alembic import op
import sqlalchemy as sa
revision = 'a1b2c3d4'
down_revision = '9f8e7d6c'
def upgrade():
op.add_column('users', sa.Column('status', sa.String(20), nullable=True))
op.execute("UPDATE users SET status = 'active' WHERE status IS NULL")
op.alter_column('users', 'status', nullable=False)
def downgrade():
op.drop_column('users', 'status')
alembic upgrade head
alembic downgrade -1
Liquibase (XML/YAML/JSON)
<databaseChangeLog>
<changeSet id="1" author="developer">
<createTable tableName="users">
<column name="id" type="int" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
<column name="email" type="varchar(255)">
<constraints nullable="false" unique="true"/>
</column>
</createTable>
</changeSet>
<changeSet id="2" author="developer">
<addColumn tableName="users">
<column name="status" type="varchar(20)" defaultValue="active"/>
</addColumn>
</changeSet>
</databaseChangeLog>
liquibase --changeLogFile=db.changelog.xml update
Explicación
- Scripts versionados: Cada archivo de migración tiene un identificador único. Las herramientas registran las migraciones aplicadas en una tabla de historial (
flyway_schema_history,alembic_version,databasechangelog), previniendo ejecución duplicada. - Migraciones forward (up): Cambios de schema que avanzan la base de datos — crear tablas, agregar columnas, crear índices. Estas corren automáticamente durante el deployment.
- Migraciones rollback (down): Testéalos en staging antes de emergencias en producción.
- Baseline y repair: Cuando introduces migraciones a una base de datos existente, las herramientas pueden hacer baseline del estado actual del schema sin intentar recrear tablas existentes.
Variantes
| Herramienta | Formato | Lenguaje | Mejor para |
|---|---|---|---|
| Flyway | SQL plano | JVM-first | Equipos que prefieren SQL crudo |
| Alembic | Código Python | Python/SQLAlchemy | Ecosistemas Python |
| Liquibase | XML/YAML/JSON | Multi-lenguaje | Enterprise, soporte multi-DB |
| Sequelize CLI | Código JS | Node.js | Proyectos Express/NestJS |
Lo que funciona
- Nunca modifiques una migración ya aplicada: una vez que una migración corre en cualquier entorno compartido, trátala como inmutable. Crea una nueva migración para corregir errores.
- Haz migraciones idempotentes cuando sea posible:
CREATE TABLE IF NOT EXISTSyDROP INDEX IF EXISTSprevienen fallas durante ejecución repetida. - Separa DDL y DML: los cambios de schema (CREATE, ALTER) y los cambios de datos (INSERT, UPDATE) deberían estar en migraciones diferentes. DDL frecuentemente bloquea tablas; DML puede batcherse.
- Testea rollbacks en cada cambio: una migración sin rollback testeado es una puerta de un solo sentido. Practica downgrades en staging para confirmar que funcionan.
- Corre migraciones antes del inicio de la aplicación: deploy el cambio de schema, luego deploy el código que depende de él. Nunca asumas que la columna existe antes de que la migración corra.
Errores comunes
- Agregar columnas non-nullable sin defaults: las filas existentes causarán que la migración falle. Agrega la columna como nullable, backfillea datos, luego agrega el constraint
NOT NULLen una migración follow-up. Consulta Safe Migrations para el patrón expand-contract. - Eliminar datos sin backups: Siempre haz backup o copia datos antes de cambios destructivos.
- Bloquear tablas durante horas pico: agregar un índice o alterar una tabla grande puede bloquear por minutos.
- Olvidar réplicas: las migraciones aplicadas a una base de datos primaria pueden no replicarse correctamente si contienen funciones no determinísticas o tablas temporales.
Tips de Rendimiento
- Usa
SET statement_timeoutpara migraciones. Evita que una migración se ejecute indefinidamente:
SET statement_timeout = '300s';
-- Ejecutar migración
SET statement_timeout = '0'; -- Reset al default
- Haz backfill de datos grandes en lotes. Actualiza 1.000-10.000 filas por lote para evitar transacciones largas:
DO $$
DECLARE
batch_size INT := 5000;
offset_val INT := 0;
rows_affected INT;
BEGIN
LOOP
UPDATE users SET status = 'active'
WHERE id > offset_val AND id <= offset_val + batch_size AND status IS NULL;
GET DIAGNOSTICS rows_affected = ROW_COUNT;
EXIT WHEN rows_affected = 0;
offset_val := offset_val + batch_size;
PERFORM pg_sleep(0.1); -- Pausa breve para reducir carga
END LOOP;
END $$;
- Usa
ALTER TABLE ... SET TABLESPACEpara tablas grandes. Mueve tablas a almacenamiento más rápido durante ventanas de mantenimiento:
ALTER TABLE large_table SET TABLESPACE fast_ssd;
- Monitorea el progreso de migraciones con
pg_stat_progress_create_index. Rastrea el progreso de creación de índices en PostgreSQL 12+:
SELECT phase, blocks_done, blocks_total,
ROUND(blocks_done::numeric / NULLIF(blocks_total, 0) * 100, 2) AS pct
FROM pg_stat_progress_create_index;
- Usa
CONCURRENTLYpara todas las operaciones de índice en producción.CREATE INDEX CONCURRENTLY,DROP INDEX CONCURRENTLYyREINDEX CONCURRENTLYevitan bloquear escrituras.
Preguntas frecuentes
¿Las migraciones deberían estar en el mismo repositorio que el código de aplicación?
Sí. Mantener migraciones y código juntos asegura que cada branch contiene el schema que necesita, y CI puede validar ambos simultáneamente.
¿Cómo manejo migraciones en un pipeline CI/CD?
Corre migraciones como un paso dedicado de deployment antes de iniciar la nueva versión de la aplicación. Usa un mecanismo de locking para prevenir ejecuciones concurrentes de migraciones.
¿Puedo automatizar rollback en falla de deployment?
Algunos equipos hacen downgrade automáticamente después de un health check fallido, pero sé cauteloso — los rollbacks también pueden fallar. Testea procedimientos de rollback exhaustivamente.
¿Cuál es la diferencia entre migraciones y seeds?
Las migraciones cambian la estructura del schema. Los seeds insertan datos de referencia (roles, países, settings). Mantenlos separados para que las migraciones permanezcan reversibles.
Recursos Relacionados
Optimizar consultas con índices de base de datos
Cómo crear, analizar y mantener índices para acelerar las consultas y evitar los errores de indexación más comunes.
RecipeOptimizar Queries Lentas de Base de Datos
Cómo identificar, analizar y corregir queries SQL lentos usando EXPLAIN, refactoring de queries y técnicas de optimización específicas por base de datos.
RecipeConfigurar connection pooling para bases de datos y HTTP
Configura connection pooling para PostgreSQL, MySQL, Redis y clientes HTTP en Python, JavaScript y Java. Reduce latencia y evita agotamiento de conexiones.
RecipeBloqueo optimista para evitar actualizaciones perdidas
Implementa bloqueo optimista con versionado para evitar actualizaciones perdidas. Ejemplos en SQL, Node.js, Java/JPA, MongoDB, DynamoDB y ETags HTTP.
RecipeEvolución de Schema de Base de Datos
Evoluciona schemas de base de datos de forma segura con cambios backward-compatible, migraciones versionadas y operaciones DDL online en ambientes de producción.
RecipeConfigurar índices de búsqueda de texto completo
Configura índices de búsqueda de texto completo en PostgreSQL para consultar grandes columnas de texto con ranking y resaltado.