3. Using inserted and deleted Tables
In most trigger
situations, you need to know what changes were made as part of the data
modification. You can find this information in the inserted and deleted tables. For the AFTER trigger, these temporary memory-resident tables contain the rows modified by the statement. With the INSTEAD OF trigger, the inserted and deleted tables are actually temporary tables created on-the-fly.
The inserted and deleted
tables have identical column structures and names as the tables that
were modified. Consider running the following statement against the BigPubs2008 database:
UPDATE titles
SET price = $15.05
WHERE type LIKE '%cook%'
When this statement is
executed, a copy of the rows to be modified is recorded, along with a
copy of the rows after the modification. These copies are available to
the trigger in the deleted and inserted tables.
If you want to be able to see the contents of the deleted and inserted tables for testing purposes, you can create a copy of the table and then create a trigger on that copy (see Listing 2).
You can perform data modification statements and view the contents of
these tables without the modification actually taking place.
Listing 2. Viewing the Contents of the inserted and deleted Tables
--Create a copy of the titles table in the BigPubs2008 database
SELECT *
INTO titles_copy
FROM titles
GO
--add an AFTER trigger to this table for testing purposes
CREATE TRIGGER tc_tr ON titles_copy
FOR INSERT, UPDATE, DELETE
AS
PRINT 'Inserted:'
SELECT title_id, type, price FROM inserted
PRINT 'Deleted:'
SELECT title_id, type, price FROM deleted
ROLLBACK TRANSACTION
|
The inserted and deleted tables are available within the trigger after INSERT, UPDATE, and DELETE. Listing 3 shows the contents of inserted and deleted, as reported by the trigger when executing the preceding UPDATE statement.
Listing 3. Viewing the Contents of the inserted and deleted Tables When Updating the titles_copy Table
UPDATE titles_copy
SET price = $15.05
WHERE type LIKE '%cook%'
Inserted:
title_id type price
-------- ------------ ---------------------
TC7777 trad_cook 15.05
TC4203 trad_cook 15.05
TC3218 trad_cook 15.05
MC3021 mod_cook 15.05
MC2222 mod_cook 15.05
Deleted:
title_id type price
-------- ------------ ---------------------
TC7777 trad_cook 14.3279
TC4203 trad_cook 14.595
TC3218 trad_cook 0.0017
MC3021 mod_cook 15.894
MC2222 mod_cook 14.9532
|
Note
In SQL Server 2008, an
error message is displayed after a rollback is initiated in a trigger.
The error message indicates that the transaction ended in the trigger
and that the batch has been aborted. Prior to SQL Server 2005, an error
message was not displayed when a rollback was encountered in the
trigger.
The nature of the inserted and deleted tables enables you to determine the action that fired the trigger. For example, when an INSERT occurs, the deleted table is empty because there were no previous values prior to the insertion. Table 1 shows the DML triggering events and the corresponding contents in the deleted and inserted tables.
Table 1. Determining the Action That Fired a Trigger
Statement | Contents of inserted | Contents of deleted |
---|
INSERT | Rows added | Empty |
UPDATE | New rows | Old rows |
DELETE | Empty | Rows deleted |
Note
Triggers do not fire on a
row-by-row basis. One common mistake in coding triggers is to assume
that only one row is modified. However, triggers are set-based. If a
single statement affects multiple rows in the table, the trigger needs
to handle the processing of all the rows that were affected, not just
one row at a time.
One common approach to
dealing with the multiple rows in a trigger is to place the rows in a
cursor and then process each row that was affected, one at a time. This
approach works, but it can have an adverse effect on the performance of
the trigger. To keep your trigger execution fast, you should try to use
rowset-based logic instead of cursors in triggers when possible.
Rowset-based logic will
typically join to the inserted or deleted table that are available to a
trigger. You can join these tables to other tables that are being
manipulated by the trigger. For example, a trigger on a Job table can
update a related employee table with rowset-based logic such as :
UPDATE employee
SET employee.job_lvl = i.min_lvl
FROM inserted i
WHERE employee.emp_id = i.emp_id
This kind of logic will allow
the trigger update to work correctly if one job record is changed or
many job rows are changed at once. This is much more efficient than
loading all of the rows from the inserted table into a cursor which
updates the employee records one at a time within the cursor loop.
Checking for Column Updates
The UPDATE() function is available inside INSERT and UPDATE triggers. UPDATE() allows a trigger to determine whether a column was affected by the INSERT or UPDATE statement that fired the trigger. By testing whether a column was actually updated, you can avoid performing unnecessary work.
For example, suppose a
rule mandates that you cannot change the city for an author (a silly
rule, but it demonstrates a few key concepts). Listing 4 creates a trigger for both INSERT and UPDATE that enforces this rule on the authors table in the BigPubs2008 database.
Listing 4. Using the UPDATE() Function in a Trigger
CREATE TRIGGER tr_au_ins_upd ON authors
FOR INSERT, UPDATE
AS
IF UPDATE(city)
BEGIN
RAISERROR ('You cannot change the city.', 15, 1)
ROLLBACK TRAN
END
GO
UPDATE authors
SET city = city
WHERE au_id = '172-32-1176'
Server: Msg 50000, Level 15, State 1, Procedure
tr_au_ins_upd, Line 5
You cannot change the city.
|
Listing 4
shows how you generally write triggers that verify the integrity of
data. If the modification violates an integrity rule, an error message
is returned to the client application, and the modification is rolled
back.
The UPDATE() function evaluates to TRUE if you update the column in the UPDATE statement. As shown in the preceding example, you do not have to change the value in the column for the UPDATE() function to evaluate to TRUE, but the column must be referenced in the UPDATE statement. For example, with the author update, the city column was set it to itself (the value does not change), but the UPDATE() function still evaluates to TRUE.
Now you can add a couple of INSERT statements on the authors table:
INSERT authors (au_id, au_lname, au_fname, city, contract)
VALUES('111-11-1111', 'White', 'Johnson','Menlo Park', 1)
--Results from the previous insert
Server: Msg 50000, Level 15, State 1
You cannot change the city.
The UPDATE() function evaluates to TRUE and displays the error message. This outcome is expected because the trigger was created for INSERT as well, and the IF UPDATE condition is evaluated for both insertions and updates.
Now you can see what happens if you change the INSERT statement so that it does not include the city column in the INSERT:
INSERT authors (au_id, au_lname, au_fname, contract)
VALUES('111-11-2222', 'White', 'Johnson', 1)
Server: Msg 50000, Level 15, State 1
You cannot change the city.
The error message is still displayed, even though the insertion was performed without the city column. This process might seem counterintuitive, but the IF UPDATE condition always returns a TRUE value for INSERT actions. The reason is that the columns have either explicit default values or implicit (NULL) values inserted, even if they are not specified. The IF UPDATE conditions see this as a change and evaluate to TRUE.
If you change the tr_au_ins_upd trigger to be for UPDATE only (not INSERT and UPDATE), the insertions can take place without error.