Skip to main content
Create your own

PostgreSQL Isolation Levels Explained

Hello! Welcome back.

In our last lesson, we examined the underlying mechanisms of ACID properties in PostgreSQL, focusing on the performance trade-offs of Durability (via WAL and checkpoints) and Isolation (via MVCC, bloat, and HOT updates). We established that PostgreSQL's MVCC architecture is key to its high concurrency but introduces costs like write amplification and the need for vacuuming.

Today, we build directly on that foundation. Instead of the how of isolation, we'll explore the what—the specific guarantees offered. Our learning outcome is to configure and compare PostgreSQL isolation levels: Read Committed, Repeatable Read, and Serializable. We will analyze the concurrency phenomena, or "anomalies," that each level prevents and the performance price for that protection. This is critical for architecting correct and performant high-load systems, where choosing the right trade-off between consistency and throughput is paramount.

1. The Spectrum of Isolation and Concurrency Anomalies

Transaction isolation isn't a binary switch; it's a spectrum. At one end, you have maximum performance with weak guarantees, and at the other, you have maximum safety (serializability) with a higher performance cost due to increased contention or transaction rollbacks.

The SQL standard defines isolation levels by the anomalies they prevent. An anomaly is an outcome that would not be possible if the transactions had run one after another in some serial order. Let's define the most common ones.

System Design: Concurrency Control in Distributed System | Optimistic & Pessimistic Concurrency Lock

This video from the 'Concept && Coding' channel provides a detailed technical breakdown of the classic concurrency anomalies. While it discusses locking strategies more broadly, the explanations of the anomalies themselves are very clear and will set the stage for our discussion of how PostgreSQL's MVCC-based approach handles them.

Please watch the section 'Introduction to Isolation Levels and Concurrency Anomalies' from 15:41 to 27:15. Focus on understanding the definitions and examples for: Dirty Read: Reading uncommitted data from another transaction. Non-Repeatable Read: Reading the same row twice within a transaction and getting different values. Phantom Read: Running the same query twice within a transaction and getting a different set of rows.

To summarize and add a few more critical anomalies that we'll encounter:

  • Dirty Read: Transaction T1 reads data modified by a concurrent transaction T2 that has not yet committed. If T2 rolls back, T1 has read data that never "existed."
  • Non-Repeatable Read: T1 reads a row. T2 then modifies or deletes that row and commits. If T1 re-reads the row, it sees a different value or that the row is gone.
  • Phantom Read: T1 runs a query with a WHERE clause. T2 then inserts a new row that satisfies the WHERE clause and commits. If T1 re-runs its query, it sees a new "phantom" row.
  • Lost Update: T1 and T2 both read a value, say X=10. T1 calculates X+1 and writes X=11. T2 also calculates X+1 and writes X=11. The first update is "lost" because the final value should have been 12.
  • Write Skew: A generalization of the lost update. T1 reads some data A and makes a decision to write to B. Concurrently, T2 reads B and makes a decision to write to A. Both transactions' decisions are valid at the time they read the data, but the combined result violates a business invariant. For example, two on-call doctors check if at least one other doctor is on-call, see that there is, and both decide to take the day off.

Now, let's see how PostgreSQL's isolation levels handle these situations.

2. PostgreSQL Isolation Levels in Practice

PostgreSQL provides three of the four standard isolation levels. We will use a fantastic, in-depth guide to walk through them. It uses a practical booking system example with two concurrent users, Alice and Bob, to demonstrate the behavior of each level.

A Practical Guide to Taming Postgres Isolation Anomalies

We will be using 'A Practical Guide to Taming Postgres Isolation Anomalies' by Dan Svetlov as our primary reference. It provides excellent, hands-on SQL examples for each anomaly and isolation level.

We will refer to different sections of this article throughout the lesson. For now, you can open it and get a feel for the structure.

2.1. Read Committed: The Default

This is the default isolation level in PostgreSQL. Its behavior stems directly from the MVCC mechanism we discussed last time: each statement in a transaction gets a new snapshot of the database. This means a statement sees only data that was committed before it began.

A Practical Guide to Taming Postgres Isolation Anomalies

Let's explore what this means in practice. Please read the 'Read Committed' section of the guide.

