Aller au contenu
Réussir mon BTS SIO

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

Par exemple

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 SET ou SELECT … INTO ;
  • écrire une condition avec IF ou CASE ;
  • écrire une boucle WHILE, REPEAT ou LOOP, 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ÉcritureDéclarationDurée de vieExemple
Variable localev_totalObligatoire : DECLARE v_total INT DEFAULT 0;Le bloc BEGIN … END qui la déclareUn compteur dans une boucle
Variable de session (ou utilisateur)@totalAucune : elle existe dès qu'on lui affecte une valeur, et vaut NULL avantLa connexionRécupérer un paramètre OUT après un CALL
Variable système@@autocommitDéfinie par le serveurLe serveur ou la sessionLire 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

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

  1. Les DECLARE se placent au début d'un bloc BEGIN … END, avant toute autre instruction. Un DECLARE après un SET provoque l'erreur 1064.
  2. Leur ordre est imposé : d'abord les variables et les conditions, puis les curseurs (chapitre 5), enfin les gestionnaires d'erreurs (chapitre 6).
  3. Une variable n'existe que dans le bloc qui la déclare et dans les blocs imbriqués. On peut ouvrir un bloc BEGIN … END au 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 :

sql
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 un LIMIT 1 avec un ORDER 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 avec DEFAULT '(aucun)' vaut toujours '(aucun)' après un SELECT … INTO qui ne trouve pas le ticket 99 ; sans DEFAULT, elle vaut NULL.

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

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

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

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

sql
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_inventairemarquemodeleid_employe
PC-0015HPProDesk 4004
PC-0016DellLatitude 5450NULL
PC-0017DellLatitude 5450NULL
PC-0018DellLatitude 5450NULL

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

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

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

sql
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
sql
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 … INTO ne trouve rien, v_age reste NULL, 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.

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

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

sql
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_iv_i > 7 ?multiple de 3 ?p_r
1nonnon1
2nonnon3
3nonoui, on saute3
4nonnon7
5nonnon12
6nonoui, on saute12
7nonnon19
8oui, on sort19

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

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 : Fonction stockée MySQL : CREATE FUNCTION et RETURNSRevoir le sommaire du cours