Pontos-chave do MVCC
Multi-Version Concurrency Control (Controle de Concorrência Multi-Versão), ou MVCC, é o mecanismo que o PostgreSQL usa para permitir que muitas transações leiam e escrevam os mesmos dados ao mesmo tempo sem interferir umas nas outras.
Em vez de bloquear uma linha para cada leitor, como faziam designs de banco de dados mais antigos, o PostgreSQL mantém várias versões de uma linha e permite que cada transação veja a versão que estava correta no momento do seu próprio snapshot.
Esta página constrói o modelo mental por trás desse sistema antes de você chegar às páginas mais aplicadas nesta seção sobre níveis de isolamento, bloqueios em nível de linha e transações longas.
Entender o MVCC muda a forma como você raciocina sobre bugs de concorrência, pois a maior parte do comportamento surpreendente que você encontrará em produção remonta ao que o snapshot de uma transação podia e não podia ver.
Resumo
- O PostgreSQL nunca sobrescreve uma linha no local; ele escreve uma nova versão e deixa a antiga até que nada mais precise dela.
- Por que Importa: Esse design permite que leituras e escritas ocorram concorrentemente sem que leitores bloqueiem escritores ou escritores bloqueiem leitores.
- Conceitos-chave: snapshot, versão de tupla, xmin/xmax, visibilidade, tupla morta, vacuum.
- Quando Usar: Este modelo está sempre ativo, mas importa mais ao raciocinar sobre transações de longa duração, bloat e escolhas de nível de isolamento.
- Limitações / Trade-offs: Versões de linha antigas se acumulam como bloat até que o vacuum as recupere, e uma transação de longa duração pode atrasar essa recuperação indefinidamente.
- Tópicos Relacionados: noções básicas de transações, níveis de isolamento, bloqueios em nível de linha, transações longas, anomalias de serialização.
Fundamentos
Cada linha em uma tabela PostgreSQL carrega duas colunas ocultas do sistema, xmin e xmax, que registram qual transação criou a versão da linha e qual transação, se houver, a excluiu ou substituiu.
Quando você executa um UPDATE, o PostgreSQL não modifica a linha existente no local; ele insere uma nova versão de linha com um xmin fresco e marca o xmax da versão antiga com o ID da transação de atualização.
Um DELETE funciona da mesma forma: a linha não é removida fisicamente imediatamente; ela é apenas marcada com um xmax para que outras transações saibam que ela não deve mais ser considerada atual.
Isso significa que uma única linha lógica pode ter muitas versões de tupla físicas empilhadas na tabela ao mesmo tempo, e o trabalho do PostgreSQL no momento da leitura é descobrir qual versão é a correta para a transação que a solicita.
Essa decisão é tomada usando um snapshot, um registro de quais transações já haviam sido confirmadas, estavam em andamento ou ainda não haviam sido iniciadas no momento em que o snapshot foi tirado.
Uma versão de linha é visível para uma transação apenas se sua transação criadora tiver sido confirmada antes que o snapshot fosse tirado e sua transação de exclusão, se houver, não o tenha sido.
Pense nisso como um documento compartilhado com histórico de versão completo: cada edição cria uma nova revisão em vez de apagar a antiga, e cada leitor recebe a revisão específica que corresponde ao momento em que abriu o documento.
Você pode observar isso diretamente olhando as colunas ocultas em uma linha.
SELECT xmin, xmax, ctid FROM app.accounts WHERE id = 1;O ctid mostrado ao lado delas é a localização física da versão da linha, que muda toda vez que uma atualização cria uma nova versão em um local diferente.
Mecânicas e Interações
O ganho prático desse design é a garantia de concorrência principal do PostgreSQL: leitores nunca bloqueiam escritores, e escritores nunca bloqueiam leitores.
Um SELECT de longa duração simplesmente continua usando seu próprio snapshot e nunca precisa esperar por um UPDATE concorrente para terminar, pois essa atualização está criando uma nova versão em vez de modificar a que o leitor já possui.
Escritores ainda podem bloquear outros escritores, já que duas transações tentando atualizar a mesma linha devem serializar através de um bloqueio em nível de linha, mas esse é um caso muito mais restrito do que bloquear todos os leitores também.
É aqui que os níveis de isolamento entram em cena, e vale a pena ser preciso sobre o que eles realmente controlam.
Um nível de isolamento não muda como o bloqueio funciona na camada de armazenamento; ele muda como o snapshot de uma transação se comporta e quais anomalias de concorrência ele tolera.
Read Committed, o padrão do PostgreSQL, tira um novo snapshot para cada instrução dentro de uma transação, então cada instrução vê os dados confirmados mais recentes no momento em que é executada.
Repeatable Read tira um único snapshot para toda a transação, então cada instrução dentro dela vê a mesma visão consistente, mesmo que outras transações confirmem alterações entre elas.
Serializable vai além, detectando e rejeitando o subconjunto de padrões de execução concorrente que não poderiam ter acontecido se as transações tivessem sido executadas uma de cada vez, mesmo quando o snapshot do Repeatable Read sozinho não detecta o conflito.
O custo da abordagem de escrever-uma-nova-versão do MVCC é que as versões antigas não desaparecem sozinhas.
Uma tupla morta, uma versão de linha que nenhuma transação pode mais ver, fica na tabela ocupando espaço até que o vacuum a recupere para reutilização.
SELECT n_dead_tup FROM pg_stat_user_tables WHERE relname = 'accounts';O Autovacuum executa essa limpeza automaticamente em segundo plano, mas uma transação que permanece aberta por muito tempo pode impedir o vacuum de recuperar versões que estão tecnicamente mortas, pois o snapshot dessa transação antiga pode ainda precisar delas.
Considerações Avançadas e Aplicações
O modo de falha de MVCC mais consequente em produção é a sessão idle-in-transaction (ociosa em transação), uma conexão que abriu uma transação, tirou um snapshot e depois ficou ociosa sem confirmar ou reverter.
Esse snapshot aberto retém o horizonte de limpeza do autovacuum para todas as tabelas do banco de dados, não apenas para as que a transação tocou, o que significa que tuplas mortas se acumulam em todos os lugares até que essa única conexão seja finalmente fechada.
idle_in_transaction_session_timeout existe especificamente para limitar esse risco, fechando forçadamente conexões que excedem o tempo permitido.
O bloat (inchaço), o espaço acumulado consumido por tuplas mortas que o vacuum ainda não recuperou, tem consequências reais de desempenho além do uso de disco, pois varreduras e buscas em índices precisam pular versões mortas para encontrar as vivas.
O MVCC também interage diretamente com o esquema e as decisões de bloqueio abordadas em outras partes desta documentação: um padrão de exclusão lógica (soft delete) usando uma coluna deleted_at produz sua própria forma de bloat, a menos que seja combinado com arquivamento periódico, e um SELECT ... FOR UPDATE explicitamente adquire um bloqueio em nível de linha sobre os mecanismos de snapshot do MVCC para evitar uma classe específica de corrida de escrita-escrita que um snapshot sozinho não consegue resolver.
A escolha de um nível de isolamento é, em última análise, um trade-off entre tolerância a anomalias e complexidade da aplicação, e diferentes níveis são genuinamente mais adequados para diferentes cargas de trabalho.
| Nível de Isolamento | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
| Read Committed | Baixo overhead, frescor intuitivo por instrução, padrão do PostgreSQL | Vulnerável a leituras não repetíveis dentro de uma transação | A maioria das transações OLTP do dia a dia com lógica curta e simples |
| Repeatable Read | Visão consistente em toda a transação, imune a leituras não repetíveis | Pode atingir falhas de serialização em conflitos de escrita-escrita que precisam ser retentados | Relatórios multi-instrução ou lógica que precisa ver um snapshot coerente |
| Serializable | Garantia mais forte, comporta-se como se as transações tivessem sido executadas uma de cada vez | Maior chance de conflitos que exigem retentativa, algum custo de throughput | Lógica crítica financeira ou de invariantes onde qualquer anomalia é inaceitável |
Consultas analíticas de longa duração e escritas OLTP competindo pelas mesmas tabelas é uma tensão comum que o MVCC ajuda, mas não elimina completamente, pois um relatório longo sob Repeatable Read ainda mantém seu snapshot aberto durante toda a duração do relatório, com a mesma consequência de bloqueio de vacuum de uma sessão idle-in-transaction.
Monitorar pg_stat_activity para transações em estado active ou idle in transaction com um xact_start antigo é o hábito operacional padrão para capturar essa classe de problema antes que se torne um incidente de bloat.
Equívocos Comuns
- "Um UPDATE modifica a linha no local." O PostgreSQL escreve uma nova versão de tupla e marca a antiga como morta; ele nunca reescreve bytes dentro de uma versão de linha existente.
- "Um SELECT pode bloquear um UPDATE concorrente, ou vice-versa." O MVCC é projetado especificamente para que leitores e escritores nunca se bloqueiem mutuamente; apenas escritores competindo pela mesma linha se bloqueiam.
- "Níveis de isolamento mais altos significam mais bloqueios." O nível de isolamento altera o comportamento do snapshot e a detecção de anomalias; ele não altera fundamentalmente como os bloqueios em nível de linha são adquiridos.
- "Linhas excluídas são removidas imediatamente." Um DELETE apenas marca uma versão de linha como não mais atual; o espaço físico não é recuperado até que o vacuum seja executado e nenhuma transação ainda precise dele.
- "Vacuum é uma limpeza opcional que posso adiar indefinidamente." Adiar o vacuum permite que tuplas mortas se acumulem em bloat que degrada o desempenho de varredura e índice e, em casos extremos, arrisca o estouro do ID da transação (transaction ID wraparound).
FAQs
O que MVCC realmente significa e qual problema ele resolve?
Multi-Version Concurrency Control (Controle de Concorrência Multi-Versão) permite que o PostgreSQL dê a cada transação uma visão consistente dos dados sem fazer leitores e escritores esperarem uns pelos outros, mantendo várias versões de cada linha em vez de uma cópia compartilhada e bloqueável.
O que são xmin e xmax?
xminé o ID da transação que criou uma determinada versão de linha.xmaxé o ID da transação que a excluiu ou substituiu, se houver.- O PostgreSQL usa ambos, comparados com o snapshot de uma transação, para decidir se uma versão é visível.
Por que um SELECT de longa duração não bloqueia outras transações de atualizar as mesmas linhas?
O SELECT está lendo de seu próprio snapshot de versões já confirmadas, e um UPDATE cria uma nova versão em vez de alterar a que o SELECT já possui, então os dois nunca competem pelos mesmos dados físicos.
O que é um snapshot em termos de MVCC?
Um snapshot é o registro privado de uma transação sobre quais outras transações foram consideradas confirmadas no momento em que começou a ler, e é o que determina quais versões de linha essa transação tem permissão para ver.
O nível de isolamento muda como os bloqueios funcionam?
Não, o nível de isolamento muda o que o snapshot de uma transação vê e quais padrões de execução concorrente são rejeitados como anomalias, enquanto o comportamento de bloqueio em nível de linha para escritores conflitantes permanece em grande parte o mesmo entre os níveis.
Por que minha tabela continua crescendo mesmo que eu esteja excluindo linhas antigas?
- Linhas excluídas e substituídas por atualizações se tornam tuplas mortas, não espaço liberado, no momento da escrita.
- O Autovacuum recupera esse espaço em segundo plano, mas ele precisa ser executado e concluído para que isso aconteça.
- Uma transação de longa duração em outro lugar no banco de dados pode atrasar essa recuperação, mesmo em tabelas que ela nunca tocou.
O que é perigoso em uma sessão idle-in-transaction?
O snapshot de uma transação aberta retém o ponto até o qual o autovacuum pode recuperar com segurança tuplas mortas em todo o banco de dados, então uma transação aberta esquecida pode causar bloat em todos os lugares, não apenas nas tabelas que consultou.
Qual é a diferença prática entre Read Committed e Repeatable Read?
Read Committed tira um novo snapshot para cada instrução, então dois SELECTs na mesma transação podem ver dados diferentes se algo for confirmado entre eles, enquanto Repeatable Read tira um snapshot para toda a transação, então cada instrução vê a mesma visão consistente.
Quando devo usar Serializable em vez de Repeatable Read?
Serializable vale a pena o overhead extra de retentativa quando a lógica impõe um invariante que um snapshot sozinho não pode proteger, como garantir que uma soma entre linhas nunca exceda um limite sob escritas concorrentes.
Uma versão de linha pode ser visível para uma transação e não para outra no mesmo instante?
Sim, esse é o objetivo do MVCC: o snapshot de cada transação determina independentemente a visibilidade, então duas transações executando no mesmo momento podem legitimamente ver versões diferentes da mesma linha lógica.
Como verifico se o bloat do MVCC está se tornando um problema?
pg_stat_user_tables expõe n_dead_tup por tabela, e um número em constante crescimento em relação às linhas vivas, combinado com o autovacuum não acompanhando o ritmo, é o sinal inicial padrão que vale a pena investigar.
O MVCC se aplica da mesma forma em uma réplica de leitura?
Uma réplica aplica o mesmo modelo de versão e visibilidade ao log de escrita antecipada (write-ahead log) que transmite do primário, mas uma consulta de longa duração em uma réplica pode entrar em conflito com mudanças recebidas e retransmitidas de maneiras que são tratadas de forma diferente dos conflitos de MVCC no lado do primário.
Relacionados
- Noções Básicas de Transações - BEGIN, COMMIT, ROLLBACK e limites de transação
- Visibilidade MVCC - exemplos práticos lendo xmin, xmax e estatísticas de tuplas mortas
- Níveis de Isolamento - Read Committed, Repeatable Read e Serializable em profundidade
- Bloqueios em Nível de Linha - bloqueio explícito sobre os mecanismos de snapshot do MVCC
- Transações Longas - o risco operacional de manter um snapshot aberto por muito tempo
- Anomalias de Serialização - os bugs de concorrência que os níveis de isolamento são projetados para prevenir
Versões da Pilha: Esta página foi escrita para PostgreSQL 18.4 (versão principal estável 18, linha de manutenção 17 também suportada).