Read the section titled 'Read Committed'. Pay close attention to the examples for Lost Update and Write Skew. Also, note the various mitigation techniques discussed, such as conditional updates (UPDATE ... WHERE), explicit locking (SELECT FOR UPDATE), and optimistic locking.

Key Characteristics of Read Committed:

  • Anomalies Prevented:
    • Dirty Reads: PostgreSQL's implementation of Read Committed is stronger than the SQL standard's minimum requirement. Because a statement's snapshot only includes committed data, dirty reads are impossible.
  • Anomalies Allowed:
    • Non-Repeatable Reads & Phantom Reads: Since each statement gets a new snapshot, if a concurrent transaction commits between two of your SELECT statements, the second SELECT will see the new data.
    • Lost Updates & Write Skew: These are very common problems in Read Committed. The classic "read-modify-write" cycle is unsafe without explicit protection, as the data you read can become stale by the time you write.
  • Performance: This level offers the highest concurrency with the least blocking, as readers don't block writers and writers only block other writers trying to modify the exact same rows.
  • Configuration: You can set it explicitly with SET TRANSACTION ISOLATION LEVEL READ COMMITTED;, but it's the default.

Practical Implications:
Read Committed is fast, but it places the burden of ensuring correctness on you, the developer. For any read-modify-write logic, you must use strategies like:

  1. Atomic UPDATE statements: UPDATE events SET available_seats = available_seats - 1 WHERE .... This is safe because the row-level lock taken by the UPDATE ensures that the calculation on the right-hand side is based on the most up-to-date row version.
  2. Pessimistic Locking (SELECT ... FOR UPDATE): This explicitly locks the rows you read, preventing any other transaction from modifying them until your transaction commits. This is a powerful tool but reduces concurrency, as other sessions will block. It's a common pattern in payment processing or order matching, where you must serialize access to a specific resource.
  3. Optimistic Locking: You add a version column to your table. The UPDATE statement then includes WHERE id = ? AND version = ?. If the row was modified by another transaction, its version will have changed, and your UPDATE will affect zero rows, signaling a conflict that the application must handle (usually by retrying).

2.2. Repeatable Read: A Stable View

The Repeatable Read level addresses the main weakness of Read Committed. It provides a more stable view of the database by using a single snapshot for the entire transaction, taken at the start of the first statement.

A Practical Guide to Taming Postgres Isolation Anomalies

Now let's see how upgrading the isolation level changes the behavior. Please read the 'Repeatable Read' section of the guide.

Read the section titled 'Repeatable Read'. Notice how Lost Updates and Write Skews now result in a serialization failure. Crucially, understand the 'Write Skew With Disjoint Write Sets' example, which is the key anomaly this level fails to prevent.

Key Characteristics of Repeatable Read:

  • Anomalies Prevented:
    • Non-Repeatable Reads & Phantom Reads: Because the entire transaction uses a single, fixed snapshot, you are guaranteed to see the exact same data for the life of the transaction. Concurrent commits are invisible.
    • Lost Updates & "Simple" Write Skew: If your transaction tries to UPDATE a row that has been modified by a concurrent transaction (which committed after your snapshot was taken), PostgreSQL detects this conflict. It doesn't block; instead, it aborts your transaction with a serialization failure.
  • Anomalies Allowed:
    • Write Skew with Disjoint Write Sets: This is the crucial limitation. If two concurrent Repeatable Read transactions read the same data but write to different rows, the database may not detect a conflict, leading to an anomaly. The article's example of Alice and Bob updating their own separate booking rows based on a shared SUM is the canonical illustration of this.
  • Performance: It has slightly more overhead than Read Committed and can significantly reduce throughput if conflicts are common, as failed transactions must be retried by the application.
  • Configuration: SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

Practical Implications:
This level is a significant step up in safety. It protects you from most common read-modify-write hazards. However, it introduces a new requirement for your application: you must be prepared to catch serialization failures and retry the entire transaction. This is a fundamental shift from the "wait for a lock" model of Read Committed.

2.3. Serializable: The Strongest Guarantee

This is the highest isolation level. It guarantees that the result of running transactions concurrently is the same as if they had been run in some serial order.

