A dirty read is a concurrency anomaly in which one transaction reads data written by another transaction that has not yet committed. If the writing transaction later rolls back, the reader has seen values that never officially existed and may already have acted on them. In the theory of transactions, the dirty read is the most basic violation of isolation, the I in ACID.
In Databases, the SQL standard defines four isolation levels by which anomalies they permit, and the dirty read is the anomaly that only the lowest level, READ UNCOMMITTED, allows. Most production systems never produce dirty reads by default, but they still appear through explicit settings, query hints and some reporting tools.
How a dirty read happens
A dirty read requires two overlapping transactions. Transaction A modifies a row but has not committed. Transaction B, running at an isolation level that ignores uncommitted changes, reads that row and sees the new, uncommitted value. If A then rolls back, the database restores the old value, but B has already used the discarded one.
The sequence below shows the classic case with an account balance of 1,000.
| Step | Transaction A | Transaction B | Balance seen |
|---|---|---|---|
| 1 | Begins, sets balance to 400 | Not yet started | Stored uncommitted: 400 |
| 2 | Still open, not committed | Reads balance at READ UNCOMMITTED | B sees 400 |
| 3 | Rolls back after a failed check | Holds the value 400 | Committed value: 1,000 |
| 4 | Finished | Rejects a payment for lack of funds | Decision based on 400 |
Whether the database can produce this depends on its concurrency control. Lock-based engines prevent dirty reads by making readers request shared locks, which wait for writers’ exclusive locks. Engines using multiversion concurrency control (MVCC) keep older committed row versions and show readers only committed data, so readers neither wait nor see uncommitted changes.
Dirty reads and the isolation levels
The SQL standard ranks isolation levels by which read phenomena they allow. Dirty reads are excluded from every level above READ UNCOMMITTED, while non-repeatable reads and phantom reads are excluded only at higher levels.
| Isolation level | Dirty read | Non-repeatable read | Phantom read |
|---|---|---|---|
| READ UNCOMMITTED | Possible | Possible | Possible |
| READ COMMITTED | Prevented | Possible | Possible |
| REPEATABLE READ | Prevented | Prevented | Possible in the standard |
| SERIALIZABLE | Prevented | Prevented | Prevented |
Real systems often provide stronger guarantees than the standard requires. PostgreSQL accepts the READ UNCOMMITTED setting but treats it as READ COMMITTED, so it never returns dirty data. Oracle Database does not offer READ UNCOMMITTED at all. SQL Server and MySQL with InnoDB do support it and will return uncommitted data at that level.
How a dirty read differs from related anomalies
A dirty read is about seeing uncommitted data, whereas the neighboring anomalies involve committed data that changes between two reads in the same transaction. Telling them apart matters, because each requires a different isolation level or technique.
| Anomaly | What the reader sees | Lowest level that prevents it |
|---|---|---|
| Dirty read | Another transaction’s uncommitted change | READ COMMITTED |
| Non-repeatable read | A row changed by a committed transaction between two reads | REPEATABLE READ |
| Phantom read | New or removed rows matching a repeated query | SERIALIZABLE |
| Lost update | Its own write overwritten by a concurrent write | Varies; often needs locking or versioning |
| Write skew | Two valid writes that together break a rule | SERIALIZABLE |
A related but distinct risk at READ UNCOMMITTED in lock-based engines is that scans can skip rows or read the same row twice when pages split or rows move during the scan. This means the results are not only possibly uncommitted but also internally inconsistent.
Where dirty reads appear in practice
Dirty reads usually appear when someone deliberately lowers isolation to avoid blocking. The most common example is the SQL Server table hint WITH (NOLOCK), which is equivalent to READ UNCOMMITTED for that table and is frequently added to reporting queries to stop them from waiting on writers.
The table summarizes typical sources and safer alternatives.
| Source | Why it is used | Safer alternative |
|---|---|---|
| `WITH (NOLOCK)` hint | Avoids blocking on busy tables | Read committed snapshot isolation |
| Session set to READ UNCOMMITTED | Faster ad hoc analysis | Snapshot isolation or a read replica |
| Reporting tool defaults | Tool sets the lowest level by default | Configure READ COMMITTED explicitly |
| Monitoring queries | Progress of long-running jobs | Acceptable when approximate data is fine |
There are legitimate uses: watching the progress of a large batch load, or computing rough row counts on a dashboard where exactness does not matter. The rule is that dirty data must never drive a business decision or be written back into the database.
Preventing dirty reads without heavy blocking
The usual motive for accepting dirty reads is reader-writer blocking, and modern engines solve that problem without sacrificing correctness. Row versioning lets readers see the last committed version while writers proceed.
- Use READ COMMITTED as the minimum for any transaction whose results affect money, inventory or user-visible state.
- Enable read committed snapshot isolation in SQL Server, which removes most reasons for
NOLOCK. - Keep write transactions short, so locks and versions are released quickly.
- Move heavy reporting to replicas or a data warehouse instead of lowering isolation on the primary.
- Search code for NOLOCK hints during reviews and require a written justification for each one.
The simple explanation
A dirty read is like reading a draft that its author might still throw away. Someone is in the middle of changing a record. Another person looks at it and sees the half-finished change.
If the first person then cancels, the second person has acted on information that never became real. Databases prevent this by showing each reader only changes that have been fully saved.
How it is used in practice
An e-commerce company’s stock dashboard used queries with WITH (NOLOCK) to avoid slowing down order processing. During a busy sale, a large order transaction reduced the stock of a popular product to zero and then rolled back because the payment failed. The dashboard query had already read the zero, and an automatic rule marked the product as sold out for almost an hour.
The database team enabled read committed snapshot isolation on the database and removed the hints. Dashboard queries now read the last committed stock values without blocking order processing, the false sold-out events stopped, and no measurable slowdown appeared in checkout response times.