PL/pgSQL avancé

Module P2

Dalibo SCOP

24.04

17 avril 2024

Sur ce document

Formation Module P2
Titre PL/pgSQL avancé
Révision 24.04
PDF https://dali.bo/p2_pdf
EPUB https://dali.bo/p2_epub
HTML https://dali.bo/p2_html
Slides https://dali.bo/p2_slides
TP https://dali.bo/p2_tp
TP (solutions) https://dali.bo/p2_solutions

Licence Creative Commons CC-BY-NC-SA

Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :

  • Paternité
  • Pas d’utilisation commerciale
  • Partage des conditions initiales à l’identique

Marques déposées

PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.

Versions de PostgreSQL couvertes

Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 12 à 16.

PL/pgSQL avancé

PostgreSQL

Préambule

Au menu

  • Routines « variadic » et polymorphes
  • Fonctions trigger
  • Curseurs
  • Récupérer les erreurs
  • Messages d’erreur dans les logs
  • Sécurité
  • Optimisation
  • Problèmes fréquents

Objectifs

  • Connaître la majorité des possibilités de PL/pgSQL
  • Les utiliser pour étendre les fonctionnalités de la base
  • Écrire du code robuste
  • Éviter les pièges de sécurité
  • Savoir optimiser une routine

Routines variadic

Routines variadic : introduction

  • Permet de créer des routines avec un nombre d’arguments variables
  • … mais du même type

Routines variadic : exemple

Récupérer le minimum d’une liste :

CREATE FUNCTION pluspetit(VARIADIC numeric[])
RETURNS numeric AS $$
SELECT min($1[i]) FROM generate_subscripts($1, 1) g(i);
$$ LANGUAGE SQL;

SELECT pluspetit(10, -1, 5, 4.4);
 pluspetit
-----------
        -1
(1 row)

Routines variadic : exemple PL/pgSQL

  • En PL/pgSQL, cette fois-ci
  • Démonstration de FOREACH xxx IN ARRAY aaa LOOP
  • Précédemment, obligé de convertir le tableau en relation pour boucler (unnest)

Routines polymorphes

Routines polymorphes : introduction

  • Typer les variables oblige à dupliquer les routines communes à plusieurs types
  • PostgreSQL propose des types polymorphes
  • Le typage se fait à l’exécution

Routines polymorphes : anyelement

  • Remplace tout type de données simple ou composite
    • pour les paramètres en entrée comme pour les paramètres en sortie
  • Tous les paramètres et type de retour de type anyelement se voient attribués le même type
  • Donc un seul type pour tous les anyelement autorisés
  • Paramètre spécial $0 : du type attribué aux éléments anyelement

Routines polymorphes : anyarray

  • anyarray remplace tout tableau de type de données simple ou composite
    • pour les paramètres en entrée comme pour les paramètres en sortie
  • Le typage se fait à l’exécution
  • Tous les paramètres de type anyarray se voient attribués le même type

Routines polymorphes : exemple

L’addition est un exemple fréquent :

CREATE OR REPLACE FUNCTION
  addition(var1 anyelement, var2 anyelement)
RETURNS anyelement
AS $$
DECLARE
  somme ALIAS FOR $0;
BEGIN
  somme := var1 + var2;
  RETURN somme;
END;
$$ LANGUAGE plpgsql;

Routines polymorphes : tests

# SELECT addition(1, 3);
 addition
----------
        4
(1 row)

# SELECT addition(1.3, 3.5);
 addition
----------
      4.8
(1 row)

Routines polymorphes : problème

  • Attention lors de l’utilisation de type polymorphe…
# SELECT addition('un'::text, 'mot'::text);
ERREUR:  L'opérateur n'existe pas : text + text
LIGNE 1 : SELECT   $1  +  $2
^
ASTUCE : Aucun opérateur correspond au nom donné et aux types d'arguments.
    Vous devez ajouter des conversions explicites de type.
REQUÊTE : SELECT   $1  +  $2
CONTEXTE : PL/pgSQL function "addition" line 4 at assignment

Fonctions trigger

Fonctions trigger : introduction

  • Fonction stockée
  • Action déclenchée par INSERT (incluant COPY), UPDATE, DELETE, TRUNCATE
  • Mode par ligne ou par instruction
  • Exécution d’une fonction stockée codée à partir de tout langage de procédure activée dans la base de données

Fonctions trigger : variables (1/5)

  • OLD :
    • type de données RECORD correspondant à la ligne avant modification
    • valable pour un DELETE et un UPDATE
  • NEW :
    • type de données RECORD correspondant à la ligne après modification
    • valable pour un INSERT et un UPDATE

