DevX Home    Premier Club    Search    RFP Exchange    eLearning    Code Library    Help    Locator+    Shop DevX    

Special Report: Winning with Web Services   Special Report: Winning with Web Services
Inside Microsoft SQL Server 7.0

by Kalen Delaney

BUY NOW!

Chapter 13

From Inside Microsoft SQL Server 7.0 by Kalen Delaney. Reproduced by permission of Microsoft Press. All rights reserved.

In this chapter:

Locking

The Lock Manager
  • The Lock Manager and Isolation Levels
  • Spinlocks
  • Deadlocks
  • Locks and Memory

    Lock Types for User Data
  • Lock Modes
  • Lock Granularity
  • Lock Duration
  • Lock Ownership
  • Viewing Locks

    Lock Compatibility

    Bound Connections

    Row-Level vs. Page-Level Locking
  • Lock Escalation

    Locking Hints and Trace Flags

    Summary


    LOCKING
    Locking is a crucial function of any multiple-user database system, including Microsoft SQL Server. As you know, SQL Server manages multiple users simultaneously and ensures that all transactions observe the properties of the specified isolation level. At the highest isolation level, Serializable, SQL Server must make the multiple-user system yield results that are indistinguishable from those of a single-user system. It does this by automatically locking data to prevent changes made by one user from unexpectedly affecting work being done by another user on the same database at the same time.

    The Lock Manager
    SQL Server can lock data using several different modes. For example, read operations acquire shared locks and write operations acquire exclusive locks. Update locks are acquired during the initial portion of an update operation when the data is read. The SQL Server lock manager acquires and releases these locks. It also manages compatibility between lock modes, resolves deadlocks, and escalates locks if necessary. It controls locks on tables, on the pages of a table, on index keys, and on individual rows of data. Locks can also be held on system data—data that's private to the database system, such as page headers and indexes.

    The lock manager provides two separate locking systems. The first system affects all fully shared data and provides row locks, page locks, and table locks for tables, data pages, text pages, and leaf-level index pages. The second system is used internally for index concurrency control, controlling access to internal data structures, and retrieving individual rows of data pages. It uses latches, which are less resource intensive than locks and provide performance optimization. You could use full-blown locks for all locking, but because of their complexity, they would slow down the system if they were used for all internal needs. If you examine locks using the sp_lock system stored procedure or a similar mechanism that gets information from the syslockinfo table, you cannot see latches—you see only information about locks for fully shared data.

    Another way to look at the difference between locks and latches is that locks ensure the logical consistency of the data and latches ensure the physical consistency. Latching happens when you place a row physically on a page or move data in other ways, such as compressing the space on a page. SQL Server must guarantee that this data movement can happen without interference.

    The Lock Manager and Isolation Levels
    SQL Server supports all four transaction isolation levels specified by ANSI and ISO: Serializable, Repeatable Read, Read Committed, and Read Uncommitted. (See Chapter 3 and Chapter 10 for details.) To achieve the Serializable isolation level, phantoms must be prevented because the transaction's behavior must be identical to what would have occurred had the transaction run on a single-user system. SQL Server provides serializability, which you can set using SET TRANSACTION ISOLATION LEVEL SERIALIZABLE. To support serializability, SQL Server locks index ranges using a special type of lock called a key-range lock. Such locks are held until the end of the transaction to prevent phantoms. If no index exists, the lock manager uses a table lock to guarantee serializability.

    The lock manager provides fairly standard two-phase locking services. Although the names are similar, two-phase locking (2PL) and the two-phase commit (2PC) protocol are not directly related, other than by the obvious fact that 2PC must use 2PL services. In two-phase locking, a transaction has a "growing" phase, during which it acquires locks, and a "shrinking" phase, during which it releases locks. To achieve serializability, all acquired locks are held until the end of the transaction and then are dropped all at once.

    For a lower isolation level, such as Committed Read, locks can be released sooner—when the use of the object is completed. For example, if a range of data is being queried in the table, there will probably be shared locks outstanding. With Committed Read isolation, a shared lock is released as soon as the scan moves off one piece of data and onto the next. Exclusive locks, on the other hand, are always held until the end of the transaction so that the transaction can be rolled back if necessary.

    With Serializable or Repeatable Read isolation, shared locks must be held until the end of the transaction to guarantee that the data that was read will not change or that new rows meeting the criteria of the query cannot be added while the transaction is in progress.

    Like shared locks, latches are not tied to the boundaries of a transaction because they are used to provide mutual exclusion (mutex) functionality rather than to directly lock data. For example, during a row insert in a table with a clustered index, the nearby index page is latched to prevent other inserts from colliding. The latches are needed to provide mutual exclusion only during long periods of time (that is, periods with more than a few instruction cycles).

    Spinlocks
    For shorter-term needs, SQL Server achieves mutual exclusion using a latch, implemented with a spinlock. Spinlocks are used purely for mutual exclusion and never to lock user data. They are even more lightweight than latches, which are lighter than the full locks used for data and index leaf pages. The spinlock is the only place in SQL Server in which processor-specific assembly language is used. A spinlock is implemented in a few lines of assembly language specific to each processor type (such as x86/Pentium or Alpha). The requester of a spinlock repeats its request if the lock is not immediately available. (That is, the requestor "spins" on the lock until it is free.)

    Spinlocks are often used as mutexes within SQL Server when a resource is usually not busy. If a resource is busy, the duration of a spinlock is short enough that retrying is better than waiting and then being rescheduled by Microsoft Windows NT, which results in context switching between threads. The savings in context switches more than offsets the cost of spinning, as long as you don't have to spin too long. Spinlocks are used for situations in which the wait for a resource is expected to be brief (or if no wait is expected).

    Deadlocks
    A deadlock occurs when two processes are waiting for a resource and neither process can advance because the other process prevents it from getting the resource. A true deadlock is a catch-22 in which, without intervention, neither process can ever progress. When a deadlock occurs, SQL Server intervenes automatically.

    NOTE   A simple wait for a lock is not a deadlock. When the process that's holding the lock completes, the waiting process gets the lock. Lock waits are normal, expected, and necessary in multiple-user systems.

     
    Figure 13-1 Click here.

    In SQL Server, two main types of deadlocks can occur: a cycle deadlock and a conversion deadlock. Figure 13-1 on page 731 shows an example of a cycle deadlock. Process A starts a transaction, acquires an exclusive table lock on the authors table, and requests an exclusive table lock on the publishers table. Simultaneously, process B starts a transaction, acquires an exclusive lock on the publishers table, and requests an exclusive lock on the authors table. The two processes become deadlocked—caught in a "deadly embrace." Each process holds a resource needed by the other process. Neither can progress, and, without intervention, both would be stuck in deadlock forever. You can actually generate the deadlock using the SQL Server Query Analyzer, as follows:

    1. Open a query window, and change your database context to the pubs database.

    Execute the following batch for process A:

    BEGIN TRAN
    UPDATE authors SET contract = 0
    GO

    2. Open a second window, and execute this batch for process B:

    BEGIN TRAN
    UPDATE publishers SET city = 'Redmond', state = 'WA'
    GO

    3. Go back to the first window, and execute this update statement:

    UPDATE publishers SET city = 'New Orleans', state = 'LA'
    GO

    At this point, the query should block. It is not deadlocked yet, however. It is waiting for a lock on the publishers table, and there is no reason to suspect that it won't eventually get that lock.

    4. Go back to the second window, and execute this update statement:

    BEGIN TRAN
    UPDATE authors SET contract = 1
    GO

    At this point, a deadlock occurs. The first connection will never get its requested lock on the publishers table because the second connection will not give it up until it gets a lock on the authors table. Since the first connection already has the lock on the authors table, we have a deadlock. One of the processes received this error message (of course, the actual process ID reported will probably be different):

    Server: Msg 1205, Level 13, State 1, Line 1
    Your transaction (process ID #12) was deadlocked with another process and has been chosen as the deadlock victim. Rerun your transaction.

     
    Figure 13-2 Click here.

    Figure 13-2 on the following page shows an example of a conversion deadlock. Process A and process B each hold a shared lock on the same page within a transaction. Each process wants to promote its shared lock to an exclusive lock but cannot do so because of the other process's lock. Again, intervention is required.

    SQL Server automatically detects deadlocks and intervenes through the lock manager, which provides deadlock detection for regular locks. Latches are not involved in deadlock detection because SQL Server uses deadlock-proof algorithms when it acquires latches. When SQL Server detects a deadlock, it terminates one process's batch, rolling back the transaction and releasing all that process's locks to resolve the deadlock.

    In SQL Server 7, a separate thread called LOCK_MONITOR checks the system for deadlocks every 5 seconds. The lock monitor also uses an internal counter called a deadlock detection counter to determine whether to check the system for deadlocks more often. The deadlock detection counter starts at a value of 3 and is reset to 3 if a deadlock occurs. If the LOCK_MONITOR thread finds no deadlocks when it checks at the end of its 5-second cycle, it decrements the deadlock detection counter. If the counter has a value greater than 0, the lock manager requests that the lock monitor also check all the locks for a deadlock cycle if a process requests a lock resource and is blocked. Thus, after 20 seconds of not finding any deadlocks, the deadlock detection counter is 0 and the lock manager stops requesting deadlock detection every time a process blocks on a lock. The deadlock detection counter stays at 0 most of the time and the checking for deadlocks happens only at the 5-second intervals of the lock monitor.

    This LOCK_MONITOR thread checks for deadlocks by inspecting the list of waiting locks for any cycles, which indicate a circular relationship between processes holding locks and processes waiting for locks. Typically, the process chosen as the victim is the process that has just requested a lock and initiated the check for deadlocks. If a deadlock is encountered during the normal execution of the lock monitor, the first process whose termination will break the deadlock cycle is typically chosen as the victim. However, certain operations are marked as golden, or unkillable. For example, a process involved in rolling back a transaction cannot be chosen as a deadlock victim because the changes being rolled back could be left in an indeterminate state, causing data corruption.

    In SQL Server 6.5, the lock manager checked for deadlocks every time a process began waiting for a lock resource. This provided rapid deadlock detection, but it wasted a lot of resources looking for deadlocks even in systems or situations in which they were rare. In earlier SQL Server releases, other algorithms were used to choose the deadlock victim (such as choosing the process that had consumed fewer CPU cycles). Those algorithms made a noble attempt at fairness, but they often resulted in another deadlock because yet another process might have been queued up for the resource that was part of the deadlock circular chain of lock requests. The result would be a chain reaction.

    Using the SET DEADLOCK_PRIORITY LOW | NORMAL statement, you can make a process sacrifice itself as the victim if a deadlock is detected. If a process has a deadlock priority of LOW, it terminates when a deadlock is detected even if it is not the process that closed the loop. However, there is no counterpart SET option to set a deadlock priority to HIGH. As much as you might want your own processes to always come out the winner in a deadlock situation, this feature has not yet been implemented in SQL Server.

    NOTE   The lightweight latches and spinlocks used internally do not have deadlock detection services. Instead, deadlocks on latches and spinlocks are avoided rather than resolved. Avoidance is achieved via strict programming guidelines used by the SQL Server development team. These lightweight locks must be acquired in a hierarchy, and a process must not have to wait for a regular lock while holding a latch or spinlock. For example, one coding rule is that a process holding a spinlock must never directly wait for a lock or call another service that might have to wait for a lock, and a request can never be made for a spinlock that is higher in the acquisition hierarchy. By establishing similar guidelines for your development team for the order in which SQL Server objects are accessed, you can go a long way toward avoiding deadlocks in the first place.

    In the example in Figure 13-1, the cycle deadlock could have been avoided if the processes had decided on a protocol beforehand—for example, if they had decided to always access the customer table first and the parts table second. Then one of the processes would get the initial exclusive lock on the table being accessed first, and the other process would wait for the lock to be released. One process waiting for a lock is normal and natural. (Remember, waiting is not a deadlock.)

    You should always try to have a standard protocol for the order in which processes access tables. If you know that the processes might need to update the row after reading it, they should initially request an update lock, not a shared lock. If both processes request an update lock rather than a shared lock, the process that is granted an update lock is assured that the lock can later be promoted to an exclusive lock. The other process requesting an update lock has to wait. The use of an update lock serializes the requests for an exclusive lock. Other processes needing only to read the data can still get their shared locks and read. Since the holder of the update lock is guaranteed an exclusive lock, the deadlock is avoided. We'll look in more detail at the compatibility of locks later in this chapter; additional information on deadlocks is presented in Chapter 14.

    By the way, the time that your process holds locks should be minimal so other processes don't wait too long for locks to be released. Although you don't usually invoke locking directly, you can influence locking by keeping transactions as short as possible. For example, don't ask for user input in the middle of a transaction. Instead, get the input first and then quickly perform the transaction.

    Locks and Memory
    Locks are not on-disk structures—you won't find a lock field directly on a data page or a table header—because it would be too slow to do disk I/O for locking operations. Locks are internal memory structures-they consume part of the memory used for SQL Server. Each locked data resource (a row, index key, page, or table) requires 32 bytes of memory to keep track of the database, the type of lock, and the information describing the locked resource. Each process holding a lock also must have a lock owner block of 32 bytes. Sometimes a single transaction can have multiple lock owner blocks; a scrollable cursor sometimes uses several. Also, one lock can have many lock owner blocks, as in the case of a shared lock. Finally, each process waiting for a lock has a lock waiter block of another 32 bytes.

    NOTE   In this context, we use the term "process" to refer to a SQL Server subtask. Every user connection is referred to as a process, as are the checkpoint manager, the lazywriter, the log writer, and the lock monitor. But these are only subtasks within SQL Server, not processes from the perspective of Windows NT, which considers the entire SQL Server engine to be a single process with multiple threads.

    Lock Types for User Data
    Now we'll examine four aspects of locking user data. First, we'll look at the mode of locking (the type of lock). We already mentioned shared, exclusive, and update locks, and we'll go into more detail about these modes as well as others. Next, we'll look at the granularity of the lock, which specifies how much data is covered by a single lock. This can be a row, a page, an index key, a range of index keys, an extent, or an entire table. The third aspect of locking is the duration of the lock. As mentioned earlier, some locks are released as soon as the data has been accessed and some locks are held until the transaction commits or rolls back. For example, cursor scroll locks are held until a new FETCH operation is executed. The fourth aspect of locking concerns the ownership of the lock (the scope of the lock). Locks can be owned by a session, a transaction, or a cursor.

    Lock Modes
    SQL Server uses several locking modes, including shared locks, exclusive locks, update locks, and intent locks.

    Shared Locks
    Shared locks are acquired automatically by SQL Server when data is read. Shared locks can be held on a table, a page, an index key, or an individual row. Many processes can hold shared locks on the same data, but no process can acquire an exclusive lock on data that has a shared lock on it (unless the process requesting the exclusive lock is the same process as the one holding the shared lock). Normally, shared locks are released as soon as the data has been read, but you can change this by using query hints or a different transaction isolation level.

    Exclusive Locks
    SQL Server automatically acquires exclusive locks on data when it is modified by an insert, update, or delete operation. Only one process at a time can hold an exclusive lock on a particular data resource; in fact, as we'll see when we discuss lock compatibility, no locks of any kind can be acquired by a process if another process has the requested data resource exclusively locked. Exclusive locks are held until the end of the transaction. This means that the changed data is normally not available to any other process until the current transaction commits or rolls back. Other processes can decide to read exclusively locked data by using query hints.

    Update Locks
    Update locks are really not a separate kind of lock; they are a hybrid between shared and exclusive locks. They are acquired when SQL Server executes a data modification operation but first needs to search the table to find the resource that will be modified. Using query hints, a process can specifically request update locks, and in that case they prevent the conversion deadlock situation presented in Figure 13-2.

    Update locks provide compatibility with other current readers of data, allowing the process to later modify data with the assurance that the data hasn't been changed since it was last read. An update lock is not sufficient to allow you to change the data—all modifications require that the data resource being modified has an exclusive lock. An update lock acts as a serialization gate to queue future requests for the exclusive lock. (Many processes can hold shared locks for a resource, but only one process can hold an update lock.) As long as a process holds an update lock on a resource, no other process can acquire an update lock or an exclusive lock for that resource; instead, another process requesting an update or exclusive lock for the same resource must wait. The process holding the update lock can acquire an exclusive lock on that resource because the update lock prevents lock incompatibility with any other processes. You can think of update locks as "intent-to-update" locks, which is essentially the role they perform. Used alone, update locks are insufficient for updating data—an exclusive lock is still required for actual data modification. Serializing access for the exclusive lock lets you avoid conversion deadlocks.

    Don't let the name fool you: update locks are not just for update operations. SQL Server uses update locks for any data modification operation that requires a search for the data prior to the actual modification. Such operations include qualified updates and deletes, as well as inserts into a table with a clustered index. In the latter case, SQL Server must first search the data (using the clustered index) to find the correct position at which to insert the new row. While SQL Server is only searching, it uses update locks to protect the data; only after it has found the correct location and begins inserting does it escalate the update lock to an exclusive lock.

    Intent Locks
    Intent locks are not really a separate mode of locking; they are a qualifier to the modes previously discussed. In other words, you can have intent shared locks, intent exclusive locks, and even intent update locks. Because SQL Server can acquire locks at different levels of granularity, a mechanism is needed to indicate that a component of a resource is already locked. For example, if one process tries to lock a table, SQL Server needs a way to determine whether a row (or a page) of that table is already locked. Intent locks serve this purpose. We'll discuss them in more detail when we look at lock granularity.

    Special Lock Modes
    SQL Server offers three additional lock modes: schema stability locks, schema modification locks, and bulk update locks. When queries are compiled, schema stability locks prevent other processes from acquiring schema modification locks, which are taken when a table's structure is being modified. A bulk update lock is acquired when the BULK INSERT command is executed or when the bcp utility is run to load data into a table. In addition, the copy operation must request this special lock by using the TABLOCK hint. Alternatively, the table can set the table option called table lock on bulk load to true, and then any bulk copy IN or BULK INSERT operation automatically requests a bulk update lock. Another lock mode that you might notice is the SIX lock. This mode is never requested directly by the lock manager but is the result of a conversion. If a transaction is holding a shared (S) lock on a resource and later an IX lock is needed, the lock mode will be indicated as SIX. For example, suppose you are operating at the Repeatable Read transaction isolation level and you issue the following batch:

    BEGIN TRAN
    SELECT * FROM bigtable 
    UPDATE bigtable
        SET col = 0
        WHERE keycolumn = 100

    Assuming the table is large, the SELECT statement will acquire a shared table lock. (If there are only a few rows in bigtable, SQL Server will acquire individual row or key locks.) The UPDATE statement will then acquire a single exclusive key lock to do the update of a single row, and the X lock at the key level will mean an IX lock at the page and table level. The table will then show SIX when viewed through sp_lock.

    Table 13-1 shows most of the lock modes, as well as the abbreviations used in the output of sp_lock.

     
    Figure 13-3 Click here.

    Lock Granularity
    SQL Server can lock user data resources (not system resources, which are protected with latches) at the table, page, or row level. SQL Server also locks index keys and ranges of index keys. Figure 13-3 shows the possible lock levels in a table. Note that if the table has a clustered index, the data rows are at the leaf level of the clustered index, and they are locked with key locks instead of row locks.

    The syslockinfo table keeps track of each lock by storing the type of resource locked (such as a row, key, or page), the mode of the lock, and an identifier for the specific resource. When a process requests a lock, SQL Server compares the lock requested to the resources already listed in the syslockinfo table and looks for an exact match on the resource type and identifier. (The lock modes don't have to be the same to yield an exact match.) However, if one process has a row exclusively locked in the authors table, another process might try to get a lock on the entire authors table. Since these are two different resources, SQL Server does not find an exact match unless additional information is already stored in syslockinfo. This is what intent locks are for. The process that has the exclusive lock on a row of the authors table also has an intent exclusive lock on the page containing the row and another intent exclusive lock on the table containing the row. When the second process attempts to acquire the exclusive lock on the table, it finds a conflicting row already in the syslockinfo table on the same lock resource (the authors table).

    Key Locks
    SQL Server 7 supports two kinds of key locks, whose use depends on the isolation level of the current transaction. If the isolation level is Read Committed or Repeatable Read, SQL Server tries to lock the actual index keys accessed while processing the query. With a table that has a clustered index, the data rows are the leaf level of the index, and you will see key locks acquired. If the table is a heap, you might see key locks for the nonclustered indexes and row locks for the actual data.

    If the isolation level is Serializable, the situation is special. We want to prevent phantoms, which means that if we have scanned a range of data within a transaction, we need to lock enough of the table to make sure that no one can insert a value into the range that was scanned. For example, we can issue the following query within an explicit transaction:

    SELECT * FROM employees
    WHERE salary BETWEEN 30000 AND 50000

    Locks must be acquired to make sure that no new rows with salary values between 30000 and 50000 are inserted before the end of the transaction. Earlier versions of SQL Server guaranteed this by locking whole pages or even the entire table. In many cases, however, this was too restrictive—more data was locked than the actual WHERE clause indicated, resulting in unnecessary contention. SQL Server 7 uses key range locks, which are associated with a particular key value in an index and indicate that all values between that key and the previous one in the index are locked.

    Suppose we have an index on the lastname field in the employee table. We are in TRANSACTION ISOLATION LEVEL SERIALIZABLE and we issue this SELECT statement:

    SELECT * 
    FROM members
    WHERE last_name BETWEEN 'Delaney' AND 'DuLaney'

    If Dallas, Donovan, and Duluth are sequential leaf-level index keys in the table, the second two of these (Donovan and Duluth) acquire key range locks (although only one row, for Donovan, is returned in the result set). The keyrange locks prevent any inserts into the ranges ending with the two key range locks. No values greater than Dallas and less than or equal to Donovan can be inserted, and no values greater than Donovan and less than or equal to Duluth can be inserted. Note that the key range locks imply an open interval starting at the previous sequential key and a closed interval ending at the key on which the lock is placed. These two key range locks prevent anyone from inserting either ior Delanie, which are in the range specified in the WHERE clause. However, the key range locks would also prevent anyone from inserting DeLancey (which is greater than Dallas and less than Donovan) even though DeLancey is not in the query's specified range. Key range locks are not perfect, but they do give much greater concurrency than locking whole pages or tables, which was the only possibility in previous SQL Server versions.

    Additional Lock Resources
    Locking is also done on extents—units of disk space that are 64 KB in size (eight pages of 8 KB each). This kind of locking occurs automatically when a table or an index needs to grow and a new extent must be allocated. You can think of an extent lock as another type of special purpose latch, but it does show up in the output of the sp_lock procedure. Extents can have both shared extent and exclusive extent locks.

    When you examine the output of sp_lock, notice that each process holds a lock on at least one database. These are always shared locks and are used by SQL Server for determining when a database is in use. Generally, you don't need to be concerned with extent or database locks, but you might see them if you are running sp_lock or perusing syslockinfo.

    Identifying Lock Resources
    When the lock manager tries to determine whether a requested lock can be granted, it checks the syslockinfo table to determine whether a matching lock with a conflicting lock mode already exists. It compares locks by looking at the database id (dbid), the object id (objid), the type of resource locked, and the description of the specific resource referenced by the lock. The lock manager knows nothing about the meaning of the resource description. It simply compares the strings identifying the lock resources to look for a match. If it finds a match, it knows that resource is already locked; it then uses the lock compatibility matrix to determine whether the current lock is compatible with the one being requested. Table 13-2 shows all the lock resources, the abbreviations used in the output of sp_lock, and the information used to define the actual resource locked.

    Note that key locks and key range locks have identical resource descriptions. When we look at some examples of output from the sp_lock procedure, you'll see that you can distinguish between these types of locks by the value in the lock mode column.

    Lock Duration
    The length of time that a lock is held depends primarily on the mode of the lock and the transaction isolation level in effect. The default isolation level for SQL Server is Read Committed. At this level, shared locks are released as soon as SQL Server has read and processed the locked data. An exclusive lock is held until the end of the transaction, whether it is committed or rolled back. An update lock is held until the end of the transaction unless it has been promoted to an exclusive lock, in which case the exclusive lock remains for the duration of the transaction. If your transaction isolation level is Repeatable Read or Serializable, shared locks have the same duration as exclusive locks. That is, they are not released until the transaction is over.

    In addition to changing your transaction isolation level, you can control the lock duration by using query hints. We'll discuss query hints for locking and for other purposes in Chapter 14.

    Lock Ownership
    Lock duration can also be affected by the lock ownership. There are three types of lock owners: transactions, cursors, and sessions. These are available through the req_ownertype column in the syslockinfo table. (This information is not visible through the sp_lock stored procedure.) A req_ownertype value of 1 indicates that the lock is owned by transaction, and its duration is as discussed above. Most of our locking discussion, in fact, deals with locks owned by a transaction. A cursor lock has a req_ownertype value of 2. If a cursor is opened using a locking mode of SCROLL_LOCKS, a cursor lock is held on every row fetched until the next row is fetched or the cursor is closed. Even if the transaction commits before the next fetch, the cursor lock is not released. Locks owned by a session have a req_ownertype value of 3. A session lock is one taken on behalf of a process that is outside the scope of a transaction. The most common example is a database lock, as discussed earlier. A process acquires a session lock on the database when it issues the USE database command, and that lock isn't released until another USE command is issued or until the process is disconnected.

    Viewing Locks
    To see the locks currently outstanding in the system as well as those that are being waited for, examine the syslockinfo system table or execute the system stored procedure sp_lock. (The syslockinfo table is not really a system table. It is not maintained on disk because locks are not maintained on disk. Rather, it is materialized in table format based on the lock manager's current accounting of locks each time syslockinfo is queried.) Another way to watch locking activity is with the excellent graphical representation of locking status provided by SQL Server Enterprise Manager. (Even those of you who think GUIs are for wimps can appreciate SQL Server Enterprise Manager's view of locking.)

    The following examples show what each of the lock types and modes discussed earlier look like when reported by the sp_lock procedure. Note that the call to the sp_lock procedure is preceded by the keyword EXECUTE, which is required when the call to a stored procedure is not the first thing in a batch. Note also that the sp_lock procedure is given an argument of @@spid, which refers to the process ID of the current process (the server process ID). We don't want to see all the locks in the system, only those held by our process.

    Example 1: SELECT with Default Isolation Level

    SQL BATCH

    USE PUBS
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    BEGIN TRAN
    SELECT * FROM authors
    WHERE au_lname = 'Ringer'
    EXEC sp_lock @@spid
    COMMIT TRAN
    OUTPUT OF sp_lock
    spid   dbid   ObjId       IndId  Type Resource         Mode     Status 
    ------ ------ ----------- ------ ---- ---------------- -------- ----- 
    11     1      0           0      DB                    S        GRANT
    11     2      0           0      DB                    S        GRANT
    11     5      0           0      DB                    S        GRANT
    11     1      117575457   0      TAB                   IS       GRANT

    Every time we run the sp_lock procedure, we acquire locks in the master database to generate the output to be displayed. If you look at the dbid column in the output, you'll see locks in database 1 (master) and database 2 (tempdb). We won't show those locks in the output for the rest of the examples. If we ignore the locks in master and tempdb, we'll see that the only lock in the pubs database is the session-level database lock. No locks on the authors table are held at this point because the batch was doing only select operations that acquired shared locks. By default, the shared locks are released as soon as the data has been read, so by the time sp_lock is executed, the locks are no longer held.

    Example 2: SELECT with Repeatable Read Isolation Level
    SQL BATCH

    USE PUBS
    SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
    BEGIN TRAN
    SELECT * FROM authors
    WHERE au_lname = 'Ringer'
    EXEC sp_lock @@spid
    COMMIT TRAN
    OUTPUT OF sp_lock
    spid   dbid   ObjId       IndId  Type Resource         Mode     Status 
    ------ ------ ----------- ------ ---- ---------------- -------- ------
    11     5      117575457   2      PAG  1:123            IS       GRANT
    11     5      117575457   1      PAG  1:96             IS       GRANT
    11     5      0           0      DB                    S        GRANT
    11     5      117575457   2      KEY  (d5f329a7dcdc)   S        GRANT
    11     5      117575457   2      KEY  (4c62318cf11f)   S        GRANT
    11     5      117575457   1      KEY  (3dc1b1ecb5be)   S        GRANT
    11     5      117575457   1      KEY  (37fdb5efbcbe)   S        GRANT
    11     5      117575457   0      TAB                   IS       GRANT

    Because the authors table has a clustered index, the rows of data are all index rows in the leaf level. The locks on the individual rows are marked as key locks instead of row locks. There are also key locks at the leaf level of the nonclustered index on the table. You can tell the two indexes apart by the value in the IndId field: the data rows have an IndId value of 1, and the nonclustered index rows have an IndId value of 2. Because the transaction isolation level is Repeatable Read, the shared locks are held until the transaction is finished. Note that the two rows and two index rows have shared (S) locks, and the data and index pages, as well as the table itself, have intent shared (IS) locks.

    Example 3: SELECT with Serializable Isolation Level
    SQL BATCH

    USE PUBS
    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
    BEGIN TRAN
    SELECT * FROM authors
    WHERE au_lname = 'Ringer'
    EXEC sp_lock @@spid
    COMMIT TRAN
    OUTPUT OF sp_lock
    spid   dbid   ObjId       IndId  Type Resource         Mode     Status 
    ------ ------ ----------- ------ ---- ---------------- -------- ------
    11     5      117575457   2      PAG  1:123            IS       GRANT
    11     5      117575457   1      PAG  1:96             IS       GRANT
    11     5      0           0      DB                    S        GRANT
    11     5      117575457   2      KEY  (d5f329a7dcdc)   IS-S     GRANT
    11     5      117575457   2      KEY  (4c62318cf11f)   IS-S     GRANT
    11     5      117575457   1      KEY  (3dc1b1ecb5be)   IS-S     GRANT
    11     5      117575457   1      KEY  (37fdb5efbcbe)   IS-S     GRANT
    11     5      117575457   2      KEY  (d5968ed3b619)   IS-S     GRANT
    11     5      117575457   0      TAB                   IS       GRANT

    The locks held with the Serializable isolation level are almost identical to those held with the Repeatable Read isolation level. The main difference is in the mode of the lock. The two-part mode IS-S indicates a key range lock in addition to the lock on the key itself. The first part (IS) is the lock on the range of keys between (and including) the key holding the lock and the previous key in the index. The key range locks prevent other transactions from inserting new rows into the table that meet the condition of this query; that is, no new rows with a last name of Ringer can be inserted. There are three key locks in the nonclustered index on au_lname (IndId = 2) because three different ranges need to be locked. SQL Server must lock the range from the key preceding the first Ringer in the table up to the first Ringer, it must lock the range between the two instances of Ringer, and it must lock the range from the second Ringer to the next key in the index. (So actually nothing between Ringer and the previous key, Panteley, and nothing between Ringer and the next key, Smith, could be inserted into the table. For example, we could not insert an author with the last name Pattin or Singh.)

    Example 4: Update Operations
    SQL BATCH

    USE PUBS
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    BEGIN TRAN
    UPDATE authors
    SET contract = 0
    WHERE au_lname = 'Ringer'
    EXEC sp_lock @@spid
    COMMIT TRAN
    OUTPUT OF sp_lock
    spid   dbid   ObjId       IndId  Type Resource         Mode     Status 
    ------ ------ ----------- ------ ---- ---------------- -------- ------
    11     5      117575457   1      PAG  1:96             IX       GRANT
    11     5      0           0      DB                    S        GRANT
    11     5      117575457   1      KEY  (3dc1b1ecb5be)   X        GRANT
    11     5      117575457   1      KEY  (37fdb5efbcbe)   X        GRANT
    11     5      117575457   0      TAB                   IX       GRANT

    The two rows in the leaf level of the clustered index are locked with X locks. The page and the table are then locked with IX locks. We discussed earlier that SQL Server actually acquires update locks while it looks for the rows to update. However, these are escalated to X locks when the actual update is done, and by the time the sp_lock procedure is run, the update locks are gone. Unless you actually force update locks with a query hint, you might never see them in the output of sp_lock.

    Example 5: Creating a Table
    SQL BATCH

    USE PUBS
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    BEGIN TRAN
    SELECT * 
    INTO newTitles
    FROM titles
    WHERE price < 5
    EXEC sp_lock @@spid
    COMMIT TRAN
    OUTPUT OF sp_lock
    spid   dbid   ObjId       IndId  Type Resource         Mode     Status 
    ------ ------ ----------- ------ ---- ---------------- -------- ------
    7      5      0           0      DB   [BULK-OP-LOG]    S        GRANT
    7      5      0           0      DB                    S        GRANT
    7      5      0           0      DB   [BULK-OP-DB]     S        GRANT
    7      5      1           0      TAB                   IX       GRANT
    7      5      3           0      TAB                   IX       GRANT
    7      5      2           0      TAB                   IX       GRANT
    7      5      0           0      PAG  1:199            X        GRANT
    7      5      0           0      PAG  1:198            X        GRANT
    7      5      0           0      PAG  1:197            X        GRANT
    7      5      0           0      PAG  1:196            X        GRANT
    7      5      0           0      PAG  1:195            X        GRANT
    7      5      0           0      PAG  1:194            X        GRANT
    7      5      0           0      PAG  1:193            X        GRANT
    7      5      0           0      EXT  1:192            X        GRANT
    7      5      0           0      PAG  1:166            X        GRANT
    7      5      0           0      PAG  1:165            X        GRANT
    7      5      3           2      KEY  (0693cc100b75)   X        GRANT
    7      5      3           2      KEY  (3790f0cf7f55)   X        GRANT
    7      5      2           1      KEY  (0257f4438466)   X        GRANT
    7      5      3           2      KEY  (242ad7320fa2)   X        GRANT
    7      5      3           2      KEY  (e794778e397a)   X        GRANT
    7      5      1061578820  0      TAB                   Sch-M    GRANT
    7      5      1           2      KEY  (269dc7159f33)   X        GRANT
    7      5      3           1      KEY  (024e7788d08c)   X        GRANT
    7      5      3           1      KEY  (02567f88d08c)   X        GRANT
    (etc.)

    Very few of these locks are actually acquired on elements of the new table. In the ObjId column, notice that most of the objects have an ID of less than 100, which means that they are system tables. As the new newTitles table is built, SQL Server acquires locks on sysobjects and syscolumns to record information about this new table. Also notice the schema modification (Sch-M) lock on the new table as well as extent (EXT) locks. While the table is built, the extents are not marked as belonging to the table; you can see that the ObjId is 0. In the output above, the extent ID is shown as 1:192. This means that page 192 in file 1 is the first page of the extent. You can also see that the subsequent seven pages (193-199) in this extent are all exclusively locked while the table is being created.

    Example 6: Row Locks
    SQL BATCH

    USE PUBS
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED
    BEGIN TRAN
    UPDATE newTitles
    SET price = 3.99
    WHERE type = 'business'
    EXEC sp_lock @@spid
    ROLLBACK TRAN
    OUTPUT OF sp_lock
    spid   dbid   ObjId       IndId  Type Resource         Mode     Status 
    ------ ------ ----------- ------ ---- ---------------- -------- ------
    13     1      0           0      DB                    S        GRANT
    13     2      0           0      DB                    S        GRANT
    13     5      0           0      DB                    S        GRANT
    13     5      1077578877  0      RID  1:184:0          X        GRANT
    13     5      1077578877  0      PAG  1:184            IX       GRANT
    13     5      1077578877  0      TAB                   IX       GRANT
    13     1      117575457   0      TAB                   IS       GRANT

    There are no indexes on the newTitles table, so the lock on the actual row meeting our criterion is an exclusive (X) lock on the row (RID). As expected, IX locks are taken on the page and the table.

    Lock Compatibility
    Two locks are compatible if one lock can be granted while another lock on the same object by a different process is outstanding. On the other hand, if a lock requested for an object is not compatible with a lock currently being held, the requesting connection must wait for the lock. For example, if a shared page lock exists on a page, another process requesting a shared page lock for the same page is granted the lock because the two lock types are compatible. But a process that requests an exclusive lock for the same page is not granted the lock because an exclusive lock is not compatible with the shared lock already held. Table 13-3 summarizes the compatibility of locks in SQL Server.

    Lock compatibility comes into play between locks on different resources, such as table locks and page locks. A table and a page obviously represent an implicit hierarchy since a table is made up of multiple pages. If an exclusive page lock is held on one page of a table, another process cannot get even a shared table lock for that table. This hierarchy is protected using intent locks. A process acquiring an exclusive page lock, update page lock, or intent exclusive page lock first acquires an intent exclusive lock on the table. This intent exclusive table lock prevents another process from acquiring the shared table lock on that table. (Remember that intent exclusive and shared locks are not compatible.)

    Similarly, a process acquiring a shared row lock must first acquire an intent shared lock for the table, which prevents another process from acquiring an exclusive table lock. Or if the exclusive table lock already exists, the intent shared lock is not granted and the shared page lock has to wait until the exclusive table lock is released. Without intent locks, process A can lock a page in a table with an exclusive page lock and process B can place an exclusive table lock on the same table and hence think that it has a right to modify the entire table, including the page that process A has exclusively locked.

    NOTE   At the risk of stating the obvious, lock compatibility is an issue only when the locks affect the same object. For example, two or more processes can each hold exclusive page locks simultaneously as long as the locks are on different pages or different tables.

    Even if two locks are compatible, the requester of the second lock might still have to wait if an incompatible lock is waiting. For example, suppose that process A holds a shared page lock. Process B requests an exclusive page lock and must wait because the shared page lock and the exclusive page lock are not compatible. Process C requests a shared page lock that is compatible with the shared page already outstanding to process A. However, the shared page lock cannot be immediately granted. Process C must wait for its shared page lock because process B is ahead of it in the lock queue with a request (exclusive page) that is not compatible.

    By examining the compatibility of locks not only to those processes granted but also to those processes waiting, SQL Server prevents lock starvation, which occurs when requests for shared locks keep overlapping so that the request for the exclusive lock can never be granted.

    Bound Connections
    Remember that the issue of lock contention applies only between different processes (also called connections). A process holding locks on a resource does not lock itself from the resource-only other processes are prevented access. But any other process (or connection to SQL Server) can actually execute as the same application and user. It is common for applications to have more than one connection to SQL Server. Every such connection is treated as an entirely different SQL Server process, and by default no sharing of the "lock space" occurs between connections, even if they belong to the same user and the same application. (Again, in this context, "process" means a SQL Server subtask, not a Windows NT process.)

    However, two different connections can share a lock space and hence not lock each other out. This capability is known as a bound connection. With a bound connection, the first connection asks SQL Server to give out its bind token. The bind token is passed by the application (using a client-side global variable, a shared memory, or another method) for use in subsequent connections. The bind token acts as a "magic cookie" so that those other connections can share the lock space of the original connection. Locks held by bound connections do not lock each other. (The sp_getbindtoken and sp_bindsession system stored procedures get and use the bind token.)

    Bound connections are especially useful if you are writing an extended stored procedure, which is a function written in your own DLL, and that extended stored procedure needs to call back into the database to do some work. Without a bound connection, the extended stored procedure collides with its own calling process's locks. When multiple processes share a lock space and a transaction space by using bound connections, a COMMIT or ROLLBACK affects all the participating connections.

    Here's an example of using bound connections between two different windows in SQL Server's Query Analyzer. Since we don't have a controlling application to declare and store the bind token in a client-side variable, we have to actually copy it from the first session and paste it into the second. So, in your first query window, you execute this batch:

    DECLARE @token varchar(30)
    EXEC sp_getbindtoken @token
    SELECT @token
    GO

    This should return something like the following:

    ---------------------------------------- 
    dPe---5---.?j0U<_WP?1HMK-3/D8;@1

    Normally, you wouldn't have to look at this messy string; your application would just store it and pass it on without your ever having to see it. But for a quick example using the Query Analyzer, it's necessary to actually see the value. You use your keyboard or mouse to select the token string that you received and use it in the following batch in a second Query Analyzer window:

    EXEC sp_bindsession 'dPe---5---.?j0U<_WP?1HMK-3/D8;@1'
    GO

    Now go back to the first query window and begin a transaction that locks some data. You can use something like this:

    USE pubs
    BEGIN TRAN
    UPDATE titles
    SET price = $100
    GO

    This should exclusively lock every row in the titles table. Now go to the second query window and select from the locked table:

    SELECT title_id, price FROM titles
    GO

    You should be able to see all the $100 prices in the titles table, just as if you were part of the same connection as the first query. Besides sharing lock space, the bound connection also shares transaction space. You can execute a ROLLBACK TRAN in the second window even though the first one began the transaction. If the first connection tries to then issue a ROLLBACK TRAN, it gets this message:

    Server: Msg 3903, Level 16, State 1, Line 1
    The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.

    Row-Level vs. Page-Level Locking
    The debate over whether row-level locking is better than page-level locking or vice versa has been one of those near-religious wars and warrants a few comments here. Although some people would have you believe that one is always better, it's not really that simple.

    Prior to SQL Server version 7, the smallest unit of data that SQL Server could lock was a page. Even though many people argued that this was unacceptable and it was impossible to maintain good concurrency while locking entire pages, many large and powerful applications were written and deployed using only page-level locking. If they were well designed and tuned, concurrency was not an issue, and some of these applications supported hundreds of active user connections with acceptable response times and throughput. However, with the change in page size from 2 KB to 8 KB for SQL Server 7, the issue has become more critical. Locking an entire page means locking four times as much data as in previous versions. SQL Server 7 implements full row-level locking, so any potential problems due to lower concurrency with the larger page size should not be an issue. However, locking isn't free. Considerable resources are required to manage locks. Recall that a lock is an in-memory structure of about 32 bytes, with another 32 bytes for each process holding the lock and each process waiting for the lock. If you need a lock for every row and you scan a million rows, you need more than 30 MB of RAM just to hold locks for that one process.

    Beyond the memory consumption issues, locking is a fairly processing-intensive operation. Managing locks requires substantial bookkeeping. (Recall that, internally, SQL Server uses a lightweight mutex called a spinlock to guard resources, and it uses latches-also lighter than full-blown locks-to protect non-leaf-level index pages. These performance optimizations avoid the overhead of full locking.) If a page of data contains 50 rows of data, all of which will be used, it is obviously more efficient to issue and manage one lock on the page than to manage 50. That's the obvious benefit of page locking-a reduction in the number of lock structures that must exist and be managed.

    If two different processes each need to update a few separate rows of data and some of the rows needed by each process happen to exist on the same page, one process must wait until the page locks of the other process are released. If, in this case, you use row-level locking instead of page-level locking, the other process does not have to wait. The finer granularity of the locks means that no conflict occurs in the first place because each process is concerned with different rows. That's the obvious benefit of row-level locking. Which of these obvious benefits wins? Well, the decision isn't clear cut, and it depends on the application and the data. Each type of locking can be shown to be superior for different types of applications and usage.

    The stored procedure sp_indexoption lets you manually control the unit of locking within an index. It also lets you disallow page locks or row locks within an index. Since these options are available only for indexes, there is no way to control the locking within the data pages of a heap. (But remember that if a table has a clustered index, the data pages are part of the index and are affected by the sp_indexoption setting.) The index options are set for each table or index individually. Two options, AllowRowLocks and AllowPageLocks, are both set to TRUE initially for every table and index. If both of these options are set to FALSE for a table, only full table locks are allowed.

    Lock Escalation
    SQL Server automatically escalates row, key, or page locks to coarser table locks as appropriate. This escalation protects system resources-it prevents the system from using too much memory for keeping track of locks-and increases efficiency. For example, after a query acquires many row locks, the lock level can be escalated to a table lock. If every row in a table must be visited, it probably makes more sense to acquire and hold a single table lock than to hold many row locks. A single table lock is acquired and the many row locks are released. This escalation to a table lock reduces locking overhead and keeps the system from running out of locks. Recall that a lock is a memory structure. Because there is a finite amount of memory, there must be a finite number of locks. You can configure SQL Server for the maximum number of locks your system is expected to need, but the default is for the locks configuration parameter to be tuned automatically by SQL Server.

    As mentioned earlier, SQL Server determines at runtime whether to lock rows, pages, or the entire table. The locking of rows (or keys) is heavily favored if at all possible. The type of locking chosen is based on the number of rows and pages to be scanned, the number of rows on a page, the isolation level in effect, the update activity going on, the number of users on the system needing memory for their own purposes, and so on. Perhaps the best of both worlds would be to "deescalate" locks-that is, to start at a coarser level of locking and change to more granular locks if and only if there is a conflict for the coarse lock.

    For example, suppose you have two processes that each update a few rows. Process A might start with page locking. If no other process requests a lock of the same page, no contention occurs, so the additional overhead of doing locking for every row does not happen. But if process B asks for a lock on one of the pages locked by process A, those held locks can be deescalated to row locks for only the rows that are actually needed. After failing to obtain the page-level lock, process B might then try to get a lock on only the specific rows it needs rather than on the entire page. If the rows needed are not the same between the processes, there is no contention and each gets its own row locks. But if there is no contention for rows, the reduced overhead of locking only at the page level can be realized. You can begin the locking at an even coarser level, such as at the table level, and deescalate as needed to a page, and then further to a row, and conceptually even further to perhaps a column level, although this might be stretching the model.

    A deescalation locking strategy is being considered for future releases. As of version 7, SQL Server uses predominantly row-level locking. As mentioned, sometimes page locks or a table lock are chosen initially, or if too many individual row locks are acquired, SQL Server dynamically escalates those to a table lock. In version 7, lock escalation converts many individual row or page locks to a table lock; escalation never converts row locks to page locks. Lock deescalation is not implemented.

    Locking Hints and Trace Flags
    Just as you can specify hints on queries to direct the optimizer to choose a certain index or strategy in its query plan, you can specify hints for locking. For example, if you know that your query will scan so many rows that its page locks will escalate to table locks, you can direct the query to use table locks in the first place, which is more efficient.

    We'll look at locking hints and locking contention issues in detail in Chapter 14. For now, armed with the knowledge of locking you've gained here, you can observe the locking activity of your system to understand how and when locks occur. Trace flag 1204 provides detailed information about deadlocks, and this information can help you understand why locks are occurring and how to change the order of access to objects to reduce them. Trace flag 1200 provides detailed locking information as every request for a lock is made. (But its output is voluminous.) You'll see details about these trace flags in Chapter 14.

    Summary
    SQL Server lets you manage multiple users simultaneously and ensure that transactions observe the properties of the chosen isolation level. Locking guards data and the internal resources that make it possible for a multiple-user system to operate like a single-user system. In this chapter, we looked at the locking mechanisms in SQL Server, including full locking for data and leaf-level index pages and lightweight locking mechanisms for internally used resources.

    We also looked at the types and modes of locks as well as lock compatibility and lock escalation. It is important to understand the issues of lock compatibility if you want to design and implement high-concurrency applications. We also looked at deadlocks and discussed ways to avoid them.

    BUY NOW!


    From Inside Microsoft SQL Server 7.0 by Kalen Delaney. Reproduced by permission of Microsoft Press. All rights reserved.

    Sponsored Links
    NEW Oracle9i Application Server JDeveloper now available!
    Nokia Announces NEW Java-Enabled Phones
    SPECIAL REPORT: Winning with Web Services
    Take the New Worlds Challenge and enter to WIN a Harley-Davidson V-Rod!
    Building Mobile Game Apps for Fun & Profit

    Special Report: Winning with Web Services   Special Report: Winning with Web Services

    DevX Home | .NET Zone | Java Zone | Get Help
    VB Zone | C++ Zone | XML Zone | Enterprise Zone | Database Dev Zone
    Wireless Zone| Security Zone | ASP Zone | DHTML Zone | UML Zone
    MarketPlace | RFP Exchange | Discussions | Newsletters | Tech Tips | Sourcebank
    Advertise | Help | Copyright | Privacy Statement