Era 14h de uma sexta-feira de novembro. O time tinha decidido que ia subir uma migration “simples” antes de bater o ponto — só adicionar uma coluna na tabela pedidos. Cinco minutos no máximo. O que poderia dar errado?

Tudo.

O contexto

A gente trabalhava com Postgres 14, tabela com aproximadamente 80 milhões de linhas, e a coluna nova era um VARCHAR(255) NOT NULL DEFAULT ''. Em Postgres moderno, adicionar coluna com default constante é uma operação metadata-only — instantânea, sem reescrever a tabela.

O problema é que a gente não tava em Postgres 14.

A gente tava em Postgres 10. E em Postgres 10, ADD COLUMN ... DEFAULT 'algo' reescreve a tabela inteira. Linha por linha.

O que aconteceu

ALTER TABLE pedidos
ADD COLUMN canal_origem VARCHAR(255) NOT NULL DEFAULT '';

O comando rodou. E rodou. E rodou.

Aos 30 segundos a gente começou a estranhar. Aos 2 minutos o monitoring começou a apitar — latência das requisições escalando. Aos 5 minutos o checkout tava fora do ar. Aos 8 minutos eu rodei pg_cancel_backend no PID da migration.

Não cancelou. Tinha lock exclusivo em metade da tabela e qualquer query nova entrava em fila.

Aos 12 minutos rodei pg_terminate_backend. Aí o Postgres começou o rollback da transação interrompida. Adivinha quanto tempo demora o rollback de uma reescrita de tabela de 80 milhões de linhas?

Quase o mesmo tempo que demoraria pra terminar.

A reação

Liguei pra todo mundo do time. Em cinco minutos a gente tinha:

  • O CTO do time logado e tentando entender o estrago
  • Suporte respondendo aos clientes que o e-commerce tava “instável”
  • Eu tentando explicar pro time por que cancelar a migration não cancelou nada
  • O time de marketing perguntando por que a campanha de Black Friday parou de converter

Levou 47 minutos pro rollback terminar. 47 minutos sem checkout. Em véspera de Black Friday.

O post-mortem

A semana seguinte foi inteira de post-mortem. As lições foram as seguintes:

  1. Sexta-feira existe por uma razão. Nunca, nunca, nunca subir migration em sexta sem motivo extraordinário. A regra do “no deploy de sexta” não é supersticão — é gestão de risco baseada em quem tá disponível pra apagar incêndio.

  2. Conhece a versão do teu banco. A gente assumiu que a otimização de “default constante = metadata only” funcionava porque tinha lido no blog do Postgres. Não verificamos a versão. Versão importa.

  3. Migration sempre tem fase de preparação. A versão correta dessa mudança seria:

    • Adicionar coluna nullable, sem default
    • Em batch separado, popular a coluna com o valor padrão
    • Em batch separado, adicionar a constraint NOT NULL
    • Ou usar uma ferramenta como pg_repack ou pt-online-schema-change
  4. pg_cancel_backend não cancela operações de DDL longas. Aprendido na marra.

  5. Tem um motivo pra existir staging. A gente tinha staging. A gente não rodou a migration em staging primeiro. “Era simples demais.”

O que eu mudei depois disso

  • Toda migration agora passa por revisão de pelo menos um sênior, mesmo que pareça boba
  • Implementamos pré-validação automática que detecta padrões perigosos no SQL (default constante em versão antiga, índice sem CONCURRENTLY, etc.)
  • Criamos uma checklist de “operações que dão lock pesado”
  • Mais importante: a gente começou a tratar deploy de sexta como exceção e não regra

A coluna canal_origem tá lá até hoje, lembrando todo mundo do dia que aprendeu o respeito pelo Postgres.