A migração que reduz a produção raramente é complexa. Geralmente é um únicoALTER TABLE que ocupou um bloqueio exclusivo em uma mesa grande enquanto o aplicativo esperava. A migração sem tempo de inatividade é mais uma disciplina do que uma ferramenta: divida cada mudança em etapas que sejam individualmente seguras, com códigos novos e antigos em execução ao mesmo tempo.
📋 Table of Contents
- O Princípio Fundamental: Expandir e Contrair
- Exemplo: Renomeando uma Coluna
- Quais operações são bloqueadas
- Adicionando índices com segurança
- Adicionando NOT NULL sem um bloqueio longo
- Preenchimento de Mesas Grandes
- Defina um tempo limite de bloqueio
- Migrando entre bancos de dados
- Planejamento de reversão
- Lista de verificação pré-voo
- Conclusão
O Princípio Fundamental: Expandir e Contrair
Você não pode implantar uma alteração de esquema e o código que precisa dela ao mesmo tempo. Durante qualquer implantação contínua, versões antigas e novas do aplicativo são executadas simultaneamente. Toda migração deve, portanto, ser compatível com ambos.
O padrão de contrato expandido divide cada mudança em três implantações:
- Expandir – adicione a nova estrutura. O código antigo o ignora; novo código pode usá-lo.
- Migrar — preencher os dados e mudar o aplicativo para a nova estrutura.
- Contrato — remova a estrutura antiga quando nada fizer referência a ela.
Cada etapa é implementada separadamente e cada uma é reversível individualmente. É isso que torna toda a sequência segura.
Exemplo: Renomeando uma Coluna
Uma renomeação parece trivial e é uma das operações mais perigosas, porque quebra o código antigo no instante em que é executado.
-- ❌ Never do this on a live system.
ALTER TABLE users RENAME COLUMN email TO email_address;
-- Every running instance still querying "email" fails immediately.
A sequência segura:
-- Step 1 (expand): add the new column. Nullable, no default — instant.
ALTER TABLE users ADD COLUMN email_address TEXT;
-- Step 2: deploy code that WRITES both columns and READS the old one.
-- UPDATE users SET email = $1, email_address = $1 WHERE id = $2
-- Step 3: backfill existing rows in batches (see below).
-- Step 4: deploy code that READS the new column and still writes both.
-- Step 5: deploy code that only uses the new column.
-- Step 6 (contract): drop the old column, once nothing references it.
ALTER TABLE users DROP COLUMN email;
Seis implantações para uma renomeação parecem excessivas até que a primeira vez que uma renomeação em uma etapa cause uma interrupção. A maioria das equipes acaba decidindo que renomear não vale a pena e simplesmente mantém o nome original.
Quais operações são bloqueadas
Saber quais instruções possuem um bloqueio de bloqueio é a maior parte da habilidade. No PostgreSQL moderno:
| Operação | Seguro? | Notas |
|---|---|---|
| Adicionar coluna anulável | Seguro | Somente metadados, instantâneo |
| Adicionar coluna com padrão constante | Seguro | Nenhuma tabela reescrita nas versões modernas |
| Adicionar coluna com padrão volátil | Perigoso | Reescreve a tabela inteira |
| Adicionar índice | Perigoso | Bloqueia gravações — use CONCORRENTEMENTE |
| Adicione NOT NULL à coluna existente | Perigoso | Varredura completa bloqueada — use primeiro uma restrição CHECK |
| Alterar tipo de coluna | Perigoso | Geralmente uma reescrita completa |
| Adicionar chave estrangeira | Perigoso | Valida todas as linhas — adicione NOT VALID primeiro |
| Coluna descartada | Seguro | Apenas metadados, mas quebra código antigo |
Sempre verifique a versão específica do seu banco de dados. O comportamento dessas operações melhorou substancialmente em relação aos lançamentos recentes, e os conselhos escritos para versões mais antigas são muitas vezes desnecessariamente cautelosos — ou perigosamente desatualizados na outra direção.
Adicionando índices com segurança
-- ❌ Blocks all writes to the table for the duration.
CREATE INDEX idx_users_email ON users(email);
-- ✅ Builds without blocking writes.
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
Duas coisas para saberCONCURRENTLY. Ele não pode ser executado dentro de um bloco de transação, o que significa que a maioria das estruturas de migração precisa de configuração explícita para permitir isso. E pode falhar parcialmente, deixando para trás um índice inválido que deve ser limpo.
-- Find invalid indexes left behind by a failed concurrent build
SELECT indexrelid::regclass AS index_name
FROM pg_index
WHERE NOT indisvalid;
-- Drop and retry
DROP INDEX CONCURRENTLY idx_users_email;
Adicionando NOT NULL sem um bloqueio longo
AdicionandoNOT NULL verifica diretamente toda a tabela enquanto segura um cadeado. Uma restrição CHECK validada atinge a mesma garantia em duas etapas sem bloqueio.
-- 1. Add the constraint without validating existing rows — instant.
ALTER TABLE users
ADD CONSTRAINT users_email_not_null
CHECK (email IS NOT NULL) NOT VALID;
-- 2. Validate separately. Takes a weaker lock that allows reads and writes.
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
O mesmo padrão NOT VALID/VALIDATE se aplica a chaves estrangeiras e pelo mesmo motivo.
Preenchimento de Mesas Grandes
Um únicoUPDATE em milhões de linhas mantém bloqueios, gera um enorme volume de log write-ahead e pode paralisar a replicação. Agrupar.
-- ❌ One statement, one very long transaction, many locked rows.
UPDATE users SET email_address = email;
-- ✅ Batched, resumable, and gentle on replication.
DO $$
DECLARE
batch_size INT := 5000;
affected INT;
BEGIN
LOOP
UPDATE users
SET email_address = email
WHERE id IN (
SELECT id FROM users
WHERE email_address IS NULL AND email IS NOT NULL
ORDER BY id
LIMIT batch_size
FOR UPDATE SKIP LOCKED
);
GET DIAGNOSTICS affected = ROW_COUNT;
EXIT WHEN affected = 0;
COMMIT;
PERFORM pg_sleep(0.1); -- let replicas catch up
END LOOP;
END $$;
FOR UPDATE SKIP LOCKED evita que o preenchimento bloqueie as linhas que seu aplicativo está atualizando no momento. O curto sono entre os lotes é o que impede o aumento do atraso na replicação e é a etapa que as pessoas omitem e se arrependem.
Para tabelas muito grandes, execute o preenchimento como um trabalho separado, em vez de dentro de uma migração, para que uma implantação nunca fique esperando por ele.
Defina um tempo limite de bloqueio
Essa configuração única evita a maioria das interrupções de migração. Sem ele, uma migração que não pode adquirir um bloqueio espera indefinidamente — e cada consulta enfileirada atrás dele também espera, e é assim que seALTER TABLE paralisa um aplicativo inteiro.
-- Fail fast instead of queueing behind a long-running query.
SET lock_timeout = '3s';
SET statement_timeout = '30s';
ALTER TABLE users ADD COLUMN email_address TEXT;
Se o tempo expirar, tente novamente mais tarde. Uma migração com falha que você pode executar novamente é muito melhor do que uma tabela bloqueada durante o pico de tráfego.
Migrando entre bancos de dados
A mudança para um novo mecanismo ou instância de banco de dados usa o mesmo princípio em uma escala maior.
- Replicar – configurar a replicação lógica ou alterar a captura de dados do antigo para o novo. Deixe-o alcançar completamente.
- Leitura dupla – leia o banco de dados antigo e leia o novo em segundo plano para comparar os resultados. Registre todas as incompatibilidades e corrija as causas antes de continuar.
- Escrita dupla – escreva para ambos. O antigo permanece autoritário.
- Cortar — alterna as leituras para o novo banco de dados. Continue escrevendo para ambos.
- Desativação— pare de gravar no banco de dados antigo quando estiver confiante, mantendo-o como um caminho de reversão por um período significativo.
A fase de comparação na etapa 2 é o que torna isso seguro. Ignorá-lo significa descobrir diferenças de dados após a transição, quando a reversão é cara.
Planejamento de reversão
Toda migração precisa de uma resposta para “e se isso estiver errado?” antes de ser executado.
Mudanças aditivas são revertidas trivialmente – descartar uma coluna que você acabou de adicionar é seguro porque nada depende disso.
Mudanças destrutivas não são revertidas. Depois que uma coluna é eliminada, os dados desaparecem. É exatamente por isso que as etapas do contrato vêm por último e somente após um período de confiança.
Os aterros também precisam de reversibilidade. Se você substituir uma coluna, mantenha os valores originais em algum lugar até ter certeza.
Nunca escreva uma migração cuja etapa inferior exclua dados. Se uma reversão resultar na perda de informações, a migração não deverá ser reversível automaticamente — torne o caminho de recuperação uma decisão manual e deliberada.
Lista de verificação pré-voo
- Teste em uma cópia restaurada dos dados de produção, em escala de produção
- Saiba se cada instrução possui um bloqueio de bloqueio
- Definir
lock_timeoutestatement_timeout - Agrupe cada preenchimento e execute-o fora da implantação
- Confirme se o código do aplicativo antigo e o novo funcionam com o esquema intermediário
- Observe o atraso da replicação durante e depois
- Tenha um backup verificado e saiba quanto tempo leva uma restauração
- Implante durante tráfego baixo, mesmo quando você não espera nenhum bloqueio
Conclusão
A migração sem tempo de inatividade se resume a algumas disciplinas:dividir cada mudança em fases de expansão, migração e contrato implantadas separadamente; saber quais instruções possuem bloqueios de bloqueio e usar CONCURRENTLY e NOT VALID para evitá-las; preenchimentos em lote com pausas para que a replicação continue; e sempre defina um tempo limite de bloqueio para que a migração falhe rapidamente, em vez de colocar todo o seu aplicativo na fila. As implantações extras parecem uma sobrecarga até a primeira migração que, de outra forma, teria causado uma interrupção.
🔗 Share this article
✍️ Leave a Comment