17

From some recent testing and reading I've done, it seems the "X" (exclusive) name part of XLOCK is misleading. It in fact doesn't lock any more than UPDLOCK. If it were exclusive, it would prevent external SELECTs, which it doesn't.

I cannot see either from reading or from testing and difference between the two.

The only time XLOCK creates an exclusive lock is when used with TABLOCK. My first question is "why only at this granularity?"

Further, I came across a blog that states the following:

However, watch out for XLOCK hint. SQL Server will effectively ignore XLOCK hint! There's an optimization where SQL Server check whether the data has changed since the oldest open transaction. If not, then an xlock is ignored. This makes xlock hints basically useless and should be avoided.

Has anyone run across this phenomenon?

Based on what I'm seeing, it seems this hint should be ignored.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
IamIC
  • 17,747
  • 20
  • 91
  • 154
  • More details of the optimisation Tibor [was talking about here](http://sqlblog.com/blogs/paul_white/archive/2010/11/01/read-committed-shared-locks-and-rollbacks.aspx). I have seen this happen in my own testing now - His phrasing made me think he was saying that the `X` lock hint would be ignored. This is not the case. The `X` locks are taken out. The specific optimisation is to do with not taking out `S` locks so there is never a conflict. Explained much better in the linked article. – Martin Smith Jan 06 '11 at 23:49

3 Answers3

22

Exclusivity of X locks vs U locks

In the lock compatibility matrix below it can be seen that the X lock is only compatible with the schema stability and Insert Range-Null lock types. U is compatible with the following additional shared lock types S/IS/RS-S/RI-S/RX-S

lock compatibility matrix http://i.msdn.microsoft.com/ms186396.LockConflictTable(en-us,SQL.105).gif

Granularity of X locks

These are taken out fine at all levels. The script and profiler trace below demonstrates them being successfully taken out at row level.

CREATE TABLE test_table (id int identity(1,1) primary key, col char(40))

INSERT INTO test_table
SELECT NEWID() FROM sys.objects

select * from test_table with (rowlock,XLOCK) where id=10

Trace

But rows can still be read!

It turns out that at read committed isolation level SQL Server will not always take out S locks, it will skip this step if there is no risk of reading uncommitted data without them. This means that there is no guarantee of a lock conflict ever occurring.

However if the initial select is with (paglock,XLOCK) then this will stop the reading transaction as the X lock on the page will block the IS page lock that will always be needed by the reader. This will of course have an impact on concurrency.

Other Caveats

Even if you lock the row/page this does not mean that you block all accesses to that row in the table. A lock on a row in the clustered index will not prevent queries reading data from the corresponding row in a covering non clustered index.

Community
  • 1
  • 1
Martin Smith
  • 438,706
  • 87
  • 741
  • 845
  • what about Insert Range-Null? – KM. Jan 05 '11 at 21:34
  • @KM - Good spot. Just scanned down and didn't spot that. Not sure what that one's for exactly. – Martin Smith Jan 05 '11 at 21:36
  • 1
    You know your stuff @Martin. It really would help if MS used more accurate naming and had non-misleading documentation. – IamIC Jan 05 '11 at 21:37
  • 2
    @IanC, it is most likely that the `XLOCK` hint dates back to Sybase: http://en.wikipedia.org/wiki/Microsoft_SQL_Server#Genesis – KM. Jan 05 '11 at 21:43
  • +1 to revert the prior error :-) @KM That's possible. It would be interesting to ask someone who worked on Sybase (no idea if you did). – IamIC Jan 05 '11 at 21:46
  • @Martin I'll have to do some study to be able to fully understand that trace :-). By the by, is that blog post snippet I posted correct? – IamIC Jan 05 '11 at 21:49
  • I did work with Sybase back in the day, but I can only say `it is most likely that...` because I can hardly remember the major differences between recent versions, let alone things that old... – KM. Jan 05 '11 at 21:53
  • @IanC - I was just looking at that. I guess I need to restart my SQL Server so the oldest open transaction is definitely younger than the most recent change. – Martin Smith Jan 05 '11 at 21:53
  • @KM you're making yourself sound much older that you probably are ;) – IamIC Jan 05 '11 at 21:57
  • @Martin thanks. I guess the verdict is "use UPDLOCK only". Since NOLOCK is being deprecated, and XLOCK doesn't behave as expected, that leaves UPDLOCK as the only mode (if required). – IamIC Jan 05 '11 at 21:58
  • @IanC - After a restart the trace was still showing that `X` locks were being taken out as expected so I'm not sure if this is a bug that is now fixed or if I misunderstood the circumstances that were meant to cause this problem to occur. – Martin Smith Jan 05 '11 at 22:04
  • @Martin Right. Unfortunately, this is out of my current knowledge scope. When you say "bug that is now fixed", are you referencing the statement in the blog post? – IamIC Jan 05 '11 at 22:08
  • 1
    @IanC - Yes, the optimization that Tibor talks about didn't seem to happen for me. There had been no changes to the data since the restart so the bit where he says "SQL Server check whether the data has changed since the oldest open transaction. If not, then an xlock is ignored" didn't happen in my test. – Martin Smith Jan 05 '11 at 22:09
  • @Martin Ok. All things given, I believe it is safe to conclude that UPDLOCK be used in favor of XLOCK as UPDLOCK is intuitive and XLOCK is misleading. Do you concur? – IamIC Jan 05 '11 at 22:14
  • They do different things. `UPDLOCK` will allow concurrent readers `XLOCK` won't. What is the purpose of using the locking hint? – Martin Smith Jan 05 '11 at 22:19
  • @Martin So XLOCK will prevent concurrent readers *on something that has been updated*, whereas UPDLOCK will allow reads (of the original version). Right? – IamIC Jan 05 '11 at 22:39
  • 1
    No. Locks are taken out on particular resources (row,page, table etc.) Only compatible locks can be taken out on the same resource at any one time. A reader via a standard `select` with no locking hints will need an `S` lock (ignoring snapshot isolation). This is compatible with a `U` lock but not an `X` lock. So the reader will be blocked until the `X` lock is released. (In the query in my answer when the lock will be released depends on whether the isolation level is `serializable` or if `holdlock` is used, if either are true it will be held till the end of the transaction) – Martin Smith Jan 05 '11 at 22:52
  • @IanC - try adding `holdlock` and putting your statement in an explicit transaction. – Martin Smith Jan 05 '11 at 22:54
  • I'm using explicit transactions... I wouldn't be testing anything otherwise :). Selecting with (HOLDLOCK, ROWLOCK) didn't prevent an external reader from reading the row. However, what I said above still holds true, per my tests. – IamIC Jan 05 '11 at 23:10
  • I know HOLDLOCK will hold the lock on an update until the end of a tran. – IamIC Jan 05 '11 at 23:11
  • 1
    Thanks @Martin. I rechecked MSDN: "XLOCK: Specifies that exclusive locks are to be taken and held until the transaction completes." Not sure what mistake I made earlier, but it works as expected now. However, I still can't get a difference in behavior between XLOCK & UPDLOCK. Both block external readers until the end of the transaction. – IamIC Jan 05 '11 at 23:21
  • Ok... shared, exclusive. Deadlocks. :) – IamIC Jan 05 '11 at 23:29
  • @Martin the default: READCOMMITTED. – IamIC Jan 05 '11 at 23:34
  • I see from Google & blogs that my question is quite common. The answers are quite contradictory. You said, "A reader via a standard select with no locking hints will need an S lock (ignoring snapshot isolation)." S = shared, right? What lock do you mean specifically? – IamIC Jan 05 '11 at 23:38
  • It'll need an `S` lock on the row/page/table and (assuming that the where clause matches a single row and it takes a row lock) on any resources it needs to read to get to the row. So if it has to do a clustered index scan it will need a shared lock on all pages or rows scanned on the way. These get released as soon as the page is read under read committed level. – Martin Smith Jan 05 '11 at 23:43
  • I looked back through my notes, and I found one difference between XLOCK and UPDLOCK. Select with XLOCK will prevent an external update. Select with UPDLOCK does nothing (obviously). – IamIC Jan 05 '11 at 23:57
  • when I try to test this, I don't see it working. I do the following: open a new window in SSMS, run `declare @ID int;begin transaction;select @id=id from MyTable WITH (rowlock,XLOCK, HOLDLOCK) where id=8716;print @id` now open another window in SSMS an run `select * from MyTable where id=8716` and I get the row back. What am I missing? – KM. Jan 06 '11 at 14:54
  • @KM - er, good Q. Don't think **you** are missing anything! Just tried `CREATE TABLE MyTable (id int primary key nonclustered,col2 int unique clustered) insert into MyTable values(8716,1)` and your script and got same result. To get blocking I added `WITH (INDEX=1)` to both queries and put reading transaction up to `REPEATABLE READ` isolation level. `sys.dm_tran_locks` shows it was blocked waiting for a row `S` lock. At read committed Profiler shows it only took an `IS` page lock - compatible with the writing transaction's page `IX` lock. No `S` lock at either row or page that would conflict. – Martin Smith Jan 06 '11 at 22:57
  • 1
    -Ah actually I think this is the optimisation that Tibor was talking about. I was looking at the wrong thing yesterday. The problem isn't that `X` locks don't get taken out (which is what I was looking at). The optimisation is that reading transactions don't always take `S` locks so there is not a guaranteed conflict. http://sqlblog.com/blogs/paul_white/archive/2010/11/01/read-committed-shared-locks-and-rollbacks.aspx – Martin Smith Jan 06 '11 at 23:52
  • @KM, @Martin there is no need to include HOLDLOCK with XLOCK. The latter will automatically do the same as the former. I quoted the MSDN snippet on this earlier. @Martin, of course I was missing something... wouldn't have made a post if this were not the case. – IamIC Jan 07 '11 at 00:33
  • @IanC - Yep I reached the same conclusion. I think the only difference `holdlock` makes here is taking exclusive range locks rather than exclusive row locks. – Martin Smith Jan 07 '11 at 01:18
  • I'm glad my question has provided some interest and provoked thought :) – IamIC Jan 07 '11 at 01:20
  • @Martin based on your comment "-Ah actually...", then it would seem my original premise of "rather don't use XLOCK - use UPDLOCK" (if possible) is valid. Do you agree? – IamIC Jan 07 '11 at 01:23
  • @IanC - Indeed - It was most educational. Bit fedup with the whole subject of locking now though! RE: Your last question I guess it depends on the objective - what is the purpose of using the locking hint? – Martin Smith Jan 07 '11 at 01:26
  • @Martin yes, it's not the most exciting topic. Simply due to misleading documentation. As I said before, forums are rife with confusions about this. Thanks for your help. As a side note, I'm surprised this question has a vote of 0. Of course, I notice that in general the "simple" ones get votes. Good thing we're not here for points ;) – IamIC Jan 07 '11 at 01:33
  • 2
    I've given up trying to understand the vagaries of SO voting! – Martin Smith Jan 07 '11 at 01:36
  • @Martin a smart decision, no doubt. – IamIC Jan 07 '11 at 06:10
