Développement avancé

Formation DEV42

Dalibo SCOP

24.09

29 août 2024

Sur ce document

Formation Formation DEV42
Titre Développement avancé
Révision 24.09
ISBN N/A
PDF https://dali.bo/dev42_pdf
EPUB https://dali.bo/dev42_epub
HTML https://dali.bo/dev42_html
Slides https://dali.bo/dev42_slides

Licence Creative Commons CC-BY-NC-SA

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

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

Marques déposées

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

Versions de PostgreSQL couvertes

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

SQL pour l’analyse de données

Préambule

  • Analyser des données est facile avec PostgreSQL
    • opérations d’agrégation disponibles
    • fonctions OLAP avancées
  • Agrégation de données
  • Clause FILTER
  • Fonctions WINDOW
  • GROUPING SETS, ROLLUP, CUBE
  • WITHIN GROUPS

Objectifs

  • Écrire des requêtes encore plus complexes
  • Analyser les données en amont
    • pour ne récupérer que le résultat

Agrégats

  • SQL dispose de fonctions de calcul d’agrégats
  • Utilité :
    • calcul de sommes, moyennes, valeur minimale et maximale
    • nombreuses fonctions statistiques disponibles

Agrégats avec GROUP BY

  • agrégat + GROUP BY
  • Utilité
    • effectue des calculs sur des regroupements : moyenne, somme, comptage, etc.
    • regroupement selon un critère défini par la clause GROUP BY
    • exemple : calcul du salaire moyen de chaque service

GROUP BY : principe

Résultat du GROUP BY

GROUP BY : exemples

SELECT service,
       sum(salaire) AS salaires_par_service
  FROM employes
 GROUP BY service;
   service   | salaires_par_service
-------------+----------------------
 Courrier    |              7500.00
 Direction   |             10000.00
 Publication |              7000.00
(3 lignes)

Agrégats et ORDER BY

  • Extension propriétaire de PostgreSQL
    • ORDER BY dans la fonction d’agrégat
  • Utilité :
    • ordonner les données agrégées
    • surtout utile avec array_agg, string_agg et xmlagg

Utiliser ORDER BY avec un agrégat

SELECT service,
       string_agg(nom, ', ' ORDER BY nom) AS liste_employes
  FROM employes
 GROUP BY service;
   service   |  liste_employes
-------------+-------------------
 Courrier    | Fantasio, Lagaffe
 Direction   | Dupuis
 Publication | Lebrac, Prunelle
(3 lignes)

Clause FILTER

  • Clause FILTER
  • Utilité :
    • filtrer les données sur les agrégats
    • évite les expressions CASE complexes
  • SQL:2003

Filtrer avec CASE

  • La syntaxe suivante était utilisée :
SELECT count(*) AS compte_pays,
       count(CASE WHEN r.nom_region='Europe' THEN 1
                  ELSE NULL
              END) AS compte_pays_europeens
  FROM pays p
  JOIN regions r
    ON (p.region_id = r.region_id);

Filtrer avec FILTER

  • La même requête écrite avec la clause FILTER :
