Migrações de esquema
Como alterar tabelas em produção sem parar o sistema e sem perder dados.
O problema: mudar a tabela com o sistema no ar
O código muda a cada deploy, e o banco precisa acompanhar: uma coluna nova, um índice, uma tabela. Fazer isso à mão, direto no banco de produção, não escala: ninguém sabe o que foi aplicado onde, e o ambiente de testes fica diferente do de produção.
A migração de esquema resolve a organização. Cada mudança vira um arquivo versionado no repositório, aplicado em ordem, uma vez só, em todos os ambientes. Uma tabela no próprio banco registra quais migrações já rodaram:
migracoes/
0001_cria_clientes.sql
0002_cria_pedidos.sql
0003_adiciona_canal_em_pedidos.sqlFerramentas como Flyway, Alembic, golang-migrate, EF Core Migrations e Prisma Migrate fazem esse controle. A parte difícil não é a organização: é que a migração roda com o sistema atendendo pedidos, contra tabelas com milhões de linhas.
Nem toda mudança custa o mesmo
Medimos, num PostgreSQL 18 com uma tabela de 3 milhões de pedidos, quanto tempo cada mudança levou. Durante esse tempo, a maioria delas segura uma trava exclusiva que impede leituras e escritas na tabela:
| Mudança | Tempo |
|---|---|
| Acrescentar coluna sem valor padrão | 0,6 ms |
Acrescentar coluna NOT NULL DEFAULT 'novo' (constante) | 1,3 ms |
Acrescentar coluna NOT NULL DEFAULT clock_timestamp() (muda a cada linha) | 3,1 s |
Mudar cliente_id de int para bigint | 3,3 s |
SET NOT NULL direto numa coluna | 0,43 s |
| Renomear coluna | 3,4 ms |
A diferença está em reescrever ou não a tabela. Desde o PostgreSQL 11, um valor padrão constante é guardado só no catálogo, e as linhas antigas o recebem na leitura. Um valor que muda a cada linha, ou uma troca de tipo, obriga o banco a reescrever as 3 milhões de linhas, com a tabela travada. Numa tabela dez vezes maior, os 3 segundos viram 30.
O SET NOT NULL não reescreve, mas lê a tabela inteira para conferir que não há nulos, também com a trava exclusiva. Há um caminho sem bloquear:
| Etapa | Tempo | Bloqueia escritas? |
|---|---|---|
ADD CONSTRAINT ... CHECK (obs IS NOT NULL) NOT VALID | 1 ms | Só por um instante |
VALIDATE CONSTRAINT | 169 ms | Não |
SET NOT NULL, aproveitando a restrição já validada | 2,3 ms | Só por um instante |
A restrição NOT VALID vale para as linhas novas na hora. A validação das antigas acontece depois, com uma trava mais fraca, que deixa a aplicação ler e escrever. E o SET NOT NULL usa a restrição validada para pular a leitura da tabela.
A fila de travas: a migração que derruba o sistema
Mesmo uma mudança instantânea pode parar o sistema. Para acrescentar uma coluna, o banco precisa de uma trava exclusiva por um instante, e espera todas as transações que estão usando a tabela terminarem. Enquanto espera, todos os pedidos que chegam depois ficam na fila atrás dela, inclusive as leituras mais simples.
Testamos com três sessões: um relatório que lê a tabela por 6 segundos, uma migração que acrescenta uma coluna meio segundo depois e uma leitura simples por chave primária logo em seguida.
| Migração | Migração | Leitura simples por chave primária |
|---|---|---|
| Sem limite de espera | Aplicada em 6,1 s, quando o relatório terminou | Esperou 5,3 s |
SET lock_timeout = '1s' | Desistiu em 1,5 s, com o erro 55P03 | Esperou 0,7 s |
Sem o limite, a leitura de 1 milissegundo esperou mais de 5 segundos. Num sistema real, são todos os pedidos que tocam a tabela, por todo esse tempo: na prática, uma queda. Com lock_timeout, a migração desiste e libera a fila. Na Implementação, um executor que desiste e tenta de novo aplicou a migração na terceira tentativa, nas cinco linguagens, e nenhuma leitura esperou mais de 1 segundo.
Índices sem parar as escritas
CREATE INDEX lê a tabela inteira e, enquanto isso, bloqueia as escritas. CREATE INDEX CONCURRENTLY constrói o índice em etapas, sem bloquear. Medimos com inserções contínuas na tabela de 3 milhões de linhas:
| Comando | Tempo do índice | Inserções durante o teste | Pior espera de uma inserção |
|---|---|---|---|
CREATE INDEX | 1,8 s | 72 | 1.747 ms |
CREATE INDEX CONCURRENTLY | 2,3 s | 278 | 4 ms |
O CONCURRENTLY é um pouco mais lento e tem regras: não roda dentro de uma transação, e, se falhar no meio, deixa um índice marcado como inválido, que precisa ser apagado e criado de novo. Muitas ferramentas de migração envolvem cada arquivo numa transação, e é preciso desligar isso para esse comando.
O código antigo continua rodando
Durante um deploy, a versão nova e a antiga do código rodam ao mesmo tempo por alguns minutos, e a migração roda antes, ou no meio. Toda migração precisa funcionar com as duas versões do código.
Renomear a coluna obs para observacao quebra a versão antiga, que continua lendo obs. A saída é dividir a mudança em etapas, no padrão expandir e contrair:
- Expandir: criar
observacao, e fazer o código novo gravar nas duas colunas. - Migrar: copiar os dados antigos de
obsparaobservacao, em lotes. - Trocar a leitura: o código passa a ler só
observacao. - Contrair: num deploy posterior, quando nenhum código usa mais
obs, apagá-la.
São quatro deploys para uma renomeação. É o preço de não ter janela de manutenção.
Por que isso importa quando a IA escreve o código
Um assistente que gera uma migração costuma escrever o SQL que funciona num banco vazio:
- Nenhum
lock_timeout, e a migração forma a fila que derruba o sistema. CREATE INDEXsemCONCURRENTLYem tabelas grandes.- Coluna nova com valor padrão calculado, ou troca de tipo, reescrevendo a tabela sob trava.
SET NOT NULLdireto, em vez deCHECK ... NOT VALIDe validação.- Renomear ou remover colunas num único deploy, quebrando a versão antiga do código.
- Atualizar milhões de linhas num único
UPDATE, numa transação gigante.
O próximo nível compara as formas de organizar e aplicar migrações, e as receitas para cada tipo de mudança.
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.
Migrações de esquema em cinco minutos
Explique em no máximo cinco minutos de leitura o que são migrações de esquema, para alguém que já alterou tabelas num banco de desenvolvimento, mas nunca em produção com o sistema no ar. Cubra só o essencial: migrações versionadas, por que algumas mudanças travam a tabela, a fila de travas, índices sem bloquear escritas e o padrão expandir e contrair. Use o exemplo de uma tabela de pedidos com milhões de linhas. No final, me faça três perguntas curtas para eu verificar se entendi, e corrija as minhas respostas.
Travas e a fila que derruba o sistema
Quero entender como as travas do banco afetam as migrações. Aja como um mentor e vá por etapas, esperando a minha resposta em cada uma: 1. Os tipos de trava de tabela no PostgreSQL que importam para migrações, e quais comandos pegam cada uma. 2. Por que uma migração instantânea ainda pode esperar por transações longas. 3. Por que os pedidos que chegam depois ficam presos atrás da migração. 4. Como lock_timeout e statement_timeout protegem o sistema. 5. Como descobrir quem está segurando a trava (pg_locks, pg_stat_activity). Em cada etapa, me descreva uma situação e peça que eu preveja o que acontece.
Quais mudanças reescrevem a tabela
Quero saber quais alterações de esquema são instantâneas e quais reescrevem ou varrem a tabela inteira. Para cada mudança abaixo, peça que eu diga se ela é instantânea, varre ou reescreve a tabela, e depois explique a resposta certa no PostgreSQL e no MySQL: 1. Acrescentar coluna sem padrão, com padrão constante e com padrão calculado. 2. Trocar o tipo de uma coluna. 3. Tornar uma coluna NOT NULL. 4. Criar um índice, com e sem CONCURRENTLY. 5. Acrescentar uma chave estrangeira. 6. Renomear e remover uma coluna.
Expandir e contrair
Quero entender o padrão expandir e contrair para mudanças de esquema sem janela de manutenção. Explique: 1. Por que o código antigo e o novo rodam ao mesmo tempo durante um deploy. 2. As etapas para renomear uma coluna, com o que muda no código e no banco em cada deploy. 3. As etapas para trocar o tipo de uma coluna numa tabela grande. 4. As etapas para remover uma coluna com segurança. 5. Como saber que é seguro passar para a próxima etapa. Depois me dê uma mudança e peça que eu escreva as etapas. Corrija a minha resposta.