Aller au contenu
Réussir mon BTS SIO

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

Par exemple

Chapitre 420 minAvancé

Fonction stockée MySQL : CREATE FUNCTION et RETURNS

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire une fonction stockée et l'utiliser dans une requête ;
  • déclarer ses caractéristiques (DETERMINISTIC, NO SQL, READS SQL DATA) ;
  • choisir entre une fonction et une procédure ;
  • connaître ce qu'une fonction n'a pas le droit de faire.

4 exercices corrigés

Une fonction stockée est une routine qui reçoit des paramètres et renvoie une seule valeur avec RETURN. Elle se crée avec CREATE FUNCTION nom(paramètres) RETURNS type et s'utilise directement dans une requête, comme une fonction native de MySQL (UPPER(), NOW(), DATEDIFF()). Ce chapitre montre comment l'écrire, comment déclarer ses caractéristiques (DETERMINISTIC, READS SQL DATA) et ce qu'elle n'a pas le droit de faire.

Ta première fonction

Le temps passé sur un ticket, c'est la somme des durées de ses interventions. On en a besoin dans plusieurs requêtes : on l'écrit une fois.

sql
DELIMITER //
CREATE FUNCTION duree_totale_ticket(p_id_ticket INT) RETURNS INT
READS SQL DATA
BEGIN
  DECLARE v_total INT;

  SELECT COALESCE(SUM(duree_min), 0) INTO v_total
  FROM intervention
  WHERE id_ticket = p_id_ticket;

  RETURN v_total;
END //
DELIMITER ;

Elle s'utilise partout où une expression est permise : dans la liste du SELECT, dans un WHERE, un ORDER BY, un SET.

sql
SELECT id_ticket, titre, duree_totale_ticket(id_ticket) AS minutes
FROM ticket
WHERE statut = 'resolu' AND duree_totale_ticket(id_ticket) >= 120
ORDER BY minutes DESC;
id_tickettitreminutes
9Rançongiciel suspecté570
5Odoo très lent210

La syntaxe complète :

sql
CREATE FUNCTION [IF NOT EXISTS] nom_fonction (parametre TYPE, …)
RETURNS type_retour
[DETERMINISTIC | NOT DETERMINISTIC]
[NO SQL | READS SQL DATA | CONTAINS SQL | MODIFIES SQL DATA]
[SQL SECURITY { DEFINER | INVOKER }]
[COMMENT 'texte']
BEGIN
  …
  RETURN expression;
END

Trois différences avec une procédure : les paramètres n'ont pas de mode (ils sont toujours en entrée), la clause RETURNS (avec un S) donne le type renvoyé, et le corps doit exécuter une instruction RETURN (sans S) sur chaque chemin. Une fonction qui atteint son END sans RETURN échoue à l'exécution avec l'erreur 1321, FUNCTION … ended without RETURN.

Procédure ou fonction ?

CritèreProcédureFonction
CréationCREATE PROCEDURECREATE FUNCTION … RETURNS type
AppelCALL nom(…), seuleDans une expression : SELECT nom(…), WHERE nom(…) > 10
Ce qu'elle renvoieDes paramètres OUT, et des jeux de résultatsUne seule valeur, par RETURN
ParamètresIN, OUT, INOUTToujours en entrée
Transactions (COMMIT, ROLLBACK)AutoriséesInterdites
SQL dynamique (PREPARE)AutoriséInterdit
Usage typiqueAgir : clôturer, installer, archiverCalculer : une durée, un libellé, un taux

Règle simple : si tu as besoin de la valeur dans une requête, c'est une fonction ; si tu agis sur les données ou renvoies plusieurs choses, c'est une procédure.

DETERMINISTIC, NO SQL, READS SQL DATA : les caractéristiques

Une fonction déclare deux informations sur elle-même.

Est-elle déterministe ? DETERMINISTIC signifie : « avec les mêmes paramètres, je renvoie toujours le même résultat ». Une fonction qui ne fait qu'un calcul sur ses paramètres l'est ; une fonction qui lit CURDATE(), NOW(), RAND() ou le contenu d'une table ne l'est pas, puisque le résultat change avec le temps ou les données. Par défaut, une routine est NOT DETERMINISTIC.

Que fait-elle des données ?

CaractéristiqueSens
NO SQLAucune instruction SQL sur les données : un pur calcul
READS SQL DATALit des tables, sans les modifier
MODIFIES SQL DATAModifie des données
CONTAINS SQL (par défaut)Contient du SQL, mais ni lecture ni écriture de tables (SET @x = 1)

