One of my development teams needed a mechanism for identifying the value of a key that was part of a lock (don’t ask). I’d never tried doing that before. Obviously if you hit the DMV sys.dm_tran_locks you can see the hash of the key in theÂ resource_description column. But how to pull the value back.Â After some research, I first found this excellent articleÂ by the late, great, Ken Henderson (I really wish he was still around).Â The articleÂ outlined, among other things, the use ofÂ an undocumented “virtual” column called %%lockres%%. Some more searching then uncovered this great articleÂ by James Rowland-Jones, AKA Claypole. He described how, in a very high volume system, he used %%lockres%% to identify the source of a deadlock as the internal mechanisms that SQL Server uses to manage locks, the hash of the key. Oh, and he opened an incident on Connect, which seems to be closed, but vote on it anyway, I did. %%lockres%% is also covered in Kalen Delaney’s excellent book on SQL Server 2008Â Internals and even warrants a bit of discussion in Professional SQL Server 2008, but that was written by James Rowland-Jones, so I’m not sure it counts.
In the meantime, while I was investigating this stuff, evidently the development team was looking into it on their own. They came to the same set of resources and decided to use the virtual column as part of their real-time, transactional application. Yeah, anÂ undocumented “virtual” column going into a major application. Since I would probably be unable to do anything about this, I decided to at least look into how this thing behaves so I can be aware of what types of problems I might run into.
First, a simple query:
FROM Person.Address AS a
WHERE a.AddressID = 432
If you run this query and take a look at the execution plan you’ll see a nice clean clustered index seek, just as you would suspect. If you take away the comment and run it again, the execution plan is identical. On the version of AdventureWorks2008Â currently installed on my machine, I get two page reads, regardless of whether or not I include %%lockres%% or not. With the comments removed, it returns the hash of the primary key: (b0004e040bc2). This looks pretty painless, free even.
If we want to see %%lockres%% in action, it’s not too difficult:
SET City = ‘dude’
WHERE AddressID = 432;
Obviously this will put a key lock on that row in the table. If I just select against sys.dm_os_tran_locks, the data returned looks like this:
resource_typeÂ Â resource_descriptionÂ Â resource_associated_entity_idÂ Â request_mode
KEYÂ Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â (b0004e040bc2)Â Â Â Â Â Â Â Â Â Â Â 72057594043564032Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â Â X
The original request from the development team was for a way to get the key value back when you know that a table is locked, such as the case here. I wrote this simple query to make that happen:
FROM person.address(NOLOCK) AS a
JOIN sys.dm_tran_locks AS dtl
ON a.%%lockres%% = dtl.resource_description
WHERE dtl.resource_type = ‘KEY’
This query works and returns our key value ofÂ 432 just as you would want. But, take a look at the execution plan:
Yes, that’s a clustered index (or table, same thing) scan followed by a Sort followed by a merge join, processing 19614 rowsÂ to return one. But hey, it was only 341 reads. To say the least, I’m not excited about seeing this in a production system. This was explicitly cautioned in Kalen Delaney’s book. While it appears that the remote scan operator, which is how the DMV is accessed in this case, is 59% of the operation, that’s the estimated cost and has been pointed out before, isn’t the best measure of real cost in the system.
The development team went off and developed their own query, they had said they were looking for the key value, but evidently they were looking for who was holding the lock on a particular key value:
FROM sys.dm_tran_locks l
INNER JOIN sys.dm_exec_sessions s
on l.request_session_id = s.session_id
inner join sys.partitions p on l.resource_associated_entity_id = p.hobt_id
where OBJECT_NAME(p.object_id) = ‘Address’ and
l.resource_description in (select %%lockres%%
from person.Address(NOLOCK) a WHERE a.AddressID = 432)
I actually had to adjust their query just a bit to get it to work correctly, but basically they had the right idea. Here’s the final execution plan:
This was still not terribly inspiring a thing to think about running in a production system although it only had one scan and seven reads. Whether or not putting this in a transactional system is a good idea, it certainly adds yet another tool, albeit an undocumented one, to the tool belt.