CHAPTER 11

image

Coding for Concurrency

“It has been my observation that most people get ahead during the time that others waste.”

—Henry Ford

Concurrency is all about having the computer utilize all of its resources simultaneously, or basically having more than one thing done at the same time when serving multiple users (technically, in SQL Server, you open multiple requests, on one or more connections). Even if you haven’t done much with multiple users, if you know anything about computing you probably are familiar with the term multitasking. The key here is that when multiple processes or users are accessing the same resources, each user expects to see a consistent view of the data and certainly expects that other users will not be stomping on his or her results.

The topics of this chapter will center on understanding why and how you should write your database code or design your objects to make them accessible concurrently by as many users as you have in your system. In this chapter, I’ll discuss the following:

  • OS and hardware concerns: I’ll briefly discuss various issues that are out of the control of SQL code but can affect concurrency.
  • Transactions: I’ll give an overview of how transactions work and how to start and stop them in T-SQL code.
  • SQL Server concurrency controls: In this section, I’ll explain locks and isolation levels that allow you to customize how isolated processes are from one another
  • Coding for concurrency: I’ll discuss methods of coding data access to protect from users simultaneously making changes to data and placing data into less-than-adequate situations. You’ll also learn how to deal with users stepping on one another and how to maximize concurrency.

The key goal of this chapter is to acquaint you with many of the kinds of things SQL Server does to make it fast and safe to have multiple users doing the same sorts of tasks with the same resources and how you can optimize your code to make it easier for this to happen.

Resource Governor

SQL Server 2008 had a new feature that is concurrency related (especially as it relates to performance tuning), though it is more of a management tool than a design concern. The feature is called Resource Governor, and it allows you to partition the workload of the entire server by specifying maximum and minimum resource allocations (memory, CPU, concurrent requests, etc.) to users or groups of users. You can classify users into groups using a simple user-defined function that, in turn, takes advantage of the basic server-level functions you have for identifying users and applications (IS_SRVROLEMEMBER, APP_NAME, SYSTEM_USER, etc.). Like many of the high-end features of SQL Server, Resource Governor is only available with the Enterprise Edition and higher editions.

Using Resource Governor , you can group together and limit the users of a reporting application, of Management Studio, or of any other application to a specific percentage of the CPU, a certain percentage and number of processors, and limited requests at one time. In SQL Server 2012, I/O limits are still not available in Resource Governor.

One nice thing about Resource Governor is that some settings can only apply when the server is under a load. So if the reporting user is the only active process, that user might get the entire server’s power. But if the server is being heavily used, users would be limited to the configured amounts. I won’t talk about Resource Governor anymore in this chapter, but it is definitely a feature that you might want to consider if you are dealing with different types of users in your applications.

What Is Concurrency?

The concept of concurrency can be boiled down to the following statement:

Maximize the amount of work that can be done by all users at the same time, and most importantly, make all users feel like they’re important.

Because of the need to balance the amount of work with the user’s perception of the amount of work being done, there are going to be the following tradeoffs:

  • Number of concurrent users: How many users can (or need to) be served at the same time?
  • Overhead: How complex are the algorithms to maintain concurrency?
  • Accuracy: How correct must the results be?
  • Performance: How fast does each process finish?
  • Cost: How much are you willing to spend on hardware and programming time?

As you can probably guess, if all the users of a database system never needed to run queries at the same time, life in database-system–design land would be far simpler. You would have no need to be concerned with what other users might want to do. The only real performance goal would be to run one process really fast and move to the next process. If no one ever shared resources, multitasking server operating systems would be unnecessary. All files could be placed on a user’s local computer, and that would be enough. And if we could single-thread all activities on a server, more work might be done, but just like the old days, people would sit around waiting for their turns (yes, with mainframes, people actually did that sort of thing). Internally, the situation is still technically the same in a way, as a computer cannot process more individual instructions than it has cores in its CPUs, but it can run and swap around fast enough to make hundreds or thousands of people feel like they are the only users. This is especially true if your system engineer builds computers that are good as SQL Server machines (and not just file servers) and the architects/programmers build systems that meet the requirements for a relational database (and not just what seems expedient at the time).

A common scenario for a multiuser database involves a sales and shipping application. You might have 50 salespeople in a call center trying to sell the last 25 closeout items that are in stock. It isn’t desirable to promise the last physical item accidentally to multiple customers, since two users might happen to read that it was available at the same time and both be allowed to place an order for it. In this case, stopping the first order wouldn’t be necessary, but you would want to disallow or otherwise prevent the second (or subsequent) orders from being placed, since they cannot be fulfilled immediately.

Most programmers instinctively write code to check for this condition and to try to make sure that this sort of thing doesn’t happen. Code is generally written that does something along these lines:

  • Check to make sure that there’s adequate stock.
  • Create a shipping row to allocate the product to the customer.

That’s simple enough, but what if one person checks to see if the product is available at the same time as another, and more orders are placed than you have adequate stock for? This is a far more common possibility than you might imagine. Is this acceptable? If you’ve ever ordered a product that you were promised in two days and then found out your items are on backorder for a month, you know the answer to this question: “No! It is very unacceptable.” When this happens, you try another retailer next time, right?

I should also note that the problems presented by concurrency aren’t quite the same as those for parallelism, which is having one task split up and done by multiple resources at the same time. Parallelism involves a whole different set of problems and luckily is more or less not your problem. In writing SQL Server code, parallelism is done automatically, as tasks can be split among resources (sometimes, you will need to adjust just how many parallel operations can take place, but in practice, SQL Server does most of that work for you). When I refer to concurrency, I generally mean having multiple different operations happening at the same time by different connections to SQL Server. Here are just a few of the questions you have to ask yourself:

  • What effect will there be if a query modifies rows that have already been used by a query in a different batch?
  • What if the other query creates new rows that would have been important to the other batch’s query? What if the other query deletes others?
  • Most importantly, can one query corrupt another’s results?

You must consider a few more questions as well. Just how important is concurrency to you, and how much are you willing to pay in performance? The whole topic of concurrency is basically a set of tradeoffs between performance, consistency, and the number of simultaneous users.

image  Tip Starting with SQL Server 2005, a new way to execute multiple batches of SQL code from the same connection simultaneously was added; it is known as Multiple Active Result Sets (MARS) . It allows interleaved execution of several statements, such as SELECT, FETCH, RECEIVE READTEXT, or BULK INSERT. As the product continues to mature, you will start to see the term “request” being used in the place where we commonly thought of connection in SQL Server 2000 and earlier. Admittedly, this is still a hard change that has not yet become embedded in people’s thought processes, but in some places (like in the Dynamic Management Views), you need to understand the difference.

MARS is principally a client technology and must be enabled by a connection, but it can change some of the ways that SQL Server handles concurrency. I’ll note places where MARS affects the fundamentals of concurrency.

OS and Hardware Concerns

SQL Server is designed to run on a variety of hardware types. Essentially the same basic code runs on a low-end netbook and on a clustered array of servers that rivals many supercomputers. Every machine running a version of SQL Server, from Express to Enterprise Edition, can have a vastly different concurrency profile. Each edition will also be able to support different amounts of hardware: Express supports 1GB of RAM and one processor socket (with up to 4 cores, which is still more than our first SQL Server that had 16MB of RAM), and at the other end of the spectrum, the Enterprise Edition can handle as much hardware as a manufacturer can stuff into one box. Additionally, a specialized version called the Parallel Data Warehouse edition is built specifically for data warehousing loads. The fact is that, in every version, many of the very same concerns exist concerning how SQL Server handles multiple users using the same resources seemingly simultaneously. Fast-forward to the future (i.e., now), and the Azure platform allows you to access your data in the cloud on massive computer systems from anywhere. In this section, I’ll briefly touch on some of the issues governing concurrency that our T-SQL code needn’t be concerned with, because concurrency is part of the environment we work in.

SQL Server and the OS balance all the different requests and needs for multiple users. It’s beyond the scope of this book to delve too deeply into the details, but it’s important to mention that concurrency is heavily tied to hardware architecture. For example, consider the following subsystems:

  • Processor: The heart of the system is the CPU. It controls the other subsystems, as well as doing any calculations needed. If you have too few processors, excessive time can be wasted switching between requests.
  • Disk subsystem: Disk is always the slowest part of the system (even with solid state drives becoming more and more prevalent). A slow disk subsystem is the downfall of many systems, particularly because of the expense involved. Each drive can read only one piece of information at a time, so to access disks concurrently, it’s necessary to have multiple disk drives, and even multiple controllers or channels to disk drive arrays. Especially important is the choice between RAID systems, which take multiple disks and configure them for performance and redundancy:
    • 0: Striping across all disks with no redundancy, performance only.
    • 1: Mirroring between two disks, redundancy only.
      • 5: Striping with distributed parity; excellent for reading, but can be slow for writing. Not typically suggested for most SQL Server OLTP usage, though it isn’t horrible for lighter loads.
      • 0+1: Mirrored stripes. Two RAID 1 arrays, mirrored. Great for performance, but not tremendously redundant.
      • 1+0 (also known as 10): Striped mirrors. Some number of RAID 0 mirrored arrays, then striped across the mirrors. Usually, the best mix of performance and redundancy for an OLTP SQL Server installation.
  • Network interface: Bandwidth to the users is critical but is usually less of a problem than disk access. However, it’s important to attempt to limit the number of round trips between the server and the client. This is highly dependent on whether the client is connecting over a dialup connection or a gigabit Ethernet (or even multiple network interface cards). Turning on SET NOCOUNT in all connections and coded objects, such as stored procedures and triggers, is a good first step, because otherwise, a message is sent to the client for each query executed, requiring bandwidth (and processing) to deal with them.
  • Memory: One of the cheapest commodities that you can improve substantially on a computer is memory. SQL Server 2012 can use a tremendous amount of memory within the limits of the edition you used (and the amount of RAM will not affect your licensing costs like processor cores either.)

Each of these subsystems needs to be in balance to work properly. You could theoretically have 100 CPUs and 128GB of RAM, and your system could still be slow. In this case, a slow disk subsystem could be causing your issues. The goal is to maximize utilization of all subsystems—the faster the better—but it’s useless to have super-fast CPUs with a super-slow disk subsystem. Ideally, as your load increases, disk, CPU, and memory usage would increase proportionally, though this is a heck of a hard thing to do. The bottom line is that the number of CPUs, disk channels, disk drives, and network cards and the amount of RAM you have all affect concurrency.

Monitoring hardware and OS performance issues is a job primarily for perfmon and/or the data collector. Watching counters for CPU, memory, SQL Server, and so on lets you see the balance among all the different subsystems. In the end, poor hardware configuration can kill you just as quickly as poor SQL Server implementation.

