Migração de Banco de Dados em 2026: Zero Downtime
Equipe Arvucore
September 22, 2025 · Atualizado em August 26, 2026
15 min read
Uma migração de banco de dados com zero downtime é uma mudança no schema ou nos dados dividida em passos pequenos o suficiente para que a aplicação em execução nunca quebre: adicione a estrutura nova, faça o código funcionar com as duas formas, mova os dados e então remova a estrutura antiga. O padrão se chama expand-and-contract e é o núcleo de toda estratégia de migração em produção. Todo o resto deste guia (DDL online, processamento em lotes, ferramentas, rollback) existe para tornar esses passos seguros em tabelas grandes sob tráfego real.
Três tipos de migração de banco de dados
A palavra "migração" cobre três problemas diferentes. Eles precisam de planos diferentes.
| Tipo | O que muda | Risco típico | Técnica principal |
|---|---|---|---|
| Migração de schema | Tabelas, colunas, índices, constraints | Locks, DDL longo, quebra do código antigo | Expand-and-contract, DDL online |
| Migração de dados | As próprias linhas: backfills, reformatação, mover valores entre colunas | Transações longas, lag de replicação, estado parcial | Lotes, jobs idempotentes, reconciliação |
| Migração de plataforma | O motor ou o host: MySQL para PostgreSQL, on-prem para nuvem gerenciada, uma versão major para outra | Diferenças semânticas, cutover, perda de dados | Replicação/CDC, dual write, cutover ensaiado |
A maioria das releases só precisa das duas primeiras. Uma migração de plataforma é um projeto, não um passo do deploy, e geralmente faz parte de um esforço maior, como migrar sistemas legados para arquiteturas modernas. Este artigo foca em migrações de schema e de dados, que são as que as equipes entregam toda semana.
Uma regra vale para as três: o banco nunca pode depender de uma versão específica da aplicação estar no ar. O código antigo e o novo vão coexistir durante qualquer rolling deploy, e uma migração que assume o contrário causa uma indisponibilidade exatamente no momento em que você não consegue fazer rollback limpo.
Deploys retrocompatíveis: a restrição da qual tudo deriva
Durante um deploy rolling ou blue-green existe uma janela em que a versão N e a versão N+1 da aplicação falam com o mesmo banco. Se você usa deploys canary ou rolling, essa janela pode durar horas. A migração precisa ser compatível com as duas.
Na prática, isso significa:
- Mudanças aditivas vão primeiro, na própria migração, antes do código que as usa. Colunas novas nullable, tabelas novas, índices novos.
- Mudanças destrutivas vão por último, depois que o código antigo sumiu. Drops, renomeações, NOT NULL em coluna existente, mudanças de tipo.
- Nenhuma migração renomeia ou apaga algo que a versão atual lê. Renomear é o erro clássico:
ALTER TABLE ... RENAME COLUMNé instantâneo, e quebra toda query da versão antiga instantaneamente. - A aplicação tolera as duas formas durante toda a transição: escreve nas duas colunas, ou lê a nova com fallback para a antiga.
Se o seu ORM gera um rename ou um drop automaticamente, trate isso como sinal para parar e dividir a mudança. Uma feature flag permite trocar as leituras da antiga para a nova sem deploy, o que torna os passos intermediários reversíveis.
Expand-and-contract, passo a passo
O padrão tem quatro fases. Cada uma é um deploy separado ou uma migração separada, nunca agrupadas.
- Expand: adicione a estrutura nova sem tocar na antiga.
- Migrar o código: a aplicação escreve nas duas, lê da nova (com fallback).
- Backfill: copie os dados existentes para a estrutura nova, em lotes.
- Contract: mude as leituras por completo, adicione constraints, apague a estrutura antiga.
Exemplo 1: renomear uma coluna
Objetivo: renomear users.fullname para users.display_name em uma tabela com milhões de linhas.
Passo 1, expand (migração, antes do deploy):
ALTER TABLE users ADD COLUMN display_name text;
Adicionar uma coluna nullable sem default é uma mudança só de metadados no PostgreSQL e no MySQL moderno (InnoDB instant ADD COLUMN). Não reescreve a tabela.
Passo 2, dual write (deploy da aplicação):
O código escreve nas duas colunas em todo insert e update, e lê display_name com fallback para fullname. Se você não consegue alterar todos os caminhos de escrita (jobs legados, outros serviços), um trigger mantém as duas em sincronia até conseguir:
CREATE OR REPLACE FUNCTION sync_display_name() RETURNS trigger AS $$
BEGIN
IF NEW.display_name IS NULL THEN NEW.display_name := NEW.fullname; END IF;
IF NEW.fullname IS NULL THEN NEW.fullname := NEW.display_name; END IF;
RETURN NEW;
END $$ LANGUAGE plpgsql;
CREATE TRIGGER users_sync_display_name
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_display_name();
Passo 3, backfill (job em lotes, não um único UPDATE):
UPDATE users
SET display_name = fullname
WHERE id IN (
SELECT id FROM users
WHERE display_name IS NULL AND fullname IS NOT NULL
ORDER BY id
LIMIT 5000
);
Rode em loop até afetar zero linhas, com um sleep curto entre os lotes. Cada lote é uma transação própria, então os locks são curtos e a replicação acompanha.
Passo 4, contract (depois que o código antigo foi totalmente desativado):
ALTER TABLE users ALTER COLUMN display_name SET NOT NULL;
DROP TRIGGER users_sync_display_name ON users;
DROP FUNCTION sync_display_name();
ALTER TABLE users DROP COLUMN fullname;
No PostgreSQL, SET NOT NULL varre a tabela. Em uma tabela grande, adicione primeiro CHECK (display_name IS NOT NULL) NOT VALID, depois VALIDATE CONSTRAINT (que só pega um lock fraco), e então o SET NOT NULL usa a check para pular a varredura.
Quatro passos em pelo menos duas releases para um rename. Esse é o custo de não sair do ar.
Exemplo 2: dividir uma tabela
Objetivo: mover os campos de endereço de customers para uma nova tabela customer_addresses, para que um cliente possa ter vários endereços.
Expand:
CREATE TABLE customer_addresses (
id bigserial PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers(id),
kind text NOT NULL DEFAULT 'primary',
street text,
city text,
postal_code text,
country text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX CONCURRENTLY customer_addresses_customer_id_idx
ON customer_addresses (customer_id);
Dual write: a aplicação escreve o endereço tanto nas colunas antigas quanto na tabela nova. As leituras vêm de customer_addresses quando existe uma linha, senão das colunas antigas.
Backfill, em lotes por faixa de id de cliente:
INSERT INTO customer_addresses (customer_id, kind, street, city, postal_code, country)
SELECT id, 'primary', street, city, postal_code, country
FROM customers
WHERE id > :last_id AND id <= :last_id + 5000
AND street IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM customer_addresses a
WHERE a.customer_id = customers.id AND a.kind = 'primary'
);
O NOT EXISTS torna o job idempotente: se ele morrer no meio, rodar de novo não duplica linhas.
Verifique, antes de contrair: conte os clientes com endereço nas colunas antigas mas sem linha primary na tabela nova. Precisa ser zero.
Contract: mude as leituras só para a tabela nova, remova o dual write e então apague as colunas antigas em uma release posterior.
Migrações longas sem travar a produção
A fase de expand é barata quando o DDL é só de metadados. Deixa de ser barata quando o motor precisa reescrever ou varrer a tabela, ou quando um lock fica na fila atrás de uma transação longa. Estas são as operações para ficar de olho.
PostgreSQL
CREATE INDEX CONCURRENTLYeDROP INDEX CONCURRENTLYevitam o lock de escrita. Não podem rodar dentro de uma transação, então a maioria das ferramentas precisa de uma flag por migração para desativar o wrapper transacional (-- atlas:txmode none,autocommit_block()no Alembic,disable_ddl_transaction!no Rails). Se interrompido, o índice ficaINVALID; confirapg_index.indisvalide recrie.- Adicionar uma coluna com default volátil (ex.:
DEFAULT now()) reescreve a tabela; um default constante não. - Foreign keys e check constraints: adicione com
NOT VALID, depoisVALIDATE CONSTRAINTseparadamente. - Sempre defina
lock_timeout(alguns segundos) na sessão da migração. UmALTER TABLEesperando um lockACCESS EXCLUSIVEbloqueia toda query atrás dele; um timeout faz ele falhar rápido em vez de congelar a aplicação. Tente de novo em loop. - Mudar o tipo de uma coluna geralmente reescreve a tabela. Prefira adicionar uma coluna nova e passar pelo expand-and-contract.
MySQL / MariaDB
- O InnoDB suporta
ALGORITHM=INSTANTpara adicionar colunas eALGORITHM=INPLACE, LOCK=NONEpara muitas operações de índice. Especifique-os explicitamente para que o comando falhe caso o motor fosse cair em uma cópia. - Para qualquer coisa que ainda exija cópia da tabela, use uma ferramenta externa de online schema change: gh-ost (baseado em binlog, com throttling, sem triggers) ou pt-online-schema-change (baseado em triggers). Os dois criam uma tabela sombra, copiam as linhas em blocos e trocam no final. Custam disco extra e adicionam carga de replicação, então rode fora do pico e acompanhe o lag.
Qualquer motor
- Processe migrações de dados em blocos de alguns milhares de linhas, por faixa de chave primária, uma transação por bloco.
- Torne os jobs de backfill retomáveis e idempotentes. Guarde o último id processado.
- Faça throttling pelo lag de replicação, não por um sleep fixo. Pause quando o lag ultrapassar o seu limite.
- Nunca rode um backfill dentro da transação da ferramenta de migração de schema. Entregue como um job da aplicação ou um script separado, com monitoramento próprio.
Comparativo de ferramentas de migração
As ferramentas fazem duas coisas: mantêm um histórico ordenado e versionado das mudanças e aplicam as que um determinado banco ainda não viu. Fora isso, diferem em como as migrações são escritas, se detectam drift entre o schema declarado e o banco real, e como se encaixam no CI.
| Ferramenta | Linguagem / ecossistema | Modelo de versionamento | Suporte a rollback | Detecção de drift | Integração com CI |
|---|---|---|---|---|---|
| Flyway | CLI Java, SQL-first, qualquer stack | Arquivos SQL versionados, checksums | Scripts de undo (plano pago) | validate confere checksums aplicados, não o schema real |
CLI, Maven/Gradle, imagem Docker |
| Liquibase | CLI Java, changesets XML/YAML/JSON/SQL | Changelog com changesets, checksums | Rollback embutido por changeset, tags | diff contra um banco de referência |
CLI, Maven/Gradle, imagem Docker |
| Alembic | Python / SQLAlchemy | Grafo de revisões (DAG), branches | downgrade por revisão |
autogenerate compara modelos vs banco |
Python, roda em qualquer pipeline |
| Prisma Migrate | TypeScript / Node | schema.prisma declarativo, histórico SQL gerado |
Nenhum embutido (escreva uma nova migração forward) | Detecta drift em migrate dev e migrate diff |
migrate deploy no CI |
| Rails Active Record | Ruby | DSL Ruby com timestamp, snapshot schema.rb |
down por migração, DSL reversível |
Compara schema.rb; a gem strong_migrations aponta DDL inseguro |
Tasks do Rake |
| Django migrations | Python | Grafo de dependências por app, autogerado | Operações reversas, migrate app 000N |
makemigrations --check falha o CI se faltar migração |
manage.py no pipeline |
| golang-migrate | CLI e biblioteca Go, SQL-first | Pares SQL up/down numerados | Arquivos down |
Nenhuma | Binário estático pequeno, imagem Docker |
| Atlas | CLI Go, qualquer stack, HCL/SQL/ORM como fonte | Declarativo ou versionado, diretório com checksum | Planejado via migrate down (ciente do estado) |
Recurso central: schema diff, schema inspect, linting de mudanças inseguras |
GitHub Action, gate de lint, imagem Docker |
Como ler a tabela:
- SQL-first vs declarativo. Flyway, golang-migrate e Liquibase (em modo SQL) guardam o que você escreveu. Prisma e Atlas guardam o estado final desejado e geram o diff. O declarativo é mais rápido de escrever, mas gera um
DROP COLUMNsem hesitar, então precisa de um gate de lint. - Suporte a rollback é menos útil do que parece. Uma down migration para
ADD COLUMNfunciona. Uma down migration paraDROP COLUMNnão consegue restaurar dados. Veja a próxima seção. - Detecção de drift é o recurso que mais falta nas equipes e que mais aparece em incidentes: um hotfix aplicado na mão em produção que nenhum arquivo de migração conhece. Atlas,
diffdo Liquibase eautogeneratedo Alembic pegam isso; Flyway e golang-migrate, não. - Linting de DDL inseguro (Atlas lint, strong_migrations para Rails, squawk para arquivos SQL do PostgreSQL) transforma as regras da seção anterior em falhas de CI. Adicione um, seja qual for a ferramenta que você usa.
Estratégia de rollback e de testes
A posição honesta sobre rollback: mudanças de schema aditivas podem ser desfeitas, mudanças de dados geralmente não. Planeje em torno dessa assimetria.
Prefira roll-forward. Como o expand-and-contract nunca quebra a versão antiga, "rollback" na fase de expand significa refazer o deploy da versão anterior da aplicação e deixar a coluna nova no lugar. É inofensivo. Na fase de contract, rollback é uma restauração de backup, e é por isso que o contract roda por último e só depois da verificação.
Ensaie em uma cópia parecida com produção. Restaure o backup da noite anterior (anonimizado, se necessário) em uma instância descartável e rode o conjunto completo de migrações. Meça o tempo de relógio por migração e anote as que passam de alguns segundos. Fixtures sintéticas com cem linhas não vão mostrar um problema de lock; uma cópia com a contagem real de linhas e a cardinalidade real dos índices vai. Isso também serve como o teste de restauração de backup que a maioria das equipes nunca faz.
Automatize as verificações no CI, junto com o seu pipeline de CI/CD normal:
- Banco limpo: aplique todas as migrações do zero e então rode a suíte de testes.
- Banco existente: aplique só as migrações novas sobre um snapshot do schema atual de produção.
- Down e up de novo para cada migração nova, para provar que o script de down pelo menos roda.
- Lint de operações inseguras (renames, drops, NOT NULL sem default,
CONCURRENTLYfaltando). - Falhe o build se o modelo do ORM e o histórico de migrações discordarem (
makemigrations --check,prisma migrate diff,atlas migrate lint).
Verifique os dados, não só o DDL. Depois de um backfill, rode queries de reconciliação: contagem de linhas de cada lado, nulls na coluna nova, foreign keys sem pai. Mantenha as queries no repositório ao lado da migração.
Acompanhe os sinais certos durante a aplicação: esperas por lock, lag de replicação, latência p99 dos endpoints mais quentes, taxa de erro. Ligue o executor de migrações ao mesmo logging e alertas da aplicação, para que abortar seja uma decisão, não um chute.
Checklist de migração em produção
Antes do merge:
- A mudança está dividida em expand, código, backfill e contract, cada um na própria migração ou release.
- Nenhum rename, drop, mudança de tipo ou NOT NULL em coluna que a versão atual lê.
- Toda migração é idempotente ou protegida (
IF NOT EXISTS,NOT EXISTSnos backfills). - Índices em tabelas grandes usam
CONCURRENTLY(PostgreSQL) ouINPLACE, LOCK=NONE/ gh-ost (MySQL). - Constraints são adicionadas como
NOT VALIDe validadas separadamente. -
lock_timeoutestatement_timeoutdefinidos para a sessão da migração. - Backfills em lotes, retomáveis e rodando fora da transação da migração.
- Revisado por alguém responsável pelo banco, não só pela funcionalidade.
Antes de aplicar em produção:
- Ensaiado em uma cópia do tamanho de produção, com duração medida.
- Backup verificado e point-in-time recovery confirmado para a janela.
- Caminho de rollback por escrito: qual versão da aplicação refazer o deploy, qual migração reverter ou qual restauração rodar.
- Aplicado na ordem certa em relação ao deploy: expand antes, contract depois.
- O runbook tem os comandos exatos, os critérios de aborto e quem está de plantão.
Depois de aplicar:
- As queries de reconciliação passam.
- Nenhum índice inválido, nenhuma constraint sem validar deixada para trás.
- Migração de contract agendada para uma release posterior, com ticket, para que a coluna antiga não viva para sempre.
Quando escolher cada abordagem
- Tabela pequena, pouco tráfego, janela curta de manutenção aceitável: uma única migração com lock costuma ser mais barata que quatro passos. Meça em uma cópia primeiro; "pequena" significa que o DDL termina bem abaixo do seu
lock_timeout. - Tabela grande ou SLA rígido: expand-and-contract com backfill em lotes, sempre. A release extra é o preço de continuar no ar.
- MySQL com uma cópia de tabela que você não consegue evitar: gh-ost ou pt-online-schema-change, fora do pico, acompanhando o lag.
- Índice ou constraint do PostgreSQL em uma tabela quente:
CONCURRENTLYeNOT VALID/VALIDATE, com o wrapper transacional desativado para essa migração. - Muitos serviços em um só schema: triggers para o dual write durante a transição, porque você não consegue coordenar o deploy de todos os escritores.
- Trocar o motor ou o host: replicação lógica ou CDC para o destino novo, dual write para verificação, cutover ensaiado com um go/no-go firme. Isso é uma migração de plataforma e merece um plano próprio.
Recomendação
Adote o expand-and-contract como padrão para toda mudança de schema, não como um procedimento especial para as grandes. Use a ferramenta de migração nativa do seu framework, se houver, e adicione ao CI um linter de DDL inseguro e uma verificação de drift, independentemente da ferramenta. Ensaie as migrações de cada release em uma cópia do tamanho de produção; é a única prática que pega problemas de lock e de duração antes dos usuários. Trate rollback como roll-forward mais backups verificados, e mantenha o passo de contract como um follow-up rastreado, para que os schemas não acumulem colunas mortas. Na Arvucore, costumamos recomendar começar pelo linter e pelo ambiente de ensaio: custam um dia para configurar e removem a maior parte do risco de toda migração daí em diante.
Pronto para Transformar seu Negócio?
Vamos conversar sobre como nossas soluções podem ajudá-lo a alcançar seus objetivos. Entre em contato com nossos especialistas hoje mesmo.
Falar com um EspecialistaTags:
Equipe Arvucore
A equipe editorial da Arvucore é formada por profissionais experientes em desenvolvimento de software. Somos dedicados a produzir e manter conteúdo de alta qualidade que reflete as melhores práticas da indústria e insights confiáveis.
Perguntas frequentes
- O que é uma migração de banco de dados com zero downtime?
- É uma mudança de schema ou de dados aplicada enquanto a aplicação continua atendendo tráfego. Funciona dividindo a mudança em passos pequenos e retrocompatíveis, de modo que a versão antiga e a nova da aplicação consigam rodar contra o mesmo banco em todos os momentos.
- O que é o padrão expand-and-contract?
- Expand adiciona a estrutura nova ao lado da antiga, a aplicação passa a escrever nas duas e ler da nova, os dados são preenchidos (backfill) e contract remove a estrutura antiga. Nenhum passo isolado quebra a versão em execução.
- As migrações devem rodar antes ou depois do deploy da aplicação?
- Migrações aditivas (expand) rodam antes do deploy, para que o código novo encontre as colunas de que precisa. Migrações destrutivas (contract) rodam depois que o código antigo foi totalmente desativado, geralmente em uma release posterior.
- Dá para reverter uma migração de banco de dados?
- Mudanças de schema apenas aditivas podem ser desfeitas com uma down migration. Qualquer coisa que apagou dados ou reescreveu linhas não pode ser revertida de verdade; nesses casos, conte com backups, point-in-time recovery e uma correção roll-forward. Projete as migrações para que o rollback raramente seja necessário.
- Qual ferramenta de migração de banco de dados devo usar?
- Use a nativa da sua stack quando existir (Rails, Django, Prisma, Alembic). Para equipes poliglotas ou SQL-first, Flyway e golang-migrate são simples e previsíveis; o Liquibase adiciona recursos de governança; o Atlas adiciona schemas declarativos, detecção de drift e linting.
- Como adicionar um índice em uma tabela grande do PostgreSQL sem travá-la?
- Use CREATE INDEX CONCURRENTLY fora de uma transação. Demora mais e pode deixar um índice inválido se for interrompido, então confira pg_index.indisvalid depois e recrie se necessário.
Artigos relacionados

