Using the OUTPUT Clause to Capture Identity Values on Multi-Row Inserts

By Bill Graziano on 14 August 2006 | 0 Comments | Tags: Identity, INSERT


SQL Server 2005 introducted the OUTPUT clause which we can use to capture values from the inserted and deleted virtual tables. Previously this data was only available through triggers. We can use this in an INSERT ... SELECT statement to capture all the inserted identity values. Previously this required some type of loop or temporarily altering the target table.

We'll start with two tables: a product table and table of products to insert. The scenario is a vendor that sends you a complete list of all their products and you only need to insert the rows that don't already exist. However you need to insert those new rows into multiple tables. The following script will create the tables in tempdb based on data in AdventureWorks.

USE tempdb
GO
IF  EXISTS (SELECT * FROM sys.objects 
		WHERE object_id = OBJECT_ID(N'[dbo].[Product]') AND type in (N'U'))
	DROP TABLE [dbo].[Product]
GO
IF  EXISTS (SELECT * FROM sys.objects 
		WHERE object_id = OBJECT_ID(N'[dbo].ProductsToInsert') AND type in (N'U'))
	DROP TABLE [dbo].ProductsToInsert
GO

CREATE TABLE Product (
	ProductID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
	[Name] NVARCHAR(50) NOT NULL,
	ProductNumber NVARCHAR(25) NOT NULL,
	ListPrice MONEY NOT NULL)
GO
CREATE UNIQUE INDEX IX_Product_ProductNumber ON Product ( ProductNumber )
GO

CREATE TABLE ProductsToInsert (
	RowID INT NOT NULL IDENTITY(1,1) PRIMARY KEY,
	[Name] NVARCHAR(50) NOT NULL,
	ProductNumber NVARCHAR(25) NOT NULL,
	ListPrice MONEY NOT NULL,
	InsertedIdentityValue INT NULL)
GO
INSERT Product ([Name], ProductNumber, ListPrice)
SELECT TOP 450 [Name], ProductNumber, ListPrice
FROM AdventureWorks.Production.Product
ORDER BY SellStartDate, ProductID
GO
INSERT ProductsToInsert ([Name], ProductNumber, ListPrice)
SELECT  [Name], ProductNumber, ListPrice
FROM AdventureWorks.Production.Product
GO

The Product table has an identity column as its primary key. Product number is a natural key on the table. The ProductsToInsert table has the ProductNumber column and a column for whatever identity value is inserted when we put the row into the Product table. You'll notice I only put 450 of the products in the Product table to start with but all 504 in the ProductsToInsert table.

A simple script to insert the new products looks like this:

use tempdb
GO
INSERT Product ([Name], ProductNumber, ListPrice)
SELECT 
    [Name], ProductNumber, ListPrice
FROM
    ProductsToInsert I
WHERE
    NOT EXISTS (SELECT 1 
                FROM Product 
                WHERE ProductNumber = I.ProductNumber)

That inserts the 54 products that weren't in the table previously. We can use the OUTPUT clause to return back the rows that were inserted. That looks like this:

INSERT Product ([Name], ProductNumber, ListPrice)
    OUTPUT inserted.ProductID,
           inserted.[Name],
           inserted.ProductNumber,
           inserted.ListPrice
SELECT 
	[Name], ProductNumber, ListPrice
FROM
	ProductsToInsert I
WHERE
	NOT EXISTS (SELECT 1 FROM Product 
				WHERE ProductNumber = I.ProductNumber)

When that statement is run it returns the following recordset back to the client:

  ProductID Name                                ProductNumber         ListPrice
----------- ----------------------------------- --------------------- -------------
        451 LL Bottom Bracket                   BB-7421                       53.99
        452 ML Bottom Bracket                   BB-8107                      101.24
        453 HL Bottom Bracket                   BB-9108                      121.49

. . . 

        504 HL Touring Handlebars               HB-T928                       91.57

(54 row(s) affected)

That is almost what we want. We have the identity values in the result set but we don't have a way to work with the data and we don't have a way to tie it back to the original source row. We'll need to add two things. First we'll need to store this result set in a table variable. I'll also remove a few columns to make it easier to read and add an update statement to save the identity value. That script looks like this:

DECLARE @InsertedRows TABLE (ProductID INT, ProductNumber NVARCHAR(25) )