Pourquoi est-ce important ? Parce que MySQL 8.4 active par défaut le journal binaire (binary log), qui sert à la réplication et à la restauration à un instant donné (cours SQL). Une fonction qui donnerait un résultat différent sur un serveur répliqué rendrait les copies incohérentes. MySQL exige donc qu'une fonction déclare au moins une des caractéristiques DETERMINISTIC, NO SQL ou READS SQL DATA :

sql
DELIMITER //
CREATE FUNCTION f_sans_caracteristique(p_id INT) RETURNS INT
BEGIN
  RETURN p_id * 2;
END //
DELIMITER ;
text
ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its
declaration and binary logging is enabled (you *might* want to use the less safe
log_bin_trust_function_creators variable)

Trois exemples justes :

sql
DELIMITER //
-- Un pur calcul : déterministe, sans SQL. 570 minutes donnent '9 h 30'.
CREATE FUNCTION libelle_duree(p_minutes INT) RETURNS VARCHAR(20)
DETERMINISTIC NO SQL
BEGIN
  IF p_minutes IS NULL THEN
    RETURN NULL;
  END IF;
  RETURN CONCAT(p_minutes DIV 60, ' h ', LPAD(p_minutes MOD 60, 2, '0'));
END //

-- Dépend de la date du jour : non déterministe, mais sans SQL
CREATE FUNCTION anciennete_ans(p_date DATE) RETURNS INT
NOT DETERMINISTIC NO SQL
BEGIN
  RETURN TIMESTAMPDIFF(YEAR, p_date, CURDATE());
END //
DELIMITER ;

-- duree_totale_ticket, plus haut, lit une table : READS SQL DATA

Les fonctions se combinent :

sql
SELECT id_ticket, titre, libelle_duree(duree_totale_ticket(id_ticket)) AS temps_passe
FROM ticket
WHERE id_ticket IN (1, 5, 9);
id_tickettitretemps_passe
1Écran noir au démarrage1 h 05
5Odoo très lent3 h 30
9Rançongiciel suspecté9 h 30

Une fonction qui modifie des données et le déclare honnêtement (MODIFIES SQL DATA, NOT DETERMINISTIC) ne coche aucune des trois cases exigées : elle est refusée avec l'erreur 1418, sauf à activer la variable log_bin_trust_function_creators. Cette variable lève le contrôle pour tout le serveur, demande un compte administrateur et ne se règle pas chez un hébergeur mutualisé. Le journal binaire impose aussi une condition au créateur : sans le privilège SUPER (ou cette variable à 1), créer une fonction échoue avec l'erreur 1419, You do not have the SUPER privilege and binary logging is enabled. Tu la rencontreras sur un serveur partagé où ton compte n'est pas administrateur. Le refus de l'erreur 1418 est un bon signal : une fonction ne devrait pas modifier les données. Si un calcul doit écrire, écris une procédure.

Des fonctions utiles pour la base parc

Les jours ouvrés d'attente. La procédure du chapitre 3 devient une fonction, utilisable dans une requête :

sql
DELIMITER //
CREATE FUNCTION jours_ouvres(p_debut DATE, p_fin DATE) RETURNS INT
DETERMINISTIC NO SQL
BEGIN
  DECLARE v_jour DATE DEFAULT p_debut;
  DECLARE v_nb   INT DEFAULT 0;

  WHILE v_jour <= p_fin DO
    IF DAYOFWEEK(v_jour) NOT IN (1, 7) THEN   -- ni dimanche (1) ni samedi (7)
      SET v_nb = v_nb + 1;
    END IF;
    SET v_jour = v_jour + INTERVAL 1 DAY;
  END WHILE;

  RETURN v_nb;
END //
DELIMITER ;

SELECT id_ticket, titre, DATE(date_ouverture) AS ouvert_le,
       jours_ouvres(DATE(date_ouverture), '2026-11-06') AS jours_ouvres_attente
FROM ticket
WHERE statut <> 'resolu'
ORDER BY id_ticket;
id_tickettitreouvert_lejours_ouvres_attente
6Ventilateur bruyant2026-09-2234
7Installer Sage pour Paul Martin2026-09-2333
11Accès VPN impossible2026-10-1915
12Clavier cassé2026-10-2113
13Lenteurs réseau à l'entrepôt2026-10-2610
14Batterie à remplacer2026-10-279

Elle est déterministe : deux dates données produisent toujours le même nombre. C'est la requête qui fixe la date de fin (ici le vendredi 6 novembre 2026) ; avec CURDATE() à la place, la requête change de résultat chaque jour, mais pas la fonction.

Le prochain numéro d'inventaire. La procédure ajouter_postes_stock du chapitre 3 calculait le numéro suivant ; ce calcul mérite sa fonction, que le formulaire de saisie peut afficher :

