Alterações no banco de dados sem interrupção com expand-contract
Aprenda a fazer alterações no banco de dados sem interrupção com o padrão expand-contract: adicione campos seguros, migre dados em etapas, mantenha o código antigo funcionando e só então remova partes legadas.

Por que alterações no banco de dados causam interrupções
A maioria das quedas durante trabalhos no banco de dados acontece quando código e esquema mudam ao mesmo tempo, mas não são implantados na mesma ordem em todos os lugares. Servidores de aplicação, jobs em background e tarefas agendadas não atualizam instantaneamente. Por um tempo, código antigo e novo rodam lado a lado. Se qualquer versão espera algo que o banco de dados já não fornece, os usuários percebem.
O erro clássico é pensar que “só rodar uma migração” é um passo seguro. Uma migração pode bloquear tabelas, reescrever muitas linhas ou remover uma coluna que algum processo ainda em execução lê. Mesmo mudanças “pequenas”, como renomear uma coluna, podem quebrar a produção se alguma requisição ainda espera o nome antigo.
A indisponibilidade geralmente aparece como:
- erros 500 quando o código lê uma coluna ou tabela que não existe mais
- dados faltantes ou incorretos quando código antigo e novo leem e escrevem formatos diferentes
- timeouts quando uma migração bloqueia gravações ou dispara queries lentas
- bugs “funciona pra mim” quando só alguns servidores foram atualizados
- jobs em background que falham, reexecutam e sobrecarregam o sistema
"Compatível com versões anteriores" significa que o código antigo pode continuar rodando com segurança enquanto o banco de dados muda. Na prática, você evita remover ou alterar qualquer coisa da qual o código antigo depende. Você adiciona novos campos ou tabelas de uma forma que ambas as versões entendam, e então move os dados gradualmente.
Alterações sem tempo de inatividade são difíceis porque bancos de dados são estado compartilhado. Uma migração arriscada pode afetar toda requisição, toda gravação e todo job ao mesmo tempo. A abordagem expand-contract reduz esse risco ao eliminar o momento de "tudo de uma vez": primeiro expanda o esquema, rode ambas as versões com segurança, migre os dados em background e só então limpe quando o novo caminho se mostrar estável.
A ideia expand-contract em uma imagem
Pense em expand-contract como reformar uma cozinha enquanto ainda cozinha todo dia. Você não arranca a pia antiga primeiro. Você adiciona a pia nova, migra o uso e só então remove a antiga.
Time ->
1) EXPAND 2) MIGRATE (gradual) 3) CONTRACT
Add new parts Copy/backfill data safely Remove old parts
Keep old path Run both paths for a while After new path is proven
- Expand: Adicione o que precisa (nova coluna, nova tabela, novo índice) sem quebrar o código antigo.
- Migrate: Mova os dados em pequenos lotes enquanto o app continua ativo. Por um tempo, formatos antigo e novo coexistem.
- Contract: Remova as peças antigas somente depois que o novo caminho estiver estável em produção.
Isso reduz o risco porque cada passo é menor, mais fácil de pausar e de entender do que uma única grande migração.
Projetando uma atualização de esquema compatível com versões anteriores
Uma mudança de esquema compatível com versões anteriores significa que versões antiga e nova do app podem rodar contra o mesmo banco.
Comece por mudanças aditivas. Adicione uma coluna nova, uma tabela nova ou uma tabela de junção, mas mantenha a forma antiga até ter certeza de que nada depende dela. Se precisar renomear algo, primeiro adicione o novo nome e mantenha o antigo funcionando (por exemplo com uma view ou uma coluna duplicada), depois remova o antigo.
Durante a fase de expand, escolha defaults que não surpreendam o código antigo. Campos nulos são geralmente mais seguros do que campos obrigatórios no primeiro dia. Se um campo precisa ser não-nulo, introduza-o com um default seguro que combine com o comportamento existente e só imponha regras mais rígidas depois que o app estiver atualizado.
Algumas regras evitam a maioria das quebras:
- Não drope colunas ou mude significados até a fase de contract.
- Evite renomes como primeiro passo. Adicione primeiro, migre, depois limpe.
- Adicione constraints gradualmente (NOT NULL, UNIQUE, foreign keys) depois que os dados estiverem no lugar.
- Planeje mudanças de índices para evitar longos locks de escrita (use opções online quando suportadas).
- Garanta que cada novo campo tenha um plano para linhas antigas.
Decida cedo como leituras e gravações vão funcionar enquanto ambas as versões estiverem vivas. Abordagens comuns são:
- Código novo grava tanto o antigo quanto o novo durante a transição (dual-write).
- Código novo grava apenas o novo formato, e uma camada de compatibilidade mantém o formato antigo atualizado.
- Leituras mudam primeiro, com fallback para a fonte antiga até o backfill completar.
Exemplo: se você dividir users.full_name em first_name e last_name, não remova full_name ainda. Adicione as novas colunas, faça o app novo gravar todas as três e mantenha as leituras antigas apontando para full_name até ter confiança.
Passo a passo: fluxo expand-contract
Alterações sem downtime funcionam melhor quando você planeja um estado temporário "intermediário". Essa ponte permite que código antigo e novo coexistam enquanto você movimenta os dados.
Expand: adicione caminhos novos sem quebrar os antigos
Escolha o modelo alvo e desenhe um modelo-ponte que represente ambas as versões (frequentemente colunas antigas mais colunas novas).
Expanda o esquema com segurança: adicione colunas ou tabelas primeiro, mantenha os campos antigos e torne os novos campos nulos ou com defaults seguros. Se precisar de índices, adicione-os de forma que não bloqueiem escritas.
Faça deploy de código dual-compatível: publique código que saiba ler de ambos os lugares e escrever de forma a manter os dados consistentes.
Migrar e alternar: mova dados, depois mude o tráfego
Faça o backfill em pequenos lotes. Torne o job reiniciável e seguro para rodar duas vezes.
Mude leituras antes de gravações. Leituras são mais fáceis de observar e reverter porque não alteram dados. Quando as leituras estiverem estáveis, migre as gravações em fases (frequentemente via dual-write) e então remova o fallback.
Só faça o contract depois da verificação. Defina a "verificação" antecipadamente (contagens de linhas batem, checagens pontuais, taxa de erro e latência normais).
Como migrar dados gradualmente sem quebrar gravações
A abordagem mais segura é backfill das linhas antigas enquanto seu app continua a servir tráfego. A chave é pequenos lotes e garantir que novas gravações não percam o novo formato.
Execute o backfill em lotes pequenos o suficiente para terminar rápido, com pausas breves entre lotes para que o tráfego normal fique suave.
Monitore progresso para poder retomar: um timestamp migrated_at, uma flag booleana ou um marcador de “último id processado”. Combine com uma query simples de “quantos faltam” para saber se você está avançando.
Enquanto o backfill roda, novas linhas chegam. Trate isso fazendo com que a aplicação escreva os novos campos para todas as linhas novas ou atualizadas. Se não for possível em todos os lugares ainda, faça dual-write por um período curto e depois leia dos novos campos com fallback para o antigo.
Mantenha o job idempotente. Deve ser seguro rodá-lo duas vezes na mesma linha:
- Atualize apenas linhas que ainda não foram migradas
- Use transformações determinísticas (mesma entrada, mesma saída)
- Evite updates do tipo append que possam duplicar dados
- Registre falhas por linha e continue em vez de parar todo o job
Também faça inventário de todos os writers, não apenas da API principal: workers, webhooks, ferramentas admin, imports e scripts. Um writer esquecido pode desfazer seu plano silenciosamente.
Estratégia de rollout: mantenha o código antigo funcionando enquanto muda dados
Assuma que formatos de dados antigo e novo existirão ao mesmo tempo. Publique código que saiba ler ambos e não quebre se um campo estiver faltando, duplicado ou não totalmente backfilled.
Um switch controlado (feature flag ou config) ajuda a mudar comportamento em passos pequenos. Uma sequência de rollout simples:
- Faça deploy de código que consegue ler colunas (ou tabelas) antigas e novas.
- Ative leituras novas para uma pequena fatia (um ambiente, um tenant ou uma pequena porcentagem de tráfego).
- Observe taxa de erros e queries lentas, então expanda.
- Uma vez que leituras estejam estáveis, comece a dual-write ou mude gravações em fases.
- Mantenha o caminho antigo disponível até ter certeza de que a migração terminou.
Rollback deve ser sem sustos. Idealmente você pode voltar as leituras para a fonte antiga e parar novas gravações sem perder dados. Com dual-write, o rollback frequentemente significa continuar gravando o formato antigo enquanto investiga.
Antes de contrair (dropar colunas ou tabelas), procure sinais de estabilidade: nenhum null inesperado nos campos novos, contagens de linhas consistentes, backlog de migração não crescendo e volume de suporte dentro do normal.
Erros comuns que causam downtime
A maioria das quedas durante expand-contract vem de dois problemas: o banco fica bloqueado, ou diferentes partes do app discordam sobre o significado dos dados.
Os erros que mais machucam:
- Locks longos. Mudar tipo de coluna em uma tabela grande ou adicionar um índice do jeito “simples” pode bloquear reads ou writes por minutos.
- Deriva silenciosa no dual-write. Perder um caminho de escrita faz com que formatos antigo e novo divergirem. Usuários veem falhas “aleatórias”.
- Renomes que quebram tudo. O app pode funcionar, mas exports, dashboards e scripts ad-hoc começam a falhar.
- Esquecer paths não-request. Cron jobs, workers, painéis admin e scripts precisam do mesmo plano de compatibilidade.
- Contrair cedo demais. Dropar a coluna antiga cedo demais e você perde a opção de rollback.
Um exemplo simples: você troca leituras para profile_json, mas um worker de email ainda usa last_name e começa a enviar “Olá ,” para usuários. Não é downtime, mas é incidente em produção.
Checklist rápido antes, durante e depois da mudança
Alterações sem downtime falham por motivos banais: a tabela é maior do que o esperado, um pico de tráfego ou um caminho do código ainda espera o esquema antigo.
Antes de começar, confirme escopo e tempo (tamanho da tabela, churn, janela de baixa carga) e confirme compatibilidade (código antigo e novo podem rodar com segurança).
Durante o rollout, monitore o app (taxa de erro, latência) e o banco (CPU, locks, lag de replicação, queries lentas). Desacelere ou pause o backfill se timeouts subirem.
Depois, prove que é seguro contrair: nenhuma leitura ou gravação acessa mais o esquema antigo, dashboards permanecem limpos por um ciclo completo de negócio e flags temporárias são removidas.
Uma forma prática de descobrir dependências ocultas: em staging, faça com que as colunas antigas retornem null temporariamente e execute fluxos normais (signup, checkout, edição de perfil). Se algo quebrar, você não está pronto para remover as peças legadas.
Exemplo: mudar esquema de perfil de usuário sem downtime
Suponha que sua tabela users tenha uma única coluna name ("Ada Lovelace"), mas você precisa agora de first_name e last_name para busca, ordenação e e-mails personalizados.
Expand
Adicione first_name e last_name como colunas nulas. Mantenha name. Não adicione NOT NULL ainda.
Atualize o app para que toda gravação defina ambos: continua preenchendo name e também first_name e last_name. Leituras podem continuar usando name por enquanto.
Migrar e rollout
Backfill as linhas existentes com um job em background. Mantenha simples: separe no primeiro espaço e, para casos complicados ("Prince", "Mary Jane Watson-Parker"), faça um best-effort para first_name e deixe last_name vazio.
Uma sequência prática:
- Deploy 1: adicione colunas
first_name,last_name - Deploy 2: dual-write (atualize campos antigos e novos)
- Backfill: migre usuários existentes em lotes
- Deploy 3: leia
first_name/last_nameprimeiro, com fallback paraname - Verifique: confirme que novos campos estão preenchidos para usuários ativos e novos cadastros
Quando o código novo estiver estável, troque UI e exports para usar os campos novos (com fallback seguro para o display name).
Contract
Depois de confirmar que nada depende de name (incluindo scripts e workers), pare de gravá-la e remova em release posterior.
Fase de contract: limpar com segurança e evitar complexidade residual
A fase de contract remove a estrutura temporária que tornou a mudança segura. Pulá-la deixa complexidade permanente e futuras migrações ficam mais arriscadas.
"Concluído" significa que o esquema antigo está realmente sem uso. Uma definição simples para a equipe:
- Nenhum código do app lê ou escreve colunas ou tabelas antigas
- Nenhum worker, cron ou script as referencia
- Nenhuma lógica de dual-write permanece
- Nenhuma feature flag existe apenas para suportar o caminho antigo
- Monitoramento mostra que só o caminho novo está sendo usado
Antes de deletar qualquer coisa, faça uma varredura focada: busque no codebase pelos nomes antigos, revise jobs agendados, cheque logs de queries por leituras contra objetos antigos e verifique dashboards e runbooks.
Depois da remoção, apague também os helpers: scripts de backfill, métricas temporárias e validações especiais que existiam só durante a transição.
Anote o que mudou e por quê: esquema antigo vs novo, como os dados se moveram e quando você declarou o caminho antigo morto. Na próxima vez, você será mais rápido.
Próximos passos: planeje sua mudança e peça uma segunda opinião
Expand-contract vale a pena quando uma mudança de esquema toca uma tabela quente, autenticação, pagamentos ou qualquer coisa que seu app escreva o dia todo. Se você não pode tolerar nem uma janela curta de manutenção, trate isso como um release, não como uma “migração rápida”. Para ferramentas internas de baixo tráfego ou tabelas de relatório pontuais, uma janela planejada pode ser suficiente.
Para estimar risco, olhe para blast radius e reversibilidade. Blast radius é quantos caminhos de código leem ou escrevem os dados (incluindo jobs e ferramentas admin). Reversibilidade é se você consegue fazer rollback do app e ainda trabalhar com o banco.
Se seu projeto começou como um protótipo gerado por IA, migrações frequentemente quebram por razões previsíveis: SQL cru escondido, writers esquecidos, lógica de dual-write incompleta e suposições de esquema espalhadas pelo código. Se precisar desembaraçar isso antes de uma mudança em produção, FixMyMess (fixmymess.ai) pode revisar o código e o plano de migração e apontar os pontos arriscados que normalmente causam outages.
Perguntas Frequentes
Por que migrações de banco de dados quebram a produção mesmo quando a mudança parece pequena?
Porque sua frota raramente atualiza tudo de uma vez. Por um tempo, código antigo e novo rodam juntos; se qualquer versão espera por uma coluna, tabela ou restrição que ainda não existe (ou já foi removida), as requisições começam a falhar ou a gravar dados no formato errado.
O que significa “compatível com versões anteriores” para uma mudança de banco de dados?
Significa que você consegue mudar o esquema enquanto o código antigo ainda está em execução sem causar crashes ou dados corrompidos. Na prática, evita-se remover ou alterar o significado de algo que o código antigo depende até que o novo caminho esteja totalmente ativo e estável.
O que é a abordagem expand-contract em termos simples?
Expand-contract é um padrão de rollout mais seguro: primeiro você adiciona as peças novas do esquema, depois migra os dados gradualmente enquanto ambos os caminhos (antigo e novo) funcionam, e só então remove as partes antigas. Reduz o momento “tudo de uma vez” que pode derrubar o sistema.
O que devo fazer primeiro na fase de expansão?
Comece adicionando colunas ou tabelas novas sem tocar nas antigas. Torne os novos campos opcionais (nullable) ou dê defaults seguros para que gravações existentes não falhem, e faça deploy de código que não caia se os novos campos estiverem ausentes ou vazios.
Quando é seguro dropar uma coluna ou tabela antiga?
Remover ou renomear colunas que ainda são lidas pelo código antigo é a forma mais rápida de causar 500s. Mesmo que a API principal esteja atualizada, jobs em background, cron jobs, ferramentas admin e scripts podem continuar referenciando os nomes antigos por horas ou dias.
Como migrar dados existentes sem bloquear o tráfego ao vivo?
Execute um backfill reiniciável que mova linhas em pequenos lotes e possa ser reexecutado com segurança. Ao mesmo tempo, garanta que novas gravações populam o novo formato (via dual-write ou camada de compatibilidade) para não ficar com um alvo móvel que nunca termina.
O que é dual-write e quando devo usar?
É quando o código novo grava tanto a representação antiga quanto a nova durante a transição. É útil para segurança e rollback, mas pode causar divergência se você perder algum caminho de escrita, então é preciso inventariar todos os writers e manter a transformação determinística.
Devo trocar leituras ou gravações primeiro durante o rollout?
Mude as leituras primeiro, porque dá para observar erros e reverter sem alterar dados. Depois que as leituras estiverem estáveis e o backfill majoritariamente completo, altere as gravações em etapas controladas e só remova a redundância depois de verificar que o novo caminho está consistente.
O que devo monitorar para detectar problemas cedo?
Monitore bloqueios no banco, queries lentas e lag de replicação durante mudanças de esquema e índices; e acompanhe taxa de erros e latência da aplicação durante o rollout. Se os timeouts subirem, desacelere ou pause o backfill e ajuste o plano de queries ou o tamanho dos lotes antes de continuar.
Como a FixMyMess pode ajudar se meu app gerado por IA continuar quebrando durante migrações?
Se seu código foi gerado por ferramentas como Lovable, Bolt, v0, Cursor ou Replit, suposições de esquema podem estar espalhadas por SQL cru, jobs e migrações incompletas. FixMyMess pode fazer uma auditoria gratuita do código para encontrar writers arriscados, auth quebrado, segredos expostos e riscos de migração, e ajudar a implementar um rollout expand-contract seguro rapidamente.