PL/pgSQL : Les bases

Module P1

Dalibo SCOP

24.04

17 avril 2024

Sur ce document

Formation Module P1
Titre PL/pgSQL : Les bases
Révision 24.04
PDF https://dali.bo/p1_pdf
EPUB https://dali.bo/p1_epub
HTML https://dali.bo/p1_html
Slides https://dali.bo/p1_slides
TP https://dali.bo/p1_tp
TP (solutions) https://dali.bo/p1_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 : les bases

PostgreSQL

Préambule

  • Vous apprendrez :
    • à choisir si vous voulez écrire du PL
    • à choisir votre langage PL
    • les principes généraux des langages PL autres que PL/pgSQL
    • les bases de PL/pgSQL

Au menu

  • Présentation du PL et des principes
  • Présentations de PL/pgSQL et des autres langages PL
  • Installation d’un langage PL
  • Détails sur PL/pgSQL

Objectifs

  • Comprendre les cas d’utilisation d’une routine PL/pgSQL
  • Choisir son langage PL en connaissance de cause
  • Comprendre la différence entre PL/pgSQL et les autres langages PL
  • Écrire une routine simple en PL/pgSQL
    • et même plus complexe

Introduction

Qu’est-ce qu’un PL ?

  • PL = Procedural Language
  • 3 langages activés par défaut :
    • C
    • SQL
    • PL/pgSQL

Quels langages PL sont disponibles ?

  • Installé par défaut :
    • PL/pgSQL
  • Intégrés au projet :
    • PL/Perl
    • PL/Python
    • PL/Tcl
  • Extensions tierces :
    • PL/java, PL/R, PL/v8 (Javascript), PL/sh …
    • extensible à volonté

Langages trusted vs untrusted

  • Trusted = langage de confiance :
    • ne permet que l’accès à la base de données
    • donc pas aux systèmes de fichiers, aux sockets réseaux, etc.
    • SQL, PL/pgSQL, PL/Perl, PL/Tcl
  • Untrusted:
    • PL/Python, C…
    • PL/TclU, PL/PerlU

Les langages PL de PostgreSQL

  • Les langages PL fournissent :
    • des fonctionnalités procédurales dans un univers relationnel
    • des fonctionnalités avancées du langage PL choisi
    • des performances de traitement souvent supérieures à celles du même code côté client

Intérêts de PL/pgSQL en particulier

  • Inspiré de l’ADA, proche du Pascal
  • Ajout de structures de contrôle au langage SQL
  • Dédié au traitement des données et au SQL
  • Peut effectuer des traitements complexes
  • Hérite de tous les types, fonctions et opérateurs définis par les utilisateurs
  • Trusted
  • Facile à utiliser

Les autres langages PL ont toujours leur intérêt

  • Avantages des autres langages PL par rapport à PL/pgSQL :
    • beaucoup plus de possibilités
    • souvent plus performants pour la résolution de certains problèmes
  • Mais :
    • pas spécialisés dans le traitement de requêtes
    • types différents
    • interpréteur séparé

Routines / Procédures stockées / Fonctions

  • Procédure stockée
    • pas de retour
    • contrôle transactionnel : COMMIT / ROLLBACK
  • Fonction
    • peut renvoyer des données (même des lignes)
    • utilisable dans un SELECT
    • peut être de type TRIGGER, agrégat, fenêtrage
  • Routine
    • procédure ou fonction

Installation

Installation des binaires nécessaires

  • SQL, C et PL/pgSQL
    • compilés et installés par défaut
  • Paquets du PGDG pour la plupart des langages :
    yum|dnf install postgresql16-plperl
    apt     install postgresql-plpython3-16
  • Autres langages :
    • à compiler soi-même

Activer un langage

Activer un langage passe par la création de l’extension :

CREATE EXTENSION plperl ;     -- pour tous
-- versions untrusted
CREATE EXTENSION plperlu ;    -- pour le superutilisateur
CREATE EXTENSION plpython3u ;
  • Liste : \dL ou pg_language

Exemples de fonctions & procédures

Fonction PL/pgSQL simple

Une fonction simple en PL/pgSQL :

CREATE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS '
DECLARE
  resultat integer;
BEGIN
  resultat := entier1 + entier2;
  RETURN resultat;
END ' ;

Exemple de fonction SQL

Même fonction en SQL pur :

