πŸŽ‰ 75% of content is free forever β€” Unlock Premium from $10/mo β†’
CW
Search courses…
πŸ’Ό Servicesℹ️ Aboutβœ‰οΈ ContactView Pricing Plansfrom $10

Database Triggers

SQL AdvancedTriggers🟒 Free Lesson

Advertisement

SQL Advanced

Database Triggers

Automatically fire SQL logic in response to data changes β€” no application code required.

  • AFTER Triggers β€” Fire after the triggering statement completes successfully
  • INSTEAD OF Triggers β€” Replace the triggering statement with custom logic
  • Event-Driven β€” Respond to INSERT, UPDATE, or DELETE operations on tables Triggers enforce business rules at the database level, ensuring consistency regardless of the application.

What Is a Trigger?

DfDatabase Trigger

A named block of SQL code that automatically executes (fires) when a specified data modification event occurs on a table or view. Triggers cannot be called directly β€” they are invoked by the database engine in response to INSERT, UPDATE, or DELETE operations.

Triggers are powerful for enforcing complex business rules, maintaining audit trails, and synchronizing data across tables. However, they execute silently and can be difficult to debug, so they should be used judiciously.

-- Simple AFTER trigger: audit trail
CREATE TRIGGER trg_AuditEmployeeChanges
ON employees
AFTER UPDATE
AS
BEGIN
    INSERT INTO employee_audit (employee_id, change_type, changed_by, changed_date)
    SELECT
        i.employee_id,
        'UPDATE',
        SYSTEM_USER,
        GETDATE()
    FROM inserted i
    INNER JOIN deleted d ON i.employee_id = d.employee_id
    WHERE i.salary <> d.salary OR i.department_id <> d.department_id;
END;

INSERT Triggers

Fires when new rows are added to a table. The inserted virtual table contains the new rows.

CREATE TRIGGER trg_ValidateNewOrder
ON orders
AFTER INSERT
AS
BEGIN
    -- Reject orders with zero or negative quantity
    IF EXISTS (SELECT 1 FROM inserted WHERE quantity <= 0)
    BEGIN
        RAISERROR('Order quantity must be positive.', 16, 1);
        ROLLBACK;
        RETURN;
    END

    -- Auto-calculate total
    UPDATE o
    SET total = i.quantity * p.price
    FROM orders o
    INNER JOIN inserted i ON o.order_id = i.order_id
    INNER JOIN products p ON i.product_id = p.product_id;

    -- Log the insertion
    INSERT INTO order_log (order_id, action, action_date, performed_by)
    SELECT order_id, 'CREATED', GETDATE(), SYSTEM_USER
    FROM inserted;
END;

UPDATE Triggers

Fires when existing rows are modified. Both inserted (new values) and deleted (old values) virtual tables are available.

CREATE TRIGGER trg_PreventSalaryDecrease
ON employees
AFTER UPDATE
AS
BEGIN
    IF EXISTS (
        SELECT 1
        FROM inserted i
        INNER JOIN deleted d ON i.employee_id = d.employee_id
        WHERE i.salary < d.salary
    )
    BEGIN
        RAISERROR('Salary decreases are not allowed. Use HR approval process.', 16, 1);
        ROLLBACK;
        RETURN;
    END

    -- Log salary increases
    INSERT INTO salary_history (employee_id, old_salary, new_salary, change_date, changed_by)
    SELECT
        i.employee_id,
        d.salary,
        i.salary,
        GETDATE(),
        SYSTEM_USER
    FROM inserted i
    INNER JOIN deleted d ON i.employee_id = d.employee_id
    WHERE i.salary > d.salary;
END;

DELETE Triggers

Fires when rows are removed. The deleted virtual table contains the rows that were removed.