SELECT count(*) AS compte_pays,
       count(*) FILTER (WHERE r.nom_region='Europe')
                AS compte_pays_europeens
  FROM pays p
  JOIN regions r
    ON (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 (PARTITION BY service)
                 AS total_salaire_service
  FROM employes;
 matricule | salaire  |   service   | total_salaire_service
-----------+----------+-------------+-----------------------
 00000004  |  4500.00 | Courrier    |               7500.00
 00000020  |  3000.00 | Courrier    |               7500.00
 00000001  | 10000.00 | Direction   |              10000.00
 00000006  |  4000.00 | Publication |               7000.00
 00000040  |  3000.00 | Publication |               7000.00

Regroupement : principe

SUM(salaire) OVER (PARTITION BY service)

Fonction de fenêtrage

Regroupement : syntaxe

SELECT
 agregation OVER (PARTITION BY <colonnes>)
  FROM <liste_tables>
 WHERE <predicats>

Tri

  • Tri
    • OVER (ORDER BY …)
  • Utilité :
    • numéroter les lignes : row_number()
    • classer des résultats : rank(), dense_rank()
    • faire appel à d’autres lignes du résultat : lead(), lag()

Tri : exemple

  • Pour numéroter des lignes :
SELECT row_number() OVER (ORDER BY matricule),
       matricule, nom
  FROM employes;
 row_number | matricule |   nom
------------+-----------+----------
          1 | 00000001  | Dupuis
          2 | 00000004  | Fantasio
          3 | 00000006  | Prunelle
          4 | 00000020  | Lagaffe
          5 | 00000040  | Lebrac
(5 lignes)

Tri : exemple avec une somme

  • Calcul d’une somme glissante :
SELECT matricule, salaire,
       SUM(salaire) OVER (ORDER BY matricule)
  FROM employes;
 matricule | salaire  |   sum
-----------+----------+----------
 00000001  | 10000.00 | 10000.00
 00000004  |  4500.00 | 14500.00
 00000006  |  4000.00 | 18500.00
 00000020  |  3000.00 | 21500.00
 00000040  |  3000.00 | 24500.00

Tri : principe

SUM(salaire) OVER (ORDER BY matricule)

Fonction de fenêtrage - tri

Tri : syntaxe

SELECT
 agregation OVER (ORDER BY <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 (PARTITION BY continent
                    ORDER BY population DESC)
              AS rang
  FROM population;
    continent     |       pays         | population | rang
------------------+--------------------+------------+------
 Afrique          | Nigéria            |      173.6 |    1
 Afrique          | Éthiopie           |       94.1 |    2
 Afrique          | Égypte             |       82.1 |    3
 Afrique          | Rép. dém. du Congo |       67.5 |    4
()
 Amérique du Nord | États-Unis         |      320.1 |    1
 Amérique du Nord | Canada             |       35.2 |    2
()

Regroupement et tri : principe

OVER (PARTITION BY continent
      ORDER BY population DESC)

Fonction de fenêtrage - partition et tri

Regroupement et tri : syntaxe

SELECT
 <agregation> OVER (PARTITION BY <colonnes>
                  ORDER BY <colonnes>)
  FROM <liste_tables>
 WHERE <predicats>

Fonctions analytiques

  • PostgreSQL dispose d’un certain nombre de fonctions analytiques
  • Utilité :
    • faire référence à d’autres lignes du même ensemble
    • évite les auto-jointures complexes et lentes

lead() et lag()

  • lead(colonne, n)
    • retourne la valeur d’une colonne, n lignes après la ligne courante
  • lag(colonne, n)
    • retourne la valeur d’une colonne, n lignes avant la ligne courante

lead() et lag() : exemple

SELECT pays, continent, population,
       lag(population) OVER (PARTITION BY continent
                             ORDER BY population DESC)
  FROM population;
         pays          | continent | population |  lag
-----------------------+-----------+------------+--------
 Chine                 | Asie      |     1385.6 |
 Iraq                  | Asie      |       33.8 | 1385.6
 Ouzbékistan           | Asie      |       28.9 |   33.8
 Arabie Saoudite       | Asie      |       28.8 |   28.9
 France métropolitaine | Europe    |       64.3 |
 Finlande              | Europe    |        5.4 |   64.3
 Lettonie              | Europe    |        2.1 |    5.4

lead() et lag() : principe

lag(population) OVER (PARTITION BY continent
                      ORDER BY population DESC)

Fonction lag()

first/last/nth_value

  • first_value(colonne)
    • retourne la première valeur pour la colonne
  • last_value(colonne)
    • retourne la dernière valeur pour la colonne
  • nth_value(colonne, n)
    • retourne la n-ème valeur (en comptant à partir de 1) pour la colonne

first/last/nth_value : exemple

SELECT pays, continent, population,
       first_value(population)
           OVER (PARTITION BY continent
                 ORDER BY population DESC)
  FROM population;
       pays      | continent | population | first_value
-----------------+-----------+------------+-------------
 Chine           | Asie      |     1385.6 |      1385.6
 Iraq            | Asie      |       33.8 |      1385.6
 Ouzbékistan     | Asie      |       28.9 |      1385.6
 Arabie Saoudite | Asie      |       28.8 |      1385.6
 France          | Europe    |       64.3 |        64.3
 Finlande        | Europe    |        5.4 |        64.3
 Lettonie        | Europe    |        2.1 |        64.3

Clause WINDOW

  • Pour factoriser la définition d’une fenêtre :
SELECT matricule, nom, salaire, service,
       rank() OVER w,
       dense_rank() OVER w
  FROM employes
 WINDOW w AS (ORDER BY salaire);

Clause WINDOW : syntaxe

SELECT fonction_agregat OVER nom,
       fonction_agregat_2 OVER nom …

  FROM <liste_tables>
 WHERE <predicats>
 WINDOW nom AS (PARTITION BYORDER BY …)

Définition de la fenêtre

  • La fenêtre de travail par défaut est :
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  • Trois modes possibles :
    • RANGE
    • ROWS
    • GROUPS (v11+)
  • Nécessite une clause ORDER BY

Définition de la fenêtre : RANGE

  • Indique un intervalle à bornes flou
  • Borne de départ :
    • UNBOUNDED PRECEDING: depuis le début de la partition
    • CURRENT ROW : depuis la ligne courante
  • Borne de fin :
    • UNBOUNDED FOLLOWING : jusqu’à la fin de la partition
    • CURRENT ROW : jusqu’à la ligne courante
    OVER (PARTITION BY
         ORDER BY
         RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING

Définition de la fenêtre : ROWS

  • Indique un intervalle borné par un nombre de ligne défini avant et après la ligne courante
  • Borne de départ :
    • xxx PRECEDING : depuis les xxx valeurs devant la ligne courante
    • CURRENT ROW : depuis la ligne courante
  • Borne de fin :
    • xxx FOLLOWING : depuis les xxx valeurs derrière la ligne courante
    • CURRENT ROW : jusqu’à la ligne courante
    OVER (PARTITION BY
         ORDER BY
         ROWS BETWEEN 2 PRECEDING AND 1 FOLLOWING

Définition de la fenêtre : GROUPS

  • Indique un intervalle borné par un groupe de lignes de valeurs identiques défini avant et après la ligne courante
  • Borne de départ :
    • xxx PRECEDING : depuis les xxx groupes de valeurs identiques devant la ligne courante
    • CURRENT ROW : depuis la ligne courante ou le premier élément identique dans le tri réalisé par ORDER BY
  • Borne de fin :
    • xxx FOLLOWING : depuis les xxx groupes de valeurs identiques derrière la ligne courante
    • CURRENT ROW : jusqu’à la ligne courante ou le dernier élément identique dans le tri réalisé par ORDER BY
   OVER (PARTITION BY
         ORDER BY
         GROUPS BETWEEN 2 PRECEDING AND 1 FOLLOWING

Définition de la fenêtre : EXCLUDE

  • Indique des lignes à exclure de la fenêtre de données (v11+)
  • EXCLUDE CURRENT ROW : exclut la ligne courante
  • EXCLUDE GROUP : exclut la ligne courante et le groupe de valeurs identiques dans l’ordre
  • EXCLUDE TIES exclut et le groupe de valeurs identiques à la ligne courante dans l’ordre mais pas la ligne courante
  • EXCLUDE NO OTHERS : pas d’exclusion (valeur par défaut)

Définition de la fenêtre : exemple

SELECT pays, continent, population,
       last_value(population)
        OVER (PARTITION BY continent ORDER BY population
              RANGE BETWEEN UNBOUNDED PRECEDING
                        AND UNBOUNDED FOLLOWING)
  FROM population;
         pays          | continent | population | last_value
-----------------------+-----------+------------+------------
 Arabie Saoudite       | Asie      |       28.8 |     1385.6
 Ouzbékistan           | Asie      |       28.9 |     1385.6
 Iraq                  | Asie      |       33.8 |     1385.6
 Chine (4)             | Asie      |     1385.6 |     1385.6
 Lettonie              | Europe    |        2.1 |       64.3
 Finlande              | Europe    |        5.4 |       64.3
 France métropolitaine | Europe    |       64.3 |       64.3

WITHIN GROUP

  • WITHIN GROUP
  • Utilité :
    • calcul de médianes, centiles

WITHIN GROUP : exemple

SELECT continent,
  percentile_disc(0.5)
    WITHIN GROUP (ORDER BY population) AS "mediane",
  percentile_disc(0.95)
    WITHIN GROUP (ORDER BY population) AS "95pct",
  ROUND(AVG(population), 1) AS moyenne
FROM population
 GROUP BY continent;
         continent         | mediane | 95pct  | moyenne
---------------------------+---------+--------+---------
 Afrique                   |    33.0 |  173.6 |    44.3
 Amérique du Nord          |    35.2 |  320.1 |   177.7
 Amérique latine. Caraïbes |    30.4 |  200.4 |    53.3
 Asie                      |    53.3 | 1252.1 |   179.9
 Europe                    |     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

GROUPING SETS : jeu de données

Opérateur GROUP BY | Opérateur GROUP BY   |   |

GROUPING SETS : exemple visuel

Opérateur GROUP BY

GROUPING SETS : exemple ordre sql

SELECT piece,region,sum(quantite)
FROM stock GROUP BY GROUPING SETS (piece,region);
 piece  | region | sum
--------+--------+-----
 clous  |        |  70
 ecrous |        |  90
 vis    |        | 160
        | est    | 120
        | nord   |  60
        | ouest  |  50
        | sud    |  90

GROUPING SETS : équivalent

  • On peut se passer de la clause GROUPING SETS
    • mais la requête sera plus lente
SELECT piece,NULL as region,sum(quantite)
  FROM stock
  GROUP BY piece
UNION ALL
SELECT NULL, region,sum(quantite)
  FROM STOCK
  GROUP BY region;

ROLLUP

  • ROLLUP
  • Utilité :
    • calcul de totaux dans la même requête

ROLLUP : exemple visuel

Opérateur GROUP BY

ROLLUP : exemple ordre sql

SELECT piece,region,sum(quantite)
FROM stock GROUP BY ROLLUP (piece,region);

Cette requête est équivalente à la requête suivante utilisant GROUPING SETS :

SELECT piece,region,sum(quantite)
FROM stock
GROUP BY GROUPING SETS ((),(piece),(piece,region));

CUBE

  • CUBE
  • Utilité :
    • calcul de totaux dans la même requête
    • sur toutes les clauses de regroupement

CUBE : exemple visuel

Opérateur GROUP BY

CUBE : exemple ordre sql

SELECT piece,region,sum(quantite)
FROM stock GROUP BY CUBE (piece,region);

Cette requête est équivalente à la requête suivante utilisant GROUPING SETS :

SELECT piece,region,sum(quantite)
FROM stock
GROUP BY GROUPING SETS (
  (),
  (piece),
  (region),
  (piece,region)
  );

Travaux pratiques

Travaux pratiques (solutions)

Types avancés

PostgreSQL offre des types avancés :

  • UUID
  • Tableaux
  • Composés :
    • hstore
    • JSON : json, jsonb
    • XML
  • Pour les objets binaires :
    • bytea
    • Large Objects

UUID

UUID : principe

  • Ex: d67572bf-5d8c-47a7-9457-a9ddce259f05
  • 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

CREATE UNIQUE INDEX ON 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.
SELECT ARRAY [1,2,3] ;

SELECT '{1,2,3}'::integer[] ;
-- lignes vers tableau
SELECT array_agg (i) FROM generate_series (1,3) i ;

-- tableau vers lignes
SELECT unnest ( '{1,2,3}'::integer[] ) ;
CREATE TABLE demotab ( id int, liste integer[] ) ;

Tableaux : recherche de valeurs

-- Recherche de valeurs (toutes)
SELECT * FROM demotab
WHERE liste @> ARRAY[15,11] ;

-- Au moins 1 élément commun ?
SELECT * FROM demotab
WHERE liste && ARRAY[11,55] ;

-- 1 tableau exact
SELECT * FROM demotab
WHERE liste = '{11,55}'::int[] ;

Tableaux : performances

Quel intérêt par rapport à 1 valeur par ligne ?

  • Allège certains modèles
    • données non triées (ex : liste de n°s de téléphone)
    • et sans 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
  • 3 types dans PostgreSQL :
    • hstore : clé/valeur
    • json : JSON, stockage texte, validation syntaxique, fonctions d’extraction
    • jsonb : JSON, stockage binaire, accès rapide, fonctions d’extraction, de requêtage, indexation avancée

hstore

hstore : principe

Stocker des données non structurées

  • Extension
  • Stockage Clé/Valeur, uniquement texte
  • Binaire
  • Indexable
  • Plusieurs opérateurs disponibles

hstore : exemple

CREATE EXTENSION hstore ;

CREATE TABLE animaux (nom text, caract hstore);
INSERT INTO animaux VALUES ('canari','pattes=>2,vole=>oui');
INSERT INTO animaux VALUES ('loup','pattes=>4,carnivore=>oui');
INSERT INTO animaux VALUES ('carpe','eau=>douce');

CREATE INDEX idx_animaux_donnees ON animaux
                                 USING gist (caract);
SELECT *, caract->'pattes' AS nb_pattes FROM animaux
WHERE caract@>'carnivore=>oui';
 nom  |              caract               | nb_pattes
------+-----------------------------------+-----------
 loup | "pattes"=>"4", "carnivore"=>"oui" | 4

JSON

JSON & PostgreSQL

{
  "firstName": "Jean",
  "lastName": "Dupont",
  "age": 27,
  "address": {
    "streetAddress": "43 rue du Faubourg Montmartre",
    "city": "Paris",
    "postalCode": "75009"
  }
}
  • json : format texte
  • jsonb : format binaire, à préférer
  • jsonpath : SQL/JSON paths (requêtage)

Type json

  • Type texte
    • avec validation du format
  • Réservé au stockage à l’identique
  • Préférer jsonb

Type jsonb

  • JSON au format Binaire
  • Indexation GIN
  • Langage JSONPath

Validation du format JSON

SELECT  '{"auteur": "JRR Tolkien","titre":"Le Hobbit"}' IS JSON ;
-- true
SELECT  '{"auteur": "JRR Tolkien","titre":"Le Hobbit}' IS JSON ;
-- false
  • Aussi : IS JSON WITH/WITHOUT UNIQUE KEYS, IS JSON ARRAY, IS JSON OBJECT, IS JSON SCALAR
  • PostgreSQL 16+

JSON : Exemple d’utilisation

CREATE TABLE personnes (datas jsonb );
INSERT INTO personnes  (datas) VALUES ('
{
  "firstName": "Jean",
  "lastName": "Valjean",
  "address": {
    "streetAddress": "43 rue du Faubourg Montmartre",
    "city": "Paris",
    "postalCode": "75002"
  },
  "phoneNumbers": [
    { "number": "06 12 34 56 78" },
    { "type": "bureau", "number": "07 89 10 11 12"}
  ],
  "children": ["Cosette"]
} '),  ('{
  "firstName": "Georges",
  "lastName": "Durand",
  "address": {
    "streetAddress": "27 rue des Moulins",
    "city": "Châteauneuf",
    "postalCode": "45990"
  },
  "phoneNumbers": [
    { "number": "06 21 34 56 78" },
    { "type": "bureau", "number": "07 98 10 11 12"}
  ],
  "children": []
} '),  ('{
  "firstName": "Jacques",
  "lastName": "Dupont",
  "isAlive": true,
  "age": 27,
  "address": {
    "streetAddress": "43 rue du Faubourg Montmartre",
    "city": "Paris",
    "state": "",
    "postalCode": "75002"
  },
  "phoneNumbers": [
    {
      "type": "personnel",
      "number": "+33 1 23 45 67 89"
    },
    {
      "type": "bureau",
      "number": "07 00 00 01 23"
    }
  ],
  "children": [],
  "spouse": "Martine Durand"
} ');

JSON : construction

SELECT '{"nom": "Lagaffe", "prenom": "Gaston"}'::jsonb ;

SELECT jsonb_build_object ('nom', 'Lagaffe', 'prenom', 'Gaston') ;

JSON : Affichage des attributs

SELECT datas->>'firstName' AS prenom,    -- text
       datas->'address'    AS addr       -- jsonb
FROM personnes ;
SELECT  datas #>> '{address,city}',      -- text
        datas #> '{address,city}'        -- jsonb
FROM personnes ; -- text
SELECT datas['address']['city'] as villes from personnes ;  -- jsonb, v14
SELECT datas['address']->>'city' as villes from personnes ;  -- text, v14
  • Attention : pas de contrôle de l’existence de la clé !
    • et attention à la casse

Modifier un JSON

-- Concaténation (attention aux NULL)
SELECT '{"nom": "Durand"}'::jsonb ||
  '{"address" : {"city": "Paris", "postalcode": "75002"}}'::jsonb ;

-- Suppression
SELECT  '{"nom": "Durand",
         "address": {"city": "Paris", "postalcode": "75002"}}'::jsonb
        - 'nom' ;

SELECT  '{"nom": "Durand",
         "address": {"city": "Paris", "postalcode": "75002"}}'::jsonb
        #- '{address,postalcode}' ;

-- Modification
SELECT jsonb_set ('{"nom": "Durand", "address": {"city": "Paris"}}'::jsonb,
       '{address}',
      '{"ville": "Lyon" }'::jsonb) ;

JSON, tableaux et agrégation

  • Manipuler des tableaux :
    • jsonb_array_elements(), jsonb_array_elements_text()
    • jsonb_agg() (de JSON ou de scalaires)
    • jsonb_agg()_strict (sans les NULL)
    SELECT jsonb_array_elements (datas->'phoneNumbers')->>'number'
    FROM   personnes ;

Conversions jsonb / relationnel

  • 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()

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';  -- textes

 WHERE 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

SELECTFROM
 -- recherche
 WHERE datas @? '$.lastName ? (@ == "Valjean")' ;
 WHERE datas @@ '$.lastName  == "Valjean"' ;
SELECT jsonb_path_query(datas,'$.phoneNumbers[*] ? (@.type == "bureau")')
FROM   personnes
WHERE  datas @@ '$.lastName == "Durand"' ;
-- Affichage + filtrage
SELECT datas->>'lastName',
       jsonb_path_query(datas,'$.address ? (@.city == "Paris")')
FROM   personnes ;

jsonb : indexation (1/2)

  • Index fonctionnel sur un attribut précis
    • bonus : statistiques
    CREATE INDEX 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
    ALTER TABLE personnes
    ADD COLUMN lastname text
    GENERATED 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 : ?, ?|, ?& , @> , @?, @@)
    CREATE INDEX idx_prs ON personnes USING gin(datas) ;
    -- index plus performant
    --  mais que @> , @?, @@
    CREATE INDEX idx_prs ON personnes USING gin(datas jsonb_path_ops) ;

Pour aller plus loin…

Lire la documentation

XML

XML : présentation

  • Type xml
    • stocke un document XML
    • valide sa structure
  • Quelques fonctions et opérateurs disponibles :
    • XMLPARSE, XMLSERIALIZE, query_to_xml, xmlagg
    • xpath (XPath 1.0 uniquement)

Objets binaires

Objets binaires : les types

  • Souvent une mauvaise idée…

  • 2 méthodes

    • bytea : type binaire
    • Large Objects : manipulation comme un fichier

bytea

  • Un type comme les autres
    • bytea : tableau d’octets
    • en texte : bytea_output = hex ou escape
  • Récupération intégralement en mémoire !
  • Toute modification entraîne la réécriture complète du bytea
  • Maxi 1 Go (à éviter)
    • en pratique intéressant pour quelques Mo
  • Import :
SELECT pg_read_binary_file ('/chemin/fichier');

Large Object

  • À éviter…
    • préférer bytea
  • Maxi 4 To (éviter…)
  • Objet indépendant des tables
    • OID à stocker dans les tables
    • se compresse mal
  • Suppression manuelle !
    • trigger
    • lo_unlink & vacuumlo
  • Fonction de manipulation, modification
    • lo_create, lo_import
    • lo_seek, lo_open, lo_read, lo_write

Quiz

Travaux pratiques

UUID

jsonb

Large Objects

Travaux pratiques (solutions)

PL/pgSQL : les bases

PostgreSQL

Préambule

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

Au menu

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

Objectifs

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

Introduction

Qu’est-ce qu’un PL ?

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

Quels langages PL sont disponibles ?

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

Langages trusted vs untrusted

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

Les langages PL de PostgreSQL

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

Intérêts de PL/pgSQL en particulier

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

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

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

Routines / Procédures stockées / Fonctions

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

Installation

Installation des binaires nécessaires

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

Activer un langage

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

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

Exemples de fonctions & procédures

Fonction PL/pgSQL simple

Une fonction simple en PL/pgSQL :

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

Exemple de fonction SQL

Même fonction en SQL pur :

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

Exemple de fonction PL/pgSQL utilisant la base

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

Exemple de fonction PL/Perl complexe

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

Exemple de fonction PL/pgSQL complexe

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

Exemple de procédure

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

Exemple de bloc anonyme en PL/pgSQL

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

Utiliser une fonction ou une procédure

Invocation d’une fonction ou procédure

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

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

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

CALL ma_procedure( mafonction() );

CREATE INDEX ON ma_table ( ma_fonction(ma_colonne) );

Contrôle transactionnel dans les procédures

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

Création et maintenance des fonctions et procédures

Création

  • CREATE FUNCTION
  • CREATE PROCEDURE

Structure d’une routine PL/pgSQL

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

Structure d’une routine PL/pgSQL (suite)

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

Blocs nommés

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

Modification du code d’une routine

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

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

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

Suppression d’une routine

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

Utilisation des guillemets

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

Paramètres et retour des fonctions et procédures

Version minimaliste

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

Paramètres IN, OUT, INOUT & retour

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

Type en retour : 1 valeur simple

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

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

Comment obtenir ceci ?

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

Type en retour : 1 ligne, plusieurs champs

3 options :

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

Retour multiligne

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

Gestion des valeurs NULL

Comment gérer les paramètres à NULL ?

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

Variables en PL/pgSQL

Clause DECLARE

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

Constantes

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

Types de variables

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

Type ROW - 1

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

Type ROW - 2

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

    $$

Type RECORD

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

Type RECORD : exemple

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

  END LOOP ;
  RETURN … ;
END $$ ;

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

Requête dans un bloc PL/pgSQL

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

Affectation d’une valeur à une variable

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

Exécution d’une requête

  • Affectation de la ligne :

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

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

    • écrire une boucle
  • Ordre statique :

    • colonnes, clause WHERE, tables figées

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

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

SQL dynamique

EXECUTE d’une requête

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

EXECUTE & requête dynamique : injection SQL

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

que renvoie ceci ?

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

EXECUTE & requête dynamique : 3 possibilités

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

EXECUTE & requête dynamique (suite)

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

Outils pour construire une requête dynamique

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

Structures de contrôle en PL/pgSQL

  • But du PL : les traitements procéduraux

Tests conditionnels - 2

Exemple :

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

Tests conditionnels : CASE

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

ou :

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

Boucle LOOP/EXIT/CONTINUE : syntaxe

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

Boucle LOOP/EXIT/CONTINUE : exemple

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

Boucle WHILE

    WHILE condition LOOP

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

Boucle FOR : syntaxe

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

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

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

    FOR ligne IN ( SELECT * FROM ma_table ) LOOP

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

Boucle FOREACH

    FOREACH variable [SLICE n] IN ARRAY expression LOOP

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

Autres propriétés des fonctions

  • Sécurité
  • Optimisations
  • Parallélisation

Politique de sécurité

  • SECURITY INVOKER : défaut

  • SECURITY DEFINER

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

Optimisation des fonctions

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

Parallélisation

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

Utilisation de fonctions dans les index

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

Conclusion

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

Pour aller plus loin

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

Questions

FOR q IN (SELECT * FROM questions ) LOOP

  répondre (q) ;

END LOOP ;

Quiz

Travaux pratiques

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

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

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

Hello

Division

SELECT sur des tables dans les fonctions

Multiplication

Salutations

Inversion de chaîne

Jours fériés

Index fonctionnels

Travaux pratiques (solutions)

PL/pgSQL avancé

PostgreSQL

Préambule

Au menu

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

Objectifs

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

Routines variadic

Routines variadic : introduction

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

Routines variadic : exemple

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

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

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

Routines variadic : exemple PL/pgSQL

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

Routines polymorphes

Routines polymorphes : introduction

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

Routines polymorphes : anyelement

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

Routines polymorphes : anyarray

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

Routines polymorphes : exemple

L’addition est un exemple fréquent :

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

Routines polymorphes : tests

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

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

Routines polymorphes : problème

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

Fonctions trigger

Fonctions trigger : introduction

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

Fonctions trigger : variables (1/5)

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

Fonctions trigger : variables (2/5)

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

Fonctions trigger : variables (3/5)

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

Fonctions trigger : variables (4/5)

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

Fonctions trigger : variables (5/5)

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

Fonctions trigger : retour

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

Fonctions trigger : exemple - 1

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

Fonctions trigger : exemple - 2

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

Options de CREATE TRIGGER

CREATE TRIGGER permet quelques variantes :

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

Tables de transition

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

Curseurs

Curseurs : introduction

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

Curseurs : déclaration d’un curseur

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

Curseurs : ouverture d’un curseur

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

Curseurs : ouverture d’un curseur lié

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

Curseurs : récupération des données

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

Curseurs : récupération des données

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

Curseurs : modification des données

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

Curseurs : fermeture d’un curseur

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

Curseurs : renvoi d’un curseur

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

Gestion des erreurs

Gestion des erreurs : introduction

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

Gestion des erreurs : une exception

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

Gestion des erreurs : flot dans une fonction

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

Gestion des erreurs : flot dans une exception

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

Gestion des erreurs : codes d’erreurs

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

Messages d’erreurs : RAISE - 1

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

Messages d’erreurs : RAISE - 2

Exemples :

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

Messages d’erreurs : configuration des logs

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

Messages d’erreurs : RAISE EXCEPTION - 1

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

Messages d’erreurs : RAISE EXCEPTION - 2

Exemple :

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

Flux des erreurs dans du code PL

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

Flux des erreurs dans du code PL - 2

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

Flux des erreurs dans du code PL - 3

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

Flux des erreurs dans du code PL - 4

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

Sécurité

Sécurité : droits

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

Sécurité : ajout

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

Sécurité : suppression

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

Sécurité : SECURITY INVOKER/DEFINER

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

Sécurité : LEAKPROOF

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

Sécurité : visibilité des sources - 1

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

Sécurité : visibilité des sources - 2

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

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

Sécurité : Injections SQL

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

Optimisation

Fonctions immutables, stables ou volatiles - 1

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

Fonctions immutables, stables ou volatiles - 2

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

Fonctions immutables, stables ou volatiles - 3

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

Optimisation : rigueur

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

Optimisation : EXCEPTION

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

Requête statique ou dynamique ?

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

Requête statique ou dynamique ? - 2

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

Requête statique ou dynamique ? -3

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

Outils

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

pldebugger

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

pldebugger - Compilation

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

pldebugger - Activation

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

auto_explain

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

pldebugger - Utilisation

  • Via pgAdmin

log_functions

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

log_functions - Compilation

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

log_functions - Activation

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

log_functions - Configuration

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

log_functions - Utilisation

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

Conclusion

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

Pour aller plus loin

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

Questions

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

Travaux pratiques

Travaux pratiques (solutions)

Extensions PostgreSQL pour l’utilisateur

PostgreSQL

Qu’est-ce qu’une extension ?

  • Pour ajouter :
    • types de données
    • méthodes d’indexation
    • fonctions et opérateurs
    • tables, vues…
  • Tous sujets, tous publics
  • Intégrées (« contribs ») ou projets externes

Administration des extensions

Techniquement :

  • « packages » pour PostgreSQL, en C, SQL, PL/pgSQL…
  • Langages : SQL, PL/pgSQL, C (!)…
  • Ensemble d’objets livrés ensemble
  • contrib <> extension

Installation des extensions

  • Packagées ou à compiler
  • Par base :
    • CREATE EXTENSION … CASCADE
    • ALTER EXTENSION UPDATE
    • DROP EXTENSION
    • \dx
  • Listées dans pg_available_extensions

Contribs - Fonctionnalités

  • Livrées avec le code source de PostgreSQL
  • Habituellement packagées (postgresql-*-contrib)
  • De qualité garantie car maintenues par le projet
  • Optionnelles, désactivées par défaut
  • Ou en cours de stabilisation
  • Documentées : Chapitre F : « Modules supplémentaires fournis »

Quelques extensions

…plus ou moins connues

pgcrypto

Module contrib de chiffrement :

  • Nombreuses fonctions pour chiffrer et déchiffrer des données
  • Gros inconvénient : oubliez les index sur les données chiffrées !
  • N’oubliez pas de chiffrer la connexion (SSL)
  • Permet d’avoir une seule méthode de chiffrement pour tout ce qui accède à la base

hstore : stockage clé/valeur

  • Contrib
  • Type hstore
  • Stockage clé-valeur
  • Plus simple que JSON
INSERT INTO demo_hstore (meta) VALUES ('river=>t');
SELECT * FROM demo_hstore WHERE meta@>'river=>t';

PostgreSQL Anonymizer

  • Extension externe (Dalibo)
  • Masquage statique et dynamique
  • Export anonyme (pg_dump_anon)
  • Les règles de masquage sont écrites en SQL
  • Autodétection de colonnes identifiantes
  • Plus simple et plus sûr qu’un ETL

PostGIS

Logo Postgis
  • Projet indépendant, GPL, https://postgis.net/
  • Module spatial pour PostgreSQL
    • Extension pour types géométriques/géographiques & outils
    • La référence des bases de données spatiales
    • « quelles sont les routes qui coupent le Rhône ? »
    • « quelles sont les villes adjacentes à Toulouse ? »
    • « quels sont les restaurants situés à moins de 3 km de la Nationale 12 ? »

Mais encore…

  • uuid-ossp : gérer des UUID
  • unaccent : supprime des accents
  • citex : recherche insensible à la casse

Autres extensions connues

  • Compatibilité :
    • orafce
  • Extensions propriétaires évitant un fork :
    • Citus (sharding)
    • TimescaleDB (time series)
    • être sûr que PostgreSQL a atteint ses limites !

Extensions pour de nouveaux langages

  • PL/pgSQL par défaut
  • Ajouter des langages :
    • PL/python
    • PL/perl
    • PL/lua
    • PL/sh
    • PL/R
    • PL/Java
    • etc.

Accès distants

Accès à des bases distantes

  • Contribs :
    • dblink (ancien)
    • les foreign data wrappers : postgresql_fdw, mysql_fdw…
  • Sharding :
    • PL/Proxy
    • Citus

Contribs orientés DBA

Accès à des informations ou des fonctions de bas niveau :

  • pg_prewarm : sauvegarde & restauration de l’état du cache de la base
  • pg_buffercache : état du cache
  • pgstattuple (fragmentation des tables et index), pg_freespacemap (blocs libres), pg_visibility (visibility map)
  • pageinspect : inspection du contenu d’une page
  • pgrowlocks : informations détaillées sur les enregistrements verrouillés
  • pg_stat_statement (requêtes normalisées), auto_explain (plans)
  • amcheck : validation des index
  • … et de nombreux projets externes

PGXN

PostgreSQL eXtension Network :

  • Site web : pgxn.org
    • nombreuses extensions
    • volontariat
    • aucune garantie de qualité
    • tests soigneux requis
  • Et optionnellement client en python pour automatisation de déploiement
  • Ancêtre : pgFoundry
  • Beaucoup de projets sont aussi sur github

Créer son extension

Conclusion

  • Un nombre toujours plus important d’extension pour étendre les possibilités de PostgreSQL
  • Un site central pour les extensions : PGXN.org
  • Rajoutez les vôtres !

Questions

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

Travaux pratiques

Masquage statique de données avec PostgreSQL Anonymizer

Masquage dynamique de données avec PostgreSQL Anonymizer

Partitionnement sous PostgreSQL

PostgreSQL
  • Ses principes et intérêts
  • Historique
  • Les différents types

Principe & intérêts du partitionnement

  • Faciliter la maintenance de gros volumes
    • VACUUM (FULL), réindexation, déplacements, sauvegarde logique…
  • Performances
    • parcours complet sur de plus petites tables
    • statistiques par partition plus précises
    • purge par partitions entières
    • pg_dump parallélisable
    • tablespaces différents (données froides/chaudes)
  • Attention à la maintenance sur le code

Partitionnement applicatif

  • …ou la réinvention de la roue
  • Gestion au niveau applicatif, table par table
  • Complexité pour le développeur
  • Intégrité des données ?

Méthodes de partitionnement intégrées à PostgreSQL

  • Partitionnement par héritage (historique)
  • Partitionnement déclaratif (>=v10, préférer v13+)

Partitionnement par héritage

  • Historique ou pour cas très spécifique
  • Syntaxe :
CREATE TABLE primates (debout boolean) INHERITS (mammiferes) ;
  • Table mère :
    • définie normalement, peut contenir des lignes
  • Tables filles :
    • héritent des propriétés de la table mère
    • …mais pas des contraintes, index et droits
    • colonnes supplémentaires possibles
  • Insertion applicative ou par trigger (lent !)

Partitionnement déclaratif

  • Préférer version 13+
  • Mise en place et administration simplifiées (intégrées au moteur)
  • Gestion automatique des lectures et écritures (et rapide)
  • Partitions
    • attacher/détacher une partition
    • contrainte implicite de partitionnement
    • expression possible pour la clé de partitionnement
    • sous-partitions possibles
    • partition par défaut

Partitionnement par liste

Partitionnement par liste

Partitionnement par liste : implémentation

  • Liste de valeurs par partition

    • statut, client, pays, année ou mois…
  • Clé de partitionnement forcément mono-colonne

  • Syntaxe :

CREATE TABLE t1(c1 integer, c2 text) PARTITION BY LIST (c1) ;

CREATE TABLE t1_a PARTITION OF t1 FOR VALUES IN (1, 2, 3);
CREATE TABLE t1_b PARTITION OF t1 FOR VALUES IN (4, 5);

Partitionnement par intervalle

Partitionnement par intervalle

Partitionnement par intervalle : implémentation

  • Clé de partitionnement mono- ou multicolonne
    • dates, id…
  • Bornes :
    • supérieure exclue
    • MINVALUE / MAXVALUE pour infinis
  • Syntaxe :
CREATE TABLE t2(c1 integer, c2 text) PARTITION BY RANGE (c1);

CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES FROM (1) TO (100);
CREATE TABLE t2_2 PARTITION OF t2 FOR VALUES FROM (100) TO (MAXVALUE);

Partitionnement par hachage

Partitionnement par hachage

Partitionnement par hachage : principe

Pour une répartition uniforme des données :

  • Hachage de valeurs par partition

    • indiquer un modulo et un reste
  • Clé de partitionnement mono- ou multicolonnes

  • Syntaxe :

CREATE TABLE t3(c1 integer, c2 text) PARTITION BY HASH (c1);

CREATE TABLE t3_a PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 0);
CREATE TABLE t3_b PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 1);
CREATE TABLE t3_c PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 2);

