MySQL & Database Design

Functions & Triggers

13 min Lesson 22 of 40

Functions & Triggers

User-defined functions and triggers are powerful database objects that automate operations and enforce business rules. Functions return single values and can be used in SQL expressions, while triggers automatically execute in response to data changes. In this lesson, we'll master both concepts.

User-Defined Functions (UDF)

Functions are similar to stored procedures but return a single value and can be used in SELECT statements and expressions.

Key Difference: Functions MUST return a value using RETURN statement, while procedures use OUT parameters. Functions can be used in SQL expressions, procedures cannot.

Creating Basic Functions

DELIMITER // -- Simple function that returns a calculated value CREATE FUNCTION CalculateTax(amount DECIMAL(10,2)) RETURNS DECIMAL(10,2) DETERMINISTIC BEGIN DECLARE tax_rate DECIMAL(5,4) DEFAULT 0.0825; RETURN amount * tax_rate; END // DELIMITER ; -- Use the function in queries SELECT order_id, total_amount, CalculateTax(total_amount) AS tax, total_amount + CalculateTax(total_amount) AS total_with_tax FROM orders;

Function Characteristics

Functions must specify their characteristics:

DELIMITER // -- DETERMINISTIC: Same input always gives same output CREATE FUNCTION GetDiscount(total_spent DECIMAL(10,2)) RETURNS DECIMAL(5,2) DETERMINISTIC BEGIN RETURN CASE WHEN total_spent >= 10000 THEN 20.00 WHEN total_spent >= 5000 THEN 15.00 WHEN total_spent >= 1000 THEN 10.00 ELSE 0.00 END; END // -- NOT DETERMINISTIC: Output may vary (uses NOW(), RAND(), etc.) CREATE FUNCTION GetCurrentYear() RETURNS INT NOT DETERMINISTIC READS SQL DATA BEGIN RETURN YEAR(NOW()); END // -- NO SQL: Function doesn't contain SQL statements -- READS SQL DATA: Function reads data but doesn't modify -- MODIFIES SQL DATA: Function modifies data (use carefully!) DELIMITER ;
Important: Be careful with MODIFIES SQL DATA functions. They can cause unexpected side effects when used in SELECT statements.

Functions with Complex Logic

DELIMITER // -- Function to calculate customer loyalty tier CREATE FUNCTION GetCustomerTier(customer_id_param INT) RETURNS VARCHAR(20) READS SQL DATA BEGIN DECLARE total_spent DECIMAL(10,2); DECLARE order_count INT; DECLARE tier VARCHAR(20); SELECT COALESCE(SUM(total_amount), 0), COUNT(*) INTO total_spent, order_count FROM orders WHERE customer_id = customer_id_param; IF total_spent > 10000 OR order_count > 20 THEN SET tier = 'Platinum'; ELSEIF total_spent > 5000 OR order_count > 10 THEN SET tier = 'Gold'; ELSEIF total_spent > 1000 OR order_count > 5 THEN SET tier = 'Silver'; ELSE SET tier = 'Bronze'; END IF; RETURN tier; END // -- Calculate string similarity (Levenshtein distance simplified) CREATE FUNCTION StringSimilarity(str1 VARCHAR(255), str2 VARCHAR(255)) RETURNS INT DETERMINISTIC BEGIN DECLARE len1 INT DEFAULT LENGTH(str1); DECLARE len2 INT DEFAULT LENGTH(str2); IF str1 = str2 THEN RETURN 100; ELSEIF len1 = 0 OR len2 = 0 THEN RETURN 0; END IF; -- Simplified similarity based on common characters RETURN ROUND( (LENGTH(str1) + LENGTH(str2) - LENGTH(REPLACE(CONCAT(str1, str2), SUBSTRING(str1, 1, 1), ''))) * 100 / (LENGTH(str1) + LENGTH(str2)) ); END // DELIMITER ; -- Use functions in queries SELECT customer_id, customer_name, GetCustomerTier(customer_id) AS tier, GetDiscount(total_spent) AS discount_rate FROM customers ORDER BY total_spent DESC;

Stored Functions vs Procedures

Functions: ✓ Must return a single value ✓ Can be used in SELECT, WHERE, HAVING ✓ Cannot modify data (usually) ✓ Cannot use OUT/INOUT parameters ✓ Can be part of expressions Procedures: ✓ May or may not return values ✓ Cannot be used in SELECT directly ✓ Can modify data ✓ Can use OUT/INOUT parameters ✓ Called with CALL statement

