Skip to content

Triggers (BEFORE, AFTER, INSTEAD OF)

Difficulty: Advanced
Generated on: 2025-07-10 02:31:52
Category: SQL Cheatsheet for Database Development


What is it? A trigger is a special stored procedure that automatically executes in response to certain events on a table (INSERT, UPDATE, DELETE). It’s a powerful mechanism for enforcing business rules, auditing data changes, and maintaining data integrity.

When to use it?

  • Auditing: Logging data changes for compliance or debugging.
  • Data Validation: Enforcing complex constraints beyond standard SQL constraints.
  • Data Transformation: Modifying data before or after it’s written to the database.
  • Business Rule Enforcement: Implementing complex business logic that involves multiple tables.
  • Replication: Automatically replicating data to other tables or systems.
-- General Syntax (ANSI SQL - Variations exist across DBMS)
CREATE TRIGGER trigger_name
{BEFORE | AFTER | INSTEAD OF} {INSERT | UPDATE | DELETE}
ON table_name
[FOR EACH {ROW | STATEMENT}] -- Row-level or Statement-level (varies across DBMS)
BEGIN
-- SQL statements to execute
END;

Specific examples:

MySQL:

CREATE TRIGGER before_insert_products
BEFORE INSERT
ON products
FOR EACH ROW
BEGIN
-- NEW refers to the new row being inserted
IF NEW.price < 0 THEN
SET NEW.price = 0; -- Prevent negative prices
END IF;
END;

PostgreSQL:

CREATE OR REPLACE FUNCTION check_order_quantity()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.quantity > 100 THEN
RAISE EXCEPTION 'Quantity exceeds allowed limit.';
END IF;
RETURN NEW; -- Important: Return NEW for BEFORE triggers, OLD for AFTER triggers.
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER before_insert_orders
BEFORE INSERT
ON orders
FOR EACH ROW
EXECUTE FUNCTION check_order_quantity();

SQL Server:

CREATE TRIGGER after_update_products
ON products
AFTER UPDATE
AS
BEGIN
-- Insert audit log entry
INSERT INTO product_audit (product_id, old_price, new_price, updated_at)
SELECT i.product_id, d.price, i.price, GETDATE()
FROM inserted i -- New values
INNER JOIN deleted d ON i.product_id = d.product_id; -- Old values
END;

Oracle:

CREATE OR REPLACE TRIGGER before_insert_employees
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary < 0 THEN
:NEW.salary := 0;
END IF;
END;
/

Key Syntax Components:

  • CREATE TRIGGER trigger_name: Creates a new trigger with a unique name.
  • BEFORE | AFTER | INSTEAD OF:
    • BEFORE: Trigger executes before the triggering event occurs. Useful for modifying data before it’s written or preventing the event.
    • AFTER: Trigger executes after the triggering event occurs. Useful for auditing, replicating data, or performing actions based on the final state of the data.
    • INSTEAD OF: Trigger executes instead of the triggering event. Useful for modifying data in views that are not directly updatable, or for implementing custom update logic.
  • INSERT | UPDATE | DELETE: Specifies the triggering event.
  • ON table_name: Specifies the table the trigger is associated with.
  • FOR EACH {ROW | STATEMENT}:
    • ROW: Trigger executes once for each row affected by the triggering statement. (Row-level trigger)
    • STATEMENT: Trigger executes once for the entire triggering statement, regardless of the number of rows affected. (Statement-level trigger) Note: Not supported by all database systems.
  • BEGIN ... END: Encloses the SQL statements to be executed when the trigger is fired.
  • NEW: Refers to the new row being inserted/updated (available in BEFORE INSERT, BEFORE UPDATE, AFTER INSERT, AFTER UPDATE triggers).
  • OLD: Refers to the old row being updated/deleted (available in BEFORE UPDATE, BEFORE DELETE, AFTER UPDATE, AFTER DELETE triggers).
  • :NEW and :OLD (Oracle syntax) : Refers to the new and old values respectively.
  • Auditing Changes: Track who changed what data and when.
  • Enforcing Complex Business Rules: Example: Preventing orders from being shipped if the customer’s credit limit is exceeded.
  • Data Validation: Example: Ensuring that the email address format is valid before inserting a new user.
  • Maintaining Derived Data: Example: Automatically updating a total_sales column in a customers table when a new order is placed.
  • Implementing Cascade Deletes/Updates: Example: Deleting related records in other tables when a record in a parent table is deleted. (Although often better handled with foreign key constraints).
  • Implementing Custom Logic for Views: Using INSTEAD OF triggers to make views updatable when they are based on multiple tables or complex queries.
  • Security: Logging failed login attempts or unauthorized access attempts.
  • Keep Triggers Simple: Complex logic is harder to debug and maintain. Consider moving complex logic to stored procedures or application code.
  • Avoid Recursive Triggers: A trigger firing itself (directly or indirectly) can lead to infinite loops and performance problems. Implement logic to prevent recursion.
  • Test Thoroughly: Triggers can have unexpected side effects. Write unit tests to ensure they behave as expected.
  • Use Triggers Sparingly: Triggers can impact performance, especially on high-volume tables. Consider alternative approaches such as application-level validation or scheduled batch jobs.
  • Document Triggers: Clearly document the purpose and behavior of each trigger.
  • Consider Performance Implications: Triggers add overhead to database operations. Optimize trigger logic to minimize performance impact.
  • Use SET NOCOUNT ON (SQL Server): In SQL Server, use SET NOCOUNT ON at the beginning of the trigger to prevent the (x row(s) affected) message from being returned to the client for each statement within the trigger. This can improve performance.
  • Avoid DML Statements on the Same Table: While sometimes unavoidable, performing INSERT/UPDATE/DELETE operations on the same table within a trigger can lead to locking issues and performance degradation. Consider alternative designs if possible.
  • Error Handling: Implement robust error handling within the trigger to prevent unexpected failures. Use TRY...CATCH blocks (SQL Server) or exception handling in PL/pgSQL (PostgreSQL) or PL/SQL (Oracle).
  • Transaction Management: Triggers execute within the same transaction as the triggering statement. Be mindful of transaction boundaries and potential rollback scenarios.
  • Security Considerations: Be aware of the security context in which the trigger executes. Ensure that the trigger has the necessary permissions to access and modify data. Avoid using dynamic SQL within triggers, as it can introduce SQL injection vulnerabilities.

