UUID 
Tout ce qui suit doit se dérouler dans la même base, par
exemple :
CREATE  DATABASE  capteurs ;Ce TP est prévu pour un shared_buffers de 128 Mo (celui
par défaut). Si le vôtre est plus gros, le TP devra peut-être durer plus
longtemps :
Utilisez au moins une fenêtre pour les ordres shell et une pour les
ordres SQL.
Créer avec le script suivants les deux versions d’un petit modèle
avec des capteurs, et les données horodatées qu’ils renvoient ; ainsi
que les deux procédures pour remplir ces tables ligne à ligne :
 
c  capteurs-- Modèle : une table 'capteurs' et ses 'donnees' horodatées -- liées par une contrainte -- Deux versions : avec ID et une séquence, et avec UUID DROP  TABLE  IF  EXISTS  donnees1, donnees2, capteurs1, capteurs2 ;-- Avec identifiants bigint CREATE  TABLE  capteurs1 (id_capteur  bigint   PRIMARY  KEY ,char  (50 ) UNIQUE ,char  (50 ) default  ' ' CREATE  TABLE  donnees1 (id_donnee   bigserial  PRIMARY  KEY ,int  NOT  NULL  REFERENCES  capteurs1,timestamp  with  time  zone ,int ,int ,float CREATE  INDEX  ON  donnees1 (horodatage) ;-- Version avec les UUID CREATE  TABLE  capteurs2 (id_capteur  uuid  PRIMARY  KEY ,char  (50 ) UNIQUE ,char  (50 ) default  ' ' CREATE  TABLE  donnees2 (id_donnee   uuid  PRIMARY  KEY ,uuid  NOT  NULL  REFERENCES  capteurs2,timestamp  with  time  zone ,int ,int ,float CREATE  INDEX  ON  donnees2 (horodatage) ;-- 1000 capteurs identiques INSERT  INTO  capteurs1 (id_capteur, nom)SELECT   i,'M- ' || md5 (i:: text )FROM  generate_series  (1 ,1000 ) iORDER  BY  random () ;INSERT  INTO  capteurs2 (id_capteur, nom)SELECT  gen_random_uuid(), nom FROM  capteurs1 ;-- 2 procédures d'insertion de données identiques sur quelques capteurs au hasard -- insertion dans donnees1 avec une séquence CREATE  OR  REPLACE PROCEDURE  insere_donnees_1 ()AS  $$     SET synchronous_commit TO off ; -- accélère     INSERT INTO donnees1 (id_donnee, id_capteur, horodatage, valeur1, valeur2, valeur3)     SELECT  nextval('donnees1_id_donnee_seq'::regclass), -- clé primaire des données             m.id_capteur,                                -- clé étrangère             now(), (random()*1000)::int,(random()*1000)::int,random()     FROM capteurs1 m TABLESAMPLE BERNOULLI (1) ; -- 1% des lignes $$  LANGUAGE  sql ;-- insertion dans donnees2 avec un UUID v7 CREATE  OR  REPLACE PROCEDURE  insere_donnees_2 ()AS  $$     SET synchronous_commit TO off ; -- accélère     INSERT INTO donnees2 (id_donnee, id_capteur, horodatage, valeur1, valeur2, valeur3)     SELECT  gen_random_uuid(),   -- clé primaire des données, UUID v4             m.id_capteur,        -- clé étrangère             now(), (random()*1000)::int,(random()*1000)::int,random()     FROM capteurs2 m TABLESAMPLE BERNOULLI (1) ; -- 1% des lignes $$  LANGUAGE  sql ;Vous devez obtenir ces tables et une séquence :
et ces index :
Créer deux fichiers SQL contenants juste les appels de fonctions, qui
serviront pour pgbench :
 
echo  "CALL insere_donnees_1 ()"  >  /tmp/insere1.sqlecho  "CALL insere_donnees_2 ()"  >  /tmp/insere2.sql
Dans la même base que les table ci-dessus, installer l’extension pg_buffercache  qui va nous permettre
de voir ce qu’il y a dans le cache de PostgreSQL :
 
CREATE  EXTENSION  IF  NOT  EXISTS  pg_buffercache ;La vue du même nom contient une ligne par bloc. La requête suivante
permet de voir lesquelles de nos tables utilisent le cache :
SELECT  CASE  WHEN  datname =  current_database ()AND  relname NOT  LIKE  'pg% ' THEN  relname ELSE  '*AUTRES* '  END  AS  objet,count (* ),pg_size_pretty (count (bufferid)* 8192 ) as  Taille_MoFROM  pg_buffercache bLEFT  OUTER  JOIN  pg_class c  ON  c .relfilenode =  b.relfilenodeLEFT  OUTER  JOIN  pg_database d ON  (d.oid =  b.reldatabase)GROUP  BY  objetORDER  BY  count (bufferid) DESC  ;Cette version semi-graphique est peut-être plus parlante :
SELECT  CASE  WHEN  datname =  current_database ()AND  relname NOT  LIKE  'pg% ' THEN  relname ELSE  '*AUTRES* '  END  AS  objet,pg_size_pretty (count (bufferid)* 8192 ) as  Taille_Mo,lpad (' ' ,(count (bufferid)/ 200 ):: int , '# ' ) AS  TailleFROM  pg_buffercache bLEFT  OUTER  JOIN  pg_class c  ON  c .relfilenode =  b.relfilenodeLEFT  OUTER  JOIN  pg_database d ON  (d.oid =  b.reldatabase)GROUP  BY  objetORDER  BY  objet DESC  ;
Dans une fenêtre, lancer l’une de ces requêtes (dans la bonne
base !), puis la répéter toutes les secondes ainsi :
 
Dans une autre fenêtre, lancer pgbench avec deux
clients, et le script pour remplir la table donnees1 :
 
# Sous Rocky Linux/Almalinux… /usr/pgsql-16/bin/pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ # Sous Debian/Ubuntu pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ Le nombre de transactions dépend fortement de la machine, mais peut
atteindre plusieurs milliers à la seconde.
Les tables peuvent rapidement atteindre plusieurs gigaoctets.
N’hésitez pas à les vider ensemble de temps à autre.
TRUNCATE  donnees1, donnees2 ; 
Quelle est la répartition des données dans le cache ?
 
Après peu de temps, la répartition doit ressembler à peu près à ceci
:
Et ce, même si la table et ses index ne tiennent plus intégralement
dans le cache.
La table donnees1 représente la majorité du cache.
Interrompre pgbench et le relancer pour remplir
donnees2 :
 
# Sous Rocky Linux/Almalinux… /usr/pgsql-16/bin/pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ # Sous Debian/Ubuntu pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ Noter que le débit en transaction est du même ordre de grandeur : les
UUID ne sont pas spécialement lourds à générer.
Que devient la répartition des données dans le cache ?
 
donnees1 et ses index est chassé du cache par les
nouvelles données, ce qui est logique.
Surtout, on constate que la clé primaire de donnnes2
finit par remplir presque tout le cache. Dans ce petit cache, il n’y a
plus de place même pour les données de donnees2 !
Interrompre pgbench, purger les tables et lancer les
deux scripts d’alimentation en même temps.
 
TRUNCATE  donnees1, donnees2 ;# Sous Rocky Linux/Almalinux… /usr/pgsql-16/bin/pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ -f  /tmp/insere2.sql# Sous Debian/Ubuntu pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ -f  /tmp/insere2.sqlOn constate le même phénomène de monopolisation du cache par
donnees2, bien que les deux tables de données aient le même
nombre de lignes :
Avez-vous remarqué une différence de vitesse entre les deux
traitements ?
 
Ce ne peut être rigoureusement établi ici. Les volumétries sont trop
faibles par rapport à la taille des mémoires et il faut tester sur la
durée. Le nombre de clients doit être étudié pour utiliser au mieux les
capacités de la machine sans monter jusqu’à ce que la contention
devienne un problème. Les checkpoints font également varier les
débits.
Cependant, si vous laissez le test tourner très longtemps avec des
tailles de tables de plusieurs Go, les effets de cache seront très
différents :
dans donnees1, le débit en insertion devrait rester
correct, car seuls les derniers blocs en cache sont utiles ; 
dans donnees2, le débit en insertion doit
progressivement baisser : chaque insertion a besoin d’un bloc de l’index
de clé primaire différent, qui a de moins en moins de chance de se
trouver dans le cache de PostgreSQL, puis dans le cache de Linux. 
 
L’impact sur les I/O augmente donc, et pas seulement à cause de la
volumétrie supérieure des tables avec UUID. À titre d’exemple, sur une
petite base de formation avec 3 Go de RAM :
# avec ID numériques, pendant des insertions dans donnees1 uniquement, # qui atteint 1,5 Go # débit des requêtes : environ 3000 tps $  iostat -h  1avg-cpu:   %user   %nice %system %iowait  %steal   %idle88,6%     0,0%   10,7%    0,0%    0,0%    0,7%tps     kB_read/s    kB_wrtn/s    kB_read    kB_wrtn Device86,00          0,0k        17,0M       0,0k      17,0M vda0,00          0,0k         0,0k       0,0k       0,0k scd0# avec UUID v4, pendant des insertions dans donnees2 uniquement, # qui atteint 1,5 Go # débit des requêtes : environ 700 tps $  iostat -h  1avg-cpu:   %user   %nice %system %iowait  %steal   %idle41,2%     0,0%   17,3%   25,9%    0,7%   15,0%tps     kB_read/s    kB_wrtn/s    kB_read    kB_wrtn Device2379,00          0,0k        63,0M       0,0k      63,0M vda0,00          0,0k         0,0k       0,0k       0,0k scd0
Comparer les tailles des tables et index avant et après un
VACUUM FULL. Où était la fragmentation ?
 
VACUUM FULL reconstruit complètement les tables et aussi
les index.
Tables avant le VACUUM FULL :
Après :
Les tailles des tables donnees1 et donnees2
ne bougent pas. C’est normal, il n’y a eu que des insertions à chaque
fois en fin de table, et ni modification ni suppression de données.
Index avant le VACUUM FULL :
Index après :
Les index d’horodatage gardent la même taille qu’avant (la différence
entre eux est dû à des nombres de lignes différents dans cet exemple).
L’index sur la clé primaire de donnees1
(bigint) n’était pas fragmenté. Par contre,
donnees2_pkey se réduit de 29% ! Les index UUID (v4) ont
effectivement tendance à se fragmenter.
Les UUID générés avec gen_random_uuid sont de version 4.
Créer la fonction suivante pour générer des UUID version 7, l’utiliser
dans la fonction d’alimentation de donnees2, et relancer
les deux alimentations :
 
-- Source : https://postgresql.verite.pro/blog/2024/07/15/uuid-v7-pure-sql.html -- Daniel Vérité d'après Kyle Hubert CREATE OR REPLACE FUNCTION  uuidv7() RETURNS  uuid AS  $$ -- Replace the first 48 bits of a uuidv4 with the current -- number of milliseconds since 1970-01-01 UTC -- and set the "ver" field to 7 by setting additional bits select  encode (set_bit (set_bit (overlay (uuid_send(gen_random_uuid()) placing substring (int8send((extract (epoch from  clock_timestamp())* 1000 ):: bigint )from  3 )from  1  for  6 ),52 , 1 ),53 , 1 ), 'hex ' ):: uuid ;$$  LANGUAGE  sql  volatile  ;-- insertion dans donnees2 avec un UUID v7 CREATE  OR  REPLACE PROCEDURE  insere_donnees_2 ()AS  $$     SET synchronous_commit TO off ; -- accélère     INSERT INTO donnees2 (id_donnee, id_capteur, horodatage, valeur1, valeur2, valeur3)     SELECT  uuidv7(),   -- clé primaire des données, UUID v7             m.id_capteur,        -- clé étrangère             now(), (random()*1000)::int,(random()*1000)::int,random()     FROM capteurs2 m TABLESAMPLE BERNOULLI (1) ; -- 1% des capteurs $$  LANGUAGE  sql ;# Sous Rocky Linux/Almalinux… /usr/pgsql-16/bin/pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ -f  /tmp/insere2.sql# Sous Debian/Ubuntu pgbench  capteurs -c2  -j1  -n  -T1800  -P1  \ -f  /tmp/insere2.sqlAprès quelques dizaines de secondes :
les deux tables doivent être présentes dans le cache de manière
similaire ; 
les index primaires doivent être présents de manière
anecdotique ; 
donnees2 occupe une taille un peu supérieure à cause de
la taille double des UUID par rapport aux bigint de
donnees1 : 
Relancez pgbench pour charger donnees2,
alternez entre les deux versions de la fonction
insere_donnees_2.
 
/usr/pgsql-16/bin/pgbench  capteurs -c2  -j1  -n  -T1800  -P1  -f  /tmp/insere2.sql Le débit en transactions varie ici d’un facteur 2. Noter que la durée
des transactions est aussi beaucoup plus stable
(stddev).
jsonb : lecture de champs 
La base personnes_et_dossiers  pèse en version
complète 613 Mo, pour 2 Go sur disque au final. Elle peut être installée
comme suit :
# Dump complet curl  -kL  https://dali.bo/tp_personnes -o  /tmp/personnes.dump# Taille 40% # curl -kL https://dali.bo/tp_personnes_200k -o /tmp/personnes.dump # Taille 16% # curl -kL https://dali.bo/tp_personnes_fr -o /tmp/personnes.dump createdb  --echo  personnes# L'erreur sur un schéma 'public' existant est normale pg_restore  -v  -d  personnes /tmp/personnes.dumprm  --  /tmp/personnes.dumpLa base personnes contient alors deux schémas
json et eav avec les mêmes données sous deux
formes différentes.
La table json.personnes contient une ligne par personne,
un identifiant et un champ JSON avec de nombreux attributs. Elle n’est
pas encore indexée :
Chercher la ville et le numéro de téléphone (sous-attribut
ville de l’attribut adresse du champ JSON
personne) de Gaston Lagaffe, grâce aux attributs
prenom et nom. Effectuer de préférence la
recherche en cherchant un JSON avec @> (« contient »)
(Ne pas chercher encore à utiliser JSONPath).
 
La recherche peut s’effectuer en convertissant tous les attributs en
texte :
SELECT  personne-> 'adresse ' ->> 'ville ' FROM    json .personnes p WHERE   personne->> 'nom '  =  'Lagaffe ' AND   personne->> 'prenom '  =  'Gaston '  ;On obtient « Bruxelles ».
Avec la syntaxe en version 14 :
SELECT  personne['adresse ' ]['ville ' ]->> 0  AS  villeFROM    json .personnes p WHERE   personne['nom ' ] =  '"Lagaffe" ' :: jsonb AND   personne['prenom ' ] =  '"Gaston" ' :: jsonb  ;Il est plus propre de rechercher grâce à une de ces syntaxes,
notamment parce qu’elles seront indexables plus tard :
SELECT  personne-> 'adresse ' ->> 'ville ' FROM  json .personnes p WHERE  personne @>  '{"nom": "Lagaffe", "prenom": "Gaston"} ' :: jsonb  ;ou :
SELECT  personne-> 'adresse ' ->> 'ville ' FROM  json .personnes p WHERE  personne @>  jsonb_build_object ('nom ' , 'Lagaffe ' , 'prenom ' , 'Gaston ' ) ;
Créer une requête qui renvoie les attributs nom,
prenom, date_naissance (comme type date) de
toutes les personnes avec le nom « Lagaffe ». Utiliser la fonction
jsonb_to_record() et LATERAL. Rajouter
ville et pays ensuite de la même manière.
 
jsonb_to_record exige que l’on fournisse le nom de
l’attribut et son type :
SELECT  r.* FROM  json .personnes,LATERAL  jsonb_to_record (personne) AS  r (nom text , prenom text , date_naissance date )WHERE  personne @>  '{"nom": "Lagaffe"} ' :: jsonb ;Avec la ville, qui est dans un sous-attribut, il faut rajouter une
clause LATERAL :
SELECT  r1.* , r2.* FROM  json .personnes,LATERAL  jsonb_to_record (personne)AS  r1 (nom text , prenom text , date_naissance date ),LATERAL  jsonb_to_record (personne-> 'adresse ' )AS  r2 (ville text , pays text )WHERE  personne @>  '{"nom": "Lagaffe"} ' :: jsonb ;
En supprimant le filtre, comparer le temps d’exécution de la requête
précédente avec cette requête plus simple qui récupère les champs plus
manuellement :
SELECT  personne->> 'nom' ,->> 'prenom' ,->> 'date_naissance' )::date ,>> '{adresse,ville}' ,>> '{adresse,pays}'  FROM  json.personnes; 
Cette dernière requête est nettement plus lente que l’utilisation de
jsonb_to_record, même si les I/O sont plus réduites :
EXPLAIN  (COSTS OFF ,ANALYZE ,BUFFERS)SELECT  personne->> 'nom ' ,->> 'prenom ' ,->> 'date ' ):: date ,#>> '{adresse,ville} ' ,#>> '{adresse,pays} '  FROM  json .personnes;EXPLAIN  (ANALYZE ,BUFFERS)SELECT  r1.* , r2.*  FROM  json .personnes,LATERAL  jsonb_to_record (personne)AS  r1 (nom text , prenom text , date_naissance date ),LATERAL  jsonb_to_record (personne-> 'adresse ' )AS  r2 (ville text , pays text ) ;La maintenabilité plaide pour la seconde version. Quant à la
lisibilité entre les deux versions de la requête, c’est un choix
personnel.
jsonb : index GIN
jsonb_path_ops 
Créer un index GIN ainsi :
CREATE  INDEX  personnes_gin ON  json.personnesUSING  gin(personne jsonb_path_ops);Quelle taille fait-il ?
 
L’index peut être un peu long à construire (plusieurs dizaines de
secondes) et est assez gros :
Retenter les requêtes précédentes. Lesquelles utilisent l’index ?
 
Les requêtes utilisant les égalités (que ce soit sur du texte ou en
JSON) n’utilisent pas l’index :
EXPLAIN  (COSTS OFF , ANALYZE ,BUFFERS)SELECT  personne-> 'adresse ' ->> 'ville ' FROM    json .personnes p WHERE   personne->> 'nom '  =  'Lagaffe ' AND   personne->> 'prenom '  =  'Gaston '  ;Par contre, la syntaxe @> (« contient ») utilise
l’index, quelle que soit la manière dont on construit le JSON critère.
Le gain en temps et en I/O (et en CPU) grâce à l’index est assez
foudroyant. Et ceci, quelle que soit la manière dont on récupère les
champs, puisqu’il n’y a plus qu’une poignée de lignes à analyser :
EXPLAIN  (COSTS OFF , ANALYZE ,BUFFERS)SELECT  personne-> 'adresse ' ->> 'ville ' FROM    json .personnes p WHERE   personne @>  '{"nom": "Lagaffe", "prenom": "Gaston"} ' :: jsonb  ;EXPLAIN  (ANALYZE , VERBOSE ) SELECT  r1.* , r2.* FROM  json .personnes,LATERAL  jsonb_to_record (personne)AS  r1 (nom text , prenom text , date_naissance date ),LATERAL  jsonb_to_record (personne-> 'adresse ' )AS  r2 (ville text , pays text )WHERE  personne @>  '{"nom": "Lagaffe"} ' :: jsonb ;Les requêtes sans filtre n’utilisent pas l’index, bien sûr.
jsonb et tableaux 
Récupérer les numéros de téléphone de Léon Prunelle.
 
--(Syntaxe pour PostgreSQL 14 minimum) SELECT  personne['adresse ' ]['telephones ' ],'adresse ' ]-> 'telephones ' ,'adresse ' ]['telephones ' ]#> '{} ' ,'adresse ' ]['telephones ' ]-> 0 ,'adresse ' ]->> 'telephones ' ,'adresse ' ]['telephones ' ]#>> '{} ' ,'adresse ' ]['telephones ' ]->> 0 FROM    json .personnes p WHERE   personne @>  '{"nom": "Prunelle", "prenom": "Léon"} ' :: jsonb  ;Le sous-attribut telephones est un tableau. La syntaxe
->0 ne renvoie que le premier élément :
Les 4 premières lignes renvoient un jsonb, les trois
dernières sa conversion en texte :
Afficher les noms et prénoms de Prunelles, et un tableau de champs
texte contenant ses numéros de téléphone (utiliser
jsonb_array_elements_text).
 
