MySQL CREATE TRIGGER MySQL · Triggers · CREATE

MySQL CREATE TRIGGER

Master MySQL CREATE TRIGGER — full syntax, OLD and NEW availability per event type, a worked audit-log example with item_changes capturing both old and new prices, SHOW TRIGGERS and information_schema.TRIGGERS for inspection, DROP TRIGGER IF EXISTS for safe deployment, and best practices.

The CREATE TRIGGER statement defines a new trigger on a table. The trigger fires automatically when the matching INSERT, UPDATE, or DELETE event occurs.

CREATE TRIGGER trigger_name {BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON table_name FOR EACH ROW [{FOLLOWS | PRECEDES} other_trigger_name] trigger_body;
ElementPurpose
trigger_nameThe name of the trigger. Unique per database.
BEFORE | AFTERWhen relative to the operation the trigger fires.
INSERT | UPDATE | DELETEWhich kind of operation activates the trigger.
ON table_nameThe table the trigger is attached to.
FOR EACH ROWRequired — confirms the trigger is row-level (the only kind MySQL supports).
FOLLOWS / PRECEDESOptional — controls order when multiple triggers exist for the same event/timing.
trigger_bodyA single SQL statement, or a BEGIN ... END block containing many statements.

If the body has multiple statements, use a BEGIN ... END block and change the delimiter so the inner semicolons don't end the CREATE TRIGGER prematurely:

DELIMITER $$ CREATE TRIGGER trigger_name {BEFORE | AFTER} {INSERT | UPDATE | DELETE} ON table_name FOR EACH ROW BEGIN -- variable declarations and statements END$$ DELIMITER ;

Inside the trigger body, the OLD and NEW pseudo-rows expose the affected row's data. Which is available depends on the event:

Trigger EventOLDNEW
INSERTThe new row (read in AFTER, read+write in BEFORE)
UPDATEThe pre-update row (read-only)The new row (read in AFTER, read+write in BEFORE)
DELETEThe row being deleted (read-only)

Reference fields with OLD.column_name and NEW.column_name. Inside a BEFORE trigger you can assign to NEW.column_name with SET NEW.column_name = ... to modify what gets stored.

Example 1 — A Complete Audit Trigger

Build a worked example: an items table with an item_changes audit log. Whenever an item is updated, the trigger writes a record to the log including who-changed-what.

First, the tables:

MySQL — Setup
DROP TABLE IF EXISTS item_changes;
DROP TABLE IF EXISTS items;

CREATE TABLE items (
    id     INT          PRIMARY KEY,
    name   VARCHAR(255) NOT NULL,
    price  DECIMAL(10, 2) NOT NULL
);

CREATE TABLE item_changes (
    change_id        INT       PRIMARY KEY AUTO_INCREMENT,
    item_id          INT,
    change_type      VARCHAR(10),
    old_price        DECIMAL(10, 2),
    new_price        DECIMAL(10, 2),
    change_timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (item_id) REFERENCES items(id)
);

-- Seed
INSERT INTO items (id, name, price) VALUES (1, 'Widget', 50.00);

Now the trigger — note DROP TRIGGER IF EXISTS at the top for idempotent deployment:

MySQL — AFTER UPDATE trigger
DROP TRIGGER IF EXISTS update_items_trigger;

DELIMITER $$

CREATE TRIGGER update_items_trigger
    AFTER UPDATE ON items
    FOR EACH ROW
BEGIN
    INSERT INTO item_changes (item_id, change_type, old_price, new_price)
    VALUES (NEW.id, 'UPDATE', OLD.price, NEW.price);
END$$

DELIMITER ;

Trigger an update:

MySQL — Update and verify
UPDATE items SET price = 60.00 WHERE id = 1;

SELECT * FROM item_changes;
OUTPUT
mysql — audit captured
mysql>UPDATE items SET price = 60.00 WHERE id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql>SELECT * FROM item_changes;
+-----------+---------+-------------+-----------+-----------+---------------------+
| change_id | item_id | change_type | old_price | new_price | change_timestamp |
+-----------+---------+-------------+-----------+-----------+---------------------+
| 1 | 1 | UPDATE | 50.00 | 60.00 | 2026-04-22 14:30:11 |
+-----------+---------+-------------+-----------+-----------+---------------------+
1 row in set (0.00 sec)

Notice we never explicitly inserted into item_changes — the trigger did it automatically. The application code only ran UPDATE items; the audit log appeared as a side effect.

💡 Capturing OLD vs NEW in audit triggers is the canonical use case. The original tutorial logged only change_type, which tells you that something changed but not what. Real audit triggers capture both old and new values for the columns of interest.

Use SHOW TRIGGERS to find what's installed:

SHOW TRIGGERS [FROM database_name] [LIKE 'pattern' | WHERE search_condition];
Example 2 — SHOW TRIGGERS
MySQL — List triggers
-- All triggers in the current database
SHOW TRIGGERS;

-- Filter by table name
SHOW TRIGGERS LIKE 'items';

-- Filter by event type
SHOW TRIGGERS WHERE `Event` = 'UPDATE';
OUTPUT
mysql — show triggers
mysql>SHOW TRIGGERS LIKE 'items'\G
*************************** 1. row ***************************
Trigger: update_items_trigger
Event: UPDATE
Table: items
Statement: BEGIN
INSERT INTO item_changes (item_id, change_type, ...)
VALUES (NEW.id, 'UPDATE', OLD.price, NEW.price);
END
Timing: AFTER
Created: 2026-04-22 14:28:00.30
Definer: root@localhost
1 row in set (0.00 sec)
⚠️ The original tutorial showed SHOW TRIGGERS LIKE '--your table_name--' with the literal placeholder text. LIKE takes a real table-name pattern (or '%' wildcards) — replace the placeholder with the actual name when copying examples.
Example 3 — Querying information_schema.TRIGGERS

For programmatic access, query the metadata directly. The same data with much more flexibility:

MySQL — information_schema
SELECT trigger_name,
       event_object_table  AS `table`,
       event_manipulation  AS `event`,
       action_timing       AS timing,
       definer
FROM   information_schema.TRIGGERS
WHERE  trigger_schema = DATABASE()
ORDER  BY event_object_table, action_timing, event_manipulation;
DROP TRIGGER [IF EXISTS] [schema_name.]trigger_name;
MySQL — Safe drop
DROP TRIGGER IF EXISTS update_items_trigger;

Always include IF EXISTS in deployment scripts — without it, dropping a non-existent trigger errors out and aborts the script.

📌 Triggers are dropped automatically when their table is dropped. If you DROP TABLE items, all triggers attached to that table go with it — you don't have to drop them separately. They're tied to the table's lifecycle.
  1. Always include DROP TRIGGER IF EXISTS at the top of any CREATE TRIGGER deployment script. Idempotent re-deployment for free.
  2. Use descriptive names. employees_before_insert_validate_age beats trg1. Triggers live for years; future you will thank present you.
  3. One trigger, one job. Don't pile multiple unrelated concerns into one trigger body — split into separate triggers ordered with FOLLOWS.
  4. Capture both OLD and NEW in audit triggers — knowing only "something changed" is rarely useful; knowing "price went from 50 to 60" is.
  5. Keep trigger bodies fast. Remember the per-row multiplier — anything slow gets multiplied by the row count of the triggering statement.
  6. Don't put complex business logic in triggers. If you need it, prefer application code or a stored procedure called explicitly.
  7. Disable triggers for bulk loads when possible. Add a flag column or temporarily drop and recreate the trigger around the bulk operation.
  8. Document with SHOW TRIGGERS output as part of your schema documentation. Triggers are easy to forget about.
  • CREATE TRIGGER name {BEFORE|AFTER} {INSERT|UPDATE|DELETE} ON table FOR EACH ROW body; creates a row-level trigger.
  • For multi-statement bodies, use BEGIN ... END with DELIMITER $$.
  • OLD.col and NEW.col access the row data; availability depends on the event (no OLD on INSERT, no NEW on DELETE).
  • BEFORE triggers can modify NEW via SET NEW.col = ... — last chance to alter the row.
  • SHOW TRIGGERS lists triggers; information_schema.TRIGGERS gives programmatic access with joins and arbitrary filters.
  • DROP TRIGGER IF EXISTS name; removes a trigger safely. Triggers also drop automatically when their table is dropped.
  • Always use DROP TRIGGER IF EXISTS in deployment scripts for idempotence.