Example 1: Auditing Product Price Changes (SQL Server)

-- Create audit table
CREATE TABLE product_audit (
audit_id INT IDENTITY(1,1) PRIMARY KEY,
product_id INT,
old_price DECIMAL(10, 2),
new_price DECIMAL(10, 2),
updated_at DATETIME
);
-- Create trigger to audit price changes
CREATE TRIGGER tr_product_price_update
ON products
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON; -- Suppress row count messages
-- Check if the price has actually changed
IF UPDATE(price)
BEGIN
INSERT INTO product_audit (product_id, old_price, new_price, updated_at)
SELECT i.product_id, d.price, i.price, GETDATE()
FROM inserted i
INNER JOIN deleted d ON i.product_id = d.product_id
WHERE i.price <> d.price; -- Only log actual price changes
END;
END;
-- Sample data and update
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(255),
price DECIMAL(10, 2)
);
INSERT INTO products (product_id, product_name, price) VALUES (1, 'Widget', 19.99);
UPDATE products SET price = 24.99 WHERE product_id = 1;
-- Check the audit table
SELECT * FROM product_audit;

Example 2: Preventing Inventory from Going Negative (MySQL)

CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(255),
inventory INT
);
INSERT INTO products (product_id, product_name, inventory) VALUES (1, 'Gadget', 10);
CREATE TRIGGER before_update_products
BEFORE UPDATE
ON products
FOR EACH ROW
BEGIN
IF NEW.inventory < 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Inventory cannot be negative.';
END IF;
END;
-- Attempt to update inventory to a negative value
-- This will raise an error
UPDATE products SET inventory = -5 WHERE product_id = 1;
-- Correct update
UPDATE products SET inventory = 5 WHERE product_id = 1;
SELECT * FROM products;

Example 3: Maintaining a Last Updated Timestamp (PostgreSQL)

CREATE TABLE employees (
employee_id SERIAL PRIMARY KEY,
employee_name VARCHAR(255),
salary DECIMAL(10, 2),
last_updated TIMESTAMP WITHOUT TIME ZONE
);
CREATE OR REPLACE FUNCTION update_employee_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.last_updated := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER before_update_employees
BEFORE UPDATE ON employees
FOR EACH ROW
EXECUTE FUNCTION update_employee_timestamp();
CREATE TRIGGER before_insert_employees
BEFORE INSERT ON employees
FOR EACH ROW
EXECUTE FUNCTION update_employee_timestamp();
-- Insert sample data
INSERT INTO employees (employee_name, salary) VALUES ('Alice', 50000);
-- Update employee salary
UPDATE employees SET salary = 55000 WHERE employee_id = 1;
-- Select employee data
SELECT * FROM employees;

Example 4: Implementing an INSTEAD OF trigger on a View (SQL Server)

