Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 NEW et OLD ;
  • 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

sql
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 : BEFORE s'exécute avant l'écriture de la ligne, AFTER après. Avec trois événements, cela fait six combinaisons possibles par table.
  • FOR EACH ROW est obligatoire en MySQL : le trigger s'exécute une fois par ligne. Un UPDATE qui modifie dix tickets déclenche dix fois le trigger.
  • Le nom est unique dans la base. Une convention lisible : trg_<table>_<rôle>, comme trg_intervention_technicien.
  • Créer un trigger demande le privilège TRIGGER sur la table.

NEW et OLD

ÉvénementOLDNEW
INSERTN'existe pas (erreur 1363 si tu l'utilises)La ligne insérée
UPDATELa ligne avant la modificationLa ligne après la modification
DELETELa ligne suppriméeN'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).

MomentCe qu'il peut faireUsage
BEFORELire et modifier NEW, ou refuser la ligne avec SIGNALContrôler, normaliser, compléter la ligne
AFTERLire NEW et OLD, modifier d'autres tablesHistoriser, auditer, propager vers une autre table
UPDATE ticket … : ce qui se passe pour chaque ligneUPDATEticketBEFORE UPDATElit OLD, corrige NEWou refuse (SIGNAL)Écriturede la ligne, puisclés et CHECKAFTER UPDATElit OLD et NEW,écrit dans audit_ticketrépété pour chaque ligne modifiéeUne erreur, à n'importe quelle étape : toute l'instruction est annulée
Pour chaque ligne : BEFORE (contrôler, corriger), écriture et contraintes, AFTER (tracer, propager)

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 :

sql
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.

sql
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 :

sql
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_tickettitreprioritestatut
15Rançongiciel sur le poste de la comptacritiqueouvert

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.

sql
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 » :

sql
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 :

text
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.

InstructionTriggers déclenchés
INSERT, LOAD DATABEFORE INSERT, AFTER INSERT
UPDATEBEFORE UPDATE, AFTER UPDATE
DELETEBEFORE DELETE, AFTER DELETE
INSERT … ON DUPLICATE KEY UPDATE sur une ligne existanteBEFORE INSERT, puis BEFORE UPDATE et AFTER UPDATE
REPLACE sur une ligne existanteBEFORE INSERT, BEFORE DELETE, AFTER DELETE, AFTER INSERT
TRUNCATE TABLE, DROP TABLEAucun
Cascade ON DELETE / ON UPDATE d'une clé étrangèreAucun 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

sql
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_NAMEevenementtable_ciblemomentordre
trg_intervention_technicienINSERTinterventionBEFORE1
trg_intervention_dateINSERTinterventionBEFORE2
trg_intervention_prise_en_chargeINSERTinterventionAFTER1
trg_ticket_avant_ajoutINSERTticketBEFORE1
trg_ticket_clotureUPDATEticketBEFORE1

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.

sql
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
sql
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 ?

sql
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.

sql
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
  1. SET NEW.duree_min = … dans un trigger AFTER : erreur 1362 à la création. La ligne est déjà écrite. (Cette ligne ne sert d'ailleurs à rien.)
  2. OLD.id_ticket dans un trigger INSERT : il n'y a pas de ligne ancienne, erreur 1363. Il faut NEW.id_ticket.

Corrigé, avec la colonne qu'il suppose et une seule instruction dans le trigger :

sql
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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Trigger d'audit et d'historique : tracer les modificationsRevoir le sommaire du cours