Um snapshot longo do PostgreSQL pode manter versões intermediárias de linhas que nunca lê. O vacuum usa um horizonte conservador de transações para decidir o que pode ser removido. Este guia explica o motivo e, em seguida, reproduz o comportamento com linhas sintéticas de artigos e node-postgres em um Mac.
A tarefa é distinguir um snapshot que retarda a limpeza de um vacuum que ainda não foi executado. O exemplo tem como alvo o PostgreSQL 18 e usa duas conexões de banco de dados: uma leitora que mantém um snapshot Repeatable Read e uma escritora que atualiza um único artigo. A documentação do PostgreSQL atualmente identifica a versão 18 como a versão principal com suporte mais recente; os detalhes de implementação abaixo estão explicitamente vinculados ao ramo 18. Documentação do PostgreSQL 18.
Por que manter versões que o leitor antigo não consegue ver?
Uma atualização cria uma nova versão da linha. O PostgreSQL mantém a versão anterior enquanto outra transação ainda puder precisar dela. Com o tempo, o vacuum pode recuperar versões obsoletas e tornar seu espaço reutilizável. Esse é o ciclo de vida básico de MVCC descrito em Vacuum de rotina.
A sutileza é que a visibilidade do snapshot e a possibilidade de remoção física respondem a perguntas diferentes. A visibilidade pergunta se uma consulta específica deve retornar uma versão. A limpeza usa um limite conservador, em vez de provar, para cada versão, exatamente quais snapshots existentes poderiam lê-la.
No PostgreSQL 18, HeapTupleSatisfiesVacuum compara a transação que tornou uma versão obsoleta com OldestXmin. Uma exclusão ou substituição confirmada não torna imediatamente removível a versão anterior: versões cuja transação de exclusão está no limite ou depois dele continuam classificadas como recentemente mortas. Essa regra está documentada diretamente na implementação oficial de visibilidade de tuplas.
Considere esta sequência ilustrativa. Cada atualização é confirmada separadamente, e não há outros leitores envolvidos:
| Etapa | Ação | O leitor antigo vê | Última versão confirmada |
|---|---|---|---|
| 1 | Inserir revisão 0 do artigo | — | Revisão 0 |
| 2 | O leitor estabelece um snapshot Repeatable Read | Revisão 0 | Revisão 0 |
| 3 | O escritor confirma a revisão 1 | Revisão 0 | Revisão 1 |
| 4 | O escritor confirma a revisão 2 | Revisão 0 | Revisão 2 |
| 5 | O escritor confirma a revisão 3 | Revisão 0 | Revisão 3 |
A revisão 0 é diretamente necessária para o leitor. As revisões 1 e 2 são versões intermediárias: este leitor não consegue vê-las porque foram criadas depois do snapshot. Ainda assim, as transações que as substituíram também são mais recentes que o limite de limpeza mantido. A aplicação da regra documentada do limite explica por que essas versões podem permanecer junto com a revisão 0. Sua retenção não significa que o leitor possa percorrê-las como resultados históricos de consulta. Implementação de visibilidade de tuplas do PostgreSQL 18.
O horizonte é mais amplo do que a linha selecionada pelo leitor. O PostgreSQL 18 calcula horizontes apropriados para cada relação a partir dos IDs de transação dos processos de backend e dos horizontes dos snapshots; dados comuns de tabelas geralmente levam em conta sessões no mesmo banco de dados. Ele não rastreia apenas as linhas retornadas por uma SELECT específica. Isso explica por que uma consulta pequena pode afetar a limpeza em outras partes desse banco de dados. Relações compartilhadas e outros tipos de relação têm regras distintas, portanto “um snapshot interrompe todo vacuum em toda parte” é uma afirmação ampla demais. Implementação de horizontes do PostgreSQL 18.
Qual transação realmente mantém um snapshot longo?
O Repeatable Read estabelece seu snapshot na primeira instrução que não seja de controle de transação e, em seguida, usa esse snapshot nas consultas seguintes. Apenas emitir BEGIN não basta para esta demonstração; a consulta inicial do artigo estabelece a visão que precisa ser mantida. O Read Committed, nível de isolamento padrão do PostgreSQL, obtém um novo snapshot para cada comando. Isolamento de transações.
Consequentemente, uma transação Repeatable Read ociosa que consultou a tabela pode manter seu snapshot depois que a consulta termina. Uma instrução Read Committed de longa duração também pode manter um snapshot enquanto está em execução, mas instruções separadas já concluídas nesse nível não compartilham a mesma visão fixa. Portanto, a duração da transação, por si só, não determina qual snapshot está sendo mantido. Isolamento de transações.
Uma transação somente leitura pode contribuir para o horizonte sem ter um ID de transação de nível superior próprio. A implementação considera tanto os IDs de transação dos processos de backend quanto os horizontes dos snapshots. Neste exemplo, um backend_xid nulo junto com um backend_xmin não nulo é, portanto, compatível com um leitor que mantém um snapshot. Implementação de horizontes do PostgreSQL 18.
Reproduza o comportamento no Terminal do Mac
Você precisa de um servidor PostgreSQL 18 local em execução, das ferramentas de linha de comando dele e do Node.js com npm. A documentação de compatibilidade do node-postgres lista o Node.js 20 ou 24. Este guia pressupõe que as configurações da sua conexão local já funcionam. Ele cria um banco de dados separado e usa apenas texto sintético de artigos. Introdução e instalação do node-postgres.
No Terminal, crie um diretório de trabalho, instale o driver e crie o banco de dados de exemplo:
mkdir article-snapshot-lab
cd article-snapshot-lab
npm init -y
npm install pg
createdb snapshot_lab
Salve o programa completo abaixo como snapshot-demo.cjs. As conexões herdam as configurações de ambiente do PostgreSQL, incluindo PGDATABASE, PGHOST e PGPORT, conforme documentado em Conexão do node-postgres. Use as mesmas configurações de conexão local para createdb e para o programa.
O leitor mantém todas as instruções da transação em um único cliente dedicado. O node-postgres exige isso: uma transação deve usar o mesmo cliente do início ao fim, então encaminhar suas instruções por pool.query() não é adequado. O segundo cliente executa as atualizações e os comandos de vacuum de forma independente. Transações no node-postgres.
O exemplo também indexa revision, que muda em cada atualização. Isso impede deliberadamente que essas atualizações usem HOT, tornando a demonstração mais fácil de interpretar. O PostgreSQL documenta que HOT exige que as atualizações não alterem colunas referenciadas por índices comuns, além de exigir espaço suficiente na página original. Tuplas somente de heap.
const { Client } = require('pg');
async function main() {
const reader = new Client({
application_name: 'article_snapshot_reader',
connectionTimeoutMillis: 5000
});
const writer = new Client({
application_name: 'article_snapshot_writer',
connectionTimeoutMillis: 5000
});
let readerOpen = false;
writer.on('notice', notice => console.log(notice.message));
try {
await reader.connect();
await writer.connect();
const version = await writer.query('SHOW server_version_num');
const major = Math.floor(Number(version.rows[0].server_version_num) / 10000);
if (major !== 18) throw new Error('This walkthrough targets PostgreSQL 18.');
await writer.query(
'CREATE TABLE public.article_snapshot_demo (' +
'id integer PRIMARY KEY, revision integer NOT NULL, title text NOT NULL)'
);
await writer.query(
'CREATE INDEX article_snapshot_demo_revision_idx ' +
'ON public.article_snapshot_demo (revision)'
);
await writer.query(
'INSERT INTO public.article_snapshot_demo VALUES (1, 0, $1)',
['Article draft']
);
await reader.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
readerOpen = true;
const selectRow = 'SELECT id, revision, title FROM public.article_snapshot_demo';
console.log('Reader before:', (await reader.query(selectRow)).rows);
for (const revision of [1, 2, 3]) {
await writer.query(
'UPDATE public.article_snapshot_demo SET revision = $1, title = $2 WHERE id = 1',
[revision, 'Article revision ' + revision]
);
}
const pid = (await reader.query('SELECT pg_backend_pid() AS pid')).rows[0].pid;
console.log('Reader activity:', (await writer.query(
'SELECT state, backend_xid, backend_xmin FROM pg_stat_activity WHERE pid = $1',
[pid]
)).rows);
console.log('Writer latest:', (await writer.query(selectRow)).rows);
console.log('Reader still:', (await reader.query(selectRow)).rows);
console.log('Vacuum with snapshot open:');
await writer.query('VACUUM (VERBOSE) public.article_snapshot_demo');
await reader.query('COMMIT');
readerOpen = false;
console.log('Vacuum after reader commit:');
await writer.query('VACUUM (VERBOSE) public.article_snapshot_demo');
console.log('Reader fresh:', (await reader.query(selectRow)).rows);
} finally {
if (readerOpen) await reader.query('ROLLBACK').catch(console.error);
await Promise.allSettled([reader.end(), writer.end()]);
}
}
main().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
Execute-o especificando o banco de dados explicitamente:
PGDATABASE=snapshot_lab node snapshot-demo.cjs
A sintaxe JavaScript do programa foi verificada com o Node.js. A sequência do banco de dados foi conferida com base na documentação consultada; ela não foi executada em um servidor PostgreSQL para este artigo.
Como interpretar as duas passagens de vacuum
Os resultados esperados das consultas são a revisão 0 para Reader before, a revisão 3 para Writer latest e novamente a revisão 0 para Reader still. Depois que o leitor confirmar a transação, sua próxima consulta independente deverá retornar a revisão 3. Esses são os resultados esperados da sequência sintética, não resultados de testes capturados.
O resultado da atividade deve identificar o leitor como idle in transaction enquanto o escritor o inspeciona, com um backend_xmin retido. O PostgreSQL define esse estado como uma transação aguardando outro comando, e backend_xmin como o horizonte atual do processo de backend. O sistema de estatísticas cumulativas.
Leia o primeiro relatório de vacuum para ver as versões obsoletas que continuam não removíveis. Depois que o leitor confirmar a transação, compare-o com o segundo relatório para verificar a remoção dessas versões. Em um banco de dados sem outras atividades, as três substituições confirmadas fornecem três versões obsoletas para examinar. Outros horizontes retidos ou atividades de manutenção simultâneas podem afetar o relatório exato, então use o padrão antes e depois, em vez de exigir texto idêntico na saída.
VACUUM (VERBOSE) produz um relatório detalhado da atividade. O programa imprime as notificações do servidor por meio do evento notice documentado pelo node-postgres. As duas chamadas de vacuum são executadas fora de blocos de transação explícitos porque o PostgreSQL não permite VACUUM dentro de um deles. VACUUM do PostgreSQL, API Client do node-postgres.
Use este guia de decisão quando o resultado diferir do esperado:
| Observação | Próxima verificação |
|---|---|
| O leitor vê a revisão 3 antes de confirmar a transação | Confirme que o Repeatable Read foi iniciado e que a primeira consulta ao artigo foi executada antes das atualizações. |
| O primeiro vacuum mantém versões obsoletas | Compare-o com a passagem após a confirmação da transação pelo leitor; é esperado que elas sejam mantidas enquanto o snapshot estiver aberto. |
| As versões permanecem após a confirmação da transação pelo leitor | Verifique se há outro horizonte retido; encerrar este leitor remove apenas sua contribuição. |
| Uma contagem nas estatísticas da tabela parece inconsistente | Trate n_dead_tup como uma estimativa e compare os relatórios de vacuum. |
| O arquivo da tabela não fica menor | Verifique se o espaço se tornou reutilizável; a redução do tamanho do arquivo é um resultado distinto. |
O PostgreSQL descreve explicitamente n_dead_tup como uma contagem estimada. Um horário de vacuum registrado também confirma que a manutenção foi executada, sem demonstrar que todas as versões obsoletas podiam ser removidas. Esses campos ajudam no diagnóstico, mas nenhum deles substitui o exame do snapshot retido e do relatório de vacuum. O sistema de estatísticas cumulativas.
O vacuum simples normalmente torna o espaço reutilizável dentro da tabela, em vez de devolvê-lo ao macOS. Páginas finais totalmente livres são uma exceção documentada. Portanto, um tamanho de arquivo estável após a limpeza não demonstra que ela falhou. Vacuum de rotina.
O que deve mudar na aplicação?
Se várias consultas a artigos precisarem compartilhar uma visão consistente do banco de dados, mantenha a transação limitada a essas consultas e encerre-a quando terminarem. Como consequência, no projeto da aplicação, da retenção de snapshots, faça o processamento não relacionado e a interação com a área de trabalho depois que essa transação terminar, sempre que a tarefa permitir.
Se cada consulta puder usar independentemente a visão confirmada mais recente, o Read Committed fornece snapshots no nível de comando. Alterar o isolamento ou confirmar a transação entre lotes muda a semântica de consistência: consultas posteriores podem ver revisões de artigos recém-confirmadas. Escolha essa abordagem somente quando esses resultados variáveis forem adequados à tarefa do leitor. Isolamento de transações.
Aumentar a frequência do vacuum não faz uma versão ultrapassar um limite de removibilidade que não mudou. Encerrar o snapshot desnecessário elimina essa contribuição para a retenção. Da mesma forma, VACUUM FULL reescreve a tabela e exige um bloqueio exclusivo, além de espaço adicional; não é a resposta rotineira para este exemplo. Implementação de visibilidade de tuplas do PostgreSQL 18, VACUUM do PostgreSQL.
O script deixa a tabela sintética disponível para inspeção. Ele falha deliberadamente se essa tabela já existir, em vez de substituir o conteúdo existente. Depois de inspecioná-la, execute o comando a seguir em uma sessão de consulta conectada a snapshot_lab se quiser remover a tabela de exemplo. Se uma execução falhar durante a configuração, inspecione a mesma tabela antes de removê-la e executar novamente.
DROP TABLE public.article_snapshot_demo;
O diagnóstico útil é a transição: estabelecer um snapshot antigo, confirmar várias substituições, executar o vacuum, encerrar o snapshot e executar o vacuum novamente. Ver a revisão original continuar legível enquanto versões intermediárias também resistem à limpeza torna a distinção concreta. As regras de visibilidade do PostgreSQL determinam o que a consulta retorna; seu horizonte de limpeza determina quando o espaço obsoleto pode ser reutilizado.
Fontes
13.2. Isolamento de transações — PostgreSQL Global Development Group.
24.1. Vacuum de rotina — PostgreSQL Global Development Group.
VACUUM — PostgreSQL Global Development Group.
27.2. O sistema de estatísticas cumulativas — PostgreSQL Global Development Group.
66.7. Tuplas somente de heap (HOT) — PostgreSQL Global Development Group.
heapam_visibility.c, REL_18_STABLE — Projeto PostgreSQL.
procarray.c, REL_18_STABLE — Projeto PostgreSQL.
Introdução — node-postgres.
Conexão — node-postgres.
Transações — node-postgres.
API Client — node-postgres.