-- Create tables
CREATE TABLE authors (
author_id INT PRIMARY KEY,
author_name VARCHAR(255)
);
CREATE TABLE books (
book_id INT PRIMARY KEY,
book_title VARCHAR(255),
author_id INT,
FOREIGN KEY (author_id) REFERENCES authors(author_id)
);
-- Create a view joining authors and books
CREATE VIEW author_book_view AS
SELECT a.author_id, a.author_name, b.book_id, b.book_title
FROM authors a
JOIN books b ON a.author_id = b.author_id;
-- Create an INSTEAD OF trigger to handle updates on the view
CREATE TRIGGER instead_of_update_author_book_view
ON author_book_view
INSTEAD OF UPDATE
AS
BEGIN
SET NOCOUNT ON;
-- Update the author name if it's changed
IF UPDATE(author_name)
BEGIN
UPDATE authors
SET author_name = i.author_name
FROM inserted i
WHERE authors.author_id = i.author_id;
END;
-- Update the book title if it's changed
IF UPDATE(book_title)
BEGIN
UPDATE books
SET book_title = i.book_title
FROM inserted i
WHERE books.book_id = i.book_id;
END;
END;
-- Sample data
INSERT INTO authors (author_id, author_name) VALUES (1, 'John Doe');
INSERT INTO books (book_id, book_title, author_id) VALUES (101, 'The Adventure', 1);
-- Update the view
UPDATE author_book_view
SET author_name = 'Jane Doe', book_title = 'The Great Adventure'
WHERE author_id = 1 AND book_id = 101;
-- Check the tables
SELECT * FROM authors;
SELECT * FROM books;
  • Infinite Loops: The most common pitfall. Ensure your trigger logic doesn’t inadvertently trigger itself. Use conditional logic or set flags to prevent recursion.
  • Performance Bottlenecks: Triggers execute synchronously with the triggering statement. Slow triggers can significantly degrade performance. Profile trigger execution and optimize as needed.
  • Unexpected Side Effects: Triggers can modify data in unexpected ways. Thorough testing is crucial.
  • Complex Logic: Avoid overly complex logic in triggers. Move complex logic to stored procedures or application code.
  • Locking Issues: Triggers can acquire locks on tables, potentially leading to deadlocks or blocking. Keep transactions short and avoid long-running operations within triggers.
  • Data Integrity Issues: If a trigger fails, the entire transaction may be rolled back, potentially leaving the database in an inconsistent state. Implement robust error handling.
  • Incorrect Use of OLD and NEW: Ensure you are using OLD and NEW correctly to access the old and new values of the affected rows. Understand which variables are available in BEFORE and AFTER triggers.
  • Not considering NULL values: When comparing values in triggers (especially using OLD and NEW), be sure to account for NULL values. Use IS NULL or IS NOT NULL for NULL comparisons, or use functions like COALESCE or ISNULL to handle NULLs.
  • Forgetting to RETURN NEW in BEFORE triggers (PostgreSQL): A BEFORE trigger must return NEW to pass the modified row to the next trigger or the INSERT/UPDATE statement, or NULL to prevent the INSERT/UPDATE.

Troubleshooting Tips:

  • Check Error Logs: Examine the database error logs for any errors or warnings related to trigger execution.
  • Use Profiling Tools: Use database profiling tools to identify slow-running triggers and pinpoint performance bottlenecks.
  • Disable Triggers Temporarily: Temporarily disable triggers to isolate whether they are causing a problem. (e.g., ALTER TABLE table_name DISABLE TRIGGER trigger_name; in SQL Server).
  • Review Trigger Logic: Carefully review the trigger logic to identify potential errors or inefficiencies.
  • Test with Sample Data: Test the trigger with a variety of sample data to ensure it behaves as expected in different scenarios.
FeatureMySQLPostgreSQLSQL ServerOracle
Trigger TimingBEFORE, AFTERBEFORE, AFTER, INSTEAD OFAFTER, INSTEAD OF, FOR REPLICATIONBEFORE, AFTER, INSTEAD OF
Row/StatementFOR EACH ROWFOR EACH ROWImplicitly row-levelFOR EACH ROW
OLD/NEW AccessOLD.column_name, NEW.column_nameOLD.column_name, NEW.column_nameinserted, deleted virtual tables:OLD.column_name, :NEW.column_name
TransactionalYesYesYesYes
Error HandlingSIGNAL SQLSTATERAISE EXCEPTIONTRY...CATCHException Handling Blocks
Trigger ManagementSHOW TRIGGERS, DROP TRIGGERDROP TRIGGER, ALTER TRIGGERALTER TABLE DISABLE TRIGGER, DROP TRIGGERDROP TRIGGER
Multiple TriggersOrder undefined on same event.Order can be controlled using constraints with trigger functionsOrder undefined on same event.Order can be specified using FOLLOWS clause.

Key Differences:

  • INSTEAD OF Triggers: Supported by PostgreSQL, SQL Server, and Oracle, but not directly in MySQL (can be emulated with views and stored procedures).
  • Accessing Old and New Values: MySQL and PostgreSQL use OLD and NEW prefixes. SQL Server uses the virtual tables inserted and deleted. Oracle uses colon prefixes :OLD and :NEW.
  • Error Handling: Each database system has its own error-handling mechanisms.
  • Multiple Triggers of the Same Type: Most database systems do not guarantee the order in which multiple triggers of the same type will be executed. SQL Server and MySQL do not. Postgres can control the order via constraints and trigger functions. Oracle allows specifying execution order using the FOLLOWS clause when creating the trigger.

This cheatsheet provides a comprehensive overview of SQL triggers, covering syntax, use cases, best practices, and common pitfalls. Remember to adapt the examples and guidelines to your specific database system and application requirements. Always prioritize thorough testing and documentation to ensure the reliability and maintainability of your triggers.