// SISTEMA_ONLINE

O PostgreSQL é mais esperto que você: Como o Serializable Snapshot Isolation previne anomalias

DATE:  2026.07.30 LATENCY:  STATUS:  ONLINE

Se você já trabalhou com bancos de dados relacionais, provavelmente conhece os níveis de isolamento de transações. O PostgreSQL, como um bom cidadão SQL, oferece os quatro níveis padrão: READ UNCOMMITTED (que na prática é READ COMMITTED), READ COMMITTED, REPEATABLE READ e SERIALIZABLE. A maioria dos desenvolvedores se contenta com o READ COMMITTED, o padrão, e raramente se aventura nos níveis mais altos. Mas é exatamente no SERIALIZABLE que o PostgreSQL mostra uma inteligência que poucos bancos de dados open-source possuem: ele não usa locks tradicionais, mas sim um algoritmo sofisticado chamado Serializable Snapshot Isolation (SSI), que monitora dependências entre transações e pode abortar uma delas antes que uma inconsistência lógica aconteça. Neste tutorial, vamos construir uma prova de conceito (PoC) com Docker e ver esse mecanismo em ação.

Créditos: Este conteúdo foi desenvolvido com base na documentação oficial do PostgreSQL 18.3 (Capítulo 13, seções 13.2.2 e 13.2.3) e com o auxílio de ferramentas de IA para estruturação e exemplos. Toda a interpretação dos conceitos, porém, é fruto do estudo direto do manual.


1. O problema: quando duas transações "parecem" corretas, mas juntas geram inconsistência

Considere a seguinte tabela, que guarda valores numéricos por uma chave class:

CREATE TABLE mytab (
    class INT,
    value  INT
);

INSERT INTO mytab VALUES (1, 10), (1, 20), (2, 100), (2, 200);

Agora imagine que duas transações ocorrem concorrentemente:

  • Transação A: soma todos os value da classe 1 (resultado = 30) e insere uma nova linha na classe 2 com esse valor.
  • Transação B: soma todos os value da classe 2 (resultado = 300) e insere uma nova linha na classe 1 com esse valor.

Em uma execução serial (uma após a outra), os resultados seriam:

  • Se A executa primeiro: A insere (2, 30); depois B soma classe 2 (agora com 100, 200 e 30 = 330) e insere (1, 330).
  • Se B executa primeiro: B insere (1, 300); depois A soma classe 1 (10+20+300 = 330) e insere (2, 330).

Ou seja, o valor final esperado depende da ordem, mas em ambos os casos, a soma final é consistente com uma ordem serial.

Agora, o que acontece se A e B rodarem ao mesmo tempo, cada uma com sua própria visão do banco, sem saber da outra? Se ambas usarem REPEATABLE READ, elas verão o snapshot do início da transação. A calcula sua soma baseada nos valores originais da classe 1 (30) e insere (2, 30). B calcula sua soma baseada nos valores originais da classe 2 (300) e insere (1, 300). No final, teremos as duas novas linhas: (1, 300) e (2, 30). O estado final do banco é:

classvalue
110
120
1300
2100
2200
230

Esse estado não é consistente com nenhuma ordem serial: se A tivesse rodado primeiro, a soma da classe 2 por B seria 330 (e não 300); se B tivesse rodado primeiro, a soma da classe 1 por A seria 330 (e não 30). Ou seja, o resultado é fruto de uma anomalia de serialização, o banco produziu um estado que nenhuma execução sequencial poderia gerar.


2. A solução: Serializable Snapshot Isolation (SSI)

O PostgreSQL oferece o nível de isolamento SERIALIZABLE, que não usa bloqueios pesados para evitar esse tipo de anomalia. Em vez disso, ele usa um mecanismo chamado Serializable Snapshot Isolation (SSI), que monitora dependências de leitura/escrita entre transações. Quando o sistema detecta um padrão de dependências que poderia levar a uma anomalia de serialização (como no exemplo acima), ele aborta uma das transações com o erro:

ERROR: could not serialize access due to read/write dependencies among transactions

Isso força o desenvolvedor a tratar esse erro e reexecutar a transação, garantindo que o estado final seja sempre serializável. O PostgreSQL não usa locks para isso, ele usa predicate locks, que marcam quais dados foram lidos, para depois verificar se alguma escrita posterior invalidaria a consistência.


3. Mãos à obra: ambiente com Docker

Vamos criar um ambiente isolado com Docker para reproduzir o exemplo. Se você não tiver o Docker instalado, pode usar uma instância local do PostgreSQL; os comandos são os mesmos.

