Dirty Records and Table Hints: Wait, Read Dirty, Skip or Fail

Dirty records and table hints work together: a hint decides whether your SELECT sees a change that isn’t committed yet. That change is a dirty record. By default, the SELECT waits. Four other choices let it read the change, skip it or give up.

Timeline of one update that is rolled back after 12 seconds and five readers that start after 3 seconds. The default reader waits 9 seconds and sees 24.00. READUNCOMMITTED and NOLOCK see 240.00 at once. READPAST skips the locked row. NOWAIT fails with Msg 1222. With row versioning on, the default reader sees 24.00 at once

The Sample Table

Four pots with a price each. Run this in any test database. Every result in this post comes from SQL Server 2025, with two query windows open against the same database.

CREATE TABLE Pots (PotID INT PRIMARY KEY, PotName VARCHAR(20) NOT NULL, Price DECIMAL(6,2) NOT NULL);
INSERT INTO Pots (PotID, PotName, Price) VALUES (1, 'Clay Pot', 24.00), (2, 'Glazed Pot', 31.00), (3, 'Hanging Pot', 18.00), (4, 'Window Box', 45.00);

One Session Changes a Price

Pretend 240.00 is a typing mistake. This session changes the price of the Clay Pot, waits 12 seconds and rolls back. For those 12 seconds the new price is a dirty record. It lives in the database, but it isn’t official, and it never will be.

BEGIN TRAN;
UPDATE Pots SET Price = 240.00 WHERE PotID = 1;
WAITFOR DELAY '00:00:12';
ROLLBACK TRAN;

Start it in the first window. Three seconds later, start each reader below in a second window.

The Default: Wait

A plain SELECT asks for the row. SQL Server makes it wait until the change is committed or rolled back. This is a safety feature. Only committed data comes back.

SELECT PotID, PotName, Price FROM Pots ORDER BY PotID;

The reader waited about 9 seconds, which is the time left on the writer’s clock. It returned 24.00. It never saw 240.00. The cost is speed: a query that could run in a millisecond waited nine seconds.

READUNCOMMITTED and NOLOCK: Read Dirty

A table hint changes that. READUNCOMMITTED tells the query to read without waiting.

SELECT PotID, PotName, Price FROM Pots WITH (READUNCOMMITTED) ORDER BY PotID;

It returned at once, with 240.00 for the Clay Pot. That price was rolled back a few seconds later, so any report built on it would be wrong. NOLOCK is the same hint under another name, and it returned the same rows.

SELECT PotID, PotName, Price FROM Pots WITH (NOLOCK) ORDER BY PotID;

The old shorthand without WITH, as in FROM Pots (NOLOCK), still ran in my test. Write WITH anyway.

A count is the classic use, when an approximate number is fine. This query returned 4 at once.

SELECT COUNT(*) AS PotRows FROM Pots WITH (NOLOCK);

Keep the count rough. A NOLOCK query can count rows twice or miss them while data moves. NOLOCK: Why It Counts Some Rows Twice and Misses Others shows how.

Table of six readers against the same uncommitted update. The default waits 9 seconds and returns price 24.00. READUNCOMMITTED and NOLOCK return 240.00 at once. READPAST returns 3 rows and skips the Clay Pot. NOWAIT returns Msg 1222. The default with row versioning on returns 24.00 at once

READPAST: Skip the Locked Row

READPAST leaves out any row that another session has locked.

SELECT PotID, PotName, Price FROM Pots WITH (READPAST) ORDER BY PotID;

The query came back at once with three pots. The Clay Pot is missing, not wrong. This suits a worklist where each worker takes whatever is free. It doesn’t suit a report that has to show every row.

NOWAIT: Fail

NOWAIT gives up instead of waiting.

SELECT PotID, PotName, Price FROM Pots WITH (NOWAIT) ORDER BY PotID;

The answer came at once: Msg 1222, lock request time out period exceeded. Your code can catch it and try again later.

A Database Option Changes the Default

A reader asked why a plain SELECT didn’t wait. A database option called READ_COMMITTED_SNAPSHOT is the usual reason. When it’s on, a plain SELECT reads the last committed version of the row instead of waiting. Check it with this query.

SELECT is_read_committed_snapshot_on FROM sys.databases WHERE name = DB_NAME();

The query returns 0 for a database with the option off and 1 for a database with it on. In this test I turned it on in my scratch database and ran the default reader again. It returned 24.00 at once, the last committed price. No dirty read, and no waiting. Switch it on only after you understand the extra work in tempdb.

Clean Up

Remove the sample table when you finish.

DROP TABLE Pots;

What to Remember

The default read waits for committed data. READUNCOMMITTED and NOLOCK read dirty data without waiting. READPAST skips locked rows, and NOWAIT fails with an error. Pick the one that matches the question: exact numbers need the default, and rough trends can use NOLOCK.

To go further, read NOLOCK: Why It Counts Some Rows Twice and Misses Others, Difference Between NOLOCK and NOWAIT Hints and The NOLOCK Question: Notes from the Field #117.

Quick reference card: Dirty Records and Table Hints

A table hint is not a speed trick, it is a decision about the version of the truth you accept.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

SQL Lock, SQL Scripts, SQL Transactions, Transaction Isolation
Previous Post
Multi-Row INSERT in SQL Server: One Statement, All or Nothing
Next Post
Many-to-Many Bridge Table in SQL Server: Model and Query It

Related Posts

2 Comments. Leave new

  • Rene Alberto Castro Velasquez
    August 7, 2011 9:14 pm

    The right answer is No. 2, the query runs normally.

    This is because according to the question as is written, NOLOCK or READUNCOMMITTED have not been used, so the query doesn’t have to wait until the data is no longer in the intermediate state.

    Now, if NOLOCK or READUNCOMMITTED are implicitly used due to the subject, then right answer is No. 1.

    Rene Castro
    El Salvador

    Reply
  • Hi Pinaldave,

    I actually had a question it… when tried to do the same my select query was not waiting for 15sec, its immediatley returning the uncommitted data without the READUNCOMITED keyword, pls let me know where i was wrong…

    Thanks
    Padma

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.