views:

71

answers:

2

I have a process that starts a transaction, inserts a record into Table1, and then calls a long running web service (up to 30 seconds). If the web service call fails then the insert is rolled back (which is what we want). Here is an example of the insert (it is actually multiple inserts into multiple tables but I am simplifying for this question):

INSERT INTO Table1 (UserId, StatusTypeId) VALUES (@UserId, 1)

I have a second process that queries Table1 from the first step like this:

SELECT TOP 1 * FROM Table1 WHERE StatusTypeId=2

and then updates that row for a user. When process 1 is running, Table1 is locked so process 2 will not complete until process 1 finishes which is a problem because a long delay is introduced while process 1 finishes its web service call.

Process 1 will only ever insert a StatusTypeId of 1 and it is also the only operation that inserts into Table1. Process 2 will only query on StatusTypeId = 2. I want to tell Process 2 to ignore any inserts into Table1 but lock the row that it selects. The default isolation level for Process 2 is waiting on too much but I have a fear that IsolationLevel.ReadUncommitted allows reading of too much dirty data. I do not want two users running Process 2 and then accidentally getting the same row.

Is there a different IsolationLevel to use other than ReadUncommitted that says ignore inserted rows but make sure the select locks the row that is selected?

+3  A: 

Edit: Having re-read the question, the lock on any insert should not effect any select under READ COMMITTED this could be an issue with your indexes.

However, from your comments and rest of the question it seems you want only one transaction to be able to read a row at a time, which is not what an isolation level prevents.

They prevent

  • Dirty Read - reading uncommitted data in a transaction which could be rolled back - occurs in READ UNCOMMITTED, prevented in READ COMMITTED, REPEATABLE READ, SERIALIZABLE

  • Non Repeatable Reads - a row is updated whilst being read in an uncommitted transaction, meaning the same read of a particular row can occur twice in a transaction and produce a different results - occurs in READ UNCOMMITTED, READ COMMITTED. prevented in REPEATABLE READ, SERIALIZABLE

  • phantom rows - a row is inserted or deleted whilst being read in an uncommited transaction, meaning that the same read of multiple rows can occur twice in a transaction and produce different results, with either added or missing rows - occurs in READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, prevented in SERIALIZABLE
Chris Diver
I am looking for non repeatable reads. I want this to be in a transaction:SET @OrderId = (SELECT TOP 1 OrderId FROM Table1 WHERE StatusTypeId=2); UPDATE Table1 SET UserId=@UserId WHERE OrderId=@OrderId;-- and not get blocked by Process 1.
Jeff Widmer
@brian, It was the way i'd written it that was confusing. I hope the edit makes it easier to read.
Chris Diver
Yes, that's better, thank you.
Brian Hooper
Thanks Chris! This was great information but Martin Smith had the answer that I needed to ADD an index on StatusTypeId. Do you know why adding an index solved the problem?
Jeff Widmer
+3  A: 

Regarding the SELECT being blocked by the insert this should be avoidable by providing appropriate indexes.

Test Table.

CREATE TABLE Table1
(
UserId INT PRIMARY KEY,
StatusTypeId INT,
AnotherColumn varchar(50)
)
insert into Table1
SELECT number, (LEN(type)%2)+1, newid()
FROM master.dbo.spt_values
where type='p'

Query window one

BEGIN TRAN
INSERT INTO Table1 (UserId, StatusTypeId) VALUES (5000, 1)
WAITFOR DELAY '00:01';
ROLLBACK

Query window two (Blocks)

SELECT TOP 1 * 
FROM Table1 
WHERE StatusTypeId=2 
ORDER BY AnotherColumn

But if you retry the test after adding an index it won't block CREATE NONCLUSTERED INDEX ix ON Table1 (StatusTypeId,AnotherColumn)

Regarding your locking of rows for Process 2 you can use the following (the READPAST hint will allow 2 concurrent Process 2 transactions to begin processing different rows rather than one blocking the other). You might find this article by Remus Rusanu relevant

BEGIN TRAN

SELECT TOP 1 * 
FROM Table1  WITH (UPDLOCK, READPAST)
WHERE StatusTypeId=2
ORDER BY AnotherColumn

/*
Rest of Process Two's code here
*/
COMMIT
Martin Smith
That's it! Adding the index on the StatusTypeId column allowed process two's query to complete immediately. But why? Why does ADDING the index allow the process 2 query to complete?
Jeff Widmer
@Jeff - Without it it will end up (probably) doing a whole clustered index scan and in the process try and get a shared lock on the row that has just been inserted. Probably the `READPAST` hint would avoid that issue but it sounds like that query could well use that index anyway.
Martin Smith