All Products
Search
Document Center

PolarDB:Transaction isolation

Last Updated:Mar 28, 2026

The SQL standard defines four levels of transaction isolation. The strictest is Serializable: any concurrent execution of a set of Serializable transactions is guaranteed to produce the same effect as running them one at a time in some order. The other three levels are defined by which concurrency phenomena they must prevent.

Concurrency phenomena

Dirty read

A transaction reads data written by a concurrent uncommitted transaction.

Nonrepeatable read

A transaction re-reads data it has previously read and finds that data has been modified by another transaction that committed since the initial read.

Phantom read

A transaction re-executes a query returning a set of rows that satisfy a search condition and finds that the set of rows satisfying the condition has changed due to another recently-committed transaction.

Serialization anomaly

The result of successfully committing a group of transactions is inconsistent with all possible orderings of running those transactions one at a time.

Isolation levels

The following table shows which phenomena each isolation level allows or prevents, as defined by the SQL standard and as implemented in PolarDB for PostgreSQL.

Isolation levelDirty readNonrepeatable readPhantom readSerialization anomaly
Read uncommittedAllowed, but not in PGPossiblePossiblePossible
Read committedNot possiblePossiblePossiblePossible
Repeatable readNot possibleNot possibleAllowed, but not in PGPossible
SerializableNot possibleNot possibleNot possibleNot possible

PolarDB for PostgreSQL accepts all four standard isolation level requests but internally implements only three distinct levels. The standard permits higher guarantees than the minimum required, which is why the Repeatable Read implementation also prevents phantom reads.

Read Uncommitted behaves identically to Read Committed in PolarDB for PostgreSQL. This is the only sensible mapping of the standard isolation levels to PostgreSQL's Multi-Version Concurrency Control (MVCC) architecture. If your application requests Read Uncommitted, it runs at Read Committed.

To set the isolation level for a transaction, use SET TRANSACTION.

Important

Some PostgreSQL data types and functions have special transactional behavior. Changes made to a sequence (including the counter of a column declared with serial) are immediately visible to all other transactions and are not rolled back if the transaction that made the changes aborts.

Read Committed isolation level

Read Committed is the default isolation level in PostgreSQL. Under this level:

  • SELECT (without FOR UPDATE/FOR SHARE) sees only data committed before the query began — never uncommitted data, and never changes committed by concurrent transactions during query execution.

  • SELECT does see the effects of earlier updates in its own transaction, even if not yet committed.

  • Two successive SELECT commands in the same transaction may return different data if another transaction commits changes between them.

UPDATE, DELETE, SELECT FOR UPDATE, and SELECT FOR SHARE search for target rows committed as of the command start time. If a target row has been updated or deleted by a concurrent transaction that is still in progress, the command waits for that transaction to commit or roll back:

  • If the concurrent transaction rolls back, the waiting command proceeds on the original row.

  • If the concurrent transaction commits and deleted the row, the waiting command ignores that row.

  • If the concurrent transaction commits and updated the row, the waiting command re-evaluates the WHERE clause against the updated version and proceeds if the row still matches.

INSERT with ON CONFLICT DO UPDATE guarantees that each proposed row will either insert or update, unless an unrelated error occurs. INSERT with ON CONFLICT DO NOTHING may skip a row if a conflict arises from a transaction whose effects are not yet visible to the INSERT snapshot.

Read Committed trade-offs

Read Committed allows a single command to see an inconsistent snapshot: it can see concurrent updates to the rows it is processing, but not concurrent updates to other rows in the database. This makes it unsuitable for commands with complex search conditions, but it works well for simple cases.

Example: bank balance transfer (works correctly)

BEGIN;
UPDATE accounts SET balance = balance + 100.00 WHERE acctnum = 12345;
UPDATE accounts SET balance = balance - 100.00 WHERE acctnum = 7534;
COMMIT;

Each command affects only a predetermined row, so seeing the updated row version does not create inconsistency.

Example: edge case with DELETE (unexpected behavior)

If the website table has two rows with hits values of 9 and 10:

BEGIN;
UPDATE website SET hits = hits + 1;
-- run from another session:  DELETE FROM website WHERE hits = 10;
COMMIT;

The DELETE has no effect even though a row with hits = 10 existed before and after the UPDATE. The pre-update value of 9 was skipped by the DELETE snapshot, and after the UPDATE commits, the row value is 11 — no longer matching the WHERE hits = 10 condition.

Read Committed provides adequate isolation for many applications and is fast to use. For applications with complex queries and updates that require a more consistent view, use Repeatable Read or Serializable.

Repeatable Read isolation level

Repeatable Read sees only data committed before the transaction began — never uncommitted data, and never changes committed by concurrent transactions during the transaction. Unlike Read Committed, the snapshot is taken at the start of the first non-transaction-control statement in the transaction, not at the start of each individual statement. Successive SELECT commands in the same transaction always see the same data.

This level prevents all phenomena in the table above except serialization anomalies, which exceeds the SQL standard's minimum requirement for Repeatable Read.

Applications using this level must be prepared to retry transactions due to serialization failures.