sql
DELIMITER //
CREATE FUNCTION prochain_num_inventaire() RETURNS CHAR(7)
READS SQL DATA
BEGIN
  DECLARE v_max INT;
  SELECT MAX(CAST(SUBSTRING(num_inventaire, 4) AS UNSIGNED)) INTO v_max FROM poste;
  RETURN CONCAT('PC-', LPAD(COALESCE(v_max, 0) + 1, 4, '0'));
END //
DELIMITER ;

SELECT prochain_num_inventaire();   -- PC-0016

Ce qu'une fonction n'a pas le droit de faire

InterditErreur
Renvoyer un jeu de résultats (SELECT sans INTO, SHOW)1415, Not allowed to return a result set from a function, à la création
Valider ou annuler une transaction (COMMIT, ROLLBACK, START TRANSACTION, et toute instruction du LDD qui valide implicitement)1422, Explicit or implicit commit is not allowed in stored function or trigger, à la création
Exécuter du SQL dynamique (PREPARE, EXECUTE)1336, à la création
S'appeler elle-même (récursivité)1424, Recursive stored functions and triggers are not allowed, à l'exécution
Modifier une table que la requête appelante utilise déjà1442, à l'exécution (même erreur que pour les triggers)
Servir dans une colonne générée, une contrainte CHECK ou une valeur DEFAULT3763 pour une colonne générée, à la création de la colonne

La dernière ligne surprend souvent : on ne peut pas écrire ALTER TABLE ticket ADD COLUMN minutes INT AS (duree_totale_ticket(id_ticket)). Une colonne générée ne peut utiliser que des fonctions natives déterministes ; pour stocker un calcul qui dépend d'autres tables, on utilise un trigger qui tient la colonne à jour (chapitre 8).

Fonction et performance

Une fonction est appelée une fois par ligne. SELECT duree_totale_ticket(id_ticket) FROM ticket exécute 14 fois la requête de la fonction ; sur 100 000 tickets, 100 000 fois. Et une condition WHERE ma_fonction(colonne) = valeur empêche d'utiliser un index sur la colonne : toute la table est lue.

La même information s'obtient en une seule requête ensembliste :

sql
SELECT t.id_ticket, t.titre, COALESCE(SUM(i.duree_min), 0) AS minutes
FROM ticket t
LEFT JOIN intervention i ON i.id_ticket = t.id_ticket
GROUP BY t.id_ticket, t.titre;

Réserve donc les fonctions qui lisent des tables aux petits volumes, ou aux calculs que tu veux absolument écrire une seule fois. Les fonctions de pur calcul (libelle_duree, jours_ouvres) ne posent pas ce problème.

Gérer ses fonctions

sql
SHOW FUNCTION STATUS WHERE Db = 'parc';
SHOW CREATE FUNCTION libelle_duree;
DROP FUNCTION IF EXISTS libelle_duree;

SELECT routine_name, is_deterministic, sql_data_access
FROM information_schema.routines
WHERE routine_schema = 'parc' AND routine_type = 'FUNCTION';
ROUTINE_NAMEIS_DETERMINISTICSQL_DATA_ACCESS
anciennete_ansNONO SQL
duree_totale_ticketNOREADS SQL DATA
jours_ouvresYESNO SQL
libelle_dureeYESNO SQL
prochain_num_inventaireNOREADS SQL DATA

Une fonction et une procédure peuvent porter le même nom (ce sont deux espaces de noms distincts), mais évite-le. Évite aussi les noms des fonctions natives (now, count) : MySQL ne te laisserait appeler la tienne qu'avec une espace avant la parenthèse.

Exercices

Exercice 1 sur 4 Le nombre de postes d'un employé Réussi À revoir

Écris une fonction nb_postes(p_id_employe INT) qui renvoie le nombre de postes affectés à un employé. Utilise-la pour lister les employés qui n'ont pas exactement un poste, puis réécris la même requête sans fonction.

Voir la réponseMasquer la réponse de l'exercice 1
sql
DELIMITER //
CREATE FUNCTION nb_postes(p_id_employe INT) RETURNS INT
READS SQL DATA
BEGIN
  DECLARE v_nb INT;
  SELECT COUNT(*) INTO v_nb FROM poste WHERE id_employe = p_id_employe;
  RETURN v_nb;
END //
DELIMITER ;

SELECT nom, prenom, nb_postes(id_employe) AS postes
FROM employe
WHERE nb_postes(id_employe) <> 1
ORDER BY nom;
nomprenompostes
BenaliSofiane2
BonnetInès0
MartinPaul0

Sans fonction, une jointure externe et un regroupement :

sql
SELECT e.nom, e.prenom, COUNT(p.id_poste) AS postes
FROM employe e
LEFT JOIN poste p ON p.id_employe = e.id_employe
GROUP BY e.id_employe, e.nom, e.prenom
HAVING COUNT(p.id_poste) <> 1
ORDER BY e.nom;

