What are database triggers and how do you implement them?

Answer

Database triggers are special stored procedures that automatically execute (or “fire”) in response to specific database events such as INSERT, UPDATE, DELETE, or DDL operations. They run implicitly without being explicitly called.

Types of Triggers

By Timing

  • BEFORE: Executes before the triggering event
  • AFTER: Executes after the triggering event
  • INSTEAD OF: Replaces the triggering event (views only)

By Event

  • INSERT: Fires when new records are added
  • UPDATE: Fires when existing records are modified
  • DELETE: Fires when records are removed
  • DDL: Fires on schema changes (CREATE, ALTER, DROP)

Basic Trigger Syntax

MySQL Syntax

CREATE TRIGGER trigger_name
    {BEFORE | AFTER} {INSERT | UPDATE | DELETE}
    ON table_name
    FOR EACH ROW
BEGIN
    -- Trigger logic
END;

SQL Server Syntax

CREATE TRIGGER trigger_name
    ON table_name
    {FOR | AFTER | INSTEAD OF} {INSERT, UPDATE, DELETE}
AS
BEGIN
    -- Trigger logic
END;

Common Trigger Examples

1. Audit Trail Trigger

-- Create audit table
CREATE TABLE audit_log (
    audit_id INT AUTO_INCREMENT PRIMARY KEY,
    table_name VARCHAR(50),
    operation VARCHAR(10),
    record_id INT,
    old_values JSON,
    new_values JSON,
    changed_by VARCHAR(100),
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Audit trigger for employee updates
DELIMITER //
CREATE TRIGGER employee_audit_trigger
    AFTER UPDATE ON employees
    FOR EACH ROW
BEGIN
    INSERT INTO audit_log (
        table_name, operation, record_id, 
        old_values, new_values, changed_by
    ) VALUES (
        'employees', 'UPDATE', NEW.employee_id,
        JSON_OBJECT(
            'name', OLD.name,
            'salary', OLD.salary,
            'department', OLD.department
        ),
        JSON_OBJECT(
            'name', NEW.name,
            'salary', NEW.salary,
            'department', NEW.department
        ),
        USER()
    );
END //
DELIMITER ;

2. Automatic Timestamp Updates

-- Update modified_at timestamp automatically
DELIMITER //
CREATE TRIGGER update_timestamp_trigger
    BEFORE UPDATE ON products
    FOR EACH ROW
BEGIN
    SET NEW.modified_at = CURRENT_TIMESTAMP;
END //
DELIMITER ;

-- Usage example
UPDATE products SET price = 29.99 WHERE product_id = 1;
-- modified_at is automatically updated

3. Data Validation Trigger

-- Validate business rules before insert/update
DELIMITER //
CREATE TRIGGER validate_employee_trigger
    BEFORE INSERT ON employees
    FOR EACH ROW
BEGIN
    -- Validate salary range
    IF NEW.salary < 0 OR NEW.salary > 1000000 THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Salary must be between 0 and 1,000,000';
    END IF;
    
    -- Validate email format
    IF NEW.email NOT REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$' THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Invalid email format';
    END IF;
    
    -- Validate hire date
    IF NEW.hire_date > CURRENT_DATE THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Hire date cannot be in the future';
    END IF;
END //
DELIMITER ;

4. Inventory Management Trigger

-- Automatically update inventory when orders are placed
DELIMITER //
CREATE TRIGGER update_inventory_trigger
    AFTER INSERT ON order_items
    FOR EACH ROW
BEGIN
    DECLARE current_stock INT;
    
    -- Get current stock
    SELECT quantity INTO current_stock
    FROM inventory
    WHERE product_id = NEW.product_id;
    
    -- Check if sufficient stock
    IF current_stock < NEW.quantity THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Insufficient inventory';
    END IF;
    
    -- Update inventory
    UPDATE inventory 
    SET quantity = quantity - NEW.quantity,
        last_updated = CURRENT_TIMESTAMP
    WHERE product_id = NEW.product_id;
    
    -- Log inventory change
    INSERT INTO inventory_log (
        product_id, change_type, quantity_change, 
        reason, changed_at
    ) VALUES (
        NEW.product_id, 'DECREASE', -NEW.quantity,
        CONCAT('Order #', NEW.order_id), CURRENT_TIMESTAMP
    );
END //
DELIMITER ;

Advanced Trigger Patterns

5. Cascading Updates with Triggers

-- Update related tables when customer information changes
DELIMITER //
CREATE TRIGGER customer_update_cascade
    AFTER UPDATE ON customers
    FOR EACH ROW
BEGIN
    -- Update customer name in orders table if changed
    IF OLD.customer_name != NEW.customer_name THEN
        UPDATE orders 
        SET customer_name = NEW.customer_name
        WHERE customer_id = NEW.customer_id;
    END IF;
    
    -- Update customer status in related tables
    IF OLD.status != NEW.status THEN
        UPDATE subscriptions 
        SET customer_status = NEW.status
        WHERE customer_id = NEW.customer_id;
        
        -- Log status change
        INSERT INTO customer_status_log (
            customer_id, old_status, new_status, changed_at
        ) VALUES (
            NEW.customer_id, OLD.status, NEW.status, CURRENT_TIMESTAMP
        );
    END IF;
END //
DELIMITER ;

6. Complex Business Logic Trigger

-- Calculate and update customer tier based on purchase history
DELIMITER //
CREATE TRIGGER update_customer_tier
    AFTER INSERT ON orders
    FOR EACH ROW
BEGIN
    DECLARE total_spent DECIMAL(12,2);
    DECLARE order_count INT;
    DECLARE new_tier VARCHAR(20);
    
    -- Calculate customer metrics
    SELECT 
        COUNT(*), 
        COALESCE(SUM(order_total), 0)
    INTO order_count, total_spent
    FROM orders
    WHERE customer_id = NEW.customer_id;
    
    -- Determine new tier
    CASE 
        WHEN total_spent >= 10000 AND order_count >= 20 THEN 
            SET new_tier = 'PLATINUM';
        WHEN total_spent >= 5000 AND order_count >= 10 THEN 
            SET new_tier = 'GOLD';
        WHEN total_spent >= 1000 AND order_count >= 5 THEN 
            SET new_tier = 'SILVER';
        ELSE 
            SET new_tier = 'BRONZE';
    END CASE;
    
    -- Update customer tier if changed
    UPDATE customers 
    SET tier = new_tier, tier_updated_at = CURRENT_TIMESTAMP
    WHERE customer_id = NEW.customer_id 
    AND (tier != new_tier OR tier IS NULL);
    
END //
DELIMITER ;

SQL Server Specific Features

7. INSTEAD OF Triggers (Views)

-- Create a view combining multiple tables
CREATE VIEW employee_details AS
SELECT 
    e.employee_id,
    e.name,
    e.email,
    d.department_name,
    p.position_title
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN positions p ON e.position_id = p.position_id;

-- INSTEAD OF trigger to handle updates through the view
CREATE TRIGGER update_employee_details
    INSTEAD OF UPDATE ON employee_details
AS
BEGIN
    -- Update employees table
    UPDATE employees 
    SET name = i.name, email = i.email
    FROM employees e
    JOIN inserted i ON e.employee_id = i.employee_id;
    
    -- Update department if changed
    UPDATE employees 
    SET department_id = d.department_id
    FROM employees e
    JOIN inserted i ON e.employee_id = i.employee_id
    JOIN departments d ON i.department_name = d.department_name
    WHERE e.department_id != d.department_id;
END;

8. DDL Triggers

-- Audit schema changes
CREATE TRIGGER audit_ddl_changes
    ON DATABASE
    FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE
AS
BEGIN
    DECLARE @event_data XML = EVENTDATA();
    
    INSERT INTO ddl_audit_log (
        event_type,
        object_name,
        object_type,
        sql_command,
        login_name,
        event_time
    ) VALUES (
        @event_data.value('(/EVENT_INSTANCE/EventType)[1]', 'VARCHAR(50)'),
        @event_data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'VARCHAR(100)'),
        @event_data.value('(/EVENT_INSTANCE/ObjectType)[1]', 'VARCHAR(50)'),
        @event_data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'NVARCHAR(MAX)'),
        @event_data.value('(/EVENT_INSTANCE/LoginName)[1]', 'VARCHAR(100)'),
        GETDATE()
    );