3

It's not a caveat, it's a misunderstanding on what happens in SELECT.

A mere SELECT does not asks for Shared locks if the pages do not contain dirty data, and thus is not blocked by XLOCK.

To be blocked by XLOCK, you need to run in REPEATABLE READ isolation level. Two things can trigger that:

  1. Modifying data, through INSERT/UPDATE/DELETE. The table updated does not have to be the one the XLOCK is on.
  2. Explicitly asking for REPEATABLE READ through transaction isolation level or table hint.
James Dingle
  • 10,581
  • 2
  • 14
  • 4
  • By #1, I'm assuming the update could be on a related table? What scenario would trigger this condition? – IamIC Aug 09 '13 at 07:13
  • Any table. It does not have to be related by a foreign key, it can be in another database. It is the type of instruction which matters (INSERT/UPDATE/DELETE). – James Dingle Sep 24 '13 at 22:49
2

based on the comments in @Martin's answer, here is a little script (run the different parts in different SSMS windows to test the lock preventing a SELECT:

--
--how to lock/block a SELECT as well as UPDATE/DELETE on a particular row
--

--drop table MyTable
--set up table to test with
CREATE TABLE MyTable (RowID int primary key clustered
                     ,RowValue int unique nonclustered not null) 

--populate test data
;WITH InsertData AS
(
    SELECT 4321 AS Number
    UNION ALL
    SELECT Number+1
        FROM InsertData
        WHERE Number<9322
)
INSERT MyTable
        (RowID,RowValue)
    SELECT
        Number, 98765-Number
        FROM InsertData
        ORDER BY Number
    OPTION (MAXRECURSION 5001)