COUNT(p.id_poste) compte 0 pour un employé sans poste, alors que COUNT(*) compterait la ligne ajoutée par le LEFT JOIN. La version avec fonction exécute la requête de nb_postes 17 fois : 14 fois dans le WHERE, une par employé, puis 3 fois dans la liste du SELECT, une par ligne retenue. La version avec jointure n'envoie qu'une seule requête.

Tu avais trouvé ?

Exercice 2 sur 4 Le taux d'utilisation des licences Réussi À revoir

Écris une fonction taux_utilisation(p_id_logiciel INT) RETURNS DECIMAL(5,1) qui renvoie le pourcentage de licences utilisées (installations × 100 / licences), ou NULL pour un logiciel libre. Quel résultat donne la requête SELECT nom, taux_utilisation(id_logiciel) AS taux FROM logiciel WHERE nb_licences IS NOT NULL ORDER BY taux DESC; ?

Voir la réponseMasquer la réponse de l'exercice 2
sql
DELIMITER //
CREATE FUNCTION taux_utilisation(p_id_logiciel INT) RETURNS DECIMAL(5,1)
READS SQL DATA
BEGIN
  DECLARE v_licences   INT;
  DECLARE v_installees INT;

  SELECT nb_licences INTO v_licences FROM logiciel WHERE id_logiciel = p_id_logiciel;
  IF v_licences IS NULL OR v_licences = 0 THEN
    RETURN NULL;
  END IF;

  SELECT COUNT(*) INTO v_installees FROM installation WHERE id_logiciel = p_id_logiciel;
  RETURN v_installees * 100 / v_licences;
END //
DELIMITER ;
nomtaux
Odoo87.5
Microsoft 36583.3
Sage 100 Comptabilité66.7
Acrobat Pro0.0

Odoo : 7 installations pour 8 licences ; Microsoft 365 : 10 pour 12 (83,33 arrondi à 83,3 par le type DECIMAL(5,1)) ; Sage : 2 pour 3 ; Acrobat Pro : aucune installation. Le test v_licences = 0 évite une division par zéro.

Tu avais trouvé ?

Exercice 3 sur 4 Les bonnes caractéristiques Réussi À revoir

Pour chaque fonction, donne la déclaration qui convient : DETERMINISTIC ou NOT DETERMINISTIC, et NO SQL ou READS SQL DATA.

  1. ttc(p_ht DECIMAL(8,2)) qui renvoie p_ht * 1.2.
  2. nb_tickets_ouverts() qui compte les tickets non résolus.
  3. delai_depasse(p_date_ouverture DATETIME, p_heures INT) qui renvoie vrai si NOW() dépasse la date d'ouverture plus le délai.
  4. initiales(p_prenom VARCHAR(50), p_nom VARCHAR(50)) qui renvoie 'C. L.' pour Chloé Lambert.
Voir la réponseMasquer la réponse de l'exercice 3
  1. DETERMINISTIC NO SQL : un calcul sur le paramètre.
  2. NOT DETERMINISTIC READS SQL DATA : le résultat change avec le contenu de la table.
  3. NOT DETERMINISTIC NO SQL : aucune table lue, mais NOW() change à chaque appel. Pour la rendre déterministe et plus facile à tester, passe la date de référence en paramètre au lieu d'appeler NOW().
  4. DETERMINISTIC NO SQL.

Tu avais trouvé ?

Exercice 4 sur 4 Trouve l'erreur Réussi À revoir

Cette fonction se crée sans erreur. Pourquoi l'appel SELECT categorie_prix(1249); échoue-t-il, alors que categorie_prix(600) répond 'entrée' ? Corrige-la.

sql
DELIMITER //
CREATE FUNCTION categorie_prix(p_prix DECIMAL(8,2)) RETURNS VARCHAR(10)
DETERMINISTIC NO SQL
BEGIN
  IF p_prix < 700 THEN
    RETURN 'entrée';
  ELSEIF p_prix < 1150 THEN
    RETURN 'milieu';
  END IF;
END //
DELIMITER ;
Voir la réponseMasquer la réponse de l'exercice 4

Pour un prix d'au moins 1 150, aucune branche ne s'exécute et la fonction atteint son END sans RETURN : erreur 1321, FUNCTION categorie_prix ended without RETURN. MySQL ne le détecte qu'à l'exécution, sur la valeur qui emprunte ce chemin. Correction : ajouter une branche ELSE RETURN 'haut de gamme';, ou un RETURN final après le END IF. Pense aussi au prix NULL : aucune comparaison n'est vraie, il faut une branche qui le traite.

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 : Curseur SQL : parcourir un résultat ligne par ligneRevoir le sommaire du cours