Guía: organizar migraciones y rollbacks en PostgreSQL para equipos pequeños
Por Kevin Marcondes · Publicado el · Actualizado el · 10 min de lectura
Esta guía muestra cómo organizar migraciones y rollbacks en PostgreSQL para equipos pequeños, con procesos ligeros y previsibles. Verás principios, estructura mínima de directorios, flujo de trabajo, estrategias de reversión y un checklist de CI/CD. El enfoque es práctico para quien no tiene un DBA dedicado pero necesita cambios seguros.
Por qué los equipos pequeños sufren con las migraciones (y cómo solucionarlo)
En equipos reducidos, la misma persona codifica, revisa y despliega. Las migraciones improvisadas se convierten en un riesgo: conflictos entre ramas, cambios que bloquean producción y ausencia de un rollback claro.
Las señales más comunes son fáciles de reconocer y muestran la necesidad de un proceso simple pero disciplinado. La buena noticia: se puede organizar con convenciones y pocos scripts bien pensados.
Si quieres ampliar fundamentos más allá de PostgreSQL, consulta nuestro guía de versionado ágil de bases de datos y conecta esas ideas con este proceso práctico.
- Migraciones que funcionan en dev y fallan en staging/producción.
- Despliegues que requieren ventanas largas por locks y reindex.
- Rollback inexistente o arriesgado, obligando a correcciones manuales.
- Ramas que generan archivos de migration en orden conflictivo.
- Scripts sin idempotencia y sin rastreo de lo aplicado.
Principios esenciales: atomicidad, idempotencia y reversibilidad
Tres principios guían el proceso: (1) atomicidad con transacciones; (2) idempotencia siempre que sea posible; (3) reversibilidad planificada. Súmales visibilidad (registro de lo ejecutado) y parámetros de seguridad (timeouts y locks) para eliminar el riesgo más frecuente en producción.
Usa transacciones para DDL siempre que sea seguro en PostgreSQL. Ajusta timeouts locales para no bloquear la base. Y aplica un lock lógico para evitar ejecutar dos migrations al mismo tiempo en la misma base.
- Atomicidad: ejecuta DDL/pequeños DML dentro de una transacción.
- Idempotencia: protecciones como IF NOT EXISTS y comprobaciones previas.
- Reversibilidad: mantiene un down.sql sencillo y testeable.
- Seguridad: SET LOCAL statement_timeout/lock_timeout.
- Visibilidad: tabla schema_migrations para auditoría.
-- 20241003_add_status_to_orders_up.sql
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
-- Evita concorrência entre execuções de migrations
SELECT pg_advisory_xact_lock(987654321);
-- Exemplo de mudança segura
ALTER TABLE public.orders
ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'pending';
COMMENT ON COLUMN public.orders.status IS 'Estado do pedido';
COMMIT;
-- 20241003_add_status_to_orders_down.sql
BEGIN;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '30s';
SELECT pg_advisory_xact_lock(987654321);
-- Reversão simples (cuidado com dados que dependam da coluna)
ALTER TABLE public.orders DROP COLUMN IF EXISTS status;
COMMIT;
Estructura mínima de directorios y convenciones de nombre
Estandariza nombres y la ubicación de los archivos. Esto ayuda en el diff, la revisión y la automatización. Prefiere timestamp + slug corto descriptivo. Separa up.sql y down.sql. Mantén scripts utilitarios en una carpeta aparte.
Crea también una tabla schema_migrations para registrar versiones aplicadas. Evita reaplicar scripts y da trazabilidad.
- Prefijo: AAAAMMDD-HHMMSS_slug.
- Separación: un par up/down por cambio atómico.
- Sin archivos gigantes: divide por etapa si es necesario.
- Incluye comentarios claros al inicio de los archivos.
# Estrutura sugerida
.
├── db/
│ ├── migrations/
│ │ ├── 20241003-120000_add_status_up.sql
│ │ ├── 20241003-120000_add_status_down.sql
│ │ ├── 20241004-093000_add_idx_orders_created_at_up.sql
│ │ └── 20241004-093000_add_idx_orders_created_at_down.sql
│ └── scripts/
│ ├── migrate.sh
│ └── rollback.sh
└── .env
Flujo de trabajo ligero: branches, PR y staging antes del merge
Organiza el flujo para reducir conflictos y validar rollbacks temprano. El objetivo es que cada PR contenga sus migrations, ejecutadas automáticamente en staging, con validación rápida y clara.
Para evitar conflictos de orden, genera el timestamp al crear la migration. Si dos PR colisionan, rebasea el que se abrió después y regenera el timestamp.
- La rama crea la migration con timestamp único.
- El PR ejecuta las migrations en un banco de staging automáticamente.
- El revisor verifica up/down y el impacto en locks y duración.
- Merge a main sólo tras éxito en staging.
- La etiqueta de release dispara las migrations en producción.
# Makefile (trecho) para padronizar comandos
migrate:
bash db/scripts/migrate.sh $(DATABASE_URL)
rollback:
bash db/scripts/rollback.sh $(DATABASE_URL) $(VERSION)
# Uso:
# make migrate DATABASE_URL=postgres://user:pass@host:5432/dbname
# make rollback DATABASE_URL=... VERSION=20241003-120000
Rollbacks en la práctica: estrategias según el tipo de cambio
No todo cambio permite un rollback trivial. Planifica la reversión ya en el PR. Para DDL potencialmente costoso, realiza el rollout en dos fases: primero hacer la modificación compatible; luego, cuando sea seguro, consolidarla.
Para datos, evita grandes actualizaciones en lote dentro de la migration. Prefiere jobs idempotentes y observables fuera de la transacción de la migration, registrando el progreso.
Si la reversión falla o el impacto es amplio, considera estrategias de recuperación como point‑in‑time recovery. Consulta cuándo es útil en el artículo sobre recuperación de tablas con PITR.
- Añadir columna: permite NULL o DEFAULT barato; backfill fuera de la migration.
- Índice: usa CREATE INDEX CONCURRENTLY; rollback con DROP INDEX CONCURRENTLY.
- Renombrar/alterar tipo: crea columna nueva, sincroniza y haz el cutover.
- Eliminar columna: desactiva el uso en la app, monitoriza y sólo entonces dropea.
- Corrección de datos: registra filas tocadas para revertir si es necesario.
-- Exemplo de data fix com trilha para reversão
BEGIN;
CREATE TABLE IF NOT EXISTS ops_datafix_log (
key text PRIMARY KEY,
details jsonb,
applied_at timestamptz DEFAULT now()
);
-- Marca registros afetados antes de atualizar
INSERT INTO ops_datafix_log(key, details)
SELECT '20241005_fix_status_orders', jsonb_build_object('count', count(*))
FROM orders WHERE status = 'pending';
-- Atualiza de forma idempotente
UPDATE orders SET status = 'new'
WHERE status = 'pending';
COMMIT;
-- Para reverter, leia ops_datafix_log e volte ao estado anterior se fizer sentido.
Herramientas ligeras que funcionan bien en equipos pequeños
Los equipos pequeños pueden empezar con SQL puro + scripts. Reduce dependencias y obliga a la claridad. Cuando el volumen crece, las herramientas de migrations ayudan con trazabilidad, validaciones y dependencias explícitas.
Criterios de elección: facilidad para revisar PR, soporte a up/down, ejecución transaccional, locks, dry‑run e integración sencilla con tu CI/CD. Evita herramientas que oculten el SQL y dificulten la auditoría.
- SQL + scripts: control total, poca 'magia'.
- Herramientas dedicadas: trazabilidad y dependencias declarativas.
- Evita generar migrations a partir de diff sin revisión humana.
- Estandariza variables de entorno y logs de ejecución.
Integración con CI/CD y checklist de release seguro
Automatiza la ejecución en staging por cada PR. En producción, dispara junto a la release, con parámetros de seguridad y logs centralizados. Monitoriza locks, tiempo de ejecución y filas modificadas.
Incluye comprobaciones de salud del banco y políticas de timeout. Los trabajos de datos grandes deben correr fuera de la ventana pico y de forma incremental.
Si los fallos en deploy son frecuentes, mapea cuellos de botella y establece un plan. Este proceso se integra con una auditoría de procesos de despliegue para ganar previsibilidad.
- Preflight: backup probado y espacio libre suficiente.
- SET LOCAL statement_timeout/lock_timeout en las migrations.
- Lock lógico (pg_advisory_xact_lock) para evitar concurrencia.
- Informe: versiones aplicadas, duración, errores, tamaño de tablas.
- Jobs de datos: batches pequeños y reentrantes.
#!/usr/bin/env bash
# db/scripts/migrate.sh
set -euo pipefail
DATABASE_URL="$1"
export PGCONNECT_TIMEOUT=5
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL'
CREATE TABLE IF NOT EXISTS schema_migrations (
version text PRIMARY KEY,
applied_at timestamptz DEFAULT now()
);
SQL
for f in $(ls -1 db/migrations/*_up.sql | sort); do
ver=$(basename "$f" | cut -d'_' -f1)
if ! psql -Atqc "select 1 from schema_migrations where version='${ver}'" "$DATABASE_URL" | grep -q 1; then
echo "Applying $f";
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f "$f"
psql "$DATABASE_URL" -c "insert into schema_migrations(version) values ('${ver}') on conflict do nothing;"
fi
done
Cuándo usar migration, script ad‑hoc o job de datos
No todo cambio encaja en una migration transaccional. Usa esta tabla rápida para decidir la aproximación adecuada. Para migraciones complejas sin downtime, evalúa patrones como CDC. Hay un artículo sobre eso en migración de bases sin downtime con CDC.
| Escenario | Enfoque principal | Rollback recomendado | Observaciones |
|---|---|---|---|
| Añadir columna con default barato | Migration transaccional (ALTER TABLE ... ADD COLUMN DEFAULT) | DROP COLUMN si es seguro | Backfill fuera de la migration si la tabla es grande. |
| Crear índice en tabla grande | CREATE INDEX CONCURRENTLY | DROP INDEX CONCURRENTLY | Evita locks largos de escritura. |
| Alterar tipo de columna con muchos datos | Columna nueva + backfill incremental + cutover | Reapuntar al campo antiguo | Planifica ventanas y monitoriza impacto. |
| Renombrar tabla/columna usada por la app | Compatibilidad por etapas (views/columnas duplicadas) | Restaurar nombres antiguos | Lanzar bandera en la app antes del drop final. |
| Corrección masiva de datos | Job batch idempotente con log de progreso | Revertir usando log/estado anterior | No mezcles con deploys críticos. |
Preguntas frecuentes
¿Qué herramienta usar para migrations en equipos pequeños?
Empieza con SQL puro y scripts versionados. Esto obliga a la claridad, facilita la revisión y reduce dependencias. Si el volumen crece, adopta una herramienta de migrations que ofrezca trazabilidad, up/down y una integración simple con tu CI/CD. Lo importante es mantener estándares y auditoría.
¿Cómo garantizar un rollback seguro en PostgreSQL?
Planifica la reversión en el PR, mantén down.sql sencillo y testeable y evita data fixes irreversibles. Para cambios riesgosos, haz rollout por etapas y monitoriza. Si el impacto es amplio, usa estrategias como point‑in‑time recovery o un cutover reversible según tu RTO/RPO.
¿Cómo evitar conflictos de migrations entre ramas?
Usa timestamp en el nombre del archivo y genera la migration al abrir la rama. Si dos PR colisionan, rebasea y regenera el timestamp. Automatiza la ejecución en staging por PR para garantizar que el orden final en el merge refleje la secuencia correcta en producción.
¿Puedo ejecutar grandes actualizaciones de datos dentro de la migration?
Mejor no. Las grandes actualizaciones bloquean y amplían ventanas de fallo. Ejecuta data fixes como jobs idempotentes y por batches, con log de progreso y posibilidad de pausa. La migration debe crear estructuras y triggers necesarios; el procesamiento ocurre fuera de ella.
¿Qué timeouts configurar en las migrations?
Define SET LOCAL lock_timeout para evitar esperar locks indefinidamente y SET LOCAL statement_timeout para impedir comandos demasiado largos. Los valores dependen de tu tráfico y tamaño de tablas. El objetivo es fallar rápido y de forma predecible, sin bloquear producción.
¿Cómo manejar deadlocks al aplicar migrations?
Reduce el alcance de la transacción, ordena operaciones de forma consistente y aplica ALTERs compatibles. Evita data fixes grandes dentro de la migration. En casos persistentes, audita jobs concurrentes y el orden de acceso a tablas. Consulta la guía sobre bloqueo y deadlocks en cron para más detalles.
¿Quieres fortalecer tu proceso de migraciones y rollbacks?
Hablemos sobre tu contexto, riesgos actuales y los próximos pasos prácticos para estructurar un flujo predecible de cambios en PostgreSQL.
Hablar sobre tu base de datosVer también
- Guía: checklist de pre‑deploy y plan de rollback rápido para aplicaciones PHP en producción
- Checklist de seguridad para pipelines CI/CD con runners self‑hosted y gestión de secretos
- Guía: centralización de logs con Fluentd y Elasticsearch para aplicaciones PHP en VPS
- Guía práctica: cómo autoescalar runners self‑hosted en GitHub Actions para pipelines CI de proyectos PHP