Introduction to Triggers

Triggers are database objects that automatically execute when specific events occur on a table.

Key Concept: Triggers run automatically in response to INSERT, UPDATE, or DELETE operations. They're perfect for maintaining data integrity and enforcing business rules.

Creating Basic Triggers

DELIMITER // -- BEFORE INSERT trigger: validates data before insertion CREATE TRIGGER validate_product_price BEFORE INSERT ON products FOR EACH ROW BEGIN IF NEW.price < 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Price cannot be negative'; END IF; IF NEW.stock_quantity < 0 THEN SET NEW.stock_quantity = 0; END IF; END // -- AFTER INSERT trigger: perform action after insertion CREATE TRIGGER log_new_customer AFTER INSERT ON customers FOR EACH ROW BEGIN INSERT INTO audit_log (table_name, action, record_id, created_at) VALUES ('customers', 'INSERT', NEW.customer_id, NOW()); END // DELIMITER ;

BEFORE vs AFTER Triggers

BEFORE Triggers: - Execute before the operation - Can modify NEW values - Can prevent operation with SIGNAL - Use for validation and data modification AFTER Triggers: - Execute after the operation - Cannot modify NEW values - Cannot prevent operation - Use for logging and cascading changes

UPDATE Triggers

DELIMITER // -- Track changes to sensitive data CREATE TRIGGER track_price_changes BEFORE UPDATE ON products FOR EACH ROW BEGIN -- Only log if price actually changed IF NEW.price != OLD.price THEN INSERT INTO price_history ( product_id, old_price, new_price, changed_by, changed_at ) VALUES ( OLD.product_id, OLD.price, NEW.price, USER(), NOW() ); END IF; END // -- Automatically update timestamps CREATE TRIGGER update_product_timestamp BEFORE UPDATE ON products FOR EACH ROW BEGIN SET NEW.updated_at = NOW(); END // -- Prevent unauthorized changes CREATE TRIGGER protect_archived_orders BEFORE UPDATE ON orders FOR EACH ROW BEGIN IF OLD.status = 'archived' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cannot modify archived orders'; END IF; END // DELIMITER ;

DELETE Triggers

DELIMITER // -- Soft delete implementation CREATE TRIGGER soft_delete_customer BEFORE DELETE ON customers FOR EACH ROW BEGIN -- Instead of deleting, mark as inactive INSERT INTO deleted_customers SELECT *, NOW() AS deleted_at FROM customers WHERE customer_id = OLD.customer_id; -- Prevent actual deletion SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Use soft delete instead'; END // -- Cascade delete with logging CREATE TRIGGER log_order_deletion BEFORE DELETE ON orders FOR EACH ROW BEGIN -- Log the deletion INSERT INTO audit_log ( table_name, action, record_id, old_data, created_at ) VALUES ( 'orders', 'DELETE', OLD.order_id, JSON_OBJECT( 'customer_id', OLD.customer_id, 'total_amount', OLD.total_amount, 'status', OLD.status ), NOW() ); -- Delete related order items DELETE FROM order_items WHERE order_id = OLD.order_id; END // DELIMITER ;

Audit Logging with Triggers

-- Create comprehensive audit log table CREATE TABLE audit_trail ( audit_id INT AUTO_INCREMENT PRIMARY KEY, table_name VARCHAR(64), operation VARCHAR(10), record_id INT, old_values JSON, new_values JSON, changed_by VARCHAR(255), changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); DELIMITER // -- Generic audit trigger for INSERT CREATE TRIGGER audit_product_insert AFTER INSERT ON products FOR EACH ROW BEGIN INSERT INTO audit_trail ( table_name, operation, record_id, new_values, changed_by ) VALUES ( 'products', 'INSERT', NEW.product_id, JSON_OBJECT( 'product_name', NEW.product_name, 'price', NEW.price, 'stock_quantity', NEW.stock_quantity ), USER() ); END // -- Generic audit trigger for UPDATE CREATE TRIGGER audit_product_update AFTER UPDATE ON products FOR EACH ROW BEGIN INSERT INTO audit_trail ( table_name, operation, record_id, old_values, new_values, changed_by ) VALUES ( 'products', 'UPDATE', NEW.product_id, JSON_OBJECT( 'product_name', OLD.product_name, 'price', OLD.price, 'stock_quantity', OLD.stock_quantity ), JSON_OBJECT( 'product_name', NEW.product_name, 'price', NEW.price, 'stock_quantity', NEW.stock_quantity ), USER() ); END // -- Generic audit trigger for DELETE CREATE TRIGGER audit_product_delete BEFORE DELETE ON products FOR EACH ROW BEGIN INSERT INTO audit_trail ( table_name, operation, record_id, old_values, changed_by ) VALUES ( 'products', 'DELETE', OLD.product_id, JSON_OBJECT( 'product_name', OLD.product_name, 'price', OLD.price, 'stock_quantity', OLD.stock_quantity ), USER() ); END // DELIMITER ;

