SQL Server 2008 - MERGE and optimistic concurrency

Most applications are written nowadays using optimistic concurrency. Locks aren’t placed on a piece of data for the duration of someone viewing that data in their application. Instead what you do is when someone wants to save the data you check to see whether someone else as changed it since the time you read the data. If someone has you generally don’t want to save the data because you are likely to overwrite the change that had been made. The check normally consists of checking the value of each column against the values you got back when you read the data or comparing the value of a rowversion against what you read (a rowversion column gets changed every time a row is changed, that means you can use this column to detect a change rather than checking all the columns). Imagine buying a

What has this got to do with the MERGE statement. Well optimistic concurrency is very easy to do when you are dealing with a single row of data as you only have to do one check. If you identify the row has changed you can return success or failure. However with a MERGE statement what happens if you are updating 10 records and 4 have already been updated. In this case you have a number of options one is to update those that haven’t changed and return to the client those that have changed. This can be done very nicely using a trick with MERGE and the OUTPUT clause .

Assuming you have a rowversion (timestamp) column on your table (you can use the same approach if you don’t, in which case you will have to compare all the columns ) the following is an example MERGE statement with a TVP (Table Valued Parameter). We are going to save a set of OrderLines

The classic MERGE statement is as follows

MERGE Sales.SalesOrderDetail OrderDetail

USING @ChangedOrderDetail    Chng        ON Chng.[SalesOrderDetailID] = OrderDetail.[SalesOrderDetailID]

                                        AND Chng.[SalesOrderID]       = OrderDetail.[SalesOrderID]

WHEN MATCHED THEN

    UPDATE SET [SalesOrderID]            = Chng.SalesOrderID           

              ,[CarrierTrackingNumber]   = Chng.CarrierTrackingNumber 

              ,.....

WHEN NOT MATCHED THEN

    INSERT ([SalesOrderID],[CarrierTrackingNumber],[OrderQty],[ProductID],[SpecialOfferID]

           ,[UnitPrice],[UnitPriceDiscount],[rowguid],[ModifiedDate])

     VALUES

           (Chng.SalesOrderID,Chng.CarrierTrackingNumber,Chng.OrderQty,Chng.ProductID,Chng.SpecialOfferID

           ,Chng.UnitPrice,Chng.UnitPriceDiscount,Chng.rowguid,Chng.ModifiedDate);



To add optimistic checking you can check at the start within a serialised transaction or check at the end to see if the number of rows affected is the same as the number passed in i.e.

 

SET TRANSACTION ISOLATION SERIALIZABLE

BEGIN TRANSACTION

IF NOT EXISTS (SELECT 1

             FROM Sales.SalesOrderDetail OrderDetail

             JOIN @ChangedOrderDetail    Chng        ON Chng.[SalesOrderDetailID] = OrderDetail.[SalesOrderDetailID]

                                                    AND Chng.[SalesOrderID]       = OrderDetail.[SalesOrderID]

            WHERE OrderDetail.Rowversion <> Chng.Rowversion)

    BEGIN

        MERGE Sales.SalesOrderDetail OrderDetail

        USING @ChangedOrderDetail    Chng        ON Chng.[SalesOrderDetailID] = OrderDetail.[SalesOrderDetailID]

        .......

    END      

ELSE

    RAISERROR(‘data has changed since it was read....)

COMMIT TRANSACTION

Or

BEGIN TRANSACTION

 

    MERGE Sales.SalesOrderDetail OrderDetail

    USING @ChangedOrderDetail    Chng        ON Chng.[SalesOrderDetailID] = OrderDetail.[SalesOrderDetailID]

    .......

   

IF @@ROWCOUNT<> (SELECT Count(1) FROM @ChangedOrderDetail)

       BEGIN

    RAISERROR(‘data has changed since it was read....)

       ROLLBACK TRANSACTION

       END

ELSE

    COMMIT TRANSACTION

   

The final way is to do a partial update and report the records that have changed . The OUTPUT clause only contains records that have been inserted, updated or deleted. So to get the records out you need to UPDATE them, however you can UPDATE the columns to their existing values (i.e. not change anything ) and that way you can get these rows in the OUTPUT clause. You can then use a nice feature of the MERGE OUTPUT clause and compare the deleted rowversion value with that in the source data. If they are different it means the record wasn’t changed.

 

MERGE Sales.SalesOrderDetail OrderDetail

USING @ChangedOrderDetail    Chng        ON Chng.[SalesOrderDetailID] = OrderDetail.[SalesOrderDetailID]

                                      AND Chng.[SalesOrderID]       = OrderDetail.[SalesOrderID]

                                  

