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:
-
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.
-
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.
-
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
-
pg_cancel_backendnão cancela operações de DDL longas. Aprendido na marra. -
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.