END;

Trigger Performance Considerations

9. Efficient Trigger Design

-- Inefficient: Processes all rows individually
DELIMITER //
CREATE TRIGGER inefficient_trigger
    AFTER UPDATE ON orders
    FOR EACH ROW
BEGIN
    -- This executes for every updated row
    UPDATE customer_stats 
    SET total_orders = (
        SELECT COUNT(*) FROM orders WHERE customer_id = NEW.customer_id
    )
    WHERE customer_id = NEW.customer_id;
END //
DELIMITER ;

-- Efficient: Batch processing (SQL Server style)
CREATE TRIGGER efficient_trigger
    ON orders
    AFTER UPDATE
AS
BEGIN
    -- Process all affected customers in one operation
    UPDATE cs
    SET total_orders = o.order_count,
        total_spent = o.total_amount
    FROM customer_stats cs
    JOIN (
        SELECT 
            customer_id,
            COUNT(*) as order_count,
            SUM(order_total) as total_amount
        FROM orders
        WHERE customer_id IN (SELECT DISTINCT customer_id FROM inserted)
        GROUP BY customer_id
    ) o ON cs.customer_id = o.customer_id;
END;

10. Conditional Trigger Execution

-- Only execute trigger logic when specific columns change
DELIMITER //
CREATE TRIGGER conditional_update_trigger
    BEFORE UPDATE ON products
    FOR EACH ROW
