On the StackOverflow podcast this week, I caught the guys talking about a SQL Server trigger question and I immediately panicked. I think nine out of ten times when I see a trigger, it’s written incorrectly, and sure enough, this one had dangerously incorrect answers.
Triggers: You’re Doing It Wrong
When writing a trigger, it’s easy to think in terms of individual records being inserted, updated or deleted. Developers and DBAs say to themselves, “I’m going to write a trigger to handle the record that just got inserted.” They write triggers like this below one, which is supposed to only insert records if SomeNumericField is actually numeric:
CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
DECLARE @isnum TINYINT;
SELECT @isnum = ISNUMERIC(SomeNumericField) FROM inserted;
IF (@isnum = 1)
INSERT INTO sometable SELECT * FROM inserted;
ELSE
RAISERROR('SomeNumericField must be numeric', 16, 1)
WITH SETERROR;
END
Examine that row in bold. What will happen in the trigger if we insert multiple records simultaneously, like if we run the following T-SQL statement?
INSERT INTO sometable SELECT * FROM someothertable
And what if some rows are valid, and some rows are not? Will we be able to predict the ISNUMERIC(SomeNumericField) result? Probably not. We can rewrite this two ways: either letting the good rows in, or not letting any rows in at all.
Trigger that Rejects Only the Bad Rows
Let’s rewrite it so that we insert the good rows, but reject the bad ones. I’m just using pseudocode here – you would want to clean this up for production:
CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
--First we insert the good rows
INSERT INTO sometable SELECT * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 1
--Then we check to see if we had any bad rows
IF EXISTS (SELECT TOP 1 * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 0)
RAISERROR('SomeNumericField must be numeric. Some rows were not inserted.', 16, 1)
WITH SETERROR;
END
So this trigger inserted our good rows where SomeNumericField is numeric, and then passed a message back to our code if any records were not inserted. This is kinda dangerous, because our application won’t know which rows succeeded and which ones failed. Ideally, we’d track which ones failed, so let’s add code to handle that:
CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
--First we insert the good rows
INSERT INTO sometable SELECT * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 1
--Then we insert the bad rows into a tracking table
INSERT INTO someRejectTable SELECT * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 0
--Then we check to see if we had any bad rows
IF EXISTS (SELECT TOP 1 * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 0)
RAISERROR('SomeNumericField must be numeric. Some rows were not inserted.', 16, 1)
WITH SETERROR;
END
When you design someRejectTable, all of the fields should be nvarchars, because we can’t predict what kind of data we’re going to be inserting. After all, the whole reason we’re dumping data into here is because SomeNumericField, which was supposed to be numeric, isn’t actually numeric.
This technique works well for data warehouses where we bring data in from text files and we can’t predict what will come in. Sometimes people change input file formats, and what used to be a numeric field now has text in it – item numbers are a great example.
Trigger that Rejects Everything if One Row is Bad
If we need to know absolutely everything is okay before we insert our data, here’s how to do it (again, this is pseudocode):
CREATE TRIGGER mytrigger ON sometable
INSTEAD OF INSERT
AS BEGIN
--Firsts we check to see if we had any bad rows
IF EXISTS (SELECT TOP 1 * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 0)
RAISERROR('SomeNumericField must be numeric. Nothing was inserted.', 16, 1)
WITH SETERROR;
ELSE
--We insert everything, since we know it's all good
INSERT INTO sometable SELECT * FROM inserted
END
In this code, we check all of the rows first, then if it succeeds, we insert the data.
More Reading on Trigger Development
If this stuff comes as a surprise to you, don’t panic: there’s a really good resource on getting started with T-SQL programming that covers this important issue and a lot of other gotchas. Check out Beginning SQL Server 2005 Programming by Robert Vieira. You can preview it in Google Books, and buy it from Amazon.
RE: inserting bad rows into reject table
Didn't you mean:
–Then we insert the bad rows into a tracking table
INSERT INTO someRejectTable SELECT * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 0 [not 1]
Otherwise it seems the two inserts seems the same except for the table.
You are absolutely right, my good man! Great catch. Fixed that.
Would changing the following optimize the query?
–Then we check to see if we had any bad rows
IF EXISTS (SELECT TOP 1 * FROM inserted WHERE ISNUMERIC(SomeNumericField) = 0)
BEGIN
–Then we insert the bad rows into a tracking table
INSERT INTO someRejectTable SELECT * FROM inserted
RAISERROR('SomeNumericField must be numeric. Some rows were not inserted.', 16, 1)
WITH SETERROR;
END
You're inserting all records from "inserted" into the someRejectTable if ANY of them have IsNumeric = 0, but that would be a problem since some of the records might be okay. It depends on how you want to handle rejects. If you want to reject ALL records if ANY of them are bad, then your method will work. If you want to take the good records but reject the bad ones, then stick with my method. Good idea though!
Duh? You're right. What was I thinking?
Hey, don't feel bad – I'm the one who wrote the blog post and had broken business logic in the first place! Thank goodness Jon Homan caught it.
Great tips btw. Thanks!
I don't use SQL Server, but doesn't is support row-level triggers, where the trigger truly is called once for each row, even when a batch of rows are inserted/modified? There are obviously efficiency issues with row-leve, but it seems like an alternative solution.
Right, there's efficiency problems – and being a DBA, I want to find the most efficient solution when I'm dealing with a performance-killing feature like triggers. Good idea though!
Please help me, I am facing a problem with instead of insert trigger where if I insert into table a, it should insert only not exixting rows, if there exists same row it should update few columns. but its working for multiple row insert like
insert into…select * from table b
here is my trigger code
ALTER TRIGGER DEMO
ON SAMPLE_DEMO
INSTEAD OF INSERT
AS
BEGIN
IF EXISTS(SELECT * FROM INSERTED I INNER JOIN SAMPLE_DEMO D ON I.INV_NUM=D.INV_NUM WHERE I.FSC_NAMED.FSC_NAME)
BEGIN
UPDATE SAMPLE_DEMO SET FSC_NAME=I.FSC_NAME, BUCKET=’NULL’ FROM INSERTED I,SAMPLE_DEMO D WHERE I.INV_NUM=D.INV_NUM
END
ELSE
BEGIN
UPDATE SAMPLE_DEMO SET BUCKET=’GLOBAL’ FROM INSERTED I,SAMPLE_DEMO D WHERE I.INV_NUM=D.INV_NUM
END
IF EXISTS(SELECT * FROM INSERTED WHERE INV_NUM NOT IN(SELECT INV_NUM FROM SAMPLE_DEMO))
BEGIN
SELECT * INTO TEMP FROM INSERTED WHERE INV_NUM NOT IN(SELECT INV_NUM FROM SAMPLE_DEMO)
INSERT INTO SAMPLE_DEMO SELECT * FROM TEMP
END
DROP TABLE TEMP
END
Deeps – rather than help you here, it’s better for you if you post this question on http://stackoverflow.com. That way you can get help from more programmers and SQL Server people at once in a collaborative environment.
i find good answers here…….. nice work and website…
thanks…….