SqlCredit - Part 17: Exploring SQL 2005's OUTPUT ClauseJuly 25, 2008
This is part 17 of a series. If you have not read part 16, you can find it here, but this article does not require any knowledge of the previous work in the series.
The OUTPUT Clause: Removing One More Reason to Run Per-Record ProcessingDatabase people love to do things in sets. Occasionally, they are forced to process one record at a time, but they don’t like it. In my previous job, I remember writing code that created multiple payment records. These payment records had change history records, and the creation of a record required a corresponding history record. It was something like this:
Create Payment record Get ID value using SCOPE_IDENTITY() Create PaymentHistory record I was convinced that I had to use per-record processing because I needed the identity value to create the history record. That was actually true at the time I originally wrote this code because I was writing it to work against SQL Server 2000. However, SQL Server 2005 introduced the OUTPUT clause. This bit of magic allows us to get the identity value, plus a number of other values, from an INSERT, UPDATE, or DELETE. INSERT, UPDATE, and DELETE all have different interaction with OUTPUT. I will concentrate on INSERT. The attached code includes examples of all three.
The “Hello World” of OUTPUT ClauseHere is the simplest possible illustration of how the OUTPUT clause works.
DECLARE @t1 TABLE (ColID tinyint IDENTITY, Col1 varchar(11))
DECLARE @t1inserted TABLE (ColID tinyint, Col1 varchar(11))
INSERT INTO @t1
OUTPUT Inserted.ColID, Inserted.Col1 INTO @t1inserted
VALUES ('Hello World')
The contents of the two tables match exactly. @t1 ColID Col1 ----- ----------- 1 Hello World @t1inserted ColID Col1 ----- ----------- 1 Hello World It would be very reasonable at this point to ask, “So what?” The short answer is that you have access to the identity value of the inserted record by use the OUTPUT clause. The value of this will become clearer in the following scenario where we are inserting a set of records. The Base TableThis code is all very simple. It uses nothing but table variables, so you can run it anywhere. It doesn’t require any existing database or anything. The base table here takes four bits and calculates the number represented by those bits. For example, X8 X4 X2 X1 ComputedNum ----- ----- ----- ----- ----------- 1 0 1 0 10 @baseTable schema: DECLARE @baseTable TABLE (
ColID int NOT NULL
IDENTITY,
X8 bit NOT NULL,
X4 bit NOT NULL,
X2 bit NOT NULL,
X1 bit NOT NULL,
ComputedNum AS
((X8 * 8) + (X4 * 4) + (X2 * 2) + (X1 * 1)),
CreateDate datetime NOT NULL
DEFAULT GETDATE(),
ColGUID uniqueidentifier NOT NULL
DEFAULT NEWSEQUENTIALID()
)
A couple of important things to note:
The “History” TableThe history (or “change-tracking”) table is similar. Yes, I understand that you would not track changes to such a simple table, but it works for illustration. DECLARE @historyTable TABLE (
HistoryID int NOT NULL IDENTITY(-1, -1),
SourceID int NOT NULL,
ComputedNum int NOT NULL,
SourceCreateDate datetime NOT NULL,
SourceGUID uniqueidentifier NOT NULL,
HistoryCreateDate datetime NOT NULL DEFAULT GETDATE()
)
This table has four regular columns that receive values from @baseTable and two “automatic columns”. It does not include the individual bit columns from @baseTable. INSERTing with an OUTPUT ClauseThe INSERT: INSERT INTO @baseTable (
X8,
X4,
X2,
X1
)
OUTPUT
Inserted.ColID,
Inserted.ComputedNum,
Inserted.CreateDate,
Inserted.ColGUID
INTO @insertedSet
SELECT
X8,
X4,
X2,
X1
FROM (SELECT 0 AS X8 UNION SELECT 1) AS t1
CROSS JOIN (SELECT 0 AS X4 UNION SELECT 1) AS t2
CROSS JOIN (SELECT 0 AS X2 UNION SELECT 1) AS t3
CROSS JOIN (SELECT 0 AS X1 UNION SELECT 1) AS t4
ORDER BY t1.X8, t2.X4, t3.X2, t4.X1
The Populating SELECT Statement The SELECT statement used to populate @baseTable uses CROSS JOIN to create the full set of possible bit values for these columns. The OUTPUT Clause The OUTPUT clause allows us to “record” any of the columns inserted into the base table, whether regular columns or automatic columns. In this code, all I am recording is the automatic columns. Understand that it is not necessary to OUTPUT any columns that you don’t plan to use. INSERTing From the OUTPUT to the History TableThe history table INSERT code: INSERT INTO @historyTable (
-- HistoryID
SourceID,
ComputedNum,
SourceCreateDate,
SourceGUID
-- HistoryCreateDate
)
SELECT
ColID,
ComputedNum,
CreateDate,
ColGUID
FROM @insertedSet
ORDER BY ColID
I added a WAITFOR of one second between the initial insert and the history insert so that GETDATE() would return different values. The ResultsAfter running the complete script, here are the results (with some editing for width): @baseTable ColID X8 X4 X2 X1 ComputedNum CreateDate ColGUID ----- -- -- -- -- ----------- ------------ ---------- 1 0 0 0 0 0 11:30:54:417 565...89DB 2 0 0 0 1 1 11:30:54:417 575...89DB 3 0 0 1 0 2 11:30:54:417 585...89DB 4 0 0 1 1 3 11:30:54:417 595...89DB 5 0 1 0 0 4 11:30:54:417 5A5...89DB 6 0 1 0 1 5 11:30:54:417 5B5...89DB 7 0 1 1 0 6 11:30:54:417 5C5...89DB 8 0 1 1 1 7 11:30:54:417 5D5...89DB 9 1 0 0 0 8 11:30:54:417 5E5...89DB 10 1 0 0 1 9 11:30:54:417 5F5...89DB 11 1 0 1 0 10 11:30:54:417 605...89DB 12 1 0 1 1 11 11:30:54:417 615...89DB 13 1 1 0 0 12 11:30:54:417 625...89DB 14 1 1 0 1 13 11:30:54:417 635...89DB 15 1 1 1 0 14 11:30:54:417 645...89DB 16 1 1 1 1 15 11:30:54:417 655...89DB @insertedSet ColID ComputedNum CreateDate ColGUID ----------- ----------- ------------ ---------- 1 0 11:30:54:417 565...89DB 2 1 11:30:54:417 575...89DB 3 2 11:30:54:417 585...89DB 4 3 11:30:54:417 595...89DB 5 4 11:30:54:417 5A5...89DB 6 5 11:30:54:417 5B5...89DB 7 6 11:30:54:417 5C5...89DB 8 7 11:30:54:417 5D5...89DB 9 8 11:30:54:417 5E5...89DB 10 9 11:30:54:417 5F5...89DB 11 10 11:30:54:417 605...89DB 12 11 11:30:54:417 615...89DB 13 12 11:30:54:417 625...89DB 14 13 11:30:54:417 635...89DB 15 14 11:30:54:417 645...89DB 16 15 11:30:54:417 655...89DB @historyTable HistoryID SourceID ComputedNum SourceCreateDate SourceGUID HistoryCreateDate --------- -------- ----------- ---------------- ---------- ----------------- -1 1 0 11:30:54:417 565...89DB 11:30:55:417 -2 2 1 11:30:54:417 575...89DB 11:30:55:417 -3 3 2 11:30:54:417 585...89DB 11:30:55:417 -4 4 3 11:30:54:417 595...89DB 11:30:55:417 -5 5 4 11:30:54:417 5A5...89DB 11:30:55:417 -6 6 5 11:30:54:417 5B5...89DB 11:30:55:417 -7 7 6 11:30:54:417 5C5...89DB 11:30:55:417 -8 8 7 11:30:54:417 5D5...89DB 11:30:55:417 -9 9 8 11:30:54:417 5E5...89DB 11:30:55:417 -10 10 9 11:30:54:417 5F5...89DB 11:30:55:417 -11 11 10 11:30:54:417 605...89DB 11:30:55:417 -12 12 11 11:30:54:417 615...89DB 11:30:55:417 -13 13 12 11:30:54:417 625...89DB 11:30:55:417 -14 14 13 11:30:54:417 635...89DB 11:30:55:417 -15 15 14 11:30:54:417 645...89DB 11:30:55:417 -16 16 15 11:30:54:417 655...89DB 11:30:55:417 Notice that the values of @historyTable.SourceCreateDate and @historyTable.HistoryCreateDate are exactly one second apart. This is due to the WAITFOR in the script. FeedbackSpecific questions for discussion in the forum:
For Next TimeHow does the use of OUTPUT affect performance? Is set-based work really that much faster? » See All Articles by Columnist Rob Garrison SqlCredit - Developing a Complete SQL Server OLTP Database Project
Performance Testing SQL 2008's Transparent Data Encryption
SQL Server 2008's Change Data Capture - Tracking the Moving Parts Performance Testing - SQL Server 2008 versus SQL Server 2005 Exploring SQL Server's Index INCLUDEs Exploring SQL 2005's Ranking Functions - NTILE() and ROW_NUMBER() SqlCredit - Part 19: Exploring SQL 2005's Ranking Functions - RANK() and DENSE_RANK() SqlCredit, Part 18: Exploring the Performance of SQL 2005's OUTPUT Clause SqlCredit - Part 17: Exploring SQL 2005's OUTPUT Clause SqlCredit - Part 16: The Cost of Bloat SqlCredit - Part 15: The Cost of Distribution SqlCredit - Part 14: The Cost of Translation SqlCredit - Part 13: More on Indexed Persisted Computed Columns SqlCredit - Part 12: Exploring Indexed Persisted Computed Columns SqlCredit - Part 11: Change Tracking Using History Records SqlCredit - Part 10: MAC Performance and Updating SqlCredit SqlCredit - Part 9: Message Authentication Codes SqlCredit Part 8: Comparing Encrypt/DecryptByCert and Encrypt/DecryptByKey SqlCredit Part 7: Performance Impact of EncryptByCert and DecryptByCert SqlCredit Part 6: Exploring EncryptByCert and DecryptByCert SqlCredit - Part 5: Adding Card, Vendor, and Purchase, Plus Much Refactoring SqlCredit - Part 4: Schema and Procedure Security SqlCredit - Part 3: Schema/Proc Updates and Automated Unit Testing SqlCredit - Part 2: Creating the Database, Tables, CRUD Procedures SqlCredit - Developing a Complete SQL Server OLTP Database Project |