Clé de partitionnement multicolonne

  • Clé sur plusieurs colonnes :
    • si partitionnement par intervalle ou hash (pas par liste)
    • et si 1er champ toujours présent
  • Syntaxe :
CREATE TABLE t1(c1 integer, c2 text, c3 date)
PARTITION BY RANGE (c1, c3) ;

CREATE TABLE t1_a PARTITION OF t1
FOR VALUES FROM (1,'2017-08-10') TO (100, '2017-08-11') ;

Sous-partitionnement

S’il y a deux chemins d’accès privilégiés :

CREATE TABLE objets (id int, statut int, annee int, t text)
 PARTITION BY LIST (statut) ;

CREATE TABLE objets_123
 PARTITION OF objets FOR VALUES IN (1, 2, 3)
 PARTITION BY LIST (annee) ;

CREATE TABLE objets_123_2023
 PARTITION OF objets_123 FOR VALUES IN (2023) ;
CREATE TABLE objets_123_2024
 PARTITION OF objets_123 FOR VALUES IN (2024) ;

CREATE TABLE objets_45
 PARTITION OF objets FOR VALUES IN (4,5) ;
  • Plus souple que le partitionnement multicolonne

Partition par défaut

  • Pour le partitionnement par liste ou par intervalle
  • Toutes les données n’allant pas dans les partitions définies iront dans la partition par défaut
