SELECTcount(*) AS compte_pays,count(CASEWHEN r.nom_region='Europe'THEN1ELSENULLEND) AS compte_pays_europeensFROM pays pJOIN regions rON (p.region_id = r.region_id);
Filtrer avec FILTER
La même requête écrite avec la clause FILTER :
SELECTcount(*) AS compte_pays,count(*) FILTER (WHERE r.nom_region='Europe')AS compte_pays_europeensFROM pays pJOIN regions rON (p.region_id = r.region_id);
Fonctions de fenêtrage
Fonctions WINDOW
travaille sur des ensembles de données regroupés et triés
indépendamment de la requête principale
Utilisation :
utiliser plusieurs critères d’agrégation dans la même requête
utiliser des fonctions de classement
faire référence à d’autres lignes de l’ensemble de données
Regroupement
Regroupement
clause OVER (PARTITION BY …)
Utilité :
plusieurs critères de regroupement différents
avec des fonctions de calcul d’agrégats
Regroupement : exemple
SELECT matricule, salaire, service,SUM(salaire) OVER (PARTITIONBY service)AS total_salaire_serviceFROM employes;
SELECT … agregation OVER (ORDERBY<colonnes>)FROM<liste_tables>WHERE<predicats>
Regroupement et tri
On peut combiner les deux
OVER (PARTITION BY .. ORDER BY ..)
Utilité :
travailler sur des jeux de données ordonnés et isolés les uns des
autres
Regroupement et tri : exemple
SELECT continent, pays, population,rank() OVER (PARTITIONBY continentORDERBY population DESC)AS rangFROM population;
continent|pays|population|rang------------------+--------------------+------------+------Afrique|Nigéria|173.6|1Afrique|Éthiopie|94.1|2Afrique|Égypte|82.1|3Afrique|Rép. dém. du Congo |67.5|4(…)Amérique du Nord |États-Unis|320.1|1Amérique du Nord |Canada|35.2|2(…)
Regroupement et tri :
principe
OVER (PARTITIONBY continentORDERBY population DESC)
SELECT continent,percentile_disc(0.5) WITHIN GROUP (ORDERBY population) AS"mediane",percentile_disc(0.95) WITHIN GROUP (ORDERBY population) AS"95pct",ROUND(AVG(population), 1) AS moyenneFROM populationGROUPBY continent;
continent|mediane|95pct|moyenne---------------------------+---------+--------+---------Afrique|33.0|173.6|44.3Amérique du Nord |35.2|320.1|177.7Amérique latine. Caraïbes |30.4|200.4|53.3Asie|53.3|1252.1|179.9Europe|9.4|82.7|21.8
Grouping Sets
GROUPING SETS/ROLLUP/CUBE
Extension de GROUP BY
Utilité :
présente le résultat de plusieurs agrégations différentes
réaliser plusieurs agrégations différentes dans la même requête
Un identifiant universellement unique sur 128 bits
Type : uuid
Avec des inconvénients…
UUID : avantages
Faciles à générer
gen_random_uuid() et d’autres
Pouvoir désigner des entités arbitrairement
Pouvoir exporter des données de sources différentes
au contraire des séquences traditionnelles
Pouvoir fusionner des bases
Pour toutes les clés primaires ?
UUID : inconvénients
Lisibilité
Temps de génération (mineur)
Taille
16 octets…
Surtout : fragmentation des index
mauvais pour le cache
sauf UUID v7 (tout récent)
UUID : utilisation sous
PostgreSQL
uuid :
type simple (16 octets)
garantit le format
Génération :
gen_random_uuid (v4, aléatoire)
extension uuid-ossp (v1,3,4,5)
extension (pg_idkit…) ou fonction en SQL (v7)
UUID : une indexation facile
CREATEUNIQUEINDEXON nomtable USING btree (champ_uuid) ;
UUID : résumé
Si vous avez besoin des UUID, préférez la version 7.
Les séquences habituelles restent recommandables en interne.
Types tableaux
Tableaux : principe
Collection ordonnée d’un même type
Types integer[], text[], etc.
SELECTARRAY [1,2,3] ;SELECT'{1,2,3}'::integer[] ;
-- lignes vers tableauSELECT array_agg (i) FROM generate_series (1,3) i ;-- tableau vers lignesSELECT unnest ( '{1,2,3}'::integer[] ) ;
CREATETABLE demotab ( idint, liste integer[] ) ;
Tableaux : recherche de
valeurs
-- Recherche de valeurs (toutes)SELECT*FROM demotabWHERE liste @>ARRAY[15,11] ;-- Au moins 1 élément commun ?SELECT*FROM demotabWHERE liste && ARRAY[11,55] ;-- 1 tableau exactSELECT*FROM demotabWHERE liste ='{11,55}'::int[] ;
Tableaux : performances
Quel intérêt par rapport à 1 valeur par ligne ?
Allège certains modèles en réduisant les jointures
ex : champ avec des n°s de téléphone
si pas de contraintes
Grosse économie en place (time series)
24 octets par ligne
et parfois mécanisme TOAST
Mais…
mise à jour compliquée/lente
indexation moins simple
Tableaux : indexation
B-tree inutilisable
GIN plus adapté pour recherche d’une valeur
opérateurs && , @>
mais plus lourd
Types composés
Types composés : généralités
Un champ = plusieurs attributs
De loin préférable à une table Entité/Attribut/Valeur
Uniquement si le modèle relationnel n’est pas assez souple
SELECT jsonb_array_elements (datas->'phoneNumbers')->>'number'FROM personnes ;
Conversions jsonb / relationnel
(1)
Construire un ensemble de tuples depuis un objet JSON :
jsonb_each()
Avec des types préexistants :
jsonb_populate_record(),
jsonb_populate_recordset
Types définis à la volée :
jsonb_to_record(),jsonb_to_recordset()
Construire un JSON depuis une requête :
to_jsonb (<requête>)
Attention aux types
jsonb_typeof()
Conversions jsonb /
relationnel (2) : JSON_TABLE
SELECT*FROM t,JSON_TABLE ( t.champ_json ,'$ ? (@.age == 42)'COLUMNS (age int PATH '$.age', nom text PATH '$.name')) ;
PostgreSQL 17+
conversion d’un JSON en vue, avec types
clause NESTED PATH pour les tableaux dans le JSON
paramétres : clause PASSING
JSON : performances
Inconvénients par rapport à un modèle normalisé :
Perte d’intégrité (types, contraintes, FK…)
Complexité du code
Pas de statistiques sur les clés JSON !
Pas forcément plus léger en disque
clés répétées
Lire 1 attribut = lire tout le JSON
voire accès table TOAST
Mise à jour : tout ou rien
Indexation délicate
Recherche dans un champ JSON
(1)
Sans JSONPath :
SELECT*FROM personnes …
WHERE datas->>'lastName'='Durand'; -- textesWHERE datas->'lastName'='"Durand"'::jsonb ; -- JSON
WHERE datas @>'{"nom": "Durand"}'::jsonb ; -- contient
WHERE datas ? 'spouse' ; -- attibut existe ?WHERE datas ?| '{spouse,children}'::text[] ; -- un des champs ?WHERE datas ?& '{spouse,children}'::text[] ; -- tous les champs ?
Mais comment indexer ?
Recherche
dans un champ JSON (2) : SQL/JSON et JSONPath
SQL:2016 introduit SQL/JSON et le langage JSONPath
JSONPath :
langage de recherche pour JSON
concis, flexible, plus rapide
depuis PostgreSQL 12, étendu à chaque version
Recherche
dans un champ JSON (3) : Exemples SQL/JSON & JSONPath
CREATEINDEX idx_prs_nom ON personnes ((datas->>'lastName')) ;ANALYZE personnes ;
Colonne générée (dénormalisée) :
champ normal, indexable, facile à utiliser, rapide à lire
statistiques
ALTERTABLE personnesADDCOLUMN lastname textGENERATED ALWAYS AS ((datas->>'lastName')) STORED ;
Préférer @> et @? que des
fonctions
jsonb : indexation (2/2)
Indexation « schemaless » grâce au GIN :
attention au choix de l’opérateur
-- opérateur par défaut-- pour critères : ?, ?|, ?& , @> , @?, @@)CREATEINDEX idx_prs ON personnes USING gin(datas) ;-- index plus performant-- mais que @> , @?, @@CREATEINDEX idx_prs ON personnes USING gin(datas jsonb_path_ops) ;
CREATE OR REPLACE FUNCTION addition (entier1 integer, entier2 integer)RETURNSintegerLANGUAGEsqlIMMUTABLERETURN entier1 + entier2 ;
Exemple de
fonction PL/pgSQL utilisant la base
CREATE OR REPLACE FUNCTION nb_lignes_table (sch text, tbl text)RETURNSbigintSTABLEAS'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
CREATEOR 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;BEGINFOR r IN (SELECT schemaname, relnameFROM pg_stat_user_tablesWHEREcoalesce(last_analyze, last_autoanalyze) ISNULL ) LOOP RAISE NOTICE 'Analyze %.%', r.schemaname, r.relname ;EXECUTE'ANALYZE '|| quote_ident(r.schemaname)||'.'|| quote_ident(r.relname) ;ENDLOOP;END$$;
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 :
CREATETYPE 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 () RETURNSintegerAS$$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 () RETURNSintegerAS$$DECLARE ligne RECORD;BEGIN-- récupération de la 1è ligne uniquementSELECT*INTO ligne FROM ma_première_table;-- ou : traitement ligne à ligneFOR ligne INSELECT*FROM ma_deuxième_table LOOP …ENDLOOP ;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 :
SELECT5INTO un_entier;
Exécution d’une requête
Affectation de la ligne :
SELECT*INTO ma_variable_ligne -- type ROW ou RECORDFROM …;
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) ;
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 =0THEN resultat :='zero';ELSEIF nombre >0THEN resultat :='positif';ELSEIF nombre <0THEN resultat :='négatif';ELSE resultat :='indéterminé';ENDIF;
Tests conditionnels : CASE
CASEWHEN nombre =0THEN'zéro'WHEN variable >0THEN'positif'WHEN variable <0THEN'négatif'ELSE'indéterminé'ENDCASE
Connaître la majorité des possibilités de PL/pgSQL
Les utiliser pour étendre les fonctionnalités de la base
Écrire du code robuste
Éviter les pièges de sécurité
Savoir optimiser une routine
Routines variadic
Routines variadic :
introduction
Permet de créer des routines avec un nombre d’arguments
variables
… mais du même type
Routines variadic : exemple
Récupérer le minimum d’une liste :
CREATE FUNCTION pluspetit(VARIADICnumeric[])RETURNSnumericAS$$SELECTmin($1[i]) FROM generate_subscripts($1, 1) g(i);$$ LANGUAGE SQL;SELECT pluspetit(10, -1, 5, 4.4); pluspetit----------- -1(1 row)
Routines variadic : exemple
PL/pgSQL
En PL/pgSQL, cette fois-ci
Démonstration de FOREACH xxx IN ARRAY aaa LOOP
Précédemment, obligé de convertir le tableau en relation pour
boucler (unnest)
Routines polymorphes
Routines polymorphes :
introduction
Typer les variables oblige à dupliquer les routines communes à
plusieurs types
PostgreSQL propose des types polymorphes
Le typage se fait à l’exécution
Routines polymorphes :
anyelement
Remplace tout type de données simple ou composite
pour les paramètres en entrée comme pour les paramètres en
sortie
Tous les paramètres et type de retour de type
anyelement se voient attribués le même type
Donc un seul type pour tous les anyelement
autorisés
Paramètre spécial $0 : du type attribué aux éléments
anyelement
Routines polymorphes :
anyarray
anyarray remplace tout tableau de type de données
simple ou composite
pour les paramètres en entrée comme pour les paramètres en
sortie
Le typage se fait à l’exécution
Tous les paramètres de type anyarray se voient
attribués le même type
Routines polymorphes :
exemple
L’addition est un exemple fréquent :
CREATE OR REPLACE FUNCTION addition(var1 anyelement, var2 anyelement)RETURNS anyelementAS$$DECLARE somme ALIAS FOR $0;BEGIN somme := var1 + var2;RETURN somme;END;$$LANGUAGE plpgsql;
FETCH [ direction { FROM|IN } ] curseur INTO cible
Récupère la prochaine ligne
FOUND indique si cette nouvelle ligne a été récupérée
Cible est
une variable RECORD
une variable ROW
un ensemble de variables séparées par des virgules
Curseurs : récupération des
données
direction du FETCH :
NEXT, PRIOR
FIRST, LAST
ABSOLUTE nombre, RELATIVE nombre
nombre
ALL
FORWARD, FORWARD nombre,
FORWARD ALL
BACKWARD, BACKWARD nombre,
BACKWARD ALL
Curseurs : modification des
données
Mise à jour d’une ligne d’un curseur :
UPDATE une_table SET...WHERECURRENTOF curseur;
Suppression d’une ligne d’un curseur :
DELETEFROM une_tableWHERECURRENTOF curseur;
Curseurs : fermeture d’un
curseur
Instruction SQL : CLOSE curseur
Ferme le curseur
Permet de récupérer de la mémoire
Permet aussi de réouvrir le curseur
Curseurs : renvoi d’un curseur
Fonction renvoyant une valeur de type refcursor
Permet donc de renvoyer plusieurs valeurs
Gestion des erreurs
Gestion des erreurs :
introduction
Sans exceptions :
toute erreur provoque un arrêt de la fonction
toute modification suite à une instruction SQL (INSERT,
UPDATE, DELETE) est annulée
d’où l’ajout d’une gestion personnalisée des erreurs avec le concept
des exceptions
Gestion des erreurs : une
exception
La fonction comporte un bloc supplémentaire,
EXCEPTION :
DECLARE-- déclaration des variables localesBEGIN-- instructions de la fonctionEXCEPTIONWHENconditionTHEN-- instructions traitant cette erreurWHENconditionTHEN-- autres instructions traitant cette autre erreur-- etc.END
Gestion des
erreurs : flot dans une fonction
L’exécution de la fonction commence après le BEGIN
Si aucune erreur ne survient, le bloc EXCEPTION est
ignoré
Si une erreur se produit
tout ce qui a été modifié dans la base dans le bloc est annulé
les variables gardent par contre leur état
l’exécution passe directement dans le bloc de gestion de
l’exception
Gestion des
erreurs : flot dans une exception
Recherche d’une condition satisfaisante
Si cette condition est trouvée
exécution des instructions correspondantes
Si aucune condition n’est compatible
sortie du bloc BEGIN/END comme si le bloc
d’exception n’existait pas
passage de l’exception au bloc BEGIN/END
contenant (après annulation de ce que ce bloc a modifié en base)
Dans un bloc d’exception, les instructions INSERT,
UPDATE, DELETE de la fonction ont été
annulées
Dans un bloc d’exception, les variables locales de la fonction ont
gardé leur ancienne valeur
Gestion des erreurs : codes
d’erreurs
SQLSTATE : code d’erreur
SQLERRM : message d’erreur
Par exemple :
Data Exception : division par zéro, overflow, argument
invalide pour certaines fonctions, etc.
Integrity Constraint Violation : unicité, CHECK, clé
étrangère, etc.
Syntax Error
PL/pgSQL Error : RAISE EXCEPTION, pas de
données, trop de lignes, etc.
Les erreurs sont contenues dans des classes d’erreurs plus
génériques, qui peuvent aussi être utilisées
Messages d’erreurs : RAISE - 1
Envoyer une trace dans les journaux applicatifs et/ou vers le client
RAISE niveau message
Niveau correspond au niveau d’importance du message
DEBUG, LOG, INFO,
NOTICE, WARNING, EXCEPTION
Message est la trace à enregistrer
Message dynamique… tout signe % est remplacé par la
valeur indiquée après le message
Champs DETAIL et HINT disponibles
Messages d’erreurs : RAISE - 2
Exemples :
RAISE WARNING 'valeur % interdite', valeur;
RAISE WARNING 'valeur % ambigue', valeurUSING HINT ='Controlez la valeur saisie en amont';
Messages d’erreurs :
configuration des logs
Deux paramètres importants pour les traces
log_min_messages
niveau minimum pour que la trace soit enregistrée dans les
journaux
client_min_messages
niveau minimum pour que la trace soit envoyée au client
Dans le cas d’un RAISE NOTICE message, il faut avoir
soit log_min_messages, soit
client_min_messages, soit les deux à la valeur
NOTICE au minimum.
Messages d’erreurs :
RAISE EXCEPTION - 1
Annule le bloc en cours d’exécution
RAISE EXCEPTION message
Sauf en cas de présence d’un bloc EXCEPTION gérant la
condition RAISE_EXCEPTION
message est la trace à enregistrer, et est dynamique… tout signe %
est remplacé par la valeur indiquée après le message
Messages d’erreurs :
RAISE EXCEPTION - 2
Exemple :
RAISE EXCEPTION'erreur interne';-- La chose à ne pas faire !
Flux des erreurs dans du code
PL
Les exceptions non traitées «remontent»
de bloc BEGIN/END imbriqués vers les blocs
parents (fonctions appelantes comprises)
jusqu’à ce que personne ne puisse les traiter
voir note pour démonstration
Flux des erreurs dans du
code PL - 2
Les erreurs remontent
Cette fois-ci, on rajoute un bloc PL pour intercepter l’erreur
Flux des erreurs dans du
code PL - 3
Cette fois-ci, on rajoute un bloc PL indépendant pour gérer le
second INSERT
Flux des erreurs dans du
code PL - 4
Illustrons maintenant la remontée d’erreurs
Nous avons deux blocs imbriqués
Une erreur non prévue va se produire dans le bloc intérieur
Abus des exceptions
Ne pas abuser des exceptions
les sous-transactions ne sont pas gratuites
Si saturation, paramétre SLRU dédié (v17)
Sécurité
Sécurité : droits
L’exécution de la routine dépend du droit EXECUTE
Par défaut, ce droit est donné à la création de la routine
au propriétaire de la routine
au groupe spécial PUBLIC
Sécurité : ajout
Ce droit peut être donné avec l’instruction SQL
GRANT :
la différence entre masquage dynamique et masquage statique
comment implémenter un masquage avancé
PostgreSQL Anonymizer
Principe
Principe :
Extension
Déclaratif (DDL)
4 méthodes de masquage
Principe :
masquage statique
masquage dynamique
sauvegardes anonymisées
vues de masquage
Prérequis
Installation
Cet exemple nécessite :
une instance PostgreSQL ;
l’extension PostgreSQL Anonymizer
(anon)
installée, initialisée par un super-utilisateur
une base boutique
dont le propriétaire est paul,
super-utilisateur
les rôles pierre et jack
avec droits de connexion à boutique
Base d’exemple
Création des rôles pierre, paul,
jack et la base boutique :
CREATEROLE paul LOGIN SUPERUSER ;CREATEROLE pierre LOGIN ;CREATEROLE jack LOGIN ;GRANT pg_read_all_data TO jack;GRANT pg_write_all_data TO jack;CREATEDATABASE boutique OWNER paul;
La fonction anon.init() permet d’initialiser les valeurs
de pseudonymisation dans les tables du schéma anon. Ces
valeurs seront ensuite utilisées lors du processus de pseudonymisation
des données.
Masquage statique
Le plus simple
Destructif
L’histoire
Au fil des années, Paul a accumulé des données sur ses clients et
leurs achats dans une base de données très simple.
Il a récemment installé un nouveau logiciel de ventes, et
l’ancienne base est obsolète.
Avant de l’archiver, il voudrait en supprimer toutes les données
personnelles.
Comment ça marche ?
Objectifs
Nous allons voir :
comment écrire des règles de masquage simples
les intérêts et limitations du masquage statique
le concept de « singularisation » d’une personne (singling
out)
Déclaration des règles de masquage
(SECURITY LABEL);
Application des règles de masquage.
Déclarer les règles de
masquage
SECURITY LABELFOR anon ONCOLUMN customer.lastnameIS'MASKED WITH FUNCTION anon.dummy_last_name()';SECURITY LABELFOR anon ONCOLUMN customer.phoneIS'MASKED WITH FUNCTION anon.partial(phone,2,$$X-XXX-XX$$,2)';
Appliquer les règles
de manière permanente
Application des règles de masquage :
SELECT anon.anonymize_table('customer');
Cette technique est nommée « masquage statique » car la donnée réelle
a été détruite de manière définitive. L’anonymisation dynamique et les
exports seront vus plus loin.
1{"meta":{"name":"Lee Perry","ip_addr":"40.87.29.113"},"content":"Hello Nasty!"}2{"meta":{"name":"","email":"biz@bizmarkie.com"},"content":"Great Shop"}3{"meta":{"name":"Jimmy"},"content":"Hi! This is me, Jimmy James "}