O Blueprint para Mudança de Schema
Uma migração de schema parece um problema de escrita SQL, mas o SQL raramente é onde o risco realmente reside.
O risco reside no bloqueio: qual bloqueio uma instrução adquire, quanto tempo ela espera para adquirir esse bloqueio e o que mais em uma tabela de produção movimentada é bloqueado enquanto ela espera.
Resumo
- Mudança de schema segura é uma disciplina de sequenciamento e bloqueio sobreposta ao DDL comum, não apenas sintaxe SQL correta.
- Por que Importa: O mesmo
ALTER TABLEque executa instantaneamente em uma tabela vazia pode entrar na fila atrás de todas as outras consultas em uma movimentada e, em seguida, bloquear todas elas por sua vez. - Conceitos-Chave: DDL transacional, força de bloqueio, restrição
NOT VALID,CONCURRENTLY, expandir/contrair. - Quando Usar: Planejando qualquer alteração em uma tabela que recebe tráfego ao vivo, escolhendo entre uma alteração na mesma release e uma faseada, ou depurando por que uma migração travou em produção.
- Limitações / Trade-offs: Técnicas de zero downtime trocam uma migração simples por várias menores e mais cuidadosamente sequenciadas, o que custa tempo de coordenação mesmo quando economiza tempo de atividade.
- Tópicos Relacionados: Transações DDL, pool de conexões sob contenção de bloqueio, estratégia de rollback, manutenção de tabelas grandes.
Fundamentos
A maioria das instruções DDL do PostgreSQL são transacionais, o que significa que um CREATE TABLE ou ALTER TABLE dentro de BEGIN ... COMMIT reverte limpa se qualquer coisa posterior nessa transação falhar.
Essa é uma vantagem genuína sobre bancos de dados onde o DDL faz commit automático instrução por instrução, já que uma migração PostgreSQL com falha pode deixar o schema exatamente como estava antes da tentativa.
Um pequeno conjunto de operações são a exceção deliberada: CREATE INDEX CONCURRENTLY, VACUUM, e algumas outras não podem ser executadas dentro de um bloco de transação, pois seu propósito é evitar a manutenção do tipo de bloqueio que uma instrução DDL encapsulada em transação exigiria.
A outra ideia fundamental é a força de bloqueio: cada instrução DDL adquire um bloqueio na tabela que ela toca, e declarações diferentes precisam de bloqueios de diferentes forças, desde aqueles que meramente bloqueiam outro DDL até aqueles que bloqueiam todas as leituras e escritas na tabela.
Uma analogia útil é uma ponte de uma faixa: um bloqueio leve é como um estreitamento temporário de faixa que o tráfego ainda pode passar lentamente, enquanto um bloqueio pesado é um fechamento completo da ponte, e o perigo não é o trabalho de construção em si, mas sim quanto tempo o fechamento dura enquanto o tráfego se acumula atrás dele.
ALTER TABLE ... ADD COLUMN sem um valor padrão, por exemplo, adquire um bloqueio forte, mas o mantém brevemente, pois apenas atualiza metadados do catálogo; a mesma instrução com um padrão volátil historicamente exigia a reescrita de cada linha enquanto mantinha esse bloqueio.
Mecânicas e Interações
O perigo prático em uma migração raramente é a força do bloqueio sozinha - é a força combinada com quanto tempo a instrução espera para adquiri-lo.
Uma solicitação de bloqueio forte entra na fila atrás de qualquer transação que já esteja tocando essa tabela, e cada consulta que chega após a solicitação de bloqueio da migração também entra na fila atrás dela, mesmo leituras simples que, de outra forma, executariam instantaneamente.
Esse efeito de enfileiramento é o que transforma um ALTER TABLE de dois segundos em um incidente de produção: a instrução em si é rápida, mas se ela tiver que esperar dez minutos para uma transação de longa duração terminar, todas as outras consultas nessa tabela esperam junto com ela.
-- Barato: mudança apenas de catálogo, bloqueio forte breve
ALTER TABLE orders ADD COLUMN priority int;
-- Mais seguro para uma tabela movimentada: constrói o índice sem bloquear escritas
CREATE INDEX CONCURRENTLY idx_orders_priority ON orders (priority);
-- Adiciona uma restrição sem um bloqueio inicial de validação de tabela completa
ALTER TABLE orders ADD CONSTRAINT priority_positive
CHECK (priority > 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT priority_positive;NOT VALID é o padrão geral por trás da maioria dos trabalhos de restrição de zero downtime: ele adiciona a restrição imediatamente sob um bloqueio breve, e então um passo separado VALIDATE CONSTRAINT verifica as linhas existentes sob um bloqueio muito mais leve que não bloqueia escritas concorrentes.
Definir lock_timeout em sessões de migração transforma uma espera indefinida em uma falha rápida e visível em vez de um congestionamento silencioso, que é exatamente a diferença entre uma migração que falha ruidosamente em um log de deploy e uma que paralisa silenciosamente a produção.
Considerações Avançadas e Aplicações
Expandir/contrair é o padrão que generaliza tudo isso em um playbook repetível para mudanças que não podem ser feitas com segurança em uma única etapa: adicione a nova forma aditivamente, migre leituras e escritas para ela gradualmente, e então remova a forma antiga quando nada mais depender dela.
Renomear uma coluna é o exemplo mais claro, pois um RENAME COLUMN direto quebra todas as instâncias de aplicação implantadas que ainda esperam o nome antigo no momento em que é confirmado, enquanto uma versão expandir/contrair adiciona a nova coluna, a preenche, muda o tráfego da aplicação e só então descarta a coluna antiga em um deploy posterior e separado.
Em escala, o próprio "preenchimento" (backfill) se torna um problema adjacente à migração, pois atualizar cada linha de uma tabela grande em uma única instrução requer o mesmo tipo de bloqueio de longa duração que uma mudança de schema, e é por isso que grandes preenchimentos são tipicamente divididos em pequenos lotes com pausas entre eles.
Ferramentas de migração (Flyway, Liquibase, ou uma tabela de histórico de schema feita manualmente) resolvem um problema genuinamente diferente da segurança de bloqueio: elas resolvem ordenação e idempotência, garantindo que cada ambiente aplique as mesmas instruções na mesma ordem exatamente uma vez, mas não têm opinião sobre se uma instrução individual é segura para ser executada em uma tabela ativa.
Essa distinção é importante porque uma ferramenta de migração bem executada ainda pode aplicar fielmente uma instrução insegura; a correção do sequenciamento e a segurança do bloqueio são duas disciplinas separadas que ambas devem ser verdadeiras ao mesmo tempo.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| DDL Direto em uma transação | Mais simples de escrever e revisar | Bloqueios de longa duração em tabelas movimentadas; sem rollout parcial | Tabelas pequenas, tabelas de baixo tráfego, janelas de manutenção |
NOT VALID + VALIDATE separado | Adiciona restrições sem um bloqueio de validação de tabela completa | Processo de duas etapas; a restrição fica não aplicada entre as etapas | Adicionando restrições a tabelas grandes e ativamente escritas |
| Expandir/contrair completo entre releases | Genuinamente zero downtime; cada etapa independentemente segura | Mais coordenação, mais deploys, linha do tempo total mais longa | Renomes, mudanças de tipo e qualquer alteração que quebre compatibilidade em uma tabela ativa |
Equívocos Comuns
- "Todo DDL do PostgreSQL é transacional, então é sempre seguro encapsular em BEGIN/COMMIT." Um pequeno número de operações, incluindo
CREATE INDEX CONCURRENTLYeVACUUM, não pode ser executado dentro de um bloco de transação e deve ser emitido por conta própria. - "Um comando ALTER TABLE de execução rápida não pode causar uma interrupção." O tempo de execução da própria instrução não é o risco; o tempo que ela gasta na fila esperando por um bloqueio, e tudo o que entra na fila atrás dela, é.
- "Adicionar uma restrição CHECK sempre requer uma varredura de validação longa e bloqueante."
ADD CONSTRAINT ... NOT VALIDseguido por umVALIDATE CONSTRAINTseparado divide isso em um bloqueio breve mais uma passagem de validação não bloqueante. - "Uma ferramenta de migração como Flyway torna as migrações seguras." Ela garante ordenação e idempotência entre ambientes, não que qualquer instrução individual evite um bloqueio perigoso.
- "Expandir/contrair só é necessário para tabelas enormes." É realmente sobre tráfego ao vivo, não tamanho; mesmo uma tabela pequena e com pouca carga pode causar uma interrupção se um rename que quebra compatibilidade chegar antes que todas as instâncias da aplicação tenham sido reimplantadas.
FAQs
O DDL no PostgreSQL é transacional?
A maior parte é, o que significa que uma transação com falha reverte as alterações de schema de forma limpa, mas um pequeno conjunto de operações - CREATE INDEX CONCURRENTLY, VACUUM entre elas - não pode ser executado dentro de um bloco de transação.
Por que uma instrução ALTER TABLE rápida às vezes causa uma interrupção na produção?
Porque o tempo de execução da própria instrução raramente é o problema; o tempo que ela gasta na fila esperando para adquirir seu bloqueio, e todas as outras consultas que entram na fila atrás dela, é o que transforma milissegundos em minutos.
O que NOT VALID realmente muda ao adicionar uma restrição?
Ele adiciona a restrição imediatamente sob um bloqueio breve sem escanear as linhas existentes, adiando a validação de tabela completa para uma etapa separada VALIDATE CONSTRAINT que adquire um bloqueio muito mais leve.
O que é expandir/contrair, em termos simples?
É dividir uma mudança de schema arriscada em uma etapa aditiva (expandir), um período de migração/corte e uma etapa de limpeza (contrair), de modo que nenhum deploy único adicione e remova algo do qual as aplicações dependem.
Por que não posso simplesmente renomear uma coluna diretamente em uma migração?
Porque um rename direto quebra qualquer instância de aplicação já implantada que ainda usa o nome antigo no momento em que é confirmado, sem uma janela para um rollout gradual.
Uma ferramenta de migração como Flyway ou Liquibase torna minhas migrações seguras em relação a bloqueios?
Não - ela garante que suas migrações sejam executadas na ordem correta exatamente uma vez entre os ambientes, mas não tem conhecimento se o comportamento de bloqueio de uma instrução individual é seguro para uma tabela ativa.
Por que os scripts de migração devem definir lock_timeout?
Sem isso, uma instrução esperando por um bloqueio pode esperar indefinidamente e silenciosamente enfileirar todas as outras consultas atrás dela; lock_timeout transforma isso em uma falha rápida e visível.
CREATE INDEX CONCURRENTLY é sempre a escolha certa?
É a escolha certa sempre que a tabela recebe escritas ativas, pois evita bloqueá-las, mas é mais lento e não pode ser executado dentro de um bloco de transação, portanto, é um overhead desnecessário em tabelas que não estão sob carga de escrita ativa.
Como o preenchimento em lotes (batched backfilling) se relaciona com migrações de schema?
Um preenchimento que atualiza cada linha em uma única instrução adquire um bloqueio de longa duração semelhante a uma mudança de schema, e é por isso que grandes preenchimentos são tipicamente divididos em lotes pequenos e pausáveis em vez de um único UPDATE gigante.
Qual é a diferença real entre força de bloqueio e tempo de espera de bloqueio?
A força do bloqueio determina o que outra atividade o bloqueio impede uma vez adquirido; o tempo de espera determina quanto tempo a instrução fica na fila antes de adquiri-lo - um bloqueio forte mantido brevemente pode ser mais seguro do que um bloqueio fraco esperando atrás de uma transação longa.
Quando uma mudança DDL em transação única ainda é a decisão correta?
Para tabelas pequenas ou de baixo tráfego, ou durante uma janela de manutenção genuína, onde a simplicidade de uma mudança direta supera o custo de coordenação de um rollout completo de expandir/contrair.
Por que a estratégia de rollback e a segurança de mudança de schema são discutidas juntas?
Porque uma migração que é segura para frente não é automaticamente segura para reverter - equipes que só pensam em avançar tratam um deploy com falha como uma nova migração para frente em vez de assumir que um rollback simétrico existe.
Relacionados
- Fundamentos de Migrações - SQL versionado e rastreamento de histórico de schema
- Padrão Expandir/Contrair - o fluxo de trabalho completo de várias releases
- DDL de Zero Downtime -
CONCURRENTLY,NOT VALIDe cutovers faseados em detalhes - Migrações de Tabelas Grandes - preenchimentos em lotes e
pg_repack - Regras de Segurança de Migração - convenções de timeout de bloqueio e instrução
Versões do Stack: Esta página é conceitual e descreve o comportamento de bloqueio DDL e transacional consistente em versões principais atuais do PostgreSQL, incluindo PostgreSQL 18.4 (linha estável 18, linha de manutenção 17).