Il vaut mieux ne pas « bricoler » avec des conversions manuelles du
JSON en texte puis en tableau. La fonction dédiée est
jsonb_array_elements_text.
SELECT  personne->> 'prenom '  AS   prenom, personne->> 'nom '  AS  nom,-> 'adresse ' -> 'telephones ' ) AS  telFROM    json .personnes p WHERE   personne @>  '{"nom": "Prunelle", "prenom": "Léon"} ' :: jsonb  ;Cependant on multiplie les lignes par le nombre de numéros de
téléphone, et il faut réagréger :
SELECT  personne->> 'prenom '  AS   prenom, personne->> 'nom '  AS  nom,SELECT  array_agg  (t ) FROM  -> 'adresse ' -> 'telephones ' ) tels(t )AS  telsFROM    json .personnes p WHERE   personne @>  '{"nom": "Prunelle", "prenom": "Léon"} ' :: jsonb  ;La version suivante fonctionnerait aussi dans ce cas précis
(cependant elle sera moins performante s’il y a beaucoup de lignes, car
PostgreSQL voudra faire un agrégat global au lieu d’un simple parcours ;
il faudra aussi vérifier que la clé d’agrégation tient compte
d’homonymes).
SELECT  personne->> 'prenom '  AS   prenom, personne->> 'nom '  AS  nom,array_agg  (t ) AS  telsFROM    json .personnes p LEFT  OUTER  JOIN  LATERAL  jsonb_array_elements_text (-> 'adresse ' -> 'telephones ' ) AS  tel(t ) ON  (true )WHERE   personne @>  '{"nom": "Prunelle", "prenom": "Léon"} ' :: jsonb GROUP  BY  1 ,2  ;(Noter que la fonction sœur jsonb_array_elements()
renverrait, elle, des JSON.)
Accès JSONPath 
Comparer le résultat et les performances de ces deux requêtes, qui
récupèrent aussi les numéros de téléphone de Prunelle :
 
