SQL Server 2008 - Table Valued Parameters vs XML vs CSV

The scenario is, you want to create a stored procedure that returns the records relating to a finite set of keys/IDs that you pass in to it. How do you do it? Well you could use dynamic SQL, but for the purpose of this article I’m going to ignore that option.

Test scenario

-- Create basic customer table  
CREATE TABLE [dbo].[Customer]  
(  
	Id INTEGER IDENTITY(1,1) PRIMARY KEY,  
	Name NVARCHAR(50) NOT NULL  
);  
GO  
  
-- Populate Customer table with 100,000 sample records  
DECLARE @Counter INTEGER  
SET @Counter = 1  
WHILE (@Counter <= 100000)  
	BEGIN  
		INSERT Customer (Name)  
		VALUES ('Test Customer #' + CAST(@Counter AS VARCHAR(10)))  
		SET @Counter = @Counter + 1  
	END

Option 1 - CSV list of keys

In SQL Server 2000, you would most likely have ended up passing in a CSV list of keys, splitting that out into a table, and then joining that on to your real data table to match the records to return. dbo.fnSplit is a user defined function that iterates through the string and splits each value out into a TABLE variable which is then returned. To save space, I haven’t supplied the definition of that, but there’s plenty of examples of this function out there via a quick Google.

CREATE PROCEDURE [dbo].[uspGetCustomersCSV]  
	@CustomerIDs VARCHAR(8000)  
AS  
BEGIN  
	SELECT c.Id, c.Name  
	FROM [dbo].[Customer] c  
		JOIN dbo.fnSplit(@CustomerIDs, ',') t ON c.Id = t.item  
END  
GO

Example Use:

EXECUTE [dbo].[uspGetCustomersCSV] '1,10,100'

Option 2 - XML

SQL Server 2005 added the XML datatype, which allows you to pass in an XML blob containing the keys and using the new built-in XML support to incorporate it into a JOIN.

CREATE PROCEDURE [dbo].[uspGetCustomersXML]  
	@CustomerIDs XML  
AS  
BEGIN  
	SELECT c.ID, c.Name  
	FROM [dbo].[Customer] c  
		JOIN @CustomerIDs.nodes('IDList/ID') AS x(Item) ON c.ID = Item.value('.', 'int' )  
END  
GO

Example Use:

EXECUTE [dbo].[uspGetCustomersXML] '<IDList><ID>1</ID><ID>10</ID><ID>100</ID></IDList>'

Option 3 - Table Valued Parameters

Now in SQL Server 2008, there’s a new kid on the block. Table Valued Parameters. In a nutshell, this is the ability to pass a TABLE type in as a parameter, which previously was not allowed. The beauty of this, is it allows you to focus on writing the query in a more natural manner - no string parsing/manipulation, no XML functionality. Plain, simple, SQL.

First you create a new TYPE, defining the TABLE structure.

CREATE TYPE CustomerIDTableType AS TABLE (ID INTEGER PRIMARY KEY);  
GO

Next you create your stored procedure, taking that new type as a parameter.

CREATE PROCEDURE [dbo].[uspGetCustomersTable]  
	@CustomerIDs CustomerIDTableType READONLY  
AS  
BEGIN  
	SELECT c.ID, c.Name  
	FROM [dbo].[Customer] c  
		JOIN @CustomerIDs t ON c.Id = t.Id  
END  
GO

Example Use:

DECLARE @Ids CustomerIDTableType  
INSERT @Ids VALUES (1)  
INSERT @Ids VALUES (10)  
INSERT @Ids VALUES (100)  
EXECUTE [dbo].[uspGetCustomersTable] @Ids

If you’re wanting to call this from .NET, it’s quite simple. You define the parameter as SqlDbType.Structured and the value can be set to any IEnumerable, DataTable, or DbDataReader!

The follow stats are the average figures over 3 runs, against the 100,000 row table, retrieving just 7 rows of data for keys: 1, 10, 200, 3000, 40000, 50000 and 90000. Each time, the cache was fully cleared to allow fair comparisons.

Option 1 (CSV): Duration=123ms, Reads=107, Writes=1, CPU=15
Option 2 (XML): Duration=80ms, Reads=69, Writes=0, CPU=5
Option 3 (TVP): Duration=69ms, Reads=27, Writes=0, CPU=0

This was quite a modest test, and by no means an absolute stress test. But the results look very promising for Table Valued Parameters. But not only that. I like them, because they let you get on with writing straightforward, maintainable queries.


See also