INSERT Product ([Name], ProductNumber, ListPrice)
    OUTPUT inserted.ProductID,
           inserted.ProductNumber
    INTO @InsertedRows
SELECT 
	[Name], ProductNumber, ListPrice
FROM
	ProductsToInsert AS I
WHERE
	NOT EXISTS (SELECT 1 FROM Product 
				WHERE ProductNumber = I.ProductNumber)

UPDATE  ProductsToInsert
SET     InsertedIdentityValue = T.ProductID
FROM    ProductsToInsert I
JOIN    @InsertedRows T ON T.ProductNumber = I.ProductNumber

SELECT  RowID, ProductNumber, InsertedIdentityValue
FROM    ProductsToInsert
WHERE   InsertedIdentityValue IS NOT NULL

We declare a table variable to store the results of the OUTPUT clause. We use the OUTPUT INTO syntax to store the results into the table variable. Next we use the table variable to update the source table with the inserted identity columns. After that a simple SELECT statement returns the new values which we can easily use in other statements:

      RowID ProductNumber             InsertedIdentityValue
----------- ------------------------- ---------------------
        451 HB-T721                                     503
        452 HB-T928                                     504
        453 FB-9873                                     502

. . . 

        504 BK-R19B-52                                  470

(54 row(s) affected)

The OUTPUT clause can also be used with UPDATE and DELETE statements and return values from either the inserted or deleted table. In its simplest form the OUTPUT clause greatly simplifies importing data into SQL Server.

Discuss this article: 0 Comments so far. Print this Article. This page has been read 30,183 times.

If you like this article you can sign up for our newsletter. We send it out each week that we post a new article. There's an opt-out link at the bottom of each newsletter so it's easy to unsubscribe at any time.

Email Address:

Email ThisSubscribe to this feedKick itSave to del.icio.usView blog reactions

Related Articles

Efficiently Reuse Gaps in an Identity Column (9 February 2010)

How to Insert Values into an Identity Column in SQL Server (6 August 2007)

Custom Auto-Generated Sequences with SQL Server (24 April 2007)

Using Views to Enforce Business Rules (9 April 2007)

Anticipating Primary Key Violations (23 November 2003)

Using EXISTS (12 October 2003)

Understanding Identity Columns (9 March 2002)

Identity and Primary Keys (28 February 2001)

Other Recent Forum Posts

Database consistency problem detected on database (13 Replies)

SQL server 2005 setup will cause setup failure (1 Reply)

Group by with MAX and id (3 Replies)

GROUP BY Question (5 Replies)

Record Column which is causing exception (4 Replies)

Please help with Query (11 Replies)

Replacing subqueries with Joins (5 Replies)

Is there anywaySql Statement without OR codition. (7 Replies)

Subscribe to SQLTeam.com

Weekly SQL Server newsletter with articles, forum posts, and blog posts via email:

SQLTeam.com Articles via RSS

SQLTeam.com Weblog via RSS

- Advertisement -

- Sponsor's Message -

SQLShare.com Videos

Writing to the Registry from TSQL Using XP_Instance_RegWrite

Reading from the registry to figure out a configuration issue or two is common, but writing to the registry isn't something you'll need to do very often. Still, it's a useful trick to know and we've put it to work showing you how to alter the default folder used for database backups.

Using XP_Instance_RegRead To Get Default Database Path

SQL saves more than a few things to the registry and we can leverage that by reading those settings from TSQL using xp_instance_regread. Easy to use and only one minor quirk to learn, and you'll reading from the registry. Look for the follow up video that demonstrates how to write changes to the registry too.

How to Change the Default Path for New Databases

Typing CREATE DATABASE MyDB gets the job done, but it puts the files in whatever location has been set as the default - which means you should make sure the default is set to a good place, or specify them as part of the CREATE. We'll show you how to adjust the setting (and we've got a video coming up that shows you how to change it from code too!).

Overview of Data Driven Subscriptions in Reporting Services

Subscriptions are a powerful feature of Reporting Services because they allow users to decide which reports to receive. But what do you do when the boss wants a report sent to a group of users, perhaps based on some additional criteria that frequently changes? That's where data driven subscriptions make sense and in our lesson today Devin does a nice overview of the options and requirements.

Using DatePart and DateName

Ever want to get the month out of a date as a number, or as literal text? Many people will do it by parsing the date as a string, but we've got some built in functions that will do it cleanly and consistently.