Articles

Database Isolation Levels: Read Committed vs Serializable

Isolation levels control how much of a concurrent transaction's uncommitted work another transaction can see, trading consistency for concurrency.

The Lycoris Team The Lycoris Team · · 4 min read
Rows of card catalog drawers used for looking up records

Isolation levels define how much of one database transaction’s in-progress, uncommitted work another concurrent transaction is allowed to see. They’re the “I” in ACID, and they exist because perfect isolation — where every transaction behaves as if it were the only one running — is expensive to guarantee under concurrent load. Each isolation level trades some of that guarantee for better throughput, and picking the right one means understanding exactly which anomalies you’re choosing to accept.

The anomalies isolation levels prevent

Concurrent transactions can interact in a few specific, well-defined bad ways:

  • Dirty read — a transaction reads data that another transaction has written but not yet committed. If that other transaction rolls back, the first transaction acted on data that never really existed.
  • Non-repeatable read — a transaction reads the same row twice and gets different values, because another transaction committed a change to that row in between.
  • Phantom read — a transaction re-runs the same query twice and gets a different set of rows, because another transaction inserted or deleted rows matching the query’s condition in between.
  • Lost update — two transactions both read the same row, both compute a new value based on what they read, and one transaction’s write silently overwrites the other’s, losing an update that should have been applied.

The four standard levels

Read uncommitted provides essentially no isolation — transactions can see each other’s uncommitted writes, allowing dirty reads. It’s rarely used in practice because the failure mode is hard to reason about.

Read committed guarantees a transaction only ever sees data that has been committed by other transactions — no dirty reads. It doesn’t protect against non-repeatable reads or phantoms: if you read the same row twice within one transaction, you can still get two different (committed) values, because each read sees whatever was most recently committed at that moment.

Repeatable read guarantees that if a transaction reads a row once, it will see the same value for that row every time it reads it again during the same transaction, even if another transaction commits a change in between. It typically still allows phantom reads in some implementations, though many databases extend repeatable read far enough to prevent them in practice.

Serializable is the strongest level: transactions are guaranteed to produce a result equivalent to some serial (one-at-a-time) execution order, even though they actually ran concurrently. No dirty reads, non-repeatable reads, phantoms, or lost updates. The cost is real — enforcing this guarantee usually means more locking or more transaction retries when the database detects a conflict it can’t resolve any other way.

What each level prevents

Isolation levelDirty readNon-repeatable readPhantom readLost update
Read uncommittedPossiblePossiblePossiblePossible
Read committedPreventedPossiblePossiblePossible
Repeatable readPreventedPreventedOften possiblePrevented
SerializablePreventedPreventedPreventedPrevented

Practical defaults and tradeoffs

Different database engines ship with different default isolation levels, and it’s worth knowing your database’s default rather than assuming — some default to read committed, others to repeatable read. Neither default is wrong; they reflect different assumptions about what most applications need most of the time.

Stricter isolation isn’t free. Serializable transactions often need to acquire more locks or rely on the database detecting conflicts and forcing one transaction to retry, which increases contention under heavy concurrent load — a concern that compounds if you’re also managing a large pool of concurrent connections; see what connection pooling is for how that pressure gets managed at the connection layer. In practice, most applications run comfortably at read committed — the default in PostgreSQL — or repeatable read and reserve serializable for the specific operations where a lost update or phantom row would cause real damage — financial transfers, inventory decrements, anything where two concurrent operations computing from stale data would corrupt the result. That’s the same class of bug a race condition describes at the application level; isolation levels are the database’s tool for preventing the equivalent problem at the data layer.

Where this fits in the bigger picture

Isolation level choice interacts with other database design decisions. A system leaning on read replicas has to reason about isolation across replication lag, not just within a single transaction. Analytical workloads that scan large ranges of data — the OLAP side of the OLTP vs OLAP split — often tolerate looser isolation than the transactional writes happening on the same data. And an application riddled with the N+1 query problem makes isolation anomalies more likely simply by holding a transaction open longer while it issues many small queries one at a time, widening the window in which another transaction’s concurrent write can interfere.

The takeaway

Isolation levels are a deliberate tradeoff between consistency and concurrency, not a single correct setting. Read committed stops dirty reads and is a reasonable default for most application code; repeatable read adds protection against a row changing under you mid-transaction; serializable eliminates every standard anomaly but costs more in locking and retries. Choose the loosest level that still protects the specific operation you’re running, and reach for serializable deliberately — for the transfers, decrements, and other operations where a subtle concurrency bug would actually cost something.

The Lycoris Team The Lycoris Team · · 5 min read

What Is a Database Trigger?

A database trigger is a procedure that runs automatically on an insert, update, or delete — enforcing rules the application layer can't guarantee.

#Databases #SQL #Backend
The Lycoris Team The Lycoris Team · · 4 min read

Primary Key vs Foreign Key vs Unique Constraint

Primary keys identify a row, foreign keys link one table to another, and unique constraints just prevent duplicates. How the three differ in SQL.

#Databases #SQL #Backend