Primeiro, crie um container PostgreSQL:

docker run --name pg-serial -e POSTGRES_PASSWORD=senha -d -p 5432:5432 postgres:18

Aguarde alguns segundos e conecte-se ao banco:

docker exec -it pg-serial psql -U postgres

Agora, crie a tabela e insira os dados iniciais:

CREATE TABLE mytab (class INT, value INT);
INSERT INTO mytab VALUES (1, 10), (1, 20), (2, 100), (2, 200);

Vamos abrir duas sessões no banco. Você pode fazer isso abrindo dois terminais e executando docker exec -it pg-serial psql -U postgres em cada um, ou usando o recurso de múltiplas abas do seu cliente SQL (como o DBeaver). Vamos chamar de Sessão A e Sessão B.


3.1. Comportamento com REPEATABLE READ (ambas cometem)

Primeiro, configure ambas as sessões para usar REPEATABLE READ:

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

Agora, execute as seguintes operações simultaneamente:

Sessão A:

BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 1;  -- resultado: 30
-- (não commita ainda)

Sessão B:

BEGIN;
SELECT SUM(value) FROM mytab WHERE class = 2;  -- resultado: 300
-- (não commita ainda)

Depois que ambas fizerem a leitura, execute os INSERTs:

Sessão A:

INSERT INTO mytab VALUES (2, 30);
COMMIT;

Sessão B:

INSERT INTO mytab VALUES (1, 300);
COMMIT;

Ambas as transações vão cometer sem erro. No final, a tabela terá as seis linhas mencionadas, gerando o estado inconsistente.


3.2. Comportamento com SERIALIZABLE (uma aborta)

Agora, repita o teste, mas altere o nível de isolamento para SERIALIZABLE em ambas as sessões:

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Repita os mesmos passos:

  1. Ambas iniciam transação (BEGIN).
  2. Ambas fazem SELECT SUM(value) ... (cada uma com sua classe).
  3. Ambas tentam inserir e comitar.

Desta vez, uma das transações vai falhar ao tentar comitar, com a mensagem:

ERROR: could not serialize access due to read/write dependencies among transactions
DETAIL: Reason: Cannot serialize access due to read/write dependencies.

A transação que falhar deve ser repetida (com ROLLBACK e recomeço). Se você refizer a transação com o novo estado, ela recalculará a soma com base no que a outra já inseriu, garantindo consistência.


  1. Sem bloqueios pesados: O SSI não usa LOCK TABLE ou SELECT FOR UPDATE para evitar anomalias. Ele apenas rastreia dependências, o que é mais escalável.
  2. Prevenção proativa: O banco aborta a transação antes que ela seja commitada, impedindo que dados inconsistentes sejam gravados.
  3. Facilita a aplicação: Você pode escrever sua lógica de negócio como se estivesse em uma única thread; o banco cuida das inconsistências. Basta tratar o erro 40001 (serialization_failure) e reexecutar.

Isso é crítico em sistemas financeiros, estoques, reservas de passagens, qualquer lugar onde a consistência dos dados dependa da ordem das operações.


5. Dica para testes e próximos passos

  • Para forçar o erro em uma das transações, você pode ajustar o tempo de espera ou simular concorrência real com ferramentas como pgbench.
  • O PostgreSQL mantém uma visão detalhada dos locks e dependências na view pg_locks, você pode consultá-la para ver os predicate locks em ação.

6. Conclusão

O SERIALIZABLE do PostgreSQL não é apenas um nome bonito, é uma implementação robusta do conceito de serialização, usando o algoritmo SSI que monitora dependências de leitura/escrita. Neste tutorial, vimos um exemplo clássico de anomalia de serialização e como o PostgreSQL a detecta e a previne, abortando uma das transações. Isso torna o banco uma ferramenta poderosa para aplicações que exigem consistência forte, sem sacrificar a performance com locks pesados.

"O PostgreSQL é mais esperto que você", e ele está sempre de olho nas suas transações para garantir que o estado final do banco faça sentido.


Referências:

  • PostgreSQL 18.3 Documentation, Chapter 13: Concurrency Control, Sections 13.2.2 e 13.2.3.
  • Exemplo adaptado do manual oficial (páginas 516-517 do PDF).

Agradecimentos: Este post foi criado com o auxílio de ferramentas de IA para estruturação de roteiro e exemplos, mas todo o conteúdo foi validado contra a documentação oficial e testes práticos.