Procédures et fonctions
Calculer un prix TTC, augmenter un salaire, transférer de l'argent entre deux comptes : plutôt que de réécrire ces opérations dans chaque programme, on les range dans la base de données, sous un nom. Une fonction quand on veut obtenir une valeur, une procédure quand on veut agir.
Après cette fiche, tu sais
- choisir entre une fonction et une procédure, et appeler chacune correctement (dans un SELECT ou avec CALL) ;
- écrire une fonction PL/pgSQL complète : paramètres, valeur par défaut, RETURNS et RETURN ;
- utiliser les paramètres IN, OUT et INOUT, et prévoir ce que voit l'appelant ;
- intercepter une erreur, renvoyer plusieurs lignes et piloter une transaction dans une procédure.
Obtenir une valeur, ou agir
En PostgreSQL, une fonction (CREATE FUNCTION) renvoie une valeur et s'utilise à l'intérieur d'une requête. Une procédure (CREATE PROCEDURE, depuis PostgreSQL 11) n'a pas de valeur de retour, s'appelle seule avec CALL, et peut valider ou annuler des transactions pendant son exécution.1
| FUNCTION | PROCEDURE | |
|---|---|---|
| Valeur de retour | oui : RETURNS type (éventuellement void) | non, mais des paramètres OUT possibles |
| Appel | dans une requête : SELECT f(…) | seule : CALL p(…) |
| COMMIT / ROLLBACK | jamais | oui, sous conditions (notion 6) |
| Usage typique | calculer, transformer, lire | modifier des données, traitements par lots |
Le trajet de la donnée
Fais défiler : un appel de fonction, puis un appel de procédure. Regarde ce qui revient, ou pas, vers l'appelant.
Ce qui revient vers l'appelant
Fonction ou procédure ?
Cinq besoins. À chaque fois, une contrainte décide pour toi : laquelle faut-il écrire ?
RETURNS dans l'en-tête, RETURN dans le corps
Une fonction qui calcule un prix TTC, avec par défaut le taux normal de la TVA suisse, 8,1 %.5
CREATE OR REPLACE FUNCTION calculer_ttc(prix NUMERIC, tva NUMERIC DEFAULT 0.081)
RETURNS NUMERIC
AS $$
BEGIN
RETURN round(prix * (1 + tva), 2); -- arrondi au centime
END;
$$ LANGUAGE plpgsql;SELECT calculer_ttc(100); -- 108.10
SELECT calculer_ttc(100, 0.026); -- 102.60 (taux réduit)
SELECT calculer_ttc(tva => 0.026, prix => 20); -- 20.52, paramètres nommés
SELECT nom, calculer_ttc(prix) AS prix_ttc FROM produits;
-- dans un bloc PL/pgSQL : affectation à une variable
DO $$
DECLARE v_total NUMERIC;
BEGIN
v_total := calculer_ttc(100, 0.026);
RAISE NOTICE 'v_total = %', v_total; -- v_total = 102.60
END $$;RETURNS NUMERIC, dans l'en-tête, déclare le type renvoyé ; RETURN …;, dans le corps, renvoie la valeur et termine la fonction.
Un paramètre avec valeur par défaut peut être omis à l'appel. Tous les paramètres d'entrée qui le suivent doivent aussi en avoir une.1
Le corps est une chaîne délimitée par des dollars : elle peut contenir des apostrophes sans qu'on doive les doubler.3
Remplace une fonction existante sans casser les vues ou déclencheurs qui l'utilisent ; mais on ne peut pas changer ainsi son type de retour.3
PostgreSQL accepte la création d'une fonction RETURNS integer dont une branche n'a pas de RETURN ; l'erreur tombe à l'exécution : control reached end of function without RETURN. Seules les fonctions RETURNS void ou à paramètres OUT peuvent s'en passer.2
Reconstruis la fonction
Les sept lignes de calculer_ttc ont été mélangées. Touche une ligne, puis celle avec laquelle l'échanger.
CALL, et des valeurs qui entrent ou sortent
employes(id, nom, salaire) contient : 42 Léa 2000, 7 Noah 5200, 3 Zoé 6100, 15 Lina 4800, 9 Elias 4300, 11 Maé 3900. Les résultats indiqués en commentaire ont été obtenus avec elle.CREATE OR REPLACE PROCEDURE augmenter_salaire(
p_employe_id INTEGER,
p_pourcentage NUMERIC
)
AS $$
BEGIN
UPDATE employes
SET salaire = round(salaire * (1 + p_pourcentage / 100), 2)
WHERE id = p_employe_id;
RAISE NOTICE 'Salaire augmenté de %', p_pourcentage || '%';
END;
$$ LANGUAGE plpgsql;
CALL augmenter_salaire(42, 10); -- Léa : 2000.00 → 2200.00
CALL augmenter_salaire(p_employe_id => 42, p_pourcentage => 10); -- paramètres nommésSELECT augmenter_salaire(42, 10); échoue : augmenter_salaire(integer, integer) is a procedure. Une procédure s'appelle avec CALL, jamais dans une requête.
Les trois modes de paramètres
La valeur entre. En PL/pgSQL, le paramètre est une variable locale : on peut la réaffecter, mais l'appelant n'en voit rien.
La valeur sort : la procédure l'écrit, l'appelant la récupère. À l'appel direct, on écrit NULL à sa place.1
Aller-retour : la valeur entre, la procédure peut la modifier, et elle ressort.
Si la procédure a des paramètres OUT ou INOUT, CALL renvoie une ligne avec leurs valeurs.1
Le sens de circulation
Fais défiler : un paramètre IN, puis OUT, puis INOUT. Les valeurs ont été vérifiées dans PostgreSQL 18.
IN : la valeur entre
CREATE OR REPLACE PROCEDURE infos_employe(
p_id IN INTEGER,
p_nom OUT VARCHAR,
p_salaire OUT NUMERIC
)
AS $$
BEGIN
SELECT nom, salaire INTO p_nom, p_salaire
FROM employes WHERE id = p_id;
END;
$$ LANGUAGE plpgsql;
CALL infos_employe(7, NULL, NULL); -- une ligne : p_nom = Noah, p_salaire = 5200
DO $$
DECLARE v_nom VARCHAR; v_sal NUMERIC;
BEGIN
CALL infos_employe(7, v_nom, v_sal);
RAISE NOTICE '% gagne %', v_nom, v_sal; -- Noah gagne 5200
END $$;BEGIN … EXCEPTION WHEN … THEN
Un bloc PL/pgSQL peut avoir une section EXCEPTION : si une instruction du bloc lève une erreur, PostgreSQL abandonne le reste du bloc, annule ce que le bloc a modifié et cherche une clause WHEN correspondante.2 Fais défiler : la flèche suit l'exécution.
diviser(10, 4) : la ligne RETURN a / b; s'exécute et renvoie 2.5. La section EXCEPTION n'est jamais visitée.
diviser(10, 0) : a / b lève l'erreur division_by_zero. Le reste du bloc est abandonné.
L'exécution saute à la section EXCEPTION, et la clause WHEN division_by_zero correspond.
Le gestionnaire affiche un avis et renvoie NULL : l'appel réussit, avec un résultat vide.
Sans cette section, l'erreur remonterait jusqu'à l'appelant : toute la requête échouerait avec division by zero.
WHEN OTHERS, qui attrape tout, cache souvent de vrais bogues.Une fonction qui se lit comme une table
RETURNS TABLE (…) (ou RETURNS SETOF type) déclare un résultat de plusieurs lignes. Dans le corps, RETURN QUERY et RETURN NEXT ne quittent pas la fonction : ils ajoutent des lignes au résultat, et c'est la fin de la fonction, ou un RETURN sans argument, qui le renvoie.2
CREATE OR REPLACE FUNCTION get_top_salaries(n INTEGER)
RETURNS TABLE(nom VARCHAR, salaire NUMERIC) AS $$
BEGIN
RETURN QUERY
SELECT e.nom, e.salaire -- alias e : évite le conflit avec nom et salaire
FROM employes e
ORDER BY e.salaire DESC
LIMIT n;
END;
$$ LANGUAGE plpgsql;
SELECT * FROM get_top_salaries(3);
nom | salaire -------+--------- Zoé | 6100 Noah | 5200 Lina | 4800
Les colonnes de RETURNS TABLE (nom, salaire) deviennent des variables dans le corps. Une requête qui écrit nom sans préfixe devient ambiguë : on qualifie les colonnes par un alias (e.nom).
COMMIT dans une procédure, jamais dans une fonction
Une procédure appelée par CALL (comme un bloc DO) peut terminer la transaction en cours avec COMMIT ou ROLLBACK ; une nouvelle commence aussitôt.2 Trois limites : jamais dans une fonction ; jamais dans un bloc qui a une section EXCEPTION, qui forme une sous-transaction ; jamais si le CALL est lui-même dans une transaction ouverte par BEGIN.1 Dans ces trois cas, PostgreSQL 18 répond invalid transaction termination.
-- comptes(id, solde) avec la contrainte CHECK (solde >= 0)
CREATE OR REPLACE PROCEDURE transferer_fonds(
p_from INTEGER,
p_to INTEGER,
p_montant NUMERIC
)
AS $$
BEGIN
UPDATE comptes SET solde = solde - p_montant WHERE id = p_from;
UPDATE comptes SET solde = solde + p_montant WHERE id = p_to;
COMMIT;
RAISE NOTICE 'Transfert de % effectué', p_montant;
END;
$$ LANGUAGE plpgsql;Deux transferts, un refusé
Fais défiler : un transfert de 200 CHF réussit, puis un transfert de 400 CHF viole la contrainte CHECK. Regarde les soldes.
Avant les transferts
Accepté ou refusé par PostgreSQL ?
Sept situations, toutes testées dans PostgreSQL 18. Range chacune.
Six bonnes pratiques
- préfixer les paramètres (
p_) et les variables (v_) : ils ne se confondent plus avec les colonnes ; - déclarer les variables avec
%TYPE:v_sal employes.salaire%TYPEsuit le type de la colonne s'il change un jour3 ; - ne pas réaffecter un paramètre IN : c'est trompeur en PL/pgSQL et refusé en Oracle ;
- pour une requête dont on ignore le résultat, écrire
PERFORM: unSELECTsansINTOprovoque l'erreurquery has no destination for result data; - n'intercepter que les erreurs qu'on sait traiter, et nommer la condition (
division_by_zero) plutôt queOTHERS; - arrondir les montants (
round(…, 2)) : les calculs enNUMERICallongent vite le nombre de décimales.
Trois exercices corrigés
Tables utilisées : eleves(id, nom, classe) et notes(eleve_id, note), notes de 1 à 6. Tous les corrigés ont été exécutés dans PostgreSQL 18.
1Écris une fonction moyenne_classe(p_classe) qui renvoie la moyenne des notes d'une classe, arrondie au centième.Voir le corrigé
CREATE OR REPLACE FUNCTION moyenne_classe(p_classe TEXT)
RETURNS NUMERIC AS $$
DECLARE
v_moyenne NUMERIC;
BEGIN
SELECT round(avg(n.note), 2) INTO v_moyenne
FROM notes n JOIN eleves e ON e.id = n.eleve_id
WHERE e.classe = p_classe;
RETURN v_moyenne;
END;
$$ LANGUAGE plpgsql;
SELECT moyenne_classe('2M3'); -- 4.75 avec les notes 5.5, 4.5, 4 et 5
Pour une classe sans notes, avg renvoie NULL, et la fonction aussi.
2Écris une procédure ajouter_note(p_eleve, p_note) qui refuse une note hors de l'échelle 1 à 6, puis l'enregistre.Voir le corrigé
CREATE OR REPLACE PROCEDURE ajouter_note(p_eleve INTEGER, p_note NUMERIC)
AS $$
BEGIN
IF p_note < 1 OR p_note > 6 THEN
RAISE EXCEPTION 'Note % hors de l''échelle 1 à 6', p_note;
END IF;
INSERT INTO notes (eleve_id, note) VALUES (p_eleve, p_note);
END;
$$ LANGUAGE plpgsql;
CALL ajouter_note(3, 5.5); -- enregistrée
CALL ajouter_note(3, 7); -- ERREUR : Note 7 hors de l'échelle 1 à 6
Dans une chaîne SQL, l'apostrophe se double (l''échelle). Une procédure convient : on agit sur la table, sans valeur à renvoyer.
3Cette fonction se crée sans erreur, mais plante dès qu'on l'appelle. Pourquoi, et comment la corriger ?Voir le corrigé
CREATE OR REPLACE FUNCTION nb_eleves(p_classe TEXT)
RETURNS INTEGER AS $$
BEGIN
SELECT count(*) FROM eleves WHERE classe = p_classe;
END;
$$ LANGUAGE plpgsql;
Deux défauts : le SELECT n'a pas d'INTO (erreur query has no destination for result data), et il manque le RETURN. Version corrigée :
CREATE OR REPLACE FUNCTION nb_eleves(p_classe TEXT)
RETURNS INTEGER AS $$
DECLARE
v_nb INTEGER;
BEGIN
SELECT count(*) INTO v_nb FROM eleves WHERE classe = p_classe;
RETURN v_nb;
END;
$$ LANGUAGE plpgsql;La fiche en huit lignes
Teste-toi
Sources
- PostgreSQL 18 Documentation : User-Defined Procedures (différences avec les fonctions), CALL (paramètres OUT, transaction ouverte), CREATE PROCEDURE (modes, valeurs par défaut). procédures, CALL, CREATE PROCEDURE (postgresql.org)
- PostgreSQL 18 Documentation, PL/pgSQL : Transaction Management et Control Structures (RETURN, RETURN QUERY, interception des erreurs, coût d'un bloc EXCEPTION). transactions, structures de contrôle (postgresql.org)
- PostgreSQL 18 Documentation : CREATE FUNCTION (OR REPLACE, chaînes entre dollars) et PL/pgSQL Declarations (%TYPE). CREATE FUNCTION, déclarations (postgresql.org)
- Oracle, Database PL/SQL Language Reference 19c, « Subprogram Parameters » (modes IN, OUT, IN OUT). docs.oracle.com
- Confédération suisse, Loi fédérale régissant la taxe sur la valeur ajoutée (LTVA), RS 641.20, art. 25 (taux normal 8,1 %, taux réduit 2,6 %). fedlex.admin.ch
Fiche relue et sourcée en septembre 2026. Tout le code a été exécuté dans PostgreSQL 18.