CREATE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE sql
IMMUTABLE
AS '     SELECT entier1 + entier2 ; ' ;
  • Intérêt : inlining & planification
  • Syntaxe allégée (v14+) :
CREATE OR REPLACE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE sql
IMMUTABLE
RETURN entier1 + entier2 ;

Exemple de fonction PL/pgSQL utilisant la base

CREATE OR REPLACE FUNCTION nb_lignes_table (sch text, tbl text)
 RETURNS bigint
 STABLE
AS '
DECLARE     n bigint ;
BEGIN
    SELECT n_live_tup
    INTO n
    FROM pg_stat_user_tables
    WHERE schemaname = sch AND relname = tbl ;
    RETURN n ;
END ; '
LANGUAGE plpgsql ;

Exemple de fonction PL/Perl complexe

  • Permet d’insérer une facture associée à un client
  • Si le client n’existe pas, une entrée est créée
  • Utilisation fréquente de spi_exec

Exemple de fonction PL/pgSQL complexe

  • Même fonction en PL/pgSQL que précédemment
  • L’accès aux données est simple et naturel
  • Les types de données SQL sont natifs
  • La capacité de traitement est limitée par le langage
  • Attention au nommage des variables et paramètres

Exemple de procédure

CREATE OR REPLACE PROCEDURE vide_tables (dry_run BOOLEAN)
AS '
BEGIN
    TRUNCATE TABLE pgbench_history ;
    TRUNCATE TABLE pgbench_accounts CASCADE ;
    TRUNCATE TABLE pgbench_tellers  CASCADE ;
    TRUNCATE TABLE pgbench_branches CASCADE ;
    IF dry_run THEN
        ROLLBACK ;
    END IF ;
END ;
 ' LANGUAGE plpgsql ;

Exemple de bloc anonyme en PL/pgSQL

  • Bloc procédural anonyme en PL/pgSQL :
DO $$
DECLARE r record;
BEGIN
    FOR r IN (SELECT schemaname, relname
              FROM pg_stat_user_tables
              WHERE coalesce(last_analyze, last_autoanalyze) IS NULL
              ) LOOP
        RAISE NOTICE 'Analyze %.%', r.schemaname, r.relname ;
        EXECUTE 'ANALYZE ' || quote_ident(r.schemaname)
                           || '.' || quote_ident(r.relname) ;
    END LOOP;
END$$;

Utiliser une fonction ou une procédure

Invocation d’une fonction ou procédure

  • Appeler une procédure : ordre spécifique CALL
CALL ma_procedure('arg1');
  • Appeler une fonction : dans une requête
SELECT ma_fonction('arg1', 'arg2') ;

SELECT * FROM ma_fonction('arg1', 'arg2') ;

INSERT INTO matable
SELECT ma_fonction( champ1, champ2 )   FROM ma_table2 ;

CALL ma_procedure( mafonction() );

CREATE INDEX ON ma_table ( ma_fonction(ma_colonne) );

Contrôle transactionnel dans les procédures

  • COMMIT et ROLLBACK : possibles dans les procédures
  • Pas de BEGIN
    • automatique après la fin d’une transaction dans le code
  • Un seul niveau de transaction
    • pas de sous-transactions
    • pas d’appel depuis une transaction
  • Incompatible avec une clause EXCEPTION

Création et maintenance des fonctions et procédures

Création

  • CREATE FUNCTION
  • CREATE PROCEDURE

Structure d’une routine PL/pgSQL

  • Reprenons le code montré plus haut :
CREATE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS '
DECLARE
  resultat integer;
BEGIN
  resultat := entier1 + entier2 ;
  RETURN resultat ;
END';

Structure d’une routine PL/pgSQL (suite)

  • DECLARE
    • déclaration des variables locales
  • BEGIN
    • début du code de la routine
  • END
    • la fin
  • Instructions séparées par des points-virgules
  • Commentaires commençant par -- ou compris entre /* et */

Blocs nommés

  • Labels de bloc possibles
  • Plusieurs blocs d’exception possibles dans une routine
  • Permet de préfixer des variables avec le label du bloc
  • De donner un label à une boucle itérative
  • Et de préciser de quelle boucle on veut sortir, quand plusieurs d’entre elles sont imbriquées

Modification du code d’une routine

  • CREATE OR REPLACE FUNCTION
  • CREATE OR REPLACE PROCEDURE
  • Une routine est définie par son nom et ses arguments
  • Si type de retour différent, la fonction doit d’abord être supprimée puis recréée