GraphQL vs REST em 2026: quando usar cada estilo de API
GraphQL vs REST comparados em fetching, cache, versionamento, erros, N+1, segurança e ferramentas, com checklist de decisão por cenário e exemplo lado a lado.

Arquitetura Hexagonal em 2026: Portas, Adaptadores, Clean
Hexagonal vs Clean vs Onion explicadas com um exemplo real em TypeScript, estrutura de pastas, estratégia de testes e os erros que afundam a maioria dos times.

Jamstack em 2026: o que sobreviveu e o que o substituiu
O rótulo Jamstack sumiu, mas as práticas venceram. Estratégias de renderização comparadas, frameworks, hospedagem e um checklist para saber quando estático é a escolha certa.

Event-Driven Architecture: Resilient and Escalável Sistemas
Na Arvucore, exploramos como a arquitetura orientada a eventos transforma sistemas distribuídos modernos, possibilitando aplicações responsivas, resilientes e escaláveis. Este artigo examina princípios básicos, padrões de design práticos e considerações operacionais para a implementação de sistemas orientados a eventos em ambientes corporativos. Os leitores encontrarão orientações sobre escolhas de arquitetura, estratégias de integração e benefícios mensuráveis para apoiar a agilidade dos negócios e a robustez técnica em implantações de produção.