Microsoft SQL Server 2000 Programming by Example
340
CREATE TRIGGER tr1_Customers
ON Customers
AFTER UPDATE
AS
Your code here
PRINT 'This is the tr1 trigger'
GO
CREATE TRIGGER tr2_Customers
ON Customers
AFTER UPDATE
AS
Your code here
PRINT 'This is the tr2 trigger'
GO
CREATE TRIGGER tr3_Customers
ON Customers
AFTER UPDATE
AS
Your code here
PRINT 'This is the tr3 trigger'
GO
Test the order of execution
By using a MOCK operation
UPDATE Customers
SET ContactName = ContactName
GO
Specify the tr3 trigger as first trigger to execute
EXEC sp_settriggerorder 'tr3_Customers', 'FIRST', 'UPDATE'
Specify the tr2 trigger as last trigger to
execute
EXEC sp_settriggerorder 'tr2_Customers', 'LAST', 'UPDATE'
Specify the tr1 trigger as any order to execute
EXEC sp_settriggerorder 'tr1_Customers', 'NONE', 'UPDATE'
GO
Test the order of execution
By using a MOCK operation
PRINT CHAR(10) + 'After reordering'+ CHAR(10)
UPDATE Customers
SET ContactName = ContactName
Go
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
341
This is the tr1 trigger
This is the tr2 trigger
This is the tr3 trigger
After reordering
This is the tr3 trigger
This is the tr1 trigger
This is the tr2 trigger
Caution
Remember that INSTEAD OF triggers are always executed before the data is modified. Therefore,
they execute before any of the AFTER triggers.
Checking for Updates on Specific Columns
To check inside a trigger if a column has been updated, you can use the IF UPDATE() clause. This clause
evaluates to TRUE if the column has been updated.
To test for changes in multiple columns in a single statement, use the COLUMNS_UPDATED() function. This
function returns a bitmap with the update status of every column in the base table. In other words,
COLUMNS_UPDATED returns a sequence of bits, one bit for every column, and the bit is 1 if the column has
been updated or otherwise it is 0.
Listing 9.15 shows an example of these two functions.
Listing 9.15 Inside a Trigger You Can Check Which Columns Have Been Updated
CREATE TRIGGER tr_OrderDetails
ON [Order Details]
AFTER UPDATE
AS
Testing for changes to the PRIMARY KEY
IF UPDATE(OrderID)
BEGIN
PRINT 'Changes to the PRIMARY KEY are not allowed'
ROLLBACK TRAN
Microsoft SQL Server 2000 Programming by Example
342
END
Testing for changes on the 2nd, 3rd and 5th columns
IF ((COLUMNS_UPDATED() & (2 + 4 + 8)) > 0)
BEGIN
IF ((COLUMNS_UPDATED() & 2) = 2)
PRINT 'ProductID updated'
IF ((COLUMNS_UPDATED() & 4) = 4)
PRINT 'UnitPrice updated'
IF ((COLUMNS_UPDATED() & 8) = 8)
PRINT 'Quantity updated'
END
GO
PRINT CHAR(10) + 'Updating ProductID and UnitPrice'
UPDATE [Order Details]
SET ProductID = ProductID,
UnitPrice = UnitPrice
PRINT CHAR(10) + 'Updating Quantity only'
UPDATE [Order Details]
SET Quantity = Quantity
PRINT CHAR(10) + 'Updating OrderID'
UPDATE [Order Details]
SET OrderID = OrderID
Updating ProductID and UnitPrice
ProductID updated
UnitPrice updated
Updating Quantity only
Quantity updated
Updating OrderID
Changes to the PRIMARY KEY are not allowed
Multiple-Row Considerations
Keep in mind that a trigger can be fired by an action that modifies a single row or multiple rows in a single
statement.
If you define your trigger to work for single rows only, you should reject changes that affect multiple rows. In
this case, you can check whether the system function @@ROWCOUNT returns a value greater than 1.
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
343
You can define your trigger to deal only with multiple-row operations. In this case, you could use aggregate
functions or use cursors. None of these strategies is efficient for single-row operations.
The ideal situation would be to create a trigger with conditional logic to deal with either single-row or multiple-
row operations depending on the value returned by @@ROWCOUNT. Listing 9.16 shows a new version of the
example of Listing 9.6, optimized for both kinds of transactions.
Listing 9.16 You Can Use @@ROWCOUNT to Detect Multiple-Row Operations
Create trigger for insert
CREATE TRIGGER isrOrderDetails
ON [Order Details]
AFTER INSERT
AS
IF @@ROWCOUNT = 1
BEGIN
Single-row operation
UPDATE TC
SET TotalSales = TotalSales
+ I.UnitPrice * Quantity * (1 - Discount)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
JOIN Inserted I
ON I.ProductID = P.productID
END
ELSE
BEGIN
Multi-row operation
UPDATE TC
SET TotalSales = TotalSales
+ (SELECT SUM(I.UnitPrice * Quantity * (1 - Discount))
FROM Inserted I
WHERE I.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
END
GO
Create trigger for delete
Microsoft SQL Server 2000 Programming by Example
344
CREATE TRIGGER delOrderDetails
ON [Order Details]
AFTER DELETE
AS
IF @@ROWCOUNT = 1
BEGIN
Single-row operation
UPDATE TC
SET TotalSales = TotalSales
- D.UnitPrice * Quantity * (1 - Discount)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
JOIN Deleted D
ON D.ProductID = P.productID
END
ELSE
BEGIN
Multi-row operation
UPDATE TC
SET TotalSales = TotalSales
- (SELECT SUM(D.UnitPrice * Quantity * (1 - Discount))
FROM Deleted D
WHERE D.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
END
GO
Create trigger for Update
CREATE TRIGGER udtOrderDetails
ON [Order Details]
AFTER UPDATE
AS
IF @@ROWCOUNT = 1
BEGIN
Single-row operation
UPDATE TC
SET TotalSales = TotalSales
+ I.UnitPrice * I.Quantity * (1 - I.Discount)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
JOIN Inserted I
ON I.ProductID = P.productID
UPDATE TC
SET TotalSales = TotalSales
- D.UnitPrice * D.Quantity * (1 - D.Discount)
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
345
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
JOIN Deleted D
ON D.ProductID = P.productID
END
ELSE
BEGIN
Multi-row operation
UPDATE TC
SET TotalSales = TotalSales
+ (SELECT SUM(I.UnitPrice * Quantity * (1 - Discount))
FROM Inserted I
WHERE I.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
UPDATE TC
SET TotalSales = TotalSales
- (SELECT SUM(D.UnitPrice * Quantity * (1 - Discount))
FROM Deleted D
WHERE D.ProductID = P.productID)
FROM TotalCategoriesSales TC
JOIN Products P
ON P.CategoryID = TC.CategoryID
END
GO
Tip
As shown previously in Listing 9.16, you can easily define a trigger for AFTER UPDATE as a
sequence of the actions defined in the AFTER INSERT and AFTER DELETE triggers.
Altering Trigger Definitions
To modify the definition of a trigger, you can use the ALTER TRIGGER statement. In this case, the trigger will
take the new definition directly. Listing 9.17 shows how to execute the ALTER TRIGGER statement to modify
the tr_Employees trigger.
The syntax is identical to the CREATE TRIGGER statement. Moreover, because triggers are independent
objects, no objects are depending on them. They can be dropped and re-created any time, if necessary.
Caution
You can change the name of a trigger using the sp_rename stored procedure, but this does not
change the name of the trigger stored in the definition of the trigger in syscomments.
To rename a trigger, it is recommended to drop the trigger and re-create it with a different name.
Microsoft SQL Server 2000 Programming by Example
346
Listing 9.17 You Can Use the ALTER TRIGGER Statement to Modify a Trigger.
USE Northwind
GO
Create a trigger to restrict
modifications to the employees table
to the dbo
CREATE TRIGGER tr_Employees
ON Employees
AFTER UPDATE, INSERT, DELETE
AS
IF CURRENT_USER <> 'dbo'
BEGIN
RAISERROR ('Only Database Owners can modify Employees, transaction rolled back',
10, 1)
ROLLBACK TRAN
END
GO
Modify the trigger to restrict
modifications to the employees table
to the members of the db_owner role
ALTER TRIGGER tr_Employees
ON Employees
AFTER UPDATE, INSERT, DELETE
AS
IF IS_MEMBER('db_owner') <> 1
BEGIN
RAISERROR ('Only Database Owners can modify Employees, transaction rolled back',
10 ,1)
ROLLBACK TRAN
END
GO
Disabling Triggers
To prevent triggers from running when data arrives through replication, you can add the NOT FOR
REPLICATION option to the CREATE TRIGGER or ALTER TRIGGER statements. In this case, the trigger will
fire on direct modifications to the base table, but not from subscription actions.
Temporarily, you can disable a trigger to speed up some processes. To do so, you can use the ALTER TABLE
statement with the DISABLE TRIGGER option, as in Listing 9.18.
Listing 9.18 You Can Disable a Trigger
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
347
USE Northwind
GO
To disable a single trigger
ALTER TABLE Employees
DISABLE TRIGGER tr_Employees , isr_Employees, udt_Employees
To disable several triggers from the same table
ALTER TABLE Employees
DISABLE TRIGGER tr_Employees, isr_Employees, udt_Employees
To disable all the triggers from a table
ALTER TABLE Employees
DISABLE TRIGGER ALL
To reenable the trigger, use the ALTER TABLE statement with the ENABLE TRIGGER option. Listing 9.19
shows how to reenable the triggers that were disabled in Listing 9.18.
Listing 9.19 You Can Reenable a Trigger
USE Northwind
GO
To enable a single trigger
ALTER TABLE Employees
ENABLE TRIGGER tr_Employees , isr_Employees, udt_Employees
To enable several triggers from the same table
ALTER TABLE Employees
ENABLE TRIGGER tr_Employees, isr_Employees, udt_Employees
To enable all the triggers from a table
ALTER TABLE Employees
ENABLE TRIGGER ALL
Nesting Triggers
Microsoft SQL Server 2000 Programming by Example
348
A trigger can be defined to modify a table, which in turn can have a trigger defined to modify another table,
and so on. In this case, triggers force the execution of other triggers, and the execution stops when the last
action does not fire any more triggers.
Because triggers are a specialized form of stored procedures, you can nest trigger execution up to 32 levels.
Triggers, stored procedures, scalar user-defined functions, and multistatement table-valued functions share
this limit. If the execution of a sequence of nested triggers requires more than 32 levels, the execution is
aborted, the transaction is rolled back, and the execution of the batch is cancelled.
Nested triggers are enabled by default. You can change this option at server level by setting the "nested
triggers" option to 0, using the system stored procedure sp_configure.
You can read the system function @@NESTLEVEL to know how many levels of nesting you have during the
execution of a trigger, stored procedure, or user-defined function.
Note
In a nested trigger situation, all the triggers are running inside the same transaction. Therefore, any
errors inside any of the triggers will roll back the entire transaction.
Listing 9.20 shows an example where you define triggers to maintain sales totals at different levels.
1. You insert, update, or delete data in the Order Details table. This data modification forces the
execution of the AFTER UPDATE trigger.
2. The AFTER UPDATE trigger in the Order Details table updates the SaleTotal column in the
Orders table.
3. Because the SaleTotal column in the Orders table has been updated, the existing AFTER
UPDATE trigger in the Orders table runs automatically and updates the SaleTotal column in the
Employees table and the Customers table.
Listing 9.20 You Can Create Triggers That Can Be Nested in Sequence
USE Northwind
GO
Add the column SaleTotal to the
Orders table
ALTER TABLE Orders
ADD SaleTotal money NULL
Add the column SaleTotal to the
Employees table
ALTER TABLE Employees
ADD SaleTotal money NULL
Add the column SaleTotal to the
Customers table
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
349
ALTER TABLE Customers
ADD SaleTotal money NULL
GO
Initialize the data
UPDATE Orders
SET SaleTotal =
(SELECT SUM([Order Details].UnitPrice * Quantity * (1 - Discount))
FROM [Order Details]
WHERE [Order Details].OrderID = Orders.OrderID)
UPDATE Employees
SET SaleTotal =
(SELECT SUM(Orders.SaleTotal)
FROM Orders
WHERE Orders.EmployeeID = Employees.EmployeeID)
UPDATE Customers
SET SaleTotal =
(SELECT SUM(Orders.SaleTotal)
FROM Orders
WHERE Orders.CustomerID = Customers.CustomerID)
GO
Create nested triggers
CREATE TRIGGER isrTotalOrderDetails
ON [Order details]
AFTER INSERT, DELETE, UPDATE
AS
IF @@rowcount = 1
Single-row operation
UPDATE Orders
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)
- ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
WHERE Deleted.OrderID = Orders.OrderID), 0)
ELSE
Multi-row operation
UPDATE Orders
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)
- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
Microsoft SQL Server 2000 Programming by Example
350
WHERE Deleted.OrderID = Orders.OrderID), 0)
GO
CREATE TRIGGER isrTotalOrders
ON Orders
AFTER INSERT, DELETE, UPDATE
AS
IF @@rowcount = 1
BEGIN
Single-row operation
UPDATE Employees
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SaleTotal
FROM Inserted
WHERE Inserted.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT SaleTotal
FROM Deleted
WHERE Deleted.EmployeeID = Employees.EmployeeID), 0)
UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SaleTotal
FROM Inserted
WHERE Inserted.CustomerID = Customers.CustomerID), 0)
- ISNULL(
(SELECT SaleTotal
FROM Deleted
WHERE Deleted.CustomerID = Customers.CustomerID), 0)
END
ELSE
BEGIN
Multi-row operation
UPDATE Employees
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(SaleTotal)
FROM Inserted
WHERE Inserted.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT SUM(SaleTotal)
FROM Deleted
WHERE Deleted.EmployeeID = Employees.EmployeeID), 0)
UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(SaleTotal)
FROM Inserted
WHERE Inserted.CustomerID = Customers.CustomerID), 0)
- ISNULL(
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
351
(SELECT SUM(SaleTotal)
FROM Deleted
WHERE Deleted.CustomerID = Customers.CustomerID), 0)
END
GO
Updating Order Details
and forcing the nested triggers
execution
update [order details]
set quantity = 100
where orderid = 10248
and productid = 11
Testing totals in Orders table
select CustomerID, EmployeeID, SaleTotal from orders
WHERE OrderID = 10248
SELECT SUM([Order Details].UnitPrice * Quantity * (1 - Discount))
FROM [Order Details]
WHERE OrderID = 10248
Testing totals in Employees
SELECT SaleTotal
FROM Employees
WHERE EmployeeID = 5
SELECT SUM(SaleTotal)
FROM Orders
WHERE EmployeeID = 5
Testing totals in Customers
SELECT SaleTotal
FROM Customers
WHERE CustomerID = 'VINET'
SELECT SUM(SaleTotal)
FROM Orders
WHERE CustomerID = 'VINET'
GO
Dropping triggers
DROP TRIGGER isrTotalOrderDetails
DROP TRIGGER isrTotalOrders
Analyzing the previous example, you can see that the data is updated only at the Order Details level, and
two nested triggers maintain the summary information in the tables Orders, Employees, and Customers.
You can solve the same problem without using nested triggers. Create three triggers in the Order Details
table: one trigger to update the SaleTotal column in the Orders table, a second trigger to update the
Employees table, and a third one to update the Customers table. You can see in Listing 9.21 how to
implement this solution (note, you must execute the code from Listing 9.20 before running the code from
Listing 9.21).
Tip
Microsoft SQL Server 2000 Programming by Example
352
Create one trigger per logical action, as in Listing 9.21, and avoid nested triggers. Your database
application will be more modular and the maintenance will be easier.
Listing 9.21 Every Table Can Have Multiple Triggers for Each Action
USE Northwind
GO
CREATE TRIGGER tr_OrderDetails_TotalOrders
ON [Order details]
AFTER INSERT, DELETE, UPDATE
AS
IF @@rowcount = 1
Single-row operation
UPDATE Orders
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)
- ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
WHERE Deleted.OrderID = Orders.OrderID), 0)
ELSE
Multi-row operation
UPDATE Orders
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
WHERE Inserted.OrderID = Orders.OrderID), 0)
- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
WHERE Deleted.OrderID = Orders.OrderID), 0)
GO
CREATE TRIGGER tr_OrderDetails_TotalEmployees
ON [Order details]
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
353
AFTER INSERT, DELETE, UPDATE
AS
IF @@rowcount = 1
Single-row operation
UPDATE Employees
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
JOIN Orders
ON Inserted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)
ELSE
Multi-row operation
UPDATE Employees
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
JOIN Orders
ON Inserted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)
- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.EmployeeID = Employees.EmployeeID), 0)
GO
CREATE TRIGGER tr_OrderDetails_TotalCustomers
ON [Order details]
AFTER INSERT, DELETE, UPDATE
AS
IF @@rowcount = 1
Single-row operation
UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Inserted
JOIN Orders
ON Inserted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)
- ISNULL(
Microsoft SQL Server 2000 Programming by Example
354
(SELECT UnitPrice * Quantity * (1 - Discount)
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)
ELSE
Multi-row operation
UPDATE Customers
SET SaleTotal = SaleTotal
+ ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Inserted
JOIN Orders
ON Inserted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)
- ISNULL(
(SELECT SUM(UnitPrice * Quantity * (1 - Discount))
FROM Deleted
JOIN Orders
ON Deleted.OrderID = Orders.OrderID
WHERE Orders.CustomerID = Customers.CustomerID), 0)
GO
DROP TRIGGER tr_OrderDetails_TotalOrders
DROP TRIGGER tr_OrderDetails_TotalCustomers
DROP TRIGGER tr_OrderDetails_TotalEmployees
GO
Note
The examples in Listings 9.20 and 9.21 create a single trigger for the three actions: INSERT,
DELETE, and UPDATE. Creating individual triggers per action is more efficient, as in Listing 9.16,
from the execution point of view. I use this strategy here only to simplify the examples.
Recursive Triggers
If a trigger defined in the Products table modifies data in the Employees table, and the Employees table
has a trigger that in turn modifies the Products table, the trigger defined in the Products table will fire again.
This situation is called indirect recursion, because a single statement forces multiple executions of the same
trigger, through the execution of other triggers. This is a special case of nested triggers, and everything said
about it in the preceding section can be applied to this case.
In some scenarios, it is possible to have direct recursion, when a table has a trigger that modifies some data
in the table again. In this case, by default, SQL Server will not fire the trigger again, avoiding this direct
recursion.
To enable trigger recursion in a database you must set the 'recursive triggers' option to 'true' at
database level using the sp_dboption system stored procedure, or set the option RECURSIVE_TRIGGERS
ON in the ALTER DATABASE statement. Listing 9.22 shows both statements.
Listing 9.22 You Can Enable Recursive Triggers at Database Level Only
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
355
Enable Recursive triggers in Northwind
EXEC sp_dboption 'Northwind', 'recursive triggers', 'true'
Disable Recursive triggers in Northwind
ALTER DATABASE Northwind
SET RECURSIVE_TRIGGERS OFF
Consider the typical hierarchical table where you save cost and budget breakdown of a project cost control
system. Every row in this table has a single ID as primary key, but it refers to another row as a parent row,
excluding the root row: the project itself. Any change on Cost or Budget in a row has to be escalated to the
highest level, and you introduce costs only in rows with no children.
This strategy is very flexible, adjusting changes easily on the distribution of activities in the project. Listing
9.23 shows the code to implement this example .
Listing 9.23 Use Triggers to Maintain Hierarchical Data
Create the base table
CREATE TABLE CostBudgetControl (
ID int NOT NULL
PRIMARY KEY,
Name nvarchar(100) NOT NULL,
ParentID int NULL
REFERENCES CostBudgetControl(ID),
Cost money NOT NULL DEFAULT 0,
Budget money NOT NULL DEFAULT 0,
HasChildren bit DEFAULT 0)
Insert Cost Structure
Create a text file (Gas.txt)
with the following contents:
/*
1, Gas Pipeline Project, 0, 85601000.0000, 117500000.0000, 1
2, Engineering, 1, 800000.0000, 950000.0000, 1
3, Materials, 1, 23400000.0000, 28000000.0000, 1
4, Construction, 1, 61000000.0000, 88000000.0000, 1
5, Supervision, 1, 401000.0000, 550000.0000, 1
6, Line, 2, 300000.0000, 400000.0000, 0
7, Stations, 2, 500000.0000, 550000.0000, 0
8, Pipes, 3, 14500000.0000, 16000000.0000, 0
9, Machinery, 3, 8900000.0000, 12000000.0000, 0
10, Section A, 4, 31000000.0000, 47000000.0000, 1
Microsoft SQL Server 2000 Programming by Example
356
11, Section B, 4, 30000000.0000, 41000000.0000, 1
12, Welding, 5, 200000.0000, 250000.0000, 0
13, Civil, 5, 145000.0000, 200000.0000, 0
14, Buildings, 5, 56000.0000, 100000.0000, 0
15, Civil works, 10, 20000000.0000, 30000000.0000, 0
16, Civil works, 11, 18000000.0000, 25000000.0000, 0
17, Pipeline, 10, 11000000.0000, 17000000.0000, 0
18, Pipeline, 11, 12000000.0000, 16000000.0000, 0
*/
BULK INSERT Northwind.dbo.CostBudgetControl
FROM 'C:\Gas.txt'
WITH
(
FIELDTERMINATOR = ', ',
ROWTERMINATOR = '\n'
)
GO
UPDATE CostBudgetControl
SET ParentID = NULL
WHERE ID = 1
GO
Create the recursive trigger
CREATE TRIGGER udtCostBudget
ON CostBudgetControl
AFTER UPDATE
AS
IF @@rowcount>0
UPDATE CostBudgetControl
SET Cost = Cost
+ ISNULL((SELECT SUM(Cost)
FROM Inserted
WHERE Inserted.ParentID = CostBudgetControl.ID), 0)
- ISNULL((SELECT SUM(Cost)
FROM Deleted
WHERE Deleted.ParentID = CostBudgetControl.ID), 0),
Budget = Budget
+ ISNULL((SELECT SUM(Budget)
FROM Inserted
WHERE Inserted.ParentID = CostBudgetControl.ID), 0)
- ISNULL((SELECT SUM(Budget)
FROM Deleted
WHERE Deleted.ParentID = CostBudgetControl.ID), 0)
WHERE ID IN
(SELECT ParentID
FROM Inserted
UNION
SELECT ParentID
FROM Deleted)
GO
Enable Recursive triggers
ALTER DATABASE Northwind
SET RECURSIVE_TRIGGERS ON
GO
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
357
Total Cost and Budget
Before the update
SELECT Cost, Budget
FROM CostBudgetControl
WHERE ID = 1
Update some cost
UPDATE CostBudgetControl
SET Cost = 12500000.0000
WHERE ID = 17
Total Cost and Budget
After the update listings;triggers;mainataining hierarchical
data>
SELECT Cost, Budget
FROM CostBudgetControl
WHERE ID = 1
GO
DROP TABLE CostBudgetControl
Cost Budget
85601000.0000 117500000.0000
Cost Budget
87101000.0000 117500000.0000
Security Implications of Using Triggers
Only certain users can create triggers:
• The owner of the table on which the trigger has to be defined
• Members of the db_owner and db_ddladmin database roles
• Members of the sysadmin server role, because permissions don't affect them
The user who creates the trigger needs specific permissions to execute the statements defined in the code of
the trigger.
Caution
If any of the objects referenced in the trigger don't belong to the same owner, you can have a
broken ownership chain situation. To avoid this situation, it is recommended that dbo must be the
owner of all the objects in a database.
Microsoft SQL Server 2000 Programming by Example
358
Enforcing Business Rules: Choosing Among INSTEAD of Triggers,
Constraints, and AFTER Triggers
This is the final chapter that discusses techniques to enforce data integrity, and as a summary, you can
propose which ways are recommended to enforce data integrity:
• To uniquely identify every row, define a PRIMARY KEY constraint. This is one of the first rules to apply
to designing a normalized database. Searching for values contained in a PRIMARY KEY is fast
because there is a UNIQUE INDEX supporting the PRIMARY KEY.
• To enforce uniqueness of required values in a column or group of columns, other than the PRIMARY
KEY, define a UNIQUE constraint. This constraint does not produce much overhead because there is
a UNIQUE INDEX supporting this constraint.
• To enforce uniqueness of optional values (columns that accept NULL), create a TRIGGER. You can
test this uniqueness before the data modification with an INSTEAD OF trigger, or after the data
modification with an AFTER trigger.
• To validate entries in a column, according to a specific pattern, range, or format, create a CHECK
constraint.
• To validate values in a row, where values in different columns must satisfy specific conditions, create
one or more CHECK constraints. If you create one CHECK constraint per condition, you can later
disable specific conditions only, if required.
• To validate values in a column, among a list of possible values, create a look-up table (LUT) with the
required values and create a FOREIGN KEY constraint to reference the look-up table. You could
create a CHECK constraint instead, but using a LUT is more flexible.
• To restrict values in a column to the values contained in a column in a second table, create a
FOREIGN KEY constraint in the first table .
• To make sure that every entry in a column is related to the primary key of another table, without
exceptions, define the FOREIGN KEY column as NOT NULL.
• To restrict the values in a column to complex conditions involving other rows in the same table, create
a TRIGGER to check these conditions. As an alternative, create a CHECK constraint with a user-
defined function to check this complex condition.
• To restrict the values in a column to complex conditions involving other tables in the same or different
database, create a TRIGGER to check these conditions.
• To declare a column as required, specify NOT NULL in the column definition.
• To specify a default value for columns where no value is supplied in INSERT operations, declare a
DEFAULT property for the column.
• To declare a column as autonumeric, declare an IDENTITY property in the column and specify the
seed value and the increment.
• To declare a default value, which depends on values in other rows or tables, declare a DEFAULT
property for the column using a user-defined function as a default expression.
• To cascade changes on primary keys to related fields in other tables, declare a FOREIGN KEY with
the ON UPDATE CASCADE clause. Do not create triggers to perform this operation.
• To delete in cascade related rows when the row in the primary table is deleted, declare a FOREIGN
KEY with the ON DELETE CASCADE clause. Do not create triggers to perform this operation.
• To cascade complex operations to other tables to maintain denormalized data, create individual
triggers to execute this operation.
• To validate INSERT, UPDATE, or DELETE operations applied through a view, define an INSTEAD OF
trigger on the view.
• Do not use RULE objects unless you want to define self-contained user-defined data types. It is
recommended to declare CHECK constraints instead.
• Do not use DEFAULT objects unless you want to define self-contained user-defined data types. It is
recommended to declare DEFAULT definitions instead .
What's Next?
This chapter covered the creation and use of triggers as a way to enforce complex data integrity.
Chapter 9. Implementing Complex Processing Logic: Programming Triggers
359
Chapter 10, "Enhancing Business Logic: User-Defined Functions (UDF)," covers user-defined
functions, which can be used as part of the trigger definition and as an alternative to triggers, providing extra
computing capabilities to CHECK constraints and DEFAULT definitions.
Chapter 12, "Row-Oriented Processing: Using Cursors," explains how to use cursors. This could be
useful in some triggers to deal with multiple-row actions.
Triggers always work inside a transaction, and Chapter 13, "Maintaining Data Consistency:
Transactions and Locks," covers specifically that: transaction and locks. There you can see the
implications of modifying data through triggers and how to increase concurrency, preventing undesired
blockings.
Chapter 10. Enhancing Business Logic: User-Defined Functions (UDF)
361
Chapter 10. Enhancing Business Logic: User-Defined
Functions (UDF)
Procedural languages are based mainly in the capability to create functions, encapsulate complex
programming functionality, and return a value as a result of the operation. Using SQL Server 2000, you can
define user-defined functions(UDF), which combine the functionality of stored procedures and views but
provide extended flexibility.
This chapter teaches you the following:
• What the built-in user-defined functions are and how to use them
• How to define user-defined functions that return a scalar value
• How to define user-defined functions that return a result set
• How to convert stored procedures and views into user-defined functions
• How to extend the functionality of constraints with user-defined functions
Benefits of User-Defined Functions
You learned in Chapter 8, "Implementing Business Logic: Programming Stored Procedures," how to
create stored procedures, which are similar to the way you create functions in other programming languages.
However, using stored procedures from Transact-SQL is not very flexible, because you can use them only
with the EXECUTE or INSERT EXECUTE statements. If you have a stored procedure that returns a single
value, you cannot use this procedure inside an expression. If your procedure returns a result set, you cannot
use this procedure in the FROM clause of any Transact-SQL statement.
In Chapter 3, "Working with Tables and Views," you learned about views and how to use them
anywhere as a replacement for tables. However, when you define a view, you are limited to a single SELECT
statement. Unlike stored procedures, you cannot define parameters in a view.
Some user-defined functions are similar to views but they can be defined with more than one statement and
they accept parameters. You can call user-defined functions in the same way you execute stored procedures,
and you can use scalar user-defined functions as part of any expression anywhere in a Transact-SQL
statement where an expression is valid. Furthermore, you can use a user-defined function that returns a table
in the FROM clause of any Transact-SQL Data Manipulation Language (DML) statement.
User-defined functions have many benefits in common with stored procedures, as covered in Chapter 8.
However, user-defined functions have more useful benefits. They enable you to
• Use the result set returned by a stored procedure in the FROM clause of a query
• Join the results of two stored procedures, without using temporary tables to store intermediate results
• Use the result of a stored procedure in the IN operator
• Use a stored procedure as a subquery in the WHERE clause
• Create a view that cannot be solved with a single SELECT statement
• Create a view with parameters similar to the way Microsoft Access creates queries with parameters
• Extend the list of built-in functions with any financial function
• Create new mathematical functions for any special scientific database applications that you might
require
This chapter will help you discover how user-defined functions can help you solve these common
programming problems.
Built-In User-Defined Functions
SQL Server 2000 implements some system functions as built-in user-defined functions. Many of them are not
documented; Query Analyzer, Enterprise Manager, Profiler, Replication, and other client applications and
system processes use some of these built-in user-defined functions internally. These functions can be used
almost as any other user-defined function, but SQL Server itself implements them.
Microsoft SQL Server 2000 Programming by Example
362
You cannot change the definition of these built-in user-defined functions. In some cases, you cannot see their
definition using the sp_help or sp_helptext system stored procedures, and you cannot script them.
However, their definition is stored in the syscomments system table as any other user-defined function.
Caution
Microsoft does not guarantee that undocumented built-in user-defined functions will remain
unchanged in the future; however, we can use some of them as examples of what kind of
operations a user-defined function can do.
In some cases, built-in user-defined functions return a single scalar value, and all of them are undocumented:
• fn_CharIsWhiteSpace(@nchar) returns 1 if the variable @nchar only contains a space, a tab
character, a newline character, or carriage return character; it returns 0 otherwise.
• fn_MSSharedVersion(@len_minorversion) returns the major and minor version number of
SQL Server. @len_minorversion specifies how many digits to show for the minor version.
• fn_MsGenSqeScStr(@pstrin) returns the string @pstring, converting single quotes into two
single quotes so that you are able to concatenate this string with other strings to execute a dynamic
statement.
• fn_IsReplMergeAgent() returns 1 if the present process is executed by the Replication Merge
Agent.
• fn_GetPersistedServerNameCaseVariation(@servername) returns the server name of the
server specified in @servername with exactly the same case it uses in the sysservers system
table, regardless of the case used to call this function.
• fn_ReplGetBinary8LoDWord(@binary8_value) takes the lower four bytes from the
@binary8_value binary variable and converts them into an integer value.
• fn_ReplPrepadBinary8(@varbinary8_value) converts the varbinary(8) value stored in
@varbinary8_value into a fixed-length binary(8) value with leading zeros.
• fn_ReplMakeStringLiteral(@string) converts the value stored in the @string value into a
UNICODE string, including quotes, such as N'Hello', to be used in dynamically constructed
statements.
• fn_ReplQuoteName(@string) returns the value stored in @string en closed in square brackets.
You can use this function in dynamic execution to select object names that contain spaces or
keywords, such as [Order Details].
• fn_GenerateParameterPattern(@parameter) returns a pattern string you can use with the
LIKE operator to test for strings containing any case variation of the value stored in @parameter,
such as converting 'Hello' into '%[hH][eE][lL][lL][oO]%'. This is useful in case-sensitive
servers, databases, or columns.
• fn_UpdateParameterWithArgument, fn_SkipParameterArgument, and
fn_RemoveParameterWithArgument are internal functions, and their study is not the purpose of
this book.
Listing 10.1 shows some examples of scalar built-in, user-defined functions and the partial result of some of
them.
Listing 10.1 Using Undocumented Built-In User-Defined Functions
Chapter 10. Enhancing Business Logic: User-Defined Functions (UDF)
363
USE Northwind
GO
PRINT CHAR(10)
+ 'fn_chariswhitespace(CHAR(9))'
+ CHAR(10)
select fn_chariswhitespace(CHAR(9))
GO
PRINT CHAR(10)
+ 'fn_mssharedversion(1)'
+ CHAR(10)
select master.dbo.fn_mssharedversion(1)
GO
PRINT CHAR(10)
+ 'fn_replgetbinary8lodword(0x0304030401020102)'
+ CHAR(10)
select fn_replgetbinary8lodword(0x0304030401020102)
GO
PRINT CHAR(10)
+ 'fn_replmakestringliteral(@a)'
+ CHAR(10)
declare @a varchar(100)
set @a = 'peter is right'
select fn_replmakestringliteral(@a)
GO
PRINT CHAR(10)
+ 'fn_replprepadbinary8(123456890123)'
+ CHAR(10)
select fn_replprepadbinary8(123456890123)
GO
PRINT CHAR(10)
+ 'fn_replquotename("hello")'
+ CHAR(10)
select fn_replquotename('hello')
Microsoft SQL Server 2000 Programming by Example
364
fn_chariswhitespace(CHAR(9))
1
fn_mssharedversion(1)
80
fn_replgetbinary8lodword(0x0304030401020102)
16908546
fn_replmakestringliteral(@a)
N'peter is right'
fn_replprepadbinary8(123456890123)
0x0C0000010BA59ABE
fn_replquotename("hello")
[hello]
In other cases, built-in, user-defined functions return a table. SQL Server documents some of them:
• fn_ListExtendedProperty produces a list of available extended properties for a given database
or database objects, such as database users, user-defined data types, tables, views, stored
procedures, user-defined functions, default objects, rule objects, columns of tables and views,
parameters of stored procedures and user-defined functions, indexes, constraints, and triggers.
• fn_HelpCollations returns a list of the available collations.
• fn_ServerSharedDrives returns a list of the drives shared by a clustered server.
• fn_VirtualServerNodes returns the list of server nodes, defining a virtual server in a clustering
server environment.
• fn_VirtualFileStats returns statistical I/O information about any file in a database, including
transaction log files.
Listing 10.2 shows some examples of how to use these table-valued, built-in, user-defined functions.
Note