Dalibo SCOP
Formation | Module P1 |
Titre | PL/pgSQL : Les bases |
Révision | 24.09 |
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 |
Vous trouverez en ligne les différentes versions complètes de ce document.
Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :
Vous n’avez pas le droit d’utiliser cette création à des fins commerciales.
Si vous modifiez, transformez ou adaptez cette création, vous n’avez le droit de distribuer la création qui en résulte que sous un contrat identique à celui-ci.
Vous devez citer le nom de l’auteur original de la manière indiquée par l’auteur de l’œuvre ou le titulaire des droits qui vous confère cette autorisation (mais pas d’une manière qui suggérerait qu’ils vous soutiennent ou approuvent votre utilisation de l’œuvre). À chaque réutilisation ou distribution de cette création, vous devez faire apparaître clairement au public les conditions contractuelles de sa mise à disposition. La meilleure manière de les indiquer est un lien vers cette page web. Chacune de ces conditions peut être levée si vous obtenez l’autorisation du titulaire des droits sur cette œuvre. Rien dans ce contrat ne diminue ou ne restreint le droit moral de l’auteur ou des auteurs.
Le texte complet de la licence est disponible sur http://creativecommons.org/licenses/by-nc-sa/2.0/fr/legalcode
Cela inclut les diapositives, les manuels eux-mêmes et les travaux pratiques. Cette formation peut également contenir quelques images et schémas dont la redistribution est soumise à des licences différentes qui sont alors précisées.
PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.
Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 12 à 16.
Sur les versions précédentes susceptibles d’être encore rencontrées en production, seuls quelques points très importants sont évoqués, en plus éventuellement de quelques éléments historiques.
Sauf précision contraire, le système d’exploitation utilisé est Linux.
Ce module présente la programmation PL/pgSQL. Il commence par décrire les routines stockées et les différents langages disponibles. Puis il aborde les bases du langage PL/pgSQL, autrement dit :
PL est l’acronyme de « Procedural Languages ». En dehors du C et du SQL, tous les langages acceptés par PostgreSQL sont des PL.
Par défaut, trois langages sont installés et activés : C, SQL et PL/pgSQL.
Les quatre langages PL supportés nativement (en plus du C et du SQL bien sûr) sont décrits en détail dans la documentation officielle :
D’autres langages PL sont accessibles en tant qu’extensions tierces. Les plus stables sont mentionnés dans la documentation, comme PL/Java ou PL/R. Ils réclament généralement d’installer les bibliothèques du langage sur le serveur.
Une liste plus large est par ailleurs disponible sur le wiki PostgreSQL, Il en ressort qu’au moins 16 langages sont disponibles, dont 10 installables en production. De plus, il est possible d’en ajouter d’autres, comme décrit dans la documentation.
Les langages de confiance ne peuvent accéder qu’à la base de données. Ils ne peuvent pas accéder aux autres bases, aux systèmes de fichiers, au réseau, etc. Ils sont donc confinés, ce qui les rend moins facilement utilisables pour compromettre le système. PL/pgSQL est l’exemple typique. Mais de ce fait, ils offrent moins de possibilités que les autres langages.
Seuls les superutilisateurs peuvent créer une routine dans un langage untrusted. Par contre, ils peuvent ensuite donner les droits d’exécution à ces routines aux autres rôles dans la base :
GRANT EXECUTE ON FUNCTION nom_fonction TO un_role ;
La question se pose souvent de placer la logique applicative du côté de la base, dans un langage PL, ou des clients. Il peut y avoir de nombreuses raisons en faveur de la première option. Simplifier et centraliser des traitements clients directement dans la base est l’argument le plus fréquent. Par exemple, une insertion complexe dans plusieurs tables, avec mise en place d’identifiants pour liens entre ces tables, peut évidemment être écrite côté client. Il est quelquefois plus pratique de l’écrire sous forme de PL. Les avantages sont :
Centralisation du code :
Si plusieurs applications ont potentiellement besoin d’opérer un même traitement, à fortiori dans des langages différents, porter cette logique dans la base réduit d’autant les risques de bugs et facilite la maintenance.
Une règle peut être que tout ce qui a trait à l’intégrité des données devrait être exécuté au niveau de la base.
Performances :
Le code s’exécute localement, directement dans le moteur de la base. Il n’y a donc pas tous les changements de contexte et échanges de messages réseaux dus à l’exécution de nombreux ordres SQL consécutifs. L’impact de la latence due au trafic réseau de la base au client est souvent sous-estimée.
Les langages PL permettent aussi d’accéder à leurs bibliothèques spécifiques (extrêmement nombreuses en python ou perl, entre autres).
Une fonction en PL peut également servir à l’indexation des données. Cela est impossible si elle se calcule sur une autre machine.
Simplicité :
Suivant le besoin, un langage PL peut être bien plus pratique que le langage client.
Il est par exemple très simple d’écrire un traitement d’insertion/mise à jour en PL/pgSQL, le langage étant créé pour simplifier ce genre de traitements, et la gestion des exceptions pouvant s’y produire. Si vous avez besoin de réaliser du traitement de chaîne puissant, ou de la manipulation de fichiers, PL/Perl ou PL/Python seront probablement des options plus intéressantes car plus performantes, là aussi utilisables dans la base.
La grande variété des différents langages PL supportés par PostgreSQL permet normalement d’en trouver un correspondant aux besoins et aux langages déjà maîtrisés dans l’entreprise.
Les langages PL permettent donc de rajouter une couche d’abstraction et d’effectuer des traitements avancés directement en base.
Le langage étant assez ancien, proche du Pascal et de l’ADA, sa syntaxe ne choquera personne. Elle est d’ailleurs très proche de celle du PLSQL d’Oracle.
Le PL/pgSQL permet d’écrire des requêtes directement dans le code PL sans déclaration préalable, sans appel à des méthodes complexes, ni rien de cette sorte. Le code SQL est mélangé naturellement au code PL, et on a donc un sur-ensemble procédural de SQL.
PL/pgSQL étant intégré à PostgreSQL, il hérite de tous les types déclarés dans le moteur, même ceux rajoutés par l’utilisateur. Il peut les manipuler de façon transparente.
PL/pgSQL est trusted. Tous les utilisateurs peuvent donc
créer des routines dans ce langage (par défaut). Vous pouvez toujours
soit supprimer le langage, soit retirer les droits à un utilisateur sur
ce langage (via la commande SQL REVOKE
).
PL/pgSQL est donc raisonnablement facile à utiliser : il y a peu de complications, peu de pièges, et il dispose d’une gestion des erreurs évoluée (gestion d’exceptions).
Les langages PL « autres », comme PL/perl et PL/Python (les deux plus utilisés après PL/pgSQL), sont bien plus évolués que PL/PgSQL. Par exemple, ils sont bien plus efficaces en matière de traitement de chaînes de caractères, possèdent des structures avancées comme des tables de hachage, permettent l’utilisation de variables statiques pour maintenir des caches, voire, pour leur version untrusted, peuvent effectuer des appels systèmes. Dans ce cas, il devient possible d’appeler un service web par exemple, ou d’écrire des données dans un fichier externe.
Il existe des langages PL spécialisés. Le plus emblématique d’entre eux est PL/R. R est un langage utilisé par les statisticiens pour manipuler de gros jeux de données. PL/R permet donc d’effectuer ces traitements R directement en base, traitements qui seraient très pénibles à écrire dans d’autres langages, et avec une latence dans le transfert des données.
Il existe aussi un langage qui est, du moins sur le papier, plus rapide que tous les langages cités précédemment : vous pouvez écrire des procédures stockées en C, directement. Elles seront compilées à l’extérieur de PostgreSQL, en respectant un certain formalisme, puis seront chargées en indiquant la bibliothèque C qui les contient et leurs paramètres et types de retour.Mais attention : toute erreur dans le code C est susceptible d’accéder à toute la mémoire visible par le processus PostgreSQL qui l’exécute, et donc de corrompre les données. Il est donc conseillé de ne faire ceci qu’en dernière extrémité.
Le gros défaut est simple et commun à tous ces langages : ils ne sont
pas spécialement conçus pour s’exécuter en tant que langage de
procédures stockées. Ce que vous utilisez quand vous écrivez du PL/Perl
est donc du code Perl, avec quelques fonctions supplémentaires
(préfixées par spi
) pour accéder à la base de données ; de
même en C. L’accès aux données est assez fastidieux au niveau
syntaxique, comparé à PL/pgSQL.
Un autre problème des langages PL (autre que C et PL/pgSQL), est que ces langages n’ont pas les mêmes types natifs que PostgreSQL, et s’exécutent dans un interpréteur relativement séparé. Les performances sont donc moindres que PL/pgSQL et C, pour les traitements dont le plus consommateur est l’accès aux données. Souvent, le temps de traitement dans un de ces langages plus évolués est tout de même meilleur grâce au temps gagné par les autres fonctionnalités (la possibilité d’utiliser un cache, ou une table de hachage par exemple).
Les programmes écrits à l’aide des langages PL sont habituellement enregistrés sous forme de « routines » :
Le code source de ces objets est stocké dans la table
pg_proc
du catalogue.
Les procédures, apparues avec PostgreSQL 11, sont très similaires aux fonctions. Les principales différences entre les deux sont :
RETURNS
ou arguments OUT
). Elles peuvent
renvoyer n’importe quel type de donnée, ou des ensembles de lignes. Il
est possible d’utiliser void
pour une fonction sans
argument de sortie ; c’était d’ailleurs la méthode utilisée pour émuler
le comportement d’une procédure avant leur introduction avec PostgreSQL
11. Les procédures n’ont pas de code retour (on peut cependant utiliser
des paramètres OUT
ou INOUT
).COMMIT
) ou annuler
(ROLLBACK
) les modifications effectuées jusqu’à ce point
par la procédure. L’intégralité d’une fonction s’effectue dans la
transaction appelante.CALL
; les fonctions peuvent être appelées dans la plupart
des ordres DML/DQL (notamment SELECT
), mais pas par
CALL
.Pour savoir si PL/Perl ou PL/Python a été compilé, on peut demander à
pg_config
:
pg_config --configure
'--prefix=/usr/local/pgsql-10_icu' '--enable-thread-safety'
'--with-openssl' '--with-libxml' '--enable-nls' '--with-perl' '--enable-debug'
'ICU_CFLAGS=-I/usr/local/include/unicode/'
'ICU_LIBS=-L/usr/local/lib -licui18n -licuuc -licudata' '--with-icu'
Si besoin, les emplacements exacts d’installation des bibliothèques
peuvent être récupérés à l’aide des options --libdir
et
--pkglibdir
de pg_config
.
Cependant, dans les paquets fournis par le PGDG, il faudra installer
explicitement le paquet dédié à plperl
pour la version
majeure de PostgreSQL concernée. Pour PostgreSQL 16, les paquets sont
postgresql16-plperl
(depuis yum.postgresql.org) ou
postgresql-plperl-16
(depuis apt.postgresql.org). De même
pour Python 3 (paquets postgresql14-plpython3
ou
postgresql-plython3-14
).
Les bibliothèques plperl.so
, plpython3.so
ou plpgsql.so
contiennent les fonctions qui permettent
l’utilisation de chaque langage. La bibliothèque nécessaire est chargée
par le moteur à la première utilisation d’une procédure utilisant ce
langage.
La plupart des langages intéressants sont disponibles sous forme de paquets. Des versions très récentes, ou des langages plus exotiques, peuvent nécessiter une compilation de l’extension.
Le langage est activé uniquement dans la base dans laquelle la
commande est lancée. Il faudra donc répéter le
CREATE EXTENSION
dans chaque base au besoin (noter
qu’activer un langage dans la base modèle template1
l’activera aussi pour toutes les bases créées par la suite, comme c’est
déjà le cas pour le PL/pgSQL).
Pour voir les langages activés, utiliser la commande \dL
qui reprend le contenu de la table système
pg_language
:
CREATE EXTENSION plperl ;
CREATE EXTENSION plpython3u ;
CREATE EXTENSION plsh ;
CREATE EXTENSION plr;
postgres=# \dL
Liste des langages
Nom | … | De confiance | Description
------------+---+--------------+-------------------------------------------
plperl | … | t | PL/PerlU untrusted procedural language
plpgsql | … | t | PL/pgSQL procedural language
plpython3u | … | f | PL/Python3U untrusted procedural language
plr | … | f | plsh | … | f | PL/sh procedural language
Noter la distinction entre les langages trusted (de confiance) et untrusted. Si un langage est trusted, tous les utilisateurs peuvent créer des procédures dans ce langage sans danger. Sinon seuls les superutilisateurs le peuvent.
Il existe par exemple deux variantes de PL/Perl : PL/Perl et PL/PerlU. La seconde est la variante untrusted et est un Perl « complet ». La version trusted n’a pas le droit d’ouvrir des fichiers, des sockets, ou autres appels systèmes qui seraient dangereux.
SQL, PL/pgSQL, PL/Tcl, PL/Perl (mais pas PL/Python) sont trusted et les utilisateurs peuvent les utiliser à volonté.
C, PL/TclU, PL/PerlU, et PL/Python3U sont untrusted. Un
superutilisateur doit alors écrire les fonctions et procédures et opérer
des GRANT EXECUTE
aux utilisateurs.
SELECT addition (1,2);
addition
---------- 3
Les fonctions simples peuvent être écrites en SQL pur. La syntaxe est plus claire, mais bien plus limitée qu’en PL/pgSQL (ni boucles, ni conditions, ni exceptions notamment).
À partir de PostgreSQL 14, il est possible de se passer des guillemets encadrants, pour les fonctions SQL uniquement. La même fonction devient donc :
CREATE OR REPLACE FUNCTION addition (entier1 integer, entier2 integer)
integer
RETURNS
LANGUAGE sql
IMMUTABLERETURN entier1 + entier2 ;
Cette nouvelle écriture respecte mieux le standard SQL. Surtout, elle autorise un parsing et une vérification des objets impliqués dès la déclaration, et non à l’utilisation. Les dépendances entre fonctions et objets utilisés sont aussi mieux tracées.
L’avantage principal des fonctions en pur SQL est, si elles sont assez simples, leur intégration lors de la réécriture interne de la requête (inlining) : elles ne sont donc pas pour l’optimiseur des « boîtes noires ». À l’inverse, l’optimiseur ne sait rien du contenu d’une fonction PL/pgSQL.
Dans l’exemple suivant, la fonction sert de filtre à la requête.
Comme elle est en pur SQL, elle permet d’utiliser l’index sur la colonne
date_embauche
de la table employes_big
:
CREATE FUNCTION employe_eligible_prime_sql (service int, date_embauche date)
boolean
RETURNS
LANGUAGE sqlAS $$
SELECT ( service !=3 AND date_embauche < '2003-01-01') ;
$$ ;
EXPLAIN (ANALYZE) SELECT matricule, num_service, nom, prenom
FROM employes_big
WHERE employe_eligible_prime_sql (num_service, date_embauche) ;
QUERY PLAN
---------------------------------------------------------------------------------
Index Scan using employes_big_date_embauche_idx on employes_big
(cost=0.42..1.54 rows=1 width=22) (actual time=0.008..0.009 rows=1 loops=1)
Index Cond: (date_embauche < '2003-01-01'::date)
Filter: (num_service <> 3)
Rows Removed by Filter: 1
Planning Time: 0.102 ms Execution Time: 0.029 ms
Avec une version de la même fonction en PL/pgSQL, le planificateur ne voit pas le critère indexé. Il n’a pas d’autre choix que de lire toute la table et d’appeler la fonction pour chaque ligne, ce qui est bien sûr plus lent :
CREATE FUNCTION employe_eligible_prime_pl (service int, date_embauche date)
boolean
RETURNS AS $$
LANGUAGE plpgsql BEGIN
RETURN ( service !=3 AND date_embauche < '2003-01-01') ;
END ;
$$ ;
EXPLAIN (ANALYZE) SELECT matricule, num_service, nom, prenom
FROM employes_big
WHERE employe_eligible_prime_pl (num_service, date_embauche) ;
QUERY PLAN
---------------------------------------------------------------------------------
Seq Scan on employes_big (cost=0.00..134407.90 rows=166338 width=22)
(actual time=0.069..269.121 rows=1 loops=1)
Filter: employe_eligible_prime_pl(num_service, date_embauche)
Rows Removed by Filter: 499014
Planning Time: 0.038 ms Execution Time: 269.157 ms
Le wiki
décrit les conditions pour que l’inlining des fonctions SQL
fonctionne : obligation d’un seul SELECT
, interdiction de
certains fonctionnalités…
Dans cet exemple, on récupère l’estimation du nombre de lignes actives d’une table passée en paramètres.
L’intérêt majeur du PL/pgSQL et du SQL sur les autres langages est la
facilité d’accès aux données. Ici, un simple
SELECT <champ> INTO <variable>
suffit à
récupérer une valeur depuis une table dans une variable.
SELECT nb_lignes_table ('public', 'pgbench_accounts');
nb_lignes_table
----------------- 10000000
Voici l’exemple de la fonction :
CREATE OR REPLACE FUNCTION
public.demo_insert_perl(nom_client text, titre_facture text)integer
RETURNS
LANGUAGE plperl
STRICT$function$
AS use strict;
my ($nom_client, $titre_facture)=@_;
my $rv;
my $id_facture;
my $id_client;
# Le client existe t'il ?
$rv = spi_exec_query('SELECT id_client FROM mes_clients WHERE nom_client = '
$nom_client)
. quote_literal(
);# Sinon on le crée :
if ($rv->{processed} == 0)
{$rv = spi_exec_query('INSERT INTO mes_clients (nom_client) VALUES ('
$nom_client) . ') RETURNING id_client'
. quote_literal(
);
}# Dans les deux cas, l'id client est dans $rv :
$id_client=$rv->{rows}[0]->{id_client};
# Insérons maintenant la facture
$rv = spi_exec_query(
'INSERT INTO mes_factures (titre_facture, id_client) VALUES ('
$titre_facture) . ", $id_client ) RETURNING id_facture"
. quote_literal(
);
$id_facture = $rv->{rows}[0]->{id_facture};
return $id_facture;
$function$ ;
Cette fonction n’est pas parfaite, elle ne protège pas de tout. Il
est tout à fait possible d’avoir une insertion concurrente entre le
SELECT
et le INSERT
par exemple.
Il est clair que l’accès aux données est malaisé en PL/Perl, comme dans la plupart des langages, puisqu’ils ne sont pas prévus spécifiquement pour cette tâche. Par contre, on dispose de toute la puissance de Perl pour les traitements de chaîne, les appels système…
PL/Perl, c’est :
spi_*
Pour éviter les conflits avec les objets de la base, il est conseillé de préfixer les variables.
CREATE OR REPLACE FUNCTION
public.demo_insert_plpgsql(p_nom_client text, p_titre_facture text)
integer
RETURNS
LANGUAGE plpgsql
STRICTAS $function$
DECLARE
int;
v_id_facture int;
v_id_client BEGIN
-- Le client existe t'il ?
SELECT id_client
INTO v_id_client
FROM mes_clients
WHERE nom_client = p_nom_client;
-- Sinon on le crée :
IF NOT FOUND THEN
INSERT INTO mes_clients (nom_client)
VALUES (p_nom_client)
RETURNING id_client INTO v_id_client;
END IF;
-- Dans les deux cas, l'id client est maintenant dans v_id_client
-- Insérons maintenant la facture
INSERT INTO mes_factures (titre_facture, id_client)
VALUES (p_titre_facture, v_id_client)
RETURNING id_facture INTO v_id_facture;
return v_id_facture;
END;
$function$ ;
Cette procédure tronque des tables de la base d’exemple
pgbench, et annule si dry_run
est
vrai.
Les procédures sont récentes dans PostgreSQL (à partir de la version
11). Elles sont à utiliser quand on n’attend pas de résultat en retour.
Surtout, elles permettent de gérer les transactions
(COMMIT
, ROLLBACK
), ce qui ne peut se faire
dans des fonctions, même si celles-ci peuvent modifier les données.
Une procédure ne peut utiliser le contrôle transactionnel que si elle est appelée en dehors de toute transaction.
Comme pour les fonctions, il est possible d’utiliser le SQL pur dans les cas les plus simples, sans contrôle transactionnel notamment :
CREATE OR REPLACE PROCEDURE vide_tables ()
AS '
TRUNCATE TABLE pgbench_history ;
TRUNCATE TABLE pgbench_accounts CASCADE ;
TRUNCATE TABLE pgbench_tellers CASCADE ;
TRUNCATE TABLE pgbench_branches CASCADE ;
' LANGUAGE sql;
Toujours pour les procédures en SQL, il existe une variante sans guillemets, à partir de PostgreSQL 14, mais qui ne supporte pas tous les ordres. Comme pour les fonctions, l’intérêt est la prise en compte des dépendances entre objets et procédures.
CREATE OR REPLACE PROCEDURE vide_tables ()
BEGIN ATOMIC
DELETE FROM pgbench_history ;
DELETE FROM pgbench_accounts ;
DELETE FROM pgbench_tellers ;
DELETE FROM pgbench_branches ;
END ;
Les blocs anonymes sont utiles pour des petits scripts ponctuels qui nécessitent des boucles ou du conditionnel, voire du transactionnel, sans avoir à créer une fonction ou une procédure. Ils ne renvoient rien. Ils sont habituellement en PL/pgSQL mais tout langage procédural installé est possible.
L’exemple ci-dessus lance un ANALYZE
sur toutes les
tables où les statistiques n’ont pas été calculées d’après la vue
système, et donne aussi un exemple de SQL dynamique. Le résultat est par
exemple :
NOTICE: Analyze public.pgbench_history
NOTICE: Analyze public.pgbench_tellers
NOTICE: Analyze public.pgbench_accounts
NOTICE: Analyze public.pgbench_branches
DO
Temps : 141,208 ms
(Pour ce genre de SQL dynamique, si l’on est sous psql
,
il est souvent plus pratique d’utiliser \gexec
.)
Noter que les ordres constituent une transaction unique, à moins de
rajouter des COMMIT
ou ROLLBACK
explicitement
(ce n’est autorisé qu’à partir de la version 11).
Demander l’exécution d’une procédure se fait en utilisant un ordre
SQL spécifique : CALL
. Il suffit
de fournir les paramètres. Il n’y a pas de code retour.
Les fonctions ne sont quant à elles pas directement compatibles avec
la commande CALL
, il faut les invoquer dans le contexte
d’une commande SQL. Elles sont le plus couramment appelées depuis des
commandes de type DML (SELECT
, INSERT
, etc.),
mais on peut aussi les trouver dans d’autres commandes.
Voici quelques exemples :
SELECT
(la fonction ne doit renvoyer qu’une
seule ligne) :SELECT ma_fonction('arg1', 'arg2');
SELECT
, en passant en argument les valeurs
d’une colonne d’une table :SELECT ma_fonction(ma_colonne) FROM ma_table;
FROM
d’un SELECT
, la fonction
renvoit ici généralement plusieurs lignes (SETOF
), et un
résultat de type RECORD
:SELECT result FROM ma_fonction() AS f(result);
INSERT
pour générer la valeur à insérer :INSERT INTO ma_table(ma_colonne) VALUES ( ma_fonction() );
CREATE INDEX ON ma_table ( ma_fonction(ma_colonne) );
ma_fonction()
(qui doit renvoyer une seule ligne) est passé
en argument d’entrée de la procédure ma_procedure()
:CALL ma_procedure( ma_fonction() );
Par ailleurs, certaines fonctions sont spécialisées et ne peuvent être invoquées que dans le contexte pour lequel elles ont été conçues (fonctions trigger, d’agrégat, de fenêtrage, etc.).
Une procédure peut contenir des ordres COMMIT
ou
ROLLBACK
pour du contrôle transactionnel. (À l’inverse une
fonction est une transaction unique, ou opère dans une transaction.)
Voici un exemple validant ou annulant une insertion suivant que le nombre est pair ou impair :
CREATE TABLE test1 (a int) ;
CREATE OR REPLACE PROCEDURE transaction_test1()
LANGUAGE plpgsqlAS $$
BEGIN
FOR i IN 0..5 LOOP
INSERT INTO test1 (a) VALUES (i);
IF i % 2 = 0 THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END LOOP;
END
$$;
CALL transaction_test1();
SELECT * FROM test1;
a | b
---+---
0 |
2 | 4 |
Une exemple plus fréquemment utilisé est celui d’une procédure
effectuant un traitement de modification des données par lots, et donc
faisant un COMMIT
à intervalle régulier.
Noter qu’il n’y a pas de BEGIN
explicite dans la gestion
des transactions. Après un COMMIT
ou un
ROLLBACK
, un BEGIN
est immédiatement
exécuté.
On ne peut pas imbriquer des transactions, car PostgreSQL ne connaît pas les sous-transactions :
BEGIN ; CALL transaction_test1() ;
transaction termination
ERROR: invalid /pgSQL function transaction_test1() line 6 at COMMIT CONTEXTE : PL
On ne peut pas utiliser en même temps une clause
EXCEPTION
et le contrôle transactionnel :
DO LANGUAGE plpgsql $$BEGIN
BEGIN
INSERT INTO test1 (a) VALUES (1);
COMMIT;
INSERT INTO test1 (a) VALUES (1/0);
COMMIT;
EXCEPTION
WHEN division_by_zero THEN
'caught division_by_zero';
RAISE NOTICE END;
END;
$$;
commit while a subtransaction is active
ERREUR: cannot /pgSQL inline_code_block, ligne 5 à COMMIT CONTEXTE : fonction PL
Voici la syntaxe complète pour une fonction d’après la documentation :
CREATE [ OR REPLACE ] FUNCTION
DEFAULT | = } default_expr ] [, …] ] )
name ( [ [ argmode ] [ argname ] argtype [ {
[ RETURNS rettypeTABLE ( column_name column_type [, …] ) ]
| RETURNS
{ LANGUAGE lang_nameFOR TYPE type_name } [, … ]
| TRANSFORM {
| WINDOW
| { IMMUTABLE | STABLE | VOLATILE }NOT ] LEAKPROOF
| [ ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT }
| { CALLED DEFINER }
| { [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY PARALLEL { UNSAFE | RESTRICTED | SAFE }
| COST execution_cost
| ROWS result_rows
|
| SUPPORT support_functionSET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
| AS 'obj_file', 'link_symbol'
|
| sql_body } …
Voici la syntaxe complète pour une procédure d’après la documentation :
CREATE [ OR REPLACE ] PROCEDURE
DEFAULT | = } default_expr ] [, …] ] )
name ( [ [ argmode ] [ argname ] argtype [ {
{ LANGUAGE lang_nameFOR TYPE type_name } [, … ]
| TRANSFORM { DEFINER
| [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
| AS 'obj_file', 'link_symbol'
|
| sql_body } …
Noter qu’il n’y a pas de langage par défaut. Il est donc nécessaire de le spécifier à chaque création d’une routine comme dans les exemples ci-dessous.
Le langage PL/pgSQL n’est pas sensible à la casse, tout comme SQL
(sauf les noms des objets ou variables, si vous les mettez entre des
guillemets doubles). L’opérateur de comparaison est =
,
l’opérateur d’affectation :=
Une routine est composée d’un bloc de déclaration des variables
locales et d’un bloc de code. Le bloc de déclaration commence par le mot
clé DECLARE
et se termine avec le mot clé
BEGIN
. Ce mot clé est celui qui débute le bloc de code. La
fin est indiquée par le mot clé END
.
Toutes les instructions se terminent avec des points-virgules.
Attention, DECLARE
, BEGIN
et END
ne sont pas des instructions.
Il est possible d’ajouter des commentaires. --
indique
le début d’un commentaire qui se terminera en fin de ligne. Pour être
plus précis dans la délimitation, il est aussi possible d’utiliser la
notation C : /*
est le début d’un commentaire et
*/
la fin.
Indiquer le nom d’un label ainsi :
<<mon_label>>
-- le code (blocs DECLARE, BEGIN-END, et EXCEPTION)
ou bien (pour une boucle)
<<mon_label>> ]
[ LOOP
ordres …END LOOP [ mon_label ];
Bien sûr, il est aussi possible d’utiliser des labels pour des
boucles FOR
, WHILE
, FOREACH
.
On sort d’un bloc ou d’une boucle avec la commande EXIT
,
on peut aussi utiliser CONTINUE
pour passer à l’exécution
suivante d’une boucle sans terminer l’itération courante.
Par exemple :
WHEN compteur > 1; EXIT [mon_label]
Une routine est surchargeable. La seule façon de les différencier est de prendre en compte les arguments (nombre et type). Les noms des arguments peuvent être indiqués mais ils seront ignorés.
Deux routines identiques aux arguments près (on parle de prototype) ne sont pas identiques, mais bien deux routines distinctes.
CREATE OR REPLACE
a principalement pour but de modifier
le code d’une routine, mais il est aussi possible de modifier les
méta-données.
Toutes les méta-données discutées plus haut sont modifiables avec un
ALTER
.
La suppression se fait avec l’ordre DROP
.
Une fonction pouvant exister en plusieurs exemplaires, avec le même nom et des arguments de type différents, il faudra parfois parfois préciser ces derniers.
Définir une fonction entre guillemets simples ('
)
devient très pénible dès que la fonction doit en contenir parce qu’elle
contient elle-même des chaînes de caractères. PostgreSQL permet de
remplacer les guillemets par $$
, ou tout mot encadré de
$
.
Par exemple, on peut reprendre la syntaxe de déclaration de la
fonction addition()
précédente en utilisant cette
méthode :
CREATE FUNCTION addition (entier1 integer, entier2 integer)
integer
RETURNS
LANGUAGE plpgsql
IMMUTABLEAS $ma_fonction_addition$
DECLARE
integer;
resultat BEGIN
:= entier1 + entier2;
resultat RETURN resultat;
END
$ma_fonction_addition$;
Ce peut être utile aussi dans tout code réalisant une concaténation de chaînes de caractères contenant des guillemets. La syntaxe traditionnelle impose de les multiplier pour les protéger, et le code devient difficile à lire. :
:= requete || '' AND vin LIKE ''''bordeaux%'''' AND xyz '' requete
En voilà une simplification grâce aux dollars :
:= requete || $sql$ AND vin LIKE 'bordeaux%' AND xyz $sql$ requete
Si vous avez besoin de mettre entre guillemets du texte qui inclut
$$
, vous pouvez utiliser $Q$
, et ainsi de
suite. Le plus simple étant de définir un marqueur de fin de routine
plus complexe, par exemple incluant le nom de la fonction.
Ceci une forme de fonction très simple (et très courante) : deux paramètres en entrée (implicitement en entrée seulement), et une valeur en retour.
Dans le corps de la fonction, il est aussi possible d’utiliser une
notation numérotée au lieu des noms de paramètre : le premier argument a
pour nom $1
, le deuxième $2
, etc. C’est à
éviter.
Tous les types sont utilisables, y compris les types définis par l’utilisateur. En dehors des types natifs de PostgreSQL, PL/pgSQL ajoute des types de paramètres spécifiques pour faciliter l’écriture des routines.
Si le mode d’un argument est omis, IN
est la valeur
implicite : la valeur en entrée ne sera pas modifiée par la
fonction.
Un paramètre OUT
sera modifié. S’il s’agit d’une
variable d’un bloc PL appelant, sa valeur sera modifiée. Un paramètre
INOUT
est un paramètre en entrée qui peut être également
modifié. (Jusque PostgreSQL 13 inclus, les procédures ne supportent pas
les arguments OUT
, seulement IN
et
INOUT
.)
Dans le corps d’une fonction, RETURN
est inutile avec
des paramètres OUT
parce que c’est la valeur des paramètres
OUT
à la fin de la fonction qui est retournée, comme dans
l’exemple plus bas.
L’option VARIADIC
permet de définir une fonction avec un
nombre d’arguments libres à condition de respecter le type de l’argument
(comme printf
en C par exemple). Seul un argument
OUT
peut suivre un argument VARIADIC
:
l’argument VARIADIC
doit être le dernier de la liste des
paramètres en entrée puisque tous les paramètres en entrée suivant
seront considérées comme faisant partie du tableau variadic. Seuls les
arguments IN
et VARIADIC
sont utilisables avec
une fonction déclarée comme renvoyant une table (clause
RETURNS TABLE
, voir plus loin).
La clause DEFAULT
permet de rendre les paramètres
optionnels. Après le premier paramètre ayant une valeur par défaut, tous
les paramètres qui suivent doivent aussi avoir une valeur par défaut.
Pour rendre le paramètre optionnel, il doit être le dernier argument ou
alors les paramètres suivants doivent aussi avoir une valeur par
défaut.
Le type de retour (clause RETURNS
dans l’entête) est
obligatoire pour les fonctions et interdit pour les procédures.
Avant la version 11, il n’était pas possible de créer une procédure,
mais il était possible de créer une fonction se comportant globalement
comme une procédure en utilisant le type de retour
void
.
Des exemples plus haut utilisent des types simples, mais tous ceux de PostgreSQL ou les types créés par l’utilisateur sont utilisables.
Depuis le corps de la fonction, le résultat est renvoyé par un appel
à RETURN
(PL/pgSQL) ou SELECT
(SQL).
S’il y a besoin de renvoyer plusieurs valeurs à la fois, une première possibilité est de renvoyer un type composé défini auparavant.
Une alternative très courante est d’utiliser plusieurs paramètres
OUT
(et pas de clause RETURN
dans l’entête)
pour obtenir un enregistrement composite :
CREATE OR REPLACE FUNCTION explose_date
IN d date, OUT jour int, OUT mois int, OUT annee int)
(AS $$
SELECT extract (day FROM d)::int,
extract(month FROM d)::int, extract (year FROM d)::int
$$ LANGUAGE sql;
SELECT * FROM explose_date ('31-12-2020');
jour | mois | annee
------+------+------- 31 | 0 | 2020
(Noter que l’exemple ci-dessus est en simple SQL.)
La clause TABLE
est une autre alternative, sans doute
plus claire. Cet exemple devient alors, toujours en pur SQL :
CREATE OR REPLACE FUNCTION explose_date_table (d date)
TABLE (jour integer, mois integer, annee integer)
RETURNS
LANGUAGE sqlAS $$
SELECT extract (day FROM d)::int,
extract(month FROM d)::int, extract (year FROM d)::int ;
$$ ;
RETURNS SETOF :
Pour renvoyer plusieurs lignes, la première possibilité est de
déclarer un type de retour SETOF
. Cet exemple utilise
RETURN NEXT
pour renvoyer les lignes une à une :
CREATE OR REPLACE FUNCTION liste_entiers_setof (limite int)
integer
RETURNS SETOF
LANGUAGE plpgsqlAS $$
BEGIN
FOR i IN 1..limite LOOP
RETURN NEXT i;
END LOOP;
END
$$ ;
SELECT * FROM liste_entiers_setof (3) ;
liste_entiers_setof
---------------------
1
2 3
Renvoyer une structure existante :
S’il y a plusieurs champs à renvoyer, une possibilité est d’utiliser
un type dédié (composé), qu’il faudra cependant créer auparavant.
L’exemple suivant utilise aussi un RETURN QUERY
pour éviter
d’itérer sur toutes les lignes du résultat :
CREATE TYPE pgt AS (schemaname text, tablename text) ;
CREATE OR REPLACE FUNCTION tables_by_owner (p_owner text)
RETURNS SETOF pgt
LANGUAGE plpgsqlAS $$
BEGIN
RETURN QUERY SELECT schemaname::text, tablename::text
FROM pg_tables WHERE tableowner=p_owner
ORDER BY tablename ;
END $$ ;
SELECT * FROM tables_by_owner ('pgbench');
schemaname | tablename
------------+------------------
public | pgbench_accounts
public | pgbench_branches
public | pgbench_history public | pgbench_tellers
Si l’on veut renvoyer une structure correspondant exactement à une
table ou vue, la syntaxe est très simple (il n’y a même pas besoin de
%ROWTYPE
) :
CREATE OR REPLACE FUNCTION tables_jamais_analyzees ()
RETURNS SETOF pg_stat_user_tables
LANGUAGE sqlAS $$
SELECT * FROM pg_stat_user_tables
WHERE coalesce(last_analyze, last_autoanalyze) IS NULL ;
$$ ;
SELECT * FROM tables_jamais_analyzees() \gx
-[ RECORD 1 ]-------+------------------------------
relid | 414453
schemaname | public
relname | table_nouvelle
…
n_mod_since_analyze | 10
n_ins_since_vacuum | 10
last_vacuum |
last_autovacuum |
last_analyze |
last_autoanalyze |
vacuum_count | 0
autovacuum_count | 0
analyze_count | 0
autoanalyze_count | 0
-[ RECORD 2 ]-------+------------------------------ …
NB : attention de ne pas oublier le SETOF
, sinon une
seule ligne sera retournée.
RETURNS TABLE :
On a vu que la clause TABLE
permet de renvoyer plusieurs
champs. Or, elle implique aussi SETOF
, et les deux exemples
ci-dessus peuvent devenir :
CREATE OR REPLACE FUNCTION liste_entiers_table (limite int)
TABLE (j int)
RETURNS AS $$
BEGIN
FOR i IN 1..limite LOOP
= i ;
j RETURN NEXT ; -- renvoie la valeur de j en cours
END LOOP;
END $$ LANGUAGE plpgsql;
SELECT * FROM liste_entiers_table (3) ;
j
---
1
2 3
(Noter ici que le nom du champ retourné dépend du nom de la variable
utilisée, et n’est pas forcément le nom de la fonction. En effet, chaque
appel à RETURN NEXT
retourne un enregistrement composé
d’une copie de toutes les variables, au moment de l’appel à
RETURN NEXT
.)
DROP FUNCTION tables_by_owner ;
CREATE FUNCTION tables_by_owner (p_owner text)
TABLE (schemaname text, tablename text)
RETURNS
LANGUAGE plpgsqlAS $$
BEGIN
RETURN QUERY SELECT t.schemaname::text, t.tablename::text
FROM pg_tables t WHERE tableowner=p_owner
ORDER BY t.tablename ;
END $$ ;
Si RETURNS TABLE
est peut-être le plus souple et le plus
clair, le choix entre toutes ces méthodes est affaire de goût, ou de
compatibilité avec du code ancien ou converti d’un produit
concurrent.
Renvoyer le résultat d’une requête :
Les exemples ci-dessus utilisent RETURN NEXT
(pour du
ligne à ligne) ou RETURN QUERY
(pour envoyer directement le
résultat d’une requête).
La variante RETURN QUERY EXECUTE …
est destinée à des
requêtes en SQL dynamique (voir plus loin).
Quand plusieurs lignes sont renvoyées, tout est conservé en mémoire
jusqu’à la fin de la fonction. S’il y en a beaucoup, cela peut poser des
problèmes de latence, voire de mémoire. Le paramètre
work_mem
permet de définir la mémoire utilisée avant de
basculer sur un fichier temporaire, qui a bien sûr un impact sur les
performances.
Appel de fonction :
En général, l’appel se fait ainsi pour obtenir des lignes :
SELECT * FROM ma_fonction();
Une alternative est d’utiliser :
SELECT ma_fonction();
pour récupérer un résultat d’une seule colonne, scalaire, type
composite ou RECORD
suivant la fonction.
Cette différence concerne aussi les fonctions système :
SELECT * FROM pg_control_system () ;
pg_control_version | catalog_version_no | system_identifier | pg_control_…
--------------------+--------------------+---------------------+-------------
1201 | 201909212 | 6744959735975969621 | 2021-09-17 … (1 ligne)
SELECT pg_control_system () ;
pg_control_system
---------------------------------------------------------------
(1201,201909212,6744959735975969621,"2021-09-17 18:24:05+02") (1 ligne)
Si une fonction est définie comme STRICT
et qu’un des
arguments d’entrée est NULL
, PostgreSQL n’exécute même pas
la fonction et utilise NULL
comme résultat.
Dans la logique relationnelle, NULL
signifie « la valeur
est inconnue ». La plupart du temps, il est logique qu’une fonction
ayant un paramètre à une valeur inconnue retourne aussi une valeur
inconnue, ce qui fait que cette optimisation est très souvent
pertinente.
On gagne à la fois en temps d’exécution, mais aussi en simplicité du
code (il n’y a pas à gérer les cas NULL
pour une fonction
dans laquelle NULL
ne doit jamais être injecté).
Dans la définition d’une fonction, les options sont
STRICT
ou son synonyme
RETURNS NULL ON NULL INPUT
, ou le défaut implicite
CALLED ON NULL INPUT
.
En PL/pgSQL, pour utiliser une variable dans le corps de la routine
(entre le BEGIN
et le END
), il est obligatoire
de l’avoir déclarée précédemment :
IN
,
INOUT
ou OUT
) ;DECLARE
.La déclaration doit impérativement préciser le nom et le type de la variable.
En option, il est également possible de préciser :
sa valeur initiale (si rien n’est précisé, ce sera
NULL
par défaut) :
integer := 42; answer
sa valeur par défaut, si on veut autre chose que
NULL
:
integer DEFAULT 42; answer
une contrainte NOT NULL
(dans ce cas, il faut
impérativement un défaut différent de NULL
, et toute
éventuelle affectation ultérieure de NULL
à la variable
provoquera une erreur) :
integer NOT NULL DEFAULT 42; answer
le collationnement à utiliser, pour les variables de type chaîne de caractères :
"en_GB"; question text COLLATE
Pour les fonctions complexes, avec plusieurs niveaux de boucle par
exemple, il est possible d’imbriquer les blocs
DECLARE
/BEGIN
/END
en y déclarant
des variables locales à ce bloc. Si une variable est par erreur utilisée
hors du scope prévu, une erreur surviendra.
L’option CONSTANT
permet de définir une variable pour
laquelle il sera alors impossible d’assigner une valeur dans le reste de
la routine.
Cela permet d’écrire des routines plus génériques.
L’utilisation de %ROWTYPE
permet de définir une variable
qui contient la structure d’un enregistrement de la table spécifiée.
%ROWTYPE
n’est pas obligatoire, il est néanmoins préférable
d’utiliser cette forme, bien plus portable. En effet, dans PostgreSQL,
toute création de table crée un type associé de même nom, le seul nom de
la table est donc suffisant.
RECORD
est beaucoup utilisé pour manipuler des curseurs,
ou dans des boucles FOR … LOOP
: cela évite de devoir se
préoccuper de déclarer un type correspondant exactement aux colonnes de
la requête associée à chaque curseur.
Dans ces exemples, on récupère la première ligne de la fonction avec
SELECT … INTO
, puis on ouvre un curseur implicite pour
balayer chaque ligne obtenue d’une deuxième table. Le type
RECORD
permet de ne pas déclarer une nouvelle variable de
type ligne.
Par expression, on entend par exemple des choses comme :
IF myvar > 0 THEN
:= 1 / myvar;
myvar2 END IF;
Dans ce cas, l’expression myvar > 0
sera préparée par
le moteur de la façon suivante :
PREPARE statement_name(integer, integer) AS SELECT $1 > $2;
Puis cette requête préparée sera exécutée en lui passant en paramètre
la valeur de myvar
et la constante 0
.
Si myvar
est supérieur à 0
, il en sera
ensuite de même pour l’instruction suivante :
PREPARE statement_name(integer, integer) AS SELECT $1 / $2;
Comme toute requête préparée, son plan sera mis en cache.
Pour les détails, voir les dessous de PL/pgSQL.
Privilégiez la première écriture pour la lisibilité, la seconde écriture est moins claire et n’apporte rien puisqu’il s’agit ici d’une affectation de constante.
À noter que l’écriture suivante est également possible pour une affectation :
:= une_colonne FROM ma_table WHERE id = 5; ma_variable
Cette méthode profite du fait que toutes les expressions du code
PL/pgSQL vont être passées au moteur SQL de PostgreSQL dans un
SELECT
pour être résolues. Cela va fonctionner, mais c’est
très peu lisible, et donc non recommandé.
Récupérer une ligne de résultat d’une requête dans une ligne de type
ROW
ou RECORD
se fait avec
SELECT … INTO
. La première ligne est récupérée.
Généralement on préférera utiliser INTO STRICT
pour lever
une de ces erreurs si la requête renvoie zéro ou plusieurs lignes :
ERROR: query returned no rows ERROR: query returned more than one row
Dans le cas du type ROW
, la définition de la ligne doit
correspondre parfaitement à la définition de la ligne renvoyée. Utiliser
un type RECORD
permet d’éviter ce type de problème. La
variable obtient directement le type ROW
de la ligne
renvoyée.
Il est possible d’utiliser SELECT INTO
avec une simple
variable si l’on n’a qu’un champ d’une ligne à récupérer.
Cette fonction compte les tables, et en trace la liste (les tables ne font pas partie du résultat) :
CREATE OR REPLACE FUNCTION compte_tables () RETURNS int LANGUAGE plpgsql AS $$
DECLARE
int ;
n RECORD ;
t BEGIN
SELECT count(*) INTO STRICT n
FROM pg_tables ;
FOR t IN SELECT * FROM pg_tables LOOP
'Table %.%', t.schemaname, t.tablename;
RAISE NOTICE END LOOP ;
RETURN n ;
END ;
$$ ;
SELECT compte_tables (); #
NOTICE: Table pg_catalog.pg_foreign_server
NOTICE: Table pg_catalog.pg_type
…
NOTICE: Table public.pgbench_accounts
NOTICE: Table public.pgbench_branches
NOTICE: Table public.pgbench_tellers
NOTICE: Table public.pgbench_history
compte_tables
---------------
186 (1 ligne)
On peut déterminer qu’aucune ligne n’a été trouvée par la requête en
utilisant la variable FOUND
:
* FROM ma_table WHERE une_colonne>0;
PERFORM IF NOT FOUND THEN
…END IF;
Pour appeler une fonction, il suffit d’utiliser PERFORM
de la manière suivante :
PERFORM mafonction(argument1);
Pour récupérer le nombre de lignes affectées par l’instruction
exécutée, il faut récupérer la variable de diagnostic
ROW_COUNT
:
= ROW_COUNT; GET DIAGNOSTICS variable
Il est à noter que le ROW_COUNT
récupéré ainsi
s’applique à l’ordre SQL précédent, quel qu’il soit :
PERFORM
;EXECUTE
;EXECUTE
dans un bloc PL/pgSQL permet notamment du SQL
dynamique : l’ordre peut être construit dans une variable.
Un danger du SQL dynamique est de faire aveuglément confiance aux valeurs des variables en construisant un ordre SQL :
CREATE TEMP TABLE eleves (nom text, id int) ;
INSERT INTO eleves VALUES ('Robert', 0) ;
-- Mise à jour d'un ID
DO $f$DECLARE
:= $$'Robert' ; DROP TABLE eleves;$$ ;
nom text id int ;
BEGIN
'A exécuter : %','SELECT * FROM eleves WHERE nom = '|| nom ;
RAISE NOTICE EXECUTE 'UPDATE eleves SET id = 327 WHERE nom = '|| nom ;
END ;
$f$ LANGUAGE plpgsql ;
NOTICE: A exécuter : SELECT * FROM eleves WHERE nom = 'Robert' ; DROP TABLE eleves;
\d+ eleves Aucune relation nommée « eleves » n'a été trouvée.
Cet exemple est directement inspiré d’un dessin très connu de XKCD.
Dans la pratique, la variable nom
(entrée ici en dur)
proviendra par exemple d’un site web, et donc contient potentiellement
des caractères terminant la requête dynamique et en insérant une autre,
potentiellement destructrice.
Moins grave, une erreur peut être levée à cause d’une apostrophe (quote) dans une chaîne texte. Il existe effectivement des gens avec une apostrophe dans le nom.
Ce qui suit concerne le SQL dynamique dans des routines PL/pgSQL,
mais le principe concerne tous les langages et clients, y compris
psql
et sa méta-commande \gexec
.
En SQL pur, la protection contre les injections SQL est un argument pour
utiliser les requêtes
préparées, dont l’ordre EXECUTE
diffère de celui-ci du
PL/pgSQL ci-dessous.
Les trois exemples précédents sont équivalents.
Le premier est le plus simple au premier abord. Il utilise
quote_ident
et quote_literal
pour protéger des
injections SQL
(voir plus loin).
Le second est plus lisible grâce à la fonction de formatage
format
qui évite ces concaténations et appelle implicitement les fonctions
quote_%
Si un paramètre ne peut pas prendre la valeur NULL,
utiliser %L
(équivalent de quote_nullable
) et
non %I
(équivalent de quote_ident
).
La troisième alternative avec USING
et les paramètres
numériques $1
et $2
est considérée comme la
plus performante. (Voir les détails
dans la documentation).
L’exemple complet suivant tiré
de la documentation officielle utilise EXECUTE
pour
rafraîchir des vues matérialisées en masse.
CREATE FUNCTION rafraichir_vuemat() RETURNS integer AS $$
DECLARE
RECORD;
mviews BEGIN
'Rafraîchissement de toutes les vues matérialisées…';
RAISE NOTICE
FOR mviews IN
SELECT n.nspname AS mv_schema,
AS mv_name,
c.relname AS owner
pg_catalog.pg_get_userbyid(c.relowner) FROM pg_catalog.pg_class c
LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
WHERE c.relkind = 'm'
ORDER BY 1
LOOP
-- Maintenant "mviews" contient un enregistrement
-- avec les informations sur la vue matérialisé
'Rafraichissement de la vue matérialisée %.% (owner: %)…',
RAISE NOTICE
quote_ident(mviews.mv_schema),
quote_ident(mviews.mv_name),
quote_ident(mviews.owner);EXECUTE format('REFRESH MATERIALIZED VIEW %I.%I',
mviews.mv_schema, mviews.mv_name) ;END LOOP;
'Fin du rafraîchissement';
RAISE NOTICE RETURN 1;
END;
$$ LANGUAGE plpgsql;
De la même manière que pour SELECT … INTO
, utiliser
STRICT
permet de garantir qu’il y a exactement une valeur
comme résultat de EXECUTE
, ou alors une erreur sera
levée.
Nous verrons plus loin comment traiter les exceptions.
La fonction format
est l’équivalent de la fonction
sprintf
en C : elle formate une chaîne en fonction d’un
patron et de valeurs à appliquer à ses paramètres et la retourne. Les
types de paramètre reconnus par format
sont :
%I
: est remplacé par un identifiant d’objet. C’est
l’équivalent de la fonction quote_ident
. L’objet en
question est entouré de guillemets doubles si nécessaire ;%L
: est remplacé par une valeur littérale. C’est
l’équivalent de la fonction quote_literal
. Des guillemets
simples sont ajoutés à la valeur et celle-ci est correctement échappée
si nécessaire ;%s
: est remplacé par la valeur donnée sans autre forme
de transformation ;%%
: est remplacé par un simple %
.Voici un exemple d’utilisation de cette fonction, utilisant des paramètres positionnels :
SELECT format(
'SELECT %I FROM %I WHERE %1$I=%3$L',
'MaColonne',
'ma_table',
'été$$
$$l);
format
------------------------------------------------------------- SELECT "MaColonne" FROM ma_table WHERE "MaColonne"='l''été'
L’instruction CASE WHEN
est proche de l’expression
CASE
des requêtes SQL dans son principe (à part qu’elle se clôt par
END
en SQL, et END CASE
en PL/pgSQL).
Elle est parfois plus légère à lire que des IF
imbriqués.
Exemple complet :
DO $$BEGIN
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 ;
END ;
$$ LANGUAGE plpgsql ;
Des boucles simples s’effectuent avec
LOOP
/END LOOP
.
Pour les détails, voir la documentation officielle.
Cette boucle incrémente le résultat de 1 à chaque itération tant que la valeur du résultat est inférieure à 50. Ensuite, le résultat est incrémenté de 1 à deux reprises pour chaque tour de boucle. On incrémente donc de 2 par tour de boucle. Arrivée à 100, la procédure sort de la boucle.
La boucle FOR
n’a pas d’originalité par rapport à
d’autres langages.
L’option BY
permet d’augmenter l’incrémentation :
FOR variable in 1..10 BY 5…
L’option REVERSE
permet de faire défiler les valeurs en
ordre inverse :
FOR variable in REVERSE 10..1 …
Cette syntaxe très pratique permet de parcourir les lignes résultant
d’une requête sans avoir besoin de créer et parcourir un curseur.
Souvent on utilisera une variable de type ROW
ou
RECORD
(comme dans l’exemple de la fonction
rafraichir_vuemat
plus haut), mais l’utilisation directe de
variables (déclarées préalablement) est possible :
FOR a, b, c, d IN
SELECT col_a, col_b, col_c, col_d FROM ma_table)
(LOOP
-- instructions utilisant ces variables
…END LOOP;
Attention de ne pas utiliser les variables en question hors de la boucle, elles auront gardé la valeur acquise dans la dernière itération.
Voici deux exemples permettant d’illustrer l’utilité de
SLICE
:
SLICE
:
DO $$DECLARE a int[] := ARRAY[[1,2],[3,4],[5,6]];
int;
b BEGIN
IN ARRAY a LOOP
FOREACH b 'var: %', b;
RAISE INFO END LOOP;
END $$ ;
INFO: var: 1
INFO: var: 2
INFO: var: 3
INFO: var: 4
INFO: var: 5 INFO: var: 6
SLICE
:
DO $$DECLARE a int[] := ARRAY[[1,2],[3,4],[5,6]];
int[];
b BEGIN
1 IN ARRAY a LOOP
FOREACH b SLICE 'var: %', b;
RAISE INFO END LOOP;
END $$;
INFO: var: {1,2}
INFO: var: {3,4} INFO: var: {5,6}
et avec SLICE 2
, on obtient :
INFO: var: {{1,2},{3,4},{5,6}}
Une fonction SECURITY INVOKER
s’exécute avec les droits
de l’appelant. C’est le mode par défaut.
Une fonction SECURITY DEFINER
s’exécute avec les droits
du créateur. Cela permet, au travers d’une fonction, de permettre à un
utilisateur d’outrepasser ses droits de façon contrôlée. C’est
l’équivalent du sudo
d’Unix.
Bien sûr, une fonction SECURITY DEFINER
doit faire
l’objet d’encore plus d’attention qu’une fonction normale. Elle peut
facilement constituer un trou béant dans la sécurité de votre base.
C’est encore plus important si le propriétaire de la fonction est un
superutilisateur, car celui-ci a la possibilité d’accéder aux fichiers
de PostgreSQL et au système d’exploitation.
Plusieurs points importants sont à noter pour
SECURITY DEFINER
:
Par défaut, toute fonction créée dans public est exécutable par le rôle public. La première chose à faire est donc de révoquer ce droit. Mieux : créer la fonction dans un schéma séparé est recommandé pour gérer plus finalement les accès.
Il faut se protéger des variables de session qui pourraient être
utilisées pour modifier le comportement de la fonction, en particulier
le search_path (qui pourrait faire pointer vers des tables de
même nom dans un autre schéma). Il doit donc
impérativement être positionné en dur dans cette
fonction (soit d’emblée, avec un SET
en début de fonction,
soit en positionnant un SET
dans le
CREATE FUNCTION
) ; et/ou les fonctions doivent préciser
systématiquement le schéma dans les appels de tables
(SELECT … FROM nomschema.nomtable …
).
Exemple d’une fonction en SECURITY DEFINER
avec
un search path sécurisé :
\c pgbench pgbench
-- A exécuter en tant que pgbench, propriétaire de la base pgbench
CREATE SCHEMA pgbench_util ;
CREATE OR REPLACE FUNCTION pgbench_util.accounts_balance (pbid integer)
integer
RETURNS
LANGUAGE sqlPARALLEL SAFE
IMMUTABLE DEFINER
SECURITY SET search_path TO '' -- précaution supplémentaire
AS $function$
SELECT bbalance FROM public.pgbench_branches br WHERE br.bid = pbid ;
$function$ ;
GRANT USAGE ON SCHEMA pgbench_util TO lecteur ;
GRANT EXECUTE ON FUNCTION pgbench_util.accounts_balance TO lecteur ;
L’utilisateur lecteur peut bien lire le résultat de la fonction sans accès à la table :
\c pgbench lecteur
SELECT pgbench_util.accounts_balance (5) ;
accounts_balance
------------------ 0
Exemple de fonction laxiste et d’attaque :
-- Exemple sur une base pgbench, appartenant à pgbench
-- créée par exemple ainsi :
-- createdb pgbench -O pgbench
-- pgbench -U pgbench -i -s 1 pgbench
-- Deux utilisateurs :
-- pgbench
-- attaquant qui a son propre schéma
set timing off
\set ECHO all
\set ON_ERROR_STOP 1
\
\c pgbench pgbench
-- Fonction non sécurisée fournie par l'utilisateur pgbench
-- à tout le monde par public
CREATE OR REPLACE FUNCTION public.accounts_balance_insecure(pbid integer)
integer
RETURNS
LANGUAGE plpgsqlPARALLEL SAFE
IMMUTABLE DEFINER
SECURITY -- oublié : SET search_path TO ''
AS $function$ BEGIN
RETURN bbalance FROM /* pas de schéma */ pgbench_branches br
WHERE br.bid = pbid ;
END $function$ ;
-- Droits trop ouverts
GRANT EXECUTE ON FUNCTION accounts_balance_insecure TO public ;
-- Résultat normal : renvoie 0
SELECT * FROM accounts_balance_insecure (1) ;
-- Création d'un utilisateur avec droit d'écrire dans un schéma
\c pgbench postgres
DROP SCHEMA IF EXISTS piege CASCADE ;
--DROP ROLE attaquant ;
CREATE ROLE attaquant LOGIN ; -- pg_hba.conf laissé en exercice au lecteur
-- Il faut que l'attaquant ait un schéma où écrire,
-- et puisse donner l'accès à la victime.
-- Le schéma public convient parfaitement pour cela avant PostgreSQL 15…
CREATE SCHEMA piege ;
GRANT ALL ON SCHEMA piege TO attaquant WITH GRANT OPTION ;
\c pgbench attaquant
\conninfo
-- Résultat normal (accès peut-être indu mais pour le moment sans danger)
SELECT * FROM accounts_balance_insecure (1) ;
-- L'attaquant peut voir la fonction et étudier comment la détourner
\sf accounts_balance_insecure
-- Fonction que l'attaquant veut faire exécuter à pgbench
CREATE FUNCTION piege.lit_donnees_cachees ()
TABLE (bid int, bbalance int)
RETURNS
LANGUAGE plpgsqlAS $$
DECLARE
int ;
n BEGIN
-- affichage de l'utilisateur pgbench
'Entrée dans fonction piégée en tant que %', current_user ;
RAISE NOTICE -- copie de données non autorisées dans le schéma de l'attaquant
CREATE TABLE piege.donnees_piratees AS SELECT * FROM pgbench_tellers ;
GRANT ALL ON piege.donnees_piratees TO attaquant ;
-- destruction de données…
DROP TABLE IF EXISTS pgbench_history ;
-- sortie propre impérative pour éviter le rollback
RETURN QUERY SELECT 666 AS bid, 42 AS bbalance ;
END ;
$$ ;
-- Vue d'enrobage pour « masquer » la vraie table de même nom
CREATE OR REPLACE VIEW piege.pgbench_branches AS
SELECT * FROM piege.lit_donnees_cachees () ;
-- Donner les droits au compte attaqué sur les objets
-- de l'attaquant
GRANT USAGE,CREATE ON SCHEMA piege TO pgbench ;
GRANT ALL ON piege.pgbench_branches TO pgbench ;
GRANT ALL ON FUNCTION piege.lit_donnees_cachees TO pgbench ;
-- Détournement du chemin d'accès
SET search_path TO piege,public ;
-- Attaque
SELECT * FROM accounts_balance_insecure (666) ;
-- Lecture des données piratées
SELECT COUNT (*) as nb_lignes_recuperees FROM piege.donnees_piratees ;
COST
est un coût représenté en unité de
cpu_operator_cost
(100 par défaut).
ROWS
vaut par défaut 1000 pour les fonctions
SETOF
ou TABLE
, et 1 pour les autres.
Ces deux paramètres ne modifient pas le comportement de la fonction. Ils ne servent que pour aider l’optimiseur de requête à estimer le coût d’appel à la fonction, afin de savoir, si plusieurs plans sont possibles, lequel est le moins coûteux par rapport au nombre d’appels de la fonction et au nombre d’enregistrements qu’elle retourne.
PARALLEL UNSAFE
indique que la fonction ne peut pas être
exécutée dans le mode parallèle. La présence d’une fonction de ce type
dans une requête SQL force un plan d’exécution en série. C’est la valeur
par défaut.
Une fonction est non parallélisable si elle modifie l’état d’une base ou si elle fait des changements sur la transaction.
PARALLEL RESTRICTED
indique que la fonction peut être
exécutée en mode parallèle mais l’exécution est restreinte au processus
principal d’exécution.
Une fonction peut être déclarée comme restreinte si elle accède aux tables temporaires, à l’état de connexion des clients, aux curseurs, aux requêtes préparées.
PARALLEL SAFE
indique que la fonction s’exécute
correctement dans le mode parallèle sans restriction.
En général, si une fonction est marquée sûre ou restreinte à la parallélisation alors qu’elle ne l’est pas, elle pourrait renvoyer des erreurs ou fournir de mauvaises réponses lorsqu’elle est utilisée dans une requête parallèle.
En cas de doute, les fonctions doivent être marquées comme
UNSAFE
, ce qui correspond à la valeur par défaut.
On peut indiquer à PostgreSQL le niveau de volatilité (ou de stabilité) d’une fonction. Ceci permet d’aider PostgreSQL à optimiser les requêtes utilisant ces fonctions, mais aussi d’interdire leur utilisation dans certains contextes.
Une fonction est « immutable » si son exécution ne
dépend que de ses paramètres. Elle ne doit donc dépendre ni du contenu
de la base (pas de SELECT
, ni de modification de donnée de
quelque sorte), ni d’aucun autre élément qui ne soit
pas un de ses paramètres. Les fonctions arithmétiques simples
(+
, *
, abs
…) sont immutables.
À l’inverse, now()
n’est évidemment pas immutable. Une
fonction sélectionnant des données d’une table non plus.
to_char()
n’est pas non plus immutable, car son
comportement dépend des paramètres de session, par exemple
to_char(timestamp with time zone, text)
dépend du paramètre
de session timezone
…
Une fonction est « stable » si son exécution donne
toujours le même résultat sur toute la durée d’un ordre SQL, pour les
mêmes paramètres en entrée. Cela signifie que la fonction ne modifie pas
les données de la base. Une fonction n’exécutant que des
SELECT
sur des tables (pas des fonctions !) sera stable.
to_char()
est stable. L’optimiseur peut réduire ainsi le
nombre d’appels sans que ce soit en pratique toujours le cas.
Une fonction est « volatile » dans tous les autres
cas. random()
est volatile. Une fonction volatile peut même
modifier les donneés. Une fonction non déclarée comme stable ou
immutable est volatile par défaut.
La volatilité des fonctions intégrées à PostgreSQL est déjà définie. C’est au développeur de préciser la volatilité des fonctions qu’il écrit. Ce n’est pas forcément évident. Une erreur peut poser des problèmes quand le plan est mis en cache, ou, on le verra, dans des index.
Quelle importance cela a-t-il ?
Prenons une table d’exemple sur les heures de l’année 2020 :
-- Une ligne par heure dans l année, 8784 lignes
CREATE TABLE heures
AS
SELECT i, '2020-01-01 00:00:00+01:00'::timestamptz + i * interval '1 hour' AS t
FROM generate_series (1,366*24) i;
Définissons une fonction un peu naïve ramenant le premier jour du mois, volatile faute de mieux :
CREATE OR REPLACE FUNCTION premierjourdumois(t timestamptz)
RETURNS timestamptz
LANGUAGE plpgsql
VOLATILEAS $$
BEGIN
'appel premierjourdumois' ; -- trace des appels
RAISE notice RETURN date_trunc ('month', t);
END $$ ;
Demandons juste le plan d’un appel ne portant que sur le dernier jour :
EXPLAIN SELECT * FROM heures
WHERE t > premierjourdumois('2020-12-31 00:00:00+02:00'::timestamptz)
LIMIT 10 ;
QUERY PLAN
-------------------------------------------------------------------------
Limit (cost=0.00..8.04 rows=10 width=12)
-> Seq Scan on heures (cost=0.00..2353.80 rows=2928 width=12)
Filter: (t > premierjourdumois( '2020-12-30 23:00:00+01'::timestamp with time zone))
Le nombre de lignes attendues (2928) est le tiers de la table, alors que nous ne demandons que le dernier mois. Il s’agit de l’estimation forfaitaire que PostgreSQL utilise faute d’informations sur ce que va retourner la fonction.
Demander à voir le résultat mène à l’affichage de milliers de
NOTICE
: la fonction est appelée à chaque ligne pour
calculer s’il faut filtrer la valeur. En effet, une fonction volatile
sera systématiquement exécutée à chaque appel, et, selon le plan, ce
peut être pour chaque ligne parcourue !
Cependant notre fonction ne fait que des calculs à partir du paramètre, sans effet de bord. Déclarons-la donc stable :
ALTER FUNCTION premierjourdumois(timestamp with time zone) STABLE ;
Une fonction stable peut en théorie être remplacée par son résultat pendant l’exécution de la requête. Mais c’est impossible de le faire plus tôt, car on ne sait pas forcément dans quel contexte la fonction va être appelée (par exemple, en cas de requête préparée, les paramètres de la session ou les données de la base peuvent même changer entre la planification et l’exécution).
Dans notre cas, le même EXPLAIN
simple mène à ceci :
NOTICE: appel premierjourdumois
QUERY PLAN
-------------------------------------------------------------------------
Limit (cost=0.00..32.60 rows=10 width=12)
-> Seq Scan on heures (cost=0.00..2347.50 rows=720 width=12)
Filter: (t > premierjourdumois( '2020-12-30 23:00:00+01'::timestamp with time zone))
Comme il s’agit d’un simple EXPLAIN
, la requête n’est
pas exécutée. Or le message NOTICE
est renvoyé : la
fonction est donc exécutée pour une simple planification. Un appel
unique suffit, puisque la valeur d’une fonction stable ne change pas
pendant toute la durée de la requête pour les mêmes paramètres (ici une
constante). Cet appel permet d’affiner la volumétrie des valeurs
attendues, ce qui peut avoir un impact énorme.
Cependant, à l’exécution, les NOTICE
apparaîtront pour
indiquer que la fonction est à nouveau appelée à chaque ligne. Pour
qu’un seul appel soit effectué pour toute la requête, il faudrait
déclarer la fonction comme immutable, ce qui serait faux, puisqu’elle
dépend implicitement du fuseau horaire.
Dans l’idéal, une fonction immutable peut être remplacée par son résultat avant même la planification d’une requête l’utilisant. C’est le cas avec les calculs arithmétiques par exemple :
EXPLAIN SELECT * FROM heures
WHERE i > abs(364*24) AND t > '2020-06-01'::date + interval '57 hours' ;
La valeur est substituée très tôt, ce qui permet de les comparer aux statistiques :
Seq Scan on heures (cost=0.00..179.40 rows=13 width=12) Filter: ((i > 8736) AND (t > '2020-06-03 09:00:00'::timestamp without time zone))
Pour forcer un appel unique quand on sait que la fonction renverra une constante, du moins le temps de la requête, même si elle est volatile, une astuce est de signifier à l’optimiseur qu’il n’y aura qu’une seule valeur de comparaison, même si on ne sait pas laquelle :
EXPLAIN (ANALYZE) SELECT * FROM heures
WHERE t > (SELECT premierjourdumois('2020-12-31 00:00:00+02:00'::timestamptz)) ;
NOTICE: appel premierjourdumois
QUERY PLAN
--------------------------------------------------------------------------------
Seq Scan on heures (cost=0.26..157.76 rows=2920 width=12)
(actual time=1.090..1.206 rows=721 loops=1)
Filter: (t > $0)
Rows Removed by Filter: 8039
InitPlan 1 (returns $0)
-> Result (cost=0.00..0.26 rows=1 width=8)
(actual time=0.138..0.139 rows=1 loops=1)
Planning Time: 0.058 ms Execution Time: 1.328 ms
On note qu’il n’y a qu’un appel. On comprend donc l’intérêt de se poser la question à l’écriture de chaque fonction.
La volatilité est encore plus importante quand il s’agit de créer des fonctions sur index :
CREATE INDEX ON heures (premierjourdumois( t )) ;
ERROR: functions in index expression must be marked IMMUTABLE
Ceci n’est possible que si la fonction est immutable. En effet, si le résultat de la fonction dépend de l’état de la base ou d’autres paramètres, la fonction exécutée au moment de la création de la clé d’index pourrait ne plus retourner le même résultat quand viendra le moment de l’interroger. PostgreSQL n’acceptera donc que les fonctions immutables dans la déclaration des index fonctionnels.
Déclarer hâtivement une fonction comme immutable juste pour pouvoir l’utiliser dans un index est dangereux : en cas d’erreur, les résultats d’une requête peuvent alors dépendre du plan d’exécution, selon que les index seront utilisés ou pas !
Cela est particulièrement fréquent quand les fuseaux horaires ou les dictionnaires sont impliqués. Vérifiez bien que vous n’utilisez que des fonctions immutables dans les index fonctionnels, les pièges sont nombreux.
Par exemple, si l’on veut une version immutable de la fonction
précédente, il faut fixer le fuseau horaire dans l’appel à
date_trunc
. En effet, on peut voir avec
df+ date_trunc
que la seule version immutable de
date_trunc
n’accepte que des timestamp
(sans
fuseau), et en renvoie un. Notre fonction devient donc :
CREATE OR REPLACE FUNCTION premierjourdumois_utc(t timestamptz)
RETURNS timestamptz
LANGUAGE plpgsql
IMMUTABLEAS $$
DECLARE
timestamp ; --sans TZ
jour1 BEGIN
:= date_trunc ('month', (t at time zone 'UTC')::timestamp) ;
jour1 RETURN jour1 AT TIME ZONE 'UTC';
END $$ ;
Testons avec une date dans les dernières heures de septembre en Alaska, qui correspond au tout début d’octobre en temps universel, et par exemple aussi au Japon :
\x
SET timezone TO 'US/Alaska';
SELECT d,
AT TIME ZONE 'UTC' AS d_en_utc,
d
premierjourdumois_utc (d),AT TIME ZONE 'UTC' as pjm_en_utc
premierjourdumois_utc (d) FROM (SELECT '2020-09-30 18:00:00-08'::timestamptz AS d) x;
-[ RECORD 1 ]---------+-----------------------
d | 2020-09-30 18:00:00-08
d_en_utc | 2020-10-01 02:00:00
premierjourdumois_utc | 2020-09-30 16:00:00-08 pjm_en_utc | 2020-10-01 00:00:00
SET timezone TO 'Japan';
SELECT d,
AT TIME ZONE 'UTC' AS d_en_utc,
d
premierjourdumois_utc (d),AT TIME ZONE 'UTC' as pjm_en_utc
premierjourdumois_utc (d) FROM (SELECT '2020-09-30 18:00:00-08'::timestamptz AS d) x;
-[ RECORD 1 ]---------+-----------------------
d | 2020-10-01 11:00:00+09
d_en_utc | 2020-10-01 02:00:00
premierjourdumois_utc | 2020-10-01 09:00:00+09 pjm_en_utc | 2020-10-01 00:00:00
Malgré les différences d’affichage dues au fuseau horaire, c’est bien le même moment (la première seconde d’octobre en temps universel) qui est retourné par la fonction.
Pour une fonction aussi simple, la version SQL est même préférable :
CREATE OR REPLACE FUNCTION premierjourdumois_utc(t timestamptz)
RETURNS timestamptz
LANGUAGE sql
IMMUTABLEAS $$
SELECT (date_trunc ('month',
at time zone 'UTC')::timestamp
(t
)AT TIME ZONE 'UTC';
) $$ ;
Enfin, la volatilité a également son importance lors d’autres opérations d’optimisation, comme l’exclusion de partitions. Seules les fonctions immutables sont compatibles avec le partition pruning effectué à la planification, mais les fonctions stable sont éligibles au dynamic partition pruning (à l’exécution) apparu avec PostgreSQL 11.
La documentation officielle sur le langage PL/pgSQL peut être consultée en français à cette adresse.
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.
Écrire une fonction
hello()
qui renvoie la chaîne de caractère « Hello World! » en SQL.
Écrire une fonction
hello_pl()
qui renvoie la chaîne de caractère « Hello World! » en PL/pgSQL.
Comparer les coûts des deux plans d’exécutions de ces requêtes. Expliquer ces coûts.
Écrire en PL/pgSQL une fonction de division appelée
division
. Elle acceptera en entrée deux arguments de type entier et renverra un nombre réel (numeric
).
Écrire cette même fonction en SQL.
Comment corriger le problème de la division par zéro ? Écrire cette nouvelle fonction dans les deux langages. (Conseil : dans ce genre de calcul impossible, il est possible d’utiliser la constante
NaN
(Not A Number) ).
Ce TP utilise les tables de la base employes_services. Le script de création se télécharge et s’installe ainsi dans une nouvelle base employes :
curl -kL https://dali.bo/tp_employes_services -o employes_services.sql
createdb employes
psql employes < employes_services.sql
Les quelques tables occupent environ 80 Mo sur le disque.
Créer une fonction qui ramène le nombre d’employés embauchés une année donnée (à partir du champ
employes.date_embauche
).
Utiliser la fonction
generate_series()
pour lister le nombre d’embauches pour chaque année entre 2000 et 2010.
Créer une fonction qui fait la même chose avec deux années en paramètres une boucle
FOR … LOOP
,RETURNS TABLE
etRETURN NEXT
.
Écrire une fonction de multiplication dont les arguments sont des chiffres en toute lettre, inférieurs ou égaux à « neuf ». Par exemple,
multiplication ('deux','trois')
doit renvoyer 6.
Si ce n’est déjà fait, faire en sorte que
multiplication
appelle une autre fonction pour faire la conversion de texte en chiffre, et n’effectue que le calcul.
Essayer de multiplier « deux » par 4. Qu’obtient-on et pourquoi ?
Corriger la fonction pour tomber en erreur si un argument est numérique (utiliser
RAISE EXCEPTION <message>
).
Écrire une fonction en PL/pgSQL qui prend en argument le nom de l’utilisateur, puis lui dit « Bonjour » ou « Bonsoir » suivant l’heure de la journée. Utiliser la fonction
to_char()
.
Écrire la même fonction avec un paramètre
OUT
.
Pour calculer l’heure courante, utiliser plutôt la fonction
extract
.
Réécrire la fonction en SQL.
Écrire une fonction
inverser
qui inverse une chaîne (pour « toto » en entrée, afficher « otot » en sortie), à l’aide d’une boucleWHILE
et des fonctionschar_length
etsubstring
.
Le calcul de la date de Pâques est complexe. On peut écrire la fonction suivante :
CREATE OR REPLACE FUNCTION paques (annee integer)
date
RETURNS AS $$
DECLARE
integer ;
a integer ;
b date ;
r BEGIN
:= (19*(annee % 19) + 24) % 30 ;
a := (2*(annee % 4) + 4*(annee % 7) + 6*a + 5) % 7 ;
b SELECT (annee::text||'-03-31')::date + (a+b-9) INTO r ;
RETURN r ;
END ;
$$ LANGUAGE plpgsql ;
Principe : Soit m
l’année. On calcule
successivement :
m/19
: c’est la valeur de
a
.m/4
: c’est la valeur de
b
.m/7
: c’est la valeur de
c
.(19a + p)/30
: c’est la valeur de
d
.(2b + 4c + 6d + q)/7
: c’est la valeur de
e
.Les valeurs de p
et de q
varient de 100 ans
en 100 ans. De 2000 à 2100, p
vaut 24, q
vaut
5. La date de Pâques est le (22 + d + e)
mars ou le
(d + e - 9)
avril.
Afficher les dates de Pâques de 2018 à 2025.
Écrire une fonction qui calcule la date de l’Ascension, soit le jeudi de la sixième semaine après Pâques. Pour simplifier, on peut aussi considérer que l’Ascension se déroule 39 jours après Pâques.
Pour écrire une fonction qui renvoie tous les jours fériés d’une année (libellé et date), en France métropolitaine :
- Prévoir un paramètre supplémentaire pour l’Alsace-Moselle, où le Vendredi saint (précédant le dimanche de Pâques) et le 26 décembre sont aussi fériés (ou toute autre variation régionale).
- Cette fonction doit renvoyer plusieurs lignes : utiliser
RETURN NEXT
.- Plusieurs variantes sont possibles : avec
SETOF record
, avec des paramètresOUT
, ou avecRETURNS TABLE (libelle, jour)
.- Enfin, il est possible d’utiliser
RETURN QUERY
.
Pour répondre aux exigences de stockage, l’application a besoin de pouvoir trouver rapidement les produits dont le volume est compris entre certaines bornes (nous négligeons ici le facteur de forme, qui est problématique dans le cadre d’un véritable stockage en entrepôt !).
Écrire une requête permettant de renvoyer l’ensemble des produits (table
magasin.produits
) dont le volume ne dépasse pas 1 litre (les unités de longueur sont en mm, 1 litre = 1 000 000 mm³).
Quel index permet d’optimiser cette requête ? (Utiliser une fonction est possible, mais pas obligatoire.)
Écrire une fonction
hello()
qui renvoie la chaîne de caractère « Hello World! » en SQL.
CREATE OR REPLACE FUNCTION hello()
RETURNS textAS $BODY$
SELECT 'hello world !'::text;
$BODY$ LANGUAGE SQL;
Écrire une fonction
hello_pl()
qui renvoie la chaîne de caractère « Hello World! » en PL/pgSQL.
CREATE OR REPLACE FUNCTION hello_pl()
RETURNS textAS $BODY$
BEGIN
RETURN 'hello world !';
END
$BODY$ LANGUAGE plpgsql;
Comparer les coûts des deux plans d’exécutions de ces requêtes. Expliquer ces coûts.
Requêtage :
EXPLAIN SELECT hello();
QUERY PLAN
------------------------------------------ Result (cost=0.00..0.01 rows=1 width=32)
EXPLAIN SELECT hello_pl();
QUERY PLAN
------------------------------------------ Result (cost=0.00..0.26 rows=1 width=32)
Par défaut, si on ne précise pas le coût (COST
) d’une
fonction, cette dernière a un coût par défaut de 100. Ce coût est à
multiplier par la valeur du paramètre cpu_operator_cost
,
par défaut à 0,0025. Le coût total d’appel de la fonction
hello_pl
est donc par défaut de :
100*cpu_operator_cost + cpu_tuple_cost
Ce n’est pas valable pour la fonction en SQL pur, qui est ici intégrée à la requête.
Écrire en PL/pgSQL une fonction de division appelée
division
. Elle acceptera en entrée deux arguments de type entier et renverra un nombre réel (numeric
).
Attention, sous PostgreSQL, la division de deux entiers est par défaut entière : il faut donc transtyper.
CREATE OR REPLACE FUNCTION division (arg1 integer, arg2 integer)
numeric
RETURNS AS $BODY$
BEGIN
RETURN arg1::numeric / arg2::numeric;
END
$BODY$ LANGUAGE plpgsql;
SELECT division (3,2) ;
division
-------------------- 1.5000000000000000
Écrire cette même fonction en SQL.
CREATE OR REPLACE FUNCTION division_sql (a integer, b integer)
numeric
RETURNS AS $$
SELECT a::numeric / b::numeric;
$$ LANGUAGE SQL;
Comment corriger le problème de la division par zéro ? Écrire cette nouvelle fonction dans les deux langages. (Conseil : dans ce genre de calcul impossible, il est possible d’utiliser la constante
NaN
(Not A Number) ).
Le problème se présente ainsi :
SELECT division(1,0);
ERROR: division by zero CONTEXTE : PL/pgSQL function division(integer,integer) line 3 at RETURN
Pour la version en PL :
CREATE OR REPLACE FUNCTION division(arg1 integer, arg2 integer)
numeric
RETURNS AS $BODY$
BEGIN
IF arg2 = 0 THEN
RETURN 'NaN';
ELSE
RETURN arg1::numeric / arg2::numeric;
END IF;
END $BODY$
LANGUAGE plpgsql;
SELECT division (3,0) ;
division
---------- NaN
Pour la version en SQL :
CREATE OR REPLACE FUNCTION division_sql(a integer, b integer)
numeric
RETURNS AS $$
SELECT CASE $2
WHEN 0 THEN 'NaN'
ELSE $1::numeric / $2::numeric
END;
$$ LANGUAGE SQL;
Ce TP utilise les tables de la base employes_services. Le script de création se télécharge et s’installe ainsi dans une nouvelle base employes :
curl -kL https://dali.bo/tp_employes_services -o employes_services.sql
createdb employes
psql employes < employes_services.sql
Les quelques tables occupent environ 80 Mo sur le disque.
Créer une fonction qui ramène le nombre d’employés embauchés une année donnée (à partir du champ
employes.date_embauche
).
CREATE OR REPLACE FUNCTION nb_embauches (v_annee integer)
integer
RETURNS AS $BODY$
DECLARE
integer;
nb BEGIN
SELECT count(*)
INTO nb
FROM employes
WHERE extract (year from date_embauche) = v_annee ;
RETURN nb;
END
$BODY$ LANGUAGE plpgsql ;
Test :
SELECT nb_embauches (2006);
nb_embauches
-------------- 9
Utiliser la fonction
generate_series()
pour lister le nombre d’embauches pour chaque année entre 2000 et 2010.
SELECT n, nb_embauches (n)
FROM generate_series (2000,2010) n
ORDER BY n;
n | nb_embauches
------+--------------
2000 | 2
2001 | 0
2002 | 0
2003 | 1
2004 | 0
2005 | 2
2006 | 9
2007 | 0
2008 | 0
2009 | 0 2010 | 0
Créer une fonction qui fait la même chose avec deux années en paramètres une boucle
FOR … LOOP
,RETURNS TABLE
etRETURN NEXT
.
CREATE OR REPLACE FUNCTION nb_embauches (v_anneedeb int, v_anneefin int)
TABLE (annee int, nombre_embauches int)
RETURNS AS $BODY$
BEGIN
FOR i in v_anneedeb..v_anneefin
LOOP
SELECT i, nb_embauches (i)
INTO annee, nombre_embauches ;
RETURN NEXT ;
END LOOP;
RETURN;
END
$BODY$ LANGUAGE plpgsql;
Le nom de la fonction a été choisi identique à la précédente, mais avec des paramètres différents. Cela ne gêne pas le requêtage :
SELECT * FROM nb_embauches (2006,2010);
annee | nombre_embauches
-------+------------------
2006 | 9
2007 | 0
2008 | 0
2009 | 0 2010 | 0
Écrire une fonction de multiplication dont les arguments sont des chiffres en toute lettre, inférieurs ou égaux à « neuf ». Par exemple,
multiplication ('deux','trois')
doit renvoyer 6.
CREATE OR REPLACE FUNCTION multiplication (arg1 text, arg2 text)
integer
RETURNS AS $BODY$
DECLARE
integer;
a1 integer;
a2 BEGIN
IF arg1 = 'zéro' THEN
:= 0;
a1 = 'un' THEN
ELSEIF arg1 := 1;
a1 = 'deux' THEN
ELSEIF arg1 := 2;
a1 = 'trois' THEN
ELSEIF arg1 := 3;
a1 = 'quatre' THEN
ELSEIF arg1 := 4;
a1 = 'cinq' THEN
ELSEIF arg1 := 5;
a1 = 'six' THEN
ELSEIF arg1 := 6;
a1 = 'sept' THEN
ELSEIF arg1 := 7;
a1 = 'huit' THEN
ELSEIF arg1 := 8;
a1 = 'neuf' THEN
ELSEIF arg1 := 9;
a1 END IF;
IF arg2 = 'zéro' THEN
:= 0;
a2 = 'un' THEN
ELSEIF arg2 := 1;
a2 = 'deux' THEN
ELSEIF arg2 := 2;
a2 = 'trois' THEN
ELSEIF arg2 := 3;
a2 = 'quatre' THEN
ELSEIF arg2 := 4;
a2 = 'cinq' THEN
ELSEIF arg2 := 5;
a2 = 'six' THEN
ELSEIF arg2 := 6;
a2 = 'sept' THEN
ELSEIF arg2 := 7;
a2 = 'huit' THEN
ELSEIF arg2 := 8;
a2 = 'neuf' THEN
ELSEIF arg2 := 9;
a2 END IF;
RETURN a1*a2;
END
$BODY$ LANGUAGE plpgsql;
Test :
SELECT multiplication('deux', 'trois');
multiplication
---------------- 6
SELECT multiplication('deux', 'quatre');
multiplication
---------------- 8
Si ce n’est déjà fait, faire en sorte que
multiplication
appelle une autre fonction pour faire la conversion de texte en chiffre, et n’effectue que le calcul.
CREATE OR REPLACE FUNCTION texte_vers_entier(arg text)
integer AS $BODY$
RETURNS DECLARE
integer;
ret BEGIN
IF arg = 'zéro' THEN
:= 0;
ret = 'un' THEN
ELSEIF arg := 1;
ret = 'deux' THEN
ELSEIF arg := 2;
ret = 'trois' THEN
ELSEIF arg := 3;
ret = 'quatre' THEN
ELSEIF arg := 4;
ret = 'cinq' THEN
ELSEIF arg := 5;
ret = 'six' THEN
ELSEIF arg := 6;
ret = 'sept' THEN
ELSEIF arg := 7;
ret = 'huit' THEN
ELSEIF arg := 8;
ret = 'neuf' THEN
ELSEIF arg := 9;
ret END IF;
RETURN ret;
END
$BODY$
LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION multiplication(arg1 text, arg2 text)
integer
RETURNS AS $BODY$
DECLARE
integer;
a1 integer;
a2 BEGIN
:= texte_vers_entier(arg1);
a1 := texte_vers_entier(arg2);
a2 RETURN a1*a2;
END
$BODY$ LANGUAGE plpgsql;
Essayer de multiplier « deux » par 4. Qu’obtient-on et pourquoi ?
SELECT multiplication('deux', 4::text);
multiplication ----------------
Par défaut, les variables internes à la fonction valent NULL. Rien n’est prévu pour affecter le second argument, on obtient donc NULL en résultat.
Corriger la fonction pour tomber en erreur si un argument est numérique (utiliser
RAISE EXCEPTION <message>
).
CREATE OR REPLACE FUNCTION texte_vers_entier(arg text)
integer AS $BODY$
RETURNS DECLARE
integer;
ret BEGIN
IF arg = 'zéro' THEN
:= 0;
ret = 'un' THEN
ELSEIF arg := 1;
ret = 'deux' THEN
ELSEIF arg := 2;
ret = 'trois' THEN
ELSEIF arg := 3;
ret = 'quatre' THEN
ELSEIF arg := 4;
ret = 'cinq' THEN
ELSEIF arg := 5;
ret = 'six' THEN
ELSEIF arg := 6;
ret = 'sept' THEN
ELSEIF arg := 7;
ret = 'huit' THEN
ELSEIF arg := 8;
ret = 'neuf' THEN
ELSEIF arg := 9;
ret ELSE
EXCEPTION 'argument "%" invalide', arg;
RAISE := NULL;
ret END IF;
RETURN ret;
END
$BODY$ LANGUAGE plpgsql;
SELECT multiplication('deux', 4::text);
ERROR: argument "4" invalide
CONTEXTE : PL/pgSQL function texte_vers_entier(text) line 26 at RAISE PL/pgSQL function multiplication(text,text) line 7 at assignment
Écrire une fonction en PL/pgSQL qui prend en argument le nom de l’utilisateur, puis lui dit « Bonjour » ou « Bonsoir » suivant l’heure de la journée. Utiliser la fonction
to_char()
.
CREATE OR REPLACE FUNCTION salutation(utilisateur text)
RETURNS textAS $BODY$
DECLARE
integer;
heure
libelle text;BEGIN
:= to_char(now(), 'HH24');
heure IF heure > 12
THEN
:= 'Bonsoir';
libelle ELSE
:= 'Bonjour';
libelle END IF;
RETURN libelle||' '||utilisateur||' !';
END
$BODY$ LANGUAGE plpgsql;
Test :
SELECT salutation ('Guillaume');
salutation
--------------------- Bonsoir Guillaume !
Écrire la même fonction avec un paramètre
OUT
.
CREATE OR REPLACE FUNCTION salutation(IN utilisateur text, OUT message text)
AS $BODY$
DECLARE
integer;
heure
libelle text;BEGIN
:= to_char(now(), 'HH24');
heure IF heure > 12
THEN
:= 'Bonsoir';
libelle ELSE
:= 'Bonjour';
libelle END IF;
:= libelle||' '||utilisateur||' !';
message END
$BODY$ LANGUAGE plpgsql;
Elle s’utilise de la même manière :
SELECT salutation ('Guillaume');
salutation
--------------------- Bonsoir Guillaume !
Pour calculer l’heure courante, utiliser plutôt la fonction
extract
.
CREATE OR REPLACE FUNCTION salutation(IN utilisateur text, OUT message text)
AS $BODY$
DECLARE
integer;
heure
libelle text;BEGIN
SELECT INTO heure extract(hour from now())::int;
IF heure > 12
THEN
:= 'Bonsoir';
libelle ELSE
:= 'Bonjour';
libelle END IF;
:= libelle||' '||utilisateur||' !';
message END
$BODY$ LANGUAGE plpgsql;
Réécrire la fonction en SQL.
Le CASE … WHEN
remplace aisément un
IF … THEN
:
CREATE OR REPLACE FUNCTION salutation_sql(nom text)
RETURNS textAS $$
SELECT CASE extract(hour from now()) > 12
WHEN 't' THEN 'Bonsoir '|| nom
ELSE 'Bonjour '|| nom
END::text;
$$ LANGUAGE SQL;
Écrire une fonction
inverser
qui inverse une chaîne (pour « toto » en entrée, afficher « otot » en sortie), à l’aide d’une boucleWHILE
et des fonctionschar_length
etsubstring
.
CREATE OR REPLACE FUNCTION inverser(str_in varchar)
varchar
RETURNS AS $$
DECLARE
varchar ; -- à renvoyer
str_out integer ;
position BEGIN
-- Initialisation de str_out, sinon sa valeur reste à NULL
:= '';
str_out -- Position initialisée ç la longueur de la chaîne
:= char_length(str_in);
position -- La chaîne est traitée ç l'envers
-- Boucle: Inverse l'ordre des caractères d'une chaîne de caractères
WHILE position > 0 LOOP
-- la chaîne donnée en argument est parcourue
-- à l'envers,
-- et les caractères sont extraits individuellement
:= str_out || substring(str_in, position, 1);
str_out := position - 1;
position END LOOP;
RETURN str_out;
END;
$$ LANGUAGE plpgsql;
SELECT inverser (' toto ') ;
inverser
---------- otot
La fonction suivante calcule la date de Pâques d’une année :
CREATE OR REPLACE FUNCTION paques (annee integer)
date
RETURNS AS $$
DECLARE
integer ;
a integer ;
b date ;
r BEGIN
:= (19*(annee % 19) + 24) % 30 ;
a := (2*(annee % 4) + 4*(annee % 7) + 6*a + 5) % 7 ;
b SELECT (annee::text||'-03-31')::date + (a+b-9) INTO r ;
RETURN r ;
END ;
$$ LANGUAGE plpgsql ;
Afficher les dates de Pâques de 2018 à 2025.
SELECT paques (n) FROM generate_series (2018, 2025) n ;
paques
------------
2018-04-01
2019-04-21
2020-04-12
2021-04-04
2022-04-17
2023-04-09
2024-03-31 2025-04-20
Écrire une fonction qui calcule la date de l’Ascension, soit le jeudi de la sixième semaine après Pâques. Pour simplifier, on peut aussi considérer que l’Ascension se déroule 39 jours après Pâques.
Version complexe :
CREATE OR REPLACE FUNCTION ascension(annee integer)
date
RETURNS AS $$
DECLARE
date;
r BEGIN
SELECT paques(annee)::date + 40 INTO r;
SELECT r + (4 - extract(dow from r))::integer INTO r;
RETURN r;
END;
$$ LANGUAGE plpgsql;
Version simple :
CREATE OR REPLACE FUNCTION ascension(annee integer)
date
RETURNS AS $$
SELECT (paques (annee) + INTERVAL '39 days')::date ;
$$ LANGUAGE sql;
Test :
SELECT paques (n), ascension(n) FROM generate_series (2018, 2025) n ;
paques | ascension
------------+------------
2018-04-01 | 2018-05-10
2019-04-21 | 2019-05-30
2020-04-12 | 2020-05-21
2021-04-04 | 2021-05-13
2022-04-17 | 2022-05-26
2023-04-09 | 2023-05-18
2024-03-31 | 2024-05-09 2025-04-20 | 2025-05-29
Pour écrire une fonction qui renvoie tous les jours fériés d’une année (libellé et date), en France métropolitaine :
- Prévoir un paramètre supplémentaire pour l’Alsace-Moselle, où le Vendredi saint (précédant le dimanche de Pâques) et le 26 décembre sont aussi fériés (ou toute autre variation régionale).
- Cette fonction doit renvoyer plusieurs lignes : utiliser
RETURN NEXT
.- Plusieurs variantes sont possibles : avec
SETOF record
, avec des paramètresOUT
, ou avecRETURNS TABLE (libelle, jour)
.- Enfin, il est possible d’utiliser
RETURN QUERY
.
Version avec SETOF record :
CREATE OR REPLACE FUNCTION vacances (
integer,
annee boolean DEFAULT false
alsace_moselle record
) RETURNS SETOF AS $$
DECLARE
integer;
f record;
r BEGIN
SELECT 'Jour de l''an'::text, (annee::text||'-01-01')::date INTO r;
RETURN NEXT r;
SELECT 'Pâques'::text, paques(annee)::date + 1 INTO r;
RETURN NEXT r;
SELECT 'Ascension'::text, ascension(annee)::date INTO r;
RETURN NEXT r;
SELECT 'Fête du travail'::text, (annee::text||'-05-01')::date INTO r;
RETURN NEXT r;
SELECT 'Victoire 1945'::text, (annee::text||'-05-08')::date INTO r;
RETURN NEXT r;
SELECT 'Fête nationale'::text, (annee::text||'-07-14')::date INTO r;
RETURN NEXT r;
SELECT 'Assomption'::text, (annee::text||'-08-15')::date INTO r;
RETURN NEXT r;
SELECT 'La toussaint'::text, (annee::text||'-11-01')::date INTO r;
RETURN NEXT r;
SELECT 'Armistice 1918'::text, (annee::text||'-11-11')::date INTO r;
RETURN NEXT r;
SELECT 'Noël'::text, (annee::text||'-12-25')::date INTO r;
RETURN NEXT r;
IF alsace_moselle THEN
SELECT 'Vendredi saint'::text, paques(annee)::date - 2 INTO r;
RETURN NEXT r;
SELECT 'Lendemain de Noël'::text, (annee::text||'-12-26')::date INTO r;
RETURN NEXT r;
END IF;
RETURN;
END;
$$ LANGUAGE plpgsql;
Le requêtage implique de nommer les colonnes :
SELECT *
FROM vacances(2020, true) AS (libelle text, jour date)
ORDER BY jour ;
libelle | jour
--------------------+------------
Jour de l'an | 2020-01-01
Vendredi saint | 2020-04-10
Pâques | 2020-04-13
Fête du travail | 2020-05-01
Victoire 1945 | 2020-05-08
Ascension | 2020-05-21
Fête nationale | 2020-07-14
Assomption | 2020-08-15
La toussaint | 2020-11-01
Armistice 1918 | 2020-11-11
Noël | 2020-12-25 Lendemain de Noël | 2020-12-26
Version avec paramètres OUT :
Une autre forme d’écriture possible consiste à indiquer les deux
colonnes de retour comme des paramètres OUT
:
CREATE OR REPLACE FUNCTION vacances(
integer,
annee boolean DEFAULT false,
alsace_moselle OUT libelle text,
OUT jour date)
record
RETURNS SETOF
LANGUAGE plpgsqlAS $function$
DECLARE
integer;
f record;
r BEGIN
SELECT 'Jour de l''an'::text, (annee::text||'-01-01')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Pâques'::text, paques(annee)::date + 1 INTO libelle, jour;
RETURN NEXT;
SELECT 'Ascension'::text, ascension(annee)::date INTO libelle, jour;
RETURN NEXT;
SELECT 'Fête du travail'::text, (annee::text||'-05-01')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Victoire 1945'::text, (annee::text||'-05-08')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Fête nationale'::text, (annee::text||'-07-14')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Assomption'::text, (annee::text||'-08-15')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'La toussaint'::text, (annee::text||'-11-01')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Armistice 1918'::text, (annee::text||'-11-11')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Noël'::text, (annee::text||'-12-25')::date INTO libelle, jour;
RETURN NEXT;
IF alsace_moselle THEN
SELECT 'Vendredi saint'::text, paques(annee)::date - 2 INTO libelle, jour;
RETURN NEXT;
SELECT 'Lendemain de Noël'::text, (annee::text||'-12-26')::date
INTO libelle, jour;
RETURN NEXT;
END IF;
RETURN;
END;
$function$;
La fonction s’utilise alors de façon simple :
SELECT *
FROM vacances(2020)
ORDER BY jour ;
libelle | jour
-----------------+------------
Jour de l'an | 2020-01-01
Pâques | 2020-04-13
Fête du travail | 2020-05-01
Victoire 1945 | 2020-05-08
Ascension | 2020-05-21
Fête nationale | 2020-07-14
Assomption | 2020-08-15
La toussaint | 2020-11-01
Armistice 1918 | 2020-11-11 Noël | 2020-12-25
Version avec RETURNS TABLE
:
Seule la déclaration en début diffère de la version avec les
paramètres OUT
:
CREATE OR REPLACE FUNCTION vacances(
integer,alsace_moselle boolean DEFAULT false)
annee TABLE (libelle text, jour date)
RETURNS
LANGUAGE plpgsqlAS $function$
…
L’utilisation est aussi simple que la version précédente.
Version avec RETURN QUERY :
C’est peut-être la version la plus compacte :
CREATE OR REPLACE FUNCTION vacances(annee integer,alsace_moselle boolean DEFAULT false)
TABLE (libelle text, jour date)
RETURNS
LANGUAGE plpgsqlAS $function$
BEGIN
RETURN QUERY SELECT 'Jour de l''an'::text, (annee::text||'-01-01')::date ;
RETURN QUERY SELECT 'Pâques'::text, paques(annee)::date + 1 ;
RETURN QUERY SELECT 'Ascension'::text, ascension(annee)::date ;
RETURN QUERY SELECT 'Fête du travail'::text, (annee::text||'-05-01')::date ;
RETURN QUERY SELECT 'Victoire 1945'::text, (annee::text||'-05-08')::date ;
RETURN QUERY SELECT 'Fête nationale'::text, (annee::text||'-07-14')::date ;
RETURN QUERY SELECT 'Assomption'::text, (annee::text||'-08-15')::date ;
RETURN QUERY SELECT 'La toussaint'::text, (annee::text||'-11-01')::date ;
RETURN QUERY SELECT 'Armistice 1918'::text, (annee::text||'-11-11')::date ;
RETURN QUERY SELECT 'Noël'::text, (annee::text||'-12-25')::date ;
IF alsace_moselle THEN
RETURN QUERY SELECT 'Vendredi saint'::text, paques(annee)::date - 2 ;
RETURN QUERY SELECT 'Lendemain de Noël'::text, (annee::text||'-12-26')::date ;
END IF;
RETURN;
END;
$function$;
Ce TP 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.
Écrire une requête permettant de renvoyer l’ensemble des produits (table
magasin.produits
) dont le volume ne dépasse pas 1 litre (les unités de longueur sont en mm, 1 litre = 1 000 000 mm³).
Concernant le volume des produits, la requête est assez simple :
SELECT * FROM produits WHERE longueur * hauteur * largeur < 1000000 ;
Quel index permet d’optimiser cette requête ? (Utiliser une fonction est possible, mais pas obligatoire.)
L’option la plus simple est de créer l’index de cette façon, sans avoir besoin d’une fonction :
CREATE INDEX ON produits((longueur * hauteur * largeur));
En général, il est plus propre de créer une fonction. On peut passer
la ligne entière en paramètre pour éviter de fournir 3 paramètres. Il
faut que cette fonction soit IMMUTABLE
pour être
indexable :
CREATE OR REPLACE function volume (p produits)
numeric
RETURNS AS $$
SELECT p.longueur * p.hauteur * p.largeur;
$$ language SQLPARALLEL SAFE
IMMUTABLE ;
(Elle est même PARALLEL SAFE
pour la même raison qu’elle
est IMMUTABLE
: elle dépend uniquement des données de la
table.)
On peut ensuite indexer le résultat de cette fonction :
CREATE INDEX ON produits (volume(produits)) ;
Il est ensuite possible d’écrire la requête de plusieurs manières, la fonction étant ici écrite en SQL et non en PL/pgSQL ou autre langage procédural :
SELECT * FROM produits WHERE longueur * hauteur * largeur < 1000000 ;
SELECT * FROM produits WHERE volume(produits) < 1000000 ;
En effet, l’optimiseur est capable de « regarder » à l’intérieur de la fonction SQL pour déterminer que les clauses sont les mêmes, ce qui n’est pas vrai pour les autres langages.
En revanche, la requête suivante, où la multiplication est faite dans un ordre différent, n’utilise pas l’index :
SELECT * FROM produits WHERE largeur * longueur * hauteur < 1000000 ;
et c’est notamment pour cette raison qu’il est plus propre d’utiliser la fonction.
De part l’origine « relationnel-objet » de PostgreSQL, on peut même écrire la requête de la manière suivante :
SELECT * FROM produits WHERE produits.volume < 1000000;