-----------------------------------------------------------------------------
-- #1
--OPEN A NEW SSMS window and run this
--
--create lock to block select/insert/update/delete
DECLARE @ID int

BEGIN TRANSACTION

SELECT @ID=RowID FROM MyTable WITH (ROWLOCK, XLOCK, HOLDLOCK) WHERE RowID=6822
PRINT @ID

--COMMIT  --<<<only run the commit when you want to release the lock
          --<<<adfter opening the other new windows and running the SQL in them



-----------------------------------------------------------------------------
-- #2
--OPEN A NEW SSMS window and run this
--
--shows how a select must wait for the lock to be released
--I couldn't get SSMS to output any text while in the trnasaction, even though
--it was completing those commands (possibly buffering them?) so look at the
--time to see that the statements were executing, and the SELECT...WHERE RowID=6822
--was what was where this script is blocked and waiting
SELECT GETDATE() AS [start of run]
SELECT '1 of 2, will select row',* FROM MyTable Where RowID=6822
go
DECLARE @SumValue int
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT GETDATE() AS [before transaction, shouldn't be nuch difference]
BEGIN TRANSACTION
SELECT @SumValue=SUM(RowID) FROM MyTable WHERE ROWID<6000
SELECT GETDATE() AS [in transaction, shouldn't be much difference]
    , @SumValue AS SumValue
--everything to here will run immediately, but the select below will wait for the
-- lock to be removed
SELECT '2 of 2, will wait for lock',* FROM MyTable Where RowID=6822
SELECT GETDATE() AS [in transaction after lock was removed, should show a difference]
COMMIT


-----------------------------------------------------------------------------
-- #3
--OPEN A NEW SSMS window and run this
--
--show how an update must wait
UPDATE MyTable SET RowValue=1111 WHERE RowID=5000  --will run immediately
GO
UPDATE MyTable SET RowValue=1111 WHERE RowID=6822 --waits for the lock to be removed

-----------------------------------------------------------------------------
-- #4
--OPEN A NEW SSMS window and run this
--
--show how a delete must wait
DELETE MyTable WHERE RowID=5000 --will run immediately
go
DELETE MyTable WHERE RowID=6822  --waits for the lock to be removed
Community
  • 1
  • 1
KM.
  • 101,727
  • 34
  • 178
  • 212