SELECT  jsonb_path_query (personne,'$.adresse.telephones[*] ? ($.nom == "Prunelle" && $.prenom == "Léon") '  ) #>> '{} '  AS  telFROM  json .personnes ;SELECT  jsonb_path_query (personne, '$.adresse.telephones[*] ' )#>> '{} ' AS  telFROM  json .personnesWHERE   personne @@  '$.nom == "Prunelle" && $.prenom == "Léon" '  ;Le résultat est le même dans les deux cas :
Par contre, le plan et les temps d’exécutions sont totalement
différents. La clause jsonb_path_query unique parcourt
complètement la table :
EXPLAIN  (COSTS OFF , ANALYZE , BUFFERS)SELECT  jsonb_path_query (personne,'$.adresse.telephones[*] ? ($.nom == "Prunelle" && $.prenom == "Léon") ' #>> '{} '  AS  telFROM    json .personnes ;Tandis que la séparation du filtrage et de l’affichage permet à
PostgreSQL de sélectionner les lignes, et donc de passer par un index
avant de procéder à l’affichage.
EXPLAIN  (COSTS OFF , ANALYZE , BUFFERS)SELECT  jsonb_path_query (personne, '$.adresse.telephones[*] ' )#>> '{} '  AS  telFROM    json .personnesWHERE   personne @@  '$.nom == "Prunelle" && $.prenom == "Léon" '  ;(À la place de @@, la syntaxe classique
@> avec un JSON comme critère, est aussi performante
dans ce cas simple.) 
Chercher qui possède le numéro de téléphone 0650041821
avec la syntaxe JSONPath.
 
