One of the most effective techniques I have found to optimize transactions in high volume OLTP databases is to persist your data in sets. Once you have a set of data available in SQL Server, you need to process that set using efficient SQL Syntax. Regardless of how you pass a set of data to SQL Server, there are some set processing techniques that work well.
Using SQL Server, one of the most effective set techniques, when normalizing set data into multiple tables, is the use of the OUTPUT clause. For any INSERT, UPDATE or DELETE query the OUTPUT clause returns two virtual sets of data representing the schema of the table being modified. It has a virtual set of INSERTED and DELETED. If you are familiar with triggers, these virtual tables work exactly the same. The difference is that you can handle the results yourself directly in your query or stored procedure.
If you were to use the example of the shopping cart from the earlier editorial, where you are saving the cart info and cart item info, each with version data to simplify the process, you could readily handle the persistence using set logic. Following is an example where the application saves a version of the data by submitting the entire cart each time the data is persisted. We’re going to using a user defined table type for the process.
Here are the four transaction tables we are populating. There is a Carts and CartItems table containing the selections from a current shopping cart for a user. The CartVersions and CartItemVersions tables are used so that you can perform INSERT, UPDATE and DELETE actions by only using INSERT actions. See TIPS for High Performing OLTP Databases for more details. Here are the transaction table structures we using for this example.
CREATE TABLE Carts ( CartId INT NOT NULL IDENTITY (1,1) ,CustomerId INT NOT NULL ,CartDTM DATETIME NOT NULL ) GO CREATE TABLE CartVersions ( CartVersionId INT NOT NULL IDENTITY(1,1) ,CartId INT NOT NULL ,VersionDTM DateTime NOT NULL ) GO CREATE TABLE CartItems ( CartItemId INT NOT NULL IDENTITY (1,1) ,CartId INT NOT NULL ,ProductId INT NOT NULL ,Quantity INT NOT NULL ,ItemCost MONEY NOT NULL ,DiscountCode VARCHAR(32) NOT NULL ,DiscountAmount MONEY NOT NULL ,ExtendedCost MONEY NOT NULL ) GO CREATE TABLE CartItemVersions ( CartItemId INT NOT NULL ,CartVersionId INT NOT NULL ) GO
I’m using a table called CartBatch as a staging table populated from the current cart selections from the users application. I’m going to save everything in the current cart with a single call by populating this batch table, and then merging the data to the appropriate transaction tables. The batch data may be presented to the query using a number of techniques as demonstrated in Tips for High Performing OLTP Databases 2. The batch set data structure I’m going to use follows.
CREATE TABLE CartBatch ( CustomerId INT NOT NULL ,CartDTM DATETIME NOT NULL ,ProductId INT NOT NULL ,Quantity INT NOT NULL ,ItemCost MONEY NOT NULL ,DiscountCode VARCHAR(32) NOT NULL ) GO
Once records are added to the batch table, I can process them. In my code, I am assuming the data in the batch table represents only the data for a single instance of a shopping cart. The following set of queries populate all four tables, and handle the identity columns created through the SCOPE_IDENTITY() function, or the OUTPUT clause during an insert statement. I am only going to insert
New Carts (the first time data is saved for this customer cart session)
A version for this instance of the cart being saved
New Cart Items where the product, quantity, or discount do not already exist
A version for each CartItem that was added or modified
The following SQL is one implementation for saving a user’s cart as a batch.
DECLARE @CartID INT DECLARE @CustomerID INT DECLARE @CartDTM DATETIME DECLARE @CartVersionId INT DECLARE @CartItmeInsert TABLE ( CartItemId INT NOT NULL ,CartId INT NOT NULL ,ProductId INT NOT NULL ) SELECT TOP 1 @CustomerID = CustomerId ,@CartDTM = CartDTM FROM CartBatch SELECT @CartID = CartId FROM Carts WHERE CustomerId = @CustomerID AND CartDTm = @CartDTM if @@ROWCOUNT = 0 BEGIN INSERT INTO Carts (CustomerId, CartDTM) VALUES (@CustomerID, @CartDTM) SET @CartID = SCOPE_IDENTITY() END INSERT INTO CartVersions (CartId, VersionDTM) VALUES (@CartId, GETDATE()) SET @CartVersionId = SCOPE_IDENTITY() -- NOTE: I'm using a Discounts table in the following query that -- I have not defined above. It is used to apply discounts -- for a product at sale time, and to compute the extended -- amount for each product. Notice the ISNULL function for -- the discount amount for those products without one. INSERT INTO CartItems ( CartId ,ProductId ,Quantity ,ItemCost ,DiscountCode ,DiscountAmount ,ExtendedCost ) OUTPUT INSERTED.CartItemId ,INSERTED.CartId ,INSERTED.ProductId INTO @CartItmeInsert SELECT @CartId ,b.ProductId ,b.Quantity ,b.ItemCost ,b.DiscountCode ,ISNULL(d.DiscountAmount, 0) ,(ItemCost - ISNULL(d.DiscountAmount, 0)) * b.Quantity FROM CartBatch b LEFT JOIN Discounts d on b.DiscountCode = d.DiscountCodes LEFT JOIN CartItems i ON i.CartId = @CartId AND i.ProductId = b.ProductId AND i.Quantity = b.Quantity AND i.DiscountAmount = b .DiscountCode WHERE i.CartItemId IS NULL INSERT INTO CartItemVersions (CartItemId, CartVersionId) SELECT CartItemId, @CartVersionId FROM @CartItemInsert INSERT INTO CartItemVersions (CartItemId, CartVersionId) SELECT ci.CartItemId, @CartVersionId FROM CartBatch b JOIN CartItems ci ON ci.CartId = @CartId AND b.ProductId = ci.ProductId LEFT JOIN @CartItmeInsert cii ON ci.ProductId = cii.ProductId WHERE cii.CartItemId IS NULL
This implementation works well with small or large sets. The larger the set is being saved, the more efficient this technique becomes.
Cheers,
Ben