Isolation Levels
Read Committed
Read aloud is unavailable in this browser.
The Default Choice for a Reason
If Read Uncommitted is the Wild West of isolation levels, Read Committed represents civilization's first foothold—a balanced middle ground where you're guaranteed to never see uncommitted data, but you accept that the data you read might change before your transaction ends.
Read Committed is the default isolation level in PostgreSQL, Oracle, SQL Server, and most other production databases. This isn't coincidence—it represents the pragmatic sweet spot where most applications get adequate consistency without excessive locking overhead.
By the end of this page, you will understand Read Committed's single core guarantee (no dirty reads), the two primary implementation strategies (locking vs MVCC), which anomalies remain possible, and why this level is sufficient for the majority of application workloads. You'll also learn about Read Committed Snapshot Isolation (RCSI) and how it changes the traditional locking model.
Formal Definition of Read Committed
Read Committed is the second isolation level in the SQL standard hierarchy. It provides one fundamental guarantee: a transaction will only see data that has been committed at the moment of each read operation. Uncommitted changes from other transactions are completely invisible.
SQL Standard Definition:
Read Committed is defined by the phenomena it prevents and permits:
- Prevents Dirty Reads: A transaction never sees uncommitted modifications from other transactions
- Permits Non-Repeatable Reads: The same row read twice may return different values if another transaction commits between reads
- Permits Phantom Reads: New rows may appear in range queries if another transaction commits insertions
| Anomaly Type | Read Uncommitted | Read Committed | Description |
|---|---|---|---|
| Dirty Read | ❌ Possible | ✅ Prevented | Reading uncommitted data |
| Non-Repeatable Read | ❌ Possible | ❌ Possible | Row values changing between reads |
| Phantom Read | ❌ Possible | ❌ Possible | New rows appearing in range queries |
1234567891011121314151617181920212223242526272829
-- Setting Read Committed isolation level in various databases -- SQL Server (explicitly, though it's the default)SET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION; -- All reads see only committed data SELECT * FROM Accounts WHERE account_id = 'ACC001'; -- If another transaction is updating this row but hasn't committed, -- this SELECT will WAIT (in locking mode) or see prior version (in RCSI)COMMIT; -- PostgreSQL (default behavior)SET TRANSACTION ISOLATION LEVEL READ COMMITTED;-- or simply BEGIN; (Read Committed is PostgreSQL's default)BEGIN; SELECT * FROM accounts WHERE account_id = 'ACC001'; -- PostgreSQL MVCC: sees last committed version, never waitsCOMMIT; -- Oracle (default behavior)-- No explicit statement needed; Oracle defaults to Read Committed-- Every query sees a snapshot of committed data as of query start -- MySQL/InnoDBSET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;-- or per-transaction:START TRANSACTION; -- InnoDB uses MVCC, similar to PostgreSQLCOMMIT;Read Committed's fundamental promise is simple: every row you read exists in a committed state. You'll never make decisions based on data that might be rolled back. This single guarantee eliminates an entire class of data corruption bugs while remaining performant enough for production workloads.
Locking-Based Implementation
The traditional approach to implementing Read Committed uses short-duration read locks. When a transaction reads a row, it acquires a shared (S) lock just long enough to read the data, then immediately releases it.
How Locking-Based Read Committed Works:
- When Transaction A attempts to read a row
- If no exclusive (X) lock exists on that row → Acquire S lock, read, release S lock
- If an X lock exists (uncommitted write) → Wait until the X lock is released
- When the writer commits/aborts → X lock released → Transaction A reads committed data
12345678910111213141516171819202122232425
-- Locking behavior under Read Committed (SQL Server without RCSI) -- Session 1: WriterBEGIN TRANSACTION; -- Acquires X lock on row UPDATE Accounts SET balance = 500 WHERE account_id = 'ACC001'; -- X lock held until commit/rollback -- Simulate long-running operation WAITFOR DELAY '00:00:30';COMMIT;-- X lock released on commit -- Session 2: Reader (runs while Session 1 is active)SET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION; SELECT balance FROM Accounts WHERE account_id = 'ACC001'; -- ⏳ BLOCKED! Waits for Session 1 to commit or rollback -- This query hangs until Session 1's transaction completes -- Then returns the COMMITTED value (either new or original)COMMIT; -- Key difference from Read Uncommitted:-- Read Uncommitted: Returns 500 immediately (dirty read)-- Read Committed (locking): WAITS until commit, then returns final valueLock Duration Comparison:
The critical characteristic of Read Committed's locking is that read locks are released immediately after reading, not held until transaction end. This is what distinguishes it from Repeatable Read.
| Isolation Level | S Lock Acquired | S Lock Released | Effect |
|---|---|---|---|
| Read Uncommitted | Never | N/A | Can read through X locks (dirty) |
| Read Committed (locking) | During read | Immediately after read | Waits for commits, then moves on |
| Repeatable Read | During read | At transaction end | Prevents re-reads from changing |
In the pure locking implementation, readers block on uncommitted writes. This can cause significant latency during high-write periods. A single long-running write transaction can queue up dozens of readers waiting for it to complete. This is the main motivation for MVCC-based alternatives.
MVCC-Based Implementation
Modern databases like PostgreSQL and Oracle use Multi-Version Concurrency Control (MVCC) to implement Read Committed. Instead of blocking readers, the database maintains multiple versions of each row and shows readers the appropriate committed version.
How MVCC-Based Read Committed Works:
- Writers create new versions of rows without removing old versions
- Each version is tagged with the transaction that created it
- Readers see the latest version that was committed at the time the read started
- No read locks needed → Readers never block on writers
123456789101112131415161718192021222324252627282930
-- MVCC behavior under Read Committed (PostgreSQL example) -- Session 1: WriterBEGIN; -- Creates new version, old version still visible to readers UPDATE accounts SET balance = 500 WHERE account_id = 'ACC001'; -- No blocking occurs - old version remains visible -- Long processing... SELECT pg_sleep(30); COMMIT;-- New version now visible to new reads -- Session 2: Reader (runs while Session 1 is active)BEGIN; -- Immediately returns! No waiting! SELECT balance FROM accounts WHERE account_id = 'ACC001'; -- Returns: 1000 (the committed version from before Session 1's update) -- After Session 1 commits, same query returns different result: SELECT balance FROM accounts WHERE account_id = 'ACC001'; -- Returns: 500 (now this version is committed) -- This is a non-repeatable read, which is allowed at Read CommittedCOMMIT; -- PostgreSQL version visibility uses:-- xmin: Transaction ID that created the row version-- xmax: Transaction ID that deleted/updated (created next version)-- Visibility rules check if xmin's transaction committed before our query started- •Readers Never Block on Writers: Queries return immediately with consistent committed data
- •Writers Never Block on Readers: Updates proceed without waiting for SELECT queries
- •Reduced Deadlocks: Read operations can't participate in deadlock cycles
- •Better Throughput: High-concurrency workloads see dramatically improved performance
- •Consistent Point-in-Time Views: Each query sees a consistent snapshot (for that query's duration)
In MVCC-based Read Committed, each individual statement (not transaction) gets its own snapshot of committed data. This is why re-reading the same row can return different values—each SELECT takes a fresh snapshot showing the latest commits at that moment.
Read Committed Snapshot Isolation (RCSI)
SQL Server offers a hybrid approach called Read Committed Snapshot Isolation (RCSI). When enabled, Read Committed transactions use row versioning (similar to MVCC) instead of locking for reads. This brings MVCC benefits to SQL Server while maintaining compatibility with existing Read Committed applications.
RCSI vs Traditional Lock-Based Read Committed:
| Characteristic | Traditional Read Committed | RCSI |
|---|---|---|
| Readers block on writers | Yes | No |
| Writers block on readers | No | No |
| Read lock acquired | Yes (short-term) | No |
| What reader sees during update | Waits for commit | Prior committed version |
| tempdb usage | Minimal | Stores row versions |
| Lock escalation possible | Yes | Reduced |
| Deadlock risk | Higher | Lower |
12345678910111213141516171819202122232425262728293031
-- Enabling Read Committed Snapshot Isolation in SQL Server -- Step 1: Check current settingSELECT name, is_read_committed_snapshot_onFROM sys.databasesWHERE name = 'YourDatabase'; -- Step 2: Enable RCSI (requires exclusive database access or single_user mode)-- In production, typically done during maintenance windowALTER DATABASE YourDatabase SET READ_COMMITTED_SNAPSHOT ON; -- Alternative: Enable with controlled connection terminationALTER DATABASE YourDatabase SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK AFTER 60 SECONDS; -- Step 3: Verify the changeSELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = 'YourDatabase';-- Should return 1 -- Now all Read Committed transactions automatically use row versioning-- No application code changes required! -- To disable (if needed):ALTER DATABASE YourDatabase SET READ_COMMITTED_SNAPSHOT OFF; -- Monitor tempdb usage for row versionsSELECT SUM(version_store_reserved_page_count) * 8.0 / 1024 AS VersionStoreSizeMBFROM sys.dm_db_file_space_usage;When to Enable RCSI:
- High-read, moderate-write workloads: Where readers frequently wait on writers
- Applications with lock escalation issues: RCSI reduces the need for read locks
- Deadlock-prone systems: By eliminating read locks from the equation
- Migrating from PostgreSQL/Oracle: To maintain similar MVCC semantics
Cautions:
- tempdb growth: Row versions are stored in tempdb; ensure adequate space and I/O capacity
- Behavior change: Queries that previously waited now return immediately with older data
- Testing required: Some applications may rely on locking behavior (rare, but possible)
Most SQL Server performance experts recommend enabling RCSI for OLTP workloads. The reduction in blocking, deadlocks, and lock escalation typically outweighs the tempdb overhead. Microsoft itself uses RCSI for many of their internal SQL Server deployments.
Non-Repeatable Reads: The Permitted Anomaly
At Read Committed, you're protected from dirty reads but exposed to non-repeatable reads. This means if you read the same row twice within a transaction, you might see different values because another transaction committed an update between your reads.
Detailed Scenario:
| Time | Transaction A (Reader) | Transaction B (Writer) | Product Price |
|---|---|---|---|
| T1 | BEGIN | $100 (committed) | |
| T2 | SELECT price → $100 | $100 (committed) | |
| T3 | Business logic processing... | BEGIN | $100 (committed) |
| T4 | UPDATE price = $120 | $100 committed, $120 uncommitted | |
| T5 | COMMIT | $120 (committed) | |
| T6 | SELECT price → $120 ⚠️ | $120 (committed) | |
| T7 | Uses $100 and $120 in same txn! | $120 (committed) |
12345678910111213141516171819202122232425262728293031323334353637
-- Non-Repeatable Read demonstration at Read Committed -- Transaction A: Calculating order total with discountSET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION; -- First read: get base price DECLARE @BasePrice DECIMAL(10,2); SELECT @BasePrice = unit_price FROM Products WHERE product_id = 'P100'; -- @BasePrice = $100.00 -- Some processing time... WAITFOR DELAY '00:00:05'; -- Second read: verify price for discount calculation DECLARE @VerifyPrice DECIMAL(10,2); SELECT @VerifyPrice = unit_price FROM Products WHERE product_id = 'P100'; -- @VerifyPrice = $120.00 (different! Another txn committed) -- Inconsistency: used $100 for initial calc, $120 for verification -- If we compare: @BasePrice <> @VerifyPrice (inconsistent within transaction) -- This could cause incorrect discount applications IF @BasePrice >= 100 AND @VerifyPrice < 100 -- Logic error: conditions based on different snapshots PRINT 'Discount eligible based on old price, not new?'; COMMIT; -- Concurrent Transaction B (runs during A's processing delay)BEGIN TRANSACTION; UPDATE Products SET unit_price = 120.00 WHERE product_id = 'P100';COMMIT;When Non-Repeatable Reads Matter:
In many applications, non-repeatable reads are perfectly acceptable. A user viewing a product page who sees a price update mid-session isn't harmed—they get the most current information.
However, non-repeatable reads become problematic when:
- Multi-step calculations depend on consistent input values throughout
- Verification checks compare values that should be stable
- Reporting needs consistent data within a single report generation
- Business rules span multiple reads of the same data
If you need consistent reads at Read Committed, you can: (1) Read all needed data into variables at the start of the transaction, (2) Use a single query with joins instead of multiple separate queries, (3) Accept the inconsistency for non-critical operations, or (4) Upgrade to Repeatable Read for critical transactions.
Phantom Reads at Read Committed
Beyond non-repeatable reads, Read Committed also permits phantom reads—where new rows matching a query's WHERE clause appear between executions of the same query.
Phantom Read Mechanics:
12345678910111213141516171819202122232425262728293031323334
-- Phantom Read demonstration at Read Committed -- Transaction A: Calculating department salary totalsSET TRANSACTION ISOLATION LEVEL READ COMMITTED;BEGIN TRANSACTION; -- First query: sum salaries for Engineering SELECT SUM(salary) AS total_salary, COUNT(*) AS employee_count FROM Employees WHERE department = 'Engineering'; -- Returns: total_salary = $500,000, employee_count = 5 -- Business logic: budget planning based on current headcount WAITFOR DELAY '00:00:05'; -- Second query: same query, same transaction SELECT SUM(salary) AS total_salary, COUNT(*) AS employee_count FROM Employees WHERE department = 'Engineering'; -- Returns: total_salary = $650,000, employee_count = 7 -- Two new employees appeared (phantoms)! -- The transaction's calculations are now inconsistent -- Initial budget was based on 5 employees, but now there are 7 COMMIT; -- Concurrent Transaction B (runs during A's delay)BEGIN TRANSACTION; INSERT INTO Employees (employee_id, name, department, salary) VALUES ('E106', 'Alice', 'Engineering', 75000), ('E107', 'Bob', 'Engineering', 75000);COMMIT;Impact of Phantom Reads:
Phantom reads affect range queries, aggregations, and any operation that scans multiple rows based on a predicate. Unlike non-repeatable reads (which affect specific known rows), phantoms involve new rows that didn't exist during your first query.
- •Aggregate Reports: COUNT, SUM, AVG calculations over a range
- •Inventory Availability: Checking if enough stock exists across warehouses
- •Booking Systems: Counting reservations within a time window
- •Batch Processing: Operating on 'all rows matching X' criteria
- •Consistency Checks: Verifying foreign key-like relationships across tables
The distinction matters: Non-repeatable reads change existing rows. Phantom reads add or remove rows from result sets. Both are possible at Read Committed. Preventing phantoms requires Serializable isolation (or Repeatable Read in some implementations).
Read Committed in Practice
Read Committed is the workhorse isolation level—good enough for the vast majority of application workloads. Understanding where it excels and where it falls short helps you make informed decisions about when to accept it and when to upgrade.
Where Read Committed Excels:
- •CRUD Operations: Simple create, read, update, delete where each operation is atomic
- •User Interfaces: Displaying current data where seeing the latest commit is desired
- •Queue Processing: Pulling messages/jobs where work items are claimed individually
- •Audit Logging: Writing log entries that don't depend on other concurrent writes
- •Session Management: Tracking user sessions with simple state updates
- •Most Web Applications: Request-response patterns where each request is independent
123456789101112131415161718192021222324252627282930313233
-- Pattern 1: Simple CRUD - Read Committed is perfect-- Each operation is independent, no multi-step consistency neededBEGIN TRANSACTION; UPDATE Users SET last_login = GETDATE() WHERE user_id = @UserId;COMMIT; -- Pattern 2: Displaying current product information-- User sees latest committed price, which is exactly what we wantBEGIN TRANSACTION; SELECT product_name, price, stock_quantity FROM Products WHERE product_id = @ProductId;COMMIT; -- Pattern 3: Job queue processing with optimistic claim-- Worker claims a job atomically; Read Committed handles this wellBEGIN TRANSACTION; -- Claim next available job UPDATE TOP(1) JobQueue SET status = 'PROCESSING', worker_id = @WorkerId, claimed_at = GETDATE() OUTPUT INSERTED.* WHERE status = 'PENDING' ORDER BY created_at;COMMIT; -- Pattern 4: Inserting independent records-- No relationship to other data that might changeBEGIN TRANSACTION; INSERT INTO AuditLog (event_type, user_id, details, timestamp) VALUES (@EventType, @UserId, @Details, GETDATE());COMMIT;Where Read Committed Falls Short:
Certain patterns require stronger guarantees than Read Committed provides. Recognizing these patterns helps you avoid subtle bugs.
- •Read-Modify-Write Cycles: Reading a value, modifying it based on the read, and writing it back
- •Multi-Table Invariants: Ensuring consistency across related tables (e.g., order total = sum of line items)
- •Balance Transfers: Moving amounts between accounts while maintaining total balance
- •Uniqueness with Compound Logic: Checking if something exists and then inserting if not
- •Report Generation: Producing internally consistent reports across multiple queries
A common mistake at Read Committed: checking if a condition holds, then acting on it. By the time you act, the condition may have changed. Example: 'IF (SELECT stock > 10) THEN place_order()' -- another transaction might reserve that stock between your check and your order.
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.