For the rest of this chapter, I’m going to ignore these types of issues and leave them to others with a deeper hardware focus, such as the MSDN web site ( http://msdn.microsoft.com ) or great blogs like Glenn Berry’s ( http://sqlserverperformance.wordpress.com ). I’ll be focusing on design- and coding-related issues pertaining to how to code better SQL to manage concurrency between SQL Server processes.

Transactions

No discussion of concurrency can really have much meaning without an understanding of the transaction. Transactions are a mechanism that allows one or more statements to be guaranteed either to be fully completed or to fail totally. It is an internal SQL Server mechanism that is used to keep the data that’s written to and read from tables consistent throughout a batch, as required by the user.

Whenever data is modified in the database, the changes are not written to the physical data files, but first to a page in RAM and then a log of every change is written to the transaction log immediately before the change is registered as complete. (Any log files need to be on a very fast disk drive subsystem for this reason). Later, the physical table structure is written to when the system is able to do the write (during what is called a checkpoint ). Understanding the process of how modifications to data are made is essential, because while tuning your overall system, you have to be cognizant that every modification operation is logged when considering how large to make your transaction log.

The purpose of transactions is to provide a mechanism to allow multiple processes access to the same data simultaneously, while ensuring that logical operations are either carried out entirely or not at all. To explain the concurrency issues that transactions help with, there’s a common acronym: ACID. It stands for the following:

  • Atomicity: Every operation within a transaction is treated as a singular operation; either all its data modifications are performed, or none of them is performed.
  • Consistency: Once a transaction is completed, the system must be left in a consistent state. This means that all the constraints on the data that are part of the RDBMS definition must be honored.
  • Isolation: This means that the operations within a transaction must be suitably isolated from other transactions. In other words, no other transactions ought to see data in an intermediate state, within the transaction, until it’s finalized. This is typically done by using locks (for details on locks, refer to the section “SQL Server Concurrency Controls” later in this chapter).
  • Durability: Once a transaction is completed (committed), all changes must be persisted as requested. The modifications should persist even in the event of a system failure.

Transactions are used in two different ways. The first way is to provide for isolation between processes. Every DML and DDL statement, including INSERT, UPDATE, DELETE, CREATE TABLE, ALTER TABLE, CREATE INDEX, and even SELECT statements, that is executed in SQL Server is run within a transaction. If you are in the middle of adding a column to the table, you don’t want another user to try to modify data in the table at the same time. For DDL and modification statements, such as INSERT, UPDATE, and DELETE, locks are placed, and all system changes are recorded in the transaction log. If any operation fails, or if the user asks for an operation to be undone, SQL Server uses the transaction log to undo the operations already performed. For SELECT operations (and during the selection of rows to modify/remove from a table), locks will also be used to ensure that data isn’t changed as it is being read.

Second, the programmer can use transaction commands to batch together multiple commands into one logical unit of work. For example, if you write data to one table successfully, and then try unsuccessfully to write to another table, the initial writes can be undone. This section will mostly be about defining and demonstrating this syntax.

The key to using transactions is that, when writing statements to modify data using one or more SQL statements, you need to make use of transactions to ensure that data is written safely and securely. A typical problem with procedures and operations in T-SQL code is underusing transactions, so that when unexpected errors (such as security problems, constraint failures, and hardware glitches) occur, orphaned or inconsistent data is the result. And when, a few weeks later, users are complaining about inconsistent results, you have to track down the issues; you lose some sleep; and your client loses confidence in the system you have created and more importantly loses confidence in you.

How long the log is stored is based on the recovery model under which your database is operating. There are three models:

  • Simple : The log is maintained only until the operation is executed and a checkpoint is executed (by SQL Server automatically or manually). A checkpoint operation makes certain that the data has been written to the data files, so it is permanently stored.
  • Full : The log is maintained until you explicitly clear it out.
  • Bulk logged : This keeps a log much like the full recovery model but doesn’t fully log some operations, such as SELECT INTO, bulk loads, index creations, or text operations. It just logs that the operation has taken place. When you back up the log, it will back up extents that were added during BULK operations, so you get full protection with quicker bulk operations.

Even in the simple model, you must be careful about log space, because if large numbers of changes are made in a single transaction or very rapidly, the log rows must be stored at least until all transactions are committed and a checkpoint takes place. This is clearly just a taste of transaction log management; for a more complete explanation, please see SQL Server 2012 Books Online.

Transaction Syntax

The syntax to start and stop transactions is pretty simple. I’ll cover four variants of the transaction syntax in this section:

  • Basic transactions: The syntax of how to start and complete a transaction
  • Nested transactions: How transactions are affected when one is started when another is already executing
  • Savepoints: Used to selectively cancel part of a transaction
  • Distributed transactions: Using transactions to control saving data on multiple SQL Servers

In the final part of this section, I’ll also cover explicit versus implicit transactions. These sections will give you the foundation needed to move ahead and start building proper code, ensuring that each modification is done properly, even when multiple SQL statements are necessary to form a single-user operation.

Basic Transactions

In transactions’ basic form, three commands are required: BEGIN TRANSACTION (to start the transaction), -COMMIT TRANSACTION (to save the data), and ROLLBACK TRANSACTION (to undo the changes that were made). It’s as simple as that.

For example, consider the case of building a stored procedure to modify two tables. Call these tables table1 and table2. You’ll modify table1, check the error status, and then modify table2 (these aren’t real tables, just syntax examples):

 BEGIN TRY

    BEGIN TRANSACTION;

       UPDATE table1

       SET value = 'value';

       UPDATE table2

       SET value = 'value';

    COMMIT TRANSACTION;

 END TRY

 BEGIN CATCH

       ROLLBACK TRANSACTION;

       THROW 50000,'An error occurred',16

 END CATCH

Now, if some unforeseen error occurs while updating either table1 or table2, you won’t get into the case where table1 is updated and table2 is not. It’s also imperative not to forget to close the transaction (either save the changes with COMMIT TRANSACTION, or undo the changes with ROLLBACK TRANSACTION), because the open transaction that contains your work is in a state of limbo, and if you don’t either complete it or roll it back, it can cause a lot of issues just hanging around in an open state. For example, if the transaction stays open and other operations are executed within that transaction, you might end up losing all work done on that connection. You may also prevent other connections from getting their work done, because each connection is isolated from one another messing up or looking at their unfinished work. Another user who needed the affected rows in table1 or table2 would have to wait (more on why this is throughout this chapter). The worst case of this I saw a number of years back was a single connection that was open all day with a transaction open after a failure because there was no error handling on the transaction. We lost a day’s work because we finally had to roll back the transactions when we killed the process.

There’s an additional setting for simple transactions known as named transactions, which I’ll introduce for completeness. (Ironically, this explanation will take more ink than introducing the more useful transaction syntax, but it is something good to know and can be useful in rare circumstances!) You can extend the functionality of transactions by adding a transaction name, as shown:

 BEGIN TRANSACTION <tranName> or <@tranvariable>;

This can be a confusing extension to the BEGIN TRANSACTION statement. It names the transaction to make sure you roll back to it, for example:

 BEGIN TRANSACTION one;

 ROLLBACK TRANSACTION one;

Only the first transaction mark is registered in the log, so the following code returns an error:

 BEGIN TRANSACTION one;

 BEGIN TRANSACTION two;

 ROLLBACK TRANSACTION two;

The error message is as follows:

 Msg 6401, Level 16, State 1, Line 3

 Cannot roll back two. No transaction or savepoint of that name was found.

Unfortunately, after this error has occurred, the transaction is still left open. For this reason, it’s seldom a good practice to use named transactions in your code unless you have a very specific purpose. The specific use that makes named transactions interesting is when named transactions use the WITH MARK setting. This allows marking the transaction log, which can be used when restoring a transaction log instead of a date and time. A common use of the marked transaction is to restore several databases back to the same condition and then restore all of the databases to a -common mark.

image  Note    A very good practice in testing your code that deals with transactions is to make sure that there are no transactions by executing ROLLBACK TRANSACTION until the message "The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION" is returned. At that point, you can feel safe that you are outside a transaction. In code, you should use @@trancount to check, which I will demonstrate later in this chapter.

The mark is only registered if data is modified within the transaction. A good example of its use might be to build a process that marks the transaction log every day before some daily batch process, especially one where the database is in single-user mode. The log is marked, and you run the process, and if there are any troubles, the database log can be restored to just before the mark in the log, no matter when the process was executed. Using the AdventureWorks2012 database, I’ll demonstrate this capability. You can do the same, but be careful to do this somewhere where you know you have a proper backup (just in case something goes wrong).

We first set up the scenario by putting the AdventureWorks2012 database in full recovery model.

 USE Master;

 GO

 ALTER DATABASE AdventureWorks2012

       SET RECOVERY FULL;

Next, we create a couple of backup devices to hold the backups we’re going to do:

 EXEC sp_addumpdevice 'disk', 'TestAdventureWorks2012',

                       'C:SQLBackupAdventureWorks2012.bak';

 EXEC sp_addumpdevice 'disk', 'TestAdventureWorks2012Log',

                       'C:SQLBackupAdventureWorks2012Log.bak';

image  Tip You can see the current setting using the following code:

SELECT recovery_model_desc

FROM sys.databases

WHERE name = 'AdventureWorks2012';

Next, we back up the database to the dump device we created:

 BACKUP DATABASE AdventureWorks2012 TO TestAdventureWorks2012;

Now, we change to the AdventureWorks2012 database and delete some data from a table:

 USE AdventureWorks2012;

 GO

 SELECT COUNT(*)

 FROM Sales.SalesTaxRate;

 BEGIN TRANSACTION Test WITH MARK 'Test';

 DELETE Sales.SalesTaxRate;

 COMMIT TRANSACTION;

This returns 29. Run the SELECT statement again, and it will return 0. Next, back up the transaction log to the other backup device:

 BACKUP LOG AdventureWorks2012 to TestAdventureWorks2012Log;

Now, we can restore the database using the RESTORE DATABASE command (the NORECOVERY setting keeps the database in a state ready to add transaction logs). We apply the log with RESTORE LOG. For the example, we’ll only restore up to before the mark that was placed, not the entire log:

 USE Master

 GO

 RESTORE DATABASE AdventureWorks2012 FROM TestAdventureWorks2012

                          WITH REPLACE, NORECOVERY;

 RESTORE LOG AdventureWorks2012 FROM TestAdventureWorks2012Log

                          WITH STOPBEFOREMARK = ’Test’, RECOVERY;

Now, execute the counting query again, and you can see that the 29 rows are in there.

 USE AdventureWorks2012;

 GO

 SELECT COUNT(*)

 FROM Sales.SalesTaxRate;

If you wanted to include the actions within the mark, you could use STOPATMARK instead of STOPBEFOREMARK. You can find the log marks that have been made in the MSDB database in the -logmarkhistory table.

Nesting Transactions

Yes, I am aware that the title of this section probably sounds a bit like Marlin Perkins is going to take over and start telling of the mating habits of transactions, but I am referring to starting a transaction after another transaction has already been started. The fact is that you can nest the starting of transactions like this:

 BEGIN TRANSACTION;

    BEGIN TRANSACTION;

        BEGIN TRANSACTION;

Technically speaking, there is really only one transaction being started, but an internal counter is keeping up with how many logical transactions have been started. To commit the transactions, you have to execute the same number of COMMIT TRANSACTION commands as the number of BEGIN TRANSACTION commands that have been executed. To tell how many BEGIN TRANSACTION commands have been executed without being committed, you can use the @@TRANCOUNT global variable. When it’s equal to one, then one BEGIN TRANSACTION has been executed. If it’s equal to two, then two have, and so on. When @@TRANCOUNT equals zero, you are no longer within a transaction context.

The limit to the number of transactions that can be nested is extremely large (the limit is 2,147,483,647, which took about 1.75 hours to reach in a tight loop on my old 2.27-GHz laptop with 2GB of RAM—clearly far, far more than any process should ever need).

As an example, execute the following:

 SELECT @@TRANCOUNT AS zeroDeep;

 BEGIN TRANSACTION;

 SELECT @@TRANCOUNT AS oneDeep;

It returns the following results:

 zeroDeep

 --------

 0

 oneDeep

 -------

 1

Then, nest another transaction, and check @@TRANCOUNT to see whether it has incremented. Afterward, commit that transaction, and check @@TRANCOUNT again:

 BEGIN TRANSACTION;

 SELECT @@TRANCOUNT AS twoDeep;

 COMMIT TRANSACTION; --commits very last transaction started with BEGIN TRANSACTION

 SELECT @@TRANCOUNT AS oneDeep;

This returns the following results:

 twoDeep

 -------

 2

 oneDeep

 -------

 1

Finally, close the final transaction:

 COMMIT TRANSACTION;

 SELECT @@TRANCOUNT AS zeroDeep;

This returns the following result:

 zeroDeep

 --------

 0

As I mentioned earlier in this section, technically only one transaction is being started. Hence, it only takes one ROLLBACK TRANSACTION command to roll back as many transactions as you have nested. So, if you’ve coded up a set of statements that end up nesting 100 transactions and you issue one rollback transaction, all transactions are rolled back—for example:

 BEGIN TRANSACTION;

 BEGIN TRANSACTION;

 BEGIN TRANSACTION;

 BEGIN TRANSACTION;

 BEGIN TRANSACTION;

 BEGIN TRANSACTION;

 BEGIN TRANSACTION;

 SELECT @@trancount as InTran;

 ROLLBACK TRANSACTION;

 SELECT @@trancount as OutTran;

This returns the following results:

 InTran

 -------

 7

 OutTran

 -------

 0

This is, by far, the trickiest part of using transactions in your code leading to some messy error handling and code management. It’s a bad idea to just issue a ROLLBACK TRANSACTION command without being cognizant of what will occur once you do—especially the command’s influence on the following code. If code is written expecting to be within a transaction and it isn’t, your data can get corrupted.

In the preceding example, if an UPDATE statement had been executed immediately after the ROLLBACK command, it wouldn’t be executed within an explicit transaction. Also, if COMMIT TRANSACTION is executed immediately after the ROLLBACK command, an error will occur:

 SELECT @@trancount

 COMMIT TRANSACTION

This will return

 -----------

 0

 Msg 3902, Level 16, State 1, Line 2

 The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.

Savepoints

In the previous section, I explained that all open transactions are rolled back using a ROLLBACK TRANSACTION call. This isn’t always desirable, so a tool is available to roll back only certain parts of a transaction. Unfortunately, it requires forethought and a special syntax. Savepoints are used to provide “selective” rollback.

For this, from within a transaction, issue the following statement:

 SAVE TRANSACTION <savePointName>; --savepoint names must follow the same rules for

                       --identifiers as other objects

For example, use the following code in whatever database you desire. In the source code, I’ll continue to place it in the tempdb, because the examples are self-contained.

 CREATE SCHEMA arts;

 GO

 CREATE TABLE arts.performer

 (

    performerId int NOT NULL identity,

    name varchar(100) NOT NULL

    );

 GO

 BEGIN TRANSACTION;

 INSERT INTO arts.performer(name) VALUES ('Elvis Costello'),

 SAVE TRANSACTION savePoint;

 INSERT INTO arts.performer(name) VALUES ('Air Supply'),

 --don't insert Air Supply, yuck! …

 ROLLBACK TRANSACTION savePoint;

 COMMIT TRANSACTION;

 SELECT *

 FROM arts.performer;

The output of this listing is as follows:

 performerId    name

 -----------    --------------

 1              Elvis Costello

In the code, there were two INSERT statements within the transaction boundaries, but in the output, there’s only one row. Obviously, the row that was rolled back to the savepoint wasn’t persisted.

Note that you don’t commit a savepoint; SQL Server simply places a mark in the transaction log to tell itself where to roll back to if the user asks for a rollback to the savepoint. The rest of the operations in the overall transaction aren’t affected. Savepoints don’t affect the value of @@trancount, nor do they release any locks that might have been held by the operations that are rolled back, until all nested transactions have been committed or rolled back.

Savepoints give the power to effect changes on only part of the operations transaction, giving you more control over what to do if you’re deep in a large number of operations.

I’ll mention savepoints later in this chapter when writing stored procedures, as they allow the rolling back of all the actions of a single stored procedure without affecting the transaction state of the stored procedure caller, though in most cases, it is usually just easier to roll back the entire transaction. Savepoints do, however, allow you to perform some operation, check to see if it is to your liking, and if it’s not, roll it back.

You can’t use savepoints in a couple situations:

  • When you’re using MARS and executing more than one batch at a time
  • When the transaction is enlisted into a distributed transaction (the next section discusses this)

How MARS Affects Transactions

There’s a slight wrinkle in how multiple statements can behave when using OLE DB or ODBC native client drivers to retrieve rows in SQL Server 2005 or later. When executing batches under MARS, there can be a couple scenarios:

  • Connections set to automatically commit : Each executed batch is within its own transaction, so there are multiple transaction contexts on a single connection.
  • Connections set to be manually committed : All executed batches are part of one transaction.

When MARS is enabled for a connection, any batch or stored procedure that starts a transaction (either implicitly in any statement or by executing BEGIN TRANSACTION) must commit the transaction; if not, the transaction will be rolled back. These transactions were new to SQL Server 2005 and are referred to as batch-scoped transactions.

Distributed Transactions

It would be wrong not to at least bring up the subject of distributed transactions. Occasionally, you might need to update data on a server that’s different from the one on which your code resides. The Microsoft Distributed Transaction Coordinator service (MS DTC) gives us this ability.

If your servers are running the MS DTC service, you can use the BEGIN DISTRIBUTED TRANSACTION command to start a transaction that covers the data residing on your server, as well as the remote server. If the server configuration 'remote proc trans' is set to 1, any transaction that touches a linked server will start a distributed transaction without actually calling the BEGIN DISTRIBUTED TRANSACTION command. However, I would strongly suggest you know if you will be using another server in a transaction (check sys.configurations or sp_configure for the current setting, and set the value using sp_configure). Note also that savepoints aren’t supported for distributed transactions.

The following code is just pseudocode and won’t run as is, but this is representative of the code needed to do a distributed transaction:

 BEGIN TRY

    BEGIN DISTRIBUTED TRANSACTION;

    --remote server is a server set up as a linked server

    UPDATE remoteServer.dbName.schemaName.tableName

    SET value = 'new value'

    WHERE keyColumn = 'value';

    --local server

    UPDATE dbName.schemaName.tableName

    SET value = 'new value'

    WHERE keyColumn = 'value';

    COMMIT TRANSACTION;

 END TRY

 BEGIN CATCH

    ROLLBACK TRANSACTION;

    DECLARE @ERRORMessage varchar(2000);

    SET @ERRORMessage = ERROR_MESSAGE();

    THROW 50000, @ERRORMessage,16;

 END CATCH

The distributed transaction syntax also covers the local transaction. As mentioned, setting the configuration option 'remote proc trans' automatically upgrades a BEGIN TRANSACTION command to a BEGIN DISTRIBUTED TRANSACTION command. This is useful if you frequently use distributed transactions. Without this setting, the remote command is executed, but it won’t be a part of the current -transaction.

Explicit vs. Implicit Transactions

Before finishing the discussion of transaction syntax, there’s one last thing that needs to be covered for the sake of completeness. I’ve alluded to the fact that every statement is executed in a transaction (again, this includes even SELECT statements). This is an important point that must be understood when writing code. Internally, SQL Server starts a transaction every time a SQL statement is started. Even if a transaction isn’t started explicitly with a BEGIN TRANSACTION statement, SQL Server automatically starts a new transaction whenever a statement starts and commits or rolls it back depending on whether or not any errors occur. This is known as an autocommit transaction. When the SQL Server engine commits the transaction, it starts for each statement-level transaction.

SQL Server gives us a setting to change this behavior of automatically committing the transaction: SET IMPLICIT_TRANSACTIONS. When this setting is turned on and the execution context isn’t already within a transaction, such as one explicitly declared using BEGIN TRANSACTION, BEGIN TRANSACTION is automatically (logically) executed when any of the following statements are executed: INSERT, UPDATE, DELETE, SELECT, TRUNCATE TABLE, DROP, ALTER TABLE, REVOKE, CREATE, GRANT, FETCH, or OPEN. This will mean that a COMMIT TRANSACTION or ROLLBACK TRANSACTION command has to be executed to end the transaction. Otherwise, once the connection terminates, all data is lost (and until the transaction terminates, locks that have been accumulated are held, other users are blocked, and pandemonium might occur).

SET IMPLICIT_TRANSACTIONS isn’t a typical setting used by SQL Server programmers or administrators but is worth mentioning because if you change the setting of ANSI_DEFAULTS to ON, IMPLICIT_TRANSACTIONS will be enabled!

I’ve mentioned that every SELECT statement is executed within a transaction, but this deserves a bit more explanation. The entire process of rows being considered for output, then transporting them from the server to the client is contained inside a transaction. The SELECT statement isn’t finished until the entire result set is exhausted (or the client cancels the fetching of rows), so the transaction doesn’t end either. This is an important point that will come back up in the “Isolation Levels” section, as I discuss how this transaction can seriously affect concurrency based on how isolated you need your queries to be.

Compiled SQL Server Code

Now that I’ve discussed the basics of transactions, it’s important to understand some of the slight differences involved in using them within compiled code versus the way we have used transactions so far in batches. You can’t use transactions in user-defined functions (you can’t change system state in a function, so they aren’t necessary anyhow), but it is important to understand the caveats when you use them in

  • Stored procedures
  • Triggers

Stored Procedures

Stored procedures, simply being compiled batches of code, use transactions as previously discussed, with one caveat. The transaction nesting level cannot be affected during the execution of a procedure. In other words, you must commit at least as many transactions as you begin in a stored procedure if you want everything to behave smoothly.

Although you can roll back any transaction, you shouldn’t roll it back unless the @@TRANCOUNT was zero when the procedure started. However, it’s better not to execute a ROLLBACK TRANSACTION statement at all in a stored procedure, so there’s no chance of rolling back to a transaction count that’s different from when the procedure started. This protects you from the situation where the procedure is executed in another transaction. Rather, it’s generally best to start a transaction and then follow it with a savepoint. Later, if the changes made in the procedure need to be backed out, simply roll back to the savepoint, and commit the transaction. It’s then up to the stored procedure to signal to any caller that it has failed and to do whatever it wants with the transaction.

As an example, let’s build the following simple procedure that does nothing but execute a BEGIN TRANSACTION and a ROLLBACK TRANSACTION:

 CREATE PROCEDURE tranTest

 AS

 BEGIN

    SELECT @@TRANCOUNT AS trancount;

    BEGIN TRANSACTION;

    ROLLBACK TRANSACTION;

 END;

Execute this procedure outside a transaction, and you will see it behaves like you would expect:

 EXECUTE tranTest;

It returns

 Trancount

 ---------

 0

However, say you execute it within an existing transaction:

 BEGIN TRANSACTION;

 EXECUTE tranTest;

 COMMIT TRANSACTION;

The procedure returns the following results:

 Trancount

 ---------

 1

 Msg 266, Level 16, State 2, Procedure tranTest, Line 0

 Transaction count after EXECUTE indicates a mismatching number of BEGIN and COMMIT statements. Previous count = 1, current count = 0.

 Msg 3902, Level 16, State 1, Line 3

 The COMMIT TRANSACTION request has no corresponding BEGIN TRANSACTION.

The errors occur because the transaction depth has changed while rolling back the transaction inside the procedure. This error is one of the most frightening errors out there, because it usually says that you probably have been doing work that you expected to be in a transaction outside of a transaction, thus you will probably end up with out-of-sync data that needs to be repaired.

Finally, let’s recode the procedure as follows, putting a savepoint name on the transaction so we only roll back the code in the procedure:

 ALTER PROCEDURE tranTest

 AS

 BEGIN

    --gives us a unique savepoint name, trim it to 125 characters if the

    --user named the procedure really really large, to allow for nestlevel

    DECLARE @savepoint nvarchar(128) =

       cast(object_name(@@procid) AS nvarchar(125)) +

                    cast(@@nestlevel AS nvarchar(3));

    SELECT @@TRANCOUNT AS trancount;

    BEGIN TRANSACTION;

    SAVE TRANSACTION @savepoint;

    --do something here

    ROLLBACK TRANSACTION @savepoint;

    COMMIT TRANSACTION;

 END;

Now, you can execute it from within any number of transactions, and it will never fail, but it will never actually do anything either:

 BEGIN TRANSACTION;

 EXECUTE tranTest;

 COMMIT TRANSACTION;

Now, it returns

 Trancount

 ---------

 1

You can call procedures from other procedures (even recursively from the same procedure) or external programs. It’s important to take these precautions to make sure that the code is safe under any calling circumstances.

image  Caution    As mentioned in the “Savepoints” section, you can’t use savepoints with distributed transactions or when sending multiple batches over a MARS–enabled connection. To make the most out of MARS, you might not be able to use this strategy. Frankly speaking, it might simply be prudent to execute modification procedures one at a time anyhow.

Naming savepoints is important. Because savepoints aren’t scoped to a procedure, you must ensure that they’re always unique. I tend to use the procedure name (retrieved here by using the object_name function called for the @@procId, but you could just enter it textually) and the current transaction nesting level. This guarantees that I can never have the same savepoint active, even if calling the same procedure recursively.

Let’s look briefly at how to code this into procedures using proper error handling:

 ALTER PROCEDURE tranTest

 AS

 BEGIN

    --gives us a unique savepoint name, trim it to 125

    --characters if the user named it really large

    DECLARE @savepoint nvarchar(128) =

              cast(object_name(@@procid) AS nvarchar(125)) +

                       cast(@@nestlevel AS nvarchar(3));

    --get initial entry level, so we can do a rollback on a doomed transaction

    DECLARE @entryTrancount int = @@trancount;

    BEGIN TRY

    BEGIN TRANSACTION;

    SAVE TRANSACTION @savepoint;

    --do something here

    THROW 50000, 'Invalid Operation',16;

    COMMIT TRANSACTION;

    END TRY

    BEGIN CATCH

    --if the tran is doomed, and the entryTrancount was 0,

    --we have to roll back

       IF xact_state()= -1 and @entryTrancount = 0

          rollback transaction;

       --otherwise, we can still save the other activities in the

       --transaction.

       ELSE IF xact_state() = 1 --transaction not doomed, but open

        BEGIN

           ROLLBACK TRANSACTION @savepoint;

           COMMIT TRANSACTION;

        END

       DECLARE @ERRORmessage nvarchar(4000);

       SET @ERRORmessage = 'Error occurred in procedure ''' + OBJECT_NAME(@@procid)

                      + ''', Original Message: ''' + ERROR_MESSAGE() + '''';

       THROW 50000, @ERRORmessage,16;

       RETURN -100

    END CATCH

 END

In the CATCH block, instead of rolling back the transaction, I checked for a doomed transaction, and if we were not in a transaction at the start of the procedure, I rolled back. A doomed transaction is one in which some operation has made it impossible to do anything other than roll back the transaction. A common cause is a trigger-based error message. It is still technically an active transaction, giving you the chance to roll it back so operations do occur outside of the expected transaction space.

If the transaction was not doomed, I simply rolled back the savepoint. An error is returned for the caller to deal with. You could also eliminate RAISERROR altogether if the error wasn’t critical and the caller needn’t ever know of the rollback. You can place any form of error handling in the CATCH block, and as long as you don’t roll back the entire transaction and the transaction does not become doomed, you can keep going and later commit the transaction.

If this procedure called another procedure that used the same error handling, it would roll back its part of the transaction. It would then raise an error, which, in turn, would cause the CATCH block to be called, roll back the savepoint, and commit the transaction (at that point, the transaction wouldn’t contain any changes at all). If the transaction is doomed, when you get to the top level, it is rolled back. You might ask why you should go through this exercise if you’re just going to roll back the transaction anyhow. The key is that each level of the calling structure can decide what to do with its part of the transaction. Plus, in the error handler we have created, we get the basic call stack for debugging purposes.

As an example of how this works, consider the following schema and table (create it in any database you desire, likely tempdb, as this sample is isolated to this section):

 CREATE SCHEMA menu;

 GO

 CREATE TABLE menu.foodItem

 (

    foodItemId int not null IDENTITY(1,1)

       CONSTRAINT PKmenu_foodItem PRIMARY KEY,

    name varchar(30) not null

       CONSTRAINT AKmenu_foodItem_name UNIQUE,

    description varchar(60) not null,

       CONSTRAINT CHKmenu_foodItem_name CHECK (name <> ''),

       CONSTRAINT CHKmenu_foodItem_description CHECK (description <> '')

 );

Now, create a procedure to do the insert:

 CREATE PROCEDURE menu.foodItem$insert

 (

    @name varchar(30),

    @description varchar(60),

    @newFoodItemId int = null output --we will send back the new id here

 )

 AS

 BEGIN

    SET NOCOUNT ON;

    --gives us a unique savepoint name, trim it to 125

    --characters if the user named it really large

    DECLARE @savepoint nvarchar(128) =

              cast(object_name(@@procid) AS nvarchar(125)) +

                       cast(@@nestlevel AS nvarchar(3));

    --get initial entry level, so we can do a rollback on a doomed transaction

    DECLARE @entryTrancount int = @@trancount;

    BEGIN TRY

    BEGIN TRANSACTION;

    SAVE TRANSACTION @savepoint;

    INSERT INTO menu.foodItem(name, description)

    VALUES (@name, @description);

    SET @newFoodItemId = scope_identity(); --if you use an instead of trigger,

                       --you will have to use name as a key

                       --to do the identity "grab" in a SELECT

                       --query

    COMMIT TRANSACTION;

    END TRY

    BEGIN CATCH

    --if the tran is doomed, and the entryTrancount was 0,

    --we have to roll back

    IF xact_state()= -1 and @entryTrancount = 0

        ROLLBACK TRANSACTION;

    --otherwise, we can still save the other activities in the

    --transaction.

    ELSE IF xact_state() = 1 --transaction not doomed, but open

        BEGIN

        ROLLBACK TRANSACTION @savepoint;

        COMMIT TRANSACTION;

        END

    DECLARE @ERRORmessage nvarchar(4000);

    SET @ERRORmessage = 'Error occurred in procedure ''' + object_name(@@procid)

                   + ''', Original Message: ''' + ERROR_MESSAGE() + '''';

    --change to RAISERROR (50000, @ERRORmessage,16) if you want to continue processing

    THROW 50000,@ERRORmessage, 16;

    RETURN -100;

    END CATCH

 END;

Next, try out the code:

 DECLARE @foodItemId int, @retval int;

 EXECUTE @retval = menu.foodItem$insert @name ='Burger',

        @description = 'Mmmm Burger',

                       @newFoodItemId = @foodItemId output;

 SELECT @retval as returnValue;

 IF @retval >= 0

    SELECT foodItemId, name, description

    FROM menu.foodItem

    where foodItemId = @foodItemId;

There's no error, so the row we created is returned:

 returnValue

 -----------

 0

 foodItemId    name    description

 ----------    ----    -----------

 1 Burger      Mmmm     Burger

Now, try out the code with an error:

 DECLARE @foodItemId int, @retval int;

 EXECUTE @retval = menu.foodItem$insert @name ='Big Burger',

                       @description = '',

                       @newFoodItemId = @foodItemId output;

 SELECT @retval as returnValue;

 IF @retval >= 0

    SELECT foodItemId, name, description

    FROM menu.foodItem

    where foodItemId = @foodItemId;

Because the description is blank, an error is returned:

 Msg 50000, Level 16, State 16, Procedure foodItem$insert, Line 50

 Error occurred in procedure 'foodItem$insert', Original Message: 'The INSERT statement conflicted with the CHECK constraint "CHKmenu_foodItem_description". The conflict occurred in database "ContainedDatabase", table "menu.foodItem", column 'description'.'

Note that no code in the batch is executed after the THROW statement is executed. Using RAISERROR will allow the processing to continue if you so desire.

Triggers

Just as in stored procedures, you can start transactions, set savepoints, and roll back to a savepoint. However, if you execute a ROLLBACK TRANSACTION statement in a trigger, two things can occur:

  • Outside a TRY-CATCH block, the entire batch of SQL statements is canceled.
  • Inside a TRY-CATCH block, the batch isn’t canceled, but the transaction count is back to zero.

Back in Chapters 6 and 7, we discussed and implemented triggers that consistently used rollbacks when any error occurred. If you’re not using TRY-CATCH blocks, this approach is generally exactly what’s desired, but when using TRY-CATCH blocks, it can make things more tricky. To handle this, in the CATCH block of stored procedures I’ve included this code:

    --if the tran is doomed, and the entryTrancount was 0,

    --we have to roll back

    IF xact_state()= -1 and @entryTrancount = 0

       rollback transaction;

    --otherwise, we can still save the other activities in the

    --transaction.

    ELSE IF xact_state() = 1 --transaction not doomed, but open

        BEGIN

        ROLLBACK TRANSACTION @savepoint;

        COMMIT TRANSACTION;

        END

This is an effective, if perhaps limited, method of working with errors from triggers that works in most any situation. Removing all ROLLBACK TRANSACTION commands but just raising an error from a trigger dooms the transaction, which is just as much trouble as the rollback. The key is to understand how this might affect the code that you’re working with and to make sure that errors are handled in an understandable way. More than anything, test all types of errors in your system (trigger, constraint, and so on).

For an example, I will create a trigger based on the framework we used for triggers in Chapter 6 and 7, which is presented in more detail in Appendix B. Instead of any validations, I will just immediately cause an error with the statement THROW 50000,'FoodItem''s cannot be done that way',16. Note that my trigger template does do a rollback in the trigger, assuming that users of these triggers follow the error handing setup here, rather than just dooming the transaction. Dooming the transaction could be a safer way to go if you do not have full control over error handling.

 CREATE TRIGGER menu.foodItem$InsertTrigger

 ON menu.foodItem

 AFTER INSERT

 AS

 BEGIN

    SET NOCOUNT ON;

    SET ROWCOUNT 0; --in case the client has modified the rowcount

    --use inserted for insert or update trigger, deleted for update or delete trigger

    --count instead of @@rowcount due to merge behavior that sets @@rowcount to a number

    --that is equal to number of merged rows, not rows being checked in trigger

    DECLARE @msg varchar(2000), --used to hold the error message

    --use inserted for insert or update trigger, deleted for update or delete trigger

    --count instead of @@rowcount due to merge behavior that sets @@rowcount to a number

    --that is equal to number of merged rows, not rows being checked in trigger

    @rowsAffected int = (SELECT COUNT(*) FROM inserted);

    --@rowsAffected int = (SELECT COUNT(*) FROM deleted);

    --no need to continue on if no rows affected

    IF @rowsAffected = 0 RETURN;

    BEGIN TRY

       --[validation blocks][validation section]

    THROW 50000, 'FoodItem''s cannot be done that way',16

       --[modification blocks][modification section]

    END TRY

    BEGIN CATCH

          IF @@trancount > 0

             ROLLBACK TRANSACTION;

          THROW;

    END CATCH

 END

In the downloadable code, I have modified the error handling in the stored procedure to put out markers, so you can see what branch of the code is being executed:

    SELECT 'In error handler'

    --if the tran is doomed, and the entryTrancount was 0,

    --we have to roll back

    IF xact_state()= -1 and @entryTrancount = 0

       begin

        SELECT 'Transaction Doomed'

        ROLLBACK TRANSACTION

        end

     --otherwise, we can still save the other activities in the

    --transaction.

    ELSE IF xact_state() = 1 --transaction not doomed, but open

        BEGIN

        SELECT 'Savepoint Rollback'

           ROLLBACK TRANSACTION @savepoint

        COMMIT TRANSACTION

        END

Executing the code that contains an error that the constraints catch:

 DECLARE @foodItemId int, @retval int;

 EXECUTE @retval = menu.foodItem$insert @name ='Big Burger',

                       @description = '',

                       @newFoodItemId = @foodItemId output;

 SELECT @retval;

This is the output, letting us know that the transaction was still technically open and we could have committed any changes we wanted to:

 ----------------

 In Error Handler

 ------------------

 Savepoint Rollback

 Msg 50000, Level 16, State 16, Procedure foodItem$insert, Line 57

 Error occurred in procedure 'foodItem$insert', Original Message: 'The INSERT statement conflicted with the CHECK constraint "CHKmenu_foodItem_description". The conflict occurred in database "ContainedDatabase", table "menu.foodItem", column 'description'.'

You can see the constraint message, after the template error. Now, try to enter some data that is technically correct but is blocked by the trigger with the ROLLBACK:

 DECLARE @foodItemId int, @retval int;

 EXECUTE @retval = menu.foodItem$insert @name ='Big Burger',

                       @description = 'Yummy Big Burger',

                       @newFoodItemId = @foodItemId output;

 SELECT @retval;

These results are a bit more mysterious, though the transaction is clearly in an error state. Since the rollback operation occurs in the trigger, once we reach the error handler, there is no need to do any savepoint or rollback, so it just finishes:

 ----------------

 In Error Handler

 Msg 50000, Level 16, State 16, Procedure foodItem$insert, Line 57

 Error occurred in procedure 'foodItem$insert', Original Message: 'FoodItem's cannot be done that way'

For the final demonstration, I will change the trigger to just do a RAISERROR, with no other error handling:

 ALTER TRIGGER menu.foodItem$InsertTrigger

 ON menu.foodItem

 AFTER INSERT

 AS

 BEGIN

     DECLARE @rowsAffected int, --stores the number of rows affected

           @msg varchar(2000); --used to hold the error message

     SET @rowsAffected = @@rowcount;

     --no need to continue on if no rows affected

     IF @rowsAffected = 0 return;

     SET NOCOUNT ON; --to avoid the rowcount messages

     SET ROWCOUNT 0; --in case the client has modified the rowcount

    THROW 50000,'FoodItem''s cannot be done that way',16;

 END;

Then, reexecute the previous statement that caused the trigger error:

 ----------------

 In Error Handler

 ------------------

 Transaction Doomed

 Msg 50000, Level 16, State 16, Procedure foodItem$insert, Line 57

 Error occurred in procedure 'foodItem$insert', Original Message: 'FoodItem's cannot be done that way'

Hence, our error handler covered all of the different bases of what can occur for errors. In each case, we got an error message that would let us know where an error was occurring and that it was an error. The main thing I wanted to show in this section is that error handling is messy and adding triggers, while useful, complicates the error handling process, so I would certainly use constraints as much as possible and triggers as rarely as possibly (the primary uses I have for them were outlined in Chapter 7). Also, be certain to develop an error handler in your T-SQL code and applications that is used with all of your code so that you capture all exceptions in a manner that is desirable for you and your developers.

Isolating Sessions

In the previous section, I introduced transactions, which are the foundation of the SQL Server concurrency controls. Even without concurrency they would be useful, but now, we are going to get a bit deeper into concurrency controls and start to demonstrate how multiple users can be manipulating and modifying the exact same data, making sure that all users get consistent usage of the data. Picture a farm with tractors and people picking vegetables. Both sets of farm users are necessary, but you definitely want to isolate their utilization from one another.

Of the ACID properties discussed earlier, isolation is probably the most difficult to understand and certainly the most important to get right. You probably don’t want to make changes to a system and have them trampled by the next user any more than the farm hand wants to become tractor fodder (well, OK, perhaps his concern is a bit more physical—a wee bit at least). In this section, I will introduce a couple important concepts that are essential to building concurrent applications, both in understanding what is going on, and introduce the knobs you can use to tune the degree of isolation for sessions:

  • Locks: These are holds put by SQL Server on objects that are being used by users.
  • Isolation levels: These are settings used to control the length of time for which SQL Server holds onto the locks.

These two important things work together to allow you to control and optimize a server’s concurrency, allowing users to work at the same time, on the same resources, while still maintaining consistency. However, just how consistent your data remains is the important thing, and that is what you will see in the “Isolation Levels” section.

Locks

Locks are tokens laid down by the SQL Server processes to stake their claims to the different resources available, so as to prevent one process from stomping on another and causing inconsistencies or prevent another process from seeing data that has not yet been verified by constraints or triggers. They are a lot like the “diver down” markers that deep-sea divers place on top of the water when working below the water. They do this to alert other divers, pleasure boaters, fishermen, and others that they’re below. Other divers are welcome, but a fishing boat with a trolling net should please stay away, thank you very much! Every SQL Server process applies a lock to anything it does to ensure that that other user processes know what they are doing as well as what they are planning to do and to ensure that other processes don’t get in it’s way. Minimally, a lock is always placed just to make sure that the database that is in use cannot be dropped.

The most common illustration of why locks are needed is called the lost update , as illustrated in Figure 11-1.

image

Figure 11-1. A lost update illustration (probably one of the major inspirations for the other definition of multitasking: “screwing up everything simultaneously”)

In the scenario in Figure 11-1, you have two concurrent users. Each of these executes some SQL statements adding money to the balance, but in the end, the final value is going to be the wrong value, and 500 will be lost from the balance. Why? Because each user fetched a reality from the database that was correct at the time and then acted on it as if it would always be true.

Locks act as a message to other processes that a resource is being used, or at least probably being used. Think of a railroad-crossing sign. When the bar crosses the road, it acts as a lock to tell you not to drive across the tracks because the train is going to use the resource. Even if the train stops and never reaches the road, the bar comes down, and the lights flash. This lock can be ignored (as can SQL Server locks), but it’s generally not advisable to do so, because if the train does come, you may not have the ability to go back to Disney World, except perhaps to the Haunted Mansion. (Ignoring locks isn’t usually as messy as ignoring a train-crossing signal, unless you are creating the system that controls that warning signal. Ignore those locks—ouch.)

In this section, I will look at a few characteristics of locks:

  • Type of lock: Indicates what is being locked
  • Mode of lock: Indicates how strong the lock is

Lock Types

If you’ve been around for a few versions of SQL Server, you probably know that since SQL Server 7.0, SQL Server primarily uses row-level locks. That is, a user locking some resource in SQL Server does it on individual rows of data, rather than on pages of data, or even on complete tables.

However, thinking that SQL Server only locks at the row level is misleading, as SQL Server can use six different types of locks to lock varying portions of the database, with the row being the finest type of lock, all the way up to a full database lock. And each of them will be used quite often. The types of locks in Table 11-1 are supported.

image  Tip In terms of locks, database object locks (row, RID, key range, key, page, table, database) are all you have much knowledge of or control over in SQL Server, so these are all I’ll cover. However, you should be aware that many more locks are in play, because SQL Server manages its hardware and internal needs as you execute queries. Hardware and internal resource locks are referred to as latches, and you’ll occasionally see them referenced in SQL Server Books Online, though the documentation is not terribly deep regarding them. You have little control over them, because they control physical resources, like the lock on the lavatory door in an airplane. Like the lavatory, though, you generally only want one user accessing a physical resource at a time.

Table 11-1. Lock Types

Type of Lock Granularity
Row or row identifier (RID) A single row in a table
Key or key range A single value or range of values (for example, to lock rows with values from A–M, even if no rows currently exist)
Page An 8-KB index or data page
Extent A group of eight 8-KB pages (64KB), generally only used when allocating new space to the database
HoBT An entire heap or B-tree structure
Table An entire table, including all rows and indexes
File An entire file, as covered in Chapter 10
Application A special type of lock that is user defined (will be covered in more detail later in this chapter)
Metadata Metadata about the schema, such as catalog objects
Allocation unit A group of 32 extents
Database The entire database

At the point of request, SQL Server determines approximately how many of the database resources (a table, a row, a key, a key range, and so on) are needed to satisfy the request. This is calculated on the basis of several factors, the specifics of which are unpublished. Some of these factors include the cost of acquiring the lock, the amount of resources needed, and how long the locks will be held (the next major section, “Isolation Levels,” will discuss the factors surrounding the question “how long?”). It’s also possible for the query processor to upgrade the lock from a more granular lock to a less specific type if the query is unexpectedly taking up large quantities of resources.

For example, if a large percentage of the rows in a table are locked with row locks, the query processor might switch to a table lock to finish out the process. Or, if you’re adding large numbers of rows into a clustered table in sequential order, you might use a page lock on the new pages that are being added.

Lock Modes

Beyond the type of lock, the next concern is how strongly to lock the resource. For example, consider a construction site. Workers are generally allowed onto the site but not civilians who are not part of the process. Sometimes, however, one of the workers might need exclusive use of the site to do something that would be dangerous for other people to be around (like using explosives, for example.)

Where the type of lock defined the amount of the database to lock, the mode of the lock refers to how strict the lock is and how protective the engine is when dealing with other locks. Table 11-2 lists these available modes.

Table 11-2. Lock Modes

Mode Description
Shared This lock mode grants access for reads only. It’s generally used when users are looking at but not editing the data. It’s called “shared” because multiple processes can have a shared lock on the same resource, allowing read-only access to the resource. However, sharing resources prevents other processes from modifying the resource.
Exclusive This mode gives exclusive access to a resource and can be used during modification of data also. Only one process may have an active exclusive lock on a resource.
Update This mode is used to inform other processes that you’re planning to modify the data but aren’t quite ready to do so. Other connections may also issue shared, but not update or exclusive, locks while you’re still preparing to do the modification. Update locks are used to prevent deadlocks (I’ll cover them later in this section) by marking rows that a statement will possibly update, rather than upgrading directly from a shared lock to an exclusive one.
Intent This mode communicates to other processes that taking one of the previously listed modes might be necessary. You might see this mode as intent shared, intent exclusive, or shared with intent exclusive.
Schema This mode is used to lock the structure of an object when it’s in use, so you cannot alter a table when a user is reading data from it.

Each of these modes, coupled with the granularity, describes a locking situation. For example, an exclusive table lock would mean that no other user can access any data in the table. An update table lock would say that other users could look at the data in the table, but any statement that might modify data in the table would have to wait until after this process has been completed.

To determine which mode of a lock is compatible with another mode of lock, we deal with lock compatibility. Each lock mode may or may not be compatible with the other lock mode on the same resource (or resource that contains other resources). If the types are compatible, two or more users may lock the same resource. Incompatible lock types would require the any additional users simply to wait until all of the incompatible locks have been released.

Table 11-3 shows which types are compatible with which others.

Table 11-3. Lock Compatibility Modes

Mode IS S U IX SIX X
Intent shared (IS)
Shared (S)
Update (U)
Intent exclusive (IX)
Shared with intent exclusive (SIX)
Exclusive (X)

Although locks are great for data consistency, as far as concurrency is considered, locked resources stink. Whenever a resource is locked with an incompatible lock type and another process cannot use it to complete its processing, concurrency is lowered, because the process must wait for the other to complete before it can continue. This is generally referred to as blocking: one process is blocking another from doing something, so the blocked process must wait its turn, no matter how long it takes.

Simply put, locks allow consistent views of the data by only letting a single process modify a single resource at a time, while allowing multiple viewers simultaneous utilization in read-only access. Locks are a necessary part of SQL Server architecture, as is blocking to honor those locks when needed, to make sure one user doesn’t trample on another’s data, resulting in invalid data in some cases.

In the next section, I’ll discuss isolation levels, which determine how long locks are held. Executing SELECT * FROM sys.dm_os_waiting_tasks gives you a list of all processes that tells you if any users are blocking and which user is doing the blocking. Executing SELECT * FROM sys.dm_tran_locks lets you see locks that are being held. SQL Server Management Studio has a decent Activity Monitor, accessible via the Object Explorer in the Management folder.

It’s possible to instruct SQL Server to use a different type of lock than it might ordinarily choose by using table hints on your queries. For individual tables in a FROM clause, you can set the type of lock to be used for the single query like so:

 FROM table1 [WITH] (<tableHintList>)

          join table2 [WITH] (<tableHintList>)

Note that these hints work on all query types. In the case of locking, you can use quite a few. A partial list of the more common hints follows:

  • PageLock: Forces the optimizer to choose page locks for the given table.
  • NoLock: Leave no locks, and honor no locks for the given table.
  • RowLock: Force row-level locks to be used for the table.
  • Tablock: Go directly to table locks, rather than row or even page locks. This can speed some operations, but seriously lowers write concurrency.
  • TablockX: This is the same as Tablock, but it always uses exclusive locks (whether it would have normally done so or not).
  • XLock: Use exclusive locks.
  • UpdLock: Use update locks.

Note that SQL Server can override your hints if necessary. For example, take the case where a query sets the table hint of NoLock, but then rows are modified in the table in the execution of the query. No shared locks are taken or honored, but exclusive locks are taken and held on the table for the rows that are modified, though not on rows that are only read (this is true even for resources that are read as part of a trigger or constraint).

A very important term that’s you need to understand is “deadlock.” A deadlock is a circumstance where two processes are trying to use the same objects, but neither will ever be able to complete because each is blocked by the other connection. For example, consider two processes (Processes 1 and 2), and two resources (Resources A and B). The following steps lead to a deadlock:

  1. Process 1 takes a lock on Resource A, and at the same time, Process 2 takes a lock on Resource B.
  2. Process 1 tries to get access to Resource B. Because it’s locked by Process 2, Process 1 goes into a wait state.
  3. Process 2 tries to get access to Resource A. Because it’s locked by Process 1, Process 2 goes into a wait state.

At this point, there’s no way to resolve this issue without ending one of the processes. SQL Server arbitrarily kills one of the processes, unless one of the processes has voluntarily raised the likelihood of being the killed process by setting DEADLOCK_PRIORITY to a lower value than the other. Values can be between integers –10 and 10, or LOW (equal to –5), NORMAL (0), or HIGH (5). SQL Server raises error 1205 to the client to tell the client that the process was stopped:

Server: Msg 1205, Level 13, State 1, Line 4

Transaction (Process ID 55) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

At this point, you could resubmit the request, as long as the call was coded such that the application knows when the transaction was started and what has occurred (something every application programmer ought to strive to do).

image  Tip Proper deadlock handling requires that you build your applications in such a way that you can easily tell how much of an operation succeeded or failed. This is done by proper use of transactions. A good practice is to send one transaction per batch from a client application. Keep in mind that the engine views nested transactions as one transaction, so what I mean here is to start and complete one high-level transaction per batch.

Deadlocks can be hard to diagnose, as you can deadlock on many things, even hardware access. A common trick to try to alleviate frequent deadlocks between pieces of code is to order object access in the same order in all code (so table dbo.Apple, dbo.Bananna, etc) if possible. This way, locks are more likely to be taken in the same order, causing the lock to block earlier, so that the next process is blocked instead of deadlocked.

An important consideration is that you really can’t completely avoid deadlocks. Frequent deadlocks can be indicative of a problem with your code, but often, if you are running a very busy server, deadlocks happen, and the best thing to do is handle them by resubmitting the last transaction executed (too many applications just raise the deadlock as an error that users don’t understand). Although frequent deadlocks are often an issue, it is very hard to code your system to be 100% safe from deadlocks (particularly when allowing users to share resources) so every call to the server should be aware that a deadlock could occur and, ideally, what to do with it.

Using SQL Server Profiler, you can add the DeadLock Graph event class to see deadlock events, which helps diagnose them. For more information about Profiler, check SQL Server Books Online.

image  Note    There’s also a bulk update mode that I didn’t mention; you use it to lock a table when inserting data in bulk into the table and applying the TABLOCK hint. It’s analogous to an exclusive table lock for concurrency issues.

Isolation Levels

In the previous section, I said that locks are placed to make sure that a resource is protected while SQL Server is using it. But how long is the lock be held? Locks can be taken just for the amount of time it takes to get data from memory, or as long as a transaction is still open, even if that turns out to be hours. The isolation level is the setting that tells SQL Server how long to hold these locks, or even whether or not to take locks for read operations, and whether or not to honor other connections locks.

The safest method to provide consistency in operations would be to take an exclusive lock on the entire database, do your operations, and then release the lock. Then the next user does the same thing. Although this was somewhat common in early file-based systems, it isn’t a reasonable alternative when you need to support 20,000 concurrent users (or even just a 10 data entry clerks, or perhaps automated users who do thousands of operations per second), no matter how beefy your hardware platform may be.

To improve concurrency, locks are held for the minimum time necessary to provide a reasonable amount of data consistency. (If the word “reasonable” concerns you, read on, because SQL Server defaults don’t provide perfect coverage.) Isolation levels control how long locks are held, and there are five distinct levels (READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE, and SNAPSHOT; each of which I will define in more detail later). From inside a transaction, locks can be held for a variable amount of time to protect the data that’s being worked with. For example, consider the following hypothetical code snippet that illustrates an extremely typical mistake made by people just getting started(note that code like this in version 2008 or later should be migrated to use the new MERGE syntax, but this is going to remain a very common type of coding problem for years to come):

 BEGIN TRANSACTION;

 SAVE TRANSACTION savePoint;

 IF EXISTS ( SELECT * FROM tableA WHERE tableAId = 'value' )

 BEGIN

    UPDATE tableB

    SET status = 'UPDATED'

    WHERE tableAId = 'value';

    IF @@error <> 0

    BEGIN

    ROLLBACK TRANSACTION savePoint;

    THROW 50000, 'Error updating tableB',16;

    END;

 END;

 --usually followed by an insert

 COMMIT TRANSACTION;

First, we check to see if a value exists in tableA. If it does, we update a value in tableB. On first glance, this seems safe—if a row exists when checked for in tableA, it will exist once the execution gets to the tableB update. However, how well this works is based solely on how long the locks are held on the SELECT from tableA, coupled with how long it takes to get to the UPDATE statement. Although the row might exist when the IF EXISTS block executed, what if a table lock exists on tableB when you try to execute the update of tableB, and the process gets blocked waiting for the lock to be cleared? While waiting for the table lock on tableB to be cleared, the key row that previously existed could have been deleted from tableA, if the lock isn’t maintained on the row in tableA until the transaction is completed. You may think that this shouldn’t be a problem, as perhaps the row was going to be deleted anyhow. But what if this row is important to a processing problem?

Your client believes that the row exists, because you updated it. And the delete operation may be removing the row because it was stale. In the section on optimistic locking techniques later in this chapter, I will demonstrate a way to make sure that the second process to try and update or delete has the most recent version when it completes, but in this case, unless you lock the row where ταβλεΑΙδ = 'ϖαλυε' in the previous block of code’s EXISTS criteria (IF EXISTS ( SELECT * FROM tableA WHERE tableAId = 'value' )) the delete may happen even before the UPDATE, depending on what process first gets the query processor when the lock occurs.

What’s the major problem here, and why is it usually a major problem? Under the default isolation level in which SQL Server connections operate, no lock would have been kept on tableA, leaving a potential hole in your data integrity if another user makes a change to the table before your transaction is complete. Usually, however, if you have checks on tableA that validate the effects on tableB’s integrity, the locks from the modification operations will protect you from integrity issues.

Deeply ingrained in the concepts of isolation levels are the concepts of repeatable reads and phantom rows . Consider that you execute a statement such as the following within a transaction. Logically, it seems like you should get back exactly the same data, but this is not always the case.

 BEGIN TRANSACTION;

 SELECT * FROM table;

And the following rows were returned:

 ColumnName

 -----------

 row1

 row2

For this SELECT statement to claim to support repeatable reads within a transaction, you must be able to execute it multiple times in the transaction and get back at least the same results, possibly more. This means that no other user could change the data that had been retrieved in the operation. Other users are allowed to create new rows, so on a subsequent query to the table, you might get back the following results:

 ColumnName

 -----------

 row1

 row2

 row3

Note that the term “repeatable read” can seem confusing (it does to me, but I had no say in the matter!) because the exact results of the read weren’t repeatable, but that’s how it’s defined. The value row3 is called a phantom row, because it just appears out of nowhere whenever you execute the query a second time.

The following bulleted list contains the isolation levels to adjust how long locks are held to prevent phantom rows and nonrepeatable reads:

  • READ UNCOMMITTED: Doesn’t honor or take locks, unless data is modified.
  • READ COMMITTED: Takes and honors locks, but releases read locks after data is retrieved. Allows phantom rows and nonrepeatable reads.
  • REPEATABLE READ: Holds locks for the duration of the transaction to prevent users from changing data. Disallows nonrepeatable reads but allows phantom rows.
  • SERIALIZABLE: Like REPEATABLE READ, but adds locks on ranges of data to make sure no new data is added. Holds these locks until the transaction is completed. Disallows phantom rows and nonrepeatable reads.
  • SNAPSHOT: Allows the user to look at data as it was when the transaction started (existed as of SQL Server 2005).

The syntax for setting the isolation level is as follows:

 SET TRANSACTION ISOLATION LEVEL <level>;

<level> is any of the five preceding settings. The default isolation level is READ COMMITTED and is a good balance between concurrency and integrity. It does bear mentioning that READ COMMITTED isn’t always the proper setting. Quite often, when only reading data, the SNAPSHOT isolation level gives the best results, though not properly setting up your servers can have some serious performance implications (more on the reasons for that in the section dedicated to SNAPSHOT).

Referring to the previous example code block—checking that a value exists in one table, then modifying another—keep in mind that the types of tables that tableA and tableB represent will greatly affect the need to change the isolation level. In that case, using the REPEATABLE READ isolation level would suffice, because you are looking for the case where the row existed. REPEATABLE READ will allow phantoms, but if one row exists and you add another, existence is still guaranteed if another row is created.

Keep in mind that locks aren’t just held for operations that you directly execute. They can be held for any constraints that fire to check existence in other tables and any code executed in trigger code. The isolation level in effect also controls how long these locks are held. Understanding that fact alone will make you a much better performance tuner, because you won’t just look on the surface but will know to dig deep into the code to figure out what is going on.

When considering solutions, you must keep in mind locking and isolation levels. As more and more critical solutions are being built on SQL Server, it’s imperative to make absolutely sure to protect data at a level that’s commensurate with the value of the data. If you are building procedures to support a system on a space shuttle or a life support system, this becomes more important than it would be in the case of a sales system, a pediatrician’s schedule, or like we set up in Chapter 6, a simple messaging system.

In some cases, losing some data really doesn’t matter. It is up to you when you are designing your system to truly understand that particular system’s needs. In the “Coding for Integrity and Concurrency” section, I’ll look at coding schemes aimed at improving the concurrency of your stored procedure programs.

image  Tip The IF EXISTS() THEN . . . ELSE . . . scenario mentioned earlier cannot be managed simply with isolation levels. In the next section, when I discuss pessimistic locking, I will present a solution using application locks that can be fitted to perform the “perfect” single threading solution.

In the next subsections, I’ll briefly discuss the different isolation levels and demonstrate how they work using the following table. After the specific isolation levels, I will have a section that covers a database setting that alters how the isolation levels work called READ COMMITTED SNAPSHOT as well. (Again, build these in any database that you choose. I’ll create them in tempdb.)

 CREATE TABLE dbo.testIsolationLevel

 (

    testIsolationLevelId int NOT NULL IDENTITY(1,1)

              CONSTRAINT PKtestIsolationLevel PRIMARY KEY,

    value varchar(10) NOT NULL

 );

 INSERT dbo.testIsolationLevel(value)

 VALUES ('Value1'),

        ('Value2'),

image  Tip Just as for locking modes, there are table query hints to apply an isolation level only to a given table in a query, rather than an entire query. These hints are READUNCOMMITTED, READCOMMITTED, REPEATABLEREAD, SNAPSHOT, and SERIALIZABLE, and they behave as their corresponding isolation levels do, only with respect to a single table in a query.

When you are coding or testing, checking to see what isolation level you are currently executing under can be useful. To do this, you can look at the results from sys.dm_exec_sessions :

 SELECT CASE transaction_isolation_level

          WHEN 1 THEN 'Read Uncomitted' WHEN 2 THEN 'Read Committed'

          WHEN 3 THEN 'Repeatable Read' WHEN 4 THEN 'Serializable'

          WHEN 5 THEN 'Snapshot' ELSE 'Unspecified'

       END

 FROM sys.dm_exec_sessions

 WHERE session_id = @@spid;

Unless you have already changed it, the default (and what you should get from executing this query in your connection) is Read Committed. Change the isolation level to serializable like so:

 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

Then, reexecute the query, and the results will now show that the isolation level is currently serializable. In the following sections, I will show you why you would want to change the isolation level at all.

image  Tip I have included all of the code for these chapters in a single file, but you will want to start your own connections for CONNECTION A and CONNECTION B. All of the example code requires multiple connections to execute, in order to allow for concurrency.

READ UNCOMMITTED

Ignore all locks, and don’t issue locks. Queries can see any data that has been saved to the table, regardless of whether or not it’s part of a transaction that hasn’t been committed (hence the name). However, READ UNCOMMITTED still leaves exclusive locks if you do modify data, to keep other users from changing data that you haven’t committed.

For the most part, READ UNCOMMITTED is a good tool for developers to use to check the progress of operations and to look at production systems when SNAPSHOT isn’t available. It should not be used as a performance tuning tool, however, because all of your code should use only committed trustable data that has passed the requirements of the constraints and triggers you have implemented. For example, say you execute the following code on one connection:

 --CONNECTION A

 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; --this is the default, just

                       --setting for emphasis

 BEGIN TRANSACTION

 INSERT INTO dbo.testIsolationLevel(value);

 VALUES('Value3'),

Then, you execute on a second connection:

 --CONNECTION B

 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

 SELECT *

 FROM dbo.testIsolationLevel;

This returns the following results:

 testIsolationLevelId   value

 --------------------   ------

 1                      Value1

 2                      Value2

 3                      Value3

Being able to see locked data is quite valuable, especially when you're in the middle of a long-running process. That’s because you won’t block the process that’s running, but you can see the data being modified. There is no guarantee that the data you see will be correct (it might fail checks and be rolled back), but for looking around and some reporting needs, this data might be good enough.

Finally, commit the transaction you started earlier:

 --CONNECTION A

 COMMIT TRANSACTION;

image  Caution    Ignoring locks using READ UNCOMMITTED is almost never a good way to build highly concurrent database systems! Yes, it is possible to make your applications screamingly fast, because they never have to wait for other processes. There is a reason for this waiting. Consistency of the data you read is highly important and should not be taken lightly. Using SNAPSHOT or READ COMMITTED SNAPSHOT, which I will cover later in the chapter, will give you sort of the same concurrency without reading dirty data.

READ COMMITTED

READ COMMITTED is the default isolation level as far as SQL Server is concerned, and as the name states, it prevents you from seeing uncommitted data. Be careful that your toolset may or may not use it as its default (some toolsets use SERIALIZABLE as the default, which, as you will see is pretty tight and is not great for concurrency). All shared and update locks are released as soon as the process is finished using the resource. Exclusive locks are held until the end of the transaction. Data modifications are usually executed under this isolation level. However, understand that this isolation level isn’t perfect, as there isn’t protection for repeatable reads or phantom rows. This means that as the length of the transaction increases, there’s a growing possibility that some data that was read during the first operations within a transaction might have been changed or deleted by the end of the transaction. It happens extremely rarely when transactions are kept short, so it’s generally considered an acceptable risk—for example:

 --CONNECTION A

 SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

 BEGIN TRANSACTION;

 SELECT * FROM dbo.testIsolationLevel;

You see all the rows in the table from the previous section (though the testIsolationLevelId might be different if you had errors when you built your code). Then, on the second connection, delete a row:

 --CONNECTION B

 DELETE FROM dbo.testIsolationLevel

 WHERE testIsolationLevelId = 1;

Finally, go back to the other connection and execute, still within the transaction:

 --CONNECTION A

 SELECT *

 FROM dbo.testIsolationLevel;

 COMMIT TRANSACTION;

This returns the following results:

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 3                      Value3

The first time you grasp this topic well (hopefully now, but it may take a bit of time for it to sink in), you may very well panic that your data integrity is in trouble. You are right in one respect. There are some holes in the default isolation level. However, since most referential integrity checks are done based on the existence of some data, the impact of READ COMMITTED is lessened by the fact that most operations in an OLTP database system are inserts and updates. The impact is further lessened because relationships are pretty well guarded by the fact that deleting the parent or child row in a relationship requires a lock on the other rows. So if someone tries to modify the parent and someone else tries to modify the child, one process will be blocked by the other.

Beyond the fact that relationships require locked checks in READ COMMITTED isolation, the key to the success of using this isolation level is simple probability. The chances of two users stepping on each other’s processes within milliseconds is pretty unlikely, even less likely is the scenario that one user will do the exact thing that would cause inconsistency. However, the longer your transactions and the higher the concurrent number of users on the system, the more likely that READ COMMITTED will produce anomalies.

In my 18 years of using SQL Server, the primary issues I have found with READ COMMITTED have centered exclusively on checking/retrieving a value and then going back later and using that value. If you do much of that, and it is important that the situation remain the same until you use the value, consider implementing your code using a higher level of isolation.

For example, consider the issues involved in implementing a system to track drugs given to a patient in a hospital. For a system such as this, you’d never want to give a user too much medicine accidentally because when you started a process to set up a schedule via a batch system, a nurse was administering the dosage off schedule. Although this situation is unlikely, as you will see in the next few sections, an adjustment in isolation level would prevent it from occurring at all.

REPEATABLE READ

The REPEATABLE READ isolation level includes protection from data being deleted from under your operation. Shared locks are now held during the entire transaction to prevent other users from modifying the data that has been read. You would be most likely to use this isolation level if your concern is the absolute guarantee of existence of some data when you finish your operation.

As an example on one connection, execute the following statement:

 --CONNECTION A

 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

 BEGIN TRANSACTION;

 SELECT * FROM dbo.testIsolationLevel;

This returns the following:

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 3                      Value3

Then, on a different connection, run the following:

 --CONNECTION B

 INSERT INTO dbo.testIsolationLevel(value)

 VALUES ('Value4'),

This executes, but try executing the following code:

 --CONNECTION B

 DELETE FROM dbo.testIsolationLevel

 WHERE value = 'Value3';

You go into a blocked state: CONNECTION B will need an exclusive lock on that particular value, because deleting that value would cause the results from CONNECTION A to return fewer rows. Back on the first connection, run the following code:

 --CONNECTION A

 SELECT * FROM dbo.testIsolationLevel;

 COMMIT TRANSACTION;

This will return the following:

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 3                      Value3

 4                      Value4

And immediately, the batch on the other connection will complete. Now, view the data (from either connection):

 --CONNECTION A

 SELECT * FROM dbo.testIsolationLevel;

This returns

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 4                      Value4

The fact that other users may be changing the data you have locked can be a very serious concern for the perceived integrity of the data. If the user on connection A goes right back and the row is deleted, that user will be confused. Of course, nothing can really be done to solve this problem, as it is just a fact of life. In the “Optimistic Locking” section, I will present a method of making sure that one user doesn’t crush the changes of another; the method could be extended to viewed data, but generally, this is not the case for performance reasons.

In the end, you can implement most any scheme to protect the data, but all you are doing is widening the window of time where users are protected. No matter what, once the user relinquishes transactional control on a row, it will be fair game to other users without some form of workflow system in place (a topic that is well beyond the scope of my book, though once you are finished reading this book, you could design and create one!).

SERIALIZABLE

SERIALIZABLE takes everything from REPEATABLE READ and adds in phantom-row protection. SQL Server accomplishes this by taking locks not only on existing data that it has read but on any ranges of data that could match any SQL statement executed. This is the most restrictive isolation level and is the best in any case where data integrity is absolutely necessary. It can cause lots of blocking; for example, consider what would happen if you executed the following query under the SERIALIZABLE isolation level:

 SELECT *

 FROM dbo.testIsolationLevel;

No other user will be able to modify the table until all rows have been returned and the transaction it was executing within (implicit or explicit) is completed.

image  Note    Be careful. I said, “No other user will be able to modify the table . . . .” I didn’t say “read.” Readers leave shared locks, not exclusive ones. This caveat is something that can be confusing at times when you are trying to write safe but concurrent SQL code. Other users could fetch the rows into cache, make some changes in memory, and then write them later. We will look at techniques to avoid this issue later in this chapter as we discuss optimistic locking techniques.

If lots of users are viewing data in the table under any of the previously mentioned isolation levels, it can be difficult to get any modifications done. If you’re going to use SERIALIZABLE, you need to be careful with your code and make sure it only uses the minimum number of rows needed (especially if you are not using SNAPSHOT isolation level for read processes, as covered in the next section).

Execute this statement on a connection to simulate a user with a table locked:

 --CONNECTION A

 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

 BEGIN TRANSACTION;

 SELECT * FROM dbo.testIsolationLevel;

Then, try to add a new row to the table:

 --CONNECTION B

 INSERT INTO dbo.testIsolationLevel(value)

 VALUES ('Value5'),

Your insert is blocked. Commit the transaction on the A connection:

 --CONNECTION A

 SELECT * FROM dbo.testIsolationLevel;

 COMMIT TRANSACTION;

This returns the following:

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 4                      Value4

The results are the same. However, this unblocks CONNECTION B, and running the SELECT again, you will see that the contents of the table are now the following:

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 4                      Value4

 5                      Value5

It is important to be careful with the SERIALIZABLE isolation level. I can't stress enough that multiple readers can read the same data, but no one can update it while others are reading. Too often, people take this to mean that they can read some data and be guaranteed that no other user might have read it also, leading occasionally to inconsistent results and more frequently to deadlocking issues.

SNAPSHOT

SNAPSHOT isolation was one of the major cool new features in SQL Server 2005, and it continues to be one of my favorites (particularly the READ COMMITTED SNAPSHOT variant that is mentioned later in this section). It lets you read the data as it was when the transaction started, regardless of any changes. It’s a special case, because although it doesn’t technically allow for phantom rows, nonrepeatable reads, or dirty reads from any queries within the transaction, it doesn’t necessarily represent the current state of the data. You might check a value in a table at the beginning of the transaction and it’s in the physical table, but later, you requery the table and it is no longer there. As long as you are inside the same transaction, even though the value exists in your virtual table, it needn’t exist in the physical table any longer (in fact, the physical table needn’t exist either!). This provides that the results of your query will reflect a consistent state of the database at some time, which is generally very desirable.

What makes SNAPSHOT particularly useful is that it doesn’t use locks in the normal way, because it looks at the data as it was at the start of the transaction. Modifying data under this isolation level has its share of problems, which I’ll demonstrate later in this section. However, I don’t want completely to scare you off, as this isolation level can become a major part of a highly concurrent design strategy (particularly useful for reads in an optimistic locking strategy, which the last sections of this chapter cover).

The largest downside is the effect it can have on performance if you are not prepared for it. This history data is written not only to the log, but the data that will be used to support other users that are in a SNAPSHOT isolation level transaction is written to the tempdb. Hence, if this server is going to be very active, you have to make sure that tempdb is up to the challenge, especially if you’re supporting large numbers of concurrent users.

The good news is that, if you employ the strategy of having readers use SNAPSHOT isolation level , data readers will no longer block data writers (in any of the other isolation levels), and they will always get a transactionally consistent view of the data. So when the vice president of the company decides to write a 20-table join query in the middle of the busiest part of the day, all other users won’t get stuck behind him with data locks. The better news is that he won’t see the mistaken ten-million-dollar entry that one of the data-entry clerks added to the data that the check constraint hasn’t had time to deny yet (the vice president would have seen the error if you were using the READ UNCOMMITTED solution, which is the unfortunate choice of many novice performance tuners). The bad news is that eventually the vice president’s query might take up all the resources and cause a major system slowdown that way. (Hey, if it was too easy, companies wouldn’t need DBAs. And I, for one, wouldn’t survive in a nontechnical field.)

To use (and demonstrate) SNAPSHOT isolation level, you have to alter the database you’re working with (you can even do this to tempdb):

 ALTER DATABASE tempDb

     SET ALLOW_SNAPSHOT_ISOLATION ON;

Now, the SNAPSHOT isolation level is available for queries.

image  Caution    The SNAPSHOT isolation level uses copies of affected data placed into tempdb. Because of this, you should make sure that your tempdb is set up optimally.

Let’s look at an example. On the first connection, start a transaction and select from the -testIsolationLevel table:

 --CONNECTION A

 SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

 BEGIN TRANSACTION;

 SELECT * FROM dbo.testIsolationLevel;

This returns the following results:

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 4                      Value4

 5                      Value5

On a second connection, run the following:

 --CONNECTION B

 SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

 INSERT INTO dbo.testIsolationLevel(value)

 VALUES ('Value6'),

This executes with no waiting. Going back to Connection A, reexecuting the SELECT returns the same set as before, so the results remain consistent. On Connection B, run the following DELETE statement:

 --CONNECTION B

 DELETE FROM dbo.testIsolationLevel

 WHERE value = 'Value4';

This doesn't have to wait either. Going back to the other connection again, nothing has changed.

 --CONNECTION A

 SELECT * FROM dbo.testIsolationLevel;

This still returns

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 4                      Value4

 5                      Value5

So what about modifying data in SNAPSHOT isolation level? If no one else has modified the row, you can make any change:

 --CONNECTION A

 UPDATE dbo.testIsolationLevel

 SET value = 'Value2-mod'

 WHERE testIsolationLevelId = 2;

This runs, but going back to the B connection, if you try to select this row

 --CONNECTION B

 SELECT * FROM dbo.testIsolationLevel

you will find the query is blocked, and the connection is forced to wait, because this row is new and has an exclusive lock on it, and connection B is not in SNAPSHOT ISOLATION level.

Commit the transaction in CONNECTION A, and you’ll see rows such as these:

 --CONNECTION A

 COMMIT TRANSACTION;

 SELECT * FROM dbo.testIsolationLevel;

This returns the current contents of the table:

 testIsolationLevelId   value

 --------------------   ------

 2                      Value2

 5                      Value5

 6                      Value6

The messy and troubling bit with modifying data under the SNAPSHOT isolation level is what happens when one user modifies a row that another user has also modified and committed the transaction for. To see this, in CONNECTION A run the following, simulating a user fetching some data into the cache:

 --CONNECTION A

 SET TRANSACTION ISOLATION LEVEL SNAPSHOT;

 BEGIN TRANSACTION;

 --touch the data

 SELECT * FROM dbo.testIsolationLevel;

This returns the same results as just shown. Then, a second user changes the value:

 --CONNECTION B

 SET TRANSACTION ISOLATION LEVEL READ COMMITTED; --any will do

 UPDATE dbo.testIsolationLevel

 SET value = 'Value5-mod'

 WHERE testIsolationLevelId = 5; --might be different surrogate key value in yours

Next, the user on CONNECTION A tries to update the row also:

 --CONNECTION A

 UPDATE dbo.testIsolationLevel

 SET value = 'Value5-mod'

 WHERE testIsolationLevelId = 5; --might be different in yours

As this row has been deleted by a different connection, the following error message rears its ugly head:

 Msg 3960, Level 16, State 2, Line 2

 Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table 'dbo.testIsolationLevel' directly or indirectly in database 'tempdb' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement.

As such, strictly for simplicity’s sake, I recommend that almost all retrieval-only operations can execute under the SNAPSHOT isolation level, and the procedures that do data modifications execute under the READ COMMITTED isolation level. As long as data is only read, the connection will see the state of the database as it was when the data was first read.

A strong word of caution—if you do data validations under SNAPSHOT isolation level and you do data-checking logic such as in a trigger or procedure, the data might already be invalid in the live database, especially if the transaction runs long. This invalid data is far worse than REPEATABLE READ, where the data is always valid when the check is done but might be changed after the violation. However, you should note that FOREIGN KEY constraints, when doing a modification, are smart enough to use the same sorts of locks as READ COMMITTED would to protect against this sort of issue. My suggestion (if you ignore the suggestion not to do writes under SNAPSHOT) would be to manually code SET TRANSACTION ISOLATION LEVEL READ COMMITTED or REPEATABLE READ or SERIALIZABLE where necessary in your modification procedures or triggers to avoid this sort of issue.

READ COMMITTED SNAPSHOT (Database Setting)

The database setting READ_COMMITTED_SNAPSHOT changes the isolation level of READ COMMITTED to behave very much like SNAPSHOT isolation level on a statement level.

Note that I said “statement” and not “transaction.” In SNAPSHOT isolation level, once you start a transaction, you get a consistent view of the database as it was when the transaction started until you close it. READ_COMMITTED_SNAPSHOT gives you a consistent view of the database for a single statement. Set the database into this mode as follows:

 --must be no active connections other than the connection executing

 --this ALTER command Does not require ALLOW_SNAPSHOT_ISOLATION enabled.

 ALTER DATABASE <databasename>

    SET READ_COMMITTED_SNAPSHOT ON;

When you do this, every statement is now in SNAPSHOT isolation level by default. For example, imagine you’re at the midpoint of the following pseudo-batch:

 BEGIN TRANSACTION;

 SELECT column FROM table1;

 --midpoint

 SELECT column FROM table1;

 COMMIT TRANSACTION;

If you’re in SNAPSHOT isolation level, table1 could change completely—even get dropped—and you wouldn’t be able to tell when you execute the second SELECT statement. You’re given a consistent view of the database for reading. With the READ_COMMITTED_SNAPSHOT database setting turned on, in a READ COMMITTED isolation level transaction, your view of table1 would be consistent with how it looked when you started reading, but when you started the second pass through the table, it might not match the data the first time you read through. This behavior is similar to plain READ COMMITTED, except that you don’t see any new phantoms or nonrepeatable reads while retrieving rows produced during the individual statement (other users can delete and add rows while you scan through the table, but you won’t be affected by the changes), and SQL Server doesn’t need to take locks or block other users.

Using READ_COMMITTED_SNAPSHOT can actually perform tremendously better than just using READ COMMITTED, though it does suffer from the same (or maybe worse) issues with data. You should remember that in the previous section on READ COMMITTED, I noted that because SQL Server releases locks immediately after reading the data, another user could come behind you and change data that you just finished using to do some validation. This same thing is true for READ_COMMITTED_SNAPSHOT, but the window of time can be slightly longer because SQL Server reads only history as it passes through different tables. This amount of time is generally insignificant and usually isn’t anything to worry about, but it can be important based on the type of system you’re creating.

For places where you might need more safety, consider using the higher isolation levels, such as REPEATABLE READ or SERIALIZABLE. I would certainly suggest that, in the triggers and modification procedures that you build using this isolation level, you consider the upgraded isolation level. The best part is that basic readers who just want to see data for a query or report will not be affected. Later in this chapter, when I present the mechanism for optimistic locking, you will see that whether or not a reading user gets an old version doesn’t really matter. Users will never be allowed to modify anything but the rows that look exactly like the ones that they fetched.

SNAPSHOT isolation level and the READ_COMMITTED_SNAPSHOT settings are very important aspects of SQL Server’s concurrency feature set. They cut down on blocking and the need to use the dirty reads to look at active OLTP data for small reports and for read-only queries to cache data for user-interface processes.

image  Note    READ COMMITTED SNAPSHOT is the feature that saved one of the major projects I worked on after version 2005 was released. We tried and tried to optimize the system under basic READ COMMITTED, but it was not possible, mostly because we had no control over the API building the queries that were used to access the database.

Coding for Integrity and Concurrency

When building database systems, you must consider that multiple users will be attempting to modify your data at the same time. So far in this chapter, I’ve talked at length about the different mechanisms, such as transactions, isolation levels, and so on, for protecting your data. Now, I’ll present some of the coding mechanisms that keep your users from stepping on one another.

The general progression of events for most applications is the same: fetch some data for a user or a process to look at, operate on this data, make changes to the data, or make some decision based on the retrieved values. Once the users have performed their operations, they’ll either commit their changes to the database or possibly save data to a different table based on their decisions.

Our coding decisions generally surround how to deal with the lag time while the users have the data cached on their clients. For example, what happens if a different user wants to access and make a change to the same data?

For this situation, you can use a couple common schemes while coding your database application:

  • Pessimistic locking: Assume it’s likely that users will try to modify the same data, so single-thread access to important resources.
  • Optimistic locking: Assume it’s unlikely that users will try to modify the exact same row at the same time other users want to. Only verify that the cached data is valid when a user wants to change the data.

Using one or parts of both of these schemes, it’s usually possible to protect data in a multiuser system at an acceptable level of integrity and concurrency.

Pessimistic Locking

A pessimistic locking scheme is restrictive. Generally, the idea is straightforward: put a lock on a resource, keeping all others out; do your work with the resource; then, release the lock. The goal is to eliminate any chance of contention. In SQL code, the process is a bit difficult to get right, as you will see, but the basics are as follows: begin a transaction, most likely a serializable one; fetch the data; manipulate the data; modify the data; and finally, commit the transaction. The goal is to serialize or single-thread all access to the resource in which the process is interested, making sure that no other user can modify or even view the data being worked on.

The most significant concern is blocking all access to given resources. This sounds easy and reasonable, but the main issue is that any query to a locked resource has to wait for the user to complete access. Even if the parts of the resource won’t be involved in the answer to a query, if a locked resource might be involved, unnecessary blocking may occur.

For example, say one user has a single row locked in a table. The next user executes a different query that requires a table scan on the same table. Even if the results of this query needn’t use the locked row, the second user will be blocked until the other connection has completed, because SQL Server won’t know if the next user needs the row until it’s unlocked.

image  Note    You might be thinking that SQL Server could simply check to see if the locked resource would be needed. However, this cannot be known, because once a row is locked with a noncompatible lock, all other users must assume that the values might change. Hence, you’re forced to wait until the lock is dropped.

Any users who need data that this next user has locked also have to wait, and soon, a chain of users is waiting on one particular user. Except for one thing—time—all this might even be reasonable. If the lock only lasted milliseconds (or possibly seconds), this could be fine for many systems. Small applications based on file managers have implemented concurrency this way for years. However, what if the user decides to take a break? (This can be a common issue with smaller systems.) All other users have to wait until this user finishes accessing the data, and if this user has modified one piece of data (possibly with complex triggers) and still has more to go, access might be blocked to most of the system data because that user was forgetful and didn’t click the Save button.

It’s possible to relieve some of the long-term stress on the system by reducing the amount of time for which locks can be held, such as setting time limits on how long the user can keep the data before rolling back the transaction. However, either way, it’s necessary to block access to large quantities of data for a more-than-reasonable period of time, because you’d need to lock any domain tables that the users will rely on to choose values for their tables so they change no related table values or any related data that other users might need.

Implementing pessimistic locks isn’t all that easy, because you have to go out of your way to force locks on data that keep other users from even viewing the data. One method is to lock data using exclusive lock hints, coupled with the SERIALIZABLE isolation level, when you fetch data and maintain the connection to the server as you modify the data. This approach is messy and will likely cause lots of undesired locks if you aren’t extremely careful how you write queries to minimize locking.

image  Caution    If the page has not been dirtied, even if an exclusive lock exists on the row or page, another reader can get access to the row for viewing. You need to actually modify (even to the same value) the row to dirty the page if you want to hold the lock, or use a PAGELOCK and XLOCK hint (see Microsoft Knowledgebase Article 324417), though doing so will lock the entire page.

SQL Server does have a built-in method you can use to implement a form of pessimistic locking: SQL Server application locks. These locks, just like other locks, must be taken inside a transaction (executing the procedures without a transaction will get you a nasty error message). The real downside is that enforcement and compliance are completely optional. If you write code that doesn’t follow the rules and use the proper application lock, you will get no error letting you know. The commands that you have to work with application locks are as follows:

  • sp_getAppLock: Use this to place a lock on an application resource. The programmer names application resources, which can be named with any string value. In the string, you could name single values or even a range.
  • sp_releaseAppLock: Use this to release locks taken inside a transaction.
  • APPLOCK_MODE: Use it to check the mode of the application lock.
  • APPLOCK_TEST: Use this one to see if you could take an application lock before starting the lock and getting blocked.

As an example, we’ll run the following code. We’ll implement this on a resource named 'invoiceId=1', which represents an invoice that we’ll lock. We’ll set it as an exclusive lock so no other user can touch it. In one connection, we run the following code:

 --CONNECTION A

 BEGIN TRANSACTION;

    DECLARE @result int

    EXEC @result = sp_getapplock @Resource = 'invoiceId=1', @LockMode = 'Exclusive';

    SELECT @result;

This returns 0, stating that the lock was taken successfully. Now, if another user tries to execute the same code to take the same lock, the second process has to wait until the first user has finished with the resource 'invoiceId=1':

 --CONNECTION B

 BEGIN TRANSACTION;

    DECLARE @result int;

    EXEC @result = sp_getapplock @Resource = 'invoiceId=1', @LockMode = 'Exclusive';

    SELECT @result;

This transaction has to wait. Let’s cancel the execution, and then execute the following code using the APPLOCK_TEST function (which has to be executed in a transaction context) to see if we can take the lock (allowing the application to check before taking the lock):

 --CONNECTION B

 BEGIN TRANSACTION

 SELECT APPLOCK_TEST('public','invoiceId=1','Exclusive','Transaction')

                       as CanTakeLock

 ROLLBACK TRANSACTION

This returns 0, meaning we cannot take this lock currently. APPLOCKs can be a great resource for building locks that are needed to implement locks that are “larger” than just SQL Server objects. The key is that every user that is to participate in the APPLOCK must implement the locking mechanism, and every user must honor the locks taken. In the next section, I will show you a very common and useful technique using APPLOCKs to create a pessimistic lock based on the application lock to single thread access to a given block of code.

image  Tip You can use application locks to implement more than just pessimistic locks using different lock modes other than exclusive, but exclusive is the mode you’d use to implement a pessimistic locking mechanism. For more information about application locks, SQL Server Books Online gives some good examples and a full reference to using application locks.

Implementing a Single-Threaded Code Block

The problem of the critical section is a very common problem. Very often, it is troublesome for more than one connection to have access to a given section of code. For example, you might need to fetch a value, increment it, and keep the result unique among other callers that could be calling simultaneously.

The general solution to the single threading problem is to exclusively lock the resources that you need to be able to work with, forcing all other users to wait even for reading. In some cases, this technique will work great, but it can be troublesome in cases like the following:

  • The code is part of a larger set of code that may have other code locked in a transaction, blocking users’ access to more than you expect. You are allowed to release the application lock in the transaction to allow other callers to continue.
  • Only one minor section of code needs to be single threaded, and you can allow simultaneous access otherwise.
  • The speed in which the data is accessed is so fast that two processes are likely to fetch the same data within microseconds of each other.
  • The single threading is not for table access. For example, you may want to write to a file of some sort or use some other resource that is not table based.

The following technique will leave the tables unlocked while manually single threading access to a code block (in this case, getting and setting a value), using an application lock to lock a section of code.

image  Note    An application lock must be used and honored manually in every piece of code where the need to lock the data matters, so there is a loss of safety associated with using application locks rather than data-oriented locks. If there is any concern with what other processes might do, be sure to still assign proper concurrency and locking hints to that code also.

To demonstrate a very common problem of building a unique value without using identities (for example, if you have to create an account number with special formatting/processing), I have created the following table:

 CREATE TABLE applock

 (

    applockId int NOT NULL CONSTRAINT PKapplock PRIMARY KEY,

                       --the value that we will be generating

                       --with the procedure

    connectionId int NOT NULL, --holds the spid of the connection so you can

                       --who creates the row

    insertTime datetime2(3) NOT NULL DEFAULT (SYSDATETIME()) --the time the row was created, so

                       --you can see the progression

 );

Next, a procedure that starts an application lock fetches some data from the table, increments the value, and stores it in a variable. I added a delay parameter, so you can tune up the problems by making the delay between incrementing and inserting more pronounced. There is also a parameter to turn on and off the application lock (noted as @useApplockFlag in the parameters), and that parameter will help you test to see how it behaves with and without the application lock.

 CREATE PROCEDURE applock$test

 (

       @connectionId int,

       @useApplockFlag bit = 1,

       @stepDelay varchar(10) = '00:00:00'

 ) as

 SET NOCOUNT ON

 BEGIN TRY

    BEGIN TRANSACTION

       DECLARE @retval int = 1;

       IF @useApplockFlag = 1 --turns on and off the applock for testing

          BEGIN

             EXEC @retval = sp_getapplock @Resource = 'applock$test',

                       @LockMode = 'exclusive';

             IF @retval < 0

                BEGIN

                   DECLARE @errorMessage nvarchar(200);

                   SET @errorMessage = CASE @retval

                       WHEN -1 THEN 'Applock request timed out.'

                       WHEN -2 THEN 'Applock request canceled.'

                       WHEN -3 THEN 'Applock involved in deadlock'

 ELSE 'Parameter validation or other call error.'

                       END;

                   THROW 50000,@errorMessage,16;

                END;

          END;

    --get the next primary key value. Reality case is a far more complex number generator

    --that couldn't be done with a sequence or identity

    DECLARE @applockId int ;

    SET @applockId = COALESCE((SELECT MAX(applockId) FROM applock),0) + 1 ;

    --delay for parameterized amount of time to slow down operations

    --and guarantee concurrency problems

    WAITFOR DELAY @stepDelay;

    --insert the next value

    INSERT INTO applock(applockId, connectionId)

    VALUES (@applockId, @connectionId);

    --won't have much effect on this code, since the row will now be

    --exclusively locked, and the max will need to see the new row to

    --be of any effect.

    IF @useApplockFlag = 1 --turns on and off the applock for testing

       EXEC @retval = sp_releaseapplock @Resource = 'applock$test';

    --this releases the applock too

    COMMIT TRANSACTION;

 END TRY

 BEGIN CATCH

    --if there is an error, roll back and display it.

    IF @@trancount > 0

       ROLLBACK transaction;

       SELECT cast(error_number() as varchar(10)) + ':' + error_message();

 END CATCH

Now, you can set up a few connections using this stored procedure, attempting multiple connections first without the application lock and then with it. Since we’re running the procedure in such a tight loop, it is not surprising that two connections will often get the same value and try to insert new rows using that value when not using the APPLOCK:

 --test on multiple connections

 WAITFOR TIME '21:47'; --set for a time to run so multiple batches

                   --can simultaneously execute

 go

 EXEC applock$test @connectionId = @@spid

             ,@useApplockFlag = 0 -- <1=use applock, 0 = don't use applock>

             ,@stepDelay = '00:00:00.001'--'delay in hours:minutes:seconds.parts of seconds';

 GO 10000 --runs the batch 10000 times in SSMS

You will probably be amazed at how many clashes you get when you have application locks turned off. Doing 10,000 iterations of this procedure on three connections on a Core2 Duo, 1.3 GHz-laptop with 0 for the APPLOCK parameter, I got over 1,000 clashes pretty much constantly (evidenced by an error message: 2627:Violation of PRIMARY KEY constraint. . . Cannot insert duplicate key in object 'dbo.applock'. . .). With application locks turned on, all rows were inserted in a slightly longer than the original time, without any clashes.

To solidify the point that every connection has to follow the rules, turn off application locks on a connection or two and see the havoc that will result. The critical section will now no longer be honored, and you will get tons of clashes quickly, especially if you use the @stepDelay parameter to slow the process down.

This is not the only method of implementing the solution to the incrementing values problem. The more common method is to change the code where you get the maximum value to increment and apply locking hints:

 SET @applockId =

          COALESCE((SELECT MAX(applockId)

    FROM APPLOCK WITH (UPDLOCK,PAGLOCK)),0) + 1;

Changing the code to do this will cause update locks to be held because of the UPDLOCK hint, and the PAGLOCK hint causes page locks to be held (SQL Server can ignore locks when a row is locked and it has not been modified, even if the row is exclusively locked).

The solution I presented is a very generic one for single threading a code segment in T-SQL code, allowing that the one procedure is the only one single threading. It does not take any locks that will block others until it needs to update the data (if there is no changing of data, it won’t block any other users, ever). This works great for a hotspot where you can clearly cordon off the things being utilized at a given level, like in this example, where all users of this procedure are getting the maximum of the same rows.

Optimistic Locking

The opposite of pessimistic locking is optimistic locking (I make this statement merely to win the obvious statement of the year award). Here, the premise is simply to assume that the likelihood of users stepping on one another is limited. Instead of locking resources, locks are only taken during actual data-modification activities, because most of the time, users just look around, and even if the data they’re looking at is slightly out of date, it won’t hurt anything (this is where the SNAPSHOT isolation level is perfect). In the case where two users do try to update the same data, we usually do something to make sure that one user’s updates don’t overwrite the others. Generally speaking, when the likelihood of two users editing the same data is reasonably low, an optimistic locking scheme can be extremely good for increasing concurrent access to the data. If this is not the case, some form of pessimistic locking might be a better choice.

As an example, it is very unlikely that the same person is calling into your sales call center on two lines talking to two different users. Even scenarios like giving a customer an inventory amount is a place where a sort of optimistic lock is acceptable. If you tell a customer that you have 1,000 of some item on hand and Joey Bigbucks walks up and buys all 1,000 of them, your first customer will be left out in the cold. Use a pessimistic lock (like the application lock example) when you need to implement that critical section of the code in which inventory is being decremented (or even selectively implement a pessimistic lock when your inventory levels are getting low, so you can tell the other customer that you might have inventory).

The idea behind optimistic locking is that, in all cases, the data is only locked at the point where the user modifies the data. Data is protected in the server using constraints, triggers, and so on. Choose the best isolation level depending on how important perfection is. That’s because, as noted in the “Isolation Levels” section, the default of READ UNCOMMITTED is flawed because for some amount of time (hopefully milliseconds), it leaves open the possibility that one user can change data on which your transaction is dependent. For the most part, using the default is considered appropriate, because it greatly enhances concurrency, and the probability of someone modifying data that your transaction is reliant on is close to the chances of being hit by lightning on ten sunny days in a row. It could happen, but it’s a slim chance.

Thinking back to the normal progression of events when a user works in an application: the user fetches data, modifies data, and finally, commits data to the database. There can easily be a long interval between fetching the data and committing the changes to the database. In fact, it’s also possible that other users could have also fetched and modified the data during the same period of time. Because of this, you need to implement some method to make sure that the data that the client originally fetched matches the data that’s stored in the database. Otherwise, the new changes could trample important changes made by another user.

Instead of locking the data by using SQL Server locks, simply employ one of the following schemes to leave data unlocked after it has been fetched to the client and the user makes desired changes:

  • Unchecked: Just let it happen. If two users modify the same row in the database, the last user wins. This is not the best idea, as the first user might have had something important to say, and this method rejects the first user’s changes. I won’t cover this any further because it’s straightforward. Note that many systems use no locking mechanism at all and just let clashes happen, and their users are as happy as they can be. Such an approach is never what I suggest for controlling your important data resources, but so far, I have not been elected data emperor, just data architect.
  • Row-based: Protect your data at the row level by checking to see if the rows being modified are the same as in the table. If not, refresh the data from the table, showing the user what was changed. When optimistic locking is actually implemented, this is by far the most common method used.
  • Logical unit of work: A logical unit of work is used to group a parent record with all its child data to allow a single optimistic lock to cover multiple tables. For example, you’d group an invoice and the line items for that invoice. Treat modifications to the line items the same way as a modification to the invoice, for locking purposes.

Although it isn’t typically a good idea to ignore the problem of users overwriting one another altogether, this is a commonly decided upon method for some companies. On the other hand, the best plan is optimally a mixture of the row-based solution for most tables and a logical unit of work for major groups of tables that make up some common object.

In the following sections, I’ll cover row-based locking and the logical unit of work. The unchecked method ignores the concern that two people might modify the same row twice, so there’s no coding (or thinking!) required.

Row-Based Locking

You must implement a row-based scheme to check on a row-by-row basis whether or not the data that the user has retrieved is still the same as the one that’s in the database. The order of events now is fetch data, modify data, check to see that the row (or rows) of data are still the same as they were, and then commit the changes.

There are three common methods to implement row-based optimistic locking:

  • Check all columns in the table: If you cannot modify the table structure, which the next two methods require, you can check to make sure that all the data you had fetched is still the same and then modify the data. This method is the most difficult, because any modification procedure you write must contain parameters for the previous values of the data, which isn’t a good idea. Checking all columns is useful when building bound data-grid types of applications, where there are direct updates to tables, especially if not all tables can follow the rather strict rules of the next two methods.
  • Add a time column to the table: Set the point in time value when the row is inserted and subsequently updated. Every update to the table is required to modify the value in the table to set the rowLastModifiedTime column. Generally, it’s best to use a trigger for keeping the column up to date, and often, it’s nice to include a column to tell which user last modified the data (you need someone to blame!). Later in this section, I’ll demonstrate a simple INSTEAD OF trigger to support this approach.
  • Use a rowversion (also known as timestamp ) column: In the previous method, you used a manually controlled value to manage the optimistic lock value. This method uses column with a rowversion datatype. The rowversion datatype automatically gets a new value for every command used to modify a given row in a table.

The next two sections cover adding the optimistic lock columns to your tables and then using them in your code.

Adding Optimistic Lock Columns

In this section, we’ll add an optimistic lock column to a table to support either adding the datetime column or the rowversion column. The first method mentioned, checking all columns, needs no table modifications.

As an example, let’s create a new simple table, in this case hr.person (again, use any database you like; the sample uses tempdb). Here’s the structure:

 CREATE SCHEMA hr;

 GO

 CREATE TABLE hr.person

 (

     personId int NOT NULL IDENTITY(1,1) CONSTRAINT PKperson primary key,

     firstName varchar(60) NOT NULL,

     middleName varchar(60) NOT NULL,

     lastName varchar(60) NOT NULL,

     dateOfBirth date NOT NULL,

     rowLastModifyTime datetime2(3) NOT NULL

        CONSTRAINT DFLTperson_rowLastModifyTime DEFAULT (SYSDATETIME()),

     rowModifiedByUserIdentifier nvarchar(128) NOT NULL

        CONSTRAINT DFLTperson_rowModifiedByUserIdentifier default suser_sname()

 );

Note the two columns for our optimistic lock, named rowLastModifyTime and rowModifiedByUserIdentifier. I’ll use these to hold the last date and time of modification and the SQL Server’s login name of the principal that changed the row. There are a couple ways to implement this:

  • Let the manipulation layer manage the value like any other column: This is often what client programmers like to do, and it’s acceptable, as long as you’re using trusted computers to manage the timestamps. I feel it’s inadvisable to allow workstations to set such values, because it can cause confusing results. For example, say your application displays a message stating that another user has made changes, and the time the changes were made is in the future, based on the client’s computer. Then the user checks out his or her PC clock, and it’s set perfectly.
  • Using SQL Server code: For the most part, triggers are implemented to fire on any modification to data.

As an example of using SQL Server code (my general method of doing this), I’ll implement an INSTEAD OF trigger on the update of the hr.person table (note that the errorLog$insert procedure was created back in Chapter 6 and has been commented out for this demonstration in case you don’t have it available):

 CREATE TRIGGER hr.person$InsteadOfUpdate

 ON hr.person

 INSTEAD OF UPDATE AS

 BEGIN

    --stores the number of rows affected

    DECLARE @rowsAffected int = @@rowcount,

          @msg varchar(2000) = ''; --used to hold the error message

    SET NOCOUNT ON; --to avoid the rowcount messages

    SET ROWCOUNT 0; --in case the client has modified the rowcount

    BEGIN TRY

          --[validation blocks]

          --[modification blocks]

          --remember to update ALL columns when building instead of triggers

          UPDATE hr.person

          SET firstName = inserted.firstName,

              middleName = inserted.middleName,

              lastName = inserted.lastName,

              dateOfBirth = inserted.dateOfBirth,

              rowLastModifyTime = default, -- set the value to the default

              rowModifiedByUserIdentifier = default

              FROM hr.person

                 JOIN inserted

                       on hr.person.personId = inserted.personId;

    END TRY

       BEGIN CATCH

             IF @@trancount > 0

                ROLLBACK TRANSACTION;

             THROW; --will halt the batch or be caught by the caller's catch block

       END CATCH

 END

Then, insert a row into the table:

 INSERT INTO hr.person (firstName, middleName, lastName, dateOfBirth)

 VALUES ('Paige','O','Anxtent','19391212'),

 SELECT *

 FROM hr.person;

Now, you can see that the data has been created:

personId firstName middleName lastName dateOfBirth
----------------- ----------------- ----------------- ----------------- -----------------
1 Paige 0 Anxtent 1939-12-12

 rowLastModifyTime          rowModifiedByUserIdentifier

 -----------------------    -------------------------

 2012-02-19 22:29:56.389    DENALI-PCAlienDrsql

Next, update the row:

 UPDATEhr.person

 SET middleName = 'Ona'

 WHERE personId = 1;

 SELECT rowLastModifyTime

 FROM hr.person;

You should see that the update date has changed (in my case, it was pretty doggone late at night, but such is the life):

 rowLastModifyTime

 -----------------------

 2012-02-19 22:30:41.664

If you want to set the value on insert, or implement rowCreatedByDate or userIdentifier columns, the code would be similar. Because this has been implemented in an INSTEAD OF trigger, the user or even the programmer cannot overwrite the values, even if they include it in the column list of an INSERT.

As previously mentioned, the other method that requires table modification is to use a rowversion column. In my opinion, this is the best way to go, and I almost always use a rowversion. I usually have the row modification columns on there as well, for the user’s benefit. I find that the modification columns take on other uses and have a tendency to migrate to the control of the application developer, and rowversion columns never do. Plus, even if the triggers don’t make it on the table for one reason or another, the rowversion column continues to work. Sometimes, you may be prohibited from using INSTEAD OF insert triggers for some reason (recently, I couldn’t use them in a project I worked on because they invalidate the identity functions).

image  Tip You might know this as the timestamp datatype, which is what it has been named since the early days of SQL Server. In the ANSI standard, a timestamp column is a date and time value. rowversion is a much better name for the datatype.

Let’s add a rowversion column to our table to demonstrate using it as an optimistic lock:

 ALTER TABLE hr.person

    ADD rowversion rowversion;

 GO

 SELECT personId, rowversion

 FROM hr.person;

You can see now that the rowversion has been added and magically updated:

 personId    rowversion

 ---------   ------------------

 1           0x00000000000007D6

Now, when the row gets updated, the rowversion is modified:

 UPDATE hr.person

 SET firstName = 'Paige' --no actual change occurs

 WHERE personId = 1;

Then, looking at the output, you can see that the value of the rowversion has changed:

 SELECT personId, rowversion

 @@FROM hr.person;

This returns the following result:

 personId    rowversion

 ---------   ------------------

 1           0x00000000000007D6

image  Caution    You aren’t guaranteed anything when it comes to rowversion values, other than that they’ll be unique in a database. Avoid using the value for any reason other than to tell when a row has changed.

Coding for Row-Level Optimistic Locking

Next, include the checking code in your stored procedure. Using the hr.person table previously created, the following code snippets will demonstrate each of the methods (note that I’ll only use the optimistic locking columns germane to each example and won’t include the others).

Check all the cached values for the columns:

 UPDATE hr.person

 SET firstName = 'Headley'

 WHERE personId = 1 --include the key

    and firstName = 'Paige'

    and middleName = 'ona'

    and lastName = 'Anxtent'

    and dateOfBirth = '19391212';

It’s a good practice to check your rowcount after an update with an optimistic lock to see how many rows have changed. If it is 0, you could check to see if the row exists with that primary key:

 IF EXISTS ( SELECT *

          FROM hr.person

          WHERE personId = 1) --check for existence of the primary key

    --raise an error stating that the row no longer exists

 ELSE

  --raise an error stating that another user has changed the row

Use a date column:

 UPDATE hr.person

 SET firstName = 'Fred'

 WHERE personId = 1 --include the key

    and rowLastModifyTime = ' 2012-02-19 22:33:53.845';

Use a rowversion column:

 UPDATE hr.person

 SET firstName = 'Fred'

 WHERE personId = 1

    and rowversion = 0x00000000000007D9;

Which is better performance-wise? Either of these generally performs just as well as the other, because in all cases, you’re going to be using the primary key to do the bulk of the work fetching the row and then your update. There’s a bit less overhead with the last two columns, because you don’t have to pass as much data into the statement, but that difference is negligible.

Deletions use the same WHERE clause, because if another user has modified the row, it’s probably a good idea to see if that user’s changes make the row still valuable:

 DELETE FROM hr.person

 WHERE personId = 1

    And rowversion = 0x00000000000007D9;

However, if the timestamp had changed since the last time the row was fetched, this would delete zero rows, since if someone has modified the rows since you last touched them, perhaps the deleted row now has value. I typically prefer using a rowversion column because it requires the least amount of work to always work perfectly. On the other hand, many client programmers prefer to have the manipulation layer of the application set a datetime value, largely because the datetime value has meaning to them to let them see when the row was last updated. Truthfully, I, too, like keeping these automatically modifying values in the table for diagnostic purposes. However, I prefer to rely on the rowversion column for locking because it is far simpler and safer and cannot be overridden by any code, no matter how you implement the other columns.

Logical Unit of Work

Although row-based optimistic locks are helpful, they do have a slight downfall. In many cases, several tables together make one “object.” A good example is an invoice with line items. The idea behind a logical unit of work is that, instead of having a row-based lock on the invoice and all the line items, you might only implement one on the invoice and use the same value for the line items. This strategy does require that the user always fetch not only the invoice line items but at least the invoice’s timestamp into the client’s cache when dealing with the invoice line items. Assuming you’re using a rowversion column, I’d just use the same kind of logic as previously used on the hr.person table. In this example, we’ll build the procedure to do the modifications.

When the user wants to insert, update, or delete line items for the invoice, the procedure requires the @objectVersion parameter and checks the value against the invoice, prior to update. Consider that there are two tables, minimally defined as follows:

 CREATE SCHEMA invoicing;

 GO

 --leaving off who invoice is for, like an account or person name

 CREATE TABLE invoicing.invoice

 (

     invoiceId int NOT NULL IDENTITY(1,1),

     number varchar(20) NOT NULL,

     objectVersion rowversion not null,

     constraint PKinvoicing_invoice primary key (invoiceId)

 );

 --also forgetting what product that the line item is for

 CREATE TABLE invoicing.invoiceLineItem

 (

     invoiceLineItemId int NOT NULL,

     invoiceId int NULL,

     itemCount int NOT NULL,

     cost int NOT NULL,

       CONSTRAINT PKinvoicing_invoiceLineItem primary key (invoiceLineItemId),

       CONSTRAINT FKinvoicing_invoiceLineItem$references$invoicing_invoice

           FOREIGN KEY (invoiceId) REFERENCES invoicing.invoice(invoiceId)

 );

For our delete procedure for the invoice line item, the parameters would have the key of the invoice and the line item, plus the rowversion value:

 CREATE PROCEDURE invoiceLineItem$del

 (

    @invoiceId int, --we pass this because the client should have it

                --with the invoiceLineItem row

    @invoiceLineItemId int,

    @objectVersion rowversion

 ) as

    BEGIN

    --gives us a unique savepoint name, trim it to 125

    --characters if the user named it really large

    DECLARE @savepoint nvarchar(128) =

                      cast(object_name(@@procid) AS nvarchar(125)) +

                       cast(@@nestlevel AS nvarchar(3));

    --get initial entry level, so we can do a rollback on a doomed transaction

    DECLARE @entryTrancount int = @@trancount;

    BEGIN TRY

       BEGIN TRANSACTION;

       SAVE TRANSACTION @savepoint;

       UPDATE invoice

       SET number = number

       WHERE invoiceId = @invoiceId

          And objectVersion = @objectVersion;

       DELETE invoiceLineItem

       FROM invoiceLineItem

       WHERE invoiceLineItemId = @invoiceLineItemId;

       COMMIT TRANSACTION;

    END TRY

    BEGIN CATCH

       --if the tran is doomed, and the entryTrancount was 0,

       --we have to roll back

       IF xact_state()= -1 and @entryTrancount = 0

          ROLLBACK TRANSACTION;

       --otherwise, we can still save the other activities in the

       --transaction.

       ELSE IF xact_state() = 1 --transaction not doomed, but open

           BEGIN

              ROLLBACK TRANSACTION @savepoint;

              COMMIT TRANSACTION;

           END

    DECLARE @ERRORmessage nvarchar(4000)

    SET @ERRORmessage = 'Error occurred in procedure ''' +

          object_name(@@procid) + ''', Original Message: '''

          + ERROR_MESSAGE() + '''';

    THROW 50000,@ERRORmessage,16;

    RETURN -100;

    END CATCH

 END

Instead of checking the rowversion on an invoiceLineItem row, we check the rowversion (in the objectVersion column) on the invoice table. Additionally, we must update the rowversion value on the invoice table when we make our change, so we update the invoice row, simply setting a single column to the same value. There’s a bit more overhead when working this way, but it’s normal to update multiple rows at a time from the client. You’d probably want to architect your solution with multiple procedures, one to update and check the optimistic lock and others to do the insert, update, and delete operations.

image  Tip Using table parameters, you could build a single procedure that accepted a list of ID values as parameters that included rowversion values quite easily. This would be yet another way to implement proper optimistic locking on a group of rows.

Best Practices

The number-one issue when it comes to concurrency is data quality. Maintaining consistent data is why you go through the work of building a database in the first place. Generally speaking, if the only way to get consistent results was to have every call single threaded, it would be worth it. Of course, we don’t have to do that except in rare situations, and SQL Server gives us tools to make it happen with the isolation levels. Use them as needed. It’s the data that matters:

  • Use transactions as liberally as needed: It’s important to protect your data, 100% of the time. Each time data is modified, enclosing the operation in a transaction isn’t a bad practice. This gives you a chance to check status, number of rows modified, and so on, and if necessary, to roll back the modification.
  • Keep transactions as short as possible: The smaller the transaction, the less chance there is of it holding locks. Try not to declare variables, create temporary tables, and so on inside a transaction unless doing so necessary. Make sure that all table access within transactions is required to be executed as an atomic operation.
  • Recognize the difference between hardware limitations and SQL Server concurrency issues: If the hardware is maxed out (excessive disk queuing, 90%–plus CPU usage, and so on), consider adding more hardware. However, if you’re single-threading calls through your database because of locking issues, you could add 20 processors and a terabyte of RAM and still see only marginal improvement.
  • Fetch all rows from a query as fast as possible: Depending on the isolation level and editability of the rows being returned, locks held can interfere with other users’ ability to modify or even read rows.
  • Make sure that all queries use reasonable execution plans: The better all queries execute, the faster the queries will execute, and it follows that locks will be held for a shorter amount of time. Remember, too, that scans will require every row in the table to be locked, whereas a seek will lock far fewer rows.
  • Use some form of optimistic locking mechanism: Use optimistic locking, preferably using a rowversion column, because it requires the smallest amount of coding and is managed entirely by SQL Server. The only code that’s required when programming is to validate the value in the rowversion column.
  • Consider using some form of the SNAPSHOT isolation level: Either code all your optimistic-locked retrieval operations with SET SNAPSHOT ISOLATION LEVEL, or change the database setting for READ_COMMITTED_SNAPSHOT to ON. This alters how the READ COMMITTED isolation level reads snapshot information at the statement level. Be careful to test existing applications if you’re going to make this change, because these settings do alter how SQL Server works and might negatively affect how your programs work. I suggest using full SNAPSHOT isolation level for read-only operations anyhow, if it’s reasonable for you to do so.

Summary

Concurrency is an important topic, and a difficult one. It seems easy enough: keep the amount of time a user needs to be in the database to a minimum; have adequate resources on your machine.

The fact is that concurrency is a juggling act for SQL Server, Windows, the disk system, the CPUs, and so on. If you have reasonable hardware for your situation, use the SNAPSHOT isolation level for retrieval and READ COMMITTED for other calls, and you should have no trouble with large-scale blocking on your server. This solution sounds perfect, but the greater the number of users, the more difficult a time you’ll have making things perform the way you want. Concurrency is one of the fun jobs for a DBA, because it’s truly a science that has a good deal of artsy qualities. You can predict only so much about how your user will use the system, and then experience comes in to tune queries, tune hardware, and tweak settings until you have them right.

I discussed some of the basics of how SQL Server implements controls to support concurrent programming, such that many users can be supported using the same data with locks and transactions. Then, I covered isolation levels, which allow you to tweak the kinds of locks taken and how long they’re held on a resource. The most important part of this chapter was the part on optimistic locking. Because the trend for implementing systems is to use cached data sets, modify that set, and then flush it back, you must make sure that other users haven’t made changes to the data while it’s cached.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.96.94