views:

1062

answers:

2

I am really having trouble with a query in my ColdFusion application (backended to MS SQL 2008). I keep getting DB deadlock errors on this transaction:

<code>
<cftransaction>
   <cfquery name="selectQuery">
      SELECT TOP 20 item_id, field2, field3
      FROM Table1
      WHERE subject_id = #subject_ID# AND lock_field IS NULL AND
            NOT EXISTS (SELECT * FROM Table2 WHERE Table2.user_ID = #user_ID# Table1.item_id = Table2.item_id)
   </cfquery>

   <cfquery name="updateQuery">
      UPDATE Table1
      SET lock_field = 1, locked_by = #user_ID#
      WHERE Table1.item_id IN (#ValueList(selectQuery.item_id#)
   </cfquery>
</cftransaction>
</code>

Bascially, I have a table (Table1) which represents a large queue of waiting items. Users "checkout" items to give them a score. Only one user can have an item checked out at a time. I need to request a block of 20 items at a time for a given user. Those items cannot already be checked out and the user cannot have already scored them before (hence the lock_field IS NULL and NOT EXISTS statement in the SELECT). Once I have determined the list of 20 item_ids, I need to then update the queue table to mark them as locked so no one else checks them out at the same time. I also need to return that list of item_ids.

I was thinking it might work better if I moved this from a cftransaction to a stored proc on the SQL Server side. I am just not sure if the cftransaction locking is interfering somehow. I am not a TSQL guru, so some help would be appreciated.

+5  A: 

Use a common table expression to select the data, then update the CTE and output from the UPDATE statement. This way everything is one single operation:

with cte as (
 SELECT TOP 20 item_id, field2, field3 
 FROM Table1 WITH (ROWLOCK, UPDLOCK)
 WHERE subject_id = #subject_ID# 
 AND lock_field IS NULL 
 AND NOT EXISTS (
   SELECT * FROM Table2 
   WHERE Table2.user_ID = #user_ID# AND Table1.item_id = Table2.item_id))
update cte   
 SET lock_field = 1, locked_by = #user_ID# 
 output inserted.item_id;
Remus Rusanu
Thanks for the quick answer. I just realized though, I need to return all 3 fields from the select query, not just the item_id. How would that change your answer?
Bryan Lewis
OUTPUT inserted.item_id, inserted.field2, inserted.field3
Remus Rusanu
Ok, so I should be able to drop this code into a new stored proc and obviously change the #varname# ColdFusion style vars to @varname stored proc vars and all should be good, no?
Bryan Lewis
Remus, never used CTEs before. A quick google search makes it look like these are sort of like dynamic Views, no?
Bryan Lewis
A CTE is another way of writing a derived table. Eg. instead of writing `SELECT ... FROM (SELECT ... FROM table)` you can write `WITH cte AS (SELECT ... FROM TABLE) SELECT ... FROM cte` and the two are identical. My example can be rewritten using derived tables, like Ralph did. I preffer CTE syntax as I find it easier to read and follow.
Remus Rusanu
Since this is being done in a single operation, there is no need for a TRANSACTION, right?
Bryan Lewis
Right, the whole statement (SELECT, UPDATE, OUTPUT) will run in its own implicit transaction.
Remus Rusanu
Since the Select from Table2 inside of the NOT EXISTS clause can be a "dirty" read, would there be any advantage to using the WITH (NOLOCK) hint on that inner query?
Bryan Lewis
I don't approve much of dirty reads, they lead to correctness issues much more often than most realise. Usually read commited works just fine, and in very high concurent system read commited snapshot can be used to spectacular results, w/o any of the dirty read corectness problems: http://msdn.microsoft.com/en-us/library/tcbchxcb(VS.80).aspx. If you really believe dirty reads are safe and appropiate, then use NOLOCK in the inner select.
Remus Rusanu
To clarify: dirty reads would give advatages (and also caus eproblems...) only if there is high probability of an update locking the Table2 rows this query is interested into, ie. the ones for #user_id# and the item_id about to be dequeued. If such updates occur, it raises some questions about how is your data/user activity paritioned, ie. *why* is another transaction updating this same user_id record? IF you find that locking occur on other user_id record is an indication table scans occur and likely you're missing an index.
Remus Rusanu
A: 

Not nothing much (read: anything) about PHP, but having some experience with TSQL you may want to consider changing your query to something like this:

update TABLE1 set LOCK_FIELD = 1
output inserted.item_id, inserted.OtherInterestingColumnsGoHere
from (select top 20 item_id from TABLE1(holdlock) ) as a
where a.item_id = table1.item_id

This should ensure that the items your select will be locked until the update is complete.

edit: added an output clause as the original question also wanted to know which rows were updated.

Ralph Shillington
Yes, but the issue with putting the select inside the UPDATE is that I need to RETURN the contents of the select query as part of this transaction.
Bryan Lewis
Why do you need HOLDLOCK? Won't the lock be held anyway since you're doing an UPDATE?
erikkallen