Fonctions trigger : variables (2/5)

  • Ces deux variables sont valables uniquement pour les triggers en mode ligne
    • pour les triggers en mode instruction, la version 10 propose les tables de transition
  • Accès aux champs par la notation pointée
    • NEW.champ1 pour accéder à la nouvelle valeur de champ1

Fonctions trigger : variables (3/5)

  • TG_NAME
    • nom du trigger qui a déclenché l’appel de la fonction
  • TG_WHEN
    • chaîne valant BEFORE, AFTER ou INSTEAD OF suivant le type du trigger
  • TG_LEVEL
    • chaîne valant ROW ou STATEMENT suivant le mode du trigger
  • TG_OP
    • chaîne valant INSERT, UPDATE, DELETE, TRUNCATE suivant l’opération qui a déclenché le trigger

Fonctions trigger : variables (4/5)

  • TG_RELID
    • OID de la table qui a déclenché le trigger
  • TG_TABLE_NAME
    • nom de la table qui a déclenché le trigger
  • TG_TABLE_SCHEMA
    • nom du schéma contenant la table qui a déclenché le trigger

Fonctions trigger : variables (5/5)

  • TG_NARGS
    • nombre d’arguments donnés à la fonction trigger
  • TG_ARGV
    • les arguments donnés à la fonction trigger (le tableau commence à 0)

Fonctions trigger : retour

  • Une fonction trigger a un type de retour spécial, trigger
  • Trigger ROW, BEFORE :
    • si retour NULL, annulation de l’opération, sans déclencher d’erreur
    • sinon, poursuite de l’opération avec cette valeur de ligne
    • attention au RETURN NEW; avec trigger BEFORE DELETE
  • Trigger ROW, AFTER : valeur de retour ignorée
  • Trigger STATEMENT : valeur de retour ignorée
  • Pour ces deux derniers cas, annulation possible dans le cas d’une erreur à l’exécution de la fonction (que vous pouvez déclencher dans le code du trigger)

Fonctions trigger : exemple - 1

  • Horodater une opération sur une ligne
CREATE TABLE ma_table (
id serial,
-- un certain nombre de champs informatifs
date_ajout timestamp,
date_modif timestamp);

Fonctions trigger : exemple - 2

CREATE OR REPLACE FUNCTION horodatage() RETURNS trigger
AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    NEW.date_ajout := now();
  ELSEIF TG_OP = 'UPDATE' THEN
    NEW.date_modif := now();
  END IF;
  RETURN NEW;
END; $$ LANGUAGE plpgsql;

Options de CREATE TRIGGER

CREATE TRIGGER permet quelques variantes :

  • CREATE TRIGGER name WHEN ( condition )
  • CREATE TRIGGER name BEFORE UPDATE OF colx ON my_table
  • CREATE CONSTRAINT TRIGGER : exécuté qu’au moment de la validation de la transaction
  • CREATE TRIGGER view_insert INSTEAD OF INSERT ON my_view

Tables de transition

  • Pour les triggers de type AFTER et de niveau statement
  • Possibilité de stocker les lignes avant et/ou après modification
    • REFERENCING OLD TABLE
    • REFERENCING NEW TABLE
  • Par exemple :
CREATE TRIGGER tr1
AFTER DELETE ON t1
REFERENCING OLD TABLE AS oldtable
FOR EACH STATEMENT
EXECUTE PROCEDURE log_delete();

Curseurs

Curseurs : introduction

  • Exécuter une requête en une fois peut ramener beaucoup de résultats
  • Tout ce résultat est en mémoire
    • risque de dépassement mémoire
  • La solution : les curseurs
  • Un curseur permet d’exécuter la requête sur le serveur mais de ne récupérer les résultats que petit bout par petit bout
  • Dans une transaction ou une routine

Curseurs : déclaration d’un curseur

  • Avec le type refcursor
  • Avec la pseudo-instruction CURSOR FOR
  • Avec une requête paramétrée
  • Exemples :
curseur1 refcursor;
curseur2 CURSOR FOR SELECT * FROM ma_table;
curseur3 CURSOR (param integer) IS
SELECT * FROM ma_table WHERE un_champ=param;

Curseurs : ouverture d’un curseur

  • Lier une requête à un curseur :
    OPEN curseur FOR requete
  • Plan de la requête mis en cache
  • Lier une requête dynamique à un curseur
    OPEN curseur FOR EXECUTE chaine_requete

Curseurs : ouverture d’un curseur lié

  • Instruction SQL : OPEN curseur(arguments)
  • Permet d’ouvrir un curseur déjà lié à une requête
  • Impossible d’ouvrir deux fois le même curseur
  • Plan de la requête mise en cache
  • Exemple
