Chapitre 820 minAvancé
Trigger MySQL : BEFORE, AFTER, NEW et OLD avec exemples
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- choisir le moment (
BEFORE,AFTER) et l'événement d'un trigger ; - lire et modifier la ligne avec
NEWetOLD; - refuser une modification ou corriger une valeur avant l'écriture ;
- connaître les limites des triggers et leurs alternatives.
4 exercices corrigés1 schéma
Un trigger (en français, déclencheur) est un programme stocké attaché à une table, que MySQL exécute automatiquement avant ou après chaque INSERT, UPDATE ou DELETE sur cette table, une fois pour chaque ligne concernée. Dans son code, NEW désigne la ligne telle qu'elle va être (ou a été) écrite, et OLD la ligne telle qu'elle était. Ce chapitre montre comment écrire un trigger de contrôle, de correction et de propagation sur la base parc, et quelles sont ses limites.
La syntaxe de CREATE TRIGGER
DELIMITER //
CREATE TRIGGER nom_trigger
{ BEFORE | AFTER } { INSERT | UPDATE | DELETE }
ON nom_table
FOR EACH ROW
[ { FOLLOWS | PRECEDES } autre_trigger ]
BEGIN
-- instructions ; NEW.colonne et OLD.colonne désignent la ligne en cours
END //
DELIMITER ;
- Le moment :
BEFOREs'exécute avant l'écriture de la ligne,AFTERaprès. Avec trois événements, cela fait six combinaisons possibles par table. FOR EACH ROWest obligatoire en MySQL : le trigger s'exécute une fois par ligne. UnUPDATEqui modifie dix tickets déclenche dix fois le trigger.- Le nom est unique dans la base. Une convention lisible :
trg_<table>_<rôle>, commetrg_intervention_technicien. - Créer un trigger demande le privilège
TRIGGERsur la table.
NEW et OLD
| Événement | OLD | NEW |
|---|---|---|
INSERT | N'existe pas (erreur 1363 si tu l'utilises) | La ligne insérée |
UPDATE | La ligne avant la modification | La ligne après la modification |
DELETE | La ligne supprimée | N'existe pas (erreur 1363) |
OLD est toujours en lecture seule. NEW peut être modifié avec SET NEW.colonne = valeur, mais seulement dans un trigger BEFORE : dans un trigger AFTER, la ligne est déjà écrite, et MySQL refuse de créer le trigger (erreur 1362, Updating of NEW row is not allowed in after trigger).
| Moment | Ce qu'il peut faire | Usage |
|---|---|---|
BEFORE | Lire et modifier NEW, ou refuser la ligne avec SIGNAL | Contrôler, normaliser, compléter la ligne |
AFTER | Lire NEW et OLD, modifier d'autres tables | Historiser, auditer, propager vers une autre table |
Contrôler : refuser une modification avec SIGNAL
Seul un membre du service informatique peut être technicien d'une intervention. Une clé étrangère ne sait pas l'exprimer (elle vérifie que l'employé existe, pas son service), une contrainte CHECK non plus (elle ne lit pas d'autre table). Un trigger BEFORE INSERT le peut :
DELIMITER //
CREATE TRIGGER trg_intervention_technicien
BEFORE INSERT ON intervention
FOR EACH ROW
BEGIN
IF NOT EXISTS (SELECT 1
FROM employe e
JOIN service s ON s.id_service = e.id_service
WHERE e.id_employe = NEW.id_technicien AND s.code = 'INFO') THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Seul un membre du service informatique peut intervenir';
END IF;
END //
DELIMITER ;
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min, compte_rendu)
VALUES (12, 9, '2026-10-22 10:00:00', 15, 'Clavier changé');
-- ERROR 1644 (45000): Seul un membre du service informatique peut intervenir
Inès Bonnet (9) travaille à la logistique : l'insertion est refusée, quelle que soit l'application qui la tente. Un second contrôle sur la même table et le même événement : une intervention ne peut pas précéder l'ouverture de son ticket.
DELIMITER //
CREATE TRIGGER trg_intervention_date
BEFORE INSERT ON intervention
FOR EACH ROW
FOLLOWS trg_intervention_technicien
BEGIN
DECLARE v_ouverture DATETIME;
SELECT date_ouverture INTO v_ouverture
FROM ticket
WHERE id_ticket = NEW.id_ticket;
IF NEW.date_intervention < v_ouverture THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Intervention antérieure à l''ouverture du ticket';
END IF;
END //
DELIMITER ;
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min, compte_rendu)
VALUES (12, 11, '2026-10-20 10:00:00', 15, 'Clavier changé');
-- ERROR 1644 (45000): Intervention antérieure à l'ouverture du ticket
Le ticket 12 a été ouvert le 21 octobre. Plusieurs triggers peuvent exister pour le même moment et le même événement : ils s'exécutent dans leur ordre de création, ou dans l'ordre que fixent FOLLOWS et PRECEDES. Ici, le contrôle du technicien passe en premier.
Corriger : modifier NEW avant l'écriture
Un trigger BEFORE peut compléter ou normaliser la ligne. À l'ouverture d'un ticket, on retire les espaces en trop du titre, on refuse un titre vide, et un ticket qui parle de rançongiciel passe d'office en priorité critique :
DELIMITER //
CREATE TRIGGER trg_ticket_avant_ajout
BEFORE INSERT ON ticket
FOR EACH ROW
BEGIN
SET NEW.titre = TRIM(NEW.titre);
IF NEW.titre = '' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Le titre du ticket est obligatoire';
END IF;
IF NEW.titre LIKE '%rançongiciel%' OR NEW.titre LIKE '%ransomware%' THEN
SET NEW.priorite = 'critique';
END IF;
END //
DELIMITER ;
INSERT INTO ticket (titre, id_demandeur) VALUES (' Rançongiciel sur le poste de la compta ', 2);
SELECT id_ticket, titre, priorite, statut FROM ticket WHERE id_ticket = 15;
| id_ticket | titre | priorite | statut |
|---|---|---|---|
| 15 | Rançongiciel sur le poste de la compta | critique | ouvert |
La priorité n'a pas été fournie : la valeur par défaut 'normale' est déjà dans NEW quand le trigger s'exécute, et il la remplace. Le statut, lui, garde sa valeur par défaut. L'interclassement par défaut ignore la casse et les accents dans les comparaisons : LIKE '%rançongiciel%' reconnaît aussi « Rançongiciel ».
Un deuxième exemple, que tu as peut-être croisé dans le chapitre aperçu du cours SQL : la date de clôture gérée par la base, à partir du statut.
DELIMITER //
CREATE TRIGGER trg_ticket_cloture
BEFORE UPDATE ON ticket
FOR EACH ROW
BEGIN
IF NEW.statut = 'resolu' AND OLD.statut <> 'resolu' THEN
SET NEW.date_cloture = NOW(); -- le ticket vient d'être résolu
ELSEIF NEW.statut <> 'resolu' THEN
SET NEW.date_cloture = NULL; -- un ticket non résolu n'a pas de date de clôture
END IF;
END //
DELIMITER ;
UPDATE ticket SET statut = 'en_cours' WHERE id_ticket = 9;
SELECT statut, date_cloture FROM ticket WHERE id_ticket = 9; -- en_cours, NULL
Propager : modifier une autre table
Un trigger AFTER peut écrire dans d'autres tables. Quand un technicien enregistre la première intervention sur un ticket ouvert, le ticket passe automatiquement « en cours » :
DELIMITER //
CREATE TRIGGER trg_intervention_prise_en_charge
AFTER INSERT ON intervention
FOR EACH ROW
BEGIN
UPDATE ticket
SET statut = 'en_cours'
WHERE id_ticket = NEW.id_ticket AND statut = 'ouvert';
END //
DELIMITER ;
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min, compte_rendu)
VALUES (12, 11, '2026-10-22 10:00:00', 15, 'Clavier changé');
SELECT statut FROM ticket WHERE id_ticket = 12; -- en_cours (il était ouvert)
L'UPDATE de ticket déclenche à son tour trg_ticket_cloture (un BEFORE UPDATE sur ticket) : les triggers s'enchaînent. C'est puissant et dangereux à la fois, car l'effet d'un simple INSERT devient difficile à prévoir. Documente chaque trigger, et garde-les courts.
Pourquoi AFTER et pas BEFORE ? Un trigger AFTER ne s'exécute que si la ligne a été écrite et a passé toutes les contraintes : la conséquence ne concerne qu'une intervention réellement enregistrée. Avec InnoDB, un BEFORE qui modifierait le ticket serait de toute façon annulé avec l'instruction en cas d'erreur, mais la convention est claire et vaut d'être suivie : BEFORE pour la ligne elle-même, AFTER pour les conséquences ailleurs.
Les limites des triggers
Un trigger ne peut pas modifier sa propre table. Un trigger sur ticket qui exécute UPDATE ticket (ou INSERT, ou DELETE sur ticket) échoue à l'exécution :
ERROR 1442 (HY000): Can't update table 'ticket' in stored function/trigger because it is
already used by statement which invoked this stored function/trigger.
Pour modifier la ligne en cours, utilise SET NEW.colonne = … dans un trigger BEFORE. Pour modifier d'autres lignes de la même table, il faut une procédure appelée par l'application.
Les actions des clés étrangères ne déclenchent pas les triggers. Supprimer le ticket 13 supprime en cascade ses interventions (ON DELETE CASCADE), mais un trigger AFTER DELETE ON intervention ne s'exécute pas pour elles. Un audit des suppressions d'interventions doit donc aussi surveiller les suppressions de tickets.
Ce qui déclenche, et ce qui ne déclenche pas.
| Instruction | Triggers déclenchés |
|---|---|
INSERT, LOAD DATA | BEFORE INSERT, AFTER INSERT |
UPDATE | BEFORE UPDATE, AFTER UPDATE |
DELETE | BEFORE DELETE, AFTER DELETE |
INSERT … ON DUPLICATE KEY UPDATE sur une ligne existante | BEFORE INSERT, puis BEFORE UPDATE et AFTER UPDATE |
REPLACE sur une ligne existante | BEFORE INSERT, BEFORE DELETE, AFTER DELETE, AFTER INSERT |
TRUNCATE TABLE, DROP TABLE | Aucun |
Cascade ON DELETE / ON UPDATE d'une clé étrangère | Aucun sur la table enfant |
Les autres interdits sont ceux des fonctions (chapitre 4) : pas de jeu de résultats (un SELECT sans INTO), pas de COMMIT ni de ROLLBACK, pas de SQL dynamique, pas de RETURN (pour sortir, LEAVE sur un bloc étiqueté). Un trigger peut appeler une procédure avec CALL, à condition qu'elle respecte les mêmes interdits.
Le coût. Un trigger s'exécute pour chaque ligne : un UPDATE de 100 000 lignes exécute 100 000 fois son code. Un trigger lent ralentit toutes les écritures de la table, pour tout le monde.
Lister, afficher et supprimer
SHOW TRIGGERS FROM parc; -- tous les triggers de la base
SHOW TRIGGERS LIKE 'intervention'; -- ceux d'une table (LIKE porte sur le nom de la table)
SHOW CREATE TRIGGER trg_ticket_cloture; -- le code complet
SELECT trigger_name, event_manipulation AS evenement, event_object_table AS table_cible,
action_timing AS moment, action_order AS ordre
FROM information_schema.triggers
WHERE trigger_schema = 'parc'
ORDER BY table_cible, evenement, moment, ordre;
| TRIGGER_NAME | evenement | table_cible | moment | ordre |
|---|---|---|---|---|
| trg_intervention_technicien | INSERT | intervention | BEFORE | 1 |
| trg_intervention_date | INSERT | intervention | BEFORE | 2 |
| trg_intervention_prise_en_charge | INSERT | intervention | AFTER | 1 |
| trg_ticket_avant_ajout | INSERT | ticket | BEFORE | 1 |
| trg_ticket_cloture | UPDATE | ticket | BEFORE | 1 |
Pour supprimer : DROP TRIGGER IF EXISTS trg_ticket_cloture;. Supprimer une table supprime ses triggers. Comme pour les procédures, il n'y a pas de ALTER TRIGGER : on supprime et on recrée. CREATE TRIGGER IF NOT EXISTS existe depuis MySQL 8.0.29 ; MariaDB ajoute CREATE OR REPLACE TRIGGER. MySQL n'a pas de commande pour désactiver un trigger : il faut le supprimer.
Exercices
Exercice 1 sur 4 Interdire une durée excessive Réussi À revoir
Une intervention de plus de 8 heures (480 minutes) est presque toujours une erreur de saisie. Faut-il un trigger ou une contrainte ? Écris la solution la plus simple, puis la requête qui la teste.
Voir la réponseMasquer la réponse de l'exercice 1
La règle ne porte que sur la ligne elle-même : une contrainte CHECK suffit, plus simple et plus rapide qu'un trigger.
ALTER TABLE intervention
ADD CONSTRAINT ck_intervention_duree_max CHECK (duree_min <= 480);
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min)
VALUES (14, 11, '2026-10-28 09:00:00', 600);
-- ERROR 3819 (HY000): Check constraint 'ck_intervention_duree_max' is violated.
L'ALTER TABLE vérifie aussi les lignes existantes : la plus longue intervention de la base dure 240 minutes, la contrainte est acceptée. Un trigger ne se justifierait que pour un message personnalisé ou une règle plus riche (« plus de 8 heures par jour pour un même technicien »).
Tu avais trouvé ?
Exercice 2 sur 4 Mettre le numéro d'inventaire en majuscules Réussi À revoir
Les techniciens saisissent parfois pc-0016 au lieu de PC-0016. Écris le ou les triggers qui mettent le numéro d'inventaire en majuscules, à l'insertion comme à la modification.
Voir la réponseMasquer la réponse de l'exercice 2
DELIMITER //
CREATE TRIGGER trg_poste_inventaire_ajout
BEFORE INSERT ON poste
FOR EACH ROW
BEGIN
SET NEW.num_inventaire = UPPER(TRIM(NEW.num_inventaire));
END //
CREATE TRIGGER trg_poste_inventaire_modif
BEFORE UPDATE ON poste
FOR EACH ROW
BEGIN
SET NEW.num_inventaire = UPPER(TRIM(NEW.num_inventaire));
END //
DELIMITER ;
Deux triggers : MySQL n'accepte qu'un seul événement par trigger (pas de BEFORE INSERT OR UPDATE comme en PostgreSQL). Tous deux sont BEFORE, seul moment où NEW peut être modifié. Le TRIM retire aussi les espaces saisies par erreur autour du numéro.
Tu avais trouvé ?
Exercice 3 sur 4 Lire un trigger Réussi À revoir
Que fait ce trigger ? Quand la requête DELETE FROM employe WHERE id_employe = 14; réussit-elle ?
DELIMITER //
CREATE TRIGGER trg_employe_suppression
BEFORE DELETE ON employe
FOR EACH ROW
BEGIN
IF EXISTS (SELECT 1 FROM ticket WHERE id_demandeur = OLD.id_employe AND statut <> 'resolu') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Employé avec des tickets en attente';
END IF;
END //
DELIMITER ;
Voir la réponseMasquer la réponse de l'exercice 3
Avant chaque suppression d'un employé, il cherche un ticket non résolu dont il est le demandeur ; s'il en trouve un, il refuse la suppression avec un message clair.
Paul Martin (14) a ouvert le ticket 7, « Installer Sage pour Paul Martin », toujours ouvert : le trigger refuse la suppression. Même sans trigger, elle échouerait : la clé étrangère fk_ticket_demandeur (sans ON DELETE) interdit de supprimer un employé qui a des tickets, résolus ou non, et compte.id_employe en fait autant pour un employé qui a un compte. Le trigger ne fait que donner un message plus clair dans un cas particulier. Pour un salarié qui quitte l'entreprise, on anonymise sa ligne plutôt que de la supprimer.
Tu avais trouvé ?
Exercice 4 sur 4 Trouve les erreurs Réussi À revoir
Ce trigger doit tenir à jour une colonne nb_interventions ajoutée à la table ticket. Trouve les deux erreurs.
DELIMITER //
CREATE TRIGGER trg_intervention_compteur
AFTER INSERT ON intervention
FOR EACH ROW
BEGIN
SET NEW.duree_min = NEW.duree_min;
UPDATE ticket SET nb_interventions = nb_interventions + 1
WHERE id_ticket = OLD.id_ticket;
END //
DELIMITER ;
Voir la réponseMasquer la réponse de l'exercice 4
SET NEW.duree_min = …dans un triggerAFTER: erreur 1362 à la création. La ligne est déjà écrite. (Cette ligne ne sert d'ailleurs à rien.)OLD.id_ticketdans un triggerINSERT: il n'y a pas de ligne ancienne, erreur 1363. Il fautNEW.id_ticket.
Corrigé, avec la colonne qu'il suppose et une seule instruction dans le trigger :
ALTER TABLE ticket ADD COLUMN nb_interventions INT NOT NULL DEFAULT 0;
UPDATE ticket t
SET nb_interventions = (SELECT COUNT(*) FROM intervention i WHERE i.id_ticket = t.id_ticket);
CREATE TRIGGER trg_intervention_compteur
AFTER INSERT ON intervention
FOR EACH ROW
UPDATE ticket SET nb_interventions = nb_interventions + 1 WHERE id_ticket = NEW.id_ticket;
Pour que le compteur reste juste, il faut aussi des triggers AFTER DELETE (et AFTER UPDATE, si une intervention peut changer de ticket) ; et la cascade d'un ticket supprimé ne déclenche rien. C'est pourquoi on préfère souvent calculer ce nombre avec COUNT(*) plutôt que de le stocker.
Tu avais trouvé ?