MySQL BEFORE INSERT MySQL · Triggers · BEFORE INSERT

MySQL BEFORE INSERT Trigger

Master MySQL BEFORE INSERT triggers for validating and transforming new row data before storage. Covers SIGNAL for rejecting invalid inserts, SET NEW.column for normalization, the difference from CHECK constraints, and worked examples with age validation and name capitalization.

A BEFORE INSERT trigger fires before a new row enters the table — your last chance to validate or modify the data on its way in. Two big use cases:

  • Validation — if the row violates a business rule, raise an error with SIGNAL to abort the INSERT.
  • Transformation — adjust column values via SET NEW.col = ... to derive defaults, normalize formats, or compute fields.
DELIMITER $$ CREATE TRIGGER trigger_name BEFORE INSERT ON table_name FOR EACH ROW BEGIN -- statements that read or modify NEW.column END$$ DELIMITER ;

Notes:

  • Only NEW is available — OLD doesn't exist for INSERTs (no previous version of the row to refer to).
  • NEW.column_name can be both read and assigned with SET NEW.column_name = expression.
  • To abort the INSERT, raise an error with SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '...'. The triggering INSERT and any pending transaction work get rolled back.
MySQL — Setup
DROP TABLE IF EXISTS employees;

CREATE TABLE employees (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    first_name  VARCHAR(50),
    last_name   VARCHAR(50),
    age         INT,
    join_date   DATE
);
Example 1 — Validation: Reject Underage Employees

Enforce a business rule that employees must be at least 18. The trigger checks NEW.age and uses SIGNAL to abort the INSERT if the rule is violated:

MySQL — Validation trigger
DROP TRIGGER IF EXISTS before_insert_employee;

DELIMITER $$

CREATE TRIGGER before_insert_employee
    BEFORE INSERT ON employees
    FOR EACH ROW
BEGIN
    IF NEW.age < 18 THEN
        SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT = 'Employee must be at least 18 years old';
    END IF;
END$$

DELIMITER ;

Try inserting a 16-year-old:

MySQL — Underage attempt
INSERT INTO employees (first_name, last_name, age, join_date)
VALUES ('John', 'Doe', 16, '2024-04-13');
OUTPUT
mysql — rejected
mysql>INSERT INTO employees ... VALUES ('John', 'Doe', 16, '2024-04-13');
ERROR 1644 (45000): Employee must be at least 18 years old

The INSERT was aborted — no row appears in employees. Now try a valid age:

MySQL — Valid insert
INSERT INTO employees (first_name, last_name, age, join_date)
VALUES ('Steven', 'Raj', 20, '2025-04-17');

SELECT * FROM employees;
OUTPUT
mysql — accepted
mysql>INSERT INTO employees ... VALUES ('Steven', 'Raj', 20, '2025-04-17');
Query OK, 1 row affected (0.01 sec)
mysql>SELECT * FROM employees;
+----+------------+-----------+------+------------+
| id | first_name | last_name | age | join_date |
+----+------------+-----------+------+------------+
| 1 | Steven | Raj | 20 | 2025-04-17 |
+----+------------+-----------+------+------------+
1 row in set (0.00 sec)
⚠️ The original tutorial had SELECT * employees; — missing the FROM keyword. That's a syntax error. The correct form is SELECT * FROM employees; as shown above.
Example 2 — Transformation: Normalize and Default

BEFORE INSERT triggers can modify the new row. Normalize names to a canonical case and supply a default join date if the caller didn't:

MySQL — Transformation trigger
DROP TRIGGER IF EXISTS before_insert_employee;

DELIMITER $$

CREATE TRIGGER before_insert_employee
    BEFORE INSERT ON employees
    FOR EACH ROW
BEGIN
    -- Validate
    IF NEW.age < 18 THEN
        SIGNAL SQLSTATE '45000'
            SET MESSAGE_TEXT = 'Employee must be at least 18 years old';
    END IF;

    -- Normalize: capitalize names
    SET NEW.first_name = CONCAT(UPPER(LEFT(NEW.first_name, 1)),
                                LOWER(SUBSTRING(NEW.first_name, 2)));
    SET NEW.last_name  = CONCAT(UPPER(LEFT(NEW.last_name, 1)),
                                LOWER(SUBSTRING(NEW.last_name, 2)));

    -- Default the join date to today if not supplied
    IF NEW.join_date IS NULL THEN
        SET NEW.join_date = CURDATE();
    END IF;
END$$

DELIMITER ;

INSERT INTO employees (first_name, last_name, age) VALUES ('jANE', 'sMITH', 25);

SELECT * FROM employees ORDER BY id;
OUTPUT
mysql — transformed
mysql>INSERT INTO employees (first_name, last_name, age) VALUES ('jANE', 'sMITH', 25);
Query OK, 1 row affected (0.01 sec)
mysql>SELECT * FROM employees ORDER BY id;
+----+------------+-----------+------+------------+
| id | first_name | last_name | age | join_date |
+----+------------+-----------+------+------------+
| 1 | Steven | Raj | 20 | 2025-04-17 |
| 2 | Jane | Smith | 25 | 2026-04-22 |
+----+------------+-----------+------+------------+
2 rows in set (0.00 sec)

Notice 'jANE' became 'Jane' and the missing join_date got defaulted to today's date — the trigger transformed the data on its way in.

Use BEFORE INSERT for…Use a CHECK / FK constraint instead for…
Cross-column validation that depends on multiple valuesSingle-column constraints (age >= 18)
Validation that queries other tablesForeign-key relationships
Transformation / normalization of incoming valuesUniqueness
Computed default values that depend on multiple columnsSimple constants or built-in defaults
💡 Prefer CHECK constraints over BEFORE INSERT triggers for simple validation. MySQL 8.0.16+ supports CHECK constraints natively: CREATE TABLE employees (age INT CHECK (age >= 18)). They're faster, clearer, and visible in the table DDL. Use BEFORE INSERT triggers for validation logic too complex for CHECK or for column transformation.
  1. Use DROP TRIGGER IF EXISTS at the top of every deployment script.
  2. Validate first, transform second. If validation fails, the SIGNAL stops the trigger before any unnecessary work.
  3. Use SQLSTATE '45000' as the convention for trigger-raised validation errors.
  4. Provide actionable messages in MESSAGE_TEXT — "age must be at least 18" beats "validation failed".
  5. Don't query the same table the trigger is attached to — MySQL prohibits it (would cause infinite recursion).
  6. Keep trigger bodies short. Per-row overhead multiplies with INSERTs that affect many rows.
  7. For column transformation only, prefer generated columns when you can: full_name VARCHAR(100) GENERATED ALWAYS AS (CONCAT(first_name, ' ', last_name)). Triggers are needed when the transformation depends on logic too complex for a generated column.
  • BEFORE INSERT triggers fire just before a new row is written, giving you the chance to validate or transform it.
  • Only NEW is available — there's no OLD on INSERT.
  • You can read and assign to NEW.column in BEFORE triggers; assigning changes what gets stored.
  • Use SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '...' to abort the INSERT with a meaningful error.
  • For simple value validation, prefer CHECK constraints (8.0.16+); reach for triggers when the logic is too complex for CHECK.
  • Trigger bodies fire once per affected row — keep them fast on bulk inserts.