Maintaining Calculated Columns

DELIMITER // -- Automatically update order totals CREATE TRIGGER calculate_order_total AFTER INSERT ON order_items FOR EACH ROW BEGIN UPDATE orders SET total_amount = ( SELECT SUM(quantity * unit_price) FROM order_items WHERE order_id = NEW.order_id ) WHERE order_id = NEW.order_id; END // -- Update customer statistics CREATE TRIGGER update_customer_stats AFTER INSERT ON orders FOR EACH ROW BEGIN UPDATE customers SET total_orders = total_orders + 1, total_spent = total_spent + NEW.total_amount, last_order_date = NEW.order_date WHERE customer_id = NEW.customer_id; END // DELIMITER ;

Managing Triggers

-- Show all triggers in current database SHOW TRIGGERS; -- Show triggers for specific table SHOW TRIGGERS WHERE `Table` = 'products'; -- Show trigger definition SHOW CREATE TRIGGER validate_product_price; -- Drop a trigger DROP TRIGGER IF EXISTS validate_product_price; -- Query information_schema for trigger details SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE, ACTION_TIMING, ACTION_STATEMENT FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = 'your_database' ORDER BY EVENT_OBJECT_TABLE, ACTION_TIMING;

Trigger Best Practices

✓ Keep triggers simple and fast ✓ Avoid complex logic in triggers ✓ Don't create triggers that call triggers (recursive) ✓ Use triggers for data integrity, not business logic ✓ Document all triggers thoroughly ✓ Test triggers extensively ✓ Consider performance impact ✓ Use BEFORE triggers for validation ✓ Use AFTER triggers for logging ✗ Don't put all business logic in triggers ✗ Avoid triggers that modify multiple tables ✗ Don't use triggers as a replacement for application logic
Performance Warning: Triggers execute for every row affected by an operation. A single UPDATE affecting 10,000 rows will fire the trigger 10,000 times!

Real-World Example: Inventory Management

DELIMITER // -- Function to check stock availability CREATE FUNCTION CheckStockAvailable( p_product_id INT, p_quantity INT ) RETURNS BOOLEAN READS SQL DATA BEGIN DECLARE available INT; SELECT stock_quantity INTO available FROM products WHERE product_id = p_product_id; RETURN available >= p_quantity; END // -- Trigger to validate and update inventory CREATE TRIGGER validate_order_item BEFORE INSERT ON order_items FOR EACH ROW BEGIN IF NOT CheckStockAvailable(NEW.product_id, NEW.quantity) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient stock for this product'; END IF; -- Reserve inventory UPDATE products SET stock_quantity = stock_quantity - NEW.quantity WHERE product_id = NEW.product_id; -- Log inventory change INSERT INTO inventory_log ( product_id, change_type, quantity_change, reason, created_at ) VALUES ( NEW.product_id, 'SALE', -NEW.quantity, CONCAT('Order item #', NEW.order_item_id), NOW() ); END // DELIMITER ;

Practice Exercise:

Create a Complete Audit System:

  1. Create a function 'FormatAuditData' that takes column values and formats them as JSON
  2. Create BEFORE/AFTER triggers on the 'users' table for:
    • INSERT: Log new user creation with all details
    • UPDATE: Log changes to email, password_hash, or status fields only
    • DELETE: Prevent deletion of admin users, log other deletions
  3. Test the system by performing INSERT, UPDATE, and DELETE operations
  4. Query the audit_trail table to verify all changes were logged

Summary

In this lesson, you learned:

  • User-defined functions return single values for use in expressions
  • Functions must be DETERMINISTIC or NOT DETERMINISTIC
  • Triggers automatically execute on INSERT, UPDATE, DELETE
  • BEFORE triggers can validate and modify data
  • AFTER triggers are ideal for logging and cascading changes
  • NEW and OLD keywords access row data in triggers
  • Triggers are powerful but should be used carefully
  • Audit logging is a perfect use case for triggers
Next Up: In the next lesson, we'll explore MySQL Events for scheduling automated tasks!