curseur CURSOR FOR SELECT * FROM ma_table;
...
OPEN curseur;

Curseurs : récupération des données

  • Instruction SQL :
    FETCH [ direction { FROM | IN } ] curseur INTO cible
  • Récupère la prochaine ligne
  • FOUND indique si cette nouvelle ligne a été récupérée
  • Cible est
    • une variable RECORD
    • une variable ROW
    • un ensemble de variables séparées par des virgules

Curseurs : récupération des données

  • direction du FETCH :
    • NEXT, PRIOR
    • FIRST, LAST
    • ABSOLUTE nombre, RELATIVE nombre
    • nombre
    • ALL
    • FORWARD, FORWARD nombre, FORWARD ALL
    • BACKWARD, BACKWARD nombre, BACKWARD ALL

Curseurs : modification des données

  • Mise à jour d’une ligne d’un curseur :
    UPDATE une_table SET ...
    WHERE CURRENT OF curseur;
  • Suppression d’une ligne d’un curseur :
    DELETE FROM une_table
    WHERE CURRENT OF curseur;

Curseurs : fermeture d’un curseur

  • Instruction SQL : CLOSE curseur
  • Ferme le curseur
  • Permet de récupérer de la mémoire
  • Permet aussi de réouvrir le curseur

Curseurs : renvoi d’un curseur

  • Fonction renvoyant une valeur de type refcursor
  • Permet donc de renvoyer plusieurs valeurs

Gestion des erreurs

Gestion des erreurs : introduction

  • Sans exceptions :
    • toute erreur provoque un arrêt de la fonction
    • toute modification suite à une instruction SQL (INSERT, UPDATE, DELETE) est annulée
    • d’où l’ajout d’une gestion personnalisée des erreurs avec le concept des exceptions

Gestion des erreurs : une exception

  • La fonction comporte un bloc supplémentaire, EXCEPTION :
DECLARE
  -- déclaration des variables locales
BEGIN
  -- instructions de la fonction
EXCEPTION
WHEN condition THEN
  -- instructions traitant cette erreur
WHEN condition THEN
  -- autres instructions traitant cette autre erreur
  -- etc.
END

Gestion des erreurs : flot dans une fonction

  • L’exécution de la fonction commence après le BEGIN
  • Si aucune erreur ne survient, le bloc EXCEPTION est ignoré
  • Si une erreur se produit
    • tout ce qui a été modifié dans la base dans le bloc est annulé
    • les variables gardent par contre leur état
    • l’exécution passe directement dans le bloc de gestion de l’exception

Gestion des erreurs : flot dans une exception

  • Recherche d’une condition satisfaisante
  • Si cette condition est trouvée
    • exécution des instructions correspondantes
  • Si aucune condition n’est compatible
    • sortie du bloc BEGIN/END comme si le bloc d’exception n’existait pas
    • passage de l’exception au bloc BEGIN/END contenant (après annulation de ce que ce bloc a modifié en base)
  • Dans un bloc d’exception, les instructions INSERT, UPDATE, DELETE de la fonction ont été annulées
  • Dans un bloc d’exception, les variables locales de la fonction ont gardé leur ancienne valeur

Gestion des erreurs : codes d’erreurs

  • SQLSTATE : code d’erreur
  • SQLERRM : message d’erreur
  • Par exemple :
    • Data Exception : division par zéro, overflow, argument invalide pour certaines fonctions, etc.
    • Integrity Constraint Violation : unicité, CHECK, clé étrangère, etc.
    • Syntax Error
    • PL/pgSQL Error : RAISE EXCEPTION, pas de données, trop de lignes, etc.
  • Les erreurs sont contenues dans des classes d’erreurs plus génériques, qui peuvent aussi être utilisées

Messages d’erreurs : RAISE - 1

  • Envoyer une trace dans les journaux applicatifs et/ou vers le client
    • RAISE niveau message
  • Niveau correspond au niveau d’importance du message
    • DEBUG, LOG, INFO, NOTICE, WARNING, EXCEPTION
  • Message est la trace à enregistrer
  • Message dynamique… tout signe % est remplacé par la valeur indiquée après le message
  • Champs DETAIL et HINT disponibles

Messages d’erreurs : RAISE - 2

Exemples :

RAISE WARNING 'valeur % interdite', valeur;
RAISE WARNING 'valeur % ambigue',
               valeur
               USING HINT = 'Controlez la valeur saisie en amont';

