Une fonction stockée est un petit programme SQL enregistré dans la base de données. Elle reçoit des paramètres, renvoie une seule valeur, et s'utilise dans une requête comme une fonction native (ROUND(), CONCAT()…). Elle existe dans MySQL depuis la version 5.0 et fonctionne toujours de la même façon dans MySQL 8 et MariaDB.
Créer une fonction stockée
Exemple : calculer un prix TTC à partir d'un prix HT et d'un taux de TVA.
DELIMITER //
CREATE FUNCTION prix_ttc(prix_ht DECIMAL(20,6), taux DECIMAL(10,3))
RETURNS DECIMAL(20,6)
DETERMINISTIC
NO SQL
BEGIN
RETURN ROUND(prix_ht * (1 + taux / 100), 6);
END //
DELIMITER ;
Et pour l'appeler :
SELECT prix_ttc(100, 20); -- 120.000000
Quelques points à connaître :
DELIMITER n'est pas du SQL : c'est une commande du client mysql en ligne de commande, qui lui évite de couper la fonction au premier ;. Dans phpMyAdmin, on règle le délimiteur dans le champ prévu sous la zone de requête. Depuis PHP (PDO, mysqli), on envoie l'instruction CREATE FUNCTION d'un seul bloc, sans DELIMITER.
DETERMINISTIC indique que la fonction renvoie toujours le même résultat pour les mêmes paramètres. Ne le déclarez que si c'est vrai.
NO SQL, READS SQL DATA, MODIFIES SQL DATA décrivent ce que la fonction fait sur les données. Ces déclarations comptent pour la réplication et pour l'erreur 1418 (voir plus bas).
Une fonction peut aussi lire des tables. Par exemple, compter les produits d'une catégorie PrestaShop :
DELIMITER //
CREATE FUNCTION nb_produits_categorie(p_id_category INT)
RETURNS INT
READS SQL DATA
BEGIN
DECLARE n INT;
SELECT COUNT(*) INTO n
FROM ps_category_product
WHERE id_category = p_id_category;
RETURN n;
END //
DELIMITER ;
Lister, afficher, supprimer
-- Les fonctions de la base courante
SHOW FUNCTION STATUS WHERE Db = DATABASE();
-- Le code d'une fonction
SHOW CREATE FUNCTION prix_ttc;
-- La supprimer (avant de la recréer, par exemple)
DROP FUNCTION IF EXISTS prix_ttc;
La table information_schema.ROUTINES donne les mêmes informations, utiles dans un script.
Fonction ou procédure stockée ?
|
Fonction stockée |
Procédure stockée |
| Résultat |
une seule valeur (RETURN) |
aucun, des paramètres OUT, ou des jeux de résultats |
| Appel |
dans une requête : SELECT prix_ttc(…) |
à part : CALL ma_procedure(…) |
SQL dynamique (PREPARE) |
interdit |
autorisé |
| Usage type |
un calcul réutilisable |
un traitement en plusieurs étapes |
Parcourir des lignes une à une : les curseurs
Une requête SQL traite un ensemble de lignes d'un coup. Quand un traitement doit agir ligne par ligne, avec une décision différente pour chacune, un programme stocké peut utiliser un curseur : il ouvre le résultat d'un SELECT et le lit ligne après ligne.
Exemple : une procédure qui archive les commandes de plus de trois ans, une par une.
DELIMITER //
CREATE PROCEDURE archiver_commandes()
MODIFIES SQL DATA
BEGIN
DECLARE fini INT DEFAULT 0;
DECLARE v_id INT;
DECLARE cur CURSOR FOR
SELECT id FROM commandes
WHERE date_commande < NOW() - INTERVAL 3 YEAR;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET fini = 1;
OPEN cur;
lecture: LOOP
FETCH cur INTO v_id;
IF fini THEN
LEAVE lecture;
END IF;
INSERT INTO commandes_archive SELECT * FROM commandes WHERE id = v_id;
DELETE FROM commandes WHERE id = v_id;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
CALL archiver_commandes();
Les règles à respecter :
- L'ordre des déclarations est imposé : d'abord les variables, puis les curseurs, puis les gestionnaires (
HANDLER). Dans le désordre, la création échoue.
- Le gestionnaire
NOT FOUND est indispensable : c'est lui qui signale la fin des lignes. Sans lui, le dernier FETCH déclenche l'erreur 1329 (« No data – zero rows fetched »).
OPEN, FETCH, CLOSE : le curseur s'ouvre, se lit, se ferme. FETCH remplit autant de variables que le SELECT renvoie de colonnes.
- Un curseur MySQL est en lecture seule et ne se lit que vers l'avant : impossible de revenir en arrière ou de modifier la ligne « sous » le curseur.
- Un curseur est utilisable dans une procédure, une fonction ou un déclencheur, mais pas dans une requête ordinaire.
Le plus souvent, une seule requête fait mieux
L'archivage ci-dessus s'écrit sans curseur, en deux requêtes qui traitent toutes les lignes d'un coup, avec la même date limite pour les deux :
SET @limite = NOW() - INTERVAL 3 YEAR;
START TRANSACTION;
INSERT INTO commandes_archive
SELECT * FROM commandes WHERE date_commande < @limite;
DELETE FROM commandes WHERE date_commande < @limite;
COMMIT;
Sur des milliers de lignes, la différence est nette : le curseur exécute deux requêtes par ligne, la version ensembliste deux requêtes en tout. Réservez les curseurs aux traitements où chaque ligne demande vraiment une logique propre, et vérifiez d'abord si un UPDATE … JOIN, un INSERT … SELECT ou une fonction de fenêtrage ne suffit pas.
Les erreurs les plus fréquentes
Erreur 1418 : « This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA… »
Le journal binaire (binlog) est actif : c'est le cas par défaut depuis MySQL 8. Le serveur refuse alors une fonction qui ne déclare pas ce qu'elle fait sur les données, car elle pourrait donner un résultat différent sur un serveur répliqué.
La bonne correction : déclarer honnêtement la fonction (DETERMINISTIC, NO SQL ou READS SQL DATA). L'autre solution, activer log_bin_trust_function_creators, demande des droits d'administration du serveur, que les hébergements mutualisés ne donnent généralement pas.
Erreur 1449 : « The user specified as a definer does not exist »
Chaque fonction garde le nom de l'utilisateur MySQL qui l'a créée (DEFINER). Après une migration vers un autre serveur, cet utilisateur n'existe souvent plus, et la fonction échoue à chaque appel. Recréez-la avec l'utilisateur du nouveau serveur, ou retirez la clause DEFINER=… du fichier d'export avant de l'importer.
« Access denied » ou « command denied » à la création ou à l'appel
Créer une fonction demande le droit CREATE ROUTINE, l'exécuter le droit EXECUTE. Sur certains hébergements, ces droits ne sont pas accordés à l'utilisateur de la base.
Sauvegarde et migration : le piège classique
Par défaut, mysqldump n'exporte pas les fonctions ni les procédures stockées. Il faut l'option --routines :
mysqldump --routines --triggers --events nom_de_la_base > sauvegarde.sql
Une sauvegarde faite sans cette option paraît complète, mais une fois restaurée, chaque requête qui appelle la fonction échoue. C'est l'un des points à vérifier lors d'un changement d'hébergeur ou d'une migration de boutique.
Les limites à connaître
- Performances : une fonction appelée dans un
WHERE sur une colonne (WHERE prix_ttc(price, 20) > 50) est exécutée ligne par ligne et empêche MySQL d'utiliser un index. Sur un gros catalogue, la requête devient lente.
- Logique cachée : le code de la fonction vit dans la base, pas dans le code source du site. Il échappe au gestionnaire de versions, aux revues de code et souvent à la documentation.
- Portabilité : chaque hébergement, chaque sauvegarde, chaque migration doit la prendre en compte.
Depuis MySQL 8 et MariaDB 10.2, plusieurs besoins autrefois couverts par une fonction stockée se règlent autrement : colonnes générées, index sur expression (MySQL 8.0.13 et plus), expressions de table communes (WITH) et fonctions de fenêtrage.
Et dans PrestaShop ?
Le cœur de PrestaShop n'utilise pas de fonction stockée : toute la logique est écrite en PHP, et la base ne contient que des tables. Un module qui en installe une ajoute donc une dépendance que personne n'attend : elle peut bloquer l'installation sur un hébergement qui refuse CREATE ROUTINE, disparaître d'une sauvegarde faite sans --routines, ou casser après un changement de serveur à cause de l'erreur 1449.
Dans un module, mieux vaut écrire le calcul en PHP, ou dans la requête elle-même, et laisser la base aux données. Les fonctions stockées restent utiles pour des traitements ponctuels côté serveur, menés par quelqu'un qui maîtrise l'hébergement : un script de maintenance, une reprise de données, un rapport.
Une base PrestaShop à reprendre ou à migrer ?
Fonctions oubliées dans une sauvegarde, requêtes lentes, module qui écrit directement en base : nous reprenons les boutiques existantes et nous les migrons sans perte de données. Notre agence PrestaShop · Migration vers PrestaShop 9.