🔄
Migrations profissionais: reversíveis, zero-downtime
⏱ 13 min de leitura·+65 XP
Migrations mal planejadas causam downtime. Em produção com milhões de linhas, cada DDL precisa ser analisada: adquire lock de tabela? Faz table rewrite? Quanto tempo leva? O padrão expand-migrate-contract resolve isso com deploys incrementais.
Alembic: migrations em Python
# Instalação e setup
# uv add alembic sqlalchemy
# Inicializar no projeto
# alembic init alembic
# Gera: alembic.ini, alembic/env.py, alembic/versions/
# alembic/env.py — aponta para seus modelos
from myapp.models import Base
target_metadata = Base.metadata
# alembic.ini
# sqlalchemy.url = postgresql://user:pass@localhost/mydb
# Gerar migration automaticamente (baseado em diff dos modelos):
# alembic revision --autogenerate -m "add_users_email_verified"
# Arquivo gerado em alembic/versions/abc123_add_users_email_verified.py
from alembic import op
import sqlalchemy as sa
def upgrade() -> None:
op.add_column(
'users',
sa.Column('email_verified', sa.Boolean(), nullable=True)
)
def downgrade() -> None:
op.drop_column('users', 'email_verified')
# Executar migrations:
# alembic upgrade head # aplica todas pendentes
# alembic downgrade -1 # reverte a última
# alembic current # mostra versão atual
# alembic history # histórico de migrations
# alembic show abc123 # mostra detalhes de uma migrationZero-downtime: o padrão expand-migrate-contract
| Passo | O que fazer | Compatibilidade com deploy anterior |
|---|---|---|
| Expand (Deploy 1) | Adicionar nova estrutura (coluna nullable, nova tabela) | Código antigo ainda funciona |
| Migrate (Deploy 2) | Backfill + código escreve em ambos os lugares | Backfill em background, sem lock |
| Contract (Deploy 3) | Remover estrutura antiga | Código novo só usa nova estrutura |
-- EXEMPLO: renomear coluna 'name' para 'full_name' sem downtime
-- Deploy 1 — Expand: adicionar nova coluna
ALTER TABLE users ADD COLUMN full_name TEXT;
-- Nullable → sem table rewrite, sem lock prolongado
-- Deploy 1 — Código: escrever em ambas as colunas
# app/models.py
def criar_usuario(nome):
db.execute("""
INSERT INTO users (name, full_name)
VALUES (:name, :full_name)
""", {"name": nome, "full_name": nome})
-- Deploy 2 — Migrate: backfill em lotes
-- Script de backfill (rodar fora do deploy):
DO $$
DECLARE
batch_size INT := 10000;
offset_val BIGINT := 0;
max_id BIGINT;
BEGIN
SELECT MAX(id) INTO max_id FROM users;
WHILE offset_val <= max_id LOOP
UPDATE users
SET full_name = name
WHERE id > offset_val
AND id <= offset_val + batch_size
AND full_name IS NULL;
offset_val := offset_val + batch_size;
PERFORM pg_sleep(0.1); -- pausa para não stressar o banco
END LOOP;
END;
$$;
-- Deploy 2 — código: código novo lê de full_name
-- Deploy 3 — Contract: adicionar NOT NULL e remover coluna antiga
ALTER TABLE users ALTER COLUMN full_name SET NOT NULL;
ALTER TABLE users DROP COLUMN name;Migrations perigosas e como fazer cada uma com segurança
-- ✅ SEGURO sem precauções especiais:
ALTER TABLE t ADD COLUMN nova TEXT; -- nullable = sem lock
ALTER TABLE t ADD COLUMN nova TEXT DEFAULT 'x'; -- PG 11+ = sem table rewrite
CREATE INDEX CONCURRENTLY idx ON t(col); -- sem lock exclusivo
CREATE TABLE nova (...);
DROP TABLE antiga; -- se vazia e sem referências
-- ⚠️ PERIGOSO sem cuidado (pode causar lock/downtime):
ALTER TABLE t ADD COLUMN nova TEXT NOT NULL; -- PG<11: table rewrite
ALTER TABLE t ALTER COLUMN tipo SET NOT NULL; -- table scan (mas não rewrite)
ALTER TABLE t DROP COLUMN qualquer; -- ok em PG, mas garanta que código não usa
CREATE INDEX idx ON t(col); -- WITHOUT CONCURRENTLY: bloqueia!
ALTER TABLE t RENAME COLUMN antigo TO novo; -- ok mas quebra queries em produção
-- ⛔ NUNCA em produção sem migration:
-- Deletar uma coluna usada pelo código ainda em produção
-- DROP TABLE ou TRUNCATE sem verificar dependências
-- Mudar tipo de coluna (DATE → TIMESTAMP) sem casting explícito
-- Verificar locks antes de migration:
SELECT
pid,
now() - pg_stat_activity.query_start AS duration,
query,
state
FROM pg_stat_activity
WHERE state != 'idle'
AND query NOT ILIKE '%pg_stat_activity%'
ORDER BY duration DESC;✅
Checklist de migration segura: (1) teste em banco com dados reais antes de produção; (2) estime tempo com EXPLAIN ANALYZE em staging; (3) use CONCURRENTLY para índices; (4) faça backfill em lotes com pausa; (5) adicione NOT NULL como passo separado após backfill completo; (6) verifique que downgrade também funciona.
💡
Próximo: Connection pool e N+1 — os problemas de banco de dados que matam performance de API silenciosamente.
🧩
Quiz rápido
3 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito
Continue lendo