Messages d’erreurs : configuration des logs

  • Deux paramètres importants pour les traces
  • log_min_messages
    • niveau minimum pour que la trace soit enregistrée dans les journaux
  • client_min_messages
    • niveau minimum pour que la trace soit envoyée au client
  • Dans le cas d’un RAISE NOTICE message, il faut avoir soit log_min_messages, soit client_min_messages, soit les deux à la valeur NOTICE au minimum.

Messages d’erreurs : RAISE EXCEPTION - 1

  • Annule le bloc en cours d’exécution
    • RAISE EXCEPTION message
  • Sauf en cas de présence d’un bloc EXCEPTION gérant la condition RAISE_EXCEPTION
  • message est la trace à enregistrer, et est dynamique… tout signe % est remplacé par la valeur indiquée après le message

Messages d’erreurs : RAISE EXCEPTION - 2

Exemple :

RAISE EXCEPTION 'erreur interne';
-- La chose à ne pas faire !

Flux des erreurs dans du code PL

  • Les exceptions non traitées «remontent»
    • de bloc BEGIN/END imbriqués vers les blocs parents (fonctions appelantes comprises)
    • jusqu’à ce que personne ne puisse les traiter
    • voir note pour démonstration

Flux des erreurs dans du code PL - 2

  • Les erreurs remontent
  • Cette fois-ci, on rajoute un bloc PL pour intercepter l’erreur

Flux des erreurs dans du code PL - 3

  • Cette fois-ci, on rajoute un bloc PL indépendant pour gérer le second INSERT

Flux des erreurs dans du code PL - 4

  • Illustrons maintenant la remontée d’erreurs
  • Nous avons deux blocs imbriqués
  • Une erreur non prévue va se produire dans le bloc intérieur

Sécurité

Sécurité : droits

  • L’exécution de la routine dépend du droit EXECUTE
  • Par défaut, ce droit est donné à la création de la routine
    • au propriétaire de la routine
    • au groupe spécial PUBLIC

Sécurité : ajout

  • Ce droit peut être donné avec l’instruction SQL GRANT :
GRANT { EXECUTE | ALL [ PRIVILEGES ] }
    ON { { FUNCTION | PROCEDURE | ROUTINE } routine_name
    [ ( [ [ argmode ] [ arg_name ] arg_type [, ...] ] ) ] [, ...  ]
         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA schema_name [, ...] }
    TO role_specification [, ...] [ WITH GRANT OPTION ]

Sécurité : suppression

  • Un droit peut être révoqué avec l’instruction SQL REVOKE
REVOKE [ GRANT OPTION FOR ]
    { EXECUTE | ALL [ PRIVILEGES ] }
    ON { { FUNCTION | PROCEDURE | ROUTINE } function_name
    [ ( [ [ argmode ] [ arg_name ] arg_type [, ...] ] ) ] [, ... ]
         | ALL { FUNCTIONS | PROCEDURES | ROUTINES } IN SCHEMA schema_name [, ...] }
    FROM { [ GROUP ] role_name | PUBLIC } [, ...]
    [ CASCADE | RESTRICT ]

Sécurité : SECURITY INVOKER/DEFINER

  • SECURITY INVOKER
    • la routine s’exécute avec les droits de l’utilisateur qui l’exécute
  • SECURITY DEFINER
    • la routine s’exécute avec les droits du propriétaire
    • équivalent du sudo Unix
    • Impérativement sécuriser les variables d’environnement
    • et surtout search_path

Sécurité : LEAKPROOF

  • LEAKPROOF
    • indique au planificateur que la routine ne peut pas faire fuiter d’information de contexte
    • réservé aux superutilisateurs
    • si on la déclare telle, s’assurer que la routine est véritablement sûre !
  • Option utile lorsque l’on utilise des vues avec l’option security_barrier

Sécurité : visibilité des sources - 1

  • Le code d’une fonction est visible par tout le monde
    • y compris ceux qui n’ont pas le droit d’exécuter la fonction
  • Vous devez donc écrire un code robuste
    • pas espérer que, comme personne n’en a le code, personne ne trouvera de faille
  • Surtout pour les fonctions SECURITY DEFINER

Sécurité : visibilité des sources - 2

# SELECT proargnames, prosrc
FROM pg_proc WHERE proname='addition';

-[ RECORD 1 ]--------------------------
proargnames | {var1,var2}
prosrc      |
            :   DECLARE
            :     somme ALIAS FOR $0;
            :   BEGIN
            :     somme := var1 + var2;
            :     RETURN somme;
            :   END;
            :