UPDATE, DELETE, SELECT FOR UPDATE, and SELECT FOR SHARE search for rows committed as of the transaction start time. If a target row has been updated or deleted by a concurrent transaction:

  • If the concurrent transaction rolls back, the repeatable read transaction proceeds on the original row.

  • If the concurrent transaction commits and actually updated or deleted the row (not just locked it), the repeatable read transaction is rolled back with:

    ERROR:  could not serialize access due to concurrent update

When this error occurs, abort the current transaction and retry from the beginning. On the second attempt, the transaction starts with a snapshot that includes the previously-committed change, so there is no logical conflict.

Only updating transactions need to retry; read-only transactions never encounter serialization conflicts.

Repeatable Read gives each transaction a consistent snapshot of the database, but that snapshot is not necessarily consistent with any serial (one-at-a-time) ordering of concurrent transactions. For example, a read-only transaction may see a control record updated to show a batch as completed, but miss a detail record that is logically part of that batch, because it read an earlier revision of the control record. Use explicit locks to enforce business rules that require strict serial ordering.

Repeatable Read is implemented using Snapshot Isolation, a technique described in academic database literature. Behavior and performance may differ from systems that use traditional locking.

Serializable isolation level

Serializable provides the strictest transaction isolation. It emulates serial transaction execution: as if all committed transactions had run one after another rather than concurrently.

Serializable works exactly like Repeatable Read, with one addition: it monitors for conditions that could cause a concurrent set of serializable transactions to produce a result inconsistent with any possible serial ordering. This monitoring adds no blocking beyond what Repeatable Read already requires, but it does add overhead. When a potential serialization anomaly is detected, one transaction is allowed to commit and the others are rolled back with:

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

Applications using this level must be prepared to retry transactions on serialization failures (SQLSTATE '40001').

Example: serialization anomaly

Given a table mytab with this initial data:

 class | value
-------+-------
     1 |    10
     1 |    20
     2 |   100
     2 |   200

Serializable transaction A computes:

SELECT SUM(value) FROM mytab WHERE class = 1;

and inserts the result (30) as a new row with class = 2. Concurrently, serializable transaction B computes:

SELECT SUM(value) FROM mytab WHERE class = 2;

and inserts the result (300) as a new row with class = 1. Both transactions try to commit. At Repeatable Read, both would commit. At Serializable, one commits and the other rolls back with the serialization error — because no serial ordering of A and B produces both results.

How predicate locking works

To guarantee true serializability, PolarDB for PostgreSQL uses predicate locking. Predicate locks track when a write would have affected the result of a prior read from a concurrent transaction. These locks:

  • Do not cause any blocking and cannot cause deadlocks.

  • Appear in the pg_locks system view with a mode of SIReadLock.

  • May be consolidated from finer-grained locks (tuple locks) into coarser-grained locks (page locks) to manage memory.

READ ONLY transactions may release their SIRead locks early if no further conflicts are possible. A SERIALIZABLE READ ONLY DEFERRABLE transaction blocks until it can establish that no serialization conflicts are possible before reading any data — the only case where Serializable blocks but Repeatable Read does not. Data read within a SERIALIZABLE READ ONLY DEFERRABLE transaction is known to be valid as soon as it is read, because the transaction waits until it can acquire a snapshot guaranteed to be free from such problems before reading any data. SIRead locks on read-write transactions are often held past transaction commit, until overlapping read-write transactions complete.

When relying on Serializable transactions to prevent anomalies, any data read from a permanent user table must not be considered valid until the transaction which read it has successfully committed. In all cases other than a deferrable read-only transaction, applications must not depend on results read during a transaction that later aborted — instead, they should retry the transaction until it succeeds.

Simplifying concurrent development

Consistent use of Serializable transactions can simplify application development. The guarantee that any set of successfully committed concurrent Serializable transactions has the same effect as if they ran one at a time means: if a single transaction does the right thing when run alone, it will do the right thing in any mix of Serializable transactions — or it will not commit. This removes the need to reason about all possible concurrent interactions.

Performance considerations

For optimal performance with Serializable transactions:

  • Declare transactions as READ ONLY when possible.

  • Control the number of active connections; use a connection pool in busy systems.

  • Keep transactions as short as needed for integrity purposes.

  • Avoid leaving connections idle in a transaction longer than necessary. Use idle_in_transaction_session_timeout to disconnect lingering sessions automatically.

  • Remove explicit locks, SELECT FOR UPDATE, and SELECT FOR SHARE that are no longer needed, since Serializable transactions provide equivalent protection automatically.

  • If predicate lock table memory is exhausted, the system consolidates page-level locks into relation-level locks, which increases the serialization failure rate. Increase max_pred_locks_per_transaction, max_pred_locks_per_relation, and/or max_pred_locks_per_page to avoid this.

  • Sequential scans always require a relation-level predicate lock, increasing the failure rate. To encourage index scans, reduce random_page_cost and/or increase cpu_tuple_cost. Balance any reduction in rollbacks against the overall change in query execution time.

Unique constraint considerations

Serializable isolation can raise unique constraint violations in cases where they would not occur in true serial execution. This happens when overlapping Serializable transactions both attempt to insert a key, even if each checked that the key was absent before inserting. To avoid this, have all Serializable transactions that insert potentially conflicting keys explicitly check for the key's existence first.

Serializable isolation is implemented using Serializable Snapshot Isolation (SSI), which builds on Snapshot Isolation by adding checks for serialization anomalies.