CREATE TRIGGER trg_ArchiveDeletedOrders
ON orders
AFTER DELETE
AS
BEGIN
    -- Archive deleted orders
    INSERT INTO orders_archive (
        order_id, customer_id, order_date, total, status,
        deleted_by, deleted_date
    )
    SELECT
        d.order_id,
        d.customer_id,
        d.order_date,
        d.total,
        d.status,
        SYSTEM_USER,
        GETDATE()
    FROM deleted d;

    -- Log the deletion
    INSERT INTO audit_log (table_name, record_id, action, action_date, performed_by)
    SELECT 'orders', d.order_id, 'DELETED', GETDATE(), SYSTEM_USER
    FROM deleted d;
END;

INSTEAD OF Triggers

DfINSTEAD OF Trigger

A trigger that replaces the original triggering statement with custom logic. The original INSERT, UPDATE, or DELETE is not executed β€” only the trigger body runs.

INSTEAD OF triggers are useful for views that are not directly updatable, or for intercepting and redirecting operations.

-- Make a view updatable using INSTEAD OF trigger
CREATE VIEW v_customer_orders AS
SELECT c.customer_id, c.name, c.email,
       o.order_id, o.order_date, o.total
FROM customers c
LEFT JOIN orders o ON c.customer_id = o.customer_id;

CREATE TRIGGER trg_UpdateCustomerOrders
ON v_customer_orders
INSTEAD OF UPDATE
AS
BEGIN
    -- Update customer info
    UPDATE customers
    SET name = i.name,
        email = i.email
    FROM customers c
    INNER JOIN inserted i ON c.customer_id = i.customer_id;

    -- Update order info
    UPDATE orders
    SET order_date = i.order_date,
        total = i.total
    FROM orders o
    INNER JOIN inserted i ON o.order_id = i.order_id
    WHERE i.order_id IS NOT NULL;
END;

Trigger Timing and Order

TimingWhen It FiresUse Case
AFTER INSERTAfter new rows are committedAudit logging, cascading updates
AFTER UPDATEAfter rows are modifiedChange tracking, validation
AFTER DELETEAfter rows are removedArchiving, cleanup
INSTEAD OFInstead of the original statementView updates, operation interception
-- Control trigger execution order
CREATE TRIGGER trg_AuditFirst
ON employees
AFTER UPDATE
AS
BEGIN
    INSERT INTO audit_log (message, log_time)
    VALUES ('Audit trigger fired', GETDATE());
END;

CREATE TRIGGER trg_NotifySecond
ON employees
AFTER UPDATE
AS
BEGIN
    INSERT INTO notification_log (message, log_time)
    VALUES ('Notification trigger fired', GETDATE());
END;

-- Set execution order (SQL Server)
EXEC sp_settriggerorder @triggername = 'trg_AuditFirst',
                         @order = 'first',
                         @stmttype = 'UPDATE';

EXEC sp_settriggerorder @triggername = 'trg_NotifySecond',
                         @order = 'last',
                         @stmttype = 'UPDATE';

Disabled and Recursive Triggers

Triggers can fire other triggers recursively. This can cause infinite loops. Use RECURSIVE_TRIGGERS setting carefully and test thoroughly before enabling recursive trigger behavior.

-- Disable a trigger
DISABLE TRIGGER trg_AuditEmployeeChanges ON employees;

-- Enable a trigger
ENABLE TRIGGER trg_AuditEmployeeChanges ON employees;

-- Disable all triggers on a table
DISABLE TRIGGER ALL ON employees;

-- Control recursive trigger behavior (SQL Server)
ALTER DATABASE YourDatabase SET RECURSIVE_TRIGGERS ON;

-- Recursive trigger example (use with caution)
CREATE TRIGGER trg_SetModifiedDate
ON employees
AFTER UPDATE
AS
BEGIN
    -- This fires the UPDATE, which fires the trigger again
    -- Only safe with proper recursion control
    UPDATE e
    SET modified_date = GETDATE()
    FROM employees e
    INNER JOIN inserted i ON e.employee_id = i.employee_id;
END;

Best Practices

