Types avancés

Module S9

Dalibo SCOP

24.09

29 août 2024

Sur ce document

Formation Module S9
Titre Types avancés
Révision 24.09
PDF https://dali.bo/s9_pdf
EPUB https://dali.bo/s9_epub
HTML https://dali.bo/s9_html
Slides https://dali.bo/s9_slides
TP https://dali.bo/s9_tp
TP (solutions) https://dali.bo/s9_solutions

Licence Creative Commons CC-BY-NC-SA

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

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

Marques déposées

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

Versions de PostgreSQL couvertes

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

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)