Ces deux syntaxes sont équivalentes :
SELECT  personne->> 'nom ' , personne->> 'prenom ' FROM    json .personnesWHERE   personne @@  '$.adresse.telephones[*] == "0650041821"  '  ;SELECT  personne->> 'nom ' , personne->> 'prenom ' FROM    json .personnesWHERE   personne @ ? '$.adresse.telephones[*] ? (@ == "0650041821") '  ;Dans les deux cas, EXPLAIN montre que l’index GIN est
bien utilisé.
Compter le nombre de personnes habitant à Paris ou Bruxelles
avec :
la syntaxe @> et un OR ; 
une syntaxe JSONPath @? et un « ou » logique
(||) ; 
une syntaxe JSONPath @? et une regex
@.ville like_regex "^(Paris|Bruxelles)$". 
 
 
Vous devez trouver 63 personnes avec la version complète de la
base.
Cet appel va utiliser l’index GIN :
EXPLAIN  SELECT  count (* ) FROM  json .personnesWHERE  personne @>  '{"adresse": {"ville": "Paris"}} ' :: jsonb OR     personne @>  '{"adresse": {"ville": "Bruxelles"}} ' :: jsonb  ;Cet appel aussi :
EXPLAIN  SELECT  count (* ) FROM  json .personnesWHERE  personne @ ? '$.adresse ? ( @.ville == "Paris" || @.ville == "Bruxelles")  '  ;Par contre, l’index GIN est inutilisable si l’on demande une
expression régulière (aussi simple soit-elle) :
EXPLAIN  SELECT  count (* ) FROM  json .personnesWHERE  personne @ ? '$.adresse ? ( @.ville like_regex "^(Paris|Bruxelles)$" )  '  ;Index fonctionnel,
colonne générée et JSON 
Le compte du nombre de personne par pays doit être optimisé au
maximum. Ajouter un index fonctionnel sur l’attribut pays.
Tester l’efficacité sur une recherche, et un décompte de toutes les
personnes par pays.
 
