As migrações de banco no Node.js registram mudanças de schema como código versionado. Em vez de alterar tabelas manualmente em cada ambiente, a equipe executa arquivos ordenados que criam colunas, índices, constraints e dados de referência de forma reproduzível.
Migrations fazem parte do deploy e podem causar indisponibilidade se bloquearem uma tabela grande, removerem uma coluna ainda utilizada ou rodarem simultaneamente em várias instâncias. Uma estratégia segura separa mudanças aditivas, backfills e remoções, mantendo versões antiga e nova compatíveis durante o rollout.
Neste guia, você aprenderá a organizar arquivos, controlar versão, usar transações, aplicar expand-and-contract, executar backfills, evitar locks, integrar com CI/CD, realizar rollback e testar em bancos realistas.
O que é uma migration?
Migration é uma transformação versionada do schema ou de dados estruturais. A documentação oficial de definição de dados do PostgreSQL descreve tabelas, constraints e alterações. A documentação oficial de ALTER TABLE apresenta operações e opções.
Para conexão e transações, consulte Pool PostgreSQL no Node.js. Para query builders tipados, veja Kysely com TypeScript e Drizzle ORM com PostgreSQL.
Por que versionar?
- reproduzir ambientes;
- revisar mudanças em pull request;
- automatizar deploy;
- saber qual versão está aplicada;
- reduzir alterações manuais;
- testar evolução do schema;
- auditar quem mudou e por quê.
Tabela de controle
CREATE TABLE schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now(),
checksum TEXT NOT NULL
);A ferramenta registra cada arquivo aplicado. Checksum ajuda a detectar edição posterior.
Não edite migration aplicada
Depois que uma migration chega a um ambiente compartilhado, crie outra para corrigir. Editar o arquivo causa divergência entre bancos que já executaram e bancos novos.
Nomes ordenados
migrations/
├── 202608241200_create_orders.sql
├── 202608241230_add_order_status.sql
└── 202608241300_index_orders_created_at.sqlTimestamp ou sequência fornece ordem. Inclua descrição curta.
Migration SQL
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
status TEXT NOT NULL,
total_cents BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);SQL explícito facilita entender a mudança e usar recursos específicos do banco.
Migration em JavaScript
exports.up = async function up(database) {
await database.schema.createTable('orders', table => {
table.bigIncrements('id').primary();
table.bigInteger('customer_id').notNullable();
table.text('status').notNullable();
table.bigInteger('total_cents').notNullable();
table.timestamp('created_at').notNullable();
});
};O formato depende da ferramenta. Evite lógica não determinística e chamadas externas.
Up e down
Algumas ferramentas usam up para aplicar e down para reverter. Nem toda mudança possui rollback seguro. Remover uma coluna e tentar recriá-la não recupera os dados.
Rollback forward
Em produção, muitas equipes corrigem com nova migration em vez de executar down. Isso preserva histórico e reduz operações destrutivas durante incidente.
Transação
BEGIN;
ALTER TABLE orders
ADD COLUMN source TEXT;
UPDATE schema_migrations
SET ...;
COMMIT;PostgreSQL permite muitas alterações DDL em transação, mas operações como CREATE INDEX CONCURRENTLY possuem restrições.
Uma migration por transação?
Aplicar cada arquivo em sua própria transação limita o impacto de falha. Uma única transação para centenas de mudanças pode manter locks e WAL por muito tempo.
Lock de migration
Duas instâncias não devem aplicar a mesma migration simultaneamente. Use advisory lock:
SELECT pg_advisory_lock(918273645);Depois de concluir:
SELECT pg_advisory_unlock(918273645);Garanta liberação em finally e use a mesma conexão.
Migration como Job
Em Kubernetes, execute um Job ou etapa separada do pipeline. Não coloque migration automática no startup de todos os pods.
Por que não executar em cada pod?
- concorrência;
- startup lento;
- restart loop;
- permissões amplas na aplicação;
- logs dispersos;
- dificuldade de aprovação;
- schema destrutivo antes do rollout.
Permissões separadas
O usuário da aplicação pode ter apenas SELECT, INSERT, UPDATE e DELETE necessários. O usuário de migration possui ALTER e CREATE, usado apenas pelo pipeline.
Expand and contract
O padrão mais seguro para deploy sem downtime possui etapas.
Expandir
Adicione nova estrutura sem remover a antiga:
ALTER TABLE users
ADD COLUMN display_name TEXT;Compatibilizar código
A nova versão lê a coluna nova e usa fallback para a antiga. Se necessário, escreve em ambas.
Backfill
Preencha registros existentes em lotes.
Contrair
Depois que nenhuma versão utiliza a estrutura antiga, remova-a em outro deploy.
Adicionar coluna com default
Comportamento e custo dependem da versão do PostgreSQL e da expressão. Defaults voláteis podem exigir reescrita. Avalie no banco de homologação com volume real.
NOT NULL
Adicionar NOT NULL diretamente em uma coluna cheia pode verificar toda a tabela e bloquear. Uma sequência segura:
- adicionar coluna nullable;
- fazer backfill;
- adicionar constraint de check NOT VALID;
- validar a constraint;
- converter para NOT NULL quando apropriado.
Constraint NOT VALID
ALTER TABLE users
ADD CONSTRAINT users_display_name_present
CHECK (display_name IS NOT NULL) NOT VALID;Depois:
ALTER TABLE users
VALIDATE CONSTRAINT users_display_name_present;Consulte a documentação da versão e teste os locks envolvidos.
Índices
CREATE INDEX CONCURRENTLY
idx_orders_created_at
ON orders (created_at);CONCURRENTLY reduz bloqueio de escritas, mas demora mais, usa mais trabalho e não pode rodar dentro de transaction block.
Índice inválido
Se uma criação concorrente falha, pode deixar índice inválido. Detecte, remova e repita de forma controlada.
Índice único
Antes de criar unique, procure duplicados. Uma migration que descobre dados inválidos em produção pode falhar após minutos de lock.
Foreign keys
Adicionar foreign key em tabela grande pode verificar registros. Considere NOT VALID e validação posterior.
Renomear coluna
Renomear quebra código antigo imediatamente. Prefira adicionar coluna nova, escrever em ambas, backfill e remover a antiga.
Alterar tipo
ALTER TABLE events
ALTER COLUMN payload TYPE JSONB
USING payload::jsonb;A operação pode reescrever a tabela. Para alto volume, crie coluna nova e migre em lotes.
Backfill
Não atualize milhões de linhas em uma única transação:
UPDATE users
SET display_name = name
WHERE id > $1
AND id <= $2
AND display_name IS NULL;Execute lotes por faixa de chave e registre progresso.
Tamanho do lote
Lotes grandes terminam rápido, mas geram locks, WAL e replica lag. Lotes pequenos reduzem impacto, porém aumentam duração. Meça e ajuste dinamicamente.
Pausa entre lotes
Introduzir delay permite que banco e réplicas recuperem capacidade.
Idempotência do backfill
A condição display_name IS NULL torna a operação repetível. Se o job reiniciar, continua sem sobrescrever dados já processados.
Checkpoint
Armazene último ID processado em tabela de jobs ou derive pela condição. Não dependa apenas de memória do processo.
Backfill separado da migration
Uma migration de schema deve ser rápida. Backfill grande pode executar como job observável e pausável.
Compatibilidade com Blue-Green
Ambientes Blue e Green compartilham banco. A migration precisa funcionar com ambos. Consulte Blue-Green Deploy no Node.js.
Compatibilidade com Canary
Versão estável e canary operam simultaneamente. Dados gravados pela nova versão precisam ser legíveis pela antiga. Consulte Canary Deploy no Node.js.
Feature flags
Use flag para ativar o caminho que depende do novo schema apenas depois da migration e do backfill. Consulte Feature Flags no Node.js.
Ordem do deploy
- Aplicar expansão compatível.
- Implantar código compatível.
- Executar backfill.
- Ativar comportamento.
- Observar.
- Remover código antigo.
- Aplicar contração.
Migration destrutiva
DROP COLUMN, DROP TABLE e alteração de tipo devem exigir revisão adicional, backup e confirmação de que o código antigo não existe.
Backup
Backup não substitui migration segura. Restaurar banco grande pode levar horas e perder dados recentes. Teste restauração e conheça RPO e RTO.
Replica lag
Backfills e índices geram WAL. Monitore atraso das réplicas antes de continuar lotes.
Timeouts
SET lock_timeout = '5s';
SET statement_timeout = '10min';Um lock_timeout curto evita esperar indefinidamente por tabela ocupada. Defina por sessão da migration.
Deadlocks
Migrations concorrentes com tráfego podem participar de deadlock. Adquira locks em ordem consistente e execute em janela adequada.
Observabilidade
Registre:
- versão;
- início e fim;
- duração;
- linhas alteradas;
- lotes;
- locks;
- replica lag;
- erro;
- responsável.
Métricas
Monitore conexões, CPU do banco, I/O, WAL, locks e latência da aplicação durante a migration. Consulte Métricas Prometheus no Node.js.
Logs
Use logs estruturados sem incluir credenciais ou SQL com dados sensíveis. Consulte Logs com Pino no Node.js.
CI
O pipeline deve criar banco vazio, aplicar todas as migrations e executar testes. Também deve iniciar de uma versão anterior real e aplicar apenas as novas.
Teste de banco vazio
Garante que um novo ambiente consegue chegar ao schema atual sem snapshots manuais esquecidos.
Teste de upgrade
Restaure um dump sanitizado da versão anterior, aplique migrations e rode a aplicação.
Teste de volume
Homologação com dez registros não revela locks de uma tabela com cem milhões. Gere volume representativo e meça.
Lint de migration
Automatize alertas para operações arriscadas:
- DROP;
- NOT NULL direto;
- CREATE INDEX sem CONCURRENTLY;
- UPDATE sem filtro;
- tipo reescrito;
- migration sem timeout.
Revisão humana
Ferramentas ajudam, mas a equipe precisa entender tamanho da tabela, versão do banco e tráfego.
Ambientes divergentes
Alterações manuais causam drift. Compare schema e bloqueie acesso administrativo desnecessário.
Checksum
Se o checksum de uma migration aplicada muda, interrompa e investigue. Não atualize o registro silenciosamente.
Privilégios
O pipeline de migration deve usar credencial temporária ou protegida. Não compartilhe a senha de owner com a aplicação.
Segredos
Carregue conexão por Secret e não imprima DATABASE_URL. Consulte ConfigMaps e Secrets no Node.js.
Execução manual de emergência
Defina runbook com aprovação, backup, comandos, observação e rollback forward. Evite colar SQL desconhecido diretamente em produção.
Erros comuns
- Migration no startup de todos os pods: ocorre concorrência.
- Editar arquivo aplicado: ambientes divergem.
- DROP no mesmo deploy: rollback de código quebra.
- Backfill único: locks e WAL explodem.
- Índice bloqueante: escritas param.
- Sem lock_timeout: pipeline fica pendurado.
- Teste apenas em banco vazio: upgrade real falha.
Boas práticas
- Versione cada mudança.
- Não edite migrations aplicadas.
- Use lock único de execução.
- Separe schema e backfill.
- Aplique expand-and-contract.
- Use índices concorrentes quando adequado.
- Defina timeouts.
- Teste com volume realista.
- Monitore banco e aplicação.
- Prefira rollback forward.
Conclusão
As migrações de banco no Node.js tornam mudanças de schema reproduzíveis e auditáveis, mas fazem parte do caminho crítico de deploy. Alterações rápidas em desenvolvimento podem causar locks e incompatibilidade em produção.
O padrão expand-and-contract, backfills em lotes, execução única e testes de upgrade permitem evoluir sem downtime. Com credenciais separadas, timeouts, métricas e rollback forward, migrations deixam de ser comandos manuais perigosos e se tornam uma etapa controlada da entrega.