CREATE TABLE t2_autres PARTITION OF t2 DEFAULT ;
  • La conserver petite

Attacher une partition

ALTER TABLE … ATTACH PARTITIONFOR VALUES … ;
  • La table doit préexister
  • Vérification du respect de la contrainte par les données existantes
    • parcours complet de la table
    • potentiellement lent !
    • …sauf si contrainte CHECK identique déjà ajoutée
  • Si la partition par défaut a des données qui iraient dans cette partition :
    • erreur à l’ajout de la nouvelle partition
    • détacher la partition par défaut
    • ajouter la nouvelle partition
    • déplacer les données de l’ancienne partition par défaut
    • ré-attacher la partition par défaut

Détacher une partition

ALTER TABLE … DETACH PARTITION
  • Simple et rapide
  • Mais nécessite un verrou exclusif
    • option CONCURRENTLY (v14+)

Supprimer une partition

DROP TABLE nom_partition ;

Fonctions de gestion et vues système

  • Sous psql : \dP
  • pg_partition_tree ('logs') : liste entière des partitions
  • pg_partition_root ('logs_2019') : racine d’une partition
  • pg_partition_ancestors ('logs_201901') : parents d’une partition

Clé primaire et clé de partitionnement

La clé primaire doit contenir toutes les colonnes de la clé de partitionnement.

  • Idem pour une contrainte unique
  • Pas un problème si on partitionne selon la clé
  • Plus gênant dans d’autres cas (date, statut…)