This diagram illustrates how PostgreSQL's `SERIALIZABLE` isolation level prevents a Write Skew anomaly. Alice's transaction reads salary data and plans an update. Concurrently, Bob's transaction inserts a new employee, which would affect the budget. When Alice tries to commit, Postgres detects that her initial read is now invalid due to Bob's committed write. It aborts her transaction with a serialization failure, thus preserving the data integrity that a simpler isolation level might have violated.

A Practical Guide to Taming Postgres Isolation Anomalies

Finally, let's examine the 'Serializable' level. Please read the corresponding section in the guide.

Read the section titled 'Serializable'. Focus on how it uses 'predicate locks' to prevent the 'Write Skew With Disjoint Write Sets' that Repeatable Read allowed. Also, note the rule that all interacting transactions must use this level for the guarantee to hold.

Key Characteristics of Serializable:

  • Anomalies Prevented: All of them, including Write Skew with Disjoint Write Sets.
  • How it Works: It does everything Repeatable Read does, but adds more sophisticated conflict detection. It tracks not just the rows that were read, but the predicates (the WHERE clauses) used to read them. If a concurrent transaction commits a write that would have changed the result of a read in your transaction, it detects a dependency and fails one of the transactions.
  • Performance: This level has the highest overhead. It requires the database to track more dependency information and can result in more serialization failures than Repeatable Read.
  • Configuration: SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Practical Implications:
Serializable provides the strongest correctness guarantee, simplifying application logic as you no longer need to reason about complex interleavings. However, it's not a silver bullet. The performance cost can be significant in high-contention workloads. Like Repeatable Read, it requires a robust application-level retry mechanism. It is best used for complex, multi-step business transactions where manual locking would be even more complex and error-prone, and where correctness is non-negotiable.

3. Comparison and Summary

The following table provides a clear visual summary of which anomalies are possible at each of PostgreSQL's isolation levels.

A comparison of PostgreSQL's isolation levels. Note that PostgreSQL's `Read Committed` prevents Dirty Reads, and its `Repeatable Read` prevents Phantom Reads, making them stronger than the minimal SQL standard definitions.
Isolation Level Dirty Read Non-Repeatable Read Phantom Read Lost Update / Write Skew Retry Logic Required?
Read Committed No Yes Yes Yes (without manual locking) No (transactions block)
Repeatable Read No No No Yes (with disjoint sets) Yes
Serializable No No No No Yes

Choosing the right level is a critical architectural decision:

  • Use Read Committed (the default) for most simple queries and transactions. When you have a read-modify-write cycle, use explicit locking (SELECT FOR UPDATE) for a pessimistic, blocking strategy, or optimistic locking for a non-blocking one.
  • Use Repeatable Read when you need a stable view of the data for the duration of a transaction (e.g., for a multi-statement report) and want protection against most write skews, and you are prepared to handle transaction retries.
  • Use Serializable for complex transactions with invariants that span multiple tables/rows, where correctness is paramount and the logic of manual locking would be too difficult to prove correct. Ensure your application has a solid retry wrapper for these transactions.

4. Conclusion

Today we've moved from the mechanics of isolation to the guarantees they provide. We've seen that PostgreSQL offers a powerful set of tools to manage the trade-off between concurrency and correctness.

Key Takeaways:

  • Read Committed is fast and scalable but requires you to manually manage concurrency control for complex operations using techniques like SELECT FOR UPDATE.
  • Repeatable Read provides a stable view of the data (snapshot isolation) and prevents many common anomalies, but it introduces the requirement for application-level transaction retries upon serialization failure. It is still vulnerable to write skews on disjoint data.
  • Serializable offers the strongest mathematical guarantee of correctness, preventing even the most subtle anomalies, at the cost of higher overhead and a greater likelihood of serialization failures that must be retried.

Preview of the Next Lesson:

We've covered the theory and seen examples. In the next lesson, "Demonstrate concurrency anomalies in PostgreSQL and resolve them using appropriate isolation levels," you will get a chance to apply this knowledge. We will set up a practical environment where you can trigger these anomalies yourself and then use the techniques we've discussed today to prevent them.

Can't find a good explanation? Sign up and we'll make it for you

Sign up