![]() |
|
|
By: Tal Olier | Read Comments (8) | Print Tal is a database expert currently working for HP holding various positions in IT and R&D departments. Related Tips: 1 | 2 | 3 | 4 | 5 | 6 | 7 | More |
|
Solution
In order to explain my point I am going to use one table and run queries from two different sessions.
The table that we will be using has the following columns:
Create Table Customers
IF OBJECT_ID ('customers') IS NOT NULL DROP TABLE customers |
As you can see I have added an additional column called "some_other_fields_size_1k". This column is simulating an additional 1K of customer data; I like adding it to my tests to make the optimizer respond to more authentic requests.
Let's fill the table with some data, we will do the following:
Table Customers - fill with data
SET nocount ON |
As I mentioned earlier, I have two processes that will be running:
Dashboard Application SELECT Statement
SELECT display_name, current_quota |
Planning Module UPDATE Statement
UPDATE customers |
I would like to state this scenario assumes that we are working with the default isolation level (READ COMMITTED) of Microsoft SQL Server.
READ COMMITTED isolation level allows good concurrency with reasonable integrity (some might argue it is not that good and not that reasonable - and we'll get to that but hey, most of the OLTP applications we know use it).
The core of the READ COMMITTED isolation level is composed of two building blocks called exclusive locks and shared locks that comply with the following guidelines:
Now to the challenge: there are cases where the planning module (long running transactions) locks a specific record and does not allow the dashboard application to view specific information for a long time; believe me, it happens - I know - I have built this scenario :).
We all know that indexes boost database performance by keeping an ordered list (tree based) of keys and a linkage to the actual data location. So when our application has some performance issues the intuitive solution would be to look at the SQL statement that does not perform well and possibly add indexes. Here is our statement that we are trying to run that is getting blocked.
Dashboard Application SELECT Statement
SELECT display_name, current_quota |
We could add an index on support_level, but since adding this index will not solve our blocking issue we will not bother adding this index.
Let's start from the beginning...
Phase 1 - Check for bottlenecks
In short, we first check the database machine's CPU, disk and memory. When we see it is not that, some of us would turn to the event log. Not finding anything unusual in the event log, scratching our head over enough time - the locks issue may pop up (hard to admit but the locks part is somehow always a surprising part :) - I usually turn to check the locks when I see that some SQL is stuck and the machine is saying: "I am going to sleep, please wake me when you need something".
Phase 2 - Drill down into locks
In order to view the locks there is of course the famous sp_lock and even sp_lock2, but I like to use my own get_locks procedure (which also gives additional information regarding the locked objects (e.g. owner object and index name etc') this can be created in any SQL Server 2005 server with the following SQL script:
get_locks stored procedure - creation script
CREATE PROCEDURE get_locks |
Now let's check to see that there are any locks, by running:
get_locks |
We get the following result, which shows there are no current locks.

Now let's open two sessions (SQL Server Management Studio query windows), in the first (we'll call it Session A) we'll run the following SQL:
Session A - Dashboard Application SELECT
SELECT @@SPID |
This statement (when not blocked by others) is fetching the display_name and current_quota of customers with support level = 1:

In the second session (we'll call it Session B) let's run the following update statement:
Session B - Planning Module UPDATE
SELECT @@SPID --added just for session identification in the dm views. |
This is returned:

The above statement opened a transaction, updated a record and did not close the transaction (with either commit or rollback command), so it is currently locking some of the resources.
Let's review the locks with get_locks
get_locks |
This time the result would be:

What we can see here is the process # 56 (Our Session B) has locked:
"IX lock" is a way to notify the database not to allow any shared locking that will block my update later on, when an object is locked with "IX lock" it:
For more information please refer to Lock Compatibility (Database Engine) in SQL Server 2005 Books Online.
Now let's re-run the dashboard application query within Session A:
Session A - Dashboard Application SELECT
SELECT @@SPID |
This time we can see that the query is blocked, because it is still executing.

Re-checking the locks with get_locks We can see the following lock status:
get_locks |

As you can see the previous locks from session B (spid = 56) are still there and we've got some new locks from session A (spid = 55):
Phase 3 - Trying an index
Well I've started the 'Solution' description by telling that adding an index on the support_level column in order to boost performance still won't help, but it will get us closer - let's try:
-- Before doing that we'll need to: |
OK, now that we have done the above if we run Session B UPDATE, Session A SELECT and get_locks, we will get the following:

As you can see same locks as before are used plus customers_ix1 index's got a new page "IS Lock" (page 77 in file 1) and a new "S Lock" on key '(970081334a1d)' placed by Session A.
Some might ask why did Session A succeed with placing a lock on customers_ix1 (line 1 in the above table) let's check the execution plan for the select statement by running:
-- 1. Stop running Session B |
This will yield:

As you can see Session A is accessing the data through the customers_ix1, so it tries to place an "S Lock" on it (and of course succeeds).
Well, this encapsulates two great hints for our solution:
So current status is that we can use indexes as a solution for Session A activity as long as they do not access the actual data row.
Voila!!!
By adding an index that will cover all columns of the table that are required for Session A's specific query (A.K.A. Covering Index), we will have a solid bypass without requiring specific access to the actual data row. Having achieved this, Session A will not be blocked while fetching data from a row that is actually blocked for writing (as done by Session B).
Adding the index is done by the following:
-- Before doing that we'll need to: |
Now let's re-run the locking update of Session B and the select for Session A.
Session B
SELECT @@SPID --added just for session identification in the dm views. |
Session A
SELECT display_name, current_quota |
Trying to run Session A's SELECT statement (while Session B's transaction is still locking) will produce the following results without being blocked:

Review New Query Plan
SET SHOWPLAN_TEXT ON |
the result is:

As you can see the only database object that is participating in fetching the data is customers_ix2 which we've just created. By using this covering index we were able to use a totally different index than the update statement and therefore have the statement complete without issue.
Some might argue that we would not need to go through a whole article just for putting a covering index and they might be correct, still there are certain situations when a covering index is not that straight forward, but it can be used to overcome locking challenges such as the above example.
Next Steps
| Share: | Share | Tweet |
|
![]() |
![]() |
Free SQL Server Learning |
| Friday, May 02, 2008 - 5:04:26 AM - BrianJones | Read The Tip |
|
Just wondering if there was any reason that the table hint WITH(NOLOCK) could not have been used ? That way you will use existing indexes and can retrieve all of the data, and not need to create another index. I try to only create indexes where a performance gain is required, especially if it is a busy table. |
|
| Friday, May 02, 2008 - 8:06:04 AM - ahains | Read The Tip |
|
Using the with(nolock) hint means you can/will get dirty reads that are not transactionally consistent. The other common warning against it is the query may error out due to data movement if the page your query is processing moves. I always use with nolock hint when running big / long running reports that don't need to be 100% accurate. Indexes certainly increase overhead of dml so like everything in sql the decision of whether or not to add one is 'it depends'. If it is a busy table with lots of reads then certainly it can increase query throughput. I think the article fails to bring up the important point that the update *will* take a lock on the index if the update changes any of the columns that the index covers. Here is an example that demonstrates this: create table t1 (id int, lastAgg int, pending int, currentAgg as lastAgg+pending) This table/index pattern can be used to implement a deferred update aggregation table. All of the readers that can afford a time lag query the lastAgg column. All readers that require up to date info query the currentAgg column and take the hit that they will be blocked by concurrent writers. All of the updates write to the pending column. A scheduled task or other background process occassionally goes through the table and does: set lastAgg=lastAgg+pending, pending=0, dirty=0. Side note 1: Note that it does not matter if the base table has a clustered index or is a heap. Side note 2: I don't know if this always holds true, but I observe that
if your query does an update/set on a column that is covered by the
index but does *not* actually change the value (i.e. set to value 3 and
current value is already 3), then the index is not locked. |
|
| Friday, May 02, 2008 - 8:44:25 AM - BrianJones | Read The Tip |
|
I agree with the comment that the data will not be transactionally consistent with WITH(NOLOCK), however as the point of this was to read columns that were not changed then I don't see the reason for using the index. As you quite rightly point out if the data changed is on a column in the index then the index will be locked anyway. Also, the point of the article I thought was to enable you to read rows that are locked for a long period of time, and if this data affects the index you are using then it looks to me that it doesn't answer the problem - getting the data consistently. I've not heard of queries erroring on a WITH(NOLOCK) and have not come across it on any of the databases I've written. I guess if the data is highly used, and the query you are executing is very slow then you will get discrepancies (data updated whilst query takes place not sure what that would do), but I would then suggest that you are writing heavy reports against a transactional database so should think about creating a replicated reporting database and remove the load from the data entry. I think I must be missing something here, because I can't really see how this will help over WITH(NOLOCK) |
|
| Friday, May 02, 2008 - 9:33:28 AM - ahains | Read The Tip |
|
For info on the exception you may hit when using with(nolock), google for: nolock data movement I think the point can be summed up as: IF Access pattern of column A is lots of DML, and access pattern of column B is lots of reads and few DML THEN IF you have a covering index, the reads from column B will experience zero blocking from DML to column A because the DML operation will only lock the base table and not the index. Note that you still have full transactional consistency in your reads -- if there is an infrequent DML to column B you will see any appropriate isolation. ELSE IF you do not have a covering index, column B will experience significant blocking from DML to column A because the DML operation will lock the whole row of the base table and the reads against column B must also use the base table. I had a need for this kind of pattern for a project so I made a blog entry with an overview |
|
| Tuesday, May 06, 2008 - 1:02:04 AM - BrianJones | Read The Tip |
|
re WITH(NOLOCK) 605 error : http://support.microsoft.com/default.aspx?scid=kb;en-us;235880 Reading this, I think that this would only occur if the data is being moved due to an update of a clustered index. I tend to make my clustered indexes non-updatable so that may answer why I have not come across this problem. However, I can see merit in using the covering index from a performance perspective as the server will read the index tree and not look at the table. And this is probably why a covering index is not subject to the row lock. I will certainly incorporate this idea into future designs should adding an index be a viable possibility. I think, in actual fact, I probably do exactly this but didn't realise that the covering index was the reason behind it all. As a side thought, would using WITH(NOLOCK) and the covering index increase the performance again (no intent locks will be created for a start) and should not suffer from the 605 error also ? Might be worth a look. |
|
| Monday, May 12, 2008 - 7:01:09 PM - tal.olier | Read The Tip |
|
Hi, As mentioed above (by ahains) using NOLOCK hint affects transaction read consistency i.e. when using it you can never be sure that you are reading correct information; I specially chose quota example since we do not like to see our qouta incorrect (think of you private bank account quota ;)...). Anyway the point of the article was not to " Hope this helps, --Tal (tal.olier@gmail.com)
|
|
| Monday, May 12, 2008 - 7:03:52 PM - tal.olier | Read The Tip |
|
mentioned and quota (sorry, was too enthusiastic to complete my answer) :) --Tal. |
|
| Monday, May 12, 2008 - 7:31:21 PM - tal.olier | Read The Tip |
|
Hello ahains, First I'd like to thank you for commenting my article it is a great pleasure knowing all this "writing stuff" do make any difference (also read your blog post...). Second, well, your observation (i.e. "update *will* take a lock on the index if the update changes any of the columns that the index covers") is correct but a bit confusing from the reader POV (I have a case with a given update and cannot change that...), the thing I wanted to show here was that update will "lock" your reading statement even if no data update occurred and to show there is a cure for that (also imply some of us practice this cure by adding covering index without knowing it solves locking issues - I did) . So when I built the article I noticed it should cover the reader POV i.e. first we have an update, then we have a read that is blocked. So I actually tried embedding your statement in two phrases in my article: Regarding the update of a value to the same value (i.e. 3=3), noticed that also and do think it is intentionally done by SQL Server, great idea for a future research :) hope I covered all points you've raised. Thanks, --Tal (tal.olier@gmail.com) |
|
|
privacy | disclaimer | copyright | advertise | about authors | contribute | feedback | giveaways | user groups Some names and products listed are the registered trademarks of their respective owners. Edgewood Solutions LLC | MSSharePointTips.com | MSSQLTips.com |