Modification des méta-données d’une routine

  • ALTER FUNCTION / ALTER PROCEDURE
  • Une routine est définie par son nom et ses arguments
  • Permet de modifier nom, propriétaire, schéma et autres options

Suppression d’une routine

  • Une routine est définie par son nom et ses arguments :
DROP FUNCTION addition (integer, integer) ;
DROP PROCEDURE public.vide_tables (boolean);
DROP PROCEDURE public.vide_tables ();

Utilisation des guillemets

  • Les guillemets deviennent très rapidement pénibles
    • préférer $$
    • ou $fonction$, $toto$

Paramètres et retour des fonctions et procédures

Version minimaliste

CREATE FUNCTION fonction (entier integer, texte text)
RETURNS int  AS

Paramètres IN, OUT, INOUT & retour

CREATE FUNCTION cree_utilisateur (
  nom text,                 -- IN
  type_id int DEFAULT 0     -- IN
) RETURNS id_utilisateur int  AS
CREATE FUNCTION explose_date (
  IN  d date,
  OUT jour int, OUT mois int, OUT annee int
 ) AS
  • VARIADIC : nombre variable

Type en retour : 1 valeur simple

  • Fonctions uniquement
RETURNS type     -- int, text, etc
  • Tous les types de base & utilisateur
  • Rien : void

Type en retour : 1 ligne, plusieurs champs (exemple)

Comment obtenir ceci ?

SELECT * FROM explose_date ('31-12-2020');
 jour | mois | annee
------+------+-------
   31 |    0 |  2020

Type en retour : 1 ligne, plusieurs champs

3 options :

  • Type composé dédié
CREATE TYPE ma_structure AS ( … ) ;
CREATE FUNCTION …  RETURNS ma_structure ;
  • Paramètres OUT
CREATE FUNCTION explose_date (IN d date,
                              OUT jour int, OUT mois int, OUT annee int) AS
  • RETURNS TABLE
CREATE FUNCTION explose_date_table (d date)
RETURNS TABLE  (jour integer, mois integer, annee integer) AS…

Retour multiligne

  • 1 seul champ ou plusieurs ?
RETURNS SETOF type   -- int, text, type personnalisé
RETURNS TABLE ( col1 type, col2 type … )
  • Ligne à ligne ou en bloc ?
RETURN NEXT
RETURN QUERY   SELECT
RETURN QUERY   EXECUTE
  • Le résultat est stocké puis envoyé

Gestion des valeurs NULL

Comment gérer les paramètres à NULL ?

  • STRICT :
    • 1 paramètre NULL : retourne NULL immédiatement
  • Défaut :
    • gestion par la fonction

Variables en PL/pgSQL

Clause DECLARE

  • Dans le source, partie DECLARE :
  DECLARE
    i  integer;
    j  integer := 5;
    k  integer NOT NULL DEFAULT 1;
    ch text    COLLATE "fr_FR";
  • Blocs DECLARE/BEGIN/END imbriqués possible
    • restriction de scope de variable

Constantes

  • Clause supplémentaire CONSTANT :
  DECLARE
    eur_to_frf   CONSTANT numeric := 6.55957 ;
    societe_nom  CONSTANT text    := 'Dalibo SARL';

Types de variables

  • Récupérer le type d’une autre variable avec %TYPE :
    quantite    integer ;
    total       quantite%TYPE ;
  • Récupérer le type de la colonne d’une table :
    quantite    ma_table.ma_colonne%TYPE ;

Type ROW - 1

  • Pour renvoyer plusieurs valeurs à partir d’une fonction
  • Utiliser un type composite :
    CREATE TYPE ma_structure AS (
        un_entier integer,
        une_chaine text,
        …);
    CREATE FUNCTION ma_fonction () RETURNS ma_structure …;

Type ROW - 2

  • Utiliser le type composite défini par la ligne d’une table
    CREATE FUNCTION ma_fonction () RETURNS integer
    AS $$
    DECLARE
      ligne ma_table%ROWTYPE;

    $$

Type RECORD

  • RECORD identique au type ROW
    • …sauf que son type n’est connu que lors de son affectation
  • RECORD peut changer de type au cours de l’exécution de la routine
  • Curseur et boucle sur une requête

Type RECORD : exemple

CREATE FUNCTION ma_fonction () RETURNS integer
AS $$
DECLARE
  ligne RECORD;
