Migrações de esquema
Como alterar tabelas em produção sem parar o sistema e sem perder dados.
As decisões sobre migrações são quatro: como escrevê-las, quando aplicá-las, como fazer cada tipo de mudança sem parar o sistema e como mudar dados em volume.
Como escrever
| Abordagem | Como funciona | Vantagens | Custos |
|---|---|---|---|
| SQL versionado | Arquivos .sql numerados, aplicados em ordem (Flyway, golang-migrate, dbmate) | Controle total do SQL; o que está no arquivo é o que roda | Escrever tudo à mão |
| Gerada pelo ORM | O ORM compara o modelo com o banco e gera a migração (Alembic, EF Core, Prisma, Django) | Rápida; acompanha o modelo | O SQL gerado não sabe de travas, CONCURRENTLY ou volume |
| Declarativa | Descreve o estado desejado; a ferramenta calcula a diferença (Atlas, Skeema) | Sem migrações acumuladas | Mudanças de dados e etapas finas ficam difíceis |
A combinação mais comum é gerar com o ORM e revisar o SQL antes do commit. O gerador acerta a estrutura e não sabe de produção: ele cria o índice sem CONCURRENTLY, troca o tipo da coluna numa tabela de 50 milhões de linhas e apaga a coluna que o código antigo ainda lê.
Por linguagem, as ferramentas mais usadas:
| Linguagem | Ferramentas |
|---|---|
| TypeScript | Prisma Migrate, Drizzle Kit, Knex, node-pg-migrate |
| Python | Alembic (SQLAlchemy), migrações do Django |
| Go | golang-migrate, goose, Atlas |
| Java | Flyway, Liquibase |
| C# | EF Core Migrations, DbUp |
Quando aplicar
| Momento | Como funciona | Cuidados |
|---|---|---|
| Etapa do deploy, antes do código novo | O pipeline roda as migrações e só então libera a versão nova | A migração precisa ser compatível com o código antigo, que ainda está no ar |
| Na inicialização da aplicação | Cada instância aplica as migrações ao subir | Várias instâncias sobem juntas e disputam; a ferramenta precisa de uma trava (Flyway e EF Core têm) |
| À mão, por uma pessoa | Alguém roda o comando | Só para mudanças excepcionais, com registro |
Aplicar como etapa separada do deploy é o mais previsível: uma migração demorada ou que falhou não deixa instâncias da aplicação presas na inicialização. E a regra de ouro vale para qualquer momento: cada migração funciona com a versão do código que está no ar e com a próxima.
Receitas para cada mudança
| Mudança | Sem cuidado | Seguro |
|---|---|---|
| Acrescentar coluna | — | Sem padrão, ou com padrão constante; ambos instantâneos no PostgreSQL 11 ou mais novo |
| Coluna com padrão calculado | Reescreve a tabela | Acrescentar sem padrão, preencher em lotes, depois definir o padrão |
Tornar NOT NULL | Lê a tabela sob trava | CHECK (col IS NOT NULL) NOT VALID, VALIDATE CONSTRAINT, depois SET NOT NULL |
| Criar índice | Bloqueia escritas | CREATE INDEX CONCURRENTLY, fora de transação |
| Chave estrangeira | Lê as duas tabelas sob trava | ADD CONSTRAINT ... NOT VALID, depois VALIDATE CONSTRAINT |
Trocar tipo (int para bigint) | Reescreve a tabela | Coluna nova, gravação dupla, cópia em lotes, troca e remoção da antiga |
| Renomear coluna ou tabela | Quebra o código antigo | Expandir e contrair, em vários deploys |
| Remover coluna | Quebra o código antigo | Primeiro um deploy que para de usar a coluna; depois a remoção |
E em todas: lock_timeout curto, com nova tentativa. No teste do nível Conceito, a migração sem limite fez uma leitura simples esperar 5,3 segundos; com lock_timeout = '1s', a espera ficou em 0,7 segundo.
O lock_timeout combina com o statement_timeout, que limita a duração do comando inteiro. Um protege contra a espera pela trava; o outro, contra uma migração que demora mais que o previsto segurando a trava.
Mudar dados em volume
Preencher uma coluna nova em 50 milhões de linhas com um único UPDATE gera uma transação enorme: trava as linhas por muito tempo, infla a tabela e atrasa as réplicas. O caminho é em lotes:
UPDATE pedidos SET canal = 'site'
WHERE id IN (SELECT id FROM pedidos WHERE canal IS NULL LIMIT 5000);
-- repetir até não sobrar nenhuma linha, com uma pausa entre os lotes| Cuidado | Por quê |
|---|---|
| Lotes de alguns milhares de linhas, cada um na sua transação | Travas curtas e progresso salvo |
| Pausa entre os lotes | As réplicas acompanham, e a aplicação continua atendendo |
| Retomável | Se parar no meio, recomeça de onde estava (a condição canal IS NULL faz isso) |
| Fora da migração de esquema | Migrações de dados longas não devem prender o deploy; um job separado é mais seguro |
Ferramentas de apoio
| Ferramenta | O que faz |
|---|---|
| Verificadores de migração (squawk, strong_migrations) | Leem o SQL e apontam operações perigosas: índice sem CONCURRENTLY, troca de tipo, NOT NULL direto |
| Ferramentas de alteração online (pg-osc, gh-ost e pt-online-schema-change no MySQL) | Fazem a mudança numa cópia da tabela e trocam no fim, para mudanças que reescreveriam a tabela |
| Ensaio com dados reais | Rodar a migração numa cópia do banco de produção para medir o tempo antes |
Resumo da decisão
- Migrações versionadas no repositório, geradas ou escritas, sempre revisadas em SQL.
- Etapa separada do deploy, compatível com o código antigo e o novo.
lock_timeoutcurto e nova tentativa em toda migração.CONCURRENTLYpara índices eNOT VALIDcom validação para restrições.- Expandir e contrair para renomear, trocar tipo e remover.
- Dados em lotes, retomáveis, fora da migração de esquema.
- Colunas explícitas nas consultas, em vez de
SELECT *.
O próximo nível mostra o executor com lock_timeout e nova tentativa nas cinco linguagens, testado com a tabela ocupada.
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Organizar as migrações do meu projeto
Quero organizar as migrações de banco do meu projeto. Pergunte qual linguagem, ORM e banco eu uso, como é o deploy e quantas instâncias da aplicação rodam. Recomende, explicando o porquê: 1. A ferramenta de migração. 2. Gerar pelo ORM ou escrever em SQL, e como revisar. 3. Quando aplicar: etapa do deploy ou inicialização da aplicação. 4. Como lidar com várias instâncias subindo ao mesmo tempo. 5. Um verificador automático de migrações perigosas na integração contínua.
Planejar uma mudança arriscada
Preciso fazer uma mudança de esquema arriscada numa tabela grande em produção. Pergunte qual é a mudança, o banco e a versão, o tamanho da tabela, o volume de leituras e escritas e como é o deploy. Monte comigo um plano com: 1. As etapas, cada uma compatível com o código no ar. 2. O SQL de cada etapa, com lock_timeout e as receitas sem bloqueio. 3. A migração de dados em lotes, se houver. 4. Como medir o tempo antes, numa cópia. 5. Como voltar atrás em cada etapa.
Migrar dados em volume
Preciso preencher ou transformar dados em milhões de linhas, com o sistema no ar. Pergunte qual banco eu uso, o tamanho da tabela, se há réplicas e qual é a transformação. Explique: 1. Por que um único UPDATE é perigoso. 2. Como dividir em lotes retomáveis, e o tamanho do lote. 3. Como não atrasar as réplicas nem pesar na aplicação. 4. Onde rodar: migração, job separado ou script. 5. Como conferir que terminou e que os dados estão certos.