Can’t be done? Oh, but it can. Let me show you.
Just quickly, a Table-Valued Parameter is a useful thing introduced in SQL 2008 that lets you have a read-only parameter which is a table type, passed into a stored procedure. To use it you need to have a user-defined table type, so that you can define what is going to be passed in. You can read about them at http://msdn.microsoft.com/en-us/library/bb510489.aspx
The 2008 code looks something like this:
/* First create a database to play in */
create database TVPDemo;
go
use TVPDemo;
go
/* And create a table with some sample data. I’m getting mine from AdventureWorks */
select ProductID, Name as ProductName, ListPrice, ProductSubcategoryID
into dbo.Products
from AdventureWorks.Production.Product;
go
/* Now for the real stuff – create a table type */
create type dbo.NewProducts as table
(ProductName nvarchar(100) collate Latin1_General_CI_AS
,ListPrice money
,SubCategory int
,DeleteMe bit
);
go
/* And a stored procedure which uses this table type */
create procedure dbo.MaintainProducts(@NewProducts dbo.NewProducts readonly) as
begin
/* Obviously we could MERGE – that’d work nicely here. But I want 2005 features */
/* Update some products into Products table */
update p set ListPrice = n.ListPrice, ProductSubcategoryID = n.SubCategory
from dbo.Products p
join @NewProducts n
on n.ProductName = p.ProductName
where n.DeleteMe = cast(0 as bit);
/* Insert some */
insert dbo.Products (ProductName, ListPrice, ProductSubcategoryID)
select n.ProductName, n.ListPrice, n.SubCategory
from @NewProducts n
where not exists (select * from dbo.Products p where p.ProductName = n.ProductName)
and n.DeleteMe = cast(0 as bit);
/* And delete some */
delete p
from dbo.Products p
join @NewProducts n
on n.ProductName = p.ProductName
where n.DeleteMe = cast(1 as bit);
/* Now list them all, returning this to the client */
select *
from dbo.Products;
end
go
/* Now let’s familiarise ourselves with what’s in Product now */
select *
from dbo.Products;
/* And do some maintenance on it. We create a table variable of the appropriate type, populate it and call the proc */
declare @SomeNewProducts dbo.NewProducts;
insert @SomeNewProducts (ProductName, ListPrice, SubCategory, DeleteMe)
select 'Blade', 0.1, 1, 0
union all
select 'Blade2', 0.1, null, 0
union all
select 'Bearing Ball', 1, 2, 1
;
exec dbo.MaintainProducts @SomeNewProducts;
/*
When we ran this stored procedure, the latest version of dbo.Products was outputted, so we can clearly see the new record, and the absence of the one we deleted.
Lovely
*/
But this wasn’t possible in SQL 2005. We didn’t have user-defined table types, and we certainly didn’t have table-valued parameters.
Except that we could still do something very similar. This was something I’d taken for granted, but when I showed this to someone at the PASS Summit, and then someone else, I got persuaded to write a blog post on it.
If you haven’t seen this idea before, I’m sure you’ll kick yourself. It’s remarkably simple, but I think it’s quite powerful. Like I said – I’d taken it for granted.
The idea is this: make a VIEW with an INSTEAD OF trigger, using the inserted table instead of the table variable.
That INSTEAD OF trigger is essentially where your stored procedure is kept. A trigger is still a procedure, it’s just not stored in the traditional list of stored procedures. But it will act just like one.
As for the view – that can just be a placeholder. Think of it as simply defining the columns you need to handle. You don’t need a FROM clause, and you don’t even need any rows to come back. I like to put a contradiction in there so that I don’t think there’s any real values coming out.
So a trigger doesn’t take a table-valued parameter, but it can leverage the inserted and deleted tables that are available in triggers. For us, we’re just interested in the former. Have a look at the code, and you’ll see what I mean.
This code can run on SQL 2005 (well, it can also run on later versions, but that’s less important).
/* First let’s set up a new database, just like we did in SQL 2008*/
create database TVPDemo;
go
use TVPDemo;
go
select ProductID, Name as ProductName, ListPrice, ProductSubcategoryID
into dbo.Products
from AdventureWorks.Production.Product;
go
/* Here’s the tricky bit. Make a view. Focus on the columns. I put WHERE 0=1 in, just to make it cleaner */
create view dbo.NewProducts as
select
cast(N'' as nvarchar(100)) collate SQL_Latin1_General_CP1_CI_AS as ProductName,
cast(0 as money) as ListPrice,
cast(0 as int) as SubCategory,
cast(0 as bit) as DeleteMe
where 0=1
;
go
/* This trigger contains the same code as in the 2008 stored procedure.
* But instead of having a table variable, we use the inserted table.
*/
create trigger dbo.MaintainProducts on dbo.NewProducts instead of insert as
begin
/* Update some products into Products table */
update p set ListPrice = i.ListPrice, ProductSubcategoryID = i.SubCategory
from dbo.Products p
join inserted i
on i.ProductName = p.ProductName
where i.DeleteMe = cast(0 as bit);
/* Insert some */
insert dbo.Products (ProductName, ListPrice, ProductSubcategoryID)
select i.ProductName, i.ListPrice, i.SubCategory
from inserted i
where not exists (select * from dbo.Products p where p.ProductName = i.ProductName)
and i.DeleteMe = cast(0 as bit);
/* And delete some */
delete p
from dbo.Products p
join inserted i
on i.ProductName = p.ProductName
where i.DeleteMe = cast(1 as bit);
/* Now list them all, returning this to the client */
select *
from dbo.Products;
end
go
/* Look what’s in there now */
select *
from dbo.Products;
/* Remember there’s never anything in here */
select *
from dbo.NewProducts;
/* Now we simply insert into our view. As we do, the trigger runs immediately and makes the changes */
insert dbo.NewProducts (ProductName, ListPrice, SubCategory, DeleteMe)
select 'Blade', 0.1, 1, 0
union all
select 'Blade2', 0.1, null, 0
union all
select 'Bearing Ball', 1, 2, 1
So there you have it – a useful TVP equivalent in versions prior to SQL 2008. I get that I’m probably writing this post about ten years too late. Sorry about that.
But if you’re not fond of the idea of having to declare and populate a table variable, then perhaps this idea is for you. This method will support any type of inserting, whether it’s row-by-row, or the results of a single SELECT statement. One day though, TVPs won’t be READONLY any more (this doesn’t seem to be the case for SQL Server 2012 unfortunately), and when that happens, you’ll want to definitely be using TVPs.
@rob_farley