Indexation

  • Propagation automatique
  • Index supplémentaires par partition possibles
  • Clés étrangères entre tables partitionnées

Planification & performances

  • Mettre la clé dans la requête autant que possible
  • ou : cibler les partitions directement
  • Temps de planification
    • nombre de tables, d’index, leurs statistisques…
    • max ~ 100 partitions
  • À activer ?
    • enable_partitionwise_aggregate
    • enable_partitionwise_join

Opérations de maintenance

  • Changement de tablespace
  • autovacuum/analyze
    • sur les partitions comme sur toute table
  • VACUUM, VACUUM FULL, ANALYZE
    • sur table mère : redescendent sur les partitions
  • REINDEX
    • avant v14 : uniquement par partition
  • ANALYZE
    • prévoir aussi sur la table mère (manuellement…)

Sauvegardes

Sauvegarde physique : peu de différence

Avec pg_dump :

  • --jobs : efficace
  • --load-via-partition-root
  • exclusion de partitions (v16+) :
    • --table-and-children
    • --exclude-table-and-children
    • --exclude-table-data-and-children

Limitations du partitionnement déclaratif et versions

  • Pas de création automatique des partitions
  • Planification : 100 partitions max conseillé
  • Pas d’héritage multiple, schéma fixe
  • Partitions distantes : sans propagation d’index
  • PostgreSQL >= 13 conseillée !
    • limitations sur versions précédentes
    • contournement : travailler par partition

Tables distantes & sharding

  • Tables distantes comme partitions : sharding
  • (v14+) Interrogation simultanée asynchrone

Extensions & outils

  • Extension pg_partman
    • automatisation
  • Extensions dédiées à un domaine :
    • timescaledb
    • citus

Conclusion

Le partitionnement déclaratif a de gros avantages pour le DBA

  • …mais les développeurs doivent savoir l’utiliser
  • Préférer une version récente de PostgreSQL

Quiz

Travaux pratiques

Partitionnement

Partitionner pendant l’activité

Travaux pratiques (solutions)

Connexions distantes

Accès à distance à d’autres sources de données

  • Modules historiques : dblink
  • SQL/MED & Foreign Data Wrappers
  • Sharding par fonctions : PL/Proxy
  • Le sharding est Work In Progress

SQL/MED

  • Management of External Data
  • Extension de la norme SQL ISO
  • Données externes présentées comme des tables
  • Grand nombre de fonctionnalités disponibles
    • mais tous les connecteurs n’implémentent pas tout
  • Données accessibles par l’intermédiaire de tables
    • ces tables ne contiennent pas les données localement
    • l’accès à ces tables provoque une récupération des données distantes

Objets proposés par SQL/MED

  • Foreign Data Wrapper
    • connecteur permettant la connexion à un serveur externe et l’exécution de requête
  • Foreign Server
    • serveur distant
  • User Mapping
    • correspondance d’utilisateur local vers distant
  • Foreign Table
    • table distante (ou table externe)

Foreign Data Wrapper

  • Pilote d’accès aux données
  • Couverture variable des fonctionnalités
  • Qualité variable
  • Exemples de connecteurs
    • PostgreSQL, SQLite, Oracle, MySQL (lecture/écriture)
    • fichier CSV, fichier fixe (en lecture)
    • ODBC, JDBC
    • CouchDB, Redis (NoSQL)
  • Disponible généralement sous la forme d’une extension
    • ajouter l’extension ajoute le Foreign Data Wrapper à une base

Fonctionnalités disponibles pour un FDW (1/2)

  • Support des lecture de tables (SELECT)
  • Support des écriture de tables (y compris TRUNCATE)
    • directement pour INSERT
    • récupération de la ligne en local pour un UPDATE/DELETE
  • Envoi sur le serveur distant
    • des prédicats
    • des jointures si les deux tables jointes font partie du même serveur distant
    • des agrégations

Fonctionnalités disponibles pour un FDW (2/2)

  • Mais aussi
    • support du EXPLAIN
    • support du ANALYZE (amélioration en v16)
    • support des triggers
    • support de la parallélisation
    • support des exécutions asynchrones (v14)
    • possibilité d’importer un schéma complet

Foreign Server

  • Encapsule les informations de connexion
  • Le Foreign Data Wrapper utilise ces informations pour la connexion
  • Chaque Foreign Data Wrapper propose des options spécifiques
    • nom du fichier pour un FDW listant des fichiers
    • adresse IP, port, nom de base pour un serveur SQL
    • autres

User Mapping

  • Correspondance utilisateur local / utilisateur distant
  • Mot de passe stocké chiffré
  • Optionnel
    • aucun intérêt pour les FDW fichiers
    • essentiel pour les FDW de bases de données

Foreign Table

  • Définit une table distante
  • Doit comporter les colonnes du bon type
    • pas forcément toutes
    • pas forcément dans le même ordre
  • Peut être une partition d’une table partitionnée
  • Possibilité d’importer un schéma complet
    • simplifie grandement la création des tables distantes

Exemple : file_fdw

Foreign Data Wrapper de lecture de fichiers CSV.

CREATE EXTENSION file_fdw;

CREATE SERVER fichier FOREIGN DATA WRAPPER file_fdw ;

CREATE FOREIGN TABLE donnees_statistiques (f1 numeric, f2 numeric)
   SERVER fichier
   OPTIONS (filename  '/tmp/fichier_donnees_statistiques.csv',
            format    'csv',
            delimiter ';') ;

Exemple : postgres_fdw

  • Pilote le plus abouti, et pour cause
    • il permet de tester les nouvelles fonctionnalités de SQL/MED
    • il sert d’exemple pour les autres FDW
  • Propose en plus :
    • une gestion des transactions explicites
    • un pooler de connexions

SQL/MED : Performances

  • Tous les FDW : vues matérialisées et indexations
  • postgres_fdw : fetch_size

SQL/MED : héritage

  • Une table locale peut hériter d’une table distante et inversement
  • Permet le partitionnement sur plusieurs serveurs
  • Pour rappel, l’héritage ne permet pas de conserver
    • les contraintes d’unicité et référentielles
    • les index
    • les droits
  • Permet le requêtage inter-bases PostgreSQL
  • Simple et bien documenté
  • En lecture seule sauf à écrire des triggers sur vue
  • Ne transmet pas les prédicats
    • tout l’objet est systématiquement récupéré
  • Préférer postgres_fdw

PL/Proxy

  • Langage de procédures
    • développée à la base par Skype
  • Fonctionnalités
    • connexion à un serveur ou à un ensemble de serveurs
    • exécution de fonctions, pas de requêtes
  • Possibilité de distribuer les requêtes
  • Utile pour le « partionnement horizontal »
  • Uniquement si votre application n’utilise que des appels de fonction
    • dans le cas contraire, il faut revoir l’application

Conclusion

  • Privilégier SQL/MED
  • dblink et PL/Proxy en perte de vitesse
    • à n’utiliser que s’ils résolvent un problème non gérable avec SQL/MED

Travaux pratiques

Foreign Data Wrapper sur un fichier

Foreign Data Wrapper sur une autre base

Travaux pratiques (solutions)

Fonctionnalités avancées pour la performance

Préambule

Comme tous les SGBD, PostgreSQL fournit des fonctionnalités avancées.

Ce module présente des fonctionnalités internes au moteur généralement liées aux performances.

Au menu

  • Tables temporaires
  • Tables non journalisées
  • JIT
  • Recherche Full Text

Tables temporaires

CREATE TEMP TABLE  travail (…) ;
  • N’existent que pendant la session
  • Non journalisées
  • Ne pas en abuser !
  • Ignorées par autovacuum : ANALYZE et VACUUM manuels !
  • Paramétrage :
    • temp_buffers : cache disque pour les objets temporaires, par session, à augmenter ?

Tables non journalisées (unlogged)

  • La durabilité est parfois accessoire :
    • tables temporaires et de travail
    • caches…
  • Tables non journalisées
    • non répliquées, non restaurées
    • remises à zéro en cas de crash
  • Respecter les contraintes

Tables non journalisées  : mise en place

CREATE UNLOGGED TABLE ma_table (col1 int …) ;

Bascule d’une table en/depuis unlogged

ALTER TABLE table_normale SET UNLOGGED ;
  • réécriture
ALTER TABLE table_unlogged SET LOGGED ;
  • passage du contenu dans les WAL !

Colonnes générées

CREATE TABLE paquet (
  code       text          PRIMARY KEY, …
  livraison  timestamptz   DEFAULT now() + interval '3d',
  largeur    int,    longueur int,   profondeur int,
  volume     int
      GENERATED ALWAYS AS ( largeur * longueur * profondeur )
      STORED    CHECK (volume > 0.0)
  ) ;
  • DEFAULT : expressions très simples, modifiables
  • GENERATED
    • fonctions « immutables », ne dépendant que de la ligne
    • danger sinon (ex : pas pour dates de mises à jour)
    • difficilement modifiables
    • peuvent porter des contraintes

JIT : la compilation à la volée

  • Compilation Just In Time des requêtes
  • Utilise le compilateur LLVM
  • Vérifier que l’installation est fonctionnelle
  • Activé par défaut
    • sauf en v11 ; et absent auparavant

JIT : qu’est-ce qui est compilé ?

  • Tuple deforming
  • Évaluation d’expressions :
    • WHERE
    • agrégats, GROUP BY
  • Appels de fonctions (inlining)
  • Mais pas les jointures

JIT : algorithme « naïf »

  • jit (défaut : on)
  • jit_above_cost (défaut : 100 000)
  • jit_inline_above_cost (défaut : 500 000)
  • jit_optimize_above_cost (défaut : 500 000)
  • À comparer au coût de la requête… I/O comprises
  • Seuils arbitraires !

Quand le JIT est-il utile ?

  • Goulot d’étranglement au niveau CPU (pas I/O)
  • Requêtes complexes (calculs, agrégats, appels de fonctions…)
  • Beaucoup de lignes, filtres
  • Assez longues pour « rentabiliser » le JIT
  • Analytiques, pas ERP

Recherche Plein Texte

Full Text Search : Recherche Plein Texte

  • Recherche « à la Google » ; fonctions dédiées
  • On n’indexe plus une chaîne de caractère mais
    • les mots (« lexèmes ») qui la composent
    • on peut rechercher sur chaque lexème indépendamment
  • Les lexèmes sont soumis à des règles spécifiques à chaque langue
    • notamment termes courants
    • permettent une normalisation, des synonymes…

Full Text Search : exemple

  • Décomposition :
SELECT to_tsvector ('french',
                   'Longtemps je me suis couché de bonne heure');
               to_tsvector
-----------------------------------------
 'bon':7 'couch':5 'heur':8 'longtemp':1
  • Recherche sur 2 mots :
SELECT * FROM textes
WHERE to_tsvector('french',contenu) @@ to_tsquery('Valjean & Cosette');
  • Recherche sur une phrase : phrase_totsquery

Full Text Search : dictionnaires

  • Configurations liées à la langue
    • basées sur des dictionnaires (parfois fournis)
    • dictionnaires filtrants (unaccent)
    • synonymes
  • Extensible grâce à des sources extérieures
  • Configuration par défaut : default_text_search_config

Full Text Search : stockage & indexation

  • Stocker to_tsvector (champtexte)
    • colonne mise à jour par trigger
    • ou colonne générée (v12)
  • Indexation GIN ou GiST

Full Text Search sur du JSON

  • Vectorisation possible des JSON
SELECT info FROM commandes c
WHERE to_tsvector ('french', c.info) @@ to_tsquery('papier') ;
                            info
----------------------------------------------------------------
 {"items": {"qté": 5, "produit": "Rame papier normal A4"},
            "client": "Benoît Delaporte"}
 {"items": {"qté": 5, "produit": "Pochette Papier dessin A3"},
            "client": "Lucie Dumoulin"}

Quiz

Travaux pratiques

Tables non journalisées

Indexation Full Text

Travaux pratiques (solutions)

Masquage de données & postgresql_anonymizer

postgresql_anonymizer

Cas d’usage

  • Paul : le propriétaire
  • Pierre : data scientist
  • Jack : employé chargé des fournisseurs

Objectifs

Nous allons découvrir :

  • comment écrire des règles de masquage
  • la différence entre masquage dynamique et masquage statique
  • comment implémenter un masquage avancé

PostgreSQL Anonymizer

Principe

Principe :

  • Extension
  • Déclaratif (DDL)

Masquages

Principe :

  • statique
  • dynamique
  • sauvegardes anonymisées
  • « généralisation »

Pré-requis

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

pierre, paul, jack et la base boutique :

CREATE ROLE paul LOGIN SUPERUSER;
CREATE ROLE pierre LOGIN;
CREATE ROLE jack LOGIN;

-- Define a password for each user with:
-- \password paul  or  ALTER ROLE paul PASSWORD 'change-me';

CREATE DATABASE boutique OWNER paul;
ALTER DATABASE boutique
  SET session_preload_libraries = 'anon';

Masquage statique avec postgresql_anonymizer

  • 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)

Table « customer »

\c boutique paul
DROP TABLE IF EXISTS customer CASCADE;
DROP TABLE IF EXISTS payout CASCADE;

CREATE TABLE customer (
    id SERIAL PRIMARY KEY,
    firstname TEXT,
    lastname TEXT,
    phone TEXT,
    birth DATE,
    postcode TEXT
);

Quelques clients

Insertion de quelques personnes :

INSERT INTO customer
VALUES
(107,'Sarah','Conor','060-911-0911', '1965-10-10', '90016'),
(258,'Luke', 'Skywalker', NULL, '1951-09-25', '90120'),
(341,'Don', 'Draper','347-515-3423', '1926-06-01', '04520')
;

Table « payout »

Les ventes sont suivies dans cette simple table :

CREATE TABLE payout (
    id SERIAL PRIMARY KEY,
    fk_customer_id INT REFERENCES customer(id),
    order_date DATE,
    payment_date DATE,
    amount INT
);

Quelques données

Quelques commandes :

INSERT INTO payout
VALUES
(1,107,'2021-10-01','2021-10-01', '7'),
(2,258,'2021-10-02','2021-10-03', '20'),
(3,341,'2021-10-02','2021-10-02', '543'),
(4,258,'2021-10-05','2021-10-05', '12'),
(5,258,'2021-10-06','2021-10-06', '92')
;

Activer l’extension

CREATE EXTENSION IF NOT EXISTS anon CASCADE ;
SELECT anon.init() ;
SELECT setseed(0) ;

Déclarer les règles de masquage

SECURITY LABEL FOR anon ON COLUMN customer.lastname
IS 'MASKED WITH FUNCTION anon.fake_last_name()';

SECURITY LABEL FOR anon ON COLUMN customer.phone
IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$X-XXX-XX$$,2)';

Appliquer les règles de manière permanente

SELECT anon.anonymize_table('customer');

Exercices

E101 - Masquer les prénoms des clients

E102 - Masquer les 3 derniers chiffres du code postal

E103 - Compter le nombre de clients dans chaque département.

E104 - Ne garder que l’année dans les dates de naissance

E105 - Identifier un client particulier

Solutions

Masquage dynamique avec postgresql_anonymizer

Principe du masquage dynamique

  • Masquer les données personnelles à certains utilisateurs
    • mais pas tous

L’histoire

Paul a 2 employés :

  • Jack s’occupe du nouveau logiciel de ventes.
    • il a besoin d’accéder aux vraies données
    • pour le RGPD c’est un « processeur de données »
  • Pierre est un analyste qui exécute des requêtes statistiques
    • il ne doit pas avoir accès aux données personnelles

Comment ça marche

Objectifs de la section

Nous allons voir :

  • comment écrire des règles de masquage simple
  • les avantages et limitations du masquage dynamique
  • le concept de « recoupement » d’une personne (linkability)

Table « company »

DROP TABLE IF EXISTS supplier CASCADE;
DROP TABLE IF EXISTS company CASCADE;

CREATE TABLE company (
    id SERIAL PRIMARY KEY,
    name TEXT,
    vat_id TEXT UNIQUE
);

Quelques données

INSERT INTO company
VALUES
(952,'Shadrach', 'FR62684255667'),
(194,E'Johnny\'s Shoe Store','CHE670945644'),
(346,'Capitol Records','GB663829617823')
;

Table « supplier »

CREATE TABLE supplier (
    id SERIAL PRIMARY KEY,
    fk_company_id INT REFERENCES company(id),
    contact TEXT,
    phone TEXT,
    job_title TEXT
) ;

Quelques données

INSERT INTO supplier
VALUES
(299,194,'Johnny Ryall','597-500-569','CEO'),
(157,346,'George Clinton', '131-002-530','Sales manager')
;

Activer l’extension

CREATE EXTENSION IF NOT EXISTS anon CASCADE ;
SELECT anon.init() ;
SELECT setseed(0) ;

Activer le masquage dynamique

SELECT anon.start_dynamic_masking();

Rôle masqué

SECURITY LABEL FOR anon ON ROLE pierre IS 'MASKED' ;
GRANT ALL ON SCHEMA public TO jack ;
GRANT ALL ON ALL TABLES IN SCHEMA public TO jack ;
GRANT SELECT ON supplier TO pierre ;

Masquer le nom des fournisseurs

En tant que Paul, une règle de masquage se définit ainsi :

\c boutique paul
SECURITY LABEL FOR anon ON COLUMN supplier.contact
IS 'MASKED WITH VALUE $$CONFIDENTIAL$$';

Exercices

E201 - Deviner qui est le PDG de « Johnny’s Shoe Store »

E202 - Anonymiser les sociétés

E203 - Pseudonymiser le nom des sociétés

Solutions

Sauvegardes anonymes avec postgresql_anonymizer

L’histoire

  • Paul a un site web qui dispose d’une section commentaires où les utilisateurs peuvent partager leurs points de vue.

  • Paul a engagé un prestataire pour développer le nouveau design de son site web.

  • Le prestataire lui demande un export de la base de données.

  • Paul veut « nettoyer » le dump et y retirer toute information personnelle qui pourrait figurer dans la section commentaire.

Comment ça marche ?

Objectifs

  • Extraire les données anonymisées de la base de données
  • Écrire une fonction de masquage personnalisée pour gérer une colonne de type JSON

Table « website_comment »

\c boutique paul
DROP TABLE IF EXISTS website_comment CASCADE ;

CREATE TABLE website_comment (
  id SERIAL PRIMARY KEY,
  message jsonb
) ;

Quelques données

curl -kLs https://dali.bo/website_comment -o /tmp/website_comment.tsv
head /tmp/website_comment.tsv
    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 "}

Activer l’extension

\c boutique paul

CREATE EXTENSION IF NOT EXISTS anon CASCADE;

SELECT anon.init();

SELECT setseed(0);

Masquer une colonne de type JSON

Généralement, les données non structurées sont difficiles à masquer…

SELECT message - ARRAY['content']
FROM website_comment
WHERE id=1 ;

Fonctions de masquage personnalisées

\c boutique paul

CREATE SCHEMA IF NOT EXISTS my_masks;

SECURITY LABEL FOR anon ON SCHEMA my_masks IS 'TRUSTED';
CREATE OR REPLACE FUNCTION my_masks.remove_content(j jsonb)
RETURNS jsonb
AS $func$
  SELECT j - ARRAY['content']
$func$
LANGUAGE sql ;
  • Super-utilisateurs seulement !

Utilisation de la fonction de masquage personnalisée

SECURITY LABEL FOR anon ON COLUMN website_comment.message
IS 'MASKED WITH FUNCTION my_masks.remove_content(message)';

Sauvegarde anonymisée

  • Export
pg_dump_anon.sh -U paul -d boutique --table=website_comment > /tmp/dump.sql
  • Limitations : risque d’inconsistence, format plain

Exercices

E301 - Exporter les données anonymisées dans une nouvelle base de données

E302 - Pseudonymiser les métadonnées du commentaire

Solutions

Généralisation avec postgresql_anonymizer

Principe

  • Flouter les données
  • Par ex : 25 juillet 1989 années 1980

L’histoire

  • Paul a embauché des dizaines de salariés au fil du temps.

  • Il conserve une trace sur la couleur de leurs cheveux, leurs tailles, et leurs conditions médicales.

  • Paul souhaite extraire des statistiques depuis ces détails.

  • Il fournit des vues généralisées à Pierre.

Comment ça marche ?

Objectifs

Nous allons voir :

  • la différence entre le masquage et la généralisation
  • le concept de « k-anonymat »

Table « employee »

DROP TABLE IF EXISTS employee CASCADE;
CREATE TABLE employee (
  id INT PRIMARY KEY,
  full_name  TEXT,
  first_day DATE, last_day DATE,
  height INT,
  hair TEXT, eyes TEXT, size TEXT,
  asthma BOOLEAN,
  CHECK(hair = ANY(ARRAY['bald','blond','dark','red'])),
  CHECK(eyes = ANY(ARRAY['blue','green','brown'])) ,
  CHECK(size = ANY(ARRAY['S','M','L','XL','XXL']))
);
  • Légal ?

Quelques données

curl -Ls https://dali.bo/employee -o /tmp/employee.tsv
\c boutique paul
\COPY employee FROM '/tmp/employee.tsv'

Suppression de données

Pierre peut trouver un lien entre asthme et yeux verts :

\c boutique paul
DROP MATERIALIZED VIEW IF EXISTS v_asthma_eyes ;

CREATE MATERIALIZED VIEW v_asthma_eyes AS
SELECT eyes, asthma
FROM employee ;

Calculer le k-anonymat

  • Les colonnes asthma et eyes sont considérés comme des identifiants indirects.
\c boutique paul
SECURITY LABEL FOR anon ON COLUMN v_asthma_eyes.eyes
IS 'INDIRECT IDENTIFIER';

SECURITY LABEL FOR anon ON COLUMN v_asthma_eyes.asthma
IS 'INDIRECT IDENTIFIER';
SELECT anon.k_anonymity('v_asthma_eyes');

Fonctions d’intervalle et de généralisation

\c boutique paul
DROP MATERIALIZED VIEW IF EXISTS v_staff_per_month ;

CREATE MATERIALIZED VIEW v_staff_per_month AS
SELECT
    anon.generalize_daterange(first_day,'month') AS first_day,
    anon.generalize_daterange(last_day, 'month') AS last_day
FROM employee ;

GRANT SELECT ON v_staff_per_month TO pierre ;

Déclarer les identifiants indirects

Calculer le facteur de k-anonymat de cette vue :

\c boutique paul
SECURITY LABEL FOR anon ON COLUMN v_staff_per_month.first_day
IS 'INDIRECT IDENTIFIER';
SECURITY LABEL FOR anon ON COLUMN v_staff_per_month.last_day
IS 'INDIRECT IDENTIFIER';
SELECT anon.k_anonymity('v_staff_per_month');

Exercices

E401 - Simplifier la vue v_staff_per_month pour en réduire la granularité.

E402 - Progression du personnel au fil des années

E403 - Atteindre le facteur 2-anonymity sur la vue v_staff_per_year

Solutions

Conclusion sur postgresql_anonymizer

Beaucoup de stratégies de masquage

Beaucoup de fonctions de masquage

  • Destruction partielle ou totale
  • Ajout de bruit
  • Randomisation
  • Falsification et falsification avancée
  • Pseudonymisation
  • Hachage générique
  • Masquage personnalisé

RTFM -> Fonctions de masquage

Avantages

  • Règles de masquage en SQL
  • Règles de masquage stockées dans le schéma de la base
  • Pas besoin d’un ETL
  • Fonctionne avec toutes les versions actuelles de PostgreSQL
  • Multiples stratégies, multiples fonctions.

Inconvénients

  • Ne fonctionne pas avec d’autres systèmes de gestion de bases de données (comme le nom l’indique)
  • Peu de retour d’expérience sur de gros volumes (> 10 To)

Pour aller plus loin

D’autres projets qui pourraient vous plaire :

  • pg_sample : Extraire un petit jeu de données d’une base de données volumineuse
  • PostgreSQL Faker : Une extension de falsification avancée basée sur la bibliothèque python Faker.

Contribuez !

C’est un projet libre !

https://labs.dalibo.com/postgresql_anonymizer

Merci de vos retours sur la manière dont vous l’utilisez, comment il répond ou non à vos attentes, etc.

Questions

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

Pooling

PostgreSQL

Au menu

  • Concepts
  • Pool de connexion avec PgBouncer

Objectifs

  • Savoir ce qu’est un pool de connexion ?
  • Avantage, inconvénients & limites
  • Savoir mettre en place un pooler de connexion avec PgBouncer

Pool de connexion

  • Qu’est-ce qu’un pool de connexion ?
  • Présentation
  • Avantages et inconvénients
  • Mise en œuvre avec PgBouncer

Serveur de pool de connexions

Schéma de principe d’un pool de connexion

Serveur de pool de connexions

  • S’intercale entre le SGBD et les clients
  • Maintient des connexions ouvertes avec le SGBD
  • Distribue aux clients ses connexions au SGBD
  • Attribue une connexion existante au SGBD dans ces conditions
    • même rôle
    • même base de données
  • Différents poolers :
    • intégrés aux applicatifs
    • service séparé (où ?)

Intérêts du pool de connexions

  • Évite le coût de connexion
    • …et de déconnexion
  • Optimise l’utilisation des ressources du SGBD
  • Contrôle les connexions, peut les rediriger
  • Évite des déconnexions
    • redémarrage (mise à jour, bascule)
    • saturations temporaires des connexions sur l’instance

Inconvénients du pool de connexions

  • Transparence suivant le mode :
    • par sessions
    • par transactions
    • par requêtes
  • Performances, si mal configuré (latence)
  • Point délicat : l’authentification !
  • Complexité
  • SPOF potentiel
  • Impact sur les fonctionnalités, selon le mode

Pooling de sessions

Une connexion par utilisateur, pendant toute la durée de la session.

Schéma de principe d’un pool de connexion par session

Intérêts du pooling de sessions

  • Avantages :
    • limite le temps d’établissement des connexions
    • mise en attente si trop de sessions
    • simple
    • transparent pour les applications
  • Inconvénients :
    • périodes de non-activité des sessions conservées
    • nombre de sessions actives au pooler égal au nombre de connexions actives au SGBD

Pooling de transactions

Multiplexe les transactions des utilisateurs sur une ou plusieurs connexions.

Schéma de principe d’un pool de connexions par transaction

Avantages & inconvénients du pooling de transactions

  • Avantages
    • mêmes avantages que le pooling de sessions
    • meilleure utilisation du temps de travail des connexions
      • les connexions sont utilisées par une ou plusieurs sessions
    • plus de sessions possibles côté pooler pour moins de connexions au SGBD
  • Inconvénients
    • prise en charge partielle des instructions préparées
    • période de non-activité des sessions toujours possible

Pooling de requêtes

  • Un pool de connexions en mode requêtes multiplexe toutes les requêtes sur une ou plusieurs connexions
Schéma de principe d’un pool de connexion en mode requête

Avantages & inconvénients du pooling de requêtes

  • Avantages
    • les mêmes que pour le pooling de sessions et de transactions.
    • utilisation optimale du temps de travail des connexions
    • encore plus de sessions possibles côté pooler pour moins de connexions au SGBD
  • Inconvénients
    • les mêmes que pour le pooling de transactions
    • interdiction des transactions !

Pooling avec PgBouncer

  • Deux projets existent : PgBouncer et PgPool-II
  • Les deux sont sous licence BSD
  • PgBouncer
    • le plus évolué et éprouvé pour le pooling

PgBouncer : Fonctionnalités

  • Techniquement : un démon
  • Disponible sous Unix & Windows
  • Modes sessions / transactions / requêtes
  • Prise en charge partielle des requêtes préparées
  • Redirection vers des serveurs et/ou bases différents
  • Mise en attente si plus de connexions disponibles
  • Mise en pause des connexions
  • Paramétrage avancé des sessions clientes et des connexions aux bases
  • Mise à jour sans couper les sessions existantes
  • Supervision depuis une base virtuelle de maintenance
  • Pas de répartition de charge

PgBouncer : Installation

  • Par les paquets fournis par le PGDG :
    • yum|dnf install pgbouncer
    • apt install pgbouncer
  • Installation par les sources

PgBouncer : Fichier de configuration

  • Format ini
  • Un paramètre par ligne
  • Aucune unité dans les valeurs
  • Tous les temps sont exprimés en seconde
  • Sections : [databases], [users], [pgbouncer]

PgBouncer : Connexions

  • TCP/IP
    • listen_addr : adresses
    • listen_port (6432)
  • Socket Unix (unix_socket_dir, unix_socket_mode, unix_socket_group)
  • Chiffrement TLS

PgBouncer : Définition des accès aux bases

  • Section [databases]
  • Une ligne par base sous la forme libpq :
data1 = host=localhost port=5433 dbname=data1 pool_size=50
  • Paramètres de connexion :
    • host, port, dbname ; user, password
    • pool_size, pool_mode, connect_query
    • client_encoding, datestyle, timezone
  • Base par défaut :
    + = host=ip1 port=5432 dbname=data0
  • auth_hba_file : équivalent à pg_hba.conf

PgBouncer : Authentification par fichier de mots de passe

  • Liste des utilisateurs contenue dans userlist.txt
  • Contenu de ce fichier
    • "utilisateur" "mot de passe"
  • Paramètres dans le fichier de configuration
    • auth_type : type d’authentification (trust, md5,scram-sha-256…)
    • auth_file : emplacement de la liste des utilisateurs et mots de passe
    • admin_users : liste des administrateurs
    • stats_users : liste des utilisateurs de supervision

PgBouncer : Authentification par délégation

  • Créer un rôle dédié
  • Copier son hash de mot de passe dans userlist.txt
  • Déclaration dans le pool avec auth_user  :
prod = host=p1 port=5432 dbname=erp auth_user=frontend
  • auth_query : requête pour vérifier le mot de passe via ce rôle
  • => Plus la peine de déclarer les autres rôles

PgBouncer : Nombre de connexions

  • Côté client :
    • max_client_conn (100)
    • attention à ulimit !
    • max_db_connections
  • Par utilisateur/base :
    • default_pool_size (20)
    • min_pool_size (0)
    • reserve_pool_size (0)

PgBouncer : Types de connexions

  • Mode de multiplexage
    • pool_mode (session)
  • À la connexion
    • ignore_startup_parameter = options
    • attention à PGOPTIONS !
  • À la déconnexion
    • server_reset_query
    • défaut : DISCARD ALL

PgBouncer : Instructions préparées

  • En mode Transaction
    • prise en charge partielle des instructions préparées
    • depuis la 1.21 : max_prepared_statements
    • nécessite un connecteur PG compatible

PgBouncer : Durée de vie

  • D’une tentative de connexion
    • client_login_timeout
    • server_connect_timeout
  • D’une connexion
    • server_lifetime
    • server_idle_timeout
    • client_idle_timeout
  • Pour recommencer une demande de connexion
    • server_login_retry
  • D’une requête
    • query_timeout = 0

PgBouncer : Traces

  • Fichier
    • logfile
  • Évènements tracés
    • log_connections
    • log_disconnections
    • log_pooler_errors
  • Statistiques
    • log_stats (tous les stats_period s)

PgBouncer : Administration

  • Pseudo-base pgbouncer :
sudo -iu postgres psql -h /var/run/postgresql -p 6432 -d pgbouncer
  • Administration
    • RELOAD, PAUSE, SUSPEND, RESUME, SHUTDOWN
  • Supervision
    • SHOW CONFIG|DATABASES|POOLS|CLIENTS|…
    • …|SERVERS|STATS|FDS|SOCKETS|…
    • …|ACTIVE_SOCKETS|LISTS|MEM

Conclusion

  • Un outil pratique :
    • pour parer à certaines limites de PostgreSQL
    • pour faciliter l’administration
  • Limitations généralement tolérables
  • Ne jamais installer un pooler sans être certain de son apport :
    • SPOF
    • complexité

Questions

SELECT * FROM questions ;

Travaux pratiques

Travaux pratiques (solutions)