WHEN MATCHED THEN

    UPDATE SET [SalesOrderID]            = CASE WHEN Chng.Rowversion = OrderDetail.Rowversion          

                                                THEN Chng.SalesOrderID           

                                                ELSE OrderDetail.SalesOrderID

                                           END

              ,[CarrierTrackingNumber]   = CASE WHEN Chng.Rowversion = OrderDetail.Rowversion          

                                                THEN Chng.CarrierTrackingNumber 

                                                ELSE OrderDetail.CarrierTrackingNumber

                                           END

              , .....

 

OUTPUT CASE WHEN Chng.Rowversion = deleted.Rowversion THEN 1 ELSE 0 END Changed

      , Chng.SalesOrderDetailID

Note: For simplicity I have removed most of the columns from the update statement. You would need to include these in your application.

You can further extend this by using another new feature of SQL Server 2008, Composable DML. This allows you to consume the output from one statement into another statement. With SQL Server 2008 you are limited to only being able to consume the data in an INSERT statement. There are also restrictions on the structure of the table you are inserting into. Refer to BOL for exact details.

Using composable DML you can store only those records that failed update into a table variable and then return those records.

 

DECLARE @ChangedOrderDetail SalesOrderDetail_Type

DECLARE @SalesOrderID int

 

DECLARE @t TABLE (Changed int, ID int)

 

INSERT INTO @t

SELECT Changed, SalesOrderDetailID

FROM (MERGE Sales.SalesOrderDetail OrderDetail

      USING @ChangedOrderDetail    Chng        ON Chng.[SalesOrderDetailID] = OrderDetail.[SalesOrderDetailID]

                                              AND Chng.[SalesOrderID]       = OrderDetail.[SalesOrderID]

                                           

       WHEN MATCHED THEN

        UPDATE SET [SalesOrderID]            = CASE WHEN Chng.Rowversion = OrderDetail.Rowversion          

                                                    THEN Chng.SalesOrderID           

                                                    ELSE OrderDetail.SalesOrderID

                                               END

                  ,[CarrierTrackingNumber]   = CASE WHEN Chng.Rowversion = OrderDetail.Rowversion          

                                                    THEN Chng.CarrierTrackingNumber 

                                                    ELSE OrderDetail.CarrierTrackingNumber

                                               END

       OUTPUT CASE WHEN Chng.Rowversion = deleted.Rowversion THEN 1 ELSE 0 END Changed

            , Chng.SalesOrderDetailID) da

WHERE CHanged = 0

 

SELECT *

  FROM @t



-
Published Wednesday, June 18, 2008 6:13 PM by simonsabin

Comments

# All Else Failed &raquo; SQL Server 2008 - MERGE and optimistic concurrency

Pingback from  All Else Failed &raquo; SQL Server 2008 - MERGE and optimistic concurrency

Wednesday, June 18, 2008 7:37 PM by Hugo Kornelis

# re: SQL Server 2008 - MERGE and optimistic concurrency

Hi Somon,

Yet another way to achieve this is by using multiple WHEN MATCHED clauses with an additional condition:

....

WHEN MATCHED AND Chng.Rowversion = OrderDetail.Rowversion

THEN UPDATE SET xxx = Chng.xxx, yyy = Chng.yyy, ...

WHEN MATCHED

THEN UPDATE SET xxx = RowVersion.xxx

....

The second WHEN MATCHED THEN UPDATE is required to get the unchanged rows in the output; at least one column must be "updated" to itself to ssatisfy the syntax requirements. No extra condition is needed for this second WHEN MATCHED, since rows with unchanged rowversion are already handled by the previous WHEN MATCHED clause.

Best, Hugo

# Dew Droplet - June 19, 2008 | Alvin Ashcraft's Morning Dew

Pingback from  Dew Droplet - June 19, 2008 | Alvin Ashcraft's Morning Dew

Sunday, June 22, 2008 10:15 PM by Weekly Link Post 47 « Rhonda Tipton’s WebLog

# Weekly Link Post 47 &laquo; Rhonda Tipton&#8217;s WebLog

Pingback from  Weekly Link Post 47 &laquo; Rhonda Tipton&#8217;s WebLog

Saturday, March 6, 2010 9:54 AM by SimonS Blog on SQL Server Stuff

# How to archive data from a table to a local or remote database in SQL 2005 and SQL 2008

Often you have the need to archive data from a table. This leads to a number of challenges 1. How can

Wednesday, March 10, 2010 1:27 PM by Simon Sabin UK SQL Consultant's Blog

# How to archive data from a table to a local or remote database in SQL 2005 and SQL 2008