PracticeReason
Keep triggers small and focusedComplex triggers are hard to debug and maintain
Use SET NOCOUNT ONPrevents extra result sets from interfering with applications
Avoid transactions in triggersTriggers run within the caller's transaction β€” nested transactions are fragile
Document trigger logicSilent execution makes triggers invisible without documentation
Test trigger interactionsMultiple triggers on the same table can conflict
Prefer application logic when possibleTriggers add hidden complexity to the database
-- Well-structured trigger with documentation
/*
    Trigger: trg_UpdateInventoryOnOrder
    Table: order_items
    Event: AFTER INSERT, UPDATE
    Purpose: Automatically decrements inventory stock when items are added to an order
    Author: DBA Team
    Created: 2024-01-15
*/
CREATE TRIGGER trg_UpdateInventoryOnOrder
ON order_items
AFTER INSERT, UPDATE
AS
BEGIN
    SET NOCOUNT ON;

    -- Decrease stock for inserted items
    UPDATE i
    SET i.stock = i.stock - ins.quantity
    FROM inventory i
    INNER JOIN inserted ins ON i.product_id = ins.product_id;

    -- Warn if stock went negative
    IF EXISTS (SELECT 1 FROM inventory WHERE stock < 0)
    BEGIN
        DECLARE @msg NVARCHAR(200) = 'Warning: inventory stock is negative for product(s)';
        RAISERROR(@msg, 10, 1); -- severity 10 = warning, does not rollback
    END
END;

Practical Example: Full Audit System

-- Create audit table
CREATE TABLE data_audit (
    audit_id INT IDENTITY(1,1) PRIMARY KEY,
    table_name NVARCHAR(128),
    record_id INT,
    action NVARCHAR(10),
    old_values NVARCHAR(MAX),
    new_values NVARCHAR(MAX),
    changed_by NVARCHAR(128),
    changed_date DATETIME2 DEFAULT GETDATE()
);

-- Generic audit trigger
CREATE TRIGGER trg_AuditEmployees
ON employees
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
    SET NOCOUNT ON;

    -- Log INSERTs
    INSERT INTO data_audit (table_name, record_id, action, new_values, changed_by)
    SELECT 'employees', employee_id, 'INSERT',
           (SELECT * FROM inserted i WHERE i.employee_id = ins.employee_id FOR JSON PATH),
           SYSTEM_USER
    FROM inserted ins;

    -- Log DELETEs
    INSERT INTO data_audit (table_name, record_id, action, old_values, changed_by)
    SELECT 'employees', employee_id, 'DELETE',
           (SELECT * FROM deleted d WHERE d.employee_id = del.employee_id FOR JSON PATH),
           SYSTEM_USER
    FROM deleted del;

    -- Log UPDATEs (only when values actually changed)
    INSERT INTO data_audit (table_name, record_id, action, old_values, new_values, changed_by)
    SELECT 'employees', i.employee_id, 'UPDATE',
           (SELECT * FROM deleted d WHERE d.employee_id = i.employee_id FOR JSON PATH),
           (SELECT * FROM inserted i2 WHERE i2.employee_id = i.employee_id FOR JSON PATH),
           SYSTEM_USER
    FROM inserted i
    INNER JOIN deleted d ON i.employee_id = d.employee_id
    WHERE i.name <> d.name
       OR i.salary <> d.salary
       OR i.department_id <> d.department_id;
END;

Key Takeaways

  1. Triggers automatically execute in response to INSERT, UPDATE, or DELETE events on tables
  2. AFTER triggers fire after the operation; INSTEAD OF triggers replace the operation entirely
  3. The inserted and deleted virtual tables provide access to affected rows
  4. Triggers execute within the caller's transaction β€” a ROLLBACK inside a trigger undoes the entire operation
  5. Keep triggers simple and well-documented β€” they execute silently and can be difficult to debug
  6. Use triggers for audit logging, validation, and cascading updates, but prefer application logic for complex business rules
⭐

Premium Content

Database Triggers

Unlock this lesson and 900+ advanced tutorials with a Premium plan.

🎯End-to-end Projects
πŸ’ΌInterview Prep
πŸ“œCertificates
🀝Community Access

Already a member? Log in

Need Expert SQL Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement