How to Use Triggers in MySQL

• Triggers execute automatically in response to INSERT, UPDATE, or DELETE operations, making them ideal for audit logging, data validation, and maintaining data consistency without application-level...

Key Insights

• Triggers execute automatically in response to INSERT, UPDATE, or DELETE operations, making them ideal for audit logging, data validation, and maintaining data consistency without application-level code.

• MySQL provides six trigger types (BEFORE/AFTER combined with INSERT/UPDATE/DELETE) that access row data through OLD and NEW keywords, enabling precise control over data modifications at the database level.

• While powerful, triggers should be used judiciously—they add hidden complexity, can impact performance, and make debugging harder compared to explicit application logic.

Introduction to MySQL Triggers

MySQL triggers are database objects that automatically execute in response to specific events on a table. Think of them as event listeners at the database level—when someone inserts, updates, or deletes a row, your trigger code runs automatically without any application intervention.

Triggers excel at three core scenarios: maintaining audit trails, enforcing complex business rules that transcend simple constraints, and keeping denormalized data synchronized. They’re particularly valuable in environments where multiple applications access the same database, since the logic lives in one place rather than being duplicated across codebases.

Here’s the fundamental concept illustrated with a simple example:

-- BEFORE trigger: runs before the row is inserted
CREATE TRIGGER validate_email_before_insert
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
    IF NEW.email NOT LIKE '%@%' THEN
        SIGNAL SQLSTATE '45000' 
        SET MESSAGE_TEXT = 'Invalid email format';
    END IF;
END;

-- AFTER trigger: runs after the row is inserted
CREATE TRIGGER log_user_creation
AFTER INSERT ON users
FOR EACH ROW
BEGIN
    INSERT INTO audit_log (action, user_id, timestamp)
    VALUES ('USER_CREATED', NEW.id, NOW());
END;

The BEFORE trigger validates data and can prevent the operation from completing. The AFTER trigger records what happened after the fact.

Trigger Syntax and Types

The CREATE TRIGGER statement follows this structure:

CREATE TRIGGER trigger_name
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON table_name
FOR EACH ROW
BEGIN
    -- trigger body (one or more SQL statements)
END;

MySQL supports six trigger types based on timing (BEFORE/AFTER) and event (INSERT/UPDATE/DELETE):

BEFORE INSERT - Validate or modify data before insertion:

CREATE TRIGGER normalize_username_before_insert
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
    SET NEW.username = LOWER(TRIM(NEW.username));
    SET NEW.created_at = NOW();
END;

AFTER UPDATE - Log changes after they’re committed:

CREATE TRIGGER track_price_changes
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
    IF OLD.price != NEW.price THEN
        INSERT INTO price_history (product_id, old_price, new_price, changed_at)
        VALUES (NEW.id, OLD.price, NEW.price, NOW());
    END IF;
END;

BEFORE DELETE - Prevent deletion under certain conditions:

CREATE TRIGGER prevent_admin_deletion
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
    IF OLD.role = 'admin' AND (SELECT COUNT(*) FROM users WHERE role = 'admin') <= 1 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Cannot delete the last admin user';
    END IF;
END;

Practical Use Cases with Examples

Audit Trail Implementation

Audit logging is the killer app for triggers. Instead of remembering to log every change in your application code, the database handles it automatically:

-- Create audit table
CREATE TABLE order_audit (
    audit_id INT AUTO_INCREMENT PRIMARY KEY,
    order_id INT,
    action VARCHAR(10),
    old_status VARCHAR(50),
    new_status VARCHAR(50),
    old_total DECIMAL(10,2),
    new_total DECIMAL(10,2),
    changed_by VARCHAR(100),
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Track all order changes
CREATE TRIGGER audit_order_changes
AFTER UPDATE ON orders
FOR EACH ROW
BEGIN
    INSERT INTO order_audit (
        order_id, action, old_status, new_status, 
        old_total, new_total, changed_by
    )
    VALUES (
        NEW.id, 'UPDATE', OLD.status, NEW.status,
        OLD.total, NEW.total, USER()
    );
END;

Data Validation Beyond Constraints

Sometimes CHECK constraints aren’t enough. Triggers can enforce complex business rules:

CREATE TRIGGER validate_order_items
BEFORE INSERT ON order_items
FOR EACH ROW
BEGIN
    DECLARE product_stock INT;
    
    SELECT stock_quantity INTO product_stock
    FROM products
    WHERE id = NEW.product_id;
    
    IF NEW.quantity > product_stock THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Insufficient stock for this order';
    END IF;
    
    IF NEW.quantity <= 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Quantity must be positive';
    END IF;
END;

Automatic Timestamp Management

Keep your updated_at columns accurate without application involvement:

CREATE TRIGGER set_updated_timestamp
BEFORE UPDATE ON articles
FOR EACH ROW
BEGIN
    SET NEW.updated_at = NOW();
END;

Maintaining Denormalized Data

When you denormalize for performance, triggers keep aggregates synchronized:

CREATE TRIGGER update_order_total
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
    UPDATE orders
    SET total = (
        SELECT SUM(quantity * unit_price)
        FROM order_items
        WHERE order_id = NEW.order_id
    )
    WHERE id = NEW.order_id;
END;

Working with OLD and NEW Keywords

The OLD and NEW keywords are your window into the data being modified. Their availability depends on the trigger type:

  • INSERT triggers: Only NEW (there’s no previous state)
  • UPDATE triggers: Both OLD and NEW
  • DELETE triggers: Only OLD (there’s no new state)

Here’s a sophisticated example that logs only specific field changes:

CREATE TRIGGER log_significant_changes
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    IF OLD.salary != NEW.salary THEN
        INSERT INTO employee_changes (
            employee_id, field_changed, old_value, 
            new_value, change_date
        )
        VALUES (
            NEW.id, 'salary', OLD.salary, 
            NEW.salary, NOW()
        );
    END IF;
    
    IF OLD.department_id != NEW.department_id THEN
        INSERT INTO employee_changes (
            employee_id, field_changed, old_value, 
            new_value, change_date
        )
        VALUES (
            NEW.id, 'department_id', OLD.department_id,
            NEW.department_id, NOW()
        );
    END IF;
END;

You can also use OLD and NEW to calculate derived values:

CREATE TRIGGER calculate_discount
BEFORE INSERT ON order_items
FOR EACH ROW
BEGIN
    SET NEW.discount_amount = NEW.unit_price * NEW.discount_percent / 100;
    SET NEW.final_price = NEW.unit_price - NEW.discount_amount;
END;

Managing and Debugging Triggers

Viewing Existing Triggers

-- Show all triggers in current database
SHOW TRIGGERS;

-- Show triggers for specific table
SHOW TRIGGERS WHERE `Table` = 'users';

-- Get detailed trigger definition
SHOW CREATE TRIGGER trigger_name;

-- Query information_schema for more control
SELECT * FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'your_database'
AND EVENT_OBJECT_TABLE = 'users';

Removing Triggers

DROP TRIGGER IF EXISTS trigger_name;

Common Pitfalls and Solutions

The mutating table problem occurs when a trigger tries to read from or write to the same table it’s triggered on:

-- PROBLEMATIC: This can cause issues
CREATE TRIGGER bad_trigger
AFTER INSERT ON products
FOR EACH ROW
BEGIN
    UPDATE products SET rank = rank + 1; -- Reading and writing same table
END;

-- BETTER: Use a separate summary table
CREATE TRIGGER good_trigger
AFTER INSERT ON products
FOR EACH ROW
BEGIN
    UPDATE product_stats SET total_count = total_count + 1;
END;

Trigger chains (triggers firing other triggers) can create performance nightmares and debugging hell. Keep trigger logic simple and avoid cascading effects when possible.

Best Practices and Limitations

When to Use Triggers vs. Application Logic

Use triggers for:

  • Cross-cutting concerns like audit logging
  • Data integrity rules that must never be violated
  • Maintaining derived/denormalized data
  • Database-level constraints too complex for CHECK constraints

Avoid triggers for:

  • Complex business logic better suited to application code
  • Operations requiring external API calls
  • Logic that needs frequent changes (triggers require DDL operations)
  • Anything that makes debugging harder than it needs to be

Performance Considerations

Triggers add overhead to every affected operation. A trigger on a high-traffic table can become a bottleneck. Profile your queries and consider whether the trigger logic could be batched or moved to scheduled jobs.

Refactoring Complex Triggers

When trigger logic grows complex, extract it into stored procedures:

-- Create reusable stored procedure
DELIMITER //
CREATE PROCEDURE update_inventory(
    IN p_product_id INT,
    IN p_quantity INT
)
BEGIN
    UPDATE products
    SET stock_quantity = stock_quantity - p_quantity
    WHERE id = p_product_id;
    
    IF (SELECT stock_quantity FROM products WHERE id = p_product_id) < 10 THEN
        INSERT INTO low_stock_alerts (product_id, alert_date)
        VALUES (p_product_id, NOW());
    END IF;
END //
DELIMITER ;

-- Simple trigger calls the procedure
CREATE TRIGGER update_stock_after_order
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
    CALL update_inventory(NEW.product_id, NEW.quantity);
END;

This approach improves testability and reusability while keeping the trigger simple.

MySQL Version Limitations

MySQL 5.7.2+ allows multiple triggers per timing/event combination. Earlier versions restrict you to one trigger per timing/event, forcing you to consolidate logic into single triggers.

Triggers are powerful tools that enforce consistency at the database level. Use them wisely for audit trails, data validation, and maintaining referential integrity. But remember: with great power comes great debugging sessions. Keep triggers simple, document them thoroughly, and always question whether application logic might be clearer.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.