Loading learning content...
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.
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:
| 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.
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:
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.
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:
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 startedIn 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.
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:
Cautions:
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.
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:
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.
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.
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 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:
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.
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.
While Read Committed is defined by the SQL standard, implementations vary significantly across database systems. Understanding these differences is crucial for database portability and for leveraging platform-specific optimizations.
| Aspect | PostgreSQL | Oracle | SQL Server | MySQL/InnoDB |
|---|---|---|---|---|
| Default isolation | Yes | Yes | Yes | No (RR is default) |
| Implementation | MVCC | MVCC | Locking or RCSI | MVCC |
| Readers block on writers | Never | Never | Yes (without RCSI) | Never |
| Statement vs query snapshot | Each statement | Each statement | Each statement | Each statement |
| Row versioning overhead | In table storage | In undo tablespace | In tempdb (RCSI) | In rollback segment |
123456789101112131415161718192021222324
-- PostgreSQL Specifics-- Uses xmin/xmax version visibility-- Old versions cleaned up by VACUUM process-- Very efficient for read-heavy workloadsSHOW default_transaction_isolation; -- Shows 'read committed' -- Oracle Specifics -- Uses System Change Numbers (SCN) for versioning-- Old versions stored in UNDO tablespace-- ORA-01555 "snapshot too old" if undo is exhaustedSELECT * FROM v$transaction; -- View active transactions -- SQL Server Specifics-- Traditional mode uses shared locks for reads-- RCSI mode uses tempdb for row versions-- RCSI can be enabled transparently for existing appsSELECT * FROM sys.dm_tran_version_store_space_usage; -- Version store usage -- MySQL/InnoDB Specifics-- Default is REPEATABLE READ (unusual!)-- Must explicitly set READ COMMITTED if desired-- Uses redo log and undo log for versioningSHOW VARIABLES LIKE 'transaction_isolation';SET GLOBAL transaction_isolation = 'READ-COMMITTED';MySQL/InnoDB defaults to REPEATABLE READ, not READ COMMITTED. This is a significant difference from other major databases. Many MySQL applications inadvertently rely on RR behavior without realizing it. If porting to PostgreSQL or SQL Server, test carefully for non-repeatable read impacts.
Read Committed represents the practical sweet spot of isolation levels—strong enough to prevent data corruption from dirty reads, flexible enough to allow high concurrency. Let's consolidate the key insights:
What's Next:
Read Committed prevents dirty reads but allows non-repeatable reads and phantoms. For applications requiring consistent reads throughout a transaction, we need Repeatable Read—the next level in our isolation hierarchy. The next page explores how holding read locks (or maintaining transaction-level snapshots) prevents non-repeatable reads.
You now understand Read Committed isolation comprehensively—its guarantee of committed-only data, locking vs MVCC implementations, RCSI optimization, permitted anomalies, and practical application patterns. This knowledge helps you understand why it's the default and when to accept or override it.