🧠FFVAcademy
🔄

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 migration

Zero-downtime: o padrão expand-migrate-contract

PassoO que fazerCompatibilidade 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 lugaresBackfill em background, sem lock
Contract (Deploy 3)Remover estrutura antigaCó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