BEGIN
  -- récupération de la 1è ligne uniquement
  SELECT * INTO ligne FROM ma_première_table;
  -- ou : traitement ligne à ligne
  FOR ligne IN SELECT * FROM ma_deuxième_table  LOOP

  END LOOP ;
  RETURN … ;
END $$ ;

Exécution de requête dans un bloc PL/pgSQL

Requête dans un bloc PL/pgSQL

  • Toutes opérations sur la base de données
  • Et calculs, comparaisons, etc.
  • Toute expression écrite en PL/pgSQL sera passée à SELECT pour interprétation par le moteur
  • PREPARE implicite, avec cache

Affectation d’une valeur à une variable

  • Utiliser l’opérateur := :
    un_entier := 5;
  • Utiliser SELECT INTO :
    SELECT 5 INTO un_entier;

Exécution d’une requête

  • Affectation de la ligne :

    SELECT *
    INTO ma_variable_ligne  -- type ROW ou RECORD
    FROM …;
  • INTO STRICT pour garantir unicité

    • INTO seul : juste 1è ligne !
  • Plus d’un enregistrement :

    • écrire une boucle
  • Ordre statique :

    • colonnes, clause WHERE, tables figées

Exécution d’une requête sans besoin du résultat

  • PERFORM : résultat ignoré
PERFORM * FROM ma_table WHERE une_colonne>0 ;
PERFORM mafonction (argument1) ;
  • Variable FOUND
    • si une ligne est affectée par l’instruction
  • Nombre de lignes :
GET DIAGNOSTICS variable = ROW_COUNT;

SQL dynamique

EXECUTE d’une requête

EXECUTE 'chaine' [INTO [STRICT] cible] [USING (paramètres)] ;
  • Exécute la requête dans chaine
  • chaine peut être construite à partir d’autres variables
  • cible : résultat (une seule ligne)

EXECUTE & requête dynamique : injection SQL

Si nom vaut : « 'Robert' ; DROP TABLE eleves ; »

que renvoie ceci ?

EXECUTE 'SELECT * FROM eleves WHERE nom = '|| nom ;

EXECUTE & requête dynamique : 3 possibilités

EXECUTE 'UPDATE tbl SET '
    || quote_ident(nom_colonne)
    || ' = '
    || quote_literal(nouvelle_valeur)
    || ' WHERE cle = '
    || quote_literal(valeur_cle) ;
EXECUTE format('UPDATE matable SET %I = %L '
   'WHERE clef = %L', nom_colonne, nouvelle_valeur, valeur_clef);
EXECUTE format('UPDATE table SET %I = $1 '
   'WHERE clef = $2', nom_colonne) USING nouvelle_valeur, valeur_clef;

EXECUTE & requête dynamique (suite)

EXECUTE 'chaine' [INTO STRICT cible] [USING (paramètres)] ;
  • STRICT : 1 résultat
    • sinon NO_DATA_FOUND ou TOO_MANY_ROWS
  • Sans STRICT :
    • 1ère ligne ou NO_DATA_FOUND
  • Nombre de lignes :
    • GET DIAGNOSTICS integer_var = ROW_COUNT

Outils pour construire une requête dynamique

  • quote_ident ()
    • pour mettre entre guillemets un identifiant d’un objet PostgreSQL (table, colonne, etc.)
  • quote_literal ()
    • pour mettre entre guillemets une valeur (chaîne de caractères)
  • quote_nullable ()
    • pour mettre entre guillemets une valeur (chaîne de caractères), sauf NULL qui sera alors renvoyé sans les guillemets
  • || : concaténer
  • Ou fonction format(…), équivalent de sprintf en C

Structures de contrôle en PL/pgSQL

  • But du PL : les traitements procéduraux

Tests conditionnels - 2

Exemple :

IF nombre = 0 THEN
  resultat := 'zero';
ELSEIF nombre > 0 THEN
   resultat := 'positif';
ELSEIF nombre < 0 THEN
   resultat := 'négatif';
ELSE
   resultat := 'indéterminé';
END IF;

Tests conditionnels : CASE

    CASE nombre
    WHEN nombre = 0  THEN 'zéro'
    WHEN variable > 0  THEN 'positif'
    WHEN variable < 0  THEN 'négatif'
    ELSE 'indéterminé'
    END CASE

ou :

