A Trigger is a special type of stored procedure that is not executed manually. It fires automatically when a specific event occurs on a table.
It’s like an alarm system or a motion sensor: you don’t turn it on every time; you simply set up “If someone opens this door, sound the alarm.”
In SQL, the rule would be: “If someone deletes a customer, save it to the history table”
In T-SQL, there are two main types of Triggers:
- AFTER Triggers: Execute after the event occurs, and the changes have already been applied to the table.
- INSTEAD OF Triggers: Execute in place of the event, meaning the original event does not take place (unless the Trigger explicitly allows it).
Event Types
A Trigger executes automatically when a specific event occurs on a table. These events can be INSERT, DELETE, and UPDATE.
Within a trigger, SQL Server creates two temporary in-memory tables that contain the data affected by the operation:
- inserted: Contains the new rows (in an
INSERT) or the new versions of the rows (in anUPDATE). - deleted: Contains the deleted rows (in a
DELETE) or the old versions of the rows (in anUPDATE).
Depending on the event type, you will have data in one, the other, or both of these tables.
| Operation | inserted Table | deleted Table |
|---|---|---|
| INSERT | The new rows | (Empty) |
| DELETE | (Empty) | The deleted rows |
| UPDATE | The rows with the new values | The rows with the old values |
AFTER Triggers
These are the most common. They execute after the action has succeeded (INSERT, UPDATE, DELETE), but before committing the transaction.
For example, imagine we want that if someone changes a product’s price, a record of “who, when, and how much” is automatically saved.
-- 1. The table where we will save the history
CREATE TABLE HistorialPrecios (
ProductoID INT,
PrecioAntiguo DECIMAL(10,2),
PrecioNuevo DECIMAL(10,2),
Usuario VARCHAR(50),
FechaCambio DATETIME DEFAULT GETDATE()
);
GO
-- 2. The Trigger
CREATE TRIGGER trg_AuditoriaPrecios
ON Productos
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- Detect if the Price column was touched
IF UPDATE(Precio)
BEGIN
-- Insert into the history by joining the magical tables
INSERT INTO HistorialPrecios (ProductoID, PrecioAntiguo, PrecioNuevo, Usuario)
SELECT
i.ProductoID,
d.Precio, -- Old value (comes from DELETED)
i.Precio, -- New value (comes from INSERTED)
SYSTEM_USER -- Current user
FROM inserted i
INNER JOIN deleted d ON i.ProductoID = d.ProductoID;
END
END
Now, every time you do an UPDATE Productos SET Precio = ..., the trigger will fire silently and save the evidence.
Beware of “Row-by-Row” Thinking! The trigger does not fire once per row; it fires once per statement.
If you do a massive UPDATE of 1,000 products, the trigger runs only once, and the inserted table will have 1,000 rows.
That’s why in the previous example we used a set-based INSERT ... SELECT, not scalar variables.
INSTEAD OF Triggers
These are (interceptor) triggers that cancel the original action and execute their own code instead. They are very useful for protecting data or handling complex Views.
For example, the case of “Soft Delete”. In many companies, deleting data (DELETE) is prohibited. Instead, a column Activo = 0 is marked.
We can enforce this with a trigger, even if the programmer tries to do a DELETE.
CREATE TRIGGER trg_ProtegerClientes
ON Clientes
INSTEAD OF DELETE
AS
BEGIN
SET NOCOUNT ON;
-- Instead of deleting, we update the Activo field
UPDATE Clientes
SET Activo = 0
WHERE ClienteID IN (SELECT ClienteID FROM deleted);
PRINT 'The record was not deleted, it has been marked as inactive.';
END
If you execute DELETE FROM Clientes WHERE ID = 1, the record will still be there, but marked as inactive.
The Dark Side of Triggers
Triggers are very powerful, but they have a bad reputation among DBAs. Why?
- They are invisible: A new developer arrives, does an
INSERT, and suddenly gets an error or it takes a long time. They don’t know why, because the trigger is hidden. - They kill performance: The trigger is part of the transaction. If the trigger takes 5 seconds, your
INSERTwill take those 5 seconds. They hold locks on tables for longer. - Recursion: Trigger A updates table B, which has a trigger that updates table A… You can create infinite loops (SQL Server stops them after 32 levels, but the error will still occur).
Best Practices
- Keep them short: Minimal and very fast logic.
- Do not return results: Never put a
SELECT * FROM ...inside a trigger, or you will break the application that expects only a “rows affected” message. - Document: If you use triggers, document them clearly so the team knows they exist.