Sécurité : Injections SQL

  • Les paramètres d’une routine doivent être considérés comme hostiles :
    • ils contiennent des données non validées (qui appelle la routine ?)
    • ils peuvent, si l’utilisateur est imaginatif, être utilisés pour exécuter du code
  • Utiliser quote_ident, quote_literal et quote_nullable
  • Utiliser aussi format

Optimisation

Fonctions immutables, stables ou volatiles - 1

  • Par défaut, PostgreSQL considère que les fonctions sont VOLATILE
  • volatile : fonction dont l’exécution ne peut ni ne doit être évitée

Fonctions immutables, stables ou volatiles - 2

  • immutable : fonctions déterministes, dont le résultat peut être précalculé avant de planifier la requête.

Fonctions immutables, stables ou volatiles - 3

  • stable : fonction ayant un comportement stable au sein d’un même ordre SQL.

Optimisation : rigueur

  • Fonction STRICT
  • La fonction renvoie NULL si au moins un des arguments est NULL

Optimisation : EXCEPTION

  • Un bloc contenant une clause EXCEPTION est plus coûteuse en entrée/sortie qu’un bloc sans
    • un SAVEPOINT est créé à chaque fois pour pouvoir annuler le bloc uniquement.
  • À utiliser avec parcimonie
  • Un bloc BEGIN imbriqué a un coût aussi 
    • un SAVEPOINT est créé à chaque fois.

Requête statique ou dynamique ?

  • Les requêtes statiques :
    • sont écrites « en dur » dans le code PL/pgSQL
    • donc pas d’EXECUTE ou PERFORM
    • sont préparées une fois par session, à leur première exécution
    • peuvent avoir un plan générique lorsque c’est jugé utile par le planificateur

Requête statique ou dynamique ? - 2

  • Les requêtes dynamiques :
    • sont écrites avec un EXECUTE, PERFORM
    • sont préparées à chaque exécution
    • ont un plan optimisé
    • sont donc plus coûteuses en planification
    • mais potentiellement plus rapides à l’exécution

Requête statique ou dynamique ? -3

  • Alors, statique ou dynamique ?
  • Si la requête est simple : statique
    • peu de WHERE
    • peu ou pas de jointure
  • Sinon dynamique

Outils

  • Deux outils disponibles
    • un debugger
    • un pseudo-profiler

pldebugger

  • License Artistic 2.0
  • Développé par EDB et intégrable dans pgAdmin
  • Installé par défaut avec le one-click installer
    • mais non activé
  • Compilation nécessaire pour les autres systèmes

pldebugger - Compilation

  • Récupérer le source avec git
  • Copier le répertoire dans le répertoire contrib des sources de PostgreSQL
  • Et les suivre étapes standards
    • make
    • make install

pldebugger - Activation

  • Configurer shared_preload_libraries
    • shared_preload_libraries = 'plugin_debugger'
  • Redémarrer PostgreSQL
  • Installer l’extension pldbgapi :
CREATE EXTENSION pldbgapi;

auto_explain

  • Mise en place globale (traces) :
    • shared_preload_libraries='auto_explain' si global
    • ALTER DATABASE erp SET auto_explain.log_min_duration = '3s'
  • Ou par session :
    • LOAD 'auto_explain'
    • SET auto_explain.log_analyze TO true;
    • SET auto_explain.log_nested_statements TO true;

pldebugger - Utilisation

  • Via pgAdmin

log_functions

  • Créé par Dalibo
  • License BSD
  • Compilation nécessaire

log_functions - Compilation

  • Récupérer l’archive sur PGXN.org
  • Décompresser l’archive puis : make USE_PGXS=1 && make USE_PGXS=1 install

log_functions - Activation

  • Permanente
    • shared_preload_libraries = 'log_functions'
    • Redémarrage de PostgreSQL
  • Au cas par cas
    • LOAD 'log_functions'

log_functions - Configuration

  • 5 paramètres en tout
  • À configurer
    • dans Postgresql.conf
    • ou avec SET

log_functions - Utilisation

  • Exécuter des procédures stockées en PL/pgSQL
  • Lire les journaux applicatifs
    • grep très utile

Conclusion

  • PL/pgSQL est un langage puissant
  • Seul inconvénient
    • sa lenteur par rapport à d’autres PL comme PL/perl ou C
    • PL/perl est très efficace pour les traitements de chaîne notamment
  • Permet néanmoins de traiter la plupart des cas, de façon simple et efficace

Pour aller plus loin

  • Documentation officielle
    • « Chapitre 40. PL/pgSQL - Langage de procédures SQL »

Questions

N’hésitez pas, c’est le moment !

Travaux pratiques

Travaux pratiques (solutions)