In my Are you ready for SQL Server 2012 or are you still partying like it is 1999? post, I wrote about how you should start using SQL Server 2005 and SQL Server 2008 functionality now in order to prepare for SQL Server 2012. I still see tons of code that is written in the pre 2005 style and people still keep using those functions, procs and statements even though SQL Server 2005 and 2008 have much better functionality.
Today we are going to take a look at how to do an upsert by using the MERGE statement. If you do not know what an upsert is, it basically will update rows in the table if there are already rows that match your condition, otherwise it will insert into the table
First let’s take a look at how you would do this before SQL Server 2008
Create this simple table
CREATE TABLE TestMerge (id int not null, SomeName varchar(100) not null)
go
INSERT TestMerge values(1,'Denis1')
INSERT TestMerge values(2,'Denis2')
INSERT TestMerge values(3,'Denis3')
INSERT TestMerge values(4,'Denis4')
INSERT TestMerge values(5,'Denis5')
INSERT TestMerge values(6,'Denis6')
INSERT TestMerge values(7,'Denis7')
One way to do an upsert in the pre 2008 days is to do the following
UPDATE TestMerge
SET SomeName = 'Denis10'
WHERE id = 10
IF @@ROWCOUNT = 0
INSERT INTO TestMerge VALUES (10,'Denis10')
SELECT * FROM TestMerge
DELETE TestMerge WHERE ID =10
As you can see, we do an update, if @@ROWCOUNT is 0, there were no rows update, this means we have to do an insert
Here is another way
IF EXISTS (SELECT 1 FROM TestMerge
WHERE ID = 10)
BEGIN
UPDATE TestMerge
SET SomeName = 'Denis11'
WHERE id = 10
END
ELSE
BEGIN
INSERT INTO TestMerge VALUES (10,'Denis10')
END
DELETE TestMerge WHERE ID =10
As you can see we check if we have a row with the id that we are looking for, if the exists check returns true, we update, otherwise we are doing an insert
Merge
Now here is how it is done in SQL Server 2008 and up
Be aware that when you use the MERGE statement, you need to terminate it with a semi-colon, otherwise you’ll get the following message
Msg 10713, Level 15, State 1, Line 10
A MERGE statement must be terminated by a semi-colon (;).
Here is an example to look at, I added comments so that you can see what is going on
DECLARE @ID int = 9, @Name varchar(100) = 'BLA10'
MERGE TestMerge AS target -- this is the table you are upadting/insert into
USING (SELECT @ID, @Name) AS source (id, SomeName) -- variables mapped to the columns
ON (target.id = source.id) -- this is the key column we want to join on
WHEN MATCHED THEN -- if we have a match then we update
UPDATE SET SomeName = source.SomeName -- update the SomeName column
WHEN NOT MATCHED THEN -- if we don't match then we do an insert
INSERT (id, SomeName) -- do an insert
VALUES (source.id, source.SomeName); --these values are the ones in the
-- variables mapped to the column
Let’s run an example, this will add a row with the id 9 and the value in the SomeName column will be BLA
DECLARE @ID int = 9, @Name varchar(100) = 'BLA9'
MERGE TestMerge AS target
USING (SELECT @ID, @Name) AS source (id, SomeName)
ON (target.id = source.id)
WHEN MATCHED THEN
UPDATE SET SomeName = source.SomeName
WHEN NOT MATCHED THEN
INSERT (id, SomeName)
VALUES (source.id, source.SomeName);
SELECT * FROM TestMerge
Output -------------- 1 Denis1 2 Denis2 3 Denis3 4 Denis4 5 Denis5 6 Denis6 7 Denis7 9 BLA9
As you can see the row got added.
Now let’s try to use the same ID but we will change the value from BLA9 to BLA10
DECLARE @ID int = 9, @Name varchar(100) = 'BLA10'
MERGE TestMerge AS target
USING (SELECT @ID, @Name) AS source (id, SomeName)
ON (target.id = source.id)
WHEN MATCHED THEN
UPDATE SET SomeName = source.SomeName
WHEN NOT MATCHED THEN
INSERT (id, SomeName)
VALUES (source.id, source.SomeName);
SELECT * FROM TestMerge
Output -------------- 1 Denis1 2 Denis2 3 Denis3 4 Denis4 5 Denis5 6 Denis6 7 Denis7 9 BLA10
And as you can see for the row with ID 9, the SomeName column was updated.
You can actually output from the MERGE statement directly what action it is taking and what the value before and after is, very similar with the inserted and deleted pseudo tables in triggers. All we have to do is add
OUTPUT deleted.*, $action, inserted.*
Deleted.* will have the values before the update and inserted will have the value what it was updated to, $action will have the value UPDATE, INSERT or DELETE
DECLARE @ID int, @Name varchar(100)
SELECT @ID = 9,@Name = 'BLA9'
MERGE TestMerge AS target
USING (SELECT @ID, @Name) AS source (id, SomeName)
ON (target.id = source.id)
WHEN MATCHED THEN
UPDATE SET SomeName = source.SomeName
WHEN NOT MATCHED THEN
INSERT (id, SomeName)
VALUES (source.id, source.SomeName)
OUTPUT deleted.*, $action, inserted.*;
Output id SomeName $action id SomeName 9 BLA10 UPDATE 9 BLA9
Let’s run the same exact statement again
DECLARE @ID int, @Name varchar(100)
SELECT @ID = 9,@Name = 'BLA9'
MERGE TestMerge AS target
USING (SELECT @ID, @Name) AS source (id, SomeName)
ON (target.id = source.id)
WHEN MATCHED THEN
UPDATE SET SomeName = source.SomeName
WHEN NOT MATCHED THEN
INSERT (id, SomeName)
VALUES (source.id, source.SomeName)
OUTPUT deleted.*, $action, inserted.*;
Output id SomeName $action id SomeName 9 BLA9 UPDATE 9 BLA9
See what happened, an update was fired but nothing needed to change, what if we don’t want the update to fire in that case? Here is what we can do
Instead of
WHEN MATCHED THEN
Add a condition that will check if the SomeName is the same in both the source and the target
WHEN MATCHED AND target.SomeName <> source.SomeName THEN
Now when you run this, you won’t get any output
DECLARE @ID int, @Name varchar(100)
SELECT @ID = 9,@Name = 'BLA9'
MERGE TestMerge AS target
USING (SELECT @ID, @Name) AS source (id, SomeName)
ON (target.id = source.id)
WHEN MATCHED AND target.SomeName <> source.SomeName THEN
UPDATE SET SomeName = source.SomeName
WHEN NOT MATCHED THEN
INSERT (id, SomeName)
VALUES (source.id, source.SomeName)
OUTPUT deleted.*, $action, inserted.*;
Besides variable, you can also use another table as a source
First create this table
CREATE TABLE TestMerge2 (id int not null, SomeName varchar(100) not null)
go
INSERT TestMerge2 values(1,'Denis1 from table existing')
INSERT TestMerge2 values(12,'Denis12 from table new')
Now we are going to change this line
USING (SELECT @ID, @Name) AS source
Change it to the following
USING (SELECT id, SomeName from TestMerge2) AS source
That will tell the merge statement to use the TestMerge2 table as the source
Now run the following
MERGE TestMerge AS target
USING (SELECT id, SomeName from TestMerge2) AS source (id, SomeName)
ON (target.id = source.id)
WHEN MATCHED AND target.SomeName <> source.SomeName THEN
UPDATE SET SomeName = source.SomeName
WHEN NOT MATCHED THEN
INSERT (id, SomeName)
VALUES (source.id, source.SomeName)
OUTPUT deleted.*, $action, inserted.*;
Output
id SomeName $action id SomeName NULL NULL INSERT 12 Denis12 from table new 1 Denis1 UPDATE 1 Denis1 from table existing
Now, let’s see what is in the table
SELECT * FROM TestMerge
id SomeName 1 Denis1 from table existing 2 Denis2 3 Denis3 4 Denis4 5 Denis5 6 Denis6 7 Denis7 9 BLA9 12 Denis12 from table new
As you can see the table contains what we expected
Hopefully this post gave you some ideas on how to use the MERGE statement to do upserts. Come back tomorrow for the next post in this series