CASE current_setting ('server_version_num')::int/10000
    WHEN 8,9,10,11      THEN RAISE NOTICE 'Version non supportée !!' ;
    WHEN 12,13,14,15,16 THEN RAISE NOTICE 'Version supportée' ;
    ELSE                RAISE NOTICE 'Version inconnue (fin 2023)' ;
END CASE ;

Boucle LOOP/EXIT/CONTINUE : syntaxe

  • Boucle :
    • LOOP / END LOOP
    • label possible
  • En sortir :
    • EXIT [label] [WHEN expression_booléenne]
  • Commencer une nouvelle itération de la boucle
    • CONTINUE [label] [WHEN expression_booléenne]

Boucle LOOP/EXIT/CONTINUE : exemple

LOOP
  resultat := resultat + 1;
  EXIT WHEN resultat > 100;
  CONTINUE WHEN resultat < 50;
  resultat := resultat + 1;
END LOOP;

Boucle WHILE

    WHILE condition LOOP

    END LOOP;
  • Boucle jusqu’à ce que la condition soit fausse
  • Label possible

Boucle FOR : syntaxe

    FOR variable in [REVERSE] entier1..entier2 [BY incrément]
    LOOP

    END LOOP;
  • variable va obtenir les différentes valeurs entre entier1 et entier2
  • Label possible

Boucle FOR … IN … LOOP : parcours de résultat de requête

    FOR ligne IN ( SELECT * FROM ma_table ) LOOP

    END LOOP;
  • Pour boucler dans les lignes résultats d’une requête
  • ligne de type RECORD, ROW, ou liste de variables séparées par des virgules
  • Utilise un curseur en interne
  • Label possible

Boucle FOREACH

    FOREACH variable [SLICE n] IN ARRAY expression LOOP

    END LOOP ;
  • Pour boucler sur les éléments d’un tableau
  • variable va obtenir les différentes valeurs du tableau retourné par expression
  • SLICE permet de jouer sur le nombre de dimensions du tableau à passer à la variable
  • Label possible

Autres propriétés des fonctions

  • Sécurité
  • Optimisations
  • Parallélisation

Politique de sécurité

  • SECURITY INVOKER : défaut

  • SECURITY DEFINER

    • « sudo de la base de données »
    • potentiellement dangereux
    • ne pas laisser à public !

Optimisation des fonctions

  • Fonctions uniquement
  • À destination de l’optimiseur
  • COST cout_execution
    • coût estimé pour l’exécution de la fonction
  • ROWS nb_lignes_resultat
    • nombre estimé de lignes que la fonction renvoie

Parallélisation

  • Fonctions uniquement
  • La fonction peut-elle être exécutée en parallèle ?
    • PARALLEL UNSAFE (défaut)
    • PARALLEL RESTRICTED
    • PARALLEL SAFE

Utilisation de fonctions dans les index

  • Fonctions uniquement !
  • IMMUTABLE | STABLE | VOLATILE
  • Ce mode précise la « volatilité » de la fonction.
  • Permet de réduire le nombre d’appels
  • Index : fonctions immutables uniquement (sinon problèmes !)

Conclusion

  • Grand nombre de structure de contrôle (test, boucle, etc.)
  • Facile à utiliser et à comprendre

Pour aller plus loin

  • Documentation officielle
    • « Chapitre 40. PL/pgSQL - Langage de procédures SQL »
  • Module de formation Dalibo P2
    • variadic, routines polymorphes
    • triggers, tables de transition
    • curseurs
    • gestion des erreurs
    • sécurité
    • optimisation

Questions

FOR q IN (SELECT * FROM questions ) LOOP

  répondre (q) ;

END LOOP ;

Quiz

Travaux pratiques

L’exercice sur les index fonctionnels utilise la base magasin. La base magasin (dump de 96 Mo, pour 667 Mo sur le disque au final) peut être téléchargée et restaurée comme suit dans une nouvelle base magasin :

createdb magasin
curl -kL https://dali.bo/tp_magasin -o /tmp/magasin.dump
pg_restore -d magasin  /tmp/magasin.dump
# le message sur public préexistant est normal
rm -- /tmp/magasin.dump

Toutes les données sont dans deux schémas nommés magasin et facturation.

Hello

Division

SELECT sur des tables dans les fonctions

Multiplication

Salutations

Inversion de chaîne

Jours fériés

Index fonctionnels

Travaux pratiques (solutions)