DevGuideTK
← Trilha Backend

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.sql

Ferramentas 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çaTempo
Acrescentar coluna sem valor padrão0,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 bigint3,3 s
SET NOT NULL direto numa coluna0,43 s
Renomear coluna3,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:

EtapaTempoBloqueia escritas?
ADD CONSTRAINT ... CHECK (obs IS NOT NULL) NOT VALID1 msSó por um instante
VALIDATE CONSTRAINT169 msNão
SET NOT NULL, aproveitando a restrição já validada2,3 msSó 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çãoMigraçãoLeitura simples por chave primária
Sem limite de esperaAplicada em 6,1 s, quando o relatório terminouEsperou 5,3 s
SET lock_timeout = '1s'Desistiu em 1,5 s, com o erro 55P03Esperou 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:

ComandoTempo do índiceInserções durante o testePior espera de uma inserção
CREATE INDEX1,8 s721.747 ms
CREATE INDEX CONCURRENTLY2,3 s2784 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:

  1. Expandir: criar observacao, e fazer o código novo gravar nas duas colunas.
  2. Migrar: copiar os dados antigos de obs para observacao, em lotes.
  3. Trocar a leitura: o código passa a ler só observacao.
  4. 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 INDEX sem CONCURRENTLY em tabelas grandes.
  • Coluna nova com valor padrão calculado, ou troca de tipo, reescrevendo a tabela sob trava.
  • SET NOT NULL direto, em vez de CHECK ... NOT VALID e 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.