Chapitre 321 minAvancé
Variables, IF, CASE et boucles WHILE en SQL procédural
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- déclarer des variables locales et les remplir avec
SETouSELECT … INTO; - écrire une condition avec
IFouCASE; - écrire une boucle
WHILE,REPEATouLOOP, et en sortir proprement ; - reconnaître une boucle qu'une seule requête remplacerait.
4 exercices corrigés
Le SQL procédural de MySQL ajoute au SQL classique ce qu'on trouve dans tout langage de programmation : des variables (DECLARE, SET), des conditions (IF, CASE) et des boucles (WHILE, REPEAT, LOOP). Ces instructions ne s'utilisent qu'à l'intérieur d'un programme stocké. Ce chapitre les présente sur la base parc et se termine par la question qui compte le plus : cette boucle ne serait-elle pas mieux écrite en une seule requête ?
Trois sortes de variables
| Sorte | Écriture | Déclaration | Durée de vie | Exemple |
|---|---|---|---|---|
| Variable locale | v_total | Obligatoire : DECLARE v_total INT DEFAULT 0; | Le bloc BEGIN … END qui la déclare | Un compteur dans une boucle |
| Variable de session (ou utilisateur) | @total | Aucune : elle existe dès qu'on lui affecte une valeur, et vaut NULL avant | La connexion | Récupérer un paramètre OUT après un CALL |
| Variable système | @@autocommit | Définie par le serveur | Le serveur ou la session | Lire ou changer un réglage |
Dans une routine, utilise des variables locales : elles sont typées, elles disparaissent à la fin, et elles ne risquent pas d'entrer en conflit avec une autre routine. Une variable de session partagée par plusieurs routines est une variable globale déguisée, source de bogues. Elle a pourtant un usage légitime, que tu verras au chapitre 9 : transmettre une information de l'application à un trigger.
DECLARE et la portée d'un bloc
DECLARE v_minutes INT DEFAULT 0;
DECLARE v_titre VARCHAR(100); -- sans DEFAULT : NULL
DECLARE v_debut, v_fin DATE; -- plusieurs variables du même type
Trois règles, que le serveur vérifie à la création :
- Les
DECLAREse placent au début d'un blocBEGIN … END, avant toute autre instruction. UnDECLAREaprès unSETprovoque l'erreur 1064. - Leur ordre est imposé : d'abord les variables et les conditions, puis les curseurs (chapitre 5), enfin les gestionnaires d'erreurs (chapitre 6).
- Une variable n'existe que dans le bloc qui la déclare et dans les blocs imbriqués. On peut ouvrir un bloc
BEGIN … ENDau milieu d'une procédure pour déclarer des variables qui ne servent qu'à un endroit.
Affecter une valeur : SET et SELECT … INTO
SET affecte le résultat d'une expression, SELECT … INTO celui d'une requête :
SET v_debut = '2026-10-01', v_fin = v_debut + INTERVAL 1 MONTH;
SELECT COUNT(*), COALESCE(SUM(duree_min), 0)
INTO v_nb, v_minutes
FROM intervention
WHERE id_technicien = 12;
SELECT … INTO exige une requête qui renvoie au plus une ligne, et autant de variables que de colonnes. Deux cas posent problème :
- Plusieurs lignes : erreur 1172,
Result consisted of more than one row. Julie Lefèvre a ouvert deux tickets :SELECT titre INTO v_titre FROM ticket WHERE id_demandeur = 3échoue. Ajoute une condition plus précise, un agrégat ou unLIMIT 1avec unORDER BY. - Aucune ligne : pas d'erreur, mais un avertissement 1329 (
No data - zero rows fetched, selected, or processed), et la variable garde sa valeur précédente. Une variable déclarée avecDEFAULT '(aucun)'vaut toujours'(aucun)'après unSELECT … INTOqui ne trouve pas le ticket 99 ; sansDEFAULT, elle vautNULL.
IF, ELSEIF, ELSE
Le service veut classer chaque ticket selon le temps qu'il a demandé : « non traité » sans intervention, « rapide » sous l'heure, « normal » sous quatre heures, « lourd » au-delà.
DELIMITER //
CREATE PROCEDURE classer_ticket(IN p_id_ticket INT, OUT p_charge VARCHAR(12))
BEGIN
DECLARE v_minutes INT;
SELECT COALESCE(SUM(duree_min), 0) INTO v_minutes
FROM intervention
WHERE id_ticket = p_id_ticket;
IF v_minutes = 0 THEN
SET p_charge = 'non traité';
ELSEIF v_minutes < 60 THEN
SET p_charge = 'rapide';
ELSEIF v_minutes < 240 THEN
SET p_charge = 'normal';
ELSE
SET p_charge = 'lourd';
END IF;
END //
DELIMITER ;
CALL classer_ticket(9, @charge);
SELECT @charge; -- lourd (570 minutes)
Le ticket 3 (10 minutes) donne rapide, le ticket 1 (65 minutes) normal, le ticket 7 (aucune intervention) non traité. Les conditions sont testées dans l'ordre et seule la première vraie s'exécute : v_minutes < 240 n'est atteint que si v_minutes vaut au moins 60.
La syntaxe à retenir : IF condition THEN … ELSEIF condition THEN … ELSE … END IF;. Un mot, ELSEIF, sans espace (ELSE IF ouvrirait un second IF qu'il faudrait refermer). Et un END IF; à la fin, avec son point-virgule.
CASE
L'instruction CASE choisit une branche selon une valeur. Le délai de traitement visé dépend de la priorité :
DELIMITER //
CREATE PROCEDURE delai_cible(IN p_priorite VARCHAR(10), OUT p_heures INT)
BEGIN
CASE p_priorite
WHEN 'critique' THEN SET p_heures = 4;
WHEN 'haute' THEN SET p_heures = 8;
WHEN 'normale' THEN SET p_heures = 24;
WHEN 'basse' THEN SET p_heures = 72;
END CASE;
END //
DELIMITER ;
CALL delai_cible('haute', @h); -- @h vaut 8
CALL delai_cible('urgente', @h);
-- ERROR 1339 (20000): Case not found for CASE statement
Sans branche ELSE, une valeur qui ne correspond à aucun WHEN provoque l'erreur 1339. Ajoute toujours un ELSE, quitte à y lever ta propre erreur (chapitre 6). La seconde forme, CASE WHEN condition THEN … END CASE, teste des conditions quelconques, comme un IF.
Ne confonds pas l'instruction CASE … END CASE, qui exécute des instructions, et l'expression CASE … END que tu connais dans les requêtes (fonctions SQL), qui renvoie une valeur. L'expression marche aussi dans une routine, et souvent plus simplement :
SET p_heures = CASE p_priorite WHEN 'critique' THEN 4 WHEN 'haute' THEN 8
WHEN 'normale' THEN 24 ELSE 72 END;
WHILE
WHILE répète tant qu'une condition est vraie, et la teste avant chaque tour : si elle est fausse au départ, le corps ne s'exécute jamais. Le service reçoit trois portables neufs et veut les enregistrer en stock avec des numéros d'inventaire qui suivent le dernier :
DELIMITER //
CREATE PROCEDURE ajouter_postes_stock(
IN p_nb INT,
IN p_type VARCHAR(10),
IN p_marque VARCHAR(30),
IN p_modele VARCHAR(50),
IN p_prix DECIMAL(8,2)
)
BEGIN
DECLARE v_i INT DEFAULT 1;
DECLARE v_numero INT;
-- Le plus grand numéro existant : 'PC-0015' donne 15
SELECT MAX(CAST(SUBSTRING(num_inventaire, 4) AS UNSIGNED)) INTO v_numero
FROM poste;
WHILE v_i <= p_nb DO
SET v_numero = v_numero + 1;
INSERT INTO poste (num_inventaire, type_poste, marque, modele, date_achat, prix_achat)
VALUES (CONCAT('PC-', LPAD(v_numero, 4, '0')), p_type, p_marque, p_modele, CURDATE(), p_prix);
SET v_i = v_i + 1;
END WHILE;
END //
DELIMITER ;
CALL ajouter_postes_stock(3, 'portable', 'Dell', 'Latitude 5450', 1149.00);
SELECT num_inventaire, marque, modele, id_employe FROM poste WHERE id_poste >= 15;
| num_inventaire | marque | modele | id_employe |
|---|---|---|---|
| PC-0015 | HP | ProDesk 400 | 4 |
| PC-0016 | Dell | Latitude 5450 | NULL |
| PC-0017 | Dell | Latitude 5450 | NULL |
| PC-0018 | Dell | Latitude 5450 | NULL |
LPAD(16, 4, '0') complète à gauche avec des zéros : 0016. Les nouveaux postes n'ont pas d'utilisateur : id_employe reste NULL, ils sont en stock. Pour revenir à la base de départ : DELETE FROM poste WHERE id_poste > 15;.
REPEAT
REPEAT … UNTIL condition END REPEAT; teste la condition après chaque tour : le corps s'exécute au moins une fois, et la boucle s'arrête quand la condition devient vraie (l'inverse de WHILE).
DECLARE v_i INT DEFAULT 0;
REPEAT
SET v_i = v_i + 1;
UNTIL v_i >= p_n
END REPEAT;
Avec p_n = 0, v_i vaut 1 à la sortie : le corps a tourné une fois avant le premier test. Pas de point-virgule après la condition de UNTIL.
LOOP, LEAVE et ITERATE
LOOP répète sans condition : on en sort avec LEAVE étiquette, et on passe directement au tour suivant avec ITERATE étiquette (l'équivalent de break et continue). La boucle porte une étiquette, un nom suivi de deux-points, qu'on répète après END LOOP.
Le délai d'un ticket se compte en jours ouvrés. Cette procédure compte les jours du lundi au vendredi entre deux dates, bornes comprises :
DELIMITER //
CREATE PROCEDURE jours_ouvres(IN p_debut DATE, IN p_fin DATE, OUT p_nb INT)
BEGIN
DECLARE v_jour DATE DEFAULT p_debut - INTERVAL 1 DAY;
SET p_nb = 0;
parcours: LOOP
SET v_jour = v_jour + INTERVAL 1 DAY;
IF v_jour > p_fin THEN
LEAVE parcours; -- sortie de la boucle
END IF;
IF DAYOFWEEK(v_jour) IN (1, 7) THEN -- 1 = dimanche, 7 = samedi
ITERATE parcours; -- on ne compte pas ce jour
END IF;
SET p_nb = p_nb + 1;
END LOOP parcours;
END //
DELIMITER ;
CALL jours_ouvres('2026-10-19', '2026-11-08', @n);
SELECT @n; -- 15 : trois semaines du lundi 19 octobre au dimanche 8 novembre
Les jours fériés ne sont pas déduits : il faudrait une table des jours fériés et un IF EXISTS (SELECT 1 FROM jour_ferie WHERE …) de plus. LEAVE sert aussi à quitter un bloc BEGIN … END étiqueté, et donc à sortir d'une procédure avant la fin : fin: BEGIN … LEAVE fin; … END fin.
Boucle ou requête ?
SQL est un langage ensembliste : une requête traite toutes les lignes d'un coup, et le moteur choisit la meilleure façon de le faire. Une boucle qui exécute une requête par tour fait autant d'allers-retours avec le moteur. Pour trois postes, peu importe ; pour trente mille lignes, la différence se compte en minutes.
L'insertion des trois postes s'écrit d'ailleurs en une seule requête, avec une CTE récursive qui génère les nombres 1, 2 et 3 :
INSERT INTO poste (num_inventaire, type_poste, marque, modele, date_achat, prix_achat)
WITH RECURSIVE n (i) AS (
SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 3
)
SELECT CONCAT('PC-', LPAD(15 + i, 4, '0')), 'portable', 'Dell', 'Latitude 5450', CURDATE(), 1149.00
FROM n;
Garde les boucles pour ce qu'aucune requête ne fait bien : un calcul itératif (les jours ouvrés), un traitement qui dépend du tour précédent, un appel de procédure pour chaque ligne, une requête construite dynamiquement pour chaque table (chapitre 5).
Exercices
Exercice 1 sur 4 Classer un poste Réussi À revoir
Écris une procédure etat_poste(IN p_id_poste INT, IN p_date_ref DATE, OUT p_etat VARCHAR(15)) qui renvoie 'récent' si le poste a moins de 3 ans à la date de référence, 'à surveiller' s'il a de 3 à 5 ans, 'à renouveler' au-delà, et 'inconnu' si le poste n'existe pas. L'âge se calcule avec TIMESTAMPDIFF(YEAR, date_achat, p_date_ref). Donne le résultat au 8 novembre 2026 pour les postes 1, 4, 10 et 99.
Voir la réponseMasquer la réponse de l'exercice 1
DELIMITER //
CREATE PROCEDURE etat_poste(IN p_id_poste INT, IN p_date_ref DATE, OUT p_etat VARCHAR(15))
BEGIN
DECLARE v_age INT;
SELECT TIMESTAMPDIFF(YEAR, date_achat, p_date_ref) INTO v_age
FROM poste
WHERE id_poste = p_id_poste;
IF v_age IS NULL THEN
SET p_etat = 'inconnu';
ELSEIF v_age < 3 THEN
SET p_etat = 'récent';
ELSEIF v_age <= 5 THEN
SET p_etat = 'à surveiller';
ELSE
SET p_etat = 'à renouveler';
END IF;
END //
DELIMITER ;
CALL etat_poste(1, '2026-11-08', @e); SELECT @e;
- Poste 1, acheté le 14 février 2019 : 7 ans,
à renouveler. - Poste 4, acheté le 3 juin 2021 : 5 ans,
à surveiller. - Poste 10, acheté le 18 mars 2024 : 2 ans,
récent. - Poste 99 : le
SELECT … INTOne trouve rien,v_ageresteNULL,inconnu.
Le test v_age IS NULL vient en premier : sans lui, aucune des conditions suivantes n'est vraie avec un NULL, et le ELSE répondrait à renouveler pour un poste qui n'existe pas.
Tu avais trouvé ?
Exercice 2 sur 4 Corriger un CASE Réussi À revoir
Cette procédure plante pour un ticket au statut inattendu. Dis pourquoi, puis réécris le calcul en une seule instruction SET avec une expression CASE.
CASE p_statut
WHEN 'ouvert' THEN SET p_libelle = 'En attente';
WHEN 'en_cours' THEN SET p_libelle = 'En cours de traitement';
WHEN 'resolu' THEN SET p_libelle = 'Résolu';
END CASE;
Voir la réponseMasquer la réponse de l'exercice 2
Une instruction CASE sans ELSE lève l'erreur 1339 (Case not found for CASE statement) quand aucune branche ne correspond, par exemple pour p_statut à NULL. Réécriture :
SET p_libelle = CASE p_statut
WHEN 'ouvert' THEN 'En attente'
WHEN 'en_cours' THEN 'En cours de traitement'
WHEN 'resolu' THEN 'Résolu'
ELSE 'Statut inconnu'
END;
L'expression se termine par END, pas END CASE. Sans ELSE, elle renverrait NULL au lieu de lever une erreur.
Tu avais trouvé ?
Exercice 3 sur 4 Dérouler une boucle Réussi À revoir
Que vaut @r après ces instructions ? Déroule la boucle tour par tour.
DELIMITER //
CREATE PROCEDURE mystere(IN p_n INT, OUT p_r INT)
BEGIN
DECLARE v_i INT DEFAULT 0;
SET p_r = 0;
boucle: LOOP
SET v_i = v_i + 1;
IF v_i > p_n THEN
LEAVE boucle;
END IF;
IF v_i MOD 3 = 0 THEN
ITERATE boucle;
END IF;
SET p_r = p_r + v_i;
END LOOP boucle;
END //
DELIMITER ;
CALL mystere(7, @r);
Voir la réponseMasquer la réponse de l'exercice 3
@r vaut 19.
| v_i | v_i > 7 ? | multiple de 3 ? | p_r |
|---|---|---|---|
| 1 | non | non | 1 |
| 2 | non | non | 3 |
| 3 | non | oui, on saute | 3 |
| 4 | non | non | 7 |
| 5 | non | non | 12 |
| 6 | non | oui, on saute | 12 |
| 7 | non | non | 19 |
| 8 | oui, on sort | 19 |
La procédure additionne les entiers de 1 à 7 qui ne sont pas multiples de 3 : 1 + 2 + 4 + 5 + 7 = 19.
Tu avais trouvé ?
Exercice 4 sur 4 Quand les licences seront-elles épuisées ? Réussi À revoir
Microsoft 365 (id_logiciel 1) a 12 licences. Le service installe en moyenne 1 poste par mois. Écris une procédure mois_avant_rupture(IN p_id_logiciel INT, IN p_par_mois INT, OUT p_mois INT) qui calcule, avec une boucle WHILE, le nombre de mois entiers avant que toutes les licences soient utilisées. Quel résultat pour Microsoft 365 ? Comment l'obtenir sans boucle ?
Voir la réponseMasquer la réponse de l'exercice 4
DELIMITER //
CREATE PROCEDURE mois_avant_rupture(IN p_id_logiciel INT, IN p_par_mois INT, OUT p_mois INT)
BEGIN
DECLARE v_licences INT;
DECLARE v_installees INT;
SELECT nb_licences INTO v_licences FROM logiciel WHERE id_logiciel = p_id_logiciel;
SELECT COUNT(*) INTO v_installees FROM installation WHERE id_logiciel = p_id_logiciel;
SET p_mois = 0;
WHILE v_installees + p_par_mois <= v_licences DO
SET v_installees = v_installees + p_par_mois;
SET p_mois = p_mois + 1;
END WHILE;
END //
DELIMITER ;
CALL mois_avant_rupture(1, 1, @m);
SELECT @m; -- 2
Microsoft 365 est installé sur 10 postes (1 à 6, 10 à 12 et 15) : il reste 2 licences, soit 2 mois. Sans boucle, c'est une division entière : (nb_licences - installées) DIV p_par_mois. Pour un logiciel libre, v_licences est NULL : la condition du WHILE n'est jamais vraie et la procédure répond 0, ce qui est faux. Il faudrait tester v_licences IS NULL et renvoyer NULL (pas de limite).
Tu avais trouvé ?