Suivant la syntaxe préférée, l’index peut être par exemple ceci :
CREATE  INDEX  personnes_pays_idx ON  json .personnesUSING  btree ( (personne-> 'adresse ' ->> 'pays ' ));VACUUM  ANALYZE  json .personnes ;L’index contient peu de valeurs et fait au plus 3 Mo (beaucoup plus
sur une version antérieure à PostgreSQL 13).
Cet index est utilisable pour une recherche à condition que
la syntaxe de l’expression soit rigoureusement identique , ce
qui limite les cas d’usage.
EXPLAIN  (ANALYZE ,BUFFERS) SELECT  count (* ) FROM  json .personnesWHERE  personne-> 'adresse ' ->> 'pays '  = 'Belgique '  ;Par contre, pour le décompte complet, il n’a aucun intérêt :
EXPLAIN  SELECT  personne-> 'adresse ' ->> 'pays ' , count (* )FROM  json .personnes GROUP  BY  1  ;En effet, un index fonctionnel ne permet pas un Index Only
Scan . Pourtant, il pourrait être très intéressant ici.
Ajouter un champ généré dans json.personne,
correspondant à l’attribut pays.
 
Attention, l’ordre va réécrire la table, ce qui peut être long (de
l’ordre de la minute, suivant le matériel) :
ALTER  TABLE  json .personnes ADD  COLUMN  pays text GENERATED  ALWAYS  AS  ( personne-> 'adresse ' ->> 'pays '  ) STORED ;VACUUM  ANALYZE  json .personnes ;
Comparer les temps d’exécution du décompte des pays par l’attribut,
et par cette colonne générée.
 