BEGIN
    -- Only process if price or category changed
    IF OLD.price != NEW.price OR OLD.category_id != NEW.category_id THEN
        
        -- Update price history
        IF OLD.price != NEW.price THEN
            INSERT INTO price_history (
                product_id, old_price, new_price, changed_at
            ) VALUES (
                NEW.product_id, OLD.price, NEW.price, CURRENT_TIMESTAMP
            );
        END IF;
        
        -- Update category statistics
        IF OLD.category_id != NEW.category_id THEN
            -- Decrease count in old category
            UPDATE category_stats 
            SET product_count = product_count - 1
            WHERE category_id = OLD.category_id;
            
            -- Increase count in new category
            UPDATE category_stats 
            SET product_count = product_count + 1
            WHERE category_id = NEW.category_id;
        END IF;
        
    END IF;
END //
DELIMITER ;

Error Handling in Triggers

11. Robust Error Handling

DELIMITER //
CREATE TRIGGER safe_order_trigger
    BEFORE INSERT ON orders
    FOR EACH ROW
BEGIN
    DECLARE customer_exists INT DEFAULT 0;
    DECLARE customer_active INT DEFAULT 0;
    
    -- Declare error handler
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        -- Log error
        INSERT INTO trigger_errors (
            trigger_name, error_message, error_time
        ) VALUES (
            'safe_order_trigger', 
            'Error processing order insert', 
            CURRENT_TIMESTAMP
        );
        
        -- Re-raise the error
        RESIGNAL;
    END;
    
    -- Validate customer exists and is active
    SELECT COUNT(*), SUM(CASE WHEN status = 'ACTIVE' THEN 1 ELSE 0 END)
    INTO customer_exists, customer_active
    FROM customers
    WHERE customer_id = NEW.customer_id;
    
    IF customer_exists = 0 THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Customer does not exist';
    END IF;
    
    IF customer_active = 0 THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Customer account is not active';
    END IF;
    
    -- Set default values
    IF NEW.order_date IS NULL THEN
        SET NEW.order_date = CURRENT_DATE;
    END IF;
    
    IF NEW.status IS NULL THEN
        SET NEW.status = 'PENDING';
    END IF;
    
END //
DELIMITER ;

Trigger Management

Viewing Triggers

-- MySQL: Show triggers
SHOW TRIGGERS;
SHOW TRIGGERS FROM database_name;
SHOW TRIGGERS LIKE 'pattern%';

-- Get trigger definition
SHOW CREATE TRIGGER trigger_name;

-- SQL Server: Query system views
SELECT 
    t.name as trigger_name,
    t.type_desc,
    t.is_disabled,
    o.name as table_name
FROM sys.triggers t
JOIN sys.objects o ON t.parent_id = o.object_id;

Disabling/Enabling Triggers

-- MySQL: Drop and recreate (no disable option)
DROP TRIGGER IF EXISTS trigger_name;

-- SQL Server: Disable/Enable
ALTER TABLE table_name DISABLE TRIGGER trigger_name;
ALTER TABLE table_name ENABLE TRIGGER trigger_name;

-- Disable all triggers on a table
ALTER TABLE table_name DISABLE TRIGGER ALL;

Best Practices

1. Keep Triggers Simple and Fast

  • Minimize complex logic in triggers
  • Avoid long-running operations
  • Use triggers for simple, atomic operations

2. Avoid Recursive Triggers

-- Prevent infinite recursion
DELIMITER //
CREATE TRIGGER prevent_recursion
    BEFORE UPDATE ON employees
    FOR EACH ROW
BEGIN
    -- Check if trigger is already running
    IF @trigger_running IS NULL THEN
        SET @trigger_running = 1;
        
        -- Trigger logic here
        
        SET @trigger_running = NULL;
    END IF;
END //
DELIMITER ;

3. Document Trigger Dependencies

-- Document what the trigger does and its dependencies
/*
Trigger: update_order_totals
Purpose: Automatically calculate order totals when items are added/updated
Dependencies: 
- order_items table
- products table (for pricing)
- tax_rates table (for tax calculation)
Tables affected:
- orders (order_total, tax_amount)
*/

Interview Tips

  • Understand the different types of triggers and when to use each
  • Know the performance implications of triggers
  • Be familiar with trigger syntax for different database systems
  • Understand how triggers fit into transaction processing
  • Know best practices for trigger design and error handling
  • Be able to explain alternatives to triggers (stored procedures, application logic)

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.