SELECT  personne-> 'adresse ' ->> 'pays ' , count (* ) FROM  json .personnes GROUP  BY  1  ;Par contre, la lecture directe du champ est nettement plus rapide
:
SELECT  pays, count (* ) FROM  json .personnes GROUP  BY  1  ;Le plan est pourtant le même : un Seq Scan , faute de clause
de filtrage et d’index, suivi d’un agrégat parallélisé n’utilisant que
quelques kilooctets de mémoire. 
Le champ généré a donc un premier intérêt en terme de rapidité de
lecture des champs, surtout avec des JSON importants comme ici.
Créer un index B-tree sur la colonne générée pays.
Consulter les statistiques dans pg_stats. Cet index est-il
utilisable pour des filtres et le décompte par pays ?
 
CREATE  INDEX  personnes_g_pays_btree ON  json .personnes (pays);VACUUM  ANALYZE  json .personnes ;Ces deux ordres ne durent qu’1 ou 2 secondes.
EXPLAIN  (ANALYZE , BUFFERS)SELECT  p .pays, count (* )FROM  json .personnes p GROUP  BY  1  ;Le gain en temps est appréciable. Mais l’intérêt principal réside ici
dans le nombre de blocs lus divisé par 100 ! Le nouvel index ne fait que
3 Mo.
(Optionnel) Créer des colonnes générées sur nom,
prenom, date_naissance, et ville
(en un seul ordre). Reprendre la requête plus haut qui les affiche tous
et comparer les performances.
 
Un champ va poser problème : la date de naissance. En effet, la date
est stockée au format texte, il faudra soi-même faire la conversion. De
plus, un simple opérateur ::date ne peut être utilisé dans
une expression de GENERATED car il n’est pas « immutable »
(pour des raisons
techniques ).
Un contournement pas très performant est celui-ci :
ALTER  TABLE  json .personnesADD  COLUMN  nom text  GENERATED  ALWAYS  AS  (personne->> 'prenom ' ) STORED,ADD  COLUMN  prenom text  GENERATED  ALWAYS  AS  (personne->> 'nom ' ) STORED,ADD  COLUMN  date_naissance date GENERATED  ALWAYS  AS  ( left (personne->> 'date_naissance ' ,4 ):: int ,substring (personne->> 'date_naissance ' ,6 ,2 ):: int ,left (personne->> 'date_naissance ' ,2 ):: int ))ADD  COLUMN  ville text  GENERATED  ALWAYS  AS  ( personne-> 'adresse ' ->> 'ville ' ) STORED ;VACUUM  ANALYZE  json .personnes ;Une autre possibilité plus performante est d’enrober
to_date() dans une fonction immutable, puisqu’il n’y a,
dans ce cas précis, pas d’ambiguïté sur le format ISO :
CREATE OR REPLACE FUNCTION  to_date_immutable (text )RETURNS  date -- Cette fonction requiert que les dates soient bien -- stockées au format ci-dessous -- et ne fait aucune gestion d'erreur sinon LANGUAGE  sql IMMUTABLE  PARALLEL  SAFEAS  $body$ SELECT  to_date ($1 , 'YYYY-MM-DD ' );$body$  ;et l’ordre devient :
ALTER  TABLE  json .personnesADD  COLUMN  date_naissance  date GENERATED  ALWAYS  AS  (to_date_immutable (personne->> 'date_naissance ' )) STORED,Les conversions de texte vers des dates sont des sources fréquentes
de problèmes. Le conseil habituel est de toujours stocker une date dans
un champ de type date ou
timestamp/timestamptz. Mais si elle provient
d’un JSON, il faudra gérer soi-même la conversion.
Quelle que soit la méthode, la requête suivante :
SELECT  nom, prenom, date_naissance, ville, pays FROM  json .personnes ;est beaucoup  plus rapide que :
SELECT  r1.nom, r1.prenom, r1.date_naissance, r2.ville, r2.paysFROM  json .personnes,LATERAL  jsonb_to_record (personne)AS  r1 (nom text , prenom text , date_naissance date ),LATERAL  jsonb_to_record (personne-> 'adresse ' )AS  r2 (ville text , pays text ) ;elle-même plus rapide que les extractions manuelles des attributs un
à un, comme vu plus haut.
Certes, la table est un peu plus grosse, mais le coût d’insertion des
colonnes générées est donc souvent rentable pour les champs fréquemment
utilisés.
jsonb et mise à jour 
Ajouter l’attribut animaux à Gaston Lagaffe, avec la
valeur 18. Vérifier en relisant la ligne.
 
UPDATE  json .personnesSET  personne =  personne ||  '{"animaux": 18} ' WHERE  personne @>   '{"nom": "Lagaffe", "prenom": "Gaston"} ' :: jsonb ;SELECT  personne->> 'animaux ' FROM  json .personnes WHERE  personne @>   '{"nom": "Lagaffe", "prenom": "Gaston"} ' :: jsonb  ;
Ajouter l’attribut animaux à 2% des individus au hasard,
avec une valeur 1 ou 2.
 
On utilise ici la fonction jsonb_build_object(), plus
adaptée à la construction d’un JSON qui n’est pas une constante. Le
choix des individus peut se faire de plusieurs manières, par exemple
avec random(), mod()…
UPDATE  json .personnesSET  personne =  personne || 'animaux ' , 1 + mod  ((personne->> 'numgen ' ):: int , 50 ))WHERE  mod ((personne->> 'numgen ' ):: int ,50 ) =  0  ;-- Conseillé après chaque mise à jour importante VACUUM  ANALYZE  json .personnes ;
Compter le nombre de personnes avec des animaux (avec ou sans
JSONPath). Proposer un index qui pourrait convenir à d’autres futurs
nouveaux attributs peu fréquents.
 
Ces requêtes renvoient 10654, mais effectuent toutes un Seq
Scan  avec une durée d’exécution aux alentours de la seconde :
SELECT  count (* ) FROM  json .personnesWHERE  (personne->> 'animaux ' ):: int  >  0  ;SELECT  count (* ) FROM  json .personnesWHERE   personne ? 'animaux '  ;  SELECT  count (* ) FROM  json .personnesWHERE   personne @@  '$.animaux > 0 '  ;SELECT  count (* ) FROM  json .personnesWHERE   personne @ ? '$.animaux ? (@ > 0)  '  ;(Remarquer que les deux dernières requêtes utiliseraient l’index GIN
pour des égalités comme (@ == 0) ou (@ == 18),
et seraient presque instantanées. Là encore, c’est une limite des index
GIN.)
On pourrait indexer (personne->>'animaux')::int,
ce qui serait excellent pour la première requête, mais ne conviendrait
pas à d’autres critères.
L’opérateur ? ne sait pas utiliser l’index GIN
jsonb_path_ops existant. Par contre, il peut profiter de
l’opérateur GIN par défaut :
CREATE  INDEX  personnes_gin_df ON  json .personnes USING  gin (personne) ;EXPLAIN SELECT  count (* ) FROM  json .personnesWHERE  personne ? 'animaux '  ;  Il est directement utilisable par tout autre attribut :
SELECT  count (* ) FROM  json .personnesWHERE  personne ? 'voitures '  ;  Cet index avec l’opérateur jsonb_ops a par contre le
gros inconvénient d’être encore plus gros que l’index GIN avec
jsonb_path_ops (303 Mo contre 235 Mo), et d’alourdir encore
les mises à jour. Il peut cependant remplacer ce dernier, de manière un
peu moins performante. Il faut aviser selon les requêtes, la place, les
écritures…
Large Objects 
Créer une table fichiers avec un texte
et une colonne permettant de référencer des Large Objects . 
 
CREATE  TABLE  fichiers (nom text  PRIMARY  KEY , data  OID);
Importer un fichier local à l’aide de psql dans un
large object. 
Noter l’oid retourné. 
 
psql  -c  "\lo_import '/etc/passwd'" INSERT  INTO  fichiers VALUES  ('/etc/passwd ' ,6821285 ) ;
Importer un fichier du serveur à l’aide de psql
dans un large object. 
 
INSERT  INTO  fichiers SELECT  'postgresql.conf ' ,'/var/lib/pgsql/15/data/postgresql.conf ' ) ;
Afficher le contenu de ces différents fichiers à
l’aide de psql. 
 
psql  -c  "SELECT nom,encode(l.data,'escape')  \            FROM fichiers f JOIN pg_largeobject l ON f.data = l.loid;" 
Les sauvegarder dans des fichiers locaux. 
 
psql  -c  "\lo_export loid_retourné '/home/dalibo/passwd_serveur';"