Vous êtes sur la page 1sur 716

Formation DEVSQLPG

SQL pour PostgreSQL

21.06
Dalibo SCOP

https://dalibo.com/formations

SQL pour PostgreSQL

Formation DEVSQLPG
TITRE : SQL pour PostgreSQL
SOUS-TITRE : Formation DEVSQLPG

REVISION: 21.06
DATE: 30 juin 2021
ISBN: -----------------
COPYRIGHT: © 2005-2021 DALIBO SARL SCOP
LICENCE: Creative Commons BY-NC-SA

Postgres®, PostgreSQL® and the Slonik Logo are trademarks or registered trademarks
of the PostgreSQL Community Association of Canada, and used with their permission.
(Les noms PostgreSQL® et Postgres®, et le logo Slonik sont des marques déposées par
PostgreSQL Community Association of Canada.
Voir https://www.postgresql.org/about/policies/trademarks/ )

Remerciements : Ce manuel de formation est une aventure collective qui se transmet au


sein de notre société depuis des années. Nous remercions chaleureusement ici toutes
les personnes qui ont contribué directement ou indirectement à cet ouvrage, notam-
ment : Jean-Paul Argudo, Alexandre Anriot, Carole Arnaud, Alexandre Baron, David Bidoc,
Sharon Bonan, Franck Boudehen, Arnaud Bruniquel, Damien Clochard, Christophe Cour-
tois, Marc Cousin, Gilles Darold, Jehan-Guillaume de Rorthais, Ronan Dunklau, Vik Fear-
ing, Stefan Fercot, Pierre Giraud, Nicolas Gollet, Dimitri Fontaine, Florent Jardin, Vir-
ginie Jourdan, Luc Lamarle, Denis Laxalde, Guillaume Lelarge, Benoit Lobréau, Jean-Louis
Louër, Thibaut Madelaine, Adrien Nayrat, Alexandre Pereira, Flavie Perette, Robin Por-
tigliatti, Thomas Reiss, Maël Rimbault, Julien Rouhaud, Stéphane Schildknecht, Julien
Tachoires, Nicolas Thauvin, Christophe Truffier, Cédric Villemain, Thibaud Walkowiak,
Frédéric Yhuel.

À propos de DALIBO : DALIBO est le spécialiste français de PostgreSQL. Nous proposons


du support, de la formation et du conseil depuis 2005. Retrouvez toutes nos formations
sur https://dalibo.com/formations
LICENCE CREATIVE COMMONS BY-NC-SA 2.0 FR

Attribution - Pas d’Utilisation Commerciale - Partage dans les Mêmes Conditions

Vous êtes autorisé à :

• Partager, copier, distribuer et communiquer le matériel par tous moyens et sous


tous formats

• Adapter, remixer, transformer et créer à partir du matériel

Dalibo ne peut retirer les autorisations concédées par la licence tant que vous appliquez
les termes de cette licence selon les conditions suivantes :

Attribution : Vous devez créditer l’œuvre, intégrer un lien vers la licence et indiquer si des
modifications ont été effectuées à l’œuvre. Vous devez indiquer ces informations par tous
les moyens raisonnables, sans toutefois suggérer que Dalibo vous soutient ou soutient la
façon dont vous avez utilisé ce document.

Pas d’Utilisation Commerciale : Vous n’êtes pas autorisé à faire un usage commercial de ce
document, tout ou partie du matériel le composant.

Partage dans les Mêmes Conditions : Dans le cas où vous effectuez un remix, que vous
transformez, ou créez à partir du matériel composant le document original, vous devez
diffuser le document modifié dans les même conditions, c’est à dire avec la même licence
avec laquelle le document original a été diffusé.

Pas de restrictions complémentaires : Vous n’êtes pas autorisé à appliquer des conditions
légales ou des mesures techniques qui restreindraient légalement autrui à utiliser le doc-
ument dans les conditions décrites par la licence.

Note : Ceci est un résumé de la licence. Le texte complet est disponible ici :

https://creativecommons.org/licenses/by-nc-sa/2.0/fr/legalcode

Pour toute demande au sujet des conditions d’utilisation de ce document, envoyez vos
questions à contact@dalibo.com !
Chers lectrices & lecteurs,

Nos formations PostgreSQL sont issues de nombreuses années d’études, d’expérience


de terrain et de passion pour les logiciels libres. Pour Dalibo, l’utilisation de PostgreSQL
n’est pas une marque d’opportunisme commercial, mais l’expression d’un engagement de
longue date. Le choix de l’Open Source est aussi le choix de l’implication dans la commu-
nauté du logiciel.

Au-delà du contenu technique en lui-même, notre intention est de transmettre les valeurs
qui animent et unissent les développeurs de PostgreSQL depuis toujours : partage, ou-
verture, transparence, créativité, dynamisme... Le but premier de nos formations est de
vous aider à mieux exploiter toute la puissance de PostgreSQL mais nous espérons égale-
ment qu’elles vous inciteront à devenir un membre actif de la communauté en partageant
à votre tour le savoir-faire que vous aurez acquis avec nous.

Nous mettons un point d’honneur à maintenir nos manuels à jour, avec des informations
précises et des exemples détaillés. Toutefois malgré nos efforts et nos multiples relec-
tures, il est probable que ce document contienne des oublis, des coquilles, des impréci-
sions ou des erreurs. Si vous constatez un souci, n’hésitez pas à le signaler via l’adresse
formation@dalibo.com !
Table des Matières

Licence Creative Commons BY-NC-SA 2.0 FR 5

1 PostgreSQL : historique & communauté 14


1.1 Préambule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.2 Un peu d’histoire... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3 Les versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
1.4 Quelle version utiliser ? . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
1.5 Versions dérivées / Forks . . . . . . . . . . . . . . . . . . . . . . . . . . 31
1.6 Quelques projets satellites . . . . . . . . . . . . . . . . . . . . . . . . . 32
1.7 Sponsors & Références . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.8 À la rencontre de la communauté . . . . . . . . . . . . . . . . . . . . . 42
1.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
1.10 Annexe : Installation de PostgreSQL depuis les paquets communautaires 52

2 Découverte des fonctionnalités 59


2.1 Au menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.2 Fonctionnalités du moteur . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.3 Objets SQL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

3 Introduction et premiers SELECT 93


3.1 Préambule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.2 Principes d’une base de données . . . . . . . . . . . . . . . . . . . . . . 94
3.3 Lecture de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
3.4 Types de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
3.6 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
3.7 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 139

4 Création d’objet et mises à jour 143


4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
4.2 DDL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
4.3 DML : mise à jour des données . . . . . . . . . . . . . . . . . . . . . . . 176
4.4 Transactions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
4.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
4.6 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
4.7 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 189

5 Plus loin avec SQL 194


5.1 Préambule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
9
SQL pour PostgreSQL

5.2 Valeur NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196


5.3 Agrégats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
5.4 Sous-requêtes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
5.5 Jointures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
5.6 Expressions CASE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
5.7 Opérateurs ensemblistes . . . . . . . . . . . . . . . . . . . . . . . . . . 225
5.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
5.9 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
5.10 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 234

6 SQL avancé pour le transactionnel 243


6.1 LIMIT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
6.2 RETURNING . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
6.3 UPSERT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
6.4 LATERAL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
6.5 Common Table Expressions . . . . . . . . . . . . . . . . . . . . . . . . . 261
6.6 Concurrence d’accès . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
6.7 Serializable Snapshot Isolation . . . . . . . . . . . . . . . . . . . . . . . 280
6.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
6.9 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
6.10 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 287

7 Types de base 294


7.1 Les types de données . . . . . . . . . . . . . . . . . . . . . . . . . . . . 295
7.2 Types numériques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 296
7.3 Types temporels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300
7.4 Types chaînes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 304
7.5 Types avancés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
7.6 Types intervalle de valeurs . . . . . . . . . . . . . . . . . . . . . . . . . 312
7.7 Types géométriques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313
7.8 Types utilisateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313

8 Types avancés 315


8.1 Types composés : généralités . . . . . . . . . . . . . . . . . . . . . . . . 315
8.2 hstore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 316
8.3 JSON . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 318
8.4 XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 326
8.5 Objets Binaires . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 328
8.6 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332
8.7 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 336

10
Table des Matières

9 SQL pour l’analyse de données 346


9.1 Préambule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
9.2 Agrégats . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
9.3 Clause FILTER . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
9.4 Fonctions de fenêtrage . . . . . . . . . . . . . . . . . . . . . . . . . . . 354
9.5 WITHIN GROUP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
9.6 Grouping Sets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372
9.7 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
9.8 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 385

10 SQL : Ce qu’il ne faut pas faire 398


10.1 Des mauvaises pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . 399
10.2 Problèmes de modélisation . . . . . . . . . . . . . . . . . . . . . . . . . 399
10.3 Atomicité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 401
10.4 Contraintes absente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 403
10.5 Stockage Entité-Clé-Valeur . . . . . . . . . . . . . . . . . . . . . . . . . 406
10.6 Attributs multi-colonnes . . . . . . . . . . . . . . . . . . . . . . . . . . 409
10.7 Nombreuses lignes de peu de colonnes . . . . . . . . . . . . . . . . . . 412
10.8 Tables aux très nombreuses colonnes . . . . . . . . . . . . . . . . . . . 414
10.9 Choix des types de données . . . . . . . . . . . . . . . . . . . . . . . . 414
10.10 Colonne de type variable . . . . . . . . . . . . . . . . . . . . . . . . . . 415
10.11 Problèmes courants d’écriture de requêtes . . . . . . . . . . . . . . . . 416
10.12 NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 416
10.13 Ordre implicite des colonnes . . . . . . . . . . . . . . . . . . . . . . . . 417
10.14 Code spaghetti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 419
10.15 Recherche textuelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 430
10.16 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
10.17 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 432
10.18 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 437

11 PL/pgSQL : les bases 458


11.1 Préambule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 459
11.2 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 460
11.3 Installation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465
11.4 Exemples de fonctions et procédures . . . . . . . . . . . . . . . . . . . 467
11.5 Invocation d’une fonction ou procédure . . . . . . . . . . . . . . . . . . 469
11.6 Création et structure d’une fonction ou procédure . . . . . . . . . . . . 470
11.7 Déclarations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 487
11.8 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491
11.9 Structures de contrôles . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
11
https://dalibo.com/formations
SQL pour PostgreSQL

11.10 Retour d’une fonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 501


11.11 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
11.12 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 503
11.13 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 508

12 PL/pgSQL avancé 526


12.1 Préambule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527
12.2 Routines variadic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 527
12.3 Routines polymorphes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 530
12.4 Fonctions trigger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 533
12.5 Curseurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 541
12.6 Contrôle transactionnel . . . . . . . . . . . . . . . . . . . . . . . . . . . 544
12.7 Gestion des erreurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 546
12.8 Sécurité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 555
12.9 Optimisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 560
12.10 Outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 566
12.11 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 578
12.12 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 579
12.13 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 580

13 Extensions PostgreSQL pour l’utilisateur 589


13.1 Qu’est-ce qu’une extension ? . . . . . . . . . . . . . . . . . . . . . . . . 589
13.2 Administration des extensions . . . . . . . . . . . . . . . . . . . . . . . 590
13.3 Contribs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 591
13.4 Quelques extensions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 592
13.5 Extensions pour de nouveaux langages . . . . . . . . . . . . . . . . . . 596
13.6 Accès distants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 597
13.7 Contribs orientés DBA . . . . . . . . . . . . . . . . . . . . . . . . . . . 597
13.8 PGXN . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 598
13.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 602

14 Partitionnement 604
14.1 Principe & intérêts du partitionnement . . . . . . . . . . . . . . . . . . 604
14.2 Partitionnement applicatif . . . . . . . . . . . . . . . . . . . . . . . . . 605
14.3 Historique du partitionnement sur PostgreSQL . . . . . . . . . . . . . . 605
14.4 Partitionnement par héritage . . . . . . . . . . . . . . . . . . . . . . . . 606
14.5 Partitionnement déclaratif . . . . . . . . . . . . . . . . . . . . . . . . . 610
14.6 Extensions & outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 623
14.7 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 624
14.8 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 627

12
Table des Matières

15 Connexions distantes 631


15.1 Accès à distance à d’autres sources de données . . . . . . . . . . . . . 631
15.2 Foreign Data Wrappers . . . . . . . . . . . . . . . . . . . . . . . . . . . 631
15.3 SQL/MED : utilisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 632
15.4 SQL/MED : PostgreSQL . . . . . . . . . . . . . . . . . . . . . . . . . . . 633
15.5 SQL/MED : Performances . . . . . . . . . . . . . . . . . . . . . . . . . . 637
15.6 SQL/MED : héritage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 637
15.7 dblink . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 643
15.8 PL/Proxy : présentation . . . . . . . . . . . . . . . . . . . . . . . . . . . 644
15.9 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 645
15.10 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 646

16 Fonctionnalités avancées pour la performance 648


16.1 Préambule . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 648
16.2 Tables non journalisées (unlogged) . . . . . . . . . . . . . . . . . . . . . 648
16.3 JIT : la compilation à la volée . . . . . . . . . . . . . . . . . . . . . . . . 650
16.4 Recherche Plein Texte . . . . . . . . . . . . . . . . . . . . . . . . . . . . 654
16.5 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 664
16.6 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 666

17 Pooling 672
17.1 Au menu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 673
17.2 Pool de connexion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 673
17.3 Pooling de sessions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 678
17.4 Pooling de transactions . . . . . . . . . . . . . . . . . . . . . . . . . . . 680
17.5 Pooling de requêtes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 682
17.6 Pooling avec PgBouncer . . . . . . . . . . . . . . . . . . . . . . . . . . . 684
17.7 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 696
17.8 Travaux pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 697
17.9 Travaux pratiques (solutions) . . . . . . . . . . . . . . . . . . . . . . . . 699

13
https://dalibo.com/formations
SQL pour PostgreSQL

1 POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

14
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.1 PRÉAMBULE

• Quelle histoire !
– parmi les plus vieux logiciels libres
– et les plus sophistiqués
• Souvent cité comme exemple
– qualité du code
– indépendance des développeurs
– réactivité de la communauté

L’histoire de PostgreSQL est longue, riche et passionnante. Au côté des projets libres
Apache et Linux, PostgreSQL est l’un des plus vieux logiciels libres en activité et fait partie
des SGBD les plus sophistiqués à l’heure actuelle.

Au sein des différentes communautés libres, PostgreSQL est souvent cité comme exemple
à différents niveaux :

• qualité du code ;
• indépendance des développeurs et gouvernance du projet ;
• réactivité de la communauté ;
• stabilité et puissance du logiciel.

Tous ces atouts font que PostgreSQL est désormais reconnu et adopté par des milliers de
grandes sociétés de par le monde.

1.1.1 AU MENU

• Origines et historique du projet


• Versions et feuille de route
• Projets satellites
• Sponsors et références
• La communauté

Cette première partie est un tour d’horizon pour découvrir les multiples facettes du sys-
tème de gestion de base de données libre PostgreSQL.

Les deux premières parties expliquent la genèse du projet et détaillent les différences
entres les versions successives du logiciel. PostgreSQL est un des plus vieux logiciels
open source ! Comprendre son histoire permet de mieux réaliser le chemin parcouru et
les raisons de son succès.
15
https://dalibo.com/formations
SQL pour PostgreSQL

Nous verrons ensuite quelques projets satellites et nous listerons quelques utilisateurs
renommés et cas d’utilisations remarquables.

Enfin, nous terminerons par un tour sur la communauté.

1.2 UN PEU D'HISTOIRE...

• La licence
• L’origine du nom
• Les origines du projet
• Les principes

1.2.1 LICENCE

• Licence PostgreSQL
– BSD / MIT
– https://www.postgresql.org/about/licence/
• Droit, sans coûts de licence, de :
– utiliser, copier, modifier, distribuer, revendre
• Reconnue par l’Open Source Initiative
– https://opensource.org/licenses/PostgreSQL
• Utilisée par un grand nombre de projets de l’écosystème

PostgreSQL est distribué sous une licence spécifique, combinant la licence BSD et la li-
cence MIT. Cette licence spécifique est reconnue comme une licence libre par l’Open
Source Initiative.

Cette licence vous donne le droit de distribuer PostgreSQL, de l’installer, de le modi-


fier... et même de le vendre. Certaines sociétés, comme EnterpriseDB et PostgresPro,
produisent leur propre version de PostgreSQL de cette façon.

Cette licence a ensuite été reprise par de nombreux projets de la communauté : pgAdmin,
pgCluu, pgstat, etc.

16
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.2.2 POSTGRESQL ?!?!

• 1985 : Michael Stonebraker recode Ingres


• post « ingres » => postingres => postgres
• postgres => PostgreSQL

L’origine du nom PostgreSQL remonte au système de gestion de base de données In-


gres, développé à l’université de Berkeley par Michael Stonebraker. En 1985, il prend la
décision de reprendre le développement à partir de zéro et nomme ce nouveau logiciel
Postgres, comme raccourci de post-Ingres.

En 1995, avec l’ajout du support du langage SQL, Postgres fut renommé Postgres95 puis
PostgreSQL.

Aujourd’hui, le nom officiel est « PostgreSQL » (prononcé « post - gresse - Q - L »). Cepen-
dant, le nom « Postgres » est accepté comme alias.
Pour aller plus loin :
• Fil de discussion sur les listes de discussiona ;
• Article sur le wiki officielb .

1.2.3 PRINCIPES FONDATEURS

• Sécurité des données (ACID)


• Respect des normes (ISO SQL)
• Fonctionnalités intéressant le plus grand nombre
• Performances
• Simplicité du code
• Documentation

Depuis son origine, PostgreSQL a toujours privilégié la stabilité et le respect des standards
plutôt que les performances.

Ceci explique en partie la réputation de relative lenteur et de complexité face aux autres
SGBD du marché. Cette image est désormais totalement obsolète, notamment grâce aux
avancées réalisées depuis les versions 8.x.

La sécurité des données est un point essentiel. Un utilisateur doit être certain qu’à partir
du moment où il a exécuté l’odre COMMIT d’une transaction, les données modifiées rela-
tives à cette transaction se trouvent bien sur disque et que même un crash ne pourra pas
a
https://archives.postgresql.org/pgsql-advocacy/2007-11/msg00109.php
b
https://wiki.postgresql.org/wiki/Postgres

17
https://dalibo.com/formations
SQL pour PostgreSQL

les faire disparaître. PostgreSQL est très attaché à ce concept et fait son possible pour
forcer le système d’exploitation à ne pas conserver les données en cache, mais à les écrire
sur disque dès l’arrivée d’un COMMIT.

Le respect des normes est un autre principe très respecté. Les devéloppeurs de Post-
greSQL cherchent à coller à la norme SQL le plus possible. PostgreSQL n’est pas com-
patible à cette norme à 100%, aucun moteur ne l’est, mais il cherche à s’en approcher.
Tout nouvel ajout d’une syntaxe ne sera accepté que si la syntaxe de la norme est ajoutée.
Des extensions sont acceptées pour différentes raisons (performances, fonctionnalités
en avance sur le comité de la norme, facilité de transition d’un moteur de bases de don-
nées à un autre) mais si une fonctionnalité existe dans la norme, une syntaxe différente
ne peut être acceptée que si la syntaxe de la norme est elle-aussi présente.

Ajouter des fonctionnalités est évidemment l’un des buts des développeurs de Post-
greSQL. Cependant, comme il s’agit d’un projet libre, rien n’empêche un développeur de
proposer une fonctionnalité, de la faire intégrer, puis de disparaître laissant aux autres
la responsabilité de la corriger le cas échéant. Comme le nombre de développeurs de
PostgreSQL est restreint, il est important que les fonctionnalités ajoutées soient vraiment
utile au plus grand nombre pour justifier le coût potentiel du débogage. Donc ne sont
ajoutées dans PostgreSQL que ce qui est vraiment le coeur du moteur de bases de
données et que ce qui sera utilisé vraiment par le plus grand nombre. Une fonctionnalité
qui ne sert que une à deux personnes aura très peu de chances d’être intégrée.

Les performances ne viennent qu’après tout ça. En effet, rien ne sert d’avoir une modifi-
cation du code qui permet de gagner énormément en performances si cela met en péril
le stockage des données.

La simplicité du code est un point important. Le code est relu scrupuleusement par dif-
férents contributeurs pour s’assurer qu’il est facile à lire et à comprendre. En effet, cela
facilitera le débogage plus tard si cela devient nécessaire.

Enfin, la documentation est là-aussi un point essentiel dans l’admission d’une nouvelle
fonctionnalité. En effet, sans documentation, peu de personnes pourront connaître cette
fonctionnalité. Très peu sauront exactement ce qu’elle est supposée faire, et il serait
donc très difficile de déduire si un problème particulier est un manque actuel de cette
fonctionnalité ou un bug.

Tous ces points sont vérifiés à chaque relecture d’un patch (nouvelle fonctionnalité ou
correction).

18
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.2.4 ORIGINES

• Années 1970 : Ingres est développé à Berkeley


• 1985 : Postgres succède à Ingres
• 1995 : Ajout du langage SQL
• 1996 : Postgres devient PostgreSQL
• 1996 : Création du PostgreSQL Global Development Group

L’histoire de PostgreSQL remonte au système de gestion de base de données Ingres,


développé à Berkeley par Michael Stonebraker. Lorsque ce dernier décida en 1985 de
recommencer le développement de zéro, il nomma le logiciel Postgres, comme raccourci
de post-Ingres. Lors de l’ajout des fonctionnalités SQL en 1995 par deux étudiants chi-
nois de Berkeley, Postgres fut renommé Postgres95. Ce nom fut changé à la fin de 1996
en PostgreSQL lors de la libération du code source de PostgreSQL.
Plus d’informations :
• Page associée sur le site officiela .

1.2.5 APPARITION DE LA COMMUNAUTÉ INTERNATIONALE

• ~ 2000: Communauté japonaise


• 2004 : Communauté francophone
• 2006 : SPI
• 2007 : Communauté italienne
• 2008 : PostgreSQL Europe et US
• 2009 : Boom des PGDay
• 2011 : Postgres Community Association of Canada
• 2017 : Community Guidelines
• ...et ça continue

Les années 2000 voient l’apparition de communautés locales organisées autour


d’association ou de manière informelle. Chaque communauté organise la promotion, la
diffusion d’information et l’entraide à son propre niveau.

En 2000 apparaît la communauté japonaise. Elle dispose d’un grand groupe, capable de
réaliser des conférences chaque année, d’éditer des livres et des magazines. Elle compte
au dernier recensement connu, plus de 3000 membres.

En 2004 naît l’association française (loi 1901) appelée PostgreSQL Fr. Cette associa-
tion a pour but de fournir un cadre légal pour pouvoir participer à certains événements
a
https://www.postgresql.org/docs/current/static/history.html

19
https://dalibo.com/formations
SQL pour PostgreSQL

comme Solutions Linux, les RMLL ou d’en organiser comme le pgDay.fr (qui a déjà eu lieu
à Toulouse, Nantes, Lyon, Toulon, Marseille). Elle permet aussi de récolter des fonds pour
aider à la promotion de PostgreSQL.

En 2006, le PGDG intègre Software in the Public Interest, Inc. (SPI)1 , une organisation à
but non lucratif chargée de collecter et redistribuer des financements. Elle a été créée à
l’initiative de Debian et dispose aussi de membres comme LibreOffice.org.

En 2008, douze ans après la création du projet, des associations d’utilisateurs apparais-
sent pour soutenir, promouvoir et développer PostgreSQL à l’échelle internationale. Post-
greSQL UK organise une journée de conférences à Londres, PostgreSQL Fr en organise
une à Toulouse. Des « sur-groupes » apparaissent aussi pour aider les groupes. PGUS
apparaît pour consolider les différents groupes américains d’utilisateurs PostgreSQL. Ces
derniers étaient plutôt créés géographiquement, par état ou grosse ville. De même, en
Europe, est fondée PostgreSQL Europe, une association chargée d’aider les utilisateurs
de PostgreSQL souhaitant mettre en place des événements. Son principal travail est
l’organisation d’un événement majeur en Europe tous les ans : pgconf.eu2 , d’abord à
Paris en 2009, puis dans divers pays d’Europe jusque Milan en 2019. Cependant, elle
aide aussi les communautés allemande, française et suédoise à monter leur propre événé-
ment (respectivement pgconf.de3 , pgday.paris4 et nordic pgday5 ).

Dès 2010, nous dénombrons plus d’une conférence par mois consacrée uniquement à
PostgreSQL dans le monde. Ce mouvement n’est pas prêt de s’arrêter :

• Communauté japonaise6 ;
• Communauté francophone7 ;
• Communauté italienne8 ;
• Communauté européenne9 ;
• Communauté US10 .

En 2011, l’association Postgres Community Association of Canada voit le jour11 . Elle est
créée par quelques membres de la Core Team pour gérer le nom déposé PostgreSQL, le
logo, le nom de domaine sur Internet, etc.

1
https://fr.wikipedia.org/wiki/Software_in_the_Public_Interest
2
https://pgconf.eu
3
https://pgconf.de
4
https://pgday.paris
5
https://nordicpgday.org
6
https://www.postgresql.jp/
7
https://www.postgresql.fr/
8
https://www.itpug.org/
9
https://www.postgresql.eu/
10
https://www.postgresql.us/
11
https://www.postgresql.org/message-id/4DC440BE.5040104%40agliodbs.com%3E

20
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

Vu l’émergence de nombreuses communautés internationales, la communauté a décidé


d’écrire quelques règles pour ces communautés. Il s’agit des Community Guidelines, ap-
parues en 2017, et disponibles sur le site officiel12 .

1.2.6 PROGRESSION DU CODE

Ce graphe (source : openhub.net13 ) représente l’évolution du nombre de lignes de code


dans les sources de PostgreSQL. Cela permet de bien visualiser l’évolution du projet en
terme de développement.

On note une augmentation constante depuis 2000 avec une croissance régulière
d’environ 25 000 lignes de code C par an. Le plus intéressant est certainement de noter
que l’évolution est constante. Il n’y a pas de gros pic, ni dans un sens, ni dans l’autre.

L’autre point intéressant dans ce graphe concerne le ratio entre le nombre de lignes de
code (en bleu) et celui des commentaires (en gris). Il y a à peu près un tiers de commen-
taires pour deux tiers de lignes de code. Ce ratio montre que le code est très commenté,
très documenté. Ceci fait qu’il est facile à lire, et donc pratique à déboguer. Et le ratio ne
change pas au fil des ans.

Actuellement, PostgreSQL est composé d’1,4 million de lignes de code (dont un quart de
commentaires), essentiellement en C, pour environ 200 développeurs actifs, et entre 100
et 200 commits par mois.

12
https://www.postgresql.org/community/recognition/
13
https://www.openhub.net/p/postgres

21
https://dalibo.com/formations
SQL pour PostgreSQL

1.3 LES VERSIONS

• Versions obsolètes
– 9.5 et antérieures
• Versions actuelles pour production
– 9.6, 10 à 13
• Version en cours de développement
– 14
• Versions dérivées

La dernière version majeure sortie est la version 13. Le développement de la version 14


est en cours depuis mai 2020.

Actuellement, les versions conseillées en production sont celles postérieures à la 9.5. La


9.6 ne sera plus maintenue à partir de novembre 2021.

1.3.1 HISTORIQUE

Sources : page Wikipédia de PostgreSQL14 et PostgreSQL Versioning Policy15

14
https://en.wikipedia.org/wiki/PostgreSQL
15
https://www.postgresql.org/support/versioning/

22
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.3.2 HISTORIQUE

• 1996 : v1.0 -> première version publiée


• 2003 : v7.4 -> première version réellement stable
• 2005 : v8.0 -> arrivée sur Windows
• 2008 : v8.3 -> performances et fonctionnalités, organisation
• 2010 : v9.0 -> réplication physique
• 2016 : v9.6 -> parallélisation
• 2017 : v10 -> réplication logique, partitionnement déclaratif
• 2020 : v13 -> performances, fonctionnalités, administration

La version 7.4 est la première version réellement stable. La gestion des journaux de trans-
actions a été nettement améliorée, et de nombreuses optimisations ont été apportées au
moteur.

La version 8.0 marque l’entrée tant attendue de PostgreSQL dans le marché des SGDB
de haut niveau, en apportant des fonctionnalités telles que les tablespaces, les routines
stockées en Java, le Point In Time Recovery, ainsi qu’une version native pour Windows.

La version 8.3 se focalise sur les performances et les nouvelles fonctionnalités. C’est aussi
la version qui a causé un changement important dans l’organisation du développement :
gestion des commitfests, création de l’outil web commitfest, etc.

Les versions 9.x sont axées réplication physique. La 9.0 intègre un système de réplication
asynchrone asymétrique. La version 9.1 ajoute une réplication synchrone et améliore de
nombreux points sur la réplication (notamment pour la partie administration et supervi-
sion). La version 9.2 apporte la réplication en cascade. La 9.3 et la 9.4 ajoutent quelques
améliorations supplémentaires. La version 9.4 intègre surtout les premières briques pour
l’intégration de la réplication logique dans PostgreSQL. La version 9.6 apporte la paralléli-
sation, ce qui était attendu par de nombreux utilisateurs.

La version 10 propose beaucoup de nouveautés, comme une amélioration nette de la


parallélisation et du partitionnement (le partitionnement déclaratif complète l’ancien par-
titionnement par héritage), mais surtout l’ajout de la réplication logique.

Les améliorations des versions 11 à 13 sont plus incrémentales, et portent sur tous les
plans. Le partitionnement déclaratif est progressivement amélioré, en performances
comme en facilité de développement. Les performances s’améliorent encore grâce
à la compilation Just In Time, la parallélisation de plus en plus d’opérations, les index
couvrants, l’affinement des statistiques. La facilité d’administration s’améliore : nouvelles
vues système, outillage de réplication, activation des sommes de contrôle sur une
instance existante.
23
https://dalibo.com/formations
SQL pour PostgreSQL

Il est toujours possible de télécharger les sources depuis la version 1.0 jusqu’à la version
courante sur postgresql.org16 .

1.3.3 NUMÉROTATION

• Avant la version 10
– X.Y : version majeure (8.4, 9.6)
– X.Y.Z : version mineure (9.6.19)
• Après la version 10
– X : version majeure (10, 11, 12, 13)
– X.Y : version mineure (12.4)
• Mise à jour en général sans souci
– Release notes
– Tests
– Redémarrage

Une version majeure apporte de nouvelles fonctionnalités, des changements de com-


portement, etc. Une version majeure sort généralement tous les 12/15 mois.

Une version mineure ne comporte que des corrections de bugs ou de failles de sécurité.
Les publications de versions mineures sont plus fréquentes que celles de versions ma-
jeures, avec un rythme de sortie trimestriel, sauf bug majeur ou faille de sécurité. Chaque
bug est corrigé dans toutes les versions stables actuellement maintenues par le projet.

En général, les mises à jour se font sans soucis et ne nécessitent qu’un redémarrage. Mais
comme pour toute mise à jour, il convient d’être prudent sur d’éventuels effets de bord.
En particulier, il faudra lire les Release Notes et, si possible, effectuer les tests ailleurs qu’en
production.

16
https://www.postgresql.org/ftp/source/

24
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.3.4 VERSIONS COURANTES

• Généralement 1 version majeure par an


– maintenues 5 ans
• Dernières mises à jour mineures (16 mai 2021) :
– version 9.6.22
– version 10.17
– version 11.12
– version 12.7
– version 13.3
• Prochaine sortie de versions mineures prévue : 12 août 2021

La philosophie générale des développeurs de PostgreSQL peut se résumer ainsi :


« Notre politique se base sur la qualité, pas sur les dates de sortie. »

Toutefois, même si cette philosophie reste très présente parmi les développeurs, les
choses évoluent depuis quelques années, et en pratique une version stable majeure
paraît tous les ans, habituellement à l’automne. Pour ne pas sacrifier la qualité des
versions, toute fonctionnalité supposée insuffisamment stable est repoussée à la version
suivante.

La tendance actuelle est de garantir un support pour chaque version courante pendant
une durée minimale de 5 ans. Ainsi ne sont plus supportées : la 9.3 depuis novembre
2018, la 9.4 depuis février 2020, la 9.5 depuis février 2021. La prochaine version qui
subira ce sort est la 9.6 en novembre 2021. Le support de la version 13 devrait durer
jusqu’en 2025.
Pour plus de détails :
• Politique de versionnementa ;
• Roadmap des versions mineuresb .

a
https://www.postgresql.org/support/versioning/
b
https://www.postgresql.org/developer/roadmap/

25
https://dalibo.com/formations
SQL pour PostgreSQL

1.3.5 VERSION 9.5

• Janvier 2016 - Février 2021


• Row Level Security
• Index BRIN
• Fonctions OLAP (GROUPING SETS, CUBE et ROLLUP)
• INSERT ... ON CONFLICT { UPDATE | IGNORE }
• SKIP LOCKED
• SQL/MED :
– import de schéma, héritage
• Supervision
– amélioration de pg_stat_statements , ajout de pg_stat_ssl
Cette version n’est plus supportée !
Pour plus de détails :
• Page officielle des nouveautés de la version 9.5a ;
• Workshop Dalibo sur la version 9.5b .

1.3.6 VERSION 9.6

• Septembre 2016 - Novembre 2021


• Parallélisation
– parcours séquentiel, jointure, agrégation
• SQL/MED
– tri distant, jointures impliquant deux tables distantes
• Index bloom
• Réplication synchrone améliorée
• Réduction des inconvénients de MVCC
– optimisation du VACUUM FREEZE, du checkpoint, timeout des vieux snapshots
• Maintenance
Cette version ne sera bientôt plus supportée !

La 9.6 est sortie le 29 septembre 2016.

La fonctionnalité majeure est certainement l’intégration du parallélisme de certaines par-


ties de l’exécution d’une requête.
a
https://wiki.postgresql.org/wiki/What%27s_new_in_PostgreSQL_9.5
b
https://kb.dalibo.com/conferences/nouveautes_de_postgresql_9.5

26
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

Pour plus de détails :


• Page officielle des nouveautés de la version 9.6a ;
• Workshop Dalibo sur la version 9.6b .

1.3.7 VERSION 10

• Octobre 2017 - Novembre 2022


• Meilleure parallélisation :
– parcours d’index, jointure MergeJoin, sous-requêtes corrélées
• Réplication logique
• Partitionnement déclaratif
• Attention : renommage de fonctions et répertoires !

Les fonctionnalités majeures sont l’intégration de la réplication logique et le partition-


nement déclaratif, longtemps attendus, qui seront améliorés dans les versions suivantes.
Cependant, d’autres améliorations devraient attirer les utilisateurs comme les tables de
transition ou les améliorations sur la parallélisation.

La version 10 a aussi été l’occasion de renommer plusieurs répertoires et fonctions sys-


tème, et même des outils. Attention donc si vous rencontrez des requêtes ou des scripts
adaptés aux versions précédentes. Entre autres :

• le répertoire pg_xlog est devenu pg_wal ;


• le répertoire pg_clog est devenu pg_xact ;
• dans les noms de fonctions, xlog a été remplacé par wal (par exemple
pg_switch_xlog est devenue pg_switch_wal) ;
• toujours dans les fonctions, location a été remplacé par lsn.
Pour plus de détails :
• Page officielle des nouveautés de la version 10a ;
• Workshop Dalibo sur la version 10b .

a
https://wiki.postgresql.org/wiki/NewIn96
b
https://github.com/dalibo/workshops/tree/master/fr
a
https://wiki.postgresql.org/wiki/New_in_postgres_10
b
https://dali.bo/workshop10_pdf

27
https://dalibo.com/formations
SQL pour PostgreSQL

1.3.8 VERSION 11

• Octobre 2018 - Novembre 2023


• Meilleure parallélisation
• Amélioration du partitionnement déclaratif
• Amélioration de la réplication logique
• JIT, index couvrants

La version 11 est sortie le 18 octobre 2018. Elle améliore le partitionnement de la version


10, le parallélisme, la réplication logique... et de nombreux autres points. Elle comprend
aussi une première version du JIT (Just In Time compilation) pour accélérer les requêtes les
plus lourdes en CPU, ou encore les index couvrants.
Pour plus de détails, voir notre workshop sur la version 11a .

1.3.9 VERSION 12

• Octobre 2019 - Novembre 2024


• Amélioration du partitionnement déclaratif
• Amélioration des performances
– sur la gestion des index
– sur les CTE (option MATERIALIZED)
• Colonnes générées
• Nouvelles vues de visualisation de la progression des commandes

La version 12 est sortie le 3 octobre 2019. Elle améliore de nouveau le partitionnement


et elle fait surtout un grand pas au niveau des performances et de la supervision.

Le fichier recovery.conf (pour la réplication et les restaurations physiques) disparaît. Il


est maintenant intégré au fichier postgresql.conf. Une source fréquente de ralentisse-
ment disparaît, avec l’intégration des CTE (clauses WITH) dans la requête principale. Des
colonnes d’une table peuvent être automatiquement générées à partir d’autres colonnes.
Pour plus de détails, voir notre workshop sur la version 12a .

a
https://dali.bo/workshop11_pdf
a
https://dali.bo/workshop12_pdf

28
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.3.10 VERSION 13

• Septembre 2020 - Septembre 2025


• Amélioration du partitionnement déclaratif :
– trigger BEFORE niveau ligne, réplication logique
• Amélioration des performances :
– index BTree, objet statistique, tri et agrégat
• Amélioration de l’autovacuum et du VACUUM :
– gestion complète des tables en insertion seule
– traitement parallélisé des index lors d’un VACUUM
• Amélioration des sauvegardes :
– génération d’un fichier manifeste, outil pg_verifybackup
• Nouvelles vues de progression de commandes :
– pg_stat_progress_basebackup, pg_stat_progress_analyze

La version 13 est sortie le 24 septembre 2020. Elle est remplie de nombreuses petites
améliorations sur différents domaines : partitionnement déclaratif, autovacuum, sauveg-
arde, etc. Les performances sont aussi améliorées grâce à un gros travail sur l’optimiseur,
ou la réduction notable de la taille de certains index.
Pour plus de détails, voir notre workshop sur la version 13a .

1.3.11 PETIT RÉSUMÉ

• Versions 7.x :
– fondations
– durabilité
• Versions 8.x :
– fonctionnalités
– performances
• Versions 9.x :
– réplication physique
– extensibilité
• Versions 10 à 13 :
– réplication logique
– parallélisation
– performances & administration
a
https://dali.bo/workshop13_pdf

29
https://dalibo.com/formations
SQL pour PostgreSQL

Si nous essayons de voir cela avec de grosses mailles, les développements des versions 7
ciblaient les fondations d’un moteur de bases de données stable et durable. Ceux des ver-
sions 8 avaient pour but de rattraper les gros acteurs du marché en fonctionnalités et en
performances. Enfin, pour les versions 9, on est plutôt sur la réplication et l’extensibilité.

La version 10 se base principalement sur la parallélisation des opérations (développement


mené principalement par EnterpriseDB) et la réplication logique (par 2ndQuadrant). Les
versions 11 à 13 améliorent ces deux points, entre mille autres améliorations en différents
points du moteur, notamment les performances et la facilité d’administration.

1.4 QUELLE VERSION UTILISER ?

• 9.5 et inférieures
– Danger !
• 9.6 :
– planifier une migration urgemment !
• 10, 11 et 12
– mise à jour uniquement
• 13
– nouvelles installations et nouveaux développements

Si vous avez une version 9.5 ou inférieure, planifiez le plus rapidement possible une mi-
gration vers une version plus récente, comme la 10 ou la 11. La 9.5 n’est plus maintenue
depuis février 2021. Si vous utilisez cette version ou une version antérieure, il serait bon
de commencer à étudier une migration de version dès que possible.

Toute version 9.6 devrait faire l’objet d’une mise à jour majeure le plus rapidement possi-
ble.

Pour les versions 10, 11 et 12, le plus important est d’appliquer les mises à jour correc-
tives.

La version 13 est officiellement stable depuis plusieurs mois. Cette version est celle con-
seillée pour les nouvelles installations en production.

Par expérience, quand une version x.0 paraît à l’automne, elle est généralement stable.
Nombre de DBA préfèrent prudemment attendre les premières mises à jour mineures
pour la mise en production.
Pour plus de détails, voir le tableau comparatif des versionsa .
a
https://www.postgresql.org/about/featurematrix

30
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.5 VERSIONS DÉRIVÉES / FORKS

• Compatibilité Oracle :
– EnterpriseDB
• Data warehouse :
– Greenplum
– Netezza
– Amazon RedShift, Aurora
• Versions spécialisées :
– BDR (multi-maître, fermé)
• Packages avec des outils & support

Il existe de nombreuses versions dérivées de PostgreSQL. Elles sont en général destinées


à des cas d’utilisation très spécifiques. Leur code est souvent fermé et nécessite
l’acquisition d’une licence payante.

Des exemples connus sont Greenplum, de Pivotal, ou Netezza, d’IBM, dédiés aux en-
trepôts de données, dérivés d’anciennes versions de PostgreSQL. Greenplum fait l’effort
de se raccrocher au PostgreSQL communautaire toutes les quelques années, ce n’est pas
le cas de Netezza.

Amazon a la particularité de modifier profondément PostgreSQL pour l’adapter à son in-


frastructure, mais ne diffuse pas ses modifications.

Des versions existent qui tentent de combler certains manques. EDB Postgres Advanced
Server d’EnterpriseDB permet de faciliter la migration depuis Oracle (mais est soumis à
licence). BDR, de 2nd Quadrant, est un fork visant à fournir une version multimaître de
PostgreSQL, mais le code a été refermé dans les dernières versions. La société russe
Postgres Pro, tout comme EnterpriseDB, proposent diverses fonctionnalités dans leurs
versions propres, tout en proposant souvent leur inclusion dans la version communautaire
— ce qui n’est pas automatique.
Il existe une liste exhaustive des « forks »a , ainsi que cette conférence de Josh Berkusb
de 2009.

Sauf cas très précis, il est recommandé d’utiliser la version officielle, libre et gratuite.

a
https://wiki.postgresql.org/wiki/PostgreSQL_derived_databases
b
https://www.slideshare.net/pgconf/elephant-roads-a-tour-of-postgres-forks

31
https://dalibo.com/formations
SQL pour PostgreSQL

1.5.1 HISTORIQUE DES VERSIONS DÉRIVÉES

Voici un schéma des différentes versions de PostgreSQL ainsi que des versions dérivées.
Cela montre principalement l’arrivée annuelle d’une nouvelle version majeure, ainsi que
de la faible résistance des versions dérivées. La majorité n’a pas survécu à la vitalité du
développement de PostgreSQL.

En conséquence, les éditeurs ont plus tendance de nos jours à publier des extensions à
PostgreSQL (parfois payantes selon la version) plutôt que de procéder à des forks difficile-
ment maintenables (citus, timescaledb...).

1.6 QUELQUES PROJETS SATELLITES

• Administration
• Sauvegarde
• Supervision
• Migration
• SIG

PostgreSQL n’est qu’un moteur de bases de données. Quand vous l’installez, vous n’avez

32
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

que ce moteur. Vous disposez de quelques outils en ligne de commande (détaillés dans
nos modules « Outils graphiques et consoles » et « Tâches courantes ») mais aucun outil
graphique n’est fourni.

Du fait de ce manque, certaines personnes ont décidé de développer ces outils


graphiques. Ceci a abouti à une grande richesse grâce à la grande variété de projets
« satellites » qui gravitent autour du projet principal.

Par choix, nous ne présenterons ici que des logiciels libres et gratuits. Pour chaque problé-
matique, il existe aussi des solutions propriétaires. Ces solutions peuvent parfois apporter
des fonctionnalités inédites. Il faut néanmoins considérer que l’offre de la communauté
Open-Source répond à la plupart des besoins des utilisateurs de PostgreSQL.

1.6.1 ADMINISTRATION, DÉVELOPPEMENT, MODÉLISATION

Entre autres :
• Administration :
– pgAdmin4, temBoard
– OmniDB
• Développement :
– DBeaver
• Modélisation :
– pgModeler

Il existe différents outils pour l’administration, le développement et la modélisation. Une


liste plus exhaustive est disponible sur le wiki PostgreSQL17 .

pgAdmin418 est un outil d’administration dédié à PostgreSQL, qui permet aussi de re-
quêter. (La version 3 est considérée comme périmée.)

temBoard19 est une console d’administration plus complète.

DBeaver20 est un outil de requêtage courant, utilisable avec de nombreuses bases de


données différentes, et adapté à PostgreSQL.

Pour la modélisation, pgModeler21 est dédié à PostgreSQL. Il permet la modélisation, la


rétro-ingénierie d’un schéma existant, la génération de scripts de migration.
17
https://wiki.postgresql.org/wiki/Community_Guide_to_PostgreSQL_GUI_Tools
18
https://www.pgadmin.org/
19
https://temboard.io/
20
https://dbeaver.io/
21
https://pgmodeler.io/

33
https://dalibo.com/formations
SQL pour PostgreSQL

1.6.2 SAUVEGARDES

• Export logique :
– pg_backa
• Sauvegarde physique (PITR) :
– pitreryb , barmanc , pgBackRestd

Les outils listés ci-dessus sont les outils principaux pour la réalisation des sauvegardes et
la gestion de leur rétention. Certains permettent aussi la restauration. Ils se basent sur
les outils standards de PostgreSQL de sauvegarde physique ou logique.

1.6.3 SUPERVISION

• pgBadger
• PoWA
• check_pgactivity

Ce ne sont que trois outils parmi les très nombreux outils disponibles.

pgBadger22 est l’outil de base à avoir pour les analyses (à posteriori) des traces de Post-
greSQL.

PoWA23 est composé d’une extension qui historise les statistiques récupérées par
l’extension pg_stat_statements et d’une application web qui permet de récupérer les
requêtes et leur statistiques facilement.

check_pgactivity24 est une sonde Nagios pouvant récupérer un grand nombre de statis-
tiques d’activités renseignées par PostgreSQL. Il faut de ce fait un serveur Nagios (ou un
de ses nombreux forks ou surcharges) pour gérer les alertes et les graphes.

a
https://github.com/orgrim/pg_back/releases
b
https://dalibo.github.io/pitrery/
c
https://www.pgbarman.org
d
https://pgbackrest.org/
22
https://pgbadger.darold.net
23
https://powa.readthedocs.io/
24
https://github.com/OPMDG/check_pgactivity

34
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.6.4 MIGRATION

• Oracle, MySQL : ora2pg


• SQL Server : sqlserver2pgsql
• DB2 (UDB) : db2topg
• MySQL, SQL Server : pgloader

Il existe de nombreux outils pour migrer vers PostgreSQL une base de données utilisant un
autre moteur. Ce qui pose le plus problème en pratique est le code applicatif (procédures
stockées).

Ora2Pg25 , de Gilles Darold, convertit le schéma de données, migre les données, et tente
même de convertir le code PL/SQL en PL/pgSQL. Il convertit aussi des bases MySQL.

Dalibo soutient le développement de sqlserver2pgsql26 qui convertit un schéma de don-


nées sous MS SQL Server et génère les flux pour migrer les données avec l’ETL Pentaho
Data Integrator (Kettle). Le langage des procédures stockées est trop différent pour être
migré automatiquement.

db2topg27 génère les scripts de conversion et de migration depuis une base DB2 UDB
(mais pas zOS).

pgloader28 , de Dimitri Fontaine, permet de migrer depuis MySQL, SQLite ou MS SQL


Server, et importe les fichiers CSV, DBF (dBase) ou IXF (fichiers d’échange indépendants
de la base).

Ces outils sont libres. Des sociétés vivant de la prestation de service autour de la migra-
tion ont également souvent développé les leurs.

25
http://ora2pg.darold.net/
26
https://github.com/dalibo/sqlserver2pgsql
27
https://github.com/dalibo/db2topg
28
https://pgloader.io/

35
https://dalibo.com/formations
SQL pour PostgreSQL

1.6.5 SIG, POSTGIS

• https://postgis.net/
• Licence : BSD
• Module spatial pour PostgreSQL
• Conforme aux spécifications de l’OpenGIS Consortium

PostGIS ajoute le support d’objets géographiques à PostgreSQL. En fait, PostGIS trans-


forme un serveur PostgreSQL en serveur de données spatiales, qui sera utilisé par un Sys-
tème d’Information Géographique (SIG), tout comme le SDE de la société ESRI ou bien
l’extension Oracle Spatial. PostGIS se conforme aux directives du consortium OpenGIS et
a été certifié par cet organisme comme tel, ce qui est la garantie du respect des standards
par PostGIS.

PostGIS a été développé par la société Refractions Research comme une technologie
Open-Source de base de données spatiale. Cette société continue à développer PostGIS,
soutenue par une communauté active de contributeurs.

La version 2.0 apporte de nombreuses nouveautés attendues par les utilisateurs comme
le support des fonctionnalités raster et les surfaces tridimensionnelles.

La version 3.0 apporte la gestion du parallélisme, un meilleur support de l’indexation SP-


GiST et GiST, ainsi qu’un meilleur support du type GeoJSON.

36
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.7 SPONSORS & RÉFÉRENCES

• Sponsors
• Références :
– françaises
– et internationales

Au-delà de ses qualités, PostgreSQL suscite toujours les mêmes questions récurrentes :

• qui finance les développements ? (et pourquoi ?)


• qui utilise PostgreSQL ?

1.7.1 SPONSORS

• Sociétés se consacrant à PostgreSQL :


– Crunchy Data (Tom Lane, Stephen Frost, Joe Conway, Greg Smith)
– EnterpriseDB (Bruce Momjian, Robert Haas, Dave Page...)
– 2nd Quadrant (Simon Riggs, Peter Eisentraut...)
– PostgresPro (Oleg Bartunov, Alexander Korotkov)
– Cybertec (Autriche), Dalibo (France), Redpill Linpro (Suède)
• Société vendant un fork ou une extension :
– Citusdata, Pivotal (Heikki Linnakangas)
• Autres sociétés :
– VMWare, Rackspace, Heroku, Conova, Red Hat, Microsoft
– NTT (streaming Replication), Fujitsu, NEC
• Historique :
– Sun Microsystems (avant le rachat par Oracle)
– Microsoft Skype Division (projet skytools)

La liste des sponsors de PostgreSQL contribuant activement au développement figure sur


le site officiel29 . Ce qui suit n’est qu’un aperçu.

EnterpriseDB est une société américaine qui a décidé de fournir une version de Post-
greSQL propriétaire fournissant une couche de compatibilité avec Oracle. Ils emploient
plusieurs codeurs importants du projet PostgreSQL (dont deux font partie de la Core Team),
et reversent un certain nombre de leurs travaux au sein du moteur communautaire. Ils ont
aussi un poids financier qui leur permet de sponsoriser la majorité des grands événements
autour de PostgreSQL : PGEast et PGWest aux États-Unis, PGDay en Europe.
29
https://www.postgresql.org/about/sponsors/

37
https://dalibo.com/formations
SQL pour PostgreSQL

EnterpriseDB vient de racheter 2nd Quadrant, une société anglaise fondée par Simon
Riggs, développeur PostgreSQL de longue date. 2nd Quadrant développe de nombreux
outils autour de PostgreSQL comme pglogical, des versions dérivées comme Postgres-XL
ou BDR, dont le code se retrouve souvent dans la version communautaire après matura-
tion, ou des outils annexes comme barman ou repmgr.

Crunchy Data offre sa propre version certifiée et finance de nombreux développements.

De nombreuses autres sociétés dédiées à PostgreSQL existent dans de nombreux pays.


Parmi les sponsors officiels, nous pouvons compter Cybertec en Autriche ou Redpill Lin-
pro en Suède. En Russie, PostgresPro maintient une version locale et reverse aussi ses
contributions à la communauté.

En Europe francophone, Dalibo participe pleinement à la communauté. La société est


Major Sponsor du projet PostgreSQL30 , ce qui indique un support de longue date. Elle
développe et maintient plusieurs outils plébiscités par la communauté, comme autrefois
Open PostgreSQL Monitoring (OPM) ou la sonde check_pgactivity31 , plus récemment
la console d’administration temBoard32 , avec de nombreux autres projets en cours33 ,
et une participation active au développement de patchs pour PostgreSQL. Dalibo spon-
sorise également des événements comme les PGDay français et européens, ainsi que la
communauté francophone.

Des sociétés comme Citusdata et Pivotal proposent ou ont proposé leur version dérivée
mais « jouent le jeu » et participent au développement de la version communautaire, no-
tamment en cherchant à ce que leur produit n’en diverge pas.

Ont également contribué à PostgreSQL nombre de sociétés non centrées autour des
bases de données.

Entre 2006 et 2016, le système d’exploitation Unix Solaris 10 de Sun embarquait Post-
greSQL dans sa distribution de base, comme base de données de référence. Cela a pris
fin avec le rachat par Oracle, sans que cela ait représenté un danger pour PostgreSQL.

NTT a financé de nombreux patchs pour PostgreSQL, notamment liés à la réplication et


inclus dans la version de la communauté depuis la 9.034 .

Fujitsu a participé à de nombreux développements aux débuts de PostgreSQL.

VMWare a longtemps employé le développeur finlandais Heikki Linnakangas. Celui-ci


travaille à présent pour Pivotal mais VMWare peut compter sur Michael Paquier.
30
https://www.postgresql.org/about/sponsors/
31
https://github.com/OPMDG/check_pgactivity
32
https://temboard.io/
33
https://labs.dalibo.com/about
34
https://wiki.postgresql.org/wiki/Streaming_Replication

38
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

Red Hat a longtemps employé Tom Lane à plein temps pour travailler sur PostgreSQL. Il
a pu dédier une très grande partie de son temps de travail à ce projet, bien qu’il ait eu
d’autres affectations au sein de Red Hat. Tom Lane a travaillé également chez SalesForce,
ensuite il a rejoint Crunchy Data Solutions fin 2015.

Skype a offert un certain nombre d’outils très intéressants : pgBouncer (pooler de connex-
ion), Londiste (réplication par trigger), etc. Ce sont des outils utilisés en interne et publiés
sous licence BSD comme retour à la communauté. Le rachat par Microsoft n’a pas affecté
le développement de ces outils.

Des sociétés liées au cloud comme Conova (Autriche), Heroku ou Rackspace (États-Unis)
figurent aussi parmi les sponsors.

1.7.2 RÉFÉRENCES

• Météo France
• IGN
• RATP, SNCF, Autolib
• CNAF
• MAIF, MSA
• Le Bon Coin
• Doctolib
• Air France-KLM
• Société Générale
• Carrefour, Leclerc, Leroy Merlin
• Instagram, Zalando, TripAdvisor
• Yandex
• CNES
• ...et plein d’autres

Météo France utilise PostgreSQL depuis plus d’une décennie pour l’essentiel de ses bases,
dont des instances critiques de plusieurs téraoctets (témoignage sur postgresql.fr35 ).

L’IGN utilise PostGIS et PostgreSQL depuis 200636 .

La RATP a fait ce choix depuis 2007 également37 .

35
https://www.postgresql.fr/temoignages/meteo_france
36
http://postgis.refractions.net/documentation/casestudies/ign/
37
https://www.journaldunet.com/solutions/dsi/1013631-la-ratp-integre-postgresql-a-son-systeme-d-information/

39
https://dalibo.com/formations
SQL pour PostgreSQL

La Caisse Nationale d’Allocations Familiales a remplacé ses mainframes par des instances
PostgreSQL38 dès 2010 (4 To et 1 milliard de requêtes par jour).

Instagram utilise PostgreSQL depuis le début39 .

Zalando a décrit plusieurs fois son infrastructure PostgreSQL40 et annonçait en 201841


utiliser pas moins de 300 bases de données en interne et 650 instances dans un cloud
AWS. Zalando contribue à la communauté, notamment par son outil de haute disponibilité
patroni42 .

Le DBA de TripAdvisor témoigne de leur utilisation de PostgreSQL dans l’interview suiv-


ante43 .

Dès 2009, Leroy Merlin migrait vers PostgreSQL des milliers de logiciels de caisse44 .

Yandex, équivalent russe de Google a décrit en 2016 la migration des 300 To de données
de Yandex.Mail depuis Oracle vers PostgreSQL45 .

La Société Générale a publié son outil de migration d’Oracle à PostgreSQL46 .

Autolib à Paris utilisait PostgreSQL. Le logiciel est encore utilisé dans les autres villes où
le service continue. Ils ont décrit leur infrastructure au PG Day 2018 à Marseille47 .

De nombreuses autres sociétés participent au Groupe de Travail Inter-Entreprises de Post-


greSQLFr48 : Air France, Carrefour, Leclerc, le CNES, la MSA, la MAIF, PeopleDoc, EDF...

Cette liste ne comprend pas les innombrables sociétés qui n’ont pas communiqué sur
le sujet. PostgreSQL étant un logiciel libre, il n’existe nulle part de dénombrement des
instances actives.

38
https://www.silicon.fr/cnaf-debarrasse-mainframes-149897.html?inf_by=5bc488a1671db858728b4c35
39
https://media.postgresql.org/sfpug/instagram_sfpug.pdf
40
http://gotocon.com/dl/goto-berlin-2013/slides/HenningJacobs_and_ValentineGogichashvili_
WhyZalandoTrustsInPostgreSQL.pdf
41
https://www.postgresql.eu/events/pgconfeu2018/schedule/session/2135-highway-to-hell-or-stairway-to-cloud/
42
https://jobs.zalando.com/tech/blog/zalandos-patroni-a-template-for-high-availability-postgresql/
43
https://www.citusdata.com/blog/25-terry/285-matthew-kelly-tripadvisor-talks-about-pgconf-silicon-valley
44
https://wiki.postgresql.org/images/6/63/Adeo_PGDay.pdf
45
https://www.pgcon.org/2016/schedule/attachments/426_2016.05.19%20Yandex.Mail%20success%20story.pdf
46
https://github.com/societe-generale/code2pg
47
https://www.youtube.com/watch?v=vd8B7B-Zca8
48
https://www.postgresql.fr/entreprises/accueil

40
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.7.3 LE BON COIN

• Site de petites annonces


• 4è site le plus consulté en France (2017)
• 27 millions d’annonces en ligne, 800 000 nouvelles chaque jour
• Instance PostgreSQL principale : 3 To de volume, 3 To de RAM
• 20 serveurs secondaires

PostgreSQL tient la charge sur de grosses bases de données et des serveurs de grande
taille.

Le Bon Coin privilégie des serveurs physiques dans ses propres datacenters.

Pour plus de détails et l’évolution de la configuration, voir les témoignages de ses di-
recteurs technique49 (témoignage de juin 2012) et infrastructure50 (juin 2017) , ou la
conférence de son DBA Flavio Gurgel au pgDay Paris 201951 .

Ce dernier s’appuie sur les outils classiques fournis par la communauté : pg_dump (pour
archivage, car ses exports peuvent être facilement restaurés), barman, pg_upgrade.

1.7.4 DOCTOLIB

• Site de rendez-vous médicaux en ligne


• Base transactionnelle de 3 To (2018) :
– SSD chiffrés, full NVMe, RAID 50
• 25 millions de visites/mois
• pointes à 40 000 commits/s
• 6 serveurs répartis sur 2 sites avec réplication en streaming
• Primaire avec 4x18 cœurs (144 threads), 512 Go de RAM
• Scaling horizontal sur un seul secondaire

Doctolib est le site de rendez-vous médicaux en ligne dominant sur le marché français,
couvrant 65 000 professionnels de santé, 1300 cliniques, hôpitaux et centres de santé,
et des millions de patients.

Une seule instance primaire PostgreSQL suffit, sans partitionnement, épaulée par une
instance secondaire qui assure une fraction du trafic web, et 4 autres secondaires pour
d’autres tâches.
49
https://www.postgresql.fr/temoignages:le_bon_coin
50
https://www.kissmyfrogs.com/jean-louis-bergamo-leboncoin-ce-qui-a-ete-fait-maison-est-ultra-performant/
51
https://www.postgresql.eu/events/pgdayparis2019/schedule/session/2376-large-databases-lots-of-servers

41
https://dalibo.com/formations
SQL pour PostgreSQL

Le RAID50 est un compromis entre le RAID 5 (pénalisant pour les écritures à cause du
calcul de parité) et le RAID 10 (trop consommateur d’espace disque).

1.8 À LA RENCONTRE DE LA COMMUNAUTÉ

• Cartographie du projet
• Pourquoi participer
• Comment participer

1.8.1 POSTGRESQL, UN PROJET MONDIAL

Figure 1: Carte des hackers

On le voit, PostgreSQL compte des contributeurs sur tous les continents !

Quelques faits :

• le projet est principalement anglophone ;


• il existe une très grande communauté au Japon ;

42
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

• la communauté francophone est très dynamique mais nous trouvons très peu de
développeurs francophones ;
• la communauté hispanophone est naissante ;
• les développeurs du noyau (Core Hackers) vivent en Europe, au Japon, en Russie et
en Amérique du Nord.

1.8.2 POSTGRESQL CORE TEAM

Le terme Core Hackers désigne les personnes qui sont dans la communauté depuis
longtemps. Ces personnes désignent directement les nouveaux membres.
Le terme hacker peut porter à confusion, il s’agit ici de la définition « universitaire » :
https://fr.wikipedia.org/wiki/Hacker_(programmation)

La Core Team est un ensemble de personnes doté d’un pouvoir assez limité. Ils peuvent
décider de la date de sortie d’une version. Ce sont les personnes qui sont immédiatement
au courant des failles de sécurité du serveur PostgreSQL. Tout le reste des décisions est
pris par la communauté dans son ensemble après discussion, généralement sur la liste
pgsql-hackers.

Les membres actuels de la Core Team sont52 :


52
https://www.postgresql.org/community/contributors/

43
https://dalibo.com/formations
SQL pour PostgreSQL

• Peter Eisentraut, EDB, Dresden (Allemagne) - développement du moteur (interna-


tionalisation, SQL/Med…), etc. ;
• Tom Lane, Crunchy Data, Pittsburgh (États-Unis) — certainement le développeur le
plus aguerri avec la vision la plus globale ;
• Bruce Momjian, EnterpriseDB, Philadelphie (États-Unis) - a lancé le projet, fait à
présent principalement de la promotion ;
• Dave Page, EnterpriseDB, Oxfordshire (Angleterre) - leader du projet pgAdmin, ad-
ministration des serveurs, secrétaire de PostgreSQL Europe ;
• Magnus Hagander, Redpill Linpro, Stockholm (Suède) - développeur (a participé au
portage Windows), administration des serveurs, président de PostgreSQL Europe ;
• Andres Freund, Citusdata (Microsoft), San Francisco (États-Unis) — contributeur
depuis des années de nombreuses fonctionnalités (JIT, réplication logique, perfor-
mances…) ;
• Jonathan Katz, Crunchy Data, New York (États-Unis) — promotion du projet, mod-
ération, revues de patchs.

1.8.3 CONTRIBUTEURS

Actuellement, PostgreSQL compte une centaine de « contributeurs » qui se répartissent

44
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

quotidiennement les tâches suivantes :

• développement des projets satellites (Slony, pgAdmin...) ;


• promotion du logiciel ;
• administration des serveurs ;
• rédaction de documentation ;
• conférences ;
• organisation de groupes locaux.

Le PGDG a fêté son 10e anniversaire à Toronto en juillet 2006. Ce « PostgreSQL Anniver-
sary Summit » a réuni pas moins de 80 membres actifs du projet. La photo ci-dessus a été
prise à l’occasion.

PGCon2009 a réuni 180 membres actifs à Ottawa, et environ 220 en 2018 et 2019.

Voir la liste des contributeurs officiels53 .

1.8.4 QUI CONTRIBUE DU CODE ?

• Principalement des personnes payées par leur société


• En 2019, en code :
– Tom Lane
– Andres Freund
– Peter Eisentraut
– Nikita Glukhov
– Alvaro Herrera
– ...et 184 autres
• Et en mails sur pgsql-hackers :
– Tom Lane
– Michael Paquier
– Andres Freund
– Alvaro Herrera
– Robert Haas

Robert Haas publie chaque année une analyse sur les contributeurs de code et les partic-
ipants aux discussions sur le développement de PostgreSQL sur la liste pgsql-hackers.

Voici ses différentes analyses par année :

• 2019 : http://rhaas.blogspot.com/2020/05/who-contributed-to-postgresql.html
• 2018 : http://rhaas.blogspot.com/2019/01/who-contributed-to-postgresql.html
53
https://www.postgresql.org/community/contributors/

45
https://dalibo.com/formations
SQL pour PostgreSQL

• 2017 : http://rhaas.blogspot.com/2018/06/who-contributed-to-postgresql.html
• 2016 : http://rhaas.blogspot.com/2017/04/who-contributes-to-postgresql.html

1.8.5 UTILISATEURS

• Vous !
• Le succès d’un logiciel libre dépend de ses utilisateurs.

Il est impossible de connaître précisément le nombre d’utilisateurs de PostgreSQL. Toute-


fois ce nombre est en constante augmentation.

Il existe différentes manières de s’impliquer dans une communauté Open-Source. Dans


le cas de PostgreSQL, vous pouvez :

• déclarer un bug ;
• tester les versions bêta ;
• témoigner.

1.8.6 POURQUOI PARTICIPER

• Rapidité des corrections de bugs


• Préparer les migrations / tester les nouvelles versions
• Augmenter la visibilité du projet
• Créer un réseau d’entraide

Au-delà de motivations idéologiques ou technologiques, il y a de nombreuses raisons


objectives de participer au projet PostgreSQL.

Envoyer une description d’un problème applicatif aux développeurs est évidemment
le meilleur moyen d’obtenir sa correction. Attention toutefois à être précis et complet
lorsque vous déclarez un bug sur pgsql-bugs54 ! Assurez-vous que vous pouvez le
reproduire.

Tester les versions « candidates » dans votre environnement (matériel et applicatif) est
la meilleure garantie que votre système d’information sera compatible avec les futures
versions du logiciel.

54
https://www.postgresql.org/list/pgsql-bugs/

46
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

Les retours d’expérience et les cas d’utilisations professionnelles sont autant de preuves
de la qualité de PostgreSQL. Ces témoignages aident de nouveaux utilisateurs à opter
pour PostgreSQL, ce qui renforce la communauté.

S’impliquer dans les efforts de traductions, de relecture ou dans les forums d’entraide
ainsi que toute forme de transmission en général est un très bon moyen de vérifier et
d’approfondir ses compétences.

1.8.7 RESSOURCES WEB DE LA COMMUNAUTÉ

• Site officiel : https://www.postgresql.org/


• Actualité : https://planet.postgresql.org/
• Des extensions : https://pgxn.org/

Le site officiel de la communauté se trouve sur https://www.postgresql.org/. Ce site


contient des informations sur PostgreSQL, la documentation des versions maintenues,
les archives des listes de discussion, etc.

Le site « Planet PostgreSQL » est un agrégateur réunissant les blogs des Core Hackers, des
contributeurs, des traducteurs et des utilisateurs de PostgreSQL.

Le site PGXN est l’équivalent pour PostgreSQL du CPAN de Perl, une collection en ligne
de librairies et extensions accessibles depuis la ligne de commande.

1.8.8 DOCUMENTATION OFFICIELLE

• LA référence, même au quotidien


• Anglais : https://www.postgresql.org/docs/
• Français : https://docs.postgresql.fr/

La documentation officielle sur https://www.postgresql.org/docs/current est maintenue


au même titre que le code du projet, et sert aussi au quotidien, pas uniquement pour des
cas obscurs.

Elle est versionnée pour chaque version majeure.

La traduction française suit de près les mises à jour de la documentation officielle : https:
//docs.postgresql.fr/.

47
https://dalibo.com/formations
SQL pour PostgreSQL

1.8.9 SERVEURS FRANCOPHONES

• Site officiel : https://www.postgresql.fr/


• Documentation traduite : https://docs.postgresql.fr/
• Forum : https://forums.postgresql.fr/
• Actualité : https://planete.postgresql.fr/
• Association PostgreSQLFr : https://www.postgresql.fr/asso/accueil
• Groupe de Travail Inter-Entreprises (PGGTIE) : https://www.postgresql.fr/
entreprises/accueil

Le site postgresql.fr est le site de l’association des utilisateurs francophones du logiciel.


La communauté francophone se charge de la traduction de toutes les documentations.

1.8.10 LISTES DE DISCUSSIONS / LISTES D'ANNONCES

• pgsql-announce
• pgsql-general
• pgsql-admin
• pgsql-sql
• pgsql-performance
• pgsql-fr-generale
• pgsql-advocacy
• pgsql-bugs

Les mailing-lists sont les outils principaux de gouvernance du projet. Toute l’activité de
la communauté (bugs, promotion, entraide, décisions) est accessible par ce canal. Les
développeurs principaux du projets répondent parfois eux-mêmes. Si vous avez une ques-
tion ou un problème, la réponse se trouve probablement dans les archives !
Pour s’inscrire ou consulter les archives : https://www.postgresql.org/list/.
Si vous pensez avoir trouvé un bug, vous pouvez le remonter sur la liste anglophone
pgsql-bugsa , par le formulaire dédiéb . Pour faciliter la tâche de ceux qui tenteront
de vous répondre, suivez bien les consignes sur les rapports de bugc : informations
complètes, reproductibilité…
Les listes de diffusion sont régies par des règles de politesse et de bonne conduite. Avant
de poser une question, nous vous conseillons de consulter le guide suivant : http://www.
linux-france.org/article/these/smart-questions/smart-questions-fr.html
a
https://www.postgresql.org/list/pgsql-bugs/
b
https://www.postgresql.org/account/submitbug/
c
https://docs.postgresql.fr/current/bogue-reporting.html

48
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.8.11 IRC

• Réseau Freenode
• IRC anglophone :
– #postgresql
– #postgresql-eu
• IRC francophone :
– #postgresqlfr

Le point d’entrée principal pour le réseau Freenode est le serveur irc.freenode.net. La


majorité des développeurs sont disponibles sur IRC et peuvent répondre à vos questions.

Des canaux de discussion spécifiques à certains projets connexes sont également


disponibles, comme par exemple #slony.
Attention ! Vous devez poser votre question en public et ne pas solliciter de l’aide par
message privé.

1.8.12 WIKI

• https://wiki.postgresql.org/

Le wiki est un outil de la communauté qui met à disposition une véritable mine
d’informations.

Au départ, le wiki avait pour but de récupérer les spécifications écrites par des
développeurs pour les grosses fonctionnalités à développer à plusieurs. Cependant, peu
de développeurs l’utilisent dans ce cadre. L’utilisation du wiki a changé en passant plus
entre les mains des utilisateurs qui y intègrent un bon nombre de pages de documenta-
tion (parfois reprises dans la documentation officielle). Le wiki est aussi utilisé par les
organisateurs d’événements pour y déposer les slides des conférences. Elle n’est pas
exhaustive et, hélas, souffre fréquemment d’un manque de mises à jour.

49
https://dalibo.com/formations
SQL pour PostgreSQL

1.8.13 L'AVENIR DE POSTGRESQL

• PostgreSQL 13 est sortie en septembre 2020


• Grandes orientations :
– réplication logique
– meilleur parallélisme
– gros volumes
• Prochaine version, la 14
• Stabilité économique
• De plus en plus de (gros) clients
• Le futur de PostgreSQL dépend de vous !

Le projet avance grâce à de plus en plus de contributions. Les grandes orientations


actuelles sont :

• une réplication de plus en plus sophistiquée ;


• une gestion plus étendue du parallélisme ;
• une volumétrie acceptée de plus en plus importante ;
• etc.

PostgreSQL est là pour durer. Le nombre d’utilisateurs, de toutes tailles, augmente tous
les jours. Il n’y a pas qu’une seule entreprise derrière ce projet. Il y en a plusieurs, pe-
tites et grosses sociétés, qui s’impliquent pour faire avancer le projet, avec des modèles
économiques et des marchés différents, garants de la pérennité du projet.

1.9 CONCLUSION

• Un projet de grande ampleur


• Un SGBD complet
• Souplesse, extensibilité
• De belles références
• Une solution stable, ouverte, performante et éprouvée
• Pas de dépendance envers UN éditeur

Certes, la licence PostgreSQL implique un coût nul (pour l’acquisition de la licence), un


code source disponible et aucune contrainte de redistribution. Toutefois, il serait erroné
de réduire le succès de PostgreSQL à sa gratuité.

Beaucoup d’acteurs font le choix de leur SGBD sans se soucier de son prix. En
l’occurrence, ce sont souvent les qualités intrinsèques de PostgreSQL qui séduisent :

50
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

• sécurité des données (reprise en cas de crash et résistance aux bogues applicatifs) ;
• facilité de configuration ;
• montée en puissance et en charge progressive ;
• gestion des gros volumes de données ;
• pas de dépendance envers un unique éditeur ou prestataire.

1.9.1 BIBLIOGRAPHIE

• Documentation officielle (préface)


• Articles fondateurs de M. Stonebracker
• Présentation du projet PostgreSQL

« Préface : 2. Bref historique de PostgreSQL55 ». PGDG, 2013

« The POSTGRES™ data model56 ». Rowe and Stonebraker, 1987

« Présentation du projet PostgreSQL57 », Guillaume Lelarge, RMLL 2008

Iconographie : La photo initiale est le logo officiel de PostgreSQL58 .

1.9.2 QUESTIONS

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

55
https://docs.postgresql.fr/current/history.html
56
http://db.cs.berkeley.edu/papers/ERL-M85-95.pdf
57
http://2008.rmll.info/IMG/pdf/presentationPG.pdf
58
https://www.postgresql.org/about/policies/trademarks/

51
https://dalibo.com/formations
SQL pour PostgreSQL

1.10 ANNEXE : INSTALLATION DE POSTGRESQL DEPUIS LES PA-


QUETS COMMUNAUTAIRES

L’installation est détaillée ici pour Red Hat/CentOS 7 et 8, et Debian/Ubuntu.

Elle ne dure que quelques minutes.

1.10.1 SUR RED HAT 7 / CENT OS 7

Installation du dépôt communautaire :

Sauf précision, tout est à effectuer en tant qu’utilisateur root.


ATTENTION : Red Hat et CentOS 6 et 7 fournissent par défaut des versions de Post-
greSQL qui ne sont plus supportées. Ne jamais installer les packages postgresql,
postgresql-client et postgresql-server !

Les dépôts de la communauté sont sur https://yum.postgresql.org/. Les commandes


qui suivent peuvent être générées par l’assistant sur : https://www.postgresql.org/
download/linux/redhat/, en précisant :

• la version majeure de PostgreSQL (ici la 13) ;


• la distribution (ici CentOS 7) ;
• l’architecture (ici x86_64, la plus courante).

# yum install https://download.postgresql.org/pub/repos/yum/reporpms\


/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm

Installation de PostgreSQL 13 :

# yum install postgresql13-server postgresql13-contrib

Tout à fait optionnellement, une fonctionnalité avancée, le JIT (Just In Time compilation),
nécessite un paquet séparé, qui lui-même nécessite des paquets du dépôt EPEL :

# yum install epel-release


# yum install postgresql13-llvmjit

Création d’une première instance :

Il est conseillé de déclarer PG_SETUP_INITDB_OPTIONS, notamment pour mettre en place


les checksums et forcer les traces en anglais :

# export PGSETUP_INITDB_OPTIONS='--data-checksums --lc-messages=C'


# /usr/pgsql-13/bin/postgresql-13-setup initdb
# cat /var/lib/pgsql/13/initdb.log

52
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

Ce dernier fichier permet de vérifier que tout s’est bien passé.

Chemins :

Objet Chemin
Binaires /usr/pgsql-13/bin
Répertoire de l’utilisateur postgres /var/lib/pgsql
PGDATA par défaut /var/lib/pgsql/13/data
Fichiers de configuration dans PGDATA/
Traces dans PGDATA/log

Configuration :

Modifier postgresql.conf est facultatif pour un premier essai.

Démarrage/arrêt de l’instance, rechargement de configuration :

# systemctl start postgresql-13


# systemctl stop postgresql-13
# systemctl reload postgresql-13

Test rapide de bon fonctionnement

# systemctl --all |grep postgres


# sudo -iu postgres psql

Démarrage de l’instance au démarrage du système d’exploitation :

# systemctl enable postgresql-13

Consultation de l’état de l’instance :

# systemctl status postgresql-13

Ouverture du firewall pour le port 5432 :

Si le firewall est actif (dans le doute, consulter systemctl status firewalld) :

# firewall-cmd --zone=public --add-port=5432/tcp --permanent


# firewall-cmd --reload
# firewall-cmd --list-all

Création d’autres instances :

Si des instances de versions majeures différentes doivent être installées, il faudra installer
les binaires pour chacune, et l’instance par défaut de chaque version vivra dans un sous-
répertoire différent de /var/lib/pgsql automatiquement créé à l’installation. Il faudra
juste modifier les ports dans les postgresql.conf.
53
https://dalibo.com/formations
SQL pour PostgreSQL

Si plusieurs instances d’une même version majeure (forcément de la même version


mineure) doivent cohabiter sur le même serveur, il faudra les installer dans des PGDATA
différents.

• Ne pas utiliser de tiret dans le nom d’une instance (problèmes potentiels avec sys-
temd).

• Respecter les normes et conventions de l’OS : placer les instances dans un


sous-répertoire de /var/lib/pgsqsl/13/ (ou l’équivalent pour d’autres versions
majeures).

• Création du fichier service de la deuxième instance :

# cp /lib/systemd/system/postgresql-13.service \
/etc/systemd/system/postgresql-13-secondaire.service

• Modification du fichier avec le nouveau chemin :


Environment=PGDATA=/var/lib/pgsql/13/secondaire

• Option 1 : création d’une nouvelle instance vierge :

# /usr/pgsql-13/bin/postgresql-13-setup initdb postgresql-13-secondaire

• Option 2 : restauration d’une sauvegarde : la procédure dépend de votre outil.

• Adaptation de postgresql.conf (port !), recovery.conf...

• Commandes de maintenance :

# systemctl [start|stop|reload|status] postgresql-13-secondaire


# systemctl [enable|disable] postgresql-13-secondaire

• Ouvrir un port dans le firewall au besoin.

1.10.2 SUR RED HAT 8 / CENT OS 8

Fondamentalement, le principe reste le même qu’en version 7. Il faudra utiliser dnf plutôt
que yum, impérativement désactiver le module PostgreSQL par défaut, et il est inutile
d’installer un dépôt EPEL :

# dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms\


/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
# dnf -qy module disable postgresql
# dnf install -y postgresql13-server postgresql13-contrib postgresql13-llvmjit

La création de l’instance et la suite sont identiques à Red Hat/CentOS 7.

54
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.10.3 SUR DEBIAN / UBUNTU

Sauf précision, tout est à effectuer en tant qu’utilisateur root.

Installation du dépôt communautaire :

Référence : https://apt.postgresql.org/

• Import des certificats et de la clé :

# apt install curl ca-certificates gnupg


# curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -

• Création du fichier du dépôt /etc/apt/sources.list.d/pgdg.list (ici pour De-


bian 10 « buster » ; adapter au nom de code de la version de Debian ou Ubuntu
correspondante : stretch, bionic, focal…) :

deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main"

Installation de PostgreSQL 13 :

# apt update
# apt install postgresql-13 postgresql-client-13

(Pour les versions 9.x, installer aussi le paquet postgresql-contrib-9.x).

La première instance est directement créée, démarrée et déclarée comme service à lancer
au démarrage du système.

Chemins :

Objet Chemin
Binaires /usr/lib/postgresql/13/bin/
Répertoire de l’utilisateur postgres /var/lib/postgresql
PGDATA de l’instance par défaut /var/lib/postgresql/13/main
Fichiers de configuration dans /etc/postgresql/13/main/
Traces dans /var/log/postgresql/

Configuration

Modifier postgresql.conf est facultatif pour un premier essai.

Démarrage/arrêt de l’instance, rechargement de configuration :

Debian fournit ses propres outils :

# pg_ctlcluster 13 main [start|stop|reload|status]

Démarrage de l’instance au lancement :


55
https://dalibo.com/formations
SQL pour PostgreSQL

C’est en place par défaut, et modifiable dans /etc/postgresql/13/main/start.conf.

Ouverture du firewall :

Debian et Ubuntu n’installent pas de firewall par défaut.

Statut des instances :

# pg_lsclusters

Test rapide de bon fonctionnement

# systemctl --all |grep postgres


# sudo -iu postgres psql

Destruction d’une instance :

# pg_dropcluster 13 main

Création d’autres instances :

Ce qui suit est valable pour remplacer l’instance par défaut par une autre, par exemple
pour mettre les checksums en place :

• les paramètres de création d’instance dans /etc/postgresql-common/createcluster.conf


peuvent être modifiés, par exemple ici pour : les checksums, les messages en anglais,
l’authentification sécurisée, le format des traces et un emplacement séparé pour
les journaux :
initdb_options = '--data-checksums --lc-messages=C --auth-host=scram-sha-256 --auth-local=peer'
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
waldir = '/var/lib/postgresql/wal/%v/%c/pg_wal'

• création de l’instance, avec possibilité là aussi de préciser certains paramètres du


postgresql.conf voire de modifier les chemins des fichiers (déconseillé si vous
pouvez l’éviter) :

# pg_createcluster 13 secondaire \
--port=5433 \
--datadir=/PGDATA/11/basedecisionnelle \
--pgoption shared_buffers='8GB' --pgoption work_mem='50MB' \
-- --data-checksums --waldir=/ssd/postgresql/11/basedecisionnelle/journaux

• démarrage :

# pg_ctlcluster 13 secondaire start

56
1. POSTGRESQL : HISTORIQUE & COMMUNAUTÉ

1.10.4 ACCÈS À L'INSTANCE

Par défaut, l’instance n’est accessible que par l’utilisateur système postgres, qui n’a pas
de mot de passe. Un détour par sudo est nécessaire :

$ sudo -iu postgres psql


psql (13.0)
Saisissez « help » pour l'aide.
postgres=#

Ce qui suit permet la connexion directement depuis un utilisateur du système :

Pour des tests (pas en production !), il suffit de passer à trust le type de la connexion en
local dans le pg_hba.conf :

local all postgres trust

La connexion en tant qu’utilisateur postgres (ou tout autre) n’est alors plus sécurisée :

dalibo:~$ psql -U postgres


psql (13.0)
Saisissez « help » pour l'aide.
postgres=#

Une authentification par mot de passe est plus sécurisée :

• dans pg_hba.conf, mise en place d’une authentification par mot de passe (md5 par
défaut) pour les accès à localhost :

# IPv4 local connections:


host all all 127.0.0.1/32 md5
# IPv6 local connections:
host all all ::1/128 md5

(une authentification scram-sha-256 est plus conseillée mais elle impose que
password_encryption soit à cette valeur dans postgresql.conf avant de définir
les mots de passe).

• ajout d’un mot de passe à l’utilisateur postgres de l’instance ;

dalibo:~$ sudo -iu postgres psql


psql (13.0)
Saisissez « help » pour l'aide.
postgres=# \password
Saisissez le nouveau mot de passe :
Saisissez-le à nouveau :
postgres=# \q
57
https://dalibo.com/formations
SQL pour PostgreSQL

dalibo:~$ psql -h localhost -U postgres


Mot de passe pour l'utilisateur postgres :
psql (13.0)
Saisissez « help » pour l'aide.
postgres=#

• pour se connecter sans taper le mot de passe, un fichier .pgpass dans le répertoire
personnel doit contenir les informations sur cette connexion :

localhost:5432:*:postgres:motdepassetrèslong

• ce fichier doit être protégé des autres utilisateurs :

$ chmod 600 ~/.pgpass

• pour n’avoir à taper que psql, on peut définir ces variables d’environnement dans
la session voire dans ~/.bashrc :

export PGUSER=postgres
export PGDATABASE=postgres
export PGHOST=localhost

Rappels :

• en cas de problème, consulter les traces (dans /var/lib/pgsql/13/data/log ou


/var/log/postgresql/) ;
• toute modification de pg_hba.conf implique de recharger la configuration par une
de ces trois méthodes selon le système :

root:~# systemctl reload postgresql-13

root:~# pg_ctlcluster 13 main reload

postgres:~$ psql -c 'SELECT pg_reload_conf();'

58
2. DÉCOUVERTE DES FONCTIONNALITÉS

2 DÉCOUVERTE DES FONCTIONNALITÉS

59
https://dalibo.com/formations
SQL pour PostgreSQL

2.1 AU MENU

• Fonctionnalités du moteur
• Objets SQL
• Connaître les différentes fonctionnalités et possibilités
• Découvrir des exemples concrets

Ce module propose un tour rapide des fonctionnalités principales du moteur : ACID,


MVCC, transactions, journaux de transactions… ainsi que des objets SQL gérés (schémas,
index, tablespaces, triggers…). Ce rappel des concepts de base permet d’avancer plus
facilement lors des modules suivants.

2.2 FONCTIONNALITÉS DU MOTEUR

• Standard SQL
• Gestion transactionnelle
• Niveaux d’isolation
• Journaux de transactions
• Administration
• Sauvegardes
• Réplication
• Supervision
• Sécurité
• Extensibilité

Cette partie couvre les différentes fonctionnalités d’un moteur de bases de données. Il
ne s’agit pas d’aller dans le détail de chacune, mais de donner une idée de ce qui est
disponible. Les modules suivants de cette formation et des autres formations détaillent
certaines de ces fonctionnalités.

60
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.2.1 RESPECT DU STANDARD SQL

• Excellent support du SQL ISO


• Objets SQL
– tables, vues, séquences, routines, triggers
• Opérations
– jointures, sous-requêtes, requêtes CTE, requêtes de fenêtrage, etc.

La dernière version du standard SQL est SQL:2016.

À ce jour, aucun SGBD ne supporte complètement SQL:2016 mais :

• PostgreSQL progresse et s’en approche au maximum, au fil des versions ;


• la majorité de SQL:2016 est supportée, parfois avec des syntaxes différentes ;
• PostgreSQL est le SGDB le plus respectueux du standard.

2.2.2 GESTION TRANSACTIONNELLE

• Atomicité (Atomic)
• Cohérence (Consistency)
• Isolation
• Durabilité (Durability)

Les propriétés ACID sont le fondement même de tout système transactionnel. Il s’agit de
quatre règles fondamentales :

• A : Une transaction est entière : « tout ou rien » ;


• C : Une transaction amène le système d’un état stable à un autre ;
• I : Les transactions n’agissent pas les unes sur les autres ;
• D : Une transaction validée provoque des changements permanents.

Les propriétés ACID sont quatre propriétés essentielles d’un sous-système de traitement
de transactions d’un système de gestion de base de données. Certains SGBD ne four-
nissent pas les garanties ACID. C’est le cas de la plupart des SGBD non-relationnels
(« NoSQL »). Cependant, la plupart des applications ont besoin de telles garanties et la
décision d’utiliser un système ne garantissant pas ces propriétés ne doit pas être prise à
la légère.

61
https://dalibo.com/formations
SQL pour PostgreSQL

2.2.3 MVCC

• MultiVersion Concurrency Control


• Le « noyau » de PostgreSQL
• Garantit les propriétés ACID
• Permet les accès concurrents sur la même table
– une lecture ne bloque pas une écriture
– une écriture ne bloque pas une lecture
– une écriture ne bloque pas les autres écritures...
– ...sauf pour la mise à jour de la même ligne

MVCC (Multi Version Concurrency Control) est le mécanisme interne de PostgreSQL util-
isé pour garantir la cohérence des données lorsque plusieurs processus accèdent simul-
tanément à la même table.

MVCC maintient toutes les versions nécessaires de chaque ligne, ainsi chaque transaction
voit une image figée de la base (appelée snapshot). Cette image correspond à l’état de la
base lors du démarrage de la requête ou de la transaction, suivant le niveau d’isolation
demandé par l’utilisateur à PostgreSQL pour la transaction.

MVCC fluidifie les mises à jour en évitant les blocages trop contraignants (verrous sur
UPDATE) entre sessions et par conséquent de meilleures performances en contexte trans-
actionnel.

C’est notamment MVCC qui permet d’exporter facilement une base à chaud et d’obtenir
un export cohérent alors même que plusieurs utilisateurs sont potentiellement en train
de modifier des données dans la base.

C’est la qualité de l’implémentation de ce système qui fait de PostgreSQL un des meilleurs


SGBD au monde : chaque transaction travaille dans son image de la base, cohérent du
début à la fin de ses opérations. Par ailleurs, les écrivains ne bloquent pas les lecteurs
et les lecteurs ne bloquent pas les écrivains, contrairement aux SGBD s’appuyant sur des
verrous de lignes. Cela assure de meilleures performances, moins de contention et un
fonctionnement plus fluide des outils s’appuyant sur PostgreSQL.

62
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.2.4 TRANSACTIONS

• Une transaction = ensemble atomique d’opérations


• « Tout ou rien »
• BEGIN obligatoire pour grouper des modifications
• COMMIT pour valider
– y compris le DDL
• Perte des modifications si :
– ROLLBACK / perte de la connexion / arrêt (brutal ou non) du serveur
• SAVEPOINT pour sauvegarde des modifications d’une transaction à un instant t
• Pas de transactions imbriquées

Voici un exemple de transaction :

BEGIN;
CREATE TABLE capitaines (id serial, nom text, age integer);
INSERT INTO capitaines VALUES (1, 'Haddock', 35);

SELECT age FROM capitaines;

age
-----
35

ROLLBACK;
SELECT age FROM capitaines;

ERROR: relation "capitaines" does not exist


LINE 1: SELECT age FROM capitaines;
^

Nous voyons que la table capitaines a existé à l’intérieur de la transaction. Mais puisque
cette transaction a été annulée (ROLLBACK), la table n’a pas été créée au final.

Cela montre aussi le support du DDL transactionnel au sein de PostgreSQL : PostgreSQL


n’effectue aucun COMMIT implicite sur des ordre DDL tels que CREATE TABLE, DROP TABLE
ou TRUNCATE TABLE. De ce fait, ces ordres peuvent être annulés au sein d’une transaction.

Un point de sauvegarde est une marque spéciale à l’intérieur d’une transaction qui au-
torise l’annulation de toutes les commandes exécutées après son établissement, restau-
rant la transaction dans l’état où elle était au moment de l’établissement du point de sauve-
garde.

BEGIN;
CREATE TABLE capitaines (id serial, nom text, age integer);
INSERT INTO capitaines VALUES (1, 'Haddock', 35);
SAVEPOINT insert_sp;
63
https://dalibo.com/formations
SQL pour PostgreSQL

UPDATE capitaines SET age = 45 WHERE nom = 'Haddock';


ROLLBACK TO SAVEPOINT insert_sp;
COMMIT;

SELECT age FROM capitaines WHERE nom = 'Haddock';

age
-----
35

Malgré le COMMIT après l’UPDATE, la mise à jour n’est pas prise en compte. En effet, le
ROLLBACK TO SAVEPOINT a permis d’annuler cet UPDATE mais pas les opérations précé-
dant le SAVEPOINT.

À partir de la version 12, il est possible de chaîner les transactions avec COMMIT AND
CHAIN ou ROLLBACK AND CHAIN. Cela veut dire terminer une transaction et en démar-
rer une autre immédiatement après avec les mêmes propriétés (par exemple, le niveau
d’isolation).

2.2.5 NIVEAUX D'ISOLATION

• Chaque transaction (et donc session) est isolée à un certain point


– elle ne voit pas les opérations des autres
– elle s’exécute indépendamment des autres
• Nous pouvons spécifier le niveau d’isolation au démarrage d’une transaction
– BEGIN ISOLATION LEVEL xxx;
• Niveaux d’isolation supportés
– read commited (défaut)
– repeatable read
– serializable

Chaque transaction, en plus d’être atomique, s’exécute séparément des autres. Le niveau
de séparation demandé sera un compromis entre le besoin applicatif (pouvoir ignorer sans
risque ce que font les autres transactions) et les contraintes imposées au niveau de Post-
greSQL (performances, risque d’échec d’une transaction).

Le standard SQL spécifie quatre niveaux, mais PostgreSQL n’en supporte que trois (il n’y a
pas de read uncommitted : les lignes non encore committées par les autres transactions
sont toujours invisibles).

64
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.2.6 FIABILITÉ : JOURNAUX DE TRANSACTIONS

• Write Ahead Logs (WAL)


• Chaque donnée est écrite 2 fois sur le disque !
• Sécurité quasiment infaillible
• Avantages :
– WAL : écriture séquentielle
– un seul sync sur le WAL
– fichiers de données : en asynchrone
– sauvegarde PITR et de la réplication fiables

Les journaux de transactions (appelés souvent WAL, autrefois XLOG) sont une garantie
contre les pertes de données.

Il s’agit d’une technique standard de journalisation appliquée à toutes les transactions.


Ainsi lors d’une modification de donnée, l’écriture au niveau du disque se fait en deux
temps :

• écriture immédiate dans le journal de transactions ;


• écriture dans le fichier de données, plus tard, lors du prochain checkpoint.

Ainsi en cas de crash :

• PostgreSQL redémarre ;
• PostgreSQL vérifie s’il reste des données non intégrées aux fichiers de données dans
les journaux (mode recovery) ;
• si c’est le cas, ces données sont recopiées dans les fichiers de données afin de retrou-
ver un état stable et cohérent.
Plus d’informations, lire cet articlea .

Les écritures dans le journal se font de façon séquentielle, donc sans grand déplacement
de la tête d’écriture (sur un disque dur classique, c’est l’opération la plus coûteuse).

De plus, comme nous n’écrivons que dans un seul fichier de transactions, la synchronisa-
tion sur disque peut se faire sur ce seul fichier, si le système de fichiers le supporte.

L’écriture définitive dans les fichiers de données, asynchrone et généralement de manière


lissée, permet là aussi de gagner du temps.

Mais les performances ne sont pas la seule raison des journaux de transactions. Ces jour-
naux ont aussi permis l’apparition de nouvelles fonctionnalités très intéressantes, comme
le PITR et la réplication physique, basés sur le rejeu des informations stockées dans ces
journaux.
a
https://public.dalibo.com/archives/publications/glmf108_postgresql_et_ses_journaux_de_transactions.pdf

65
https://dalibo.com/formations
SQL pour PostgreSQL

2.2.7 SAUVEGARDES

• Sauvegarde des fichiers à froid


– outils système
• Import/Export logique
– pg_dump, pg_dumpall, pg_restore
• Sauvegarde physique à chaud
– pg_basebackup
– sauvegarde PITR

PostgreSQL supporte différentes solutions pour la sauvegarde.

La plus simple revient à sauvegarder à froid tous les fichiers des différents répertoires
de données mais cela nécessite d’arrêter le serveur, ce qui occasionne une mise hors
production plus ou moins longue, suivant la volumétrie à sauvegarder.

L’export logique se fait avec le serveur démarré. Plusieurs outils sont proposés : pg_dump
pour sauvegarder une base, pg_dumpall pour sauvegarder toutes les bases. Suivant le
format de l’export, l’import se fera avec les outils psql ou pg_restore. Les sauvegardes
se font à chaud et sont cohérentes sans blocage de l’activité (seuls la suppression des
tables et le changement de leur définition sont interdits).

Enfin, il est possible de sauvegarder les fichiers à chaud. Cela nécessite de mettre en
place l’archivage des journaux de transactions. L’outil pg_basebackup est conseillé pour
ce type de sauvegarde.

Il est à noter qu’il existe un grand nombre d’outils développés par la communauté pour
faciliter encore plus la gestion des sauvegardes avec des fonctionnalités avancées comme
le PITR (Point In Time Recovery) ou la gestion de la rétention, notamment pg_back (sauve-
garde logique) pgBackRest, pitrery ou barman (sauvegarde physique).

66
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.2.8 RÉPLICATION

• Réplication physique
– instance complète
– même architecture
• Réplication logique (PG 10+)
– table par table
– voire opération par opération
• Asynchrones ou synchrone
• Asymétriques

PostgreSQL dispose de la réplication depuis de nombreuses années.

Le premier type de réplication intégrée est la réplication physique. Il n’y a pas de granu-
larité, c’est forcément l’instance complète (toutes les bases de données), et au niveau des
fichiers de données. Cette réplication est asymétrique : un seul serveur primaire effectue
lectures comme écritures, et les serveurs secondaires n’acceptent que des lectures.

Le deuxième type de réplication est bien plus récent vu qu’il a été ajouté en version 10.
Il s’agit d’une réplication logique, où les données elles-mêmes sont répliquées. Cette ré-
plication est elle aussi asymétrique. Cependant, ceci se configure table par table (et non
pas au niveau de l’instance comme pour la réplication physique).

La réplication logique n’est pas intéressante quand nous voulons un serveur sur lequel
basculer en cas de problème sur le primaire. Dans ce cas, il vaut mieux utiliser la réplica-
tion physique. Par contre, c’est le bon type de réplication pour une réplication partielle
ou pour une mise à jour de version majeure.

Dans les deux cas, les modifications sont transmises en asynchrone (avec un délai possi-
ble). Il est cependant possible de la configurer en synchrone pour tous les serveurs ou
seulement certains.

2.2.9 EXTENSIBILITÉ

• Extensions
– CREATE EXTENSION monextension ;
– nombreuses : contrib, packagées... selon provenance
– notion de confiance (v13+)
– dont langages de procédures stockées !
• Système des hooks
• Background workers
67
https://dalibo.com/formations
SQL pour PostgreSQL

Les développeurs de PostgreSQL sont peu nombreux par rapport aux développeurs de
SGBD commerciaux. De ce fait, il n’est pas possible de tout intégrer dans PostgreSQL
et les développeurs ont donc orienté leur développement pour permettre d’étendre les
fonctionnalités de PostgreSQL sans avoir à modifier le code de PostgreSQL.

La possibilité de développer des routines dans différents langages en est un exemple :


perl, python, PHP, Ruby ou JavaScript sont disponibles.

Autre exemple, la possibilité d’ajouter des types de données, des routines et des opéra-
teurs a permis l’émergence de la couche spatiale de PostgreSQL (appelée PostGIS).

Cependant, il est rapidement devenu évident que la gestion d’un grand nombre de types
de données, fonctions et opérateurs était compliquée, à l’installation comme à la désin-
stallation. Les développeurs de PostgreSQL ont donc ajouté la possibilité de créer des
extensions. Une extension contient un ensemble de types de données, de fonctions,
d’opérateurs, etc. en un seul objet logique. Il suffit de créer ou de supprimer cet objet
logique pour intégrer ou supprimer tous les objets qu’il contient. Les extensions peuvent
être codées en différents langages, généralement en C ou en PL/SQL.

Les extensions ont eu un grand succès. Leurs provenance, rôle et niveau de finition sont
très variables. Certaines sont des utilitaires éprouvés fournis avec PostgreSQL (parmi les
« contrib »). D’autres sont des utilitaires aussi complexes que PostGIS ou un langage de
procédures stockées. Des éditeurs diffusent leur produit comme une extension plutôt
que forker PostgreSQL (citus, timescaledb…). Beaucoup d’extensions peuvent être instal-
lées très simplement depuis des paquets disponibles dans les dépôts habituels (de la dis-
tribution ou du PGDG), ou le site du concepteur. Certaines sont diffusées comme code
source à compiler. Comme tout logiciel, il faut faire attention à en vérifier la source, la
qualité, la réputation et la pérennité.

Une fois les binaires de l’extension en place sur le serveur, l’ordre CREATE EXTENSION suf-
fit généralement dans la base cible, et les fonctionnalités sont immédiatement exploita-
bles.

Les extensions sont habituellement installées par un administrateur (un utilisateur doté
de l’attribut SUPERUSER). À partir de la version 13, certaines extensions sont déclarées de
confiance trusted). Ces extensions peuvent être installées par un utilisateur standard (à
condition qu’il dispose des droits de création dans la base et le ou les schémas concernés).

Les développeurs de PostgreSQL ont aussi ajouté des hooks pour accrocher du
code à exécuter sur certains cas. Cela a permis entre autres de créer l’extension
pg_stat_statements qui s’accroche au code de l’exécuteur de requêtes pour savoir
quelles sont les requêtes exécutées et pour récupérer des statistiques sur ces requêtes.

Enfin, les background workers ont vu le jour. Ce sont des processus spécifiques lancés

68
2. DÉCOUVERTE DES FONCTIONNALITÉS

par le serveur PostgreSQL lors de son démarrage et stoppés lors de son arrêt. Cela a
permis la création de PoWA (outil qui historise les statistiques sur les requêtes) et une
amélioration très intéressante de pg_prewarm (sauvegarde du contenu du cache disque à
l’arrêt de PostgreSQL, restauration du contenu au démarrage).

2.2.10 SÉCURITÉ

• Fichier pg_hba.conf
• Filtrage IP
• Authentification interne (MD5, SCRAM-SHA-256)
• Authentification externe (identd, LDAP, Kerberos...)
• Support natif de SSL

Le filtrage des connexions se paramètre dans le fichier de configuration pg_hba.conf.


Nous pouvons y définir quels utilisateurs (déclarés auprès de PostgreSQL) peuvent se
connecter à quelles bases, et depuis quelles adresses IP.

L’authentification peut se baser sur des mots de passe chiffrés propres à PostgreSQL (md5
ou le plus récent et plus sécurisé scram-sha-256 en version 10), ou se baser sur une
méthode externe (auprès de l’OS, ou notamment LDAP ou Kerberos qui couvre aussi
Active Directory).

L’authentification et le chiffrement de la connexion par SSL sont couverts.

2.3 OBJETS SQL

• Instances
• Objets globaux :
– Bases
– Rôles
– Tablespaces
• Objets locaux :
– Schémas
– Tables
– Vues
– Index
– Routines
– ...
69
https://dalibo.com/formations
SQL pour PostgreSQL

Le but de cette partie est de passer en revue les différents objets logiques maniés par un
moteur de bases de données PostgreSQL.

Nous allons donc aborder la notion d’instance, les différents objets globaux et les objets
locaux. Tous ne seront pas vus, mais le but est de donner une idée globale des objets et
des fonctionnalités de PostgreSQL.

2.3.1 ORGANISATION LOGIQUE

Il est déjà important de bien comprendre une distinction entre les objets. Une instance
est un ensemble de bases de données, de rôles et de tablespaces. Ces objets sont ap-
pelés des objets globaux parce qu’ils sont disponibles quelque soit la base de données de
connexion. Chaque base de données contient ensuite des objets qui lui sont propres. Ils
sont spécifiques à cette base de données et accessibles uniquement lorsque l’utilisateur
est connecté à la base qui les contient. Il est donc possible de voir les bases comme des
conteneurs hermétiques en dehors des objets globaux.

70
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.3.2 INSTANCES

• Une instance
– un répertoire de données
– un port TCP
– une configuration
– plusieurs bases de données
• Plusieurs instances possibles sur un serveur

Une instance est un ensemble de bases de données. Après avoir installé PostgreSQL, il est
nécessaire de créer un répertoire de données contenant un certain nombre de répertoires
et de fichiers qui permettront à PostgreSQL de fonctionner de façon fiable. Le contenu de
ce répertoire est créé initialement par la commande initdb. Ce répertoire stocke ensuite
tous les objets des bases de données de l’instance, ainsi que leur contenu.

Chaque instance a sa propre configuration. Il n’est possible de lancer qu’un seul


postmaster par instance, et ce dernier acceptera les connexions à partir d’un port TCP
spécifique.

Il est possible d’avoir plusieurs instances sur le même serveur, physique ou virtuel. Dans
ce cas, chaque instance aura son répertoire de données dédié et son port TCP dédié.
Ceci est particulièrement utile quand l’on souhaite disposer de plusieurs versions de Post-
greSQL sur le même serveur (par exemple pour tester une application sur ces différentes
versions).

2.3.3 RÔLES

• Permet de se connecter
• Différents attributs et droits
• Utilisateurs / Groupes

Une instance contient un ensemble de rôles. Certains sont prédéfinis et permettent de


disposer de droits particuliers (lecture de fichier avec pg_read_server_files, annulation
d’une requête avec pg_signal_backend, etc). Cependant, la majorité est composée de
rôles créés pour permettre la connexion des utilisateurs.

Chaque rôle créé peut être utilisé pour se connecter à n’importe quelle base de l’instance,
à condition que ce rôle en ait le droit. Ceci se gère directement avec l’attribution du droit
LOGIN au rôle, et avec la configuration du fichier d’accès pg_hba.conf.

Chaque rôle peut être propriétaire d’objets, auquel cas il a tous les droits sur ces objets.
71
https://dalibo.com/formations
SQL pour PostgreSQL

Pour les objets dont il n’est pas propriétaire, il peut se voir donner des droits, en lecture,
écriture, exécution, etc par le propriétaire.

Nous parlons aussi d’utilisateurs et de groupes. Un utilisateur est un rôle qui a la pos-
sibilité de se connecter aux bases alors qu’un groupe ne le peut pas. Un groupe sert
principalement à gérer plus simplement les droits d’accès aux objets.

2.3.4 TABLESPACES

• Répertoire physique contenant les fichiers de données de l’instance


• Une base peut
– se trouver sur un seul tablespace
– être répartie sur plusieurs tablespaces
• Permet de gérer l’espace disque et les performances
• Pas de quota

Toutes les données des tables, vues matérialisées et index sont stockées dans le répertoire
de données principal. Cependant, il est possible de stocker des données ailleurs que dans
ce répertoire. Il faut pour cela créer un tablespace. Un tablespace est tout simplement
la déclaration d’un autre répertoire de données utilisable par PostgreSQL pour y stocker
des données :

CREATE TABLESPACE chaud LOCATION '/SSD/tbl/chaud';

Il est possible d’avoir un tablespace par défaut pour une base de données, auquel cas
tous les objets logiques créés dans cette base seront enregistrés physiquement dans le
répertoire lié à ce tablespace. Il est aussi possible de créer des objets en indiquant spéci-
fiquement un tablespace, ou de les déplacer d’un tablespace à un autre. Un objet spéci-
fique ne peut appartenir qu’à un seul tablespace (autrement dit, un index ne pourra pas
être enregistré sur deux tablespaces). Cependant, pour les objets partitionnés, le choix
du tablespace peut se faire partition par partition.

Le but des tablespaces est de fournir une solution à des problèmes d’espace disque ou de
performances. Si la partition où est stocké le répertoire des données principal se remplit
fortement, il est possible de créer un tablespace dans une autre partition et donc d’utiliser
l’espace disque de cette partition. Si de nouveaux disques plus rapides sont à disposition,
il est possible de placer les objets fréquemment utilisés sur le tablespace contenant les
disques rapides. Si des disques SSD sont à disposition, il est très intéressant d’y placer les
index, les fichiers de tri temporaires, des tables de travail…

Par contre, contrairement à d’autres moteurs de bases de données, PostgreSQL n’a pas

72
2. DÉCOUVERTE DES FONCTIONNALITÉS

de notion de quotas. Les tablespaces ne peuvent donc pas être utilisés pour contraindre
l’espace disque utilisé par certaines applications ou certains rôles.

2.3.5 BASES

• Conteneur hermétique
• Un rôle ne se connecte pas à une instance
– il se connecte forcément à une base
• Une fois connecté, il ne voit que les objets de cette base
– contournement : foreign data wrappers, dblink

Une base de données est un conteneur hermétique. En dehors des objets globaux, le rôle
connecté à une base de données ne voit et ne peut interagir qu’avec les objets contenus
dans cette base. De même, il ne voit pas les objets locaux des autres bases. Néanmoins, il
est possible de lui donner le droit d’accéder à certains objets d’une autre base (de la même
instance ou d’une autre instance) en utilisant les Foreign Data Wrappers (postgres_fdw)
ou l’extension dblink.

Un rôle ne se connecte pas à l’instance. Il se connecte forcément à une base spécifique.

2.3.6 SCHÉMAS

• Espace de noms
• Sous-ensemble de la base
• Non lié à un utilisateur
• Schéma visible par défaut : search_path
• pg_catalog, information_schema
– pour catalogues système (lecture seule !)

Les schémas sont des espaces de noms à l’intérieur d’une base de données permettant :

• de grouper logiquement les objets d’une base de données ;


• de séparer les utilisateurs entre eux ;
• de contrôler plus efficacement les accès aux données ;
• d’éviter les conflits de noms dans les grosses bases de données.

Un schéma n’a à priori aucun lien avec un utilisateur donné.

Un schéma est un espace logique sans lien avec les emplacements physiques des données
(ne pas confondre avec les tablespaces).
73
https://dalibo.com/formations
SQL pour PostgreSQL

Un utilisateur peut avoir accès à tous les schémas ou à un sous-ensemble, tout dépend
des droits dont il dispose. Par défaut, il a accès au schéma public de chaque base et peut
y créer des objets.

PostgreSQL vérifie la présence des objets par rapport au paramètre search_path valable
pour la session en cours lorsque le schéma n’est pas indiqué explicitement pour les objets
d’une requête.

Voici un exemple d’utilisation des schémas :

-- création de deux schémas


CREATE SCHEMA s1;
CREATE SCHEMA s2;

-- création d'une table sans spécification du schéma


CREATE TABLE t1 (id integer);

-- comme le montre la méta-commande \d, la table est créée dans le schéma public

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+----------
public | capitaines | table | postgres
public | capitaines_id_seq | sequence | postgres
public | t1 | table | postgres

-- ceci est dû à la configuration par défaut du paramètre search_path


-- modification du search_path
SET search_path TO s1;

-- création d'une nouvelle table sans spécification du schéma


CREATE TABLE t2 (id integer);

-- cette fois, le schéma de la nouvelle table est s1 car la configuration du


-- search_path est à s1
-- nous pouvons aussi remarquer que les tables capitaines et s1 ne sont plus affichées
-- ceci est dû au fait que le search_path ne contient que le schéma s1 et
-- n'affiche donc que les objets de ce schéma.

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+------+-------+----------
s1 | t2 | table | postgres

-- nouvelle modification du search_path


SET search_path TO s1, public;

74
2. DÉCOUVERTE DES FONCTIONNALITÉS

-- cette fois, les deux tables apparaissent

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+----------
public | capitaines | table | postgres
public | capitaines_id_seq | sequence | postgres
public | t1 | table | postgres
s1 | t2 | table | postgres

-- création d'une nouvelle table en spécifiant cette fois le schéma


CREATE TABLE s2.t3 (id integer);

-- changement du search_path pour voir la table


SET search_path TO s1, s2, public;

-- la table apparaît bien, et le schéma d'appartenance est bien s2

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+----------
public | capitaines | table | postgres
public | capitaines_id_seq | sequence | postgres
public | t1 | table | postgres
s1 | t2 | table | postgres
s2 | t3 | table | postgres

-- création d'une nouvelle table en spécifiant cette fois le schéma


-- attention, cette table a un nom déjà utilisé par une autre table
CREATE TABLE s2.t2 (id integer);

-- la création se passe bien car, même si le nom de la table est identique,


-- le schéma est différent
-- par contre, \d ne montre que la première occurence de la table
-- ici, nous ne voyons t2 que dans s1

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+----------
public | capitaines | table | postgres
public | capitaines_id_seq | sequence | postgres
public | t1 | table | postgres
s1 | t2 | table | postgres
s2 | t3 | table | postgres
75
https://dalibo.com/formations
SQL pour PostgreSQL

-- changeons le search_path pour placer s2 avant s1


SET search_path TO s2, s1, public;

-- maintenant, la seule table t2 affichée est celle du schéma s2

postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------+----------+----------
public | capitaines | table | postgres
public | capitaines_id_seq | sequence | postgres
public | t1 | table | postgres
s2 | t2 | table | postgres
s2 | t3 | table | postgres

Tous ces exemples se basent sur des ordres de création de table. Cependant, le com-
portement serait identique sur d’autres types de commande (SELECT, INSERT, etc) et sur
d’autres types d’objets locaux.

Pour des raisons de sécurité, il est très fortement conseillé de laisser le schéma public
en toute fin du search_path. En effet, s’il est placé au début, comme tout le monde a
droit par défaut de créer des objets dans public, quelqu’un de mal intentionné pourrait
placer un objet dans le schéma public pour servir de proxy à un autre objet d’un schéma
situé après public.

Les schémas pg_catalog et information_schema contiennent des tables utilitaires (« cat-


alogues système ») et des vues. Les catalogues système représentent l’endroit où une base
de données relationnelle stocke les métadonnées des schémas, telles que les informations
sur les tables, et les colonnes, et des données de suivi interne. Dans PostgreSQL, ce sont
de simples tables. Un simple utilisateur lit fréquemment ces tables, plus ou moins directe-
ment, mais n’a aucune raison d’y modifier des données. Toutes les opérations habituelles
pour un utilisateur ou administrateur sont disponibles sous la forme de commandes SQL.
Ne modifiez jamais directement les tables et vues système dans les schémas pg_catalog
et information_schema ; n’y ajoutez ni n’y effacez jamais rien !

Même si cela est techniquement possible, seules des exceptions particulièrement


ésotériques peuvent justifier une modification directe des tables systèmes (par exemple,
une correction de vue système, suite à un bug corrigé dans une version mineure). Ces
tables n’apparaissent d’ailleurs pas dans une sauvegarde logique (pg_dump).

76
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.3.7 TABLES

Par défaut, une table est :


• Permanente
– si temporaire, vivra le temps de la session (ou de la transaction)
• Journalisée
– si unlogged, perdue en cas de crash, pas de réplication
• Non partitionnée
– (>=v10) partitionnement par intervalle, par valeur, par hachage

Par défaut, les tables sont permanentes, journalisées et non partitionnées.

Il est possible de créer des tables temporaires (CREATE TEMPORARY TABLE). Celles-ci ne
sont visibles que par la session qui les a créées et seront supprimées par défaut à la fin
de cette session. Il est aussi possible de les supprimer automatiquement à la fin de la
transaction qui les a créées. Il n’existe pas dans PostgreSQL de notion de table temporaire
globale. Cependant, une extension59 existe pour combler leur absence.

Pour des raisons de performance, il est possible de créer une table non journalisée (CREATE
UNLOGGED TABLE). La définition de la table est journalisée mais pas son contenu. De ce
fait, en cas de crash, il est impossible de dire si la table est corrompue ou non, et donc,
au redémarrage du serveur, PostgreSQL vide la table de tout contenu. De plus, n’étant
pas journalisée, la table n’est pas présente dans les sauvegardes PITR, ni repliquée vers
d’éventuels serveurs secondaires.

Enfin, depuis la version 10, il est possible de partitionner les tables suivant un certain
type de partitionnement : par intervalle, par valeur ou par hachage. Avant la version 10,
il est possible de se rabattre sur le partitionnement par héritage, moins pratique et moins
performant.

2.3.8 VUES

• Masquer la complexité
– structure : interface cohérente vers les données, même si les tables évoluent
– sécurité : contrôler l’accès aux données de manière sélective
• Vues matérialisées
– à rafraîchir à une certaine fréquence

Le but des vues est de masquer une complexité, qu’elle soit du côté de la structure de la
base ou de l’organisation des accès. Dans le premier cas, elles permettent de fournir un
59
https://github.com/darold/pgtt

77
https://dalibo.com/formations
SQL pour PostgreSQL

accès qui ne change pas même si les structures des tables évoluent. Dans le second cas,
elles permettent l’accès à seulement certaines colonnes ou certaines lignes. De plus, les
vues étant exécutées avec les mêmes droits que l’utilisateur qui les a créées, cela permet
un changement temporaire des droits d’accès très appréciable dans certains cas.

Voici un exemple d’utilisation :


SET search_path TO public;

-- création de l'utilisateur guillaume


-- il n'aura pas accès à la table capitaines
-- par contre, il aura accès à la vue capitaines_anon
CREATE ROLE guillaume LOGIN;

-- ajoutons une colonne à la table capitaines


-- et ajoutons-y des données
ALTER TABLE capitaines ADD COLUMN num_cartecredit text;
INSERT INTO capitaines (nom, age, num_cartecredit)
VALUES ('Robert Surcouf', 20, '1234567890123456');

-- création de la vue
CREATE VIEW capitaines_anon AS
SELECT nom, age, substring(num_cartecredit, 0, 10) || '******' AS num_cc_anon
FROM capitaines;

-- ajout du droit de lecture à l'utilisateur guillaume


GRANT SELECT ON TABLE capitaines_anon TO guillaume;

-- connexion en tant qu'utilisateur guillaume


SET ROLE TO guillaume;

-- vérification qu'on lit bien la vue mais pas la table


SELECT * FROM capitaines_anon WHERE nom LIKE '%Surcouf';

nom | age | num_cc_anon


----------------+-----+-----------------
Robert Surcouf | 20 | 123456789******

-- tentative de lecture directe de la table


SELECT * FROM capitaines;
ERROR: permission denied for relation capitaines

Il est possible de modifier une vue en lui ajoutant des colonnes à la fin, au lieu de devoir les
détruire et recréer (ainsi que toutes les vues qui en dépendent, ce qui peut être fastidieux).

Par exemple :
SET ROLE postgres;

78
2. DÉCOUVERTE DES FONCTIONNALITÉS

CREATE OR REPLACE VIEW capitaines_anon AS SELECT


nom,age,substring(num_cartecredit,0,10)||'******' AS num_cc_anon,
md5(substring(num_cartecredit,0,10)) AS num_md5_cc
FROM capitaines;

SELECT * FROM capitaines_anon WHERE nom LIKE '%Surcouf';

nom | age | num_cc_anon | num_md5_cc


----------------+-----+-----------------+----------------------------------
Robert Surcouf | 20 | 123456789****** | 25f9e794323b453885f5181f1b624d0b

Nous pouvons aussi modifier les données au travers des vues simples, sans ajout de code
et de trigger :
UPDATE capitaines_anon SET nom = 'Nicolas Surcouf' WHERE nom = 'Robert Surcouf';

SELECT * from capitaines_anon WHERE nom LIKE '%Surcouf';

nom | age | num_cc_anon | num_md5_cc


-----------------+-----+-----------------+----------------------------------
Nicolas Surcouf | 20 | 123456789****** | 25f9e794323b453885f5181f1b624d0b

UPDATE capitaines_anon SET num_cc_anon = '123456789xxxxxx'


WHERE nom = 'Nicolas Surcouf';

ERROR: cannot update column "num_cc_anon" of view "capitaines_anon"


DETAIL: View columns that are not columns of their base relation
are not updatable.

PostgreSQL gère le support natif des vues matérialisées (CREATE MATERIALIZED VIEW
nom_vue_mat AS SELECT …). Les vues matérialisées sont des vues dont le contenu est
figé sur disque, permettant de ne pas recalculer leur contenu à chaque appel. De plus, il
est possible de les indexer pour accélérer leur consultation. Il faut cependant faire atten-
tion à ce que leur contenu reste synchrone avec le reste des données.

Les vues matérialisées ne sont pas mises à jour automatiquement, il faut deman-
der explicitement le rafraîchissement (REFRESH MATERIALIZED VIEW). Avec la clause
CONCURRENTLY, s’il y a un index d’unicité, le rafraîchissement ne bloque pas les sessions
lisant en même temps les données d’une matérialisée.
-- Suppression de la vue
DROP VIEW capitaines_anon;

-- Création de la vue matérialisée


CREATE MATERIALIZED VIEW capitaines_anon AS
SELECT nom,
age,
substring(num_cartecredit, 0, 10) || '******' AS num_cc_anon
FROM capitaines;
79
https://dalibo.com/formations
SQL pour PostgreSQL

-- Les données sont bien dans la vue matérialisée


SELECT * FROM capitaines_anon WHERE nom LIKE '%Surcouf';

nom | age | num_cc_anon


-----------------+-----+-----------------
Nicolas Surcouf | 20 | 123456789******

-- Mise à jour d'une ligne de la table


-- Cette mise à jour est bien effectuée, mais la vue matérialisée
-- n'est pas impactée
UPDATE capitaines SET nom = 'Robert Surcouf' WHERE nom = 'Nicolas Surcouf';

SELECT * FROM capitaines WHERE nom LIKE '%Surcouf';

id | nom | age | num_cartecredit


----+----------------+-----+------------------
1 | Robert Surcouf | 20 | 1234567890123456

SELECT * FROM capitaines_anon WHERE nom LIKE '%Surcouf';

nom | age | num_cc_anon


-----------------+-----+-----------------
Nicolas Surcouf | 20 | 123456789******

-- Le résultat est le même mais le plan montre bien que PostgreSQL ne passe
-- plus par la table mais par la vue matérialisée :
EXPLAIN SELECT * FROM capitaines_anon WHERE nom LIKE '%Surcouf';

QUERY PLAN
-----------------------------------------------------------------
Seq Scan on capitaines_anon (cost=0.00..20.62 rows=1 width=68)
Filter: (nom ~~ '%Surcouf'::text)

-- Après un rafraîchissement explicite de la vue matérialisée,


-- cette dernière contient bien les bonnes données
REFRESH MATERIALIZED VIEW capitaines_anon;

SELECT * FROM capitaines_anon WHERE nom LIKE '%Surcouf';

nom | age | num_cc_anon


----------------+-----+-----------------
Robert Surcouf | 20 | 123456789******

-- Pour rafraîchir la vue matérialisée sans bloquer les autres sessions


-- ( >= 9.4 ) :
REFRESH MATERIALIZED VIEW CONCURRENTLY capitaines_anon;

ERROR: cannot refresh materialized view "public.capitaines_anon" concurrently


HINT: Create a unique index with no WHERE clause on one or more columns
of the materialized view.

80
2. DÉCOUVERTE DES FONCTIONNALITÉS

-- En effet, il faut un index d'unicité pour faire un rafraîchissement


-- sans bloquer les autres sessions.
CREATE UNIQUE INDEX ON capitaines_anon(nom);

REFRESH MATERIALIZED VIEW CONCURRENTLY capitaines_anon;

2.3.9 INDEX

• Algorithmes supportés
– B-tree (par défaut)
– Hash (dangereux si version < 10)
– GiST / SP-GiST
– GIN
– BRIN (version 9.5)
– Bloom (version 9.6)
• Type
– Mono ou multi-colonnes
– Partiel
– Fonctionnel
– Couvrant

PostgreSQL propose plusieurs algorithmes d’index.

Pour une indexation standard, nous utilisons en général un index Btree, de par ses nom-
breuses possibilités et ses très bonnes performances.

Les index hash sont peu utilisés, essentiellement dans la comparaison d’égalité de grandes
chaînes de caractères. Avant la version 10, leur utilisation est déconseillée car ils ne sont
pas journalisés, d’où plusieurs problèmes : reconstruction (REINDEX) obligatoire en cas de
crash ou de restauration PITR, pas de réplication possible.

Moins simples d’abord, les index plus spécifiques (GIN, GIST) sont spécialisés pour les
grands volumes de données complexes et multidimensionnelles : indexation textuelle,
géométrique, géographique, ou de tableaux de données par exemple.

Les index BRIN sont des index très compacts destinés aux grandes tables où les données
sont fortement corrélées par rapport à leur emplacement physique sur les disques.

Les index bloom sont des index probabilistes visant à indexer de nombreuses colonnes
interrogées simultanément. Ils nécessitent l’ajout d’une extension (nommée bloom).

Le module pg_trgm permet l’utilisation d’index dans des cas habituellement impossibles,
comme les expressions rationnelles et les LIKE '%...%'.
81
https://dalibo.com/formations
SQL pour PostgreSQL

Généralement, l’indexation porte sur la valeur d’une ou plusieurs colonnes. Il est néan-
moins possible de n’indexer qu’une partie des lignes (index partiel) ou le résultat d’une
fonction sur une ou plusieurs colonnes en paramètre. Enfin, il est aussi possible de mod-
ifier les index de certaines contraintes (unicité et clé primaire) pour inclure des colonnes
supplémentaires.
Plus d’informations :
• Article Wikipédia sur les arbres Ba ;
• Article Wikipédia sur les tables de hachageb ;
• Documentation officielle françaisec .

2.3.10 TYPES DE DONNÉES

• Types de base
– natif : int, float
– standard SQL : numeric, char, varchar, date, time, timestamp, bool
• Type complexe
– tableau
– XML
– JSON (jsonb)
• Types métier
– réseau, géométrique, etc.
• Types créés par les utilisateurs
– structure SQL, C, Domaine, Enum

PostgreSQL dispose d’un grand nombre de types de base, certains natifs (comme la
famille des integer et celle des float), et certains issus de la norme SQL (numeric, char,
varchar, date, time, timestamp, bool).

Il dispose aussi de types plus complexes. Les tableaux (array) permettent de lister un
ensemble de valeurs discontinues. Les intervalles (range) permettent d’indiquer toutes
les valeurs comprises entre une valeur de début et une valeur de fin. Ces deux types
dépendent évidemment d’un type de base : tableau d’entiers, intervalle de dates, etc.
Existent aussi les types complexes les données XML et JSON (préférer le type optimisé
jsonb).

Enfin, il existe des types métiers ayant trait principalement au réseau (adresse IP, masque
réseau), à la géométrie (point, ligne, boite). Certains sont apportés par des extensions.
a
https://fr.wikipedia.org/wiki/Arbre_B
b
https://fr.wikipedia.org/wiki/Table_de_hachage
c
https://docs.postgresql.fr/current/textsearch-indexes.html

82
2. DÉCOUVERTE DES FONCTIONNALITÉS

Tout ce qui vient d’être décrit est natif. Il est cependant possible de créer ses propres
types de données, soit en SQL soit en C. Les possibilités et les performances ne sont
évidemment pas les mêmes.

Voici comment créer un type en SQL :


CREATE TYPE serveur AS (
nom text,
adresse_ip inet,
administrateur text
);

Ce type de données va pouvoir être utilisé dans tous les objets SQL habituels : table, rou-
tine, opérateur (pour redéfinir l’opérateur + par exemple), fonction d’agrégat, contrainte,
etc.

Voici un exemple de création d’un opérateur :


CREATE OPERATOR + (
leftarg = stock,
rightarg = stock,
procedure = stock_fusion,
commutator = +
);

(Il faut au préalable avoir défini le type stock et la fonction stock_fusion.)

Il est aussi possible de définir des domaines. Ce sont des types créés par les utilisateurs
à partir d’un type de base et en lui ajoutant des contraintes supplémentaires.

En voici un exemple :
CREATE DOMAIN code_postal_francais AS text CHECK (value ~ '^\d{5}$');
ALTER TABLE capitaines ADD COLUMN cp code_postal_francais;
UPDATE capitaines SET cp = '35400' WHERE nom LIKE '%Surcouf';
UPDATE capitaines SET cp = '1420' WHERE nom = 'Haddock';

ERROR: value for domain code_postal_francais violates check constraint


"code_postal_francais_check"

UPDATE capitaines SET cp = '01420' WHERE nom = 'Haddock';


SELECT * FROM capitaines;

id | nom | age | num_cartecredit | cp


----+----------------+-----+------------------+-------
1 | Robert Surcouf | 20 | 1234567890123456 | 35400
1 | Haddock | 35 | | 01420

Les domaines permettent d’intégrer la déclaration des contraintes à la déclaration d’un


type, et donc de simplifier la maintenance de l’application si ce type peut être utilisé dans
plusieurs tables : si la définition du code postal est insuffisante pour une évolution de
83
https://dalibo.com/formations
SQL pour PostgreSQL

l’application, il est possible de la modifier par un ALTER DOMAIN, et définir de nouvelles


contraintes sur le domaine. Ces contraintes seront vérifiées sur l’ensemble des champs
ayant le domaine comme type avant que la nouvelle version du type ne soit considérée
comme valide.

Le défaut par rapport à des contraintes CHECK classiques sur une table est que
l’information ne se trouvant pas dans la table, les contraintes sont plus difficiles à lister
sur une table.

Enfin, il existe aussi les enums. Ce sont des types créés par les utilisateurs composés
d’une liste ordonnée de chaînes de caractères.

En voici un exemple :
CREATE TYPE jour_semaine
AS ENUM ('Lundi', 'Mardi', 'Mercredi', 'Jeudi', 'Vendredi',
'Samedi', 'Dimanche');

ALTER TABLE capitaines ADD COLUMN jour_sortie jour_semaine;

UPDATE capitaines SET jour_sortie = 'Mardi' WHERE nom LIKE '%Surcouf';


UPDATE capitaines SET jour_sortie = 'Samedi' WHERE nom LIKE 'Haddock';

SELECT * FROM capitaines WHERE jour_sortie >= 'Jeudi';

id | nom | age | num_cartecredit | cp | jour_sortie


----+---------+-----+-----------------+----+-------------
1 | Haddock | 35 | | | Samedi

Les enums permettent de déclarer une liste de valeurs statiques dans le dictionnaire de
données plutôt que dans une table externe sur laquelle il faudrait rajouter des jointures :
dans l’exemple, nous aurions pu créer une table jour_de_la_semaine, et stocker la clé
associée dans planning. Nous aurions pu tout aussi bien positionner une contrainte
CHECK, mais nous n’aurions plus eu une liste ordonnée.
Conférence de Heikki Linakangas sur la création d’un type colora .

a
https://wiki.postgresql.org/images/1/11/FOSDEM2011-Writing_a_User_defined_type.pdf

84
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.3.11 CONTRAINTES

• CHECK
– prix > 0
• NOT NULL
– id_client NOT NULL
• Unicité
– id_client UNIQUE
• Clés primaires
– UNIQUE NOT NULL ==> PRIMARY KEY (id_client)
• Clés étrangères
– produit_id REFERENCES produits(id_produit)
• EXCLUDE
– EXCLUDE USING gist (room WITH =, during WITH &&)

Les contraintes sont la garantie de conserver des données de qualité ! Elles permettent
une vérification qualitative des données, au-delà du type de données.

Elles donnent des informations au planificateur qui lui permettent d’optimiser les
requêtes. Par exemple, le planificateur sait ne pas prendre en compte une jointure dans
certains cas, notamment grâce à l’existence d’une contrainte d’unicité.

Les contraintes d’exclusion permettent un test sur plusieurs colonnes avec différents
opérateurs (et non pas que l’égalité dans le cas d’une contrainte unique, qui n’est qu’une
contrainte d’exclusion très spécialisée). Si le test se révèle positif, la ligne est refusée.

2.3.12 COLONNES À VALEUR GÉNÉRÉE

• Valeur calculée à l’insertion


• DEFAULT
• Identité (v10)
– GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY
• Expression (v12)
– GENERATED ALWAYS AS ( generation_expr ) STORED

Une colonne a par défaut la valeur NULL si aucune valeur n’est fournie lors de l’insertion
de la ligne. Il existe néanmoins trois cas où le moteur peut substituer une autre valeur.

Le plus connu correspond à la clause DEFAULT. Dans ce cas, la valeur insérée correspond
à la valeur indiquée avec cette clause si aucune valeur n’est indiquée pour la colonne. Si
85
https://dalibo.com/formations
SQL pour PostgreSQL

une valeur est précisée, cette valeur surcharge la valeur par défaut. L’exemple suivant
montre cela :

CREATE TABLE t2 (c1 integer, c2 integer, c3 integer DEFAULT 10);


INSERT INTO t2 (c1, c2, c3) VALUES (1, 2, 3);
INSERT INTO t2 (c1) VALUES (2);
SELECT * FROM t2;

c1 | c2 | c3
----+----+----
1 | 2 | 3
2 | | 10

La clause DEFAULT ne peut pas être utilisée avec des clauses complexes, notamment des
clauses comprenant des requêtes.

Pour aller un peu plus loin, à partir de PostgreSQL 12, il est possible d’utiliser GENERATED
ALWAYS AS ( expression ) STORED. Cela permet d’avoir une valeur calculée pour la
colonne, valeur qui ne peut pas être surchargée, ni à l’insertion, ni à la mise à jour (mais
qui est bien stockée sur le disque).

Comme exemple, nous allons reprendre la table capitaines et lui ajouter une colonne
ayant comme valeur la version modifiée du numéro de carte de crédit :

ALTER TABLE capitaines


ADD COLUMN num_cc_anon text
GENERATED ALWAYS AS (substring(num_cartecredit, 0, 10) || '******') STORED;

SELECT nom, num_cartecredit, num_cc_anon FROM capitaines;

nom | num_cartecredit | num_cc_anon


----------------+------------------+-----------------
Robert Surcouf | 1234567890123456 | 123456789******
Haddock | |

INSERT INTO capitaines VALUES


(2, 'Joseph Pradere-Niquet', 40, '9876543210987654', '44000', 'Lundi', 'test');
ERROR: cannot insert into column "num_cc_anon"
DETAIL: Column "num_cc_anon" is a generated column.

INSERT INTO capitaines VALUES


(2, 'Joseph Pradere-Niquet', 40, '9876543210987654', '44000', 'Lundi');

SELECT nom, num_cartecredit, num_cc_anon FROM capitaines;

nom | num_cartecredit | num_cc_anon


-----------------------+------------------+-----------------
Robert Surcouf | 1234567890123456 | 123456789******

86
2. DÉCOUVERTE DES FONCTIONNALITÉS

Haddock | |
Joseph Pradere-Niquet | 9876543210987654 | 987654321******

Enfin, GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY permet d’obtenir une


colonne d’identité, bien meilleure que ce que le pseudo-type serial propose. Si ALWAYS
est indiqué, la valeur n’est pas modifiable.
ALTER TABLE capitaines
ADD COLUMN id2 integer GENERATED ALWAYS AS IDENTITY;

SELECT nom, id2 FROM capitaines;

nom | id2
-----------------------+-----
Robert Surcouf | 1
Haddock | 2
Joseph Pradere-Niquet | 3

INSERT INTO capitaines (nom) VALUES ('Tom Souville');

SELECT nom, id2 FROM capitaines;

nom | id2
-----------------------+-----
Robert Surcouf | 1
Haddock | 2
Joseph Pradere-Niquet | 3
Tom Souville | 4

2.3.13 LANGAGES

• Procédures & fonctions en différents langages


• Par défaut : SQL, C et PL/pgSQL
• Extensions officielles : Perl, Python
• Mais aussi Java, Ruby, Javascript…
• Intérêts : fonctionnalités, performances

Les langages officiellement supportés par le projet sont :

• PL/pgSQL ;
• PL/Perl60 ;
• PL/Python61 (version 2 et 3) ;
• PL/Tcl.
60
https://docs.postgresql.fr/current/plperl.html
61
https://docs.postgresql.fr/current/plpython.html

87
https://dalibo.com/formations
SQL pour PostgreSQL

Voici une liste non exhaustive des langages procéduraux disponibles, à différents degrés
de maturité :

• PL/sh62 ;
• PL/R63 ;
• PL/Java64 ;
• PL/lolcode ;
• PL/Scheme ;
• PL/PHP ;
• PL/Ruby ;
• PL/Lua65 ;
• PL/pgPSM ;
• PL/v866 (Javascript).
Tableau des langages supportésa .

Pour qu’un langage soit utilisable, il doit être activé au niveau de la base où il sera utilisé.
Les trois langages activés par défaut sont le C, le SQL et le PL/pgSQL. Les autres doivent
être ajoutés à partir des paquets de la distribution ou du PGDG, ou compilés à la main,
puis l’extension installée dans la base (CREATE EXTENSION plperl, CREATE EXTENSION
plpython3u, etc.).

Chaque langage a ses avantages et inconvénients. Par exemple, PL/pgSQL est très simple
à apprendre mais n’est pas performant quand il s’agit de traiter des chaînes de caractères.
Pour ce traitement, il est souvent préférable d’utiliser PL/Perl, voire PL/Python. Évidem-
ment, une routine en C aura les meilleures performances mais sera beaucoup moins facile
à coder et à maintenir. Par ailleurs, les procédures peuvent s’appeler les unes les autres
quel que soit le langage. Les index et triggers peuvent également utiliser ces langages.
S’ajoute l’intérêt de ne pas avoir à réécrire en PL/pgSQL des fonctions existantes dans
d’autres langages ou d’accéder à des modules bien établis de ces langages.

62
https://github.com/petere/plsh
63
https://github.com/postgres-plr/plr
64
https://tada.github.io/pljava/
65
https://github.com/pllua/pllua
66
https://github.com/plv8/plv8
a
https://wiki.postgresql.org/wiki/PL_Matrix

88
2. DÉCOUVERTE DES FONCTIONNALITÉS

2.3.14 FONCTIONS & PROCÉDURES

• Fonction
– renvoie une ou plusieurs valeurs
– SETOF ou TABLE pour plusieurs lignes
• Procédure (v11+)
– ne renvoie rien
– peut gérer le transactionnel dans certains cas

Historiquement, PostgreSQL ne proposait que l’écriture de fonctions. Depuis la version


11, il est aussi possible de créer des procédures. Le terme « routine » est utilisé pour
signifier procédure ou fonction.

Une fonction renvoie une donnée. Cette donnée peut comporter une ou plusieurs
colonnes. Elle peut aussi avoir plusieurs lignes dans le cas d’une fonction SETOF ou
TABLE.

Une procédure ne renvoie rien. Elle a cependant un gros avantage par rapport aux fonc-
tions dans le fait qu’elle peut gérer le transactionnel. Elle peut valider ou annuler la trans-
action en cours. Dans ce cas, une nouvelle transaction est ouverte immédiatement après
la fin de la transaction précédente.

2.3.15 OPÉRATEURS

• Dépend d’un ou deux types de données


• Exécute une fonction prédéfinie
CREATE OPERATOR // (FUNCTION=division0, LEFTARG=integer,
RIGHTARG=integer);

Il est possible de créer de nouveaux opérateurs sur un type de base ou sur un type utilisa-
teur. Un opérateur exécute une fonction, soit à un argument pour un opérateur unitaire,
soit à deux arguments pour un opérateur binaire.

Voici un exemple d’opérateur acceptant une division par zéro sans erreur :
-- définissons une fonction de division en PL/pgSQL
CREATE FUNCTION division0 (p1 integer, p2 integer) RETURNS integer
LANGUAGE plpgsql
AS $$
BEGIN
IF p2 = 0 THEN
RETURN NULL;
END IF;
89
https://dalibo.com/formations
SQL pour PostgreSQL

RETURN p1 / p2;
END
$$;

-- créons l'opérateur
CREATE OPERATOR // (FUNCTION = division0, LEFTARG = integer, RIGHTARG = integer);

-- une division normale se passe bien

SELECT 10/5;

?column?
----------
2

SELECT 10//5;

?column?
----------
2

-- une division par 0 ramène une erreur avec l'opérateur natif


SELECT 10/0;

ERROR: division by zero

-- une division par 0 renvoie NULL avec notre opérateur


SELECT 10//0;

?column?
----------

(1 row)

2.3.16 TRIGGERS

• Opérations : INSERT, COPY, UPDATE, DELETE, TRUNCATE


• Trigger sur :
– une colonne, et/ou avec condition
– une vue
– DDL
• Tables de transition (v 10)
• Effet sur :
– l’ensemble de la requête (FOR STATEMENT)
– chaque ligne impactée (FOR EACH ROW)

90
2. DÉCOUVERTE DES FONCTIONNALITÉS

• N’importe quel langage supporté

Les triggers peuvent être exécutés avant (BEFORE) ou après (AFTER) une opération.

Il est possible de les déclencher pour chaque ligne impactée (FOR EACH ROW) ou une
seule fois pour l’ensemble de la requête (FOR STATEMENT). Dans le premier cas, il est
possible d’accéder à la ligne impactée (ancienne et nouvelle version). Dans le deuxième
cas, il a fallu attendre la version 10 pour disposer des tables de transition qui donnent à
l’utilisateur une vision des lignes avant et après modification.

Par ailleurs, les triggers peuvent être écrits dans n’importe lequel des langages de routine
supportés par PostgreSQL (C, PL/pgSQL, PL/Perl, etc. )

Exemple :

ALTER TABLE capitaines ADD COLUMN salaire integer;

CREATE FUNCTION verif_salaire()


RETURNS trigger AS $verif_salaire$
BEGIN
-- Nous verifions que les variables ne sont pas vides
IF NEW.nom IS NULL THEN
RAISE EXCEPTION 'Le nom ne doit pas être null.';
END IF;

IF NEW.salaire IS NULL THEN


RAISE EXCEPTION 'Le salaire ne doit pas être null.';
END IF;

-- pas de baisse de salaires !


IF NEW.salaire < OLD.salaire THEN
RAISE EXCEPTION 'Pas de baisse de salaire !';
END IF;

RETURN NEW;
END;
$verif_salaire$ LANGUAGE plpgsql;

CREATE TRIGGER verif_salaire BEFORE INSERT OR UPDATE ON capitaines


FOR EACH ROW EXECUTE PROCEDURE verif_salaire();

UPDATE capitaines SET salaire = 2000 WHERE nom = 'Robert Surcouf';


UPDATE capitaines SET salaire = 3000 WHERE nom = 'Robert Surcouf';
UPDATE capitaines SET salaire = 2000 WHERE nom = 'Robert Surcouf';

ERROR: pas de baisse de salaire !


CONTEXTE : PL/pgSQL function verif_salaire() line 13 at RAISE
91
https://dalibo.com/formations
SQL pour PostgreSQL

2.3.17 QUESTIONS

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

92
3. INTRODUCTION ET PREMIERS SELECT

3 INTRODUCTION ET PREMIERS SELECT

3.1 PRÉAMBULE

• Qu’est-ce que le standard SQL ?


• Comment lire des données
• Quels types de données sont disponibles ?

Ce module a pour but de présenter le standard SQL. Un module ne permet pas de tout
voir, aussi ce module se concentrera sur la lecture de données déjà présentes en base.
Cela permet d’aborder aussi la question des types de données disponibles.

3.1.1 MENU

• Principes d’une base de données


• Premières requêtes
• Connaître les types de données

3.1.2 OBJECTIFS

• Comprendre les principes


– Écrire quelques requêtes en lecture
– Connaître les différents types de données
* et quelques fonctions très utiles

93
https://dalibo.com/formations
SQL pour PostgreSQL

3.2 PRINCIPES D'UNE BASE DE DONNÉES

• Base de données
– ensemble organisé d’informations
– Système de Gestion de Bases de Données
* acronyme SGBD (DBMS en anglais)
* programme assurant la gestion et l’accès à une base de données
* assure la cohérence des données
Si des données sont récoltées, organisées et stockées afin de répondre à un besoin spé-
cifique, alors on parle de base de données. Une base de données peut utiliser différents
supports : papier, fichiers informatiques, etc.

Le Système de Gestion de Bases de Données (SGBD), appelé Database Management Sys-


tem (DBMS) en anglais, assure la gestion d’une base de données informatisée. Il permet
l’accès aux données et assure également la cohérence des données.

3.2.1 TYPE DE BASES DE DONNÉES

• Modèle hiérarchique
– Modèle réseau
– Modèle relationnel
– Modèle objet
– Modèle relationnel-objet
– NoSQL

Au fil des années ont été développés plusieurs modèles de données, que nous allons
décrire.

3.2.2 TYPE DE BASES DE DONNÉES (1)

• Modèle hiérarchique
– structure arborescente
– redondance des données
• Modèle réseau
– structure arborescente, mais permettant des associations
– ex : Bull IDS2 sur GCOS

94
3. INTRODUCTION ET PREMIERS SELECT

Les modèles hiérarchiques et réseaux ont été les premiers modèles de données utilisées
dans les années 60 sur les mainframes IBM ou Bull. Ils ont été rapidement supplantés par
le modèle relationnel car les requêtes étaient dépendantes du modèle de données. Il était
nécessaire de connaître les liens entre les différents nœuds de l’arborescence pour con-
cevoir les requêtes. Les programmes sont donc complètement dépendants de la structure
de la base de données.

Des recherches souhaitent néanmoins arriver à rendre indépendant la vue logique de


l’implémentation physique de la base de données.

3.2.3 TYPE DE BASES DE DONNÉES (2)

• Modèle relationnel
– basé sur la théorie des ensembles et la logique des prédicats
– standardisé par la norme SQL
• Modèle objet
– structure objet
– pas de standard
• Modèle relationnel-objet
– le standard SQL ajoute des concepts objets

Le modèle relationnel est issu des travaux du Docteur Edgar F. Codd qu’il a menés dans les
laboratoires d’IBM à la fin des années 60. Ses travaux avaient pour but de rendre indépen-
dant le stockage physique de la vue logique de la base de données. Et, mathématicien de
formation, il s’est appuyé sur la théorie des ensembles et la logique des prédicats pour
établir les fondements des bases de données relationnelles. Pour manipuler les données
de façon ensembliste, le Dr Codd a mis au point le point langage SQL. Ce langage est
à l’origine du standard SQL qui a émergé dans les années 80 et qui a rendu le modèle
relationnel très populaire.

Le modèle objet est, quant à lui, issu de la mouvance autour des langages objets. Du fait
de l’absence d’un standard avéré, le modèle objet n’a jamais été populaire et est toujours
resté dans l’ombre du modèle relationnel.

Le modèle relationnel a néanmoins été étendu par la norme SQL:1999 pour intégrer des
fonctionnalités objets. On parle alors de modèle relationnel-objet. PostgreSQL en est un
exemple, c’est un SGBDRO (Système de Gestion de Bases de Données Relationnel-Objet).

95
https://dalibo.com/formations
SQL pour PostgreSQL

3.2.4 TYPE DE BASES DE DONNÉES (3)

• NoSQL : Not only SQL


– pas de norme de langage de requête
– clé-valeur (Redis, Riak)
– graphe (Neo4J)
– document (MongoDB, CouchDB)
– orienté colonne (HBase)
• Rapprochement relationnel/NoSQL
– PostgreSQL permet de stocker des documents (JSON, XML)

Les bases NoSQL sont une famille de bases de données qui répondent à d’autres besoins
et contraintes que les bases relationnelles. Les bases NoSQL sont souvent des bases
« sans schéma », la base ne vérifiant plus l’intégrité des données selon des contraintes
définies dans le modèle de données. Chaque base de ce segment dispose d’un langage
de requête spécifique, qui n’est pas normé. Une tentative de standardisation, débutée en
2011, n’a d’ailleurs abouti à aucun résultat.

Ce type de base offre souvent la possibilité d’offrir du sharding simple à mettre en œuvre.
Le sharding consiste à répartir les données physiquement sur plusieurs serveurs. Cer-
taines technologies semblent mieux marcher que d’autres de ce point de vue là. En contre-
partie, la durabilité des données n’est pas assurée, au contraire d’une base relationnelle
qui assure la durabilité dès la réponse à un COMMIT.

Exemple de requête SQL :

SELECT person, SUM(score), AVG(score), MIN(score), MAX(score), COUNT(*)


FROM demo
WHERE score > 0 AND person IN('bob','jake')
GROUP BY person;

La même requête, pour MongoDB :

db.demo.group({
"key": {
"person": true
},
"initial": {
"sumscore": 0,
"sumforaverageaveragescore": 0,
"countforaverageaveragescore": 0,
"countstar": 0
},
"reduce": function(obj, prev) {
prev.sumscore = prev.sumscore + obj.score - 0;

96
3. INTRODUCTION ET PREMIERS SELECT

prev.sumforaverageaveragescore += obj.score;
prev.countforaverageaveragescore++;
prev.minimumvaluescore = isNaN(prev.minimumvaluescore) ? obj.score :
Math.min(prev.minimumvaluescore, obj.score);
prev.maximumvaluescore = isNaN(prev.maximumvaluescore) ? obj.score :
Math.max(prev.maximumvaluescore, obj.score);
if (true != null) if (true instanceof Array) prev.countstar +=
true.length;
else prev.countstar++;
},
"finalize": function(prev) {
prev.averagescore = prev.sumforaverageaveragescore /
prev.countforaverageaveragescore;
delete prev.sumforaverageaveragescore;
delete prev.countforaverageaveragescore;
},
"cond": {
"score": {
"$gt": 0
},
"person": {
"$in": ["bob", "jake"]
}
}
});

Un des avantages de ces technologies, c’est qu’un modèle clé-valeur permet facilement
d’utiliser des algorithmes de type MapReduce : diviser le problème en sous-problèmes
traités parallèlement par différents nœuds (phase Map), puis synthétisés de façon cen-
tralisée (phase Reduce).

Les bases de données relationnelles ne sont pas incompatibles avec Map Reduce en soit.
Simplement, le langage SQL étant déclaratif, il est conceptuellement opposé à la descrip-
tion fine des traitements qu’on doit réaliser avec MapReduce. C’est (encore une fois) le
travail de l’optimiseur d’être capable d’effectuer ce genre d’opérations : la parallélisation
(répartition d’une tâche sur plusieurs processeurs) est possible dans certains cas avec
PostgreSQL.

97
https://dalibo.com/formations
SQL pour PostgreSQL

3.2.5 MODÈLE RELATIONNEL

• Indépendance entre la vue logique et la vue physique


– le SGBD gère lui-même le stockage physique
• Table ou relation
• Un ensemble de tables représente la vue logique

Le modèle relationnel garantit l’indépendance entre la vue logique et la vue physique.


L’utilisateur ne se préoccupe que des objets logiques (pour lire ou écrire des enreg-
istrements), et le SGBD traduit la demande exprimée avec des objets logiques en actions
à réaliser sur des objets physiques.

Les objets logiques sont appelés des relations. Ce sont généralement les tables, mais il
existe d’autres objets qui sont aussi des relations (les vues par exemple, mais aussi les
index et les séquences).

3.2.6 CARACTÉRISTIQUES DU MODÈLE RELATIONNEL

• Théorie des ensembles


• Logique des prédicats
• Logique 3 états

Le modèle relationnel se base sur la théorie des ensembles. Chaque relation contient un
ensemble de données et ces différents ensembles peuvent se joindre suivant certaines
conditions.

La logique des prédicats est un sous-ensemble de la théorie des ensembles. Elle sert à
exprimer des formules logiques qui permettent de filtrer les ensembles de départ pour
créer de nouveaux ensembles (autrement dit, filtrer les enregistrements d’une relation).

Cependant, tout élément d’un enregistrement n’est pas forcément connu à un instant t.
Les filtres et les jointures doivent donc gérer trois états lors d’un calcul de prédicat : vrai,
faux ou inconnu.

98
3. INTRODUCTION ET PREMIERS SELECT

3.2.7 ACID

• Atomicité (Atomic)
• Cohérence (Consistent)
• Isolation (Isolated)
• Durabilité (Durable)

Les propriétés ACID (acronyme de Atomic Consistent Isolated Durable) sont le fondement
même de toute base de données. Il s’agit de quatre règles fondamentales que toute trans-
action doit respecter :

• A : Une transaction est entière : « tout ou rien ».


• C : Une transaction amène la base d’un état stable à un autre.
• I : Les transactions n’agissent pas les unes sur les autres.
• D : Une transaction validée provoque des changements permanents.

Les propriétés ACID sont quatre propriétés essentielles d’un sous-système de traitement
de transactions d’un système de gestion de base de données. On considère parfois
que seuls les SGBD qui respectent ces quatre propriétés sont dignes d’être considérées
comme des bases de données relationnelles. Les SGBD de la famille des NoSQL (Mon-
goDB, Cassandra, BigTable...) sont en effet des bases de données, mais ne respectent
pas la Cohérence. Elles sont cohérentes à terme, ou en anglais eventually consistent, mais
la cohérence en fin de transaction n’est pas garantie.

3.2.8 LANGAGE SQL

• Norme ISO 9075


– dernière version stable : 2016
• Langage déclaratif
– on décrit le résultat et pas la façon de l’obtenir
– comme Prolog
• Traitement ensembliste
– par opposition au traitement procédural
– « on effectue des opérations sur des relations pour obtenir des relations »

Le langage SQL a été normalisé par l’ANSI en 1986 et est devenu une norme ISO inter-
nationale en 1987. Elle a subi plusieurs évolutions dans le but d’ajouter des fonctionnal-
ités correspondant aux attentes de l’industrie logicielle. Parmi ces améliorations, notons
l’intégration de quelques fonctionnalités objets pour le modèle relationnel-objet.

99
https://dalibo.com/formations
SQL pour PostgreSQL

3.2.9 SQL EST UN LANGAGE

• Langage
– règles d’écriture
– règles de formatage
– commentaires
• Améliore la lisibilité d’une requête

Il n’y a pas de règles établies concernant l’écriture de requêtes SQL. Il faut néanmoins avoir
à l’esprit qu’il s’agit d’un langage à part entière et, au même titre que ce qu’un développeur
fait avec n’importe quel code source, il convient de l’écrire de façon lisible.

3.2.10 RECOMMANDATIONS D'ÉCRITURE ET DE FORMATAGE

• Écriture
– mots clés SQL en MAJUSCULES
– identifiants de colonnes/tables en minuscule
• Formatage
– dissocier les éléments d’une requête
– un prédicat par ligne
– indentation

Quelle est la requête la plus lisible ?

celle-ci ?
select groupeid,datecreationitem from itemagenda where typeitemagenda = 5 and
groupeid in(12225,12376) and datecreationitem > now() order by groupeid,
datecreationitem ;

ou celle-ci ?
SELECT groupeid, datecreationitem
FROM itemagenda
WHERE typeitemagenda = 5
AND groupeid IN (12225,12376)
AND datecreationitem > now()
ORDER BY groupeid, datecreationitem;

Cet exemple est tiré du forum postgresql.fr67 .

67
https://forum.postgresql.fr/viewtopic.php?id=2610

100
3. INTRODUCTION ET PREMIERS SELECT

3.2.11 COMMENTAIRES

• Commentaire sur le reste de la ligne


-- commentaire
• Commentaire dans un bloc
/* bloc
*/

Une requête SQL peut être commentée au même titre qu’un programme standard.

Le marqueur -- permet de signifier à l’analyseur syntaxique que le reste de la ligne est


commenté, il n’en tiendra donc pas compte dans son analyse de la requête.

Un commentaire peut aussi se présenter sous la forme d’un bloc de commentaire, le bloc
pouvant occuper plusieurs lignes :
/* Ceci est un commentaire
sur plusieurs
lignes
*/

Aucun des éléments compris entre le marqueur de début de bloc /* et le marqueur de


fin de bloc */ ne sera pris en compte. Certains SGBDR propriétaires utilisent ces com-
mentaires pour y placer des informations (appelées hints sur Oracle) qui permettent
d’influencer le comportement de l’optimiseur, mais PostgreSQL ne possède pas ce genre
de mécanisme.

3.2.12 LES 4 TYPES D'ORDRES SQL

• DDL
– Data Definition Language
– définit les structures de données
• DML
– Data Manipulation Language
– manipule les données
• DCL
– Data Control Language
– contrôle l’accès aux données
• TCL
– Transaction Control Language
– contrôle les transactions
– implicites si « autocommit »
101
https://dalibo.com/formations
SQL pour PostgreSQL

Le langage SQL est divisé en quatre sous-ensembles qui ont chacun un but différent.

Les ordres DDL (pour Data Definition Language) permettent de définir les structures
de données. On retrouve les ordres suivants :

• CREATE : crée un objet


• ALTER : modifie la définition d’un objet
• DROP : supprime un objet
• TRUNCATE : vide un objet
• COMMENT : ajoute un commentaire sur un objet

Les ordres DML (pour Data Manipulation Language) permettent l’accès et la modifica-
tion des données. On retrouve les ordres suivants :

• SELECT : lit les données d’une ou plusieurs tables


• INSERT : ajoute des données dans une table
• UPDATE : modifie les données d’une table
• DELETE : supprime les données d’une table

Les ordres DCL (pour Data Control Language) permettent de contrôler l’accès aux don-
nées. Ils permettent plus précisément de donner ou retirer des droits à des utilisateurs
ou des groupes sur les objets de la base de données :

• GRANT : donne un droit d’accès à un rôle sur un objet


• REVOKE : retire un droit d’accès d’un rôle sur un objet

Enfin, les ordres TCL (pour Transaction Control Language) permettent de contrôler
les transactions :

• BEGIN : ouvre une transaction


• COMMIT : valide les traitements d’une transaction
• ROLLBACK : annule les traitements d’une transaction
• SAVEPOINT : crée un point de reprise dans une transaction
• SET TRANSACTION : modifie les propriétés d’une transaction en cours

Les ordres BEGIN et COMMIT sont souvent implicites dans le cas d’ordres isolés, si l’« au-
tocommit » est activé. Vous devez entrer donc manuellement BEGIN ; / COMMIT ; pour
faire des transactions de plus d’un ordre. C’est en fait dépendant de l’outil client, et psql
a un paramètre autocommit à on par défaut. Mais ce n’est pas forcément le cas sur votre
configuration précise et le défaut peut être inversé sur d’autres bases de données (notam-
ment Oracle).

Le ROLLBACK est implicite en cas de sortie brutale.

Noter que, contrairement à d’autres bases (et surtout Oracle), PostgreSQL n’effectue pas
de COMMIT implicite sur certaines opérations : les ordres CREATE TABLE, DROP TABLE,

102
3. INTRODUCTION ET PREMIERS SELECT

TRUNCATE TABLE... sont transactionnels, n’effectuent aucun COMMIT et peuvent être an-
nulés par ROLLBACK.

3.3 LECTURE DE DONNÉES

• Ordre SELECT
– lecture d’une ou plusieurs tables
– ou appel de fonctions

La lecture des données se fait via l’ordre SELECT. Il permet de récupérer des données
d’une ou plusieurs tables (il faudra dans ce cas joindre les tables). Il permet aussi de faire
appel à des fonctions stockées en base.

3.3.1 SYNTAXE DE SELECT

SELECT expressions_colonnes
[ FROM elements_from ]
[ WHERE predicats ]
[ ORDER BY expressions_orderby ]
[ LIMIT limite ]
[ OFFSET offset ];

L’ordre SELECT est composé de différents éléments dont la plupart sont optionnels.
L’exemple de syntaxe donné ici n’est pas complet.

La syntaxe complète de l’ordre SELECT est disponible dans le manuel de PostgreSQL68 .

68
https://docs.postgresql.fr/current/sql-select.html

103
https://dalibo.com/formations
SQL pour PostgreSQL

3.3.2 LISTE DE SÉLECTION

• Description du résultat de la requête


– colonnes retournées
– renommage
– dédoublonnage

La liste de sélection décrit le format de la table virtuelle qui est retournée par l’ordre
SELECT. Les types de données des colonnes retournées seront conformes au type des
éléments donnés dans la liste de sélection.

3.3.3 COLONNES RETOURNÉES

• Liste des colonnes retournées


– expression
– séparées par une virgule
• Expression
– constante
– référence de colonne :
table.colonne
• opération sur des colonnes et/ou des constantes

La liste de sélection décrit le format de la table virtuelle qui est retournée par l’ordre
SELECT. Cette liste est composée d’expressions séparées par une virgule.

Chaque expression peut être une simple constante, peut faire référence à des colonnes
d’une table lue par la requête, et peut être un appel à une fonction.

Une expression peut être plus complexe. Par exemple, elle peut combiner plusieurs con-
stantes et/ou colonnes à l’aide d’opérations. Parmi les opérations les plus classiques,
les opérateurs arithmétiques classiques sont utilisables pour les données numériques.
L’opérateur de concaténation permet de concaténer des chaînes de caractères.

L’expression d’une colonne peut être une constante :

SELECT 1;
?column?
----------
1
(1 row)

Elle peut aussi être une référence à une colonne d’une table :

104
3. INTRODUCTION ET PREMIERS SELECT

SELECT appellation.libelle
FROM appellation;

Comme il n’y a pas d’ambiguïté avec la colonne libelle, la référence de la colonne


appellation.libelle peut être simplifiée en libelle :
SELECT libelle
FROM appellation;

Le SGBD saura déduire la table et la colonne mises en œuvre dans cette requête. Il faudra
néanmoins utiliser la forme complète table.colonne si la requête met en œuvre des
tables qui possèdent des colonnes qui portent des noms identiques.

Une requête peut sélectionner plusieurs colonnes. Dans ce cas, les expressions de
colonnes sont définies sous la forme d’une liste dont chaque élément est séparé par une
virgule :
SELECT id, libelle, region_id
FROM appellation;

Le joker * permet de sélectionner l’ensemble des colonnes d’une table, elles apparaitront
dans leur ordre physique (attention si l’ordre change !) :
SELECT *
FROM appellation;

Si une requête met en œuvre plusieurs tables, on peut choisir de retourner toutes les
colonnes d’une seule table :
SELECT appellation.*
FROM appellation;

Enfin, on peut récupérer un tuple entier de la façon suivante :


SELECT appellation
FROM appellation;

Une expression de colonne peut également être une opération, par exemple une addition :
SELECT 1 + 1;
?column?
----------
2
(1 row)

Ou une soustraction :
SELECT annee, nombre - 10
FROM stock;

105
https://dalibo.com/formations
SQL pour PostgreSQL

3.3.4 ALIAS DE COLONNE

• Renommage
– ou alias
– AS :
expression AS alias
• le résultat portera le nom de l’alias

Afin de pouvoir nommer de manière adéquate les colonnes du résultat d’une requête
SELECT, le mot clé AS permet de définir un alias de colonne. Cet alias sera utilisé dans le
résultat pour nommer la colonne en sortie :
SELECT 1 + 1 AS somme;
somme
-------
2
(1 row)

Cet alias n’est pas utilisable dans le reste de la requête (par exemple dans la clause WHERE).

3.3.5 DÉDOUBLONNAGE DES RÉSULTATS

SELECT DISTINCT expressions_colonnes...


• Dédoublonnage des résultats avant de les retourner
– à ne pas utiliser systématiquement

Par défaut, SELECT retourne tous les résultats d’une requête. Parfois, des doublons peu-
vent se présenter dans le résultat. La clause DISTINCT permet de les éviter en réalisant
un dédoublonnage des données avant de retourner le résultat de la requête.

Il faut néanmoins faire attention à l’utilisation systématique de la clause DISTINCT. En


effet, elle entraîne un tri systématique des données juste avant de retourner les résultats
de la requête, ce qui va consommer de la ressource mémoire, voire de la ressource disque
si le volume de données à trier est important. De plus, cela va augmenter le temps de
réponse de la requête du fait de cette opération supplémentaire.

En règle générale, la clause DISTINCT devient inutile lorsqu’elle doit trier un ensemble qui
contient des colonnes qui sont déjà uniques. Si une requête récupère une clé primaire,
les données sont uniques par définition. Le SELECT DISTINCT sera alors transformé en
simple SELECT.

106
3. INTRODUCTION ET PREMIERS SELECT

3.3.6 DÉRIVATION

• SQL permet de dériver les valeurs des colonnes


– opérations arithmétiques : +, -, /, *
– concaténation de chaînes : ||
– appel de fonction

Les constantes et valeurs des colonnes peuvent être dérivées selon le type des données
manipulées.

Les données numériques peuvent être dérivées à l’aide des opérateurs arithmétiques stan-
dards : +, -, /, *. Elles peuvent faire l’objet d’autres calculs à l’aide de fonctions internes
et de fonctions définies par l’utilisateur.

La requête suivante permet de calculer le volume total en litres de vin disponible dans le
stock du caviste :
SELECT SUM(c.contenance * s.nombre) AS volume_total
FROM stock s
JOIN contenant c
ON (contenant_id=c.id);

Les données de type chaînes de caractères peuvent être concaténées à l’aide de


l’opérateur dédié ||. Cet opérateur permet de concaténer deux chaînes de caractères
mais également des données numériques avec une chaîne de caractères.

Dans la requête suivante, l’opérateur de concaténation est utilisé pour ajouter l’unité. Le
résultat est ainsi implicitement converti en chaîne de caractères.
SELECT SUM(c.contenance * s.nombre) || ' litres' AS volume_total
FROM stock AS s
JOIN contenant AS c
ON (contenant_id=c.id);

De manière générale, il n’est pas recommandé de réaliser les opérations de formatage des
données dans la base de données. La base de données ne doit servir qu’à récupérer les
résultats, le formatage étant assuré par l’application.

Différentes fonctions sont également applicables aux chaînes de caractères, de même


qu’aux autres types de données.

107
https://dalibo.com/formations
SQL pour PostgreSQL

3.3.7 FONCTIONS UTILES

• Fonctions sur données temporelles :


– date et heure courante : now()
– âge : age(timestamp)
– extraire une partie d’une date : extract( 'year' FROM timestamp)
– ou date_part('Y',timestamp)
• Fonctions sur données caractères :
– longueur d’une chaîne de caractère : char_length(chaine)
• Compter les lignes : count(*)

Parmi les fonctions les plus couramment utilisés, la fonction now() permet d’obtenir la
date et l’heure courante. Elle ne prend aucun argument. Elle est souvent utilisée, notam-
ment pour affecter automatiquement la valeur de l’heure courante à une colonne.

La fonction age(timestamp) permet de connaître l’âge d’une date par rapport à la date
courante.

La fonction char_length(varchar) permet de connaître la longueur d’une chaîne de


caractère.

Enfin, la fonction count(*) permet de compter le nombre de lignes. Il s’agit d’une fonction
d’agrégat, il n’est donc pas possible d’afficher les valeurs d’autres colonnes sans faire appel
aux capacités de regroupement des lignes de SQL.

Exemples

Affichage de l’heure courante :

SELECT now();
now
------------------------------
2017-08-29 14:45:17.213097+02

Affichage de l’âge du 1er janvier 2000 :

SELECT age(date '2000-01-01');


age
------------------------
17 years 7 mons 28 days

Affichage de la longueur de la chaîne ”Dalibo” :

SELECT char_length('Dalibo');
char_length
-------------
6

108
3. INTRODUCTION ET PREMIERS SELECT

Affichage du nombre de lignes de la table vin :


SELECT count(*) FROM vin;
count
-------
6067

3.3.8 CLAUSE FROM

FROM expression_table [, expression_table ...]


• Description des tables mises en œuvre dans la requête
– une seule table
– plusieurs tables jointes
– sous-requête

La clause FROM permet de lister les tables qui sont mises en œuvres dans la requêtes
SELECT. Il peut s’agir d’une table physique, d’une vue ou d’une sous-requête. Le résultat
de leur lecture sera une table du point de vue de la requête qui la met en œuvre.

Plusieurs tables peuvent être mises en œuvre, généralement dans le cadre d’une jointure.

3.3.9 ALIAS DE TABLE

• mot-clé AS
– optionnel :
reference_table alias
• la table sera ensuite référencée par l’alias
reference_table [AS] alias
reference_table AS alias (alias_colonne1, ...)

De la même façon qu’on peut créer des alias de colonnes, on peut créer des alias de tables.
La table sera ensuite référencée uniquement par cet alias dans la requête. Elle ne pourra
plus être référencée par son nom réel. L’utilisation du nom réel provoquera d’ailleurs une
erreur.

Le mot clé AS permet de définir un alias de table. Le nom réel de la table se trouve à gauche,
l’alias se trouve à droite. L’exemple suivant définie un alias reg sur la table region :
SELECT id, libelle
FROM region AS reg;

Le mot clé AS est optionnel :


109
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT id, libelle


FROM region reg;

La requête suivante montre l’utilisation d’un alias pour les deux tables mises en œuvre
dans la requête. La table stock a pour alias s et la table contenant a pour alias c. Les deux
tables possèdent toutes les deux une colonnes id, ce qui peut poser une ambiguïté dans
la clause de jointure (ON (contenant_id=c.id)). La condition de jointure portant sur la
colonne contenant_id de la table stock, son nom est unique et ne porte pas à ambiguïté.
La condition de jointure porte également sur la colonne id de table contenant, il faut
préciser le nom complet de la colonne en utilisant le préfixe c pour la nommer : c.id.
SELECT SUM(c.contenance * s.nombre) AS volume_total
FROM stock s
JOIN contenant c
ON (contenant_id=c.id);

Enfin, la forme reference_table AS alias (alias_colonne1, ...) permet de


définir un alias de table et définir par la même occasion des alias de colonnes. Cette
forme est peu recommandé car les alias de colonnes dépendent de l’ordre physique de
ces colonnes. Cet ordre peut changer dans le temps et donc amener à des erreurs :
SELECT id_region, nom_region
FROM region AS reg (id_region, nom_region);

3.3.10 NOMMAGE DES OBJETS

• Noms d’objets convertis en minuscules


– Nom_Objet devient nom_objet
– certains nécessitent l’emploi de majuscules
• Le guillemet double " conserve la casse
– "Nom_Objet"

Avec PostgreSQL, les noms des objets sont automatiquement convertis en minuscule,
sauf s’ils sont englobés entre des guillemets doubles. Si jamais ils sont créés avec une
casse mixte en utilisant les guillemets doubles, chaque appel à cet objet devra utiliser la
bonne casse et les guillemets doubles. Il est donc conseillé d’utiliser une notation des
objets ne comprenant que des caractères minuscules.

Il est aussi préférable de ne pas utiliser d’accents ou de caractères exotiques dans les
noms des objets.

110
3. INTRODUCTION ET PREMIERS SELECT

3.3.11 CLAUSE WHERE

• Permet d’exprimer des conditions de filtrage


– prédicats
• Un prédicat est une opération logique
– renvoie vrai ou faux
• La ligne est présente dans le résultat
– si l’expression logique des prédicats est vraie

La clause WHERE permet de définir des conditions de filtrage des données. Ces conditions
de filtrage sont appelées des prédicats.

Après le traitement de la clause FROM, chaque ligne de la table virtuelle dérivée est vérifiée
avec la condition de recherche. Si le résultat de la vérification est positif (true), la ligne est
conservée dans la table de sortie, sinon (c’est-à-dire si le résultat est faux ou nul) la ligne
est ignorée.

La condition de recherche référence typiquement au moins une colonne de la table


générée dans la clause FROM ; ceci n’est pas requis mais, dans le cas contraire, la clause
WHERE n’aurait aucune utilité.

3.3.12 EXPRESSION ET OPÉRATEURS DE PRÉDICATS

• Comparaison
– =, <, >, <=, >=, <>
• Négation
– NOT
expression operateur_comparaison expression

Un prédicat est composé d’une expression qui est soumise à un opérateur de prédicat
pour être éventuellement comparé à une autre expression. L’opérateur de prédicat re-
tourne alors true si la condition est vérifiée ou false si elle ne l’est pas ou NULL si son
résultat ne peut être calculé.

Les opérateurs de comparaison sont les opérateurs de prédicats les plus souvent utilisés.
L’opérateur d’égalité = peut être utilisé pour vérifier l’égalité de l’ensemble des types de
données supportés par PostgreSQL. Il faudra faire attention à ce que les données com-
parées soient de même type.

L’opérateur <> signifie « pas égal à » et peut aussi s’écrire !=.


111
https://dalibo.com/formations
SQL pour PostgreSQL

L’opérateur NOT est une négation. Si un prédicat est vrai, l’opérateur NOT retournera faux.
À l’inverse, si un prédicat est faux, l’opérateur NOT retournera vrai. La clause NOT se place
devant l’expression entière.

Exemples

Sélection de la région dont l’identifiant est égal à 3 (et ensuite différent de 3) :

SELECT *
FROM region
WHERE id = 3;

SELECT *
FROM region
WHERE NOT id = 3;

3.3.13 COMBINER DES PRÉDICATS

• OU logique
– predicat OR predicat
• ET logique
– predicat AND predicat

Les opérateurs logiques OR et AND permettent de combiner plusieurs prédicats dans la


clause WHERE.

L’opérateur OR est un OU logique. Il retourne vrai si au moins un des deux prédicats


combinés est vrai. L’opérateur AND est un ET logique. Il retourne vrai si et seulement si
les deux prédicats combinés sont vrais.

Au même titre qu’une multiplication ou une division sont prioritaires sur une addition ou
une soustraction dans un calcul, l’évaluation de l’opérateur AND est prioritaire sur celle de
l’opérateur OR. Et, tout comme dans un calcul, il est possible de protéger les opérations
prioritaires en les encadrant de parenthèses.

Exemples

Dans le stock, affiche les vins dont le nombre de bouteilles est inférieur à 2 ou supérieur
à 16 :

SELECT *
FROM stock
WHERE nombre < 2
OR nombre > 16;

112
3. INTRODUCTION ET PREMIERS SELECT

3.3.14 CORRESPONDANCE DE MOTIF

• Comparaison de motif
chaine LIKE motif ESCAPE 'c'
• % : toute chaîne de 0 à plusieurs caractères
– _ : un seul caractère
• Expression régulière POSIX
chaine ~ motif

L’opérateur LIKE permet de réaliser une recherche simple sur motif. La chaîne exprimant
le motif de recherche peut utiliser deux caractères joker : _ et %. Le caractère _ prend la
place d’un caractère inconnu, qui doit toujours être présent. Le caractère % est un joker
qui permet d’exprimer que PostgreSQL doit trouver entre 0 et plusieurs caractères.

Exploiter la clause LIKE avec un motif sans joker ne présente pas d’intérêt. Il est préférable
dans ce cas d’utiliser l’opérateur d’égalité.

Le mot clé ESCAPE 'c' permet de définir un caractère d’échappement pour protéger les
caractères _ et % qui font légitimement partie de la chaîne de caractère du motif évalué.
Lorsque PostgreSQL rencontre le caractère d’échappement indiqué, les caractères _ et %
seront évalués comme étant les caractères _ et % et non comme des jokers.

L’opérateur LIKE dispose d’une déclinaison qui n’est pas sensible à la casse. Il s’agit de
l’opérateur ILIKE.

Exemples

Création d’un jeu d’essai :

CREATE TABLE motif (chaine varchar(30));


INSERT INTO motif (chaine) VALUES ('Durand'), ('Dupont'), ('Dupond'),
('Dupon'), ('Dupuis');

Toutes les chaînes commençant par la suite de caractères Dur :

SELECT * FROM motif WHERE chaine LIKE 'Dur%';


chaine
--------
Durand

Toutes les chaînes terminant par d :

SELECT * FROM motif WHERE chaine LIKE '%d';


chaine
--------
113
https://dalibo.com/formations
SQL pour PostgreSQL

Durand
Dupond

Toutes les chaînes qui commencent par Dupon suivi d’un caractère inconnu. La chaîne
Dupon devrait être ignorée :
SELECT * FROM motif WHERE chaine LIKE 'Dupon_';
chaine
------------
Dupont
Dupond

3.3.15 LISTES ET INTERVALLES

• Liste de valeurs
expression IN (valeur1 [, ...])
• Chevauchement d’intervalle de valeurs
expression BETWEEN expression AND expression
• Chevauchement d’intervalle de dates
(date1, date2) OVERLAPS (date3, date4)

La clause IN permet de vérifier que l’expression de gauche est égale à une valeur présente
dans l’expression de droite, qui est une liste d’expressions. La négation peut être utilisée
en utilisant la construction NOT IN.

L’opérateur BETWEEN permet de vérifier que la valeur d’une expression est comprise entre
deux bornes. Par exemple, l’expression valeur BETWEEN 1 AND 10 revient à exprimer la
condition suivante : valeur >= 1 AND valeur<= 10. La négation peut être utilisée en
utilisant la construction NOT BETWEEN.

Exemples

Recherche les chaînes qui sont présentes dans la liste IN :


SELECT * FROM motif WHERE chaine IN ('Dupont', 'Dupond', 'Ducobu');
chaine
--------
Dupont
Dupond

114
3. INTRODUCTION ET PREMIERS SELECT

3.3.16 TRIS

• SQL ne garantit pas l’ordre des résultats


– tri explicite requis
• Tris des lignes selon des expressions
ORDER BY expression [ ASC | DESC | USING opérateur ]
[ NULLS { FIRST | LAST } ] [, ...]
• ordre du tri : ASC ou DESC
– placement des valeurs NULL : NULLS FIRST ou NULLS LAST
– ordre de tri des caractères : COLLATE collation

La clause ORDER BY permet de trier les lignes du résultat d’une requête selon une ou
plusieurs expressions combinées.

L’expression la plus simple est le nom d’une colonne. Dans ce cas, les lignes seront triées
selon les valeurs de la colonne indiquée, et par défaut dans l’ordre ascendant, c’est-à-dire
de la valeur la plus petite à la plus grande pour une donnée numérique ou temporelle, et
dans l’ordre alphabétique pour une donnée textuelle.

Les lignes peuvent être triées selon une expression plus complexe, par exemple en déri-
vant la valeur d’une colonne.

L’ordre de tri peut être modifié à l’aide de la clause DESC qui permet un tri dans l’ordre
descendant, donc de la valeur la plus grande à la plus petite (ou alphabétique inverse le
cas échéant).

La clause NULLS permet de contrôler l’ordre d’apparition des valeurs NULL. La clause NULLS
FIRST permet de faire apparaître d’abord les valeurs NULL puis les valeurs non NULL selon
l’ordre de tri. La clause NULLS LAST permet de faire apparaître d’abord les valeurs non
NULL selon l’ordre de tri suivies par les valeurs NULL. Si cette clause n’est pas précisée,
alors PostgreSQL utilise implicitement NULLS LAST dans le cas d’un tri ascendant (ASC,
par défaut) ou NULLS FIRST dans le cas d’un tri descendant (DESC, par défaut).

Exemples

Tri de la table region selon le nom de la région :

SELECT *
FROM region
ORDER BY libelle;

Tri de la table stock selon le nombre de bouteille, dans l’ordre décroissant :

SELECT *
FROM stock
ORDER BY nombre DESC;
115
https://dalibo.com/formations
SQL pour PostgreSQL

Enfin, la clause COLLATE permet d’influencer sur l’ordre de tri des chaînes de caractères.

3.3.17 LIMITER LE RÉSULTAT

• Obtenir des résultats à partir de la ligne n


– OFFSET n
• Limiter le nombre de lignes à n lignes
– FETCH {FIRST | NEXT} n ROWS ONLY
– LIMIT n
• Opérations combinables
– OFFSET doit apparaitre avant FETCH
• Peu d’intérêt sur des résultats non triés

La clause OFFSET permet d’exclure les n premières lignes du résultat. Toutes les autres
lignes sont ramenées.

La clause FETCH permet de limiter le résultat d’une requête. La requête retournera au


maximum n lignes de résultats. Elle en retournera moins, voire aucune, si la requête ne
peut ramener suffisamment de lignes. La clause FIRST ou NEXT est obligatoire mais le
choix de l’une ou l’autre n’a aucune conséquence sur le résultat.

La clause FETCH est synonyme de la clause LIMIT. Mais LIMIT est une clause propre à
PostgreSQL et quelques autres SGBD. Il est recommandé d’utiliser FETCH pour se con-
former au standard.

Ces deux opérations peuvent être combinées. La norme impose de faire apparaître la
clause OFFSET avant la clause FETCH. PostgreSQL permet néanmoins d’exprimer ces
clauses dans un ordre différent, mais la requête ne pourra pas être portée sur un autre
SGBD sans transformation.

Il faut faire attention au fait que ces fonctions ne permettent pas d’obtenir des résultats
stables si les données ne sont pas triées explicitement. En effet, le standard SQL ne
garantie en aucune façon l’ordre des résultats à moins d’employer la clause ORDER BY.

Exemples

La fonction generate_series permet de générer une suite de valeurs numériques. Par


exemple, une suite comprise entre 1 et 10 :
SELECT * FROM generate_series(1, 10);
generate_series
-----------------
1

116
3. INTRODUCTION ET PREMIERS SELECT

(...)
10
(10 rows)

La clause FETCH permet donc de limiter le nombre de lignes du résultats :

SELECT * FROM generate_series(1, 10) FETCH FIRST 5 ROWS ONLY;


generate_series
-----------------
1
2
3
4
5
(5 rows)

La clause LIMIT donne un résultat équivalent :

SELECT * FROM generate_series(1, 10) LIMIT 5;


generate_series
-----------------
1
2
3
4
5
(5 rows)

La clause OFFSET 4 permet d’exclure les quatre premières lignes et de retourner les autres
lignes du résultat :

SELECT * FROM generate_series(1, 10) OFFSET 4;


generate_series
-----------------
5
6
7
8
9
10
(6 rows)

Les clauses LIMIT et OFFSET peuvent être combinées pour ramener les deux lignes en
excluant les quatre premières :

SELECT * FROM generate_series(1, 10) OFFSET 4 LIMIT 2;


generate_series
-----------------
5
117
https://dalibo.com/formations
SQL pour PostgreSQL

6
(2 rows)

3.3.18 UTILISER PLUSIEURS TABLES

• Clause FROM
– liste de tables séparées par ,
• Une table est combinée avec une autre
– jointure
– produit cartésien

Il est possible d’utiliser plusieurs tables dans une requête SELECT. Lorsque c’est le cas, et
sauf cas particulier, on fera correspondre les lignes d’une table avec les lignes d’une autre
table selon certains critères. Cette mise en correspondance s’appelle une jointure et les
critères de correspondances s’appellent une condition de jointure.

Si aucune condition de jointure n’est donnée, chaque ligne de la première table est mise en
correspondance avec toutes les lignes de la seconde table. C’est un produit cartésien. En
général, un produit cartésien n’est pas souhaitable et est généralement le résultat d’une
erreur de conception de la requête.

Exemples

Création d’un jeu de données simple :

CREATE TABLE mere (id integer PRIMARY KEY, val_mere text);


CREATE TABLE fille (
id_fille integer PRIMARY KEY,
id_mere integer REFERENCES mere(id),
val_fille text
);

INSERT INTO mere (id, val_mere) VALUES (1, 'mere 1');


INSERT INTO mere (id, val_mere) VALUES (2, 'mere 2');

INSERT INTO fille (id_fille, id_mere, val_fille) VALUES (1, 1, 'fille 1');
INSERT INTO fille (id_fille, id_mere, val_fille) VALUES (2, 1, 'fille 2');

Pour procéder à une jointure entre les tables mere et fille, les identifiants id_mere de
la table fille doivent correspondre avec les identifiants id de la table mere :
SELECT * FROM mere, fille
WHERE mere.id = fille.id_mere;
id | val_mere | id_fille | id_mere | val_fille

118
3. INTRODUCTION ET PREMIERS SELECT

----+----------+----------+---------+-----------
1 | mere 1 | 1 | 1 | fille 1
1 | mere 1 | 2 | 1 | fille 2
(2 rows)

Un produit cartésien est créé en omettant la condition de jointure, le résultat n’a plus de
sens :
SELECT * FROM mere, fille;
id | val_mere | id_fille | id_mere | val_fille
----+----------+----------+---------+-----------
1 | mere 1 | 1 | 1 | fille 1
1 | mere 1 | 2 | 1 | fille 2
2 | mere 2 | 1 | 1 | fille 1
2 | mere 2 | 2 | 1 | fille 2
(4 rows)

3.4 TYPES DE DONNÉES

• Type de données
– du standard SQL
– certains spécifiques PostgreSQL

PostgreSQL propose l’ensemble des types de données du standard SQL, à l’exception du


type BLOB qui a toutefois un équivalent. Mais PostgreSQL a été conçu pour être exten-
sible et permet de créer facilement des types de données spécifiques. C’est pourquoi
PostgreSQL propose un certain nombre de types de données spécifiques qui peuvent
être intéressants.

3.4.1 QU'EST-CE QU'UN TYPE DE DONNÉES ?

• Le système de typage valide les données


• Un type détermine
– les valeurs possibles
– comment les données sont stockées
– les opérations que l’on peut appliquer

On utilise des types de données pour représenter une information de manière pertinente.
Les valeurs possibles d’une donnée vont dépendre de son type. Par exemple, un entier
long ne permet par exemple pas de coder des valeurs décimales. De la même façon,
119
https://dalibo.com/formations
SQL pour PostgreSQL

un type entier ne permet pas de représenter une chaîne de caractère, mais l’inverse est
possible.

L’intérêt du typage des données est qu’il permet également à la base de données de valider
les données manipulées. Ainsi un entier integer permet de représenter des valeurs com-
prises entre -2,147,483,648 et 2,147,483,647. Si l’utilisateur tente d’insérer une don-
née qui dépasse les capacités de ce type de données, une erreur lui sera retournée. On
retrouve ainsi la notion d’intégrité des données. Comme pour les langages de program-
mation fortement typés, cela permet de détecter davantage d’erreurs, plus tôt : à la com-
pilation dans les langages typés, ou ici des la première exécution d’une requête, plutôt
que plus tard, quand une chaîne de caractère ne pourra pas être convertie à la volée en
entier par exemple.

Le choix d’un type de données va également influencer la façon dont les données sont
représentées. En effet, toute donnée à une représentation textuelle et une représentation
en mémoire et sur disque. Ainsi, un integer est représenté sous la forme d’une suite de 4
octets, manipulables directement par le processeur, alors que sa représentation textuelle
est une suite de caractères. Cela a une implication forte sur les performances de la base
de données.

Le type de données choisi permet également de déterminer les opérations que l’on pourra
appliquer. Tous les types de données permettent d’utiliser des opérateurs qui leur sont
propres. Ainsi il est possible d’additionner des entiers, de concaténer des chaînes de
caractères, etc. Si une opération ne peut être réalisée nativement sur le type de données,
il faudra utiliser des conversions coûteuses. Vaut-il mieux additionner deux entiers issus
d’une conversion d’une chaîne de caractère vers un entier ou additionner directement
deux entiers ? Vaut-il mieux stocker une adresse IP avec un varchar ou avec un type de
données dédié ?

Il est à noter que l’utilisateur peut contrôler lui-même certains types de données
paramétrés. Le paramètre représente la longueur ou la précision du type de données.
Ainsi, un type varchar(15) permettra de représenter des chaînes de caractères de 15
caractères maximum.

120
3. INTRODUCTION ET PREMIERS SELECT

3.4.2 TYPES DE DONNÉES

• Types standards SQL


• Types dérivés
• Types spécifiques à PostgreSQL
• Types utilisateurs

Les types de données standards permettent de traiter la plupart des situations qui peu-
vent survenir. Dans certains cas, il peut être nécessaire de faire appel aux types spéci-
fiques à PostgreSQL, par exemple pour stocker des adresses IP avec le type spécifique
et bénéficier par la même occasion de toutes les classes d’opérateurs qui permettent de
manipuler simplement ce type de données.

Et si cela ne s’avère pas suffisant, PostgreSQL permet à l’utilisateur de créer lui-même


ses propres types de données, ainsi que les classes d’opérateurs et fonctions permettant
d’indexer ces données.

3.4.3 TYPES STANDARDS (1)

• Caractère
– char, varchar
• Numérique
– integer, smallint, bigint
– real, double precision
– numeric, decimal
• Booléen
– boolean

Le standard SQL propose des types standards pour stocker des chaînes de caractères
(de taille fixe ou variable), des données numériques (entières, à virgule flottante) et des
booléens.

121
https://dalibo.com/formations
SQL pour PostgreSQL

3.4.4 TYPES STANDARDS (2)

• Temporel
– date, time
– timestamp
– interval
• Chaînes de bit
– bit, bit varying
• Formats validés
– JSON
– XML

Le standard SQL propose également des types standards pour stocker des éléments tem-
porels (date, heure, la combinaison des deux avec ou sans fuseau horaire, intervalle).

D’utilisation plus rare, SQL permet également de stocker des chaînes de bit et des don-
nées validées au format XML. Le format JSON est de plus en plus courant.

3.4.5 CARACTÈRES

• char(n)
– longueur fixe
– de n caractères
– complété à droite par des espaces si nécessaire
• varchar(n)
– longueur variable
– maximum n caractères
– n optionnel

Le type char(n) permet de stocker des chaînes de caractères de taille fixe, donnée par
l’argument n. Si la chaîne que l’on souhaite stocker est plus petite que la taille donnée à la
déclaration de la colonne, elle sera complétée par des espaces à droite. Si la chaîne que
l’on souhaite stocker est trop grande, une erreur sera levée.

Le type varchar(n) permet de stocker des chaînes de caractères de taille variable. La


taille maximale de la chaîne est donnée par l’argument n. Toute chaîne qui excèdera cette
taille ne sera pas prise en compte et génèrera une erreur. Les chaînes de taille inférieure
à la taille limite seront stockées sans altérations.

La longueur de chaîne est mesurée en nombre de caractères sous PostgreSQL. Ce n’est


pas forcément le cas dans d’autres SGBD.

122
3. INTRODUCTION ET PREMIERS SELECT

3.4.6 REPRÉSENTATION DONNÉES CARACTÈRES

• Norme SQL
– chaîne encadrée par '
– 'chaîne de caractères'
• Chaînes avec échappement du style C
– chaîne précédée par E ou e
– E'chaîne de caractères'
• Chaînes avec échappement Unicode
– chaîne précédée par U&
– U&'chaîne de caractères'

La norme SQL définit que les chaînes de caractères sont représentées encadrées de
guillemets simples (caractère '). Le guillemet double (caractère ") ne peut être utilisé
car il sert à protéger la casse des noms d’objets. PostgreSQL interprétera alors la chaîne
comme un nom d’objet et générera une erreur.

Une représentation correcte d’une chaîne de caractères est donc de la forme suivante :

'chaîne de caractères'

Les caractères ' doivent être doublés s’ils apparaissent dans la chaîne :

'J''ai acheté des croissants'

Une extension de la norme par PostgreSQL permet d’utiliser les méta-caractères des lan-
gages tels que le C, par exemple \n pour un retour de ligne, \t pour une tabulation, etc. :

E'chaîne avec un retour \nde ligne et une \ttabulation'

3.4.7 NUMÉRIQUES

123
https://dalibo.com/formations
SQL pour PostgreSQL

• Entier
– smallint, integer, bigint
– signés
• Virgule flottante
– real, double precision
– valeurs inexactes
• Précision arbitraire
– numeric(precision, echelle), decimal(precision, echelle)
– valeurs exactes

Le standard SQL propose des types spécifiques pour stocker des entiers signés. Le type
smallint permet de stocker des valeurs codées sur 2 octets, soit des valeurs comprises
entre -32768 et +32767. Le type integer ou int, codé sur 4 octets, permet de stocker
des valeurs comprises entre -2147483648 et +2147483647. Enfin, le type bigint, codé
sur 8 octets, permet de stocker des valeurs comprises entre -9223372036854775808
et 9223372036854775807. Le standard SQL ne propose pas de stockage d’entiers non
signés.

Le standard SQL permet de stocker des valeurs décimales en utilisant les types à virgules
flottantes. Avant de les utiliser, il faut avoir à l’esprit que ces types de données ne permet-
tent pas de stocker des valeurs exactes, des différences peuvent donc apparaître entre la
donnée insérée et la donnée restituée. Le type real permet d’exprimer des valeurs à vir-
gules flottantes sur 4 octets, avec une précision relative de six décimales. Le type double
precision permet d’exprimer des valeurs à virgules flottantes sur huit octets, avec une
précision relative de 15 décimales.

Beaucoup d’applications, notamment les applications financières, ne se satisfont pas de


valeurs inexactes. Pour cela, le standard SQL propose le type numeric, ou son synonyme
decimal, qui permet de stocker des valeurs exactes, selon la précision arbitraire donnée.
Dans la déclaration numeric(precision, echelle), la partie precision indique com-
bien de chiffres significatifs sont stockés, la partie echelle exprime le nombre de chiffres
après la virgule. Au niveau du stockage, PostgreSQL ne permet pas d’insérer des valeurs
qui dépassent les capacités du type déclaré. En revanche, si l’échelle de la valeur à stocker
dépasse l’échelle déclarée de la colonne, alors sa valeur est simplement arrondie.

On peut aussi utiliser numeric sans aucune contrainte de taille, pour stocker de façon
exacte n’importe quel nombre.

124
3. INTRODUCTION ET PREMIERS SELECT

3.4.8 REPRÉSENTATION DE DONNÉES NUMÉRIQUES

• Chiffres décimaux : 0 à 9
• Séparateur décimal : .
• chiffres
• chiffres.[chiffres][e[+-]chiffres]
• [chiffres].chiffres[e[+-]chiffres]
• chiffrese[+-]chiffres
• Conversion
– TYPE 'chaine'

Au moins un chiffre doit être placé avant ou après le point décimal, s’il est utilisé. Au
moins un chiffre doit suivre l’indicateur d’exponentiel (caractère e), s’il est présent. Il peut
ne pas y avoir d’espaces ou d’autres caractères imbriqués dans la constante. Notez que
tout signe plus ou moins en avant n’est pas forcément considéré comme faisant part de
la constante ; il est un opérateur appliqué à la constante.

Les exemples suivants montrent différentes représentations valides de constantes


numériques :

42
3.5
4.
.001
5e2
1.925e-3

Une constante numérique contenant soit un point décimal soit un exposant est tout
d’abord présumée du type integer si sa valeur est contenue dans le type integer (4
octets). Dans le cas contraire, il est présumé de type bigint si sa valeur entre dans un
type bigint (8 octets). Dans le cas contraire, il est pris pour un type numeric. Les con-
stantes contenant des points décimaux et/ou des exposants sont toujours présumées de
type numeric.

Le type de données affecté initialement à une constante numérique est seulement un


point de départ pour les algorithmes de résolution de types. Dans la plupart des cas,
la constante sera automatiquement convertie dans le type le plus approprié suivant le
contexte. Si nécessaire, vous pouvez forcer l’interprétation d’une valeur numérique sur
un type de données spécifiques en la convertissant. Par exemple, vous pouvez forcer une
valeur numérique à être traitée comme un type real (float4) en écrivant :
REAL '1.23'

125
https://dalibo.com/formations
SQL pour PostgreSQL

3.4.9 BOOLÉENS

• boolean
• 3 valeurs possibles
– TRUE
– FALSE
– NULL (ie valeur absente)

Le type boolean permet d’exprimer des valeurs booléennes, c’est-à-dire une valeur exp-
rimant vrai ou faux. Comme tous les types de données en SQL, une colonne booléenne
peut aussi ne pas avoir de valeur, auquel cas sa valeur sera NULL.

Un des intérêts des types booléens est de pouvoir écrire :


SELECT * FROM ma_table WHERE valide;
SELECT * FROM ma_table WHERE not consulte;

3.4.10 TEMPOREL

• Date
– date
• Heure
– time
– avec ou sans fuseau horaire
• Date et heure
– timestamp
– avec ou sans fuseau horaire
• Intervalle de temps
– interval

Le type date exprime une date. Ce type ne connaît pas la notion de fuseau horaire.

Le type time exprime une heure. Par défaut, il ne connaît pas la notion de fuseau ho-
raire. En revanche, lorsque le type est déclaré comme time with time zone, il prend en
compte un fuseau horaire. Mais cet emploi n’est pas recommandé. En effet, une heure
convertie d’un fuseau horaire vers un autre pose de nombreux problèmes. En effet, le
décalage horaire dépend également de la date : quand il est 6h00, heure d’été, à Paris, il
est 21H00 sur la côte Pacifique aux États-Unis mais encore à la date de la veille.

Le type timestamp permet d’exprimer une date et une heure. Par défaut, il ne connaît pas
la notion de fuseau horaire. Lorsque le type est déclaré timestamp with time zone, il
est adapté aux conversions d’heure d’un fuseau horaire vers un autre car le changement

126
3. INTRODUCTION ET PREMIERS SELECT

de date sera répercuté dans la composante date du type de données. Il est précis à la
microseconde.

Le format de saisie et de restitution des dates et heures dépend du paramètre DateStyle.


La documentation de ce paramètre permet de connaître les différentes valeurs possibles.
Il reste néanmoins recommandé d’utiliser les fonctions de formatage de date qui permet-
tent de rendre l’application indépendante de la configuration du SGBD.

La norme ISO (ISO-8601) impose le format de date « année-mois-jour ». La norme SQL est
plus permissive et permet de restituer une date au format « jour/mois/année » si DateStyle
est égal à 'SQL, DMY'.
SET datestyle = 'ISO, DMY';

SELECT current_timestamp;
now
-------------------------------
2017-08-29 16:11:58.290174+02

SET datestyle = 'SQL, DMY';

SELECT current_timestamp;
now
--------------------------------
29/08/2017 16:12:25.650716 CEST

3.4.11 REPRÉSENTATION DES DONNÉES TEMPORELLES

• Conversion explicite
– TYPE 'chaine'
• Format d’un timestamp
– 'YYYY-MM-DD HH24:MI:SS.ssssss'
– 'YYYY-MM-DD HH24:MI:SS.ssssss+fuseau'
– 'YYYY-MM-DD HH24:MI:SS.ssssss' AT TIME ZONE 'fuseau'
• Format d’un intervalle
– INTERVAL 'durée interval'

Expression d’une date, forcément sans gestion du fuseau horaire :


DATE '2017-08-29'

Expression d’une heure sans fuseau horaire :


TIME '10:20:10'
127
https://dalibo.com/formations
SQL pour PostgreSQL

Ou, en spécifiant explicitement l’absence de fuseau horaire :

TIME WITHOUT TIME ZONE '10:20:10'

Expression d’une heure, avec fuseau horaire invariant. Cette forme est déconseillée :

TIME WITH TIME ZONE '10:20:10' AT TIME ZONE 'CEST'

Expression d’un timestamp sans fuseau horaire :

TIMESTAMP '2017-08-29 10:20:10'

Ou, en spécifiant explicitement l’absence de fuseau horaire :

TIMESTAMP WITHOUT TIME ZONE '2017-08-29 10:20:10'

Expression d’un timestamp avec fuseau horaire, avec microseconde :

TIMESTAMP WITH TIME ZONE '2017-08-29 10:20:10.123321'


AT TIME ZONE 'Europe/Paris'

Expression d’un intervalle d’une journée :

INTERVAL '1 day'

Il est possible de cumuler plusieurs expressions :

INTERVAL '1 year 1 day'

Les valeurs possibles sont :

• YEAR pour une année ;


• MONTH pour un mois ;
• DAY pour une journée ;
• HOUR pour une heure ;
• MINUTE pour une minute ;
• SECOND pour une seconde.

3.4.12 GESTION DES FUSEAUX HORAIRES

• Paramètre timezone
• Session : SET TIME ZONE
• Expression d’un fuseau horaire
– nom complet : 'Europe/Paris'
– nom abbrégé : 'CEST'
– décalage : '+02'

128
3. INTRODUCTION ET PREMIERS SELECT

Le paramètre timezone du postgresql.conf permet de positionner le fuseau horaire de


l’instance PostgreSQL. Elle est initialisée par défaut en fonction de l’environnement du
système d’exploitation.

Le fuseau horaire de l’instance peut également être défini au cours de la session à l’aide
de la commande SET TIME ZONE.

La France utilise deux fuseaux horaires normalisés. Le premier, CET, correspond à Central
European Time ou autrement dit à l’heure d’hiver en Europe centrale. Le second, CEST,
correspond à Central European Summer Time, c’est-à-dire l’heure d’été en Europe centrale.

La liste des fuseaux horaires supportés est disponible dans la table système pg_timezone_names :

SELECT * FROM pg_timezone_names ;


name | abbrev | utc_offset | is_dst
----------------------------------+--------+------------+--------
GB | BST | 01:00:00 | t
ROK | KST | 09:00:00 | f
Greenwich | GMT | 00:00:00 | f
(...)

Il est possible de positionner le fuseau horaire au niveau de la session avec l’ordre SET
TIME ZONE :

SET TIME ZONE "Europe/Paris";

SELECT now();
now
-------------------------------
2017-08-29 10:19:56.640162+02

SET TIME ZONE "Europe/Kiev";

SELECT now();
now
-------------------------------
2017-08-29 11:20:17.199983+03

Conversion implicite d’une donnée de type timestamp dans le fuseau horaire courant :

SET TIME ZONE "Europe/Kiev";

SELECT TIMESTAMP WITH TIME ZONE '2017-08-29 10:20:10 CEST';


timestamptz
------------------------
2017-08-29 11:20:10+03

Conversion explicite d’une donnée de type timestamp dans un autre fuseau horaire :
129
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT '2017-08-29 06:00:00' AT TIME ZONE 'US/Pacific';


timezone
---------------------
28/08/2017 21:00:00

3.4.13 CHAÎNES DE BITS

• Chaînes de bits
– bit(n), bit varying(n)

Les types bit et bit varying permettent de stocker des masques de bits. Le type bit(n)
est à longueur fixe alors que le type bit varying(n) est à longueur variable mais avec
un maximum de n bits.

3.4.14 REPRÉSENTATION DES CHAÎNES DE BITS

• Représentation binaire
– Chaîne de caractères précédée de la lettre B
– B'01010101'
• Représentation hexadécimale
– Chaîne de caractères précédée de la lettre X
– X'55'

3.4.15 XML

• Type validé
– xml
• Chaîne de caractères
– validation du document XML

Le type xml permet de stocker des documents XML. Par rapport à une chaîne de carac-
tères simple, le type xml apporte la vérification de la structure du document XML ainsi
que des fonctions de manipulations spécifiques (voir la documentation officielle69 ).

69
https://docs.postgresql.fr/current/functions-xml.html

130
3. INTRODUCTION ET PREMIERS SELECT

3.4.16 JSON

• Type json : texte, avec validation du format JSON


• Préférer le type jsonb (binaire)
• Fonctions de manipulation

Les types json et jsonb permettent de stocker des documents JSON. Ces deux types
permettent de vérifier la structure du document JSON ainsi que des fonctions de ma-
nipulations spécifiques (voir la documentation officielle70 ). On préférera de loin le type
jsonb pour son stockage optimisé (en binaire), et ses fonctionnalités supplémentaires,
notamment en terme d’indexation.

3.4.17 TYPES DÉRIVÉS

• Types spécifiques à PostgreSQL


• Sériés
– principe de l’« autoincrement »
– serial
– smallserial
– bigserial
– équivalent à un type entier associé à une séquence et avec une valeur par
défaut
– (v 10+) préférer un type entier + la propriété IDENTITY
• Caractères
– text

Les types smallserial, serial et bigserial permettent d’obtenir des fonctionnalités


similaires aux types autoincrement rencontrés dans d’autres SGBD.

Néanmoins, ces types restent assez proches de la norme car ils définissent au final une
colonne qui utilise un type et des objets standards. Selon le type dérivé utilisé, la colonne
sera de type smallint, integer ou bigint. Une séquence sera également créée et la
colonne prendra pour valeur par défaut la prochaine valeur de cette séquence.

Il est à noter que la notion d’identité apparaît en version 10 et qu’il est préférable de
passer par cette contrainte que par ces types dérivés.

Attention : ces types n’interdisent pas l’insertion manuelle de doublons. Une contrainte
de clé primaire explicite reste nécessaire pour les éviter.
70
https://docs.postgresql.fr/current/functions-json.html

131
https://dalibo.com/formations
SQL pour PostgreSQL

Le type text est l’équivalent du type varchar mais sans limite de taille de la chaîne de
caractère.

3.4.18 TYPES ADDITIONNELS NON SQL

• bytea
• array
• enum
• cidr, inet, macaddr
• uuid
• json, jsonb, hstore
• range

Les types standards ne sont pas toujours suffisants pour représenter certaines données.
À l’instar d’autres SGBDR, PostgreSQL propose des types de données pour répondre à
certains besoins.

On notera le type bytea qui permet de stocker des objets binaires dans une table. Le
type array permet de stocker des tableaux et enum des énumérations.

Les types json et hstore permettent de stocker des documents non structurés dans la
base de données. Le premier au format JSON, le second dans un format de type clé/-
valeur. Le type hstore est d’ailleurs particulièrement efficace car il dispose de méthodes
d’indexation et de fonctions de manipulations performantes. Le type json a été completé
par jsonb qui permet de stocker un document JSON binaire et optimisé, et d’accéder à
une propriété sans désérialiser intégralement le document.

Le type range permet de stocker des intervalles de données. Ces données sont ensuite
manipulables par un jeu d’opérateurs dédiés et par le biais de méthodes d’indexation per-
mettant d’accélérer les recherches.

132
3. INTRODUCTION ET PREMIERS SELECT

3.4.19 TYPES UTILISATEURS

• Types utilisateurs
– composites
– énumérés (enum)
– intervalles (range)
– scalaires
– tableau
CREATE TYPE

PostgreSQL permet de créer ses propres types de données. Les usages les plus courants
consistent à créer des types composites pour permettre à des fonctions de retourner des
données sous forme tabulaire (retour de type SETOF).

L’utilisation du type énuméré (enum) nécessite aussi la création d’un type spécifique. Le
type sera alors employé pour déclarer les objets utilisant une énumération.

Enfin, si l’on souhaite étendre les types intervalles (range) déjà disponibles, il est néces-
saire de créer un type spécifique.

La création d’un type scalaire est bien plus marginale. Elle permet en effet d’étendre les
types fournis par PostgreSQL mais nécessite d’avoir des connaissances fines des mécan-
ismes de PostgreSQL. De plus, dans la majeure partie des cas, les types standards suffisent
en général à résoudre les problèmes qui peuvent se poser à la conception.

Quant aux types tableaux, ils sont créés implicitement par PostgreSQL quand un utilisa-
teur crée un type personnalisé.

Exemples

Utilisation d’un type enum :

CREATE TYPE arc_en_ciel AS ENUM (


'red', 'orange', 'yellow', 'green', 'blue', 'purple'
);

CREATE TABLE test (id integer, couleur arc_en_ciel);

INSERT INTO test (id, couleur) VALUES (1, 'red');

INSERT INTO test (id, couleur) VALUES (2, 'pink');


ERROR: invalid input value for enum arc_en_ciel: "pink"
LINE 1: INSERT INTO test (id, couleur) VALUES (2, 'pink');

Création d’un type interval float8_range :

CREATE TYPE float8_range AS RANGE (subtype = float8, subtype_diff = float8mi);


133
https://dalibo.com/formations
SQL pour PostgreSQL

3.5 CONCLUSION

• SQL : traitement d’ensembles d’enregistrements


• Pour les lectures : SELECT
• Nom des objets en minuscules
• Des types de données simples et d’autres plus complexes

Le standard SQL permet de traiter des ensembles d’enregistrements. Un enregistrement


correspond à une ligne dans une relation. Il est possible de lire ces relations grâce à l’ordre
SELECT.

3.5.1 BIBLIOGRAPHIE

• Bases de données - de la modélisation au SQL (Laurent Audibert)


• SQL avancé : programmation et techniques avancées (Joe Celko)
• SQL : Au coeur des performances (Markus Winand)
• The Manga Guide to Databases (Takahashi, Mana, Azuma, Shoko)
• The Art of SQL (Stéphane Faroult)

Bases de données - de la modélisation au SQL

• Auteur : Laurent Audibert


• Éditeur : Ellipses
• ISBN : 978-2729851200

Ce livre présente les notions essentielles pour modéliser une base de données et utiliser
le langage SQL pour utiliser les bases de données créées. L’auteur appuie ses exercices
sur PostgreSQL.

SQL avancé : programmation et techniques avancées

• Auteur : Joe Celko


• Editeur : Vuibert
• ISBN : 978-2711786503

Ce livre est écrit par une personne ayant participé à l’élaboration du standard SQL. Il
a souhaité montré les bonnes pratiques pour utiliser le SQL pour résoudre un certain
nombre de problèmes de tous les jours. Le livre s’appuie cependant sur la norme SQL-92,

134
3. INTRODUCTION ET PREMIERS SELECT

voire SQL-89. L’édition anglaise SQL for Smarties est bien plus à jour. Pour les anglophones,
la lecture de l’ensemble des livres de Joe Celko est particulièrement recommandée.

SQL : Au coeur des performances

• Auteur : Markus Winand


• Éditeur : auto-édité
• ISBN : 978-3950307832
• site Internet71

Il s’agit du livre de référence sur les performances en SQL. Il dresse un inventaire des
différents cas d’utilisation des index par la base de données, ce qui permettra de mieux
prévoir l’indexation dès la conception. Ce livre s’adresse à un public avancé.

The Manga Guide to Databases

• Auteur : Takahashi, Mana, Azuma, Shoko


• Éditeur : No Starch Press
• ASIN : B00BUFN70E

The Art of SQL

• Auteur : Stéphane Faroult


• Éditeur : O’Reilly
• ISBN : 978-0-596-00894-9
• ISBN : 978-0-596-15971-9 (e-book)

Ce livre s’adresse également à un public avancé. Il présente également les bonnes pra-
tiques lorsque l’on utilise une base de données.

3.5.2 QUESTIONS

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

71
https://use-the-index-luke.com/fr

135
https://dalibo.com/formations
SQL pour PostgreSQL

3.6 TRAVAUX PRATIQUES

Ce TP utilise la base tpc. La base tpc peut être téléchargée depuis https://dali.bo/tp_tpc
(dump de 31 Mo, pour 267 Mo sur le disque au final). Auparavant créer les utilisateurs
depuis le script sur https://dali.bo/tp_tpc_roles.
$ psql < tpc_roles.sql # Exécuter le script de création des rôles
$ createdb --owner tpc_owner tpc # Création de la base
$ pg_restore -d tpc tpc.dump # Une erreur sur un schéma 'public' existant est normale

Les mots de passe sont dans le script. Pour vous connecter :


$ psql -U tpc_admin -h localhost -d tpc

Le schéma suivant montre les différentes tables de la base :

Figure 2: Schéma base tpc

1. Afficher l’heure courante, au méridien de Greenwich.

2. Afficher la date et l’heure qu’il sera dans 1 mois et 1 jour.

3. Ajouter 1 au nombre de type réel ’1.42’. Pourquoi ce résultat ? Quel type de don-
nées permet d’obtenir un résultat correct ?

136
3. INTRODUCTION ET PREMIERS SELECT

4. Afficher le contenu de la table pays en classant les pays dans l’ordre alphabétique.

5. Afficher les pays contenant la lettre a, majuscule ou minuscule. Plusieurs solutions


sont possibles.

6. Afficher le nombre lignes de commandes dont la quantité commandée est comprise


entre 5 et 10.

7. Pour chaque pays, afficher son nom et la région du monde dont il fait partie.

nom_pays | nom_region
-------------------------------+---------------------------
ALGÉRIE | Afrique
(...)

8. Afficher le nombre total de clients français et allemands.

Sortie attendue :

count
-------
12418

9. Afficher le numéro de commande et le nom du client ayant passé la commande. Seul


un sous-ensemble des résultats sera affiché : les 20 premières lignes du résultat
seront exclues et seules les 20 suivantes seront affichées. Il faut penser à ce que le
résultat de cette requête soit stable entre plusieurs exécutions.

Sortie attendue :

numero_commande | nom_client
-----------------+--------------
67 | Client112078
68 | Client33842
(...)

10. Afficher les noms et codes des pays qui font partie de la région « Europe ».

Sortie attendue :

nom_pays | code_pays
-----------------------+-----------
ALLEMAGNE | DE
(...)

11. Pour chaque pays, afficher une chaîne de caractères composée de son nom, suivi
entre parenthèses de son code puis, séparé par une virgule, du nom de la région
dont il fait partie.
137
https://dalibo.com/formations
SQL pour PostgreSQL

Sortie attendue :

detail_pays
--------------------------------------------------
ALGÉRIE (DZ), Afrique
(...)

12. Pour les clients ayant passé des commandes durant le mois de janvier 2011, affichez
les identifiants des clients, leur nom, leur numéro de téléphone et le nom de leur
pays.

Sortie attendue :

client_id | nom | telephone | nom_pays


-----------+--------------+-----------------+-------------------------------
83279 | Client83279 | 12-835-574-2048 | JAPON

13. Pour les dix premières commandes de l’année 2011, afficher le numéro de la com-
mande, la date de la commande ainsi que son âge.

Sortie attendue :

numero_commande | date_commande | age


-----------------+---------------+---------------------------
11364 | 2011-01-01 | 1392 days 15:25:19.012521
(...)

138
3. INTRODUCTION ET PREMIERS SELECT

3.7 TRAVAUX PRATIQUES (SOLUTIONS)

1. Afficher l’heure courante, au méridien de Greenwich :


SELECT now() AT TIME ZONE 'GMT';

2. Afficher la date et l’heure qu’il sera dans 1 mois et 1 jour


SELECT now() + INTERVAL '1 month 1 day';

3. Ajouter 1 au nombre de type réel ’1.42’. Pourquoi ce résultat ?


SELECT REAL '1.42' + 1 AS resultat;
resultat
------------------
2.41999995708466
(1 row)

Le type de données real est un type numérique à virgule flottante, codé sur 4 octets. Il
n’offre pas une précision suffisante pour les calculs précis. Son seul avantage est la vitesse
de calcul. Pour effectuer des calculs précis, il vaut mieux privilégier le type de données
numeric.

4. Afficher le contenu de la table pays en classant les pays dans l’ordre alphabétique.
SELECT * FROM pays ORDER BY nom_pays;

5. Afficher les pays contenant la lettre a, majuscule ou minuscule :


SELECT * FROM pays WHERE lower(nom_pays) LIKE '%a%';

SELECT * FROM pays WHERE nom_pays ILIKE '%a%';

SELECT * FROM pays WHERE nom_pays LIKE '%a%' OR nom_pays LIKE '%A%';

En terme de performances, la seconde variante sera plus rapide sur un volume de données
important si l’on dispose du bon index. La taille de la table pays ne permet pas d’observer
de différence significative sur cette requête.

6. Afficher le nombre lignes de commandes dont la quantité commandée est comprise


entre 5 et 10 :
SELECT count(*)
FROM lignes_commandes
WHERE quantite BETWEEN 5 AND 10;

Autre écriture possible :


SELECT count(*)
FROM lignes_commandes
WHERE quantite >= 5
AND quantite <= 10;
139
https://dalibo.com/formations
SQL pour PostgreSQL

7. Pour chaque pays, afficher son nom et la région du monde dont il fait partie :
SELECT nom_pays, nom_region
FROM pays p, regions r
WHERE p.region_id = r.region_id;

8. Afficher le nombre total de clients français et allemands :


SELECT count(*)
FROM clients cl, contacts cn, pays p
WHERE cl.contact_id = cn.contact_id
AND cn.code_pays = p.code_pays
AND p.nom_pays IN ('FRANCE', 'ALLEMAGNE');

À noter que cette syntaxe est obsolète, il faut utiliser la clause JOIN, plus lisible et plus
complète, qui sera vue plus loin :
SELECT count(*)
FROM clients cl
JOIN contacts cn ON (cl.contact_id = cn.contact_id)
JOIN pays p ON (cn.code_pays = p.code_pays)
WHERE p.nom_pays IN ('FRANCE', 'ALLEMAGNE');

En connaissant les codes de ces pays, il est possible d’éviter la lecture de la table pays :
SELECT count(*)
FROM clients cl, contacts cn
WHERE cl.contact_id = cn.contact_id
AND cn.code_pays IN ('FR', 'DE');

L’équivalent avec la syntaxe JOIN serait :


SELECT count(*)
FROM clients cl
JOIN contacts cn ON (cl.contact_id = cn.contact_id)
WHERE cn.code_pays IN ('FR', 'DE');

9. Afficher le numéro de commande et le nom du client ayant passé la commande. Seul


un sous-ensemble des résultats sera affiché : les 20 premières lignes du résultat
seront exclues et seules les 20 suivantes seront affichées. Il faut penser à ce que le
résultat de cette requête soit stable entre plusieurs exécutions.

La syntaxe normalisée SQL impose d’écrire la requête de la façon suivante. La stabilité


du résultat de la requête est garantie par un tri explicite, s’il n’est pas précisé, la base de
données va retourner les lignes dans l’ordre physique qui est susceptible de changer entre
deux exécutions :
SELECT numero_commande, nom AS nom_client
FROM commandes cm, clients cl, contacts cn
WHERE cm.client_id = cl.client_id

140
3. INTRODUCTION ET PREMIERS SELECT

AND cl.contact_id = cn.contact_id


ORDER BY numero_commande
FETCH FIRST 20 ROWS ONLY
OFFSET 20;

Mais PostgreSQL supporte également la clause LIMIT :


SELECT numero_commande, nom AS nom_client
FROM commandes cm, clients cl, contacts cn
WHERE cm.client_id = cl.client_id
AND cl.contact_id = cn.contact_id
ORDER BY numero_commande
LIMIT 20
OFFSET 20;

Et l’équivalent avec la syntaxe JOIN serait :


SELECT numero_commande, nom AS nom_client
FROM commandes cm
JOIN clients cl ON (cm.client_id = cl.client_id)
JOIN contacts cn ON (cl.contact_id = cn.contact_id)
ORDER BY numero_commande
LIMIT 20
OFFSET 20;

10. Afficher les noms et codes des pays qui font partie de la région « Europe ».
SELECT nom_pays, code_pays
FROM regions r, pays p
WHERE r.region_id = p.region_id
AND r.nom_region = 'Europe';

Et l’équivalent avec la syntaxe JOIN serait :


SELECT nom_pays, code_pays
FROM regions r
JOIN pays p ON (r.region_id = p.region_id)
WHERE r.nom_region = 'Europe';

11. Pour chaque pays, afficher une chaîne de caractères composée de son nom, suivi
entre parenthèses de son code puis, séparé par une virgule, du nom de la région
dont il fait partie.
SELECT nom_pays || ' (' || code_pays || '), ' || nom_region
FROM regions r, pays p
WHERE r.region_id = p.region_id;

Et l’équivalent avec la syntaxe JOIN serait :


SELECT nom_pays || ' (' || code_pays || '), ' || nom_region
FROM regions r
141
https://dalibo.com/formations
SQL pour PostgreSQL

JOIN pays p ON (r.region_id = p.region_id);

12. Pour les clients ayant passé des commandes durant le mois de janvier 2011, affichez
les identifiants des clients, leur nom, leur numéro de téléphone et le nom de leur
pays.
SELECT cl.client_id, nom, telephone, nom_pays
FROM clients cl, commandes cm, contacts cn, pays p
WHERE cl.client_id = cm.client_id
AND cl.contact_id = cn.contact_id
AND cn.code_pays = p.code_pays
AND date_commande BETWEEN '2011-01-01' AND '2011-01-31';

Le troisième module de la formation abordera les jointures et leurs syntaxes. À l’issue de


ce prochain module, la requête de cet exercice pourrait être écrite de la façon suivante :
SELECT cl.client_id, nom, telephone, nom_pays
FROM clients cl
JOIN commandes cm
USING (client_id)
JOIN contacts co
USING (contact_id)
JOIN pays p
USING (code_pays)
WHERE date_commande BETWEEN '2011-01-01' AND '2011-01-31';

13. Pour les dix premières commandes de l’année 2011, afficher le numéro de la com-
mande, la date de la commande ainsi que son âge.
SELECT numero_commande, date_commande, now() - date_commande AS age
FROM commandes
WHERE date_commande BETWEEN '2011-01-01' AND '2011-12-31'
ORDER BY date_commande
LIMIT 10;

142
4. CRÉATION D’OBJET ET MISES À JOUR

4 CRÉATION D'OBJET ET MISES À JOUR

4.1 INTRODUCTION

• DDL, gérer les objets


• DML, écrire des données
• Gérer les transactions

Le module précédent nous a permis de voir comment lire des données à partir de requêtes
SQL. Ce module a pour but de présenter la création et la gestion des objets dans la base
de données (par exemple les tables), ainsi que l’ajout, la suppression et la modification de
données.

Une dernière partie sera consacrée aux transactions.

4.1.1 MENU

• DDL (Data Definition Language)


• DML (Data Manipulation Language)
• TCL (Transaction Control Language)

4.1.2 OBJECTIFS

• Savoir créer, modifier et supprimer des objets


• Savoir utiliser les contraintes d’intégrité
• Savoir mettre à jour les données
• Savoir utiliser les transactions

143
https://dalibo.com/formations
SQL pour PostgreSQL

4.2 DDL

• DDL
– Data Definition Language
– langage de définition de données
• Permet de créer des objets dans la base de données

Les ordres DDL (acronyme de Data Definition Language) permettent de créer des ob-
jets dans la base de données et notamment la structure de base du standard SQL : les
tables.

4.2.1 OBJETS D'UNE BASE DE DONNÉES

• Objets définis par la norme SQL :


– schémas
– séquences
– tables
– contraintes
– domaines
– vues
– fonctions
– triggers

La norme SQL définit un certain nombre d’objets standards qu’il est possible de créer
en utilisant les ordres DDL. D’autres types d’objets existent bien entendu, comme les
domaines. Les ordres DDL permettent également de créer des index, bien qu’ils ne soient
pas définis dans la norme SQL.

La seule structure de données possible dans une base de données relationnelle est la
table.

144
4. CRÉATION D’OBJET ET MISES À JOUR

4.2.2 CRÉER DES OBJETS

• Ordre CREATE
• Syntaxe spécifique au type d’objet
• Exemple :
CREATE SCHEMA s1;

La création d’objet passe généralement par l’ordre CREATE. La syntaxe dépend fortement
du type d’objet. Voici trois exemples :
CREATE SCHEMA s1;
CREATE TABLE t1 (c1 integer, c2 text);
CREATE SEQUENCE s1 INCREMENT BY 5 START 10;

Pour créer un objet, il faut être propriétaire du schéma ou de la base auquel appartiendra
l’objet ou avoir le droit CREATE sur le schéma ou la base.

4.2.3 MODIFIER DES OBJETS

• Ordre ALTER
• Syntaxe spécifique pour modifier la définition d’un objet, exemple:
• renommage
ALTER type_objet ancien_nom RENAME TO nouveau_nom ;
• changement de propriétaire
ALTER type_objet nom_objet OWNER TO proprietaire ;
• changement de schéma
ALTER type_objet nom_objet SET SCHEMA nom_schema ;

Modifier un objet veut dire modifier ses propriétés. On utilise dans ce cas l’ordre ALTER.
Il faut être propriétaire de l’objet pour pouvoir le faire.

Deux propriétés sont communes à tous les objets : le nom de l’objet et son propriétaire.
Deux autres sont fréquentes et dépendent du type de l’objet : le schéma et le tablespace.
Les autres propriétés dépendent directement du type de l’objet.

145
https://dalibo.com/formations
SQL pour PostgreSQL

4.2.4 SUPPRIMER DES OBJETS

• Ordre DROP
• Exemples :
– supprimer un objet :
DROP type_objet nom_objet ;
– supprimer un objet et ses dépendances :
DROP type_objet nom_objet CASCADE ;

Seul un propriétaire peut supprimer un objet. Il utilise pour cela l’ordre DROP. Pour les
objets ayant des dépendances, l’option CASCADE permet de tout supprimer d’un coup.
C’est très pratique, et c’est en même temps très dangereux : il faut donc utiliser cette
option à bon escient.

4.2.5 SCHÉMA

• Identique à un espace de nommage


• Permet d’organiser les tables de façon logique
• Possibilité d’avoir des objets de même nom dans des schémas différents
• Pas d’imbrication (contrairement à des répertoires par exemple)
• Schéma public
– créé par défaut dans une base de données PostgreSQL

La notion de schéma dans PostgreSQL est à rapprocher de la notion d’espace de nom-


mage (ou namespace) de certains langages de programmation. Le catalogue système qui
contient la définition des schémas dans PostgreSQL s’appelle d’ailleurs pg_namespace.

Les schémas sont utilisés pour répartir les objets de façon logique, suivant un schéma
interne à l’entreprise. Ils servent aussi à faciliter la gestion des droits (il suffit de révoquer
le droit d’utilisation d’un schéma à un utilisateur pour que les objets contenus dans ce
schéma ne soient plus accessibles à cet utilisateur).

Un schéma public est créé par défaut dans toute nouvelle base de données. Tout le
monde a le droit d’y créer des objets. Il est cependant possible de révoquer ce droit ou
supprimer ce schéma.

146
4. CRÉATION D’OBJET ET MISES À JOUR

4.2.6 GESTION D'UN SCHÉMA

• CREATE SCHEMA nom_schéma


• ALTER SCHEMA nom_schéma
– renommage
– changement de propriétaire
• DROP SCHEMA [ IF EXISTS ] nom_schéma [ CASCADE ]

L’ordre CREATE SCHEMA permet de créer un schéma. Il suffit de lui spécifier le nom du
schéma. CREATE SCHEMA offre d’autres possibilités qui sont rarement utilisées.

L’ordre ALTER SCHEMA nom_schema RENAME TO nouveau_nom_schema permet de


renommer un schéma. L’ordre ALTER SCHEMA nom_schema OWNER TO proprietaire
permet de donner un nouveau propriétaire au schéma.

Enfin, l’ordre DROP SCHEMA permet de supprimer un schéma. La clause IF EXISTS permet
d’éviter la levée d’une erreur si le schéma n’existe pas (très utile dans les scripts SQL). La
clause CASCADE permet de supprimer le schéma ainsi que tous les objets qui sont posi-
tionnés dans le schéma.

Exemples

Création d’un schéma reference :


CREATE SCHEMA reference;

Une table peut être créée dans ce schéma :


CREATE TABLE reference.communes (
commune text,
codepostal char(5),
departement text,
codeinsee integer
);

La suppression directe du schéma ne fonctionne pas car il porte encore la table communes :
DROP SCHEMA reference;
ERROR: cannot drop schema reference because other objects depend on it
DETAIL: table reference.communes depends on schema reference
HINT: Use DROP ... CASCADE to drop the dependent objects too.

L’option CASCADE permet de supprimer le schéma et ses objets dépendants :


DROP SCHEMA reference CASCADE;
NOTICE: drop cascades to table reference.communes

147
https://dalibo.com/formations
SQL pour PostgreSQL

4.2.7 ACCÈS AUX OBJETS

• Nommage explicite
– nom_schema.nom_objet
• Chemin de recherche de schéma
– paramètre search_path
– SET search_path = schema1,schema2,public;
– par défaut : $user, public

Le paramètre search_path permet de définir un chemin de recherche pour pouvoir


retrouver les tables dont le nom n’est pas qualifié par le nom de son schéma. PostgreSQL
procèdera de la même façon que le système avec la variable $PATH : il recherche la
table dans le premier schéma listé. S’il trouve une table portant ce nom dans le schéma,
il préfixe le nom de table avec celui du schéma. S’il ne trouve pas de table de ce
nom dans le schéma, il effectue la même opération sur le prochain schéma de la liste
du search_path. S’il n’a trouvé aucune table de ce nom dans les schémas listés par
search_path, PostgreSQL lève une erreur.

Comme beaucoup d’autres paramètres, le search_path peut être positionné à différents


endroits. Par défaut, il est assigné à $user, public, c’est-à-dire que le premier schéma
de recherche portera le nom de l’utilisateur courant, et le second schéma de recherche
est public.

On peut vérifier la variable search_path à l’aide de la commande SHOW :

SHOW search_path;
search_path
----------------
"$user",public
(1 row)

Pour obtenir une configuration particulière, la variable search_path peut être position-
née dans le fichier postgresql.conf :

search_path = '"$user",public'

Cette variable peut aussi être positionnée au niveau d’un utilisateur. Chaque fois que
l’utilisateur se connectera, il prendra le search_path de sa configuration spécifique :

ALTER ROLE nom_role SET search_path = "$user", public;

Cela peut aussi se faire au niveau d’une base de données. Chaque fois qu’un utilisateur
se connectera à la base, il prendra le search_path de cette base, sauf si l’utilisateur a déjà
une configuration spécifique :

ALTER DATABASE nom_base SET search_path = "$user", public;

148
4. CRÉATION D’OBJET ET MISES À JOUR

La variable search_path peut également être positionnée pour un utilisateur particulier,


dans une base particulière :

ALTER ROLE nom_role IN DATABASE nom_base SET search_path = "$user", public;

Enfin, la variable search_path peut être modifiée dynamiquement dans la session avec
la commande SET :

SET search_path = "$user", public;

Avant la version 9.3, les requêtes préparées et les fonctions conservaient en mémoire
le plan d’exécution des requêtes. Ce plan ne faisait plus référence aux noms des ob-
jets mais à leurs identifiants. Du coup, un search_path changeant entre deux exécu-
tions d’une requête préparée ou d’une fonction ne permettait pas de cibler une table
différente. Voici un exemple le montrant :
-- création des objets
CREATE SCHEMA s1;
CREATE SCHEMA s2;
CREATE TABLE s1.t1 (c1 text);
CREATE TABLE s2.t1 (c1 text);
INSERT INTO s1.t1 VALUES('schéma s1');
INSERT INTO s2.t1 VALUES('schéma s2');

SELECT * FROM s1.t1;


c1
-----------
schéma s1
(1 row)

SELECT * FROM s2.t1;


c1
-----------
schéma s2
(1 row)

-- il y a bien des données différentes dans chaque table

SET search_path TO s1;


PREPARE req AS SELECT * FROM t1;

EXECUTE req;
c1
-----------
schéma s1
(1 row)

SET search_path TO s2;


149
https://dalibo.com/formations
SQL pour PostgreSQL

EXECUTE req;
c1
-----------
schéma s1
(1 row)

-- malgré le changement de search_path, nous en sommes toujours


-- aux données de l'autre table

b1=# SELECT * FROM t1;


c1
-----------
schéma s2
(1 row)
Dans ce cas, il est préférable de configurer le paramètre search_path directement au
niveau de la fonction.
À partir de la version 9.3, dès que le search_path change, les plans en cache sont sup-
primés (dans le cas de la fonction) ou recréés (dans le cas des requêtes préparées).

4.2.8 SÉQUENCES

• Séquence
– génère une séquence de nombres
• Paramètres
– valeur minimale MINVALUE
– valeur maximale MAXVALUE
– valeur de départ START
– incrément INCREMENT
– cache CACHE
– cycle autorisé CYCLE

Les séquences sont des objets standards qui permettent de générer des séquences de
valeur. Elles sont utilisées notamment pour générer un numéro unique pour un identifiant
ou, plus rarement, pour disposer d’un compteur informatif, mis à jour au besoin.

Le cache de la séquence a pour effet de générer un certain nombre de valeurs en mé-


moire afin de les mettre à disposition de la session qui a utilisé la séquence. Même si les
valeurs pré-calculées ne sont pas consommées dans la session, elles seront consommées
au niveau de la séquence. Cela peut avoir pour effet de créer des trous dans les séquences
d’identifiants et de consommer très rapidement les numéros de séquence possibles. Le
cache de séquence n’a pas besoin d’être ajusté sur des applications réalisant de petites

150
4. CRÉATION D’OBJET ET MISES À JOUR

transactions. Il permet en revanche d’améliorer les performances sur des applications qui
utilisent massivement des numéros de séquences, notamment pour réaliser des insertions
massives.

4.2.9 CRÉATION D'UNE SÉQUENCE

CREATE SEQUENCE nom [ INCREMENT incrément ]


[ MINVALUE valeurmin | NO MINVALUE ]
[ MAXVALUE valeurmax | NO MAXVALUE ]
[ START [ WITH ] début ]
[ CACHE cache ]
[ [ NO ] CYCLE ]
[ OWNED BY { nom_table.nom_colonne | NONE } ]

La syntaxe complète est donnée dans le slide.

Le mot clé TEMPORARY ou TEMP permet de définir si la séquence est temporaire. Si tel est
le cas, elle sera détruite à la déconnexion de l’utilisateur.

Le mot clé INCREMENT définit l’incrément de la séquence, MINVALUE, la valeur minimale de


la séquence et MAXVALUE, la valeur maximale. START détermine la valeur de départ initiale
de la séquence, c’est-à-dire juste après sa création. La clause CACHE détermine le cache
de séquence. CYCLE permet d’indiquer au SGBD que la séquence peut reprendre son
compte à MINVALUE lorsqu’elle aura atteint MAXVALUE. La clause NO CYCLE indique que
le rebouclage de la séquence est interdit, PostgreSQL lèvera alors une erreur lorsque la
séquence aura atteint son MAXVALUE. Enfin, la clause OWNED BY détermine l’appartenance
d’une séquence à une colonne d’une table. Ainsi, si la colonne est supprimée, la séquence
sera implicitement supprimée.

Exemple de séquence avec rebouclage :


CREATE SEQUENCE testseq INCREMENT BY 1 MINVALUE 3 MAXVALUE 5 CYCLE START WITH 4;

SELECT nextval('testseq');
nextval
---------
4

SELECT nextval('testseq');
nextval
---------
5

SELECT nextval('testseq');
151
https://dalibo.com/formations
SQL pour PostgreSQL

nextval
---------
3

4.2.10 MODIFICATION D'UNE SÉQUENCE

ALTER SEQUENCE nom [ INCREMENT increment ]


[ MINVALUE valeurmin | NO MINVALUE ]
[ MAXVALUE valeurmax | NO MAXVALUE ]
[ START [ WITH ] début ]
[ RESTART [ [ WITH ] nouveau_début ] ]
[ CACHE cache ] [ [ NO ] CYCLE ]
[ OWNED BY { nom_table.nom_colonne | NONE } ]
• Il est aussi possible de modifier
– le propriétaire
– le schéma

Les propriétés de la séquence peuvent être modifiés avec l’ordre ALTER SEQUENCE.

La séquence peut être affectée à un nouveau propriétaire :


ALTER SEQUENCE [ IF EXISTS ] nom OWNER TO nouveau_propriétaire

Elle peut être renommée :


ALTER SEQUENCE [ IF EXISTS ] nom RENAME TO nouveau_nom

Enfin, elle peut être positionnée dans un nouveau schéma :


ALTER SEQUENCE [ IF EXISTS ] nom SET SCHEMA nouveau_schema

4.2.11 SUPPRESSION D'UNE SÉQUENCE

DROP SEQUENCE nom [, ...]

Voici la syntaxe complète de DROP SEQUENCE :


DROP SEQUENCE [ IF EXISTS ] nom [, ...] [ CASCADE | RESTRICT ]

Le mot clé CASCADE permet de supprimer la séquence ainsi que tous les objets dépendants
(par exemple la valeur par défaut d’une colonne).

152
4. CRÉATION D’OBJET ET MISES À JOUR

4.2.12 SÉQUENCES, UTILISATION

• Obtenir la valeur suivante


– nextval('nom_sequence')
• Obtenir la valeur courante
– currval('nom_sequence')
– mais nextval() doit être appelé avant dans la même session

La fonction nextval() permet d’obtenir le numéro de séquence suivant. Son comporte-


ment n’est pas transactionnel. Une fois qu’un numéro est consommé, il n’est pas possible
de revenir dessus, malgré un ROLLBACK de la transaction. La séquence est le seul objet à
avoir un comportement de ce type.

La fonction currval() permet d’obtenir le numéro de séquence courant, mais son usage
nécessite d’avoir utilisé nextval() dans la session.

Il est possible d’interroger une séquence avec une requête SELECT. Cela permet d’obtenir
des informations sur la séquence, dont la dernière valeur utilisée dans la colonne
last_value. Cet usage n’est pas recommandé en production et doit plutôt être utilisé à
titre informatif.

Exemples

Utilisation d’une séquence simple :

CREATE SEQUENCE testseq


INCREMENT BY 1 MINVALUE 10 MAXVALUE 20 START WITH 15 CACHE 1;

SELECT currval('testseq');
ERROR: currval of sequence "testseq" is not yet defined in this session

SELECT * FROM testseq ;


- [ RECORD 1 ]-+--------
sequence_name | testseq
last_value | 15
start_value | 15
increment_by | 1
max_value | 20
min_value | 10
cache_value | 5
log_cnt | 0
is_cycled | f
is_called | f

153
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT nextval('testseq');
nextval
---------
15
(1 row)

SELECT currval('testseq');
currval
---------
15

SELECT nextval('testseq');
nextval
---------
16
(1 row)

ALTER SEQUENCE testseq RESTART WITH 5;


ERROR: RESTART value (5) cannot be less than MINVALUE (10)

DROP SEQUENCE testseq;

Utilisation d’une séquence simple avec cache :


CREATE SEQUENCE testseq INCREMENT BY 1 CACHE 10;

SELECT nextval('testseq');
nextval
---------
1

Déconnexion et reconnexion de l’utilisateur :


SELECT nextval('testseq');
nextval
---------
11

Suppression en cascade d’une séquence :

CREATE TABLE t2 (id serial);

\d t2
Table "s2.t2"
Column | Type | Modifiers
--------+---------+-------------------------------------------------

154
4. CRÉATION D’OBJET ET MISES À JOUR

id | integer | not null default nextval('t2_id_seq'::regclass)

DROP SEQUENCE t2_id_seq;


ERROR: cannot drop sequence t2_id_seq because other objects depend on it
DETAIL: default for table t2 column id depends on sequence t2_id_seq
HINT: Use DROP ... CASCADE to drop the dependent objects too.

DROP SEQUENCE t2_id_seq CASCADE;


NOTICE: drop cascades to default for table t2 column id

\d t2
Table "s2.t2"
Column | Type | Modifiers
--------+---------+-----------
id | integer | not null

4.2.13 TYPE SERIAL

• Type serial/bigserial/smallserial
– séquence générée automatiquement
– valeur par défaut nextval(...)
• (v 10+) Préférer un entier avec IDENTITY

Certaines bases de données offrent des colonnes auto-incrémentées (autoincrement de


MySQL ou identity de SQL Server).

PostgreSQL ne possède identity qu’à partir de la v 10. Jusqu’en 9.6 on pourra utiliser
serial un équivalent qui s’appuie sur les séquences et la possibilité d’appliquer une valeur
par défaut à une colonne.

Par exemple, si l’on crée la table suivante :


CREATE TABLE exemple_serial (
id SERIAL PRIMARY KEY,
valeur INTEGER NOT NULL
);

On s’aperçoit que table a été créée telle que demandé, mais qu’une séquence a aussi été
créée. Elle porte un nom dérivé de la table associé à la colonne correspondant au type
serial, terminé par seq :
postgres=# \d
List of relations
Schema | Name | Type | Owner
--------+-----------------------+----------+--------
155
https://dalibo.com/formations
SQL pour PostgreSQL

public | exemple_serial | table | thomas


public | exemple_serial_id_seq | sequence | thomas

En examinant plus précisément la définition de la table, on s’aperçoit que la colonne id


porte une valeur par défaut qui correspond à l’appel de la fonction nextval() sur la
séquence qui a été créée implicitement :
postgres=# \d exemple_serial
Table "public.exemple_serial"
Column | Type | Modifiers
--------+---------+-------------------------------------------------------------
id | integer | not null default nextval('exemple_serial_id_seq'::regclass)
valeur | integer | not null
Indexes:
"exemple_serial_pkey" PRIMARY KEY, btree (id)

smallserial et bigserial sont des variantes de serial s’appuyant sur des types
d’entiers plus courts ou plus longs.

4.2.14 DOMAINES

• Permet d’associer
– un type standard
– et une contrainte (optionnelle)

Un domaine est utilisé pour définir un type utilisateur qui est en fait un type utilisateur
standard accompagné de la définition de contraintes particulières.

Les domaines sont utiles pour ramener la définition de contraintes communes à plusieurs
colonnes sur un seul objet. La maintenance en est ainsi facilitée.

L’ordre CREATE DOMAIN permet de créer un domaine, ALTER DOMAIN permet de modifier
sa définition, et enfin, DROP DOMAIN permet de supprimer un domaine.

Exemples

Gestion d’un domaine salaire :


-- ajoutons le domaine et la table
CREATE DOMAIN salaire AS integer CHECK (VALUE > 0);
CREATE TABLE employes (id serial, nom text, paye salaire);

\d employes
Table « public.employes »
Colonne | Type | NULL-able | Par défaut
---------+---------+------------+--------------------------------------

156
4. CRÉATION D’OBJET ET MISES À JOUR

id | integer | not null | nextval('employes_id_seq'::regclass)


nom | text | |
paye | salaire | |

-- insérons des données dans la nouvelle table


INSERT INTO employes (nom, paye) VALUES ('Albert', 1500);
INSERT INTO employes (nom, paye) VALUES ('Alphonse', 0);
ERROR: value for domain salaire violates check constraint "salaire_check"
-- erreur logique vu qu'on ne peut avoir qu'un entier strictement positif
INSERT INTO employes (nom, paye) VALUES ('Alphonse', 1000);
INSERT 0 1
INSERT INTO employes (nom, paye) VALUES ('Bertrand', NULL);
INSERT 0 1
-- tous les employés doivent avoir un salaire
-- il faut donc modifier la contrainte, pour s'assurer
-- qu'aucune valeur NULL ne soit saisi
ALTER DOMAIN salaire SET NOT NULL;
ERROR: column "paye" of table "employes" contains null values
-- la ligne est déjà présente, il faut la modifier
UPDATE employes SET paye=1500 WHERE nom='Bertrand';
-- maintenant, on peut ajouter la contrainte au domaine
ALTER DOMAIN salaire SET NOT NULL;
INSERT INTO employes (nom, paye) VALUES ('Delphine', NULL);
ERROR: domain salaire does not allow null values
-- la contrainte est bien vérifiée
-- supprimons maintenant la contrainte
DROP DOMAIN salaire;
ERROR: cannot drop type salaire because other objects depend on it
DETAIL: table employes column paye depends on type salaire
HINT: Use DROP ... CASCADE to drop the dependent objects too.
-- il n'est pas possible de supprimer le domaine car il est référencé dans une
-- table. Il faut donc utiliser l'option CASCADE
b1=# DROP DOMAIN salaire CASCADE;
NOTICE: drop cascades to table employes column paye
DROP DOMAIN
-- le domaine a été supprimée ainsi que toutes les colonnes ayant ce type
\d employes
Table « public.employes »
Colonne | Type | NULL-able | Par défaut
---------+---------+-----------+--------------------------------------
id | integer | not null | nextval('employes_id_seq'::regclass)
nom | text | |

Création et utilisation d’un domaine code_postal_us :

CREATE DOMAIN code_postal_us AS TEXT


157
https://dalibo.com/formations
SQL pour PostgreSQL

CHECK(
VALUE ~ '^\d{5}$'
OR VALUE ~ '^\d{5}-\d{4}$'
);

CREATE TABLE courrier_us (


id_adresse SERIAL PRIMARY KEY,
rue1 TEXT NOT NULL,
rue2 TEXT,
rue3 TEXT,
ville TEXT NOT NULL,
code_postal code_postal_us NOT NULL
);

INSERT INTO courrier_us (rue1,ville,code_postal)


VALUES ('51 Franklin Street', 'Boston, MA', '02110-1335' );

INSERT 0 1

INSERT INTO courrier_us (rue1,ville,code_postal)


VALUES ('10 rue d''Uzès','Paris','F-75002') ;

ERREUR: la valeur pour le domaine code_postal_us viole la contrainte de


vérification « code_postal_us_check »

4.2.15 TABLES

• Équivalent ensembliste d’une relation


• Composé principalement de
– colonnes ordonnées
– contraintes

La table est l’élément de base d’une base de données. Elle est composée de colonnes (à
sa création) et est remplie avec des enregistrements (lignes de la table). Sa définition peut
aussi faire intervenir des contraintes, qui sont au niveau table ou colonne.

158
4. CRÉATION D’OBJET ET MISES À JOUR

4.2.16 CRÉATION D'UNE TABLE

• Définition de son nom


• Définition de ses colonnes
– nom, type, contraintes éventuelles
• Clauses de stockage
• CREATE TABLE

Pour créer une table, il faut donner son nom et la liste des colonnes. Une colonne est
définie par son nom et son type, mais aussi des contraintes optionnelles.

Des options sont possibles pour les tables, comme les clauses de stockage. Dans ce cas,
on sort du contexte logique pour se placer au niveau physique.

4.2.17 CREATE TABLE

CREATE TABLE nom_table (


definition_colonnes
definition_contraintes
) clause_stockage;

La création d’une table passe par l’ordre CREATE TABLE. La définition des colonnes et des
contraintes sont entre parenthèse après le nom de la table.

4.2.18 DÉFINITION DES COLONNES

nom_colonne type [ COLLATE collation ] [ contrainte ]


[, ...]

Les colonnes sont indiquées l’une après l’autre, en les séparant par des virgules.

Deux informations sont obligatoires pour chaque colonne : le nom et le type de la colonne.
Dans le cas d’une colonne contenant du texte, il est possible de fournir le collationnement
de la colonne. Quelque soit la colonne, il est ensuite possible d’ajouter des contraintes.

159
https://dalibo.com/formations
SQL pour PostgreSQL

4.2.19 VALEUR PAR DÉFAUT

• DEFAULT
– affectation implicite
• Utiliser directement par les types sériés

La clause DEFAULT permet d’affecter une valeur par défaut lorsqu’une colonne n’est pas
référencée dans l’ordre d’insertion ou si une mise à jour réinitialise la valeur de la colonne
à sa valeur par défaut.

Les types sériés définissent une valeur par défaut sur les colonnes de ce type. Cette valeur
est le retour de la fonction nextval() sur la séquence affectée automatiquement à cette
colonne.

Exemples

Assignation d’une valeur par défaut :

CREATE TABLE valdefaut (


id integer,
i integer DEFAULT 0,
j integer DEFAULT 0
);

INSERT INTO valdefaut (id, i) VALUES (1, 10);

SELECT * FROM valdefaut ;


id | i | j
----+----+---
1 | 10 | 0
(1 row)

4.2.20 COPIE DE LA DÉFINITION D'UNE TABLE

• Création d’une table à partir d’une autre table


– CREATE TABLE ... (LIKE table clause_inclusion)
• Avec les valeurs par défaut des colonnes :
– INCLUDING DEFAULTS
• Avec ses autres contraintes :
– INCLUDING CONSTRAINTS
• Avec ses index :
– INCLUDING INDEXES

160
4. CRÉATION D’OBJET ET MISES À JOUR

L’ordre CREATE TABLE permet également de créer une table à partir de la définition d’une
table déjà existante en utilisant la clause LIKE en lieu et place de la définition habituelles
des colonnes. Par défaut, seule la définition des colonnes avec leur typage est repris.

Les clauses INCLUDING permettent de récupérer d’autres éléments de la définition de la


table, comme les valeurs par défaut (INCLUDING DEFAULTS), les contraintes d’intégrité
(INCLUDING CONSTRAINTS), les index (INCLUDING INDEXES), les clauses de stockage
(INCLUDING STORAGE) ainsi que les commentaires (INCLUDING COMMENTS). Si l’ensemble
de ces éléments sont repris, il est possible de résumer la clause INCLUDING à INCLUDING
ALL.

La clause CREATE TABLE suivante permet de créer une table archive_evenements_2010


à partir de la définition de la table evenements :
CREATE TABLE archive_evenements_2010
(LIKE evenements
INCLUDING DEFAULTS
INCLUDING CONSTRAINTS
INCLUDING INDEXES
INCLUDING STORAGE
INCLUDING COMMENTS
);

Elle est équivalente à :


CREATE TABLE archive_evenements_2010
(LIKE evenements
INCLUDING ALL
);

4.2.21 MODIFICATION D'UNE TABLE

• ALTER TABLE
• Définition de la table
– renommage de la table
– ajout/modification/suppression d’une colonne
– déplacement dans un schéma différent
– changement du propriétaire
• Définition des colonnes
– renommage d’une colonne
– changement de type d’une colonne
• Définition des contraintes
– ajout/suppression d’une contrainte
161
https://dalibo.com/formations
SQL pour PostgreSQL

• Attention aux conséquences


– contention avec les verrous
– vérification des données
– performance avec une possible réécriture de la table

Pour modifier la définition d’une table (et non pas son contenu), il convient d’utiliser l’ordre
ALTER TABLE. Il permet de traiter la définition de la table (nom, propriétaire, schéma, liste
des colonnes), la définition des colonnes (ajout, modification de nom et de type, suppres-
sion... mais pas de changement au niveau de leur ordre), et la définition des contraintes
(ajout et suppression).

Suivant l’opération réalisée, les verrous posés ne seront pas les mêmes, même si le verrou
par défaut sera un verrou exclusif. Par exemple, renommer une table nécessite un verrou
exclusif mais changer la taille de l’échantillon statistiques bloque uniquement certaines
opérations de maintenance (comme VACUUM et ANALYZE) et certaines opérations DDL. Il
convient donc d’être très prudent lors de l’utilisation de la commande ALTER TABLE sur
un serveur en production.

Certaines opérations nécessitent de vérifier les données. C’est évident lors de l’ajout
d’une contrainte (comme une clé primaire ou une contrainte NOT NULL), mais c’est aussi
le cas lors d’un changement de type de données. Passer une colonne du type text vers
le type timestamp nécessite de vérifier que les données de cette colonne ne contien-
nent que des données convertibles vers le type timestamp. Dans les anciennes ver-
sions, la vérification était effectuée en permanence, y compris pour des cas simples où
cela n’était pas nécessaire. Par exemple, convertir une colonne du type varchar(200) à
varchar(100) nécessite de vérifier que la colonne ne contient que des chaînes de carac-
tères de longueur inférieure à 100. Mais convertir une colonne du type varchar(100)
vers le type varchar(200) ne nécessite pas de vérification. Les dernières versions de
PostgreSQL font la différence, ce qui permet d’éviter de perdre du temps pour une vérifi-
cation inutile.

Certaines opérations nécessitent une réécriture de la table. Par exemple, convertir une
colonne de type varchar(5) vers le type int4 impose une réécriture de la table car il n’y a
pas de compatibilité binaire entre les deux types. Ce n’est pas le cas si la modification est
uniquement sur la taille d’une colonne varchar. Certaines optimisations sont ajoutées
sur les nouvelles versions de PostgreSQL. Par exemple, l’ajout d’une colonne avec une
valeur par défaut causait la réécriture complète de la table pour intégrer la valeur de
cette nouvelle colonne alors que l’ajout d’une colonne sans valeur par défaut n’avait pas
la même conséquence. À partir de la version 11, cette valeur par défaut est enregistrée
dans la colonne attmissingval du catalogue système pg_attribute et la table n’a de ce
fait plus besoin d’être réécrite.

162
4. CRÉATION D’OBJET ET MISES À JOUR

Il convient donc d’être très prudent lors de l’utilisation de la commande ALTER TABLE.
Elle peut poser des problèmes de performances, à cause de verrous posés par d’autres
commandes, de verrous qu’elle réclame, de vérification des données, voire de réécriture
de la table.

4.2.22 SUPPRESSION D'UNE TABLE

• Supprimer une table :


DROP TABLE nom_table;
• Supprimer une table et tous les objets dépendants :
DROP TABLE nom_table CASCADE;

L’ordre DROP TABLE permet de supprimer une table. L’ordre DROP TABLE ... CASCADE
permet de supprimer une table ainsi que tous ses objets dépendants. Il peut s’agir de
séquences rattachées à une colonne d’une table, à des colonnes référençant la table à
supprimer, etc.

4.2.23 CONTRAINTES D'INTÉGRITÉ

• ACID
– Cohérence
– une transaction amène la base d’un état stable à un autre
• Assurent la cohérence des données
– unicité des enregistrements
– intégrité référentielle
– vérification des valeurs
– identité des enregistrements
– règles sémantiques

Les données dans les différentes tables ne sont pas indépendantes mais obéissent à des
règles sémantiques mises en place au moment de la conception du modèle conceptuel des
données. Les contraintes d’intégrité ont pour principal objectif de garantir la cohérence
des données entre elles, et donc de veiller à ce qu’elles respectent ces règles sémantiques.
Si une insertion, une mise à jour ou une suppression viole ces règles, l’opération est pure-
ment et simplement annulée.

163
https://dalibo.com/formations
SQL pour PostgreSQL

4.2.24 CLÉS PRIMAIRES

• Identifie une ligne de manière unique


• Une seule clé primaire par table
• Une ou plusieurs colonnes
• À choisir parmi les clés candidates
– parfois, utiliser une clé artificielle

Une clé primaire permet d’identifier une ligne de façon unique, il n’en existe qu’une seule
par table.

Une clé primaire garantit que toutes les valeurs de la ou des colonnes qui composent
cette clé sont uniques et non nulles. Elle peut être composée d’une seule colonne ou de
plusieurs colonnes, selon le besoin.

La clé primaire est déterminée au moment de la conception du modèle de données.

Les clés primaires créent implicitement un index qui permet de renforcer cette contrainte.

4.2.25 DÉCLARATION D'UNE CLÉ PRIMAIRE

Construction :
[CONSTRAINT nom_contrainte]
PRIMARY KEY ( nom_colonne [, ... ] )

Exemples

Définition de la table region :


CREATE TABLE region
(
id serial UNIQUE NOT NULL,
libelle text NOT NULL,

PRIMARY KEY(id)
);

INSERT INTO region VALUES (1, 'un');


INSERT INTO region VALUES (2, 'deux');

INSERT INTO region VALUES (NULL, 'trois');


ERROR: null value in column "id" violates not-null constraint
DETAIL: Failing row contains (null, trois).

INSERT INTO region VALUES (1, 'trois');

164
4. CRÉATION D’OBJET ET MISES À JOUR

ERROR: duplicate key value violates unique constraint "region_pkey"


DETAIL: Key (id)=(1) already exists.

INSERT INTO region VALUES (3, 'trois');

SELECT * FROM region;


id | libelle
----+---------
1 | un
2 | deux
3 | trois
(3 rows)

4.2.26 CONTRAINTE D'UNICITÉ

• Garantie l’unicité des valeurs d’une ou plusieurs colonnes


• Permet les valeurs NULL
• Clause UNIQUE
• Contrainte UNIQUE != index UNIQUE

Une contrainte d’unicité permet de garantir que les valeurs de la ou des colonnes sur
lesquelles porte la contrainte sont uniques. Elle autorise néanmoins d’avoir plusieurs
valeurs NULL car elles ne sont pas considérées comme égales mais de valeur inconnue
(UNKNOWN).

Une contrainte d’unicité peut être créée simplement en créant un index UNIQUE approprié.
Ceci est fortement déconseillé du fait que la contrainte ne sera pas référencée comme
telle dans le schéma de la base de données. Il sera donc très facile de ne pas la remar-
quer au moment d’une reprise du schéma pour une évolution majeure de l’application.
Une colonne possédant un index UNIQUE peut malgré tout être référencée par une clé
étrangère.

Les contraintes d’unicité créent implicitement un index qui permet de renforcer cette
unicité.

165
https://dalibo.com/formations
SQL pour PostgreSQL

4.2.27 DÉCLARATION D'UNE CONTRAINTE D'UNICITÉ

Construction :
[ CONSTRAINT nom_contrainte]
{ UNIQUE ( nom_colonne [, ... ] )

4.2.28 INTÉGRITÉ RÉFÉRENTIELLE

• Contrainte d’intégrité référentielle


– ou Clé étrangère
• Référence une clé primaire ou un groupe de colonnes UNIQUE et NOT NULL
• Garantie l’intégrité des données
• FOREIGN KEY

Une clé étrangère sur une table fait référence à une clé primaire ou une contrainte
d’unicité d’une autre table. La clé étrangère garantit que les valeurs des colonnes de
cette clé existent également dans la table portant la clé primaire ou la contrainte d’unicité.
On parle de contrainte référentielle d’intégrité : la contrainte interdit les valeurs qui
n’existent pas dans la table référencée.

Ainsi, la base cave définit une table region et une table appellation. Une appella-
tion d’origine est liée au terroir, et par extension à son origine géographique. La table
appellation est donc liée par une clé étrangère à la table region : la colonne region_id
de la table appellation référence la colonne id de la table region.

Cette contrainte permet d’empêcher les utilisateurs d’entrer dans la table appellation
des identifiants de région (region_id) qui n’existent pas dans la table region.

166
4. CRÉATION D’OBJET ET MISES À JOUR

4.2.29 EXEMPLE

4.2.30 DÉCLARATION D'UNE CLÉ ÉTRANGÈRE

[ CONSTRAINT nom_contrainte ] FOREIGN KEY ( nom_colonne [, ...] )


REFERENCES table_reference [ (colonne_reference [, ... ] ) ]
[ MATCH FULL | MATCH PARTIAL | MATCH SIMPLE ]
[ ON DELETE action ] [ ON UPDATE action ] }

Exemples

Définition de la table stock :

CREATE TABLE stock


(
vin_id int not null,
contenant_id int not null,
annee int4 not null,
nombre int4 not null,

PRIMARY KEY(vin_id,contenant_id,annee),

FOREIGN KEY(vin_id) REFERENCES vin(id) ON DELETE CASCADE,


FOREIGN KEY(contenant_id) REFERENCES contenant(id) ON DELETE CASCADE
);

Création d’une table mère et d’une table fille. La table fille possède une clé étrangère qui
référence la table mère :
167
https://dalibo.com/formations
SQL pour PostgreSQL

CREATE TABLE mere (id integer, t text);

CREATE TABLE fille (id integer, mere_id integer, t text);

ALTER TABLE mere ADD CONSTRAINT pk_mere PRIMARY KEY (id);

ALTER TABLE fille


ADD CONSTRAINT fk_mere_fille
FOREIGN KEY (mere_id)
REFERENCES mere (id)
MATCH FULL
ON UPDATE NO ACTION
ON DELETE CASCADE;

INSERT INTO mere (id, t) VALUES (1, 'val1'), (2, 'val2');

-- l'ajout de données dans la table fille qui font bien référence


-- à la table mere fonctionne
INSERT INTO fille (id, mere_id, t) VALUES (1, 1, 'val1');
INSERT INTO fille (id, mere_id, t) VALUES (2, 2, 'val2');

-- l'ajout de données dans la table fille qui ne font pas référence


-- à la table mere est annulé
INSERT INTO fille (id, mere_id, t) VALUES (3, 3, 'val3');
ERROR: insert or update on table "fille" violates foreign key constraint
"fk_mere_fille"
DETAIL: Key (mere_id)=(3) is not present in table "mere".

b1=# SELECT * FROM fille;


id | mere_id | t
----+---------+------
1 | 1 | val1
2 | 2 | val2
(2 rows)

-- mettre à jour la référence dans la table mere ne fonctionnera pas


-- car la contrainte a été définie pour refuser les mises à jour
-- (ON UPDATE NO ACTION)

b1=# UPDATE mere SET id=3 WHERE id=2;


ERROR: update or delete on table "mere" violates foreign key constraint
"fk_mere_fille" on table "fille"
DETAIL: Key (id)=(2) is still referenced from table "fille".

-- par contre, la suppression d'une ligne de la table mere référencée dans la


-- table fille va propager la suppression jusqu'à la table fille

168
4. CRÉATION D’OBJET ET MISES À JOUR

-- (ON DELETE CASCADE)

b1=# DELETE FROM mere WHERE id=2;


DELETE 1

b1=# SELECT * FROM fille;


id | mere_id | t
----+---------+------
1 | 1 | val1
(1 row)

b1=# SELECT * FROM mere;


id | t
----+------
1 | val1
(1 row)

4.2.31 VÉRIFICATION SIMPLE OU COMPLÈTE

• Vérification complète ou partielle d’une clé étrangère


• MATCH
– MATCH FULL (complète)
– MATCH SIMPLE (partielle)

La directive MATCH permet d’indiquer si la contrainte doit être entièrement vérifiée (MATCH
FULL) ou si la clé étrangère autorise des valeurs NULL (MATCH SIMPLE). MATCH SIMPLE est
la valeur par défaut.

Avec MATCH FULL, toutes les valeurs des colonnes qui composent la clé étrangère de la
table référençant doivent avoir une correspondance dans la table référencée.

Avec MATCH SIMPLE, les valeurs des colonnes qui composent la clé étrangère de la ta-
ble référençant peuvent comporter des valeurs NULL. Dans le cas des clés étrangères
multi-colonnes, toutes les colonnes peuvent ne pas être renseignées. Dans le cas des
clés étrangères sur une seule colonne, la contrainte autorise les valeurs NULL.

Exemples

Les exemples reprennent les tables mere et fille créées plus haut.
INSERT INTO fille VALUES (4, NULL, 'test');

SELECT * FROM fille;


id | mere_id | t
169
https://dalibo.com/formations
SQL pour PostgreSQL

----+---------+------
1 | 1 | val1
2 | 2 | val2
4 | | test
(2 rows)

4.2.32 COLONNES D'IDENTITÉ

• Identité d’un enregistrement


• GENERATED ... AS IDENTITY
– ALWAYS
– BY DEFAULT
• Préférer à serial
• Unicité non garantie sans contrainte explicite !

Cette contrainte permet d’avoir une colonne dont la valeur est incrémentée automatique-
ment, soit en permanence (clause ALWAYS), soit quand aucune valeur n’est saisie (clause
BY DEFAULT). Cette technique d’auto-incrémentation correspond au standard SQL, con-
trairement au pseudo-type serial qui était utilisé jusqu’à la version 10.

De plus, elle corrige certains défauts de ce pseudo-type. Avec le type serial, l’utilisation
de CREATE TABLE .. LIKE copiait la contrainte de valeur par défaut sans changer le nom
de la séquence. Il n’est pas possible d’ajouter ou de supprimer un pseudo-type serial
avec l’instruction ALTER TABLE. La suppression de la contrainte DEFAULT d’un type serial
ne supprime pas la séquence associée. Tout ceci fait que la définition d’une colonne
d’identité est préférable à l’utilisation du pseudo-type serial.

Il reste obligatoire de définir une clé primaire ou unique si l’on tient à l’unicité des valeurs
car même une clause GENERATED ALWAYS AS IDENTITY peut être contournée avec une
mise à jour portant la mention OVERRIDING SYSTEM VALUE.

Exemple :

CREATE table personnes (id int GENERATED ALWAYS AS IDENTITY, nom TEXT);

CREATE TABLE

INSERT INTO personnes (nom) VALUES ('Dupont') ;


INSERT 0 1
INSERT INTO personnes (nom) VALUES ('Durand') ;
INSERT 0 1

170
4. CRÉATION D’OBJET ET MISES À JOUR

SELECT * FROM personnes ;


id | nom
----+--------
1 | Dupont
2 | Durand
(2 lignes)

INSERT INTO personnes (id,nom) VALUES (3,'Martin') ;

ERROR: cannot insert into column "id"


DÉTAIL : Column "id" is an identity column defined as GENERATED ALWAYS.
ASTUCE : Use OVERRIDING SYSTEM VALUE to override.

INSERT INTO personnes (id,nom) OVERRIDING SYSTEM VALUE VALUES (3,'Martin') ;


INSERT 0 1

INSERT INTO personnes (id,nom) OVERRIDING SYSTEM VALUE VALUES (3,'Dupond') ;


INSERT 0 1

SELECT * FROM personnes ;


id | nom
----+--------
1 | Dupont
2 | Durand
3 | Martin
3 | Dupond

4.2.33 MISE À JOUR DE LA CLÉ PRIMAIRE

• Que faire en cas de mise à jour d’une clé primaire ?


– les clés étrangères seront fausses
• ON UPDATE
• ON DELETE
• Définition d’une action au niveau de la clé étrangère
– interdiction
– propagation de la mise à jour
– NULL
– valeur par défaut

Si des valeurs d’une clé primaire sont mises à jour ou supprimées, cela peut entrainer des
171
https://dalibo.com/formations
SQL pour PostgreSQL

incohérences dans la base de données si des valeurs de clés étrangères font référence
aux valeurs de la clé primaire touchées par le changement.

Afin de pouvoir gérer cela, la norme SQL prévoit plusieurs comportements possibles.
La clause ON UPDATE permet de définir comment le SGBD va réagir si la clé primaire
référencée est mise à jour. La clause ON DELETE fait de même pour les suppressions.

Les actions possibles sont :

• NO ACTION (ou RESTRICT), qui produit une erreur si une ligne référence encore le
ou les lignes touchées par le changement ;
• CASCADE, pour laquelle la mise à jour ou la suppression est propagée aux valeurs
référençant le ou les lignes touchées par le changement ;
• SET NULL, la valeur de la colonne devient NULL ;
• SET DEFAULT, pour lequel la valeur de la colonne prend la valeur par défaut de la
colonne.

Le comportement par défaut est NO ACTION, ce qui est habituellement recommandé pour
éviter les suppressions en chaîne mal maîtrisées.

Exemples

Les exemples reprennent les tables mere et fille créées plus haut.

Tentative d’insertion d’une ligne dont la valeur de mere_id n’existe pas dans la table mere :

INSERT INTO fille (id, mere_id, t) VALUES (1, 3, 'val3');


ERROR: insert or update on table "fille" violates foreign key constraint
"fk_mere_fille"
DETAIL: Key (mere_id)=(3) is not present in table "mere".

Mise à jour d’une ligne de la table mere pour modifier son id. La clé étrangère est déclarée
ON UPDATE NO ACTION, donc la mise à jour devrait être interdite :

UPDATE mere SET id = 3 WHERE id = 1;


ERROR: update or delete on table "mere" violates foreign key constraint
"fk_mere_fille" on table "fille"
DETAIL: Key (id)=(1) is still referenced from table "fille".

Suppression d’une ligne de la table mere. La clé étrangère sur fille est déclarée ON
DELETE CASCADE, la suppression sera donc propagée aux tables qui référencent la table
mere :

DELETE FROM mere WHERE id = 1;

SELECT * FROM fille ;


id | mere_id | t
----+---------+------

172
4. CRÉATION D’OBJET ET MISES À JOUR

2 | 2 | val2
(1 row)

4.2.34 VÉRIFICATIONS

• Présence d’une valeur


– NOT NULL
• Vérification de la valeur d’une colonne
– CHECK

La clause NOT NULL permet de s’assurer que la valeur de la colonne portant cette con-
trainte est renseignée. Dis autrement, elle doit obligatoirement être renseignée. Par dé-
faut, la colonne peut avoir une valeur NULL, donc n’est pas obligatoirement renseignée.

La clause CHECK spécifie une expression de résultat booléen que les nouvelles lignes ou
celles mises à jour doivent satisfaire pour qu’une opération d’insertion ou de mise à jour
réussisse. Les expressions de résultat TRUE ou UNKNOWN réussissent. Si une des lignes
de l’opération d’insertion ou de mise à jour produit un résultat FALSE, une exception est
levée et la base de données n’est pas modifiée. Une contrainte de vérification sur une
colonne ne fait référence qu’à la valeur de la colonne tandis qu’une contrainte sur la table
fait référence à plusieurs colonnes.

Actuellement, les expressions CHECK ne peuvent ni contenir des sous-requêtes ni faire


référence à des variables autres que les colonnes de la ligne courante. C’est technique-
ment réalisable, mais non supporté.

4.2.35 VÉRIFICATIONS DIFFÉRÉS

• Vérifications après chaque ordre SQL


– problèmes de cohérence
• Différer les vérifications de contraintes
– clause DEFERRABLE, NOT DEFERRABLE
– INITIALLY DEFERED, INITIALLY IMMEDIATE

Par défaut, toutes les contraintes d’intégrité sont vérifiées lors de l’exécution de chaque
ordre SQL de modification, y compris dans une transaction. Cela peut poser des prob-
lèmes de cohérences de données : insérer dans une table fille alors qu’on n’a pas encore
inséré les données dans la table mère, la clé étrangère de la table fille va rejeter l’insertion
et annuler la transaction.
173
https://dalibo.com/formations
SQL pour PostgreSQL

Le moment où les contraintes sont vérifiées est modifiable dynamiquement par l’ordre
SET CONSTRAINTS :
SET CONSTRAINTS { ALL | nom [, ...] } { DEFERRED | IMMEDIATE }

mais ce n’est utilisable que pour les contraintes déclarées comme déferrables.

Voici quelques exemples :

• avec la définition précédente des tables mere et fille


b1=# BEGIN;
UPDATE mere SET id=3 where id=1;
ERROR: update or delete on table "mere" violates foreign key constraint
"fk_mere_fille" on table "fille"
DETAIL: Key (id)=(1) is still referenced from table "fille".

• cette erreur survient aussi dans le cas où on demande que la vérification des con-
traintes soit différée pour cette transaction :
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
UPDATE mere SET id=3 WHERE id=1;
ERROR: update or delete on table "mere" violates foreign key constraint
"fk_mere_fille" on table "fille"
DETAIL: Key (id)=(1) is still referenced from table "fille".

• il faut que la contrainte soit déclarée comme étant différable :

CREATE TABLE mere (id integer, t text);


CREATE TABLE fille (id integer, mere_id integer, t text);
ALTER TABLE mere ADD CONSTRAINT pk_mere PRIMARY KEY (id);
ALTER TABLE fille
ADD CONSTRAINT fk_mere_fille
FOREIGN KEY (mere_id)
REFERENCES mere (id)
MATCH FULL
ON UPDATE NO ACTION
ON DELETE CASCADE
DEFERRABLE;
INSERT INTO mere (id, t) VALUES (1, 'val1'), (2, 'val2');
INSERT INTO fille (id, mere_id, t) VALUES (1, 1, 'val1');
INSERT INTO fille (id, mere_id, t) VALUES (2, 2, 'val2');

BEGIN;
SET CONSTRAINTS all deferred;
UPDATE mere SET id=3 WHERE id=1;
SELECT * FROM mere;
id | t

174
4. CRÉATION D’OBJET ET MISES À JOUR

----+------
2 | val2
3 | val1
(2 rows)

SELECT * FROM fille;


id | mere_id | t
----+---------+------
1 | 1 | val1
2 | 2 | val2
(2 rows)

UPDATE fille SET mere_id=3 WHERE mere_id=1;


COMMIT;

4.2.36 VÉRIFICATIONS PLUS COMPLEXES

• Un trigger
– si une contrainte porte sur plusieurs tables
– si sa vérification nécessite une sous-requête
• Préférer les contraintes déclaratives

Les contraintes d’intégrités du SGBD ne permettent pas d’exprimer une contrainte qui
porte sur plusieurs tables ou simplement si sa vérification nécessite une sous-requête.
Dans ce cas là, il est nécessaire d’écrire un trigger spécifique qui sera déclenché après
chaque modification pour valider la contrainte.

Il ne faut toutefois pas systématiser l’utilisation de triggers pour valider des contraintes
d’intégrité. Cela aurait un impact fort sur les performances et sur la maintenabilité de
la base de données. Il vaut mieux privilégier les contraintes déclaratives et n’envisager
l’emploi de triggers que dans les cas où ils sont réellement nécessaires.

175
https://dalibo.com/formations
SQL pour PostgreSQL

4.3 DML : MISE À JOUR DES DONNÉES

• SELECT peut lire les données d’une table ou plusieurs tables


– mais ne peut pas les mettre à jour
• Ajout de données dans une table
– INSERT
• Modification des données d’une table
– UPDATE
• Suppression des données d’une table
– DELETE

L’ordre SELECT permet de lire une ou plusieurs tables. Les mises à jours utilisent des ordres
distincts.

L’ordre INSERT permet d’ajouter ou insérer des données dans une table. L’ordre UPDATE
permet de modifier des lignes déjà existantes. Enfin, l’ordre DELETE permet de supprimer
des lignes. Ces ordres ne peuvent travailler que sur une seule table à la fois. Si on
souhaite par exemple insérer des données dans deux tables, il est nécessaire de réaliser
deux INSERT distincts.

4.3.1 AJOUT DE DONNÉES : INSERT

• Ajoute des lignes à partir des données de la requête


• Ajoute des lignes à partir d’une requête SELECT
• Syntaxe :
INSERT INTO nom_table [ ( nom_colonne [, ...] ) ]
{ liste_valeurs | requete }

L’ordre INSERT insère de nouvelles lignes dans une table. Il permet d’insérer une ou
plusieurs lignes spécifiées par les expressions de valeur, ou zéro ou plusieurs lignes
provenant d’une requête.

La liste des noms de colonnes est optionnelle. Si elle n’est pas spécifiée, alors PostgreSQL
utilisera implicitement la liste de toutes les colonnes de la table dans l’ordre de leur décla-
ration, ou les N premiers noms de colonnes si seules N valeurs de colonnes sont fournies
dans la clause VALUES ou dans la requête. L’ordre des noms des colonnes dans la liste n’a
pas d’importance particulière, il suffit de nommer les colonnes mises à jour.

Chaque colonne absente de la liste, implicite ou explicite, se voit attribuer sa valeur par
défaut, s’il y en a une ou NULL dans le cas contraire. Les expressions de colonnes qui

176
4. CRÉATION D’OBJET ET MISES À JOUR

ne correspondent pas au type de données déclarées sont transtypées automatiquement,


dans la mesure du possible.

4.3.2 INSERT AVEC LISTE D'EXPRESSIONS

INSERT INTO nom_table [ ( nom_colonne [, ...] ) ]


VALUES ( { expression | DEFAULT } [, ...] ) [, ...]

La clause VALUES permet de définir une liste d’expressions qui va constituer la ligne à in-
sérer dans la base de données. Les éléments de cette liste d’expression sont séparés par
une virgule. Cette liste d’expression est composée de constantes ou d’appels à des fonc-
tions retournant une valeur, pour obtenir par exemple la date courante ou la prochaine
valeur d’une séquence. Les valeurs fournies par la clause VALUES ou par la requête sont
associées à la liste explicite ou implicite des colonnes de gauche à droite.

Exemples

Insertion d’une ligne dans la table stock :


INSERT INTO stock (vin_id, contenant_id, annee, nombre)
VALUES (12, 1, 1935, 1);

Insertion d’une ligne dans la table vin :


INSERT INTO vin (id, recoltant_id, appellation_id, type_vin_id)
VALUES (nextval('vin_id_seq'), 3, 6, 1);

4.3.3 INSERT À PARTIR D'UN SELECT

INSERT INTO nom_table [ ( nom_colonne [, ...] ) ]


requête

L’ordre INSERT peut aussi prendre une requête SQL en entrée. Dans ce cas, INSERT va
insérer autant de lignes dans la table d’arrivée qu’il y a de lignes retournées par la re-
quête SELECT. L’ordre des colonnes retournées par SELECT doit correspondre à l’ordre
des colonnes de la liste des colonnes. Leur type de données doit également correspon-
dre.

Exemples

Insertion dans une table stock2 à partir d’une requête SELECT sur la table stock1 :
INSERT INTO stock2 (vin_id, contenant_id, annee, nombre)
SELECT vin_id, contenant_id, annee, nombre FROM stock;
177
https://dalibo.com/formations
SQL pour PostgreSQL

4.3.4 INSERT ET COLONNES IMPLICITES

• L’ordre physique peut changer dans le temps


– résultats incohérents
– requêtes en erreurs

Il est préférable de lister explicitement les colonnes touchées par l’ordre INSERT afin de
garder un ordre d’insertion déterministe. En effet, l’ordre des colonnes peut changer
notamment lorsque certains ETL sont utilisés pour modifier le type d’une colonne
varchar(10) en varchar(11). Par exemple, pour la colonne username, l’ETL Kettle
génère les ordres suivants :
ALTER TABLE utilisateurs ADD COLUMN username_KTL VARCHAR(11);
UPDATE utilisateurs SET username_KTL=username;
ALTER TABLE utilisateurs DROP COLUMN username;
ALTER TABLE utilisateurs RENAME username_KTL TO username

Il génère des ordres SQL inutiles et consommateurs d’entrées/sorties disques car il doit
générer des ordres SQL compris par tous les SGBD du marché. Or, tous les SGBD ne
permettent pas de changer le type d’une colonne aussi simplement que dans PostgreSQL.

Exemples

Exemple de modification du schéma pouvant entrainer des problèmes d’insertion si les


colonnes ne sont pas listées explicitement :
CREATE TABLE insere (id integer PRIMARY KEY, col1 varchar(5), col2 integer);

INSERT INTO insere VALUES (1, 'XX', 10);

ALTER TABLE insere ADD COLUMN col1_tmp varchar(6);


UPDATE insere SET col1_tmp = col1;
ALTER TABLE insere DROP COLUMN col1;
ALTER TABLE insere RENAME COLUMN col1_tmp TO col1;

INSERT INTO insere VALUES (2, 'XXX', 10);


ERROR: invalid input syntax for integer: "XXX"
LINE 1: INSERT INTO insere VALUES (2, 'XXX', 10);

178
4. CRÉATION D’OBJET ET MISES À JOUR

4.3.5 MISE À JOUR DE DONNÉES : UPDATE

• Ordre UPDATE
• Met à jour une ou plusieurs colonnes d’une même ligne
– à partir des valeurs de la requête
– à partir des anciennes valeurs
– à partir d’une requête SELECT
– à partir de valeurs d’une autre table

L’ordre de mise à jour de lignes s’appelle UPDATE.

4.3.6 CONSTRUCTION D'UPDATE

UPDATE nom_table
SET
{
nom_colonne = { expression | DEFAULT }
|
( nom_colonne [, ...] ) = ( { expression | DEFAULT } [, ...] )
} [, ...]
[ FROM liste_from ]
[ WHERE condition | WHERE CURRENT OF nom_curseur ]

L’ordre UPDATE permet de mettre à jour les lignes d’une table.

L’ordre UPDATE ne met à jour que les lignes qui satisfont les conditions de la clause WHERE.
La clause SET permet de définir les colonnes à mettre à jour. Le nom des colonne mises à
jour doivent faire partie de la table mise à jour.

Les valeurs mises à jour peuvent faire référence aux valeurs avant mise à jour de la
colonne, dans ce cas on utilise la forme nom_colonne = nom_colonne. La partie de
gauche référence la colonne à mettre à jour, la partie de droite est une expression qui
permet de déterminer la valeur à appliquer à la colonne. La valeur à appliquer peut bien
entendu être une référence à une ou plusieurs colonnes et elles peuvent être dérivées
par une opération arithmétique.

La clause FROM ne fait pas partie de la norme SQL mais certains SGBDR la supportent,
notamment SQL Server et PostgreSQL. Elle permet de réaliser facilement la mise à jour
d’une table à partir des valeurs d’une ou plusieurs tables annexes.

La norme SQL permet néanmoins de réaliser des mises à jour en utilisant une sous-
requête, permettant d’éviter l’usage de la clause FROM.
179
https://dalibo.com/formations
SQL pour PostgreSQL

Exemples

Mise à jour du prix d’un livre particulier :


UPDATE livres SET prix = 10 WHERE isbn = '978-3-8365-3872-5';

Augmentation de 5 % du prix des livres :


UPDATE livres SET prix = prix * 1.05;

Mise à jour d’une table employees à partir des données d’une table bonus_plan :
UPDATE employees e
SET commission_rate = bp.commission_rate
FROM bonus_plan bp
ON (e.bonus_plan = bp.planid)

La même requête avec une sous-requête, conforme à la norme SQL :


UPDATE employees
SET commission_rate = (SELECT commission_rate
FROM bonus_plan bp
WHERE bp.planid = employees.bonus_plan);

Lorsque plusieurs colonnes doivent être mises à jour à partir d’une jointure, il est possible
d’utiliser ces deux écritures :
UPDATE employees e
SET commission_rate = bp.commission_rate,
commission_rate2 = bp.commission_rate2
FROM bonus_plan bp
ON (e.bonus_plan = bp.planid);

et
UPDATE employees e
SET (commission_rate, commission_rate2) = (
SELECT bp.commission_rate, bp.commission_rate2
FROM bonus_plan bp ON (e.bonus_plan = bp.planid)
);

180
4. CRÉATION D’OBJET ET MISES À JOUR

4.3.7 SUPPRESSION DE DONNÉES : DELETE

• Supprime les lignes répondant au prédicat


• Syntaxe :
DELETE FROM nom_table [ [ AS ] alias ]
[ WHERE condition

L’ordre DELETE supprime l’ensemble des lignes qui répondent au prédicat de la clause
WHERE.
DELETE FROM nom_table [ [ AS ] alias ]
[ WHERE condition | WHERE CURRENT OF nom_curseur ]

Exemples

Suppression d’un livre épuisé du catalogue :


DELETE FROM livres WHERE isbn = '978-0-8707-0635-6';

4.3.8 CLAUSE RETURNING

• Spécifique à PostgreSQL
• Permet de retourner les lignes complètes ou partielles résultants de INSERT,
UPDATE ou DELETE
• Syntaxe :
requete_sql RETURNING ( * | expression )

La clause RETURNING est une extension de PostgreSQL. Elle permet de retourner les lignes
insérées, mises à jour ou supprimées par un ordre DML de modification. Il est également
possible de dériver une valeur retournée.

L’emploi de la clause RETURNING peut nécessiter des droits complémentaires sur les objets
de la base.

Exemples

Mise à jour du nombre de bouteilles en stock :


SELECT annee, nombre FROM stock
WHERE vin_id = 7 AND contenant_id = 1 AND annee = 1967;
annee | nombre
-------+--------
1967 | 17
(1 row)

UPDATE stock SET nombre = nombre - 1


181
https://dalibo.com/formations
SQL pour PostgreSQL

WHERE vin_id = 7 AND contenant_id = 1 AND annee = 1967 RETURNING nombre;


nombre
--------
16
(1 row)

4.4 TRANSACTIONS

• ACID
– Atomicité
– un traitement se fait en entier ou pas du tout
• TCL pour Transaction Control Language
– valide une transaction
– annule une transaction
– points de sauvegarde

Les transactions sont une partie essentielle du langage SQL. Elles permettent de rendre
atomique un certain nombre de requêtes. Le résultat de toutes les requêtes d’une trans-
action est validée ou pas, mais on ne peut pas avoir d’état intermédiaire.

Le langage SQL définit qu’une transaction peut être validée ou annulée. Ce sont respec-
tivement les ordres COMMIT et ROLLBACK. Il est aussi possible de faire des points de reprise
ou de sauvegarde dans une transaction. Ils se font en utilisant l’ordre SAVEPOINT.

4.4.1 AUTO-COMMIT ET TRANSACTIONS

• Par défaut, PostgreSQL fonctionne en auto-commit


– à moins d’ouvrir explicitement une transaction
• Ouvrir une transaction
– BEGIN TRANSACTION

PostgreSQL fonctionne en auto-commit. Autrement dit, sans BEGIN, une requête est con-
sidérée comme une transaction complète et n’a donc pas besoin de COMMIT.

Une transaction débute toujours par un START ou un BEGIN.

182
4. CRÉATION D’OBJET ET MISES À JOUR

4.4.2 VALIDATION OU ANNULATION D'UNE TRANSACTION

• Valider une transaction


– COMMIT
• Annuler une transaction
– ROLLBACK
• Sans validation, une transaction est forcément annulée

Une transaction est toujours terminée par une COMMIT ou un END quand on veut que
les modifications soient définitivement enregistrées, et par un ROLLBACK dans le cas con-
traire.

La transaction en cours d’une session qui se termine, quelle que soit la raison, sans COMMIT
et sans ROLLBACK est considérée comme annulée.

Exemples

Avant de retirer une bouteille du stock, on vérifie tout d’abord qu’il reste suffisamment
de bouteilles en stock :
BEGIN TRANSACTION;

SELECT annee, nombre FROM stock WHERE vin_id = 7 AND contenant_id = 1


AND annee = 1967;
annee | nombre
-------+--------
1967 | 17
(1 row)

UPDATE stock SET nombre = nombre - 1


WHERE vin_id = 7 AND contenant_id = 1 AND annee = 1967 RETURNING nombre;
nombre
--------
16
(1 row)

COMMIT;

183
https://dalibo.com/formations
SQL pour PostgreSQL

4.4.3 PROGRAMMATION

• Certains langages implémentent des méthodes de gestion des transactions


– PHP, Java, etc.
• Utiliser ces méthodes prioritairement

La plupart des langages permettent de gérer les transactions à l’aide de méthodes ou


fonctions particulières. Il est recommandé de les utiliser.

En Java, ouvrir une transaction revient à désactiver l’auto-commit :


String url =
"jdbc:postgresql://localhost/test?user=fred&password=secret&ssl=true";
Connection conn = DriverManager.getConnection(url);
conn.setAutoCommit(false);

La transaction est confirmée (COMMIT) avec la méthode suivante :


conn.commit();

À l’inverse, elle est annulée (ROLLBACK) avec la méthode suivante :


conn.rollback();

4.4.4 POINTS DE SAUVEGARDE

• Certains traitements dans une transaction peuvent être annulés


– mais la transaction est atomique
• Définir un point de sauvegarde
– SAVEPOINT nom_savepoint
• Valider le traitement depuis le dernier point de sauvegarde
– RELEASE SAVEPOINT nom_savepoint
• Annuler le traitement depuis le dernier point de sauvegarde
– ROLLBACK TO SAVEPOINT nom_savepoint

déroule jusqu’au bout, le point de sauvegarde pourra être relâché (RELEASE SAVEPOINT),
confirmant ainsi les traitements. Si le traitement tombe en erreur, il suffira de revenir au
point de sauvegarde (ROLLBACK TO SAVEPOINT pour annuler uniquement cette partie du
traitement sans affecter le reste de la transaction.

Les points de sauvegarde sont des éléments nommés, il convient donc de leur affecter un
nom particulier. Leur nom doit être unique dans la transaction courante.

Les langages de programmation permettent également de gérer les points de sauvegarde


en utilisant des méthodes dédiées. Par exemple, en Java :

184
4. CRÉATION D’OBJET ET MISES À JOUR

Savepoint save1 = connection.setSavepoint();

En cas d’erreurs, la transaction peut être ramener à l’état du point de sauvegarde avec :
connection.rollback(save1);

À l’inverse, un point de sauvegarde est relâché de la façon suivante :


connection.releaseSavepoint(save1);

Exemples

Transaction avec un point de sauvegarde et la gestion de l’erreur :


BEGIN;

INSERT INTO mere (id, val_mere) VALUES (10, 'essai');

SAVEPOINT insert_fille;

INSERT INTO fille (id_fille, id_mere, val_fille) VALUES (1, 10, 'essai 2');
ERROR: duplicate key value violates unique constraint "fille_pkey"
DETAIL: Key (id_fille)=(1) already exists.

ROLLBACK TO SAVEPOINT insert_fille;

COMMIT;

SELECT * FROM mere;


id | val_mere
----+----------
1 | mere 1
2 | mere 2
10 | essai

4.5 CONCLUSION

• SQL : toujours un traitement d’ensembles d’enregistrements


– c’est le côté relationnel
• Pour les définitions d’objets
– CREATE, ALTER, DROP
• Pour les données
– INSERT, UPDATE, DELETE

Le standard SQL permet de traiter des ensembles d’enregistrements, que ce soit en lec-
ture, en insertion, en modification et en suppression. Les ensembles d’enregistrements
185
https://dalibo.com/formations
SQL pour PostgreSQL

sont généralement des tables qui, comme tous les autres objets, sont créées (CREATE),
modifier (ALTER) et/ou supprimer (DROP).

4.5.1 QUESTIONS

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

186
4. CRÉATION D’OBJET ET MISES À JOUR

4.6 TRAVAUX PRATIQUES

Cet exercice utilise la base tpc. La base tpc peut être téléchargée depuis https://dali.
bo/tp_tpc (dump de 31 Mo, pour 267 Mo sur le disque au final). Auparavant créer les
utilisateurs depuis le script sur https://dali.bo/tp_tpc_roles.

$ psql < tpc_roles.sql # Exécuter le script de création des rôles


$ createdb --owner tpc_owner tpc # Création de la base
$ pg_restore -d tpc tpc.dump # Une erreur sur un schéma 'public' existant est normale

Les mots de passe sont dans le script. Pour vous connecter :

$ psql -U tpc_admin -h localhost -d tpc

Pour cet exercice, les modifications de schéma doivent être effectuées par un rôle ayant
suffisamment de droits pour modifier son schéma. Le rôle tpc_admin a les droits suff-
isants.

1. Ajouter une colonne email de type text à la table contacts. Cette colonne va
permettre de stocker l’adresse e-mail des clients et des fournisseurs. Ajouter égale-
ment un commentaire décrivant cette colonne dans le catalogue de PostgreSQL
(utiliser la commande COMMENT).

2. Mettre à jour la table des contacts pour indiquer l’adresse e-mail de Client6657 qui
est client6657@dalibo.com.

3. Ajouter une contrainte d’intégrité qui valide que la valeur de la colonne email créée
est bien formée (vérifier que la chaîne de caractère contient au moins le caractère
@).

4. Valider la contrainte dans une transaction de test.

5. Déterminer quels sont les contacts qui disposent d’une adresse e-mail et affichez
leur nom ainsi que le code de leur pays.

6. La génération des numéros de commande est actuellement réalisée à l’aide de la


séquence commandes_commande_id_seq. Cette méthode ne permet pas de garan-
tir que tous les numéros de commande se suivent. Proposer une solution pour séri-
aliser la génération des numéros de commande. Autrement dit, proposer une méth-
ode pour obtenir un numéro de commande sans avoir de « trou » dans la séquence
en cas d’échec d’une transaction.

7. Noter le nombre de lignes de la table pieces. Dans une transaction, majorer de 5%


le prix des pièces de moins de 1500 € et minorer de 5 % le prix des pièces dont le
prix actuel est égal ou supérieur à 1500 €. Vérifier que le nombre de lignes mises à
jour au total correspond au nombre total de lignes de la table pieces.
187
https://dalibo.com/formations
SQL pour PostgreSQL

8. Dans une même transaction, créer un nouveau client en incluant l’ajout de


l’ensemble des informations requises pour pouvoir le contacter. Un nouveau client
a un solde égal à 0.

188
4. CRÉATION D’OBJET ET MISES À JOUR

4.7 TRAVAUX PRATIQUES (SOLUTIONS)

1. Ajouter une colonne email de type text à la table contacts. Cette colonne va
permettre de stocker l’adresse e-mail des clients et des fournisseurs. Ajouter égale-
ment un commentaire décrivant cette colonne dans le catalogue de PostgreSQL
(utiliser la commande COMMENT).
ALTER TABLE contacts
ADD COLUMN email text
;

COMMENT ON COLUMN contacts.email IS


'Adresse e-mail du contact'
;

2. Mettre à jour la table des contacts pour indiquer l’adresse e-mail de Client6657 qui
est client6657@dalibo.com.
UPDATE contacts
SET email = 'client6657@dalibo.com'
WHERE nom = 'Client6657'
;

Vérifier les résultats :


SELECT *
FROM contacts
WHERE nom = 'Client6657'
;

3. Ajouter une contrainte d’intégrité qui valide que la valeur de la colonne email créée
est bien formée (vérifier que la chaîne de caractère contient au moins le caractère
@).
ALTER TABLE contacts
ADD CONSTRAINT chk_contacts_email_valid
CHECK (email LIKE '%@%')
;

Cette expression régulière est simplifiée et simpliste pour les besoins de l’exercice. Des
expressions régulières plus complexes permettent de valider réellement une adresse e-
mail.

Voici un exemple un tout petit peu plus évolué en utilisant une expression rationnelle sim-
ple, ici pour vérifier que la chaîne précédent le caractère @ contient au moins un caractère,
et que la chaîne le suivant est une chaîne de caractères contenant un point :
ALTER TABLE contacts
ADD CONSTRAINT chk_contacts_email_valid
189
https://dalibo.com/formations
SQL pour PostgreSQL

CHECK (email ~ '.+@.+\..+')


;

4. Valider la contrainte dans une transaction de test.

Démarrer la transaction :
BEGIN ;

Tenter de mettre à jour la table contacts avec une adresse e-mail ne répondant pas à la
contrainte :
UPDATE contacts
SET email = 'test'
;

L’ordre UPDATE retourne l’erreur suivante, indiquant que l’expression régulière est fonc-
tionnelle :

ERROR: new row for relation "contacts" violates check constraint


"chk_contacts_email_valid"
DETAIL: Failing row contains
(300001, Client1737, nkD, SA, 20-999-929-1440, test).

La transaction est ensuite annulée :


ROLLBACK ;

5. Déterminer quels sont les contacts qui disposent d’une adresse e-mail et afficher
leur nom ainsi que le code de leur pays.
SELECT nom, code_pays
FROM contacts
WHERE email IS NOT NULL
;

6. La génération des numéros de commande est actuellement réalisée à l’aide de la


séquence commandes_commande_id_seq. Cette méthode ne permet pas de garan-
tir que tous les numéros de commande se suivent. Proposer une solution pour
sérialiser la génération des numéros de commande. Autrement dit, proposer une
méthode transactionnelle pour obtenir un numéro de commande, sans avoir de «
trou » dans la séquence en cas d’échec d’une transaction.

La solution la plus simple pour imposer la sérialisation des numéros de commandes est
d’utiliser une table de séquences. Une ligne de cette table correspondra au compteur des
numéros de commande.
-- création de la table qui va contenir la séquence :
CREATE TABLE numeros_sequences (
nom text NOT NULL PRIMARY KEY,

190
4. CRÉATION D’OBJET ET MISES À JOUR

sequence integer NOT NULL


)
;

-- initialisation de la séquence :
INSERT INTO numeros_sequences (nom, sequence)
SELECT 'sequence_numero_commande', max(numero_commande)
FROM commandes
;

L’obtention d’un nouveau numéro de commande sera réalisé dans la transaction de créa-
tion de la commande de la façon suivante :

BEGIN ;

UPDATE numeros_sequences
SET sequence = sequence + 1
WHERE nom = 'numero_commande'
RETURNING sequence
;

/* insertion d'une nouvelle commande en utilisant le numéro de commande


retourné par la commande précédente :
INSERT INTO commandes (numero_commande, ...)
VALUES (<la nouvelle valeur de la séquence>, ...) ;
*/

COMMIT ;

L’ordre UPDATE pose un verrou exclusif sur la ligne mise à jour. Tant que la mise à jour
n’aura pas été validée ou annulée par COMMIT ou ROLLBACK, le verrou posé va bloquer
toutes les autres transactions qui tenteraient de mettre à jour cette ligne. De cette façon,
toutes les transactions seront sérialisées.

Concernant la génération des numéros de séquence, si la transaction est annulée, alors le


compteur sequence retrouvera sa valeur précédente et la transaction suivante obtiendra
le même numéro de séquence. Si la transaction est validée, alors le compteur sequence
est incrémenté. La transaction suivante verra alors cette nouvelle valeur et non plus
l’ancienne. Cette méthode garantit qu’il n’y ait pas de rupture de séquence.

Il va de soi que les transactions de création de commandes doivent être extrêmement


courtes. Si une telle transaction est bloquée, toutes les transactions suivantes seront
également bloquées, paralysant ainsi tous les utilisateurs de l’application.

7. Noter le nombre de lignes de la table pieces. Dans une transaction, majorer de 5 %


le prix des pièces de moins de 1500 € et minorer de 5 % le prix des pièces dont le
191
https://dalibo.com/formations
SQL pour PostgreSQL

prix actuel est égal ou supérieur à 1500 €. Vérifier que le nombre de lignes mises à
jour au total correspond au nombre total de lignes de la table pieces.
BEGIN ;

SELECT count(*)
FROM pieces
;

UPDATE pieces
SET prix = prix * 1.05
WHERE prix < 1500
;

UPDATE pieces
SET prix = prix * 0.95
WHERE prix >= 1500
;

Au total, la transaction a mis à jour 214200 (99922+114278) lignes, soit 14200 lignes de
trop mises à jour.

Annuler la mise à jour :


ROLLBACK ;

Explication : Le premier UPDATE a majoré de 5 % les pièces dont le prix est inférieur à
1500 €. Or, tous les prix supérieurs à 1428,58 € passent la barre des 1500 € après le
premier UPDATE. Le second UPDATE minore les pièces dont le prix est égal ou supérieur à
1500 €, ce qui inclue une partie des prix majorés par le précédent UPDATE. Certaines lignes
ont donc subies deux modifications au lieu d’une. L’instruction CASE du langage SQL, qui
sera abordée dans le prochain module, propose une solution à ce genre de problématique
:
UPDATE pieces
SET prix = (
CASE
WHEN prix < 1500 THEN prix * 1.05
WHEN prix >= 1500 THEN prix * 0.95
END
)
;

8. Dans une même transaction, créer un nouveau client en incluant l’ajout de


l’ensemble des informations requises pour pouvoir le contacter. Un nouveau client
a un solde égal à 0.
-- démarrer la transaction

192
4. CRÉATION D’OBJET ET MISES À JOUR

BEGIN ;

-- créer le contact et récupérer le contact_id généré


INSERT INTO contacts (nom, adresse, telephone, code_pays)
VALUES ('M. Xyz', '3, Rue du Champignon, 96000 Champiville',
'+33554325432', 'FR')
RETURNING contact_id
;

-- réaliser l'insertion en utilisant le numéro de contact récupéré précédemment


INSERT INTO clients (solde, segment_marche, contact_id, commentaire)
-- par exemple ici avec le numéro 350002
VALUES (0, 'AUTOMOBILE', 350002, 'Client très important')
;

-- valider la transaction
COMMIT ;

193
https://dalibo.com/formations
SQL pour PostgreSQL

5 PLUS LOIN AVEC SQL

194
5. PLUS LOIN AVEC SQL

5.1 PRÉAMBULE

• Après la définition des objets, leur lecture et leur écriture


• Aller plus loin dans l’écriture de requêtes
– avec les jointures
– avec les requêtes intégrées

Maintenant que nous avons vu comment définir des objets, comment lire des données
provenant de relation et comment écrire des données, nous allons pousser vers les per-
fectionnements du langage SQL. Nous allons notamment aborder la lecture de plusieurs
tables en même temps, que ce soit par des jointures ou par des sous-requêtes.

5.1.1 MENU

• Valeur NULL
• Agrégats, GROUP BY, HAVING
• Sous-requêtes
• Jointures
• Expression conditionnelle CASE
• Opérateurs ensemblistes : UNION, EXCEPT, INTERSECT

195
https://dalibo.com/formations
SQL pour PostgreSQL

5.1.2 OBJECTIFS

• Comprendre l’intérêt du NULL


• Savoir écrire des requêtes complexes

5.2 VALEUR NULL

• Comment représenter une valeur que l’on ne connaît pas ?


– Valeur NULL
• Trois sens possibles pour NULL :
– valeur inconnue
– valeur inapplicable
– absence de valeur
• Logique 3 états

Le standard SQL définit très précisément la valeur que doit avoir une colonne dont on ne
connaît pas la valeur. Il faut utiliser le mot clé NULL. En fait, ce mot clé est utilisé dans
trois cas : pour les valeurs inconnues, pour les valeurs inapplicables et pour une absence
de valeurs.

5.2.1 AVERTISSEMENT

• Chris J. Date a écrit :


– La valeur NULL telle qu’elle est implémentée dans SQL peut poser plus de prob-
lèmes qu’elle n’en résout. Son comportement est parfois étrange et est source de
nombreuses erreurs et de confusions.
• Éviter d’utiliser NULL le plus possible
– utiliser NULL correctement lorsqu’il le faut

Il ne faut utiliser NULL que lorsque cela est réellement nécessaire. La gestion des valeurs
NULL est souvent source de confusions et d’erreurs, ce qui explique qu’il est préférable
de l’éviter tant qu’on n’entre pas dans les trois cas vu ci-dessus (valeur inconnue, valeur
inapplicable, absence de valeur).

196
5. PLUS LOIN AVEC SQL

5.2.2 ASSIGNATION DE NULL

• Assignation de NULL pour INSERT et UPDATE


• Explicitement :
– NULL est indiqué explicitement dans les assignations
• Implicitement :
– la colonne n’est pas affectée par INSERT
– et n’a pas de valeur par défaut
• Empêcher la valeur NULL
– contrainte NOT NULL

Il est possible de donner le mot-clé NULL pour certaines colonnes dans les INSERT et les
UPDATE. Si jamais une colonne n’est pas indiquée dans un INSERT, elle aura comme valeur
sa valeur par défaut (très souvent, il s’agit de NULL). Si jamais on veut toujours avoir une
valeur dans une colonne particulière, il faut utiliser la clause NOT NULL lors de l’ajout de
la colonne. C’est le cas pour les clés primaires par exemple.

Voici quelques exemples d’insertion et de mise à jour :

CREATE TABLE public.personnes


(
id serial,
nom character varying(60) NOT NULL,
prenom character varying(60),
date_naissance date,
CONSTRAINT pk_personnes PRIMARY KEY (id)
);

INSERT INTO personnes(


nom, prenom, date_naissance)
VALUES ('Lagaffe', 'Gaston', date '1957-02-28');

-- assignation explicite

INSERT INTO personnes(


nom, prenom, date_naissance)
VALUES ('Fantasio', NULL, date '1938-01-01');

-- assignation implicite

INSERT INTO personnes(


nom, prenom)
VALUES ('Prunelle', 'Léon');

-- observation des résultats


197
https://dalibo.com/formations
SQL pour PostgreSQL

id | nom | prenom | date_naissance


----+----------+--------+----------------
1 | Lagaffe | Gaston | 1957-02-28
2 | Fantasio | (null) | 1938-01-01
3 | Prunelle | Léon | (null)
(3 rows)

L’affichage (null) dans psql est obtenu avec la méta-commande \pset null (null).

5.2.3 CALCULS AVEC NULL

• Utilisation dans un calcul


– propagation de NULL
• NULL est inapplicable
– le résultat vaut NULL

La valeur NULL est définie comme inapplicable. Ainsi, si elle présente dans un calcul, elle
est propagée sur l’ensemble du calcul : le résultat vaudra NULL.

Exemples de calcul

Calculs simples :
SELECT 1 + 2 AS resultat;
resultat
----------
3
(1 row)

SELECT 1 + 2 + NULL AS resultat;


resultat
----------
(null)
(1 row)

Calcul à partir de l’âge :


SELECT nom, prenom,
1 + extract('year' from age(date_naissance)) AS calcul_age FROM personnes;
nom | prenom | calcul_age
----------+--------+------------
Lagaffe | Gaston | 60
Fantasio | (null) | 79
Prunelle | Léon | (null)
(3 rows)

198
5. PLUS LOIN AVEC SQL

Exemple d’utilisation de NULL dans une concaténation :

SELECT nom || ' ' || prenom AS nom_complet FROM personnes;


nom_complet
----------------
Lagaffe Gaston
(null)
Prunelle Léon
(3 rows)

L’affichage (null) est obtenu avec la méta-commande \pset null (null) du shell psql.

5.2.4 NULL ET LES PRÉDICATS

• Dans un prédicat du WHERE :


– opérateur IS NULL ou IS NOT NULL
• AND :
– vaut false si NULL AND false
– vaut NULL si NULL AND true ou NULL AND NULL
• OR :
– vaut true si NULL OR true
– vaut NULL si NULL OR false ou NULL OR NULL

Les opérateurs de comparaisons classiques ne sont pas fonctionnels avec une valeur NULL.
Du fait de la logique à trois états de PostgreSQL, une comparaison avec NULL vaut toujours
NULL, ainsi expression = NULL vaudra toujours NULL et de même pour expression <>
NULL vaudra toujours NULL. Cette comparaison ne vaudra jamais ni vrai, ni faux.

De ce fait, il existe les opérateurs de prédicats IS NULL et IS NOT NULL qui permettent
de vérifier qu’une expression est NULL ou n’est pas NULL.

Pour en savoir plus sur la logique ternaire qui régit les règles de calcul des prédicats, se
conformer à la page Wikipédia sur la logique ternaire72 .

Exemples

Comparaison directe avec NULL, qui est invalide :

SELECT * FROM personnes WHERE date_naissance = NULL;


id | nom | prenom | date_naissance
----+-----+--------+----------------
(0 rows)

72
https://fr.wikipedia.org/wiki/Logique_ternaire

199
https://dalibo.com/formations
SQL pour PostgreSQL

L’opérateur IS NULL permet de retourner les lignes dont la date de naissance n’est pas
renseignée :
SELECT * FROM personnes WHERE date_naissance IS NULL;
id | nom | prenom | date_naissance
----+----------+--------+----------------
3 | Prunelle | Léon | (null)
(1 row)

5.2.5 NULL ET LES AGRÉGATS

• Opérateurs d’agrégats
– ignorent NULL
– sauf count(*)

Les fonctions d’agrégats ne tiennent pas compte des valeurs NULL :


SELECT SUM(extract('year' from age(date_naissance))) AS age_cumule
FROM personnes;
age_cumule
------------
139
(1 row)

Sauf count(*) et uniquement count(*), la fonction count(_expression_) tient compte


des valeurs NULL :
SELECT count(*) AS compte_lignes, count(date_naissance) AS compte_valeurs
FROM (SELECT date_naissance
FROM personnes) date_naissance;
compte_lignes | compte_valeurs
---------------+----------------
3 | 2
(1 row)

200
5. PLUS LOIN AVEC SQL

5.2.6 COALESCE

• Remplacer NULL par une autre valeur


– COALESCE(attribut, ...);

Cette fonction permet de tester une colonne et de récupérer sa valeur si elle n’est pas
NULL et une autre valeur dans le cas contraire. Elle peut avoir plus de deux arguments.
Dans ce cas, la première expression de la liste qui ne vaut pas NULL sera retournée par la
fonction.

Voici quelques exemples :

Remplace les prénoms non-renseignés par la valeur X dans le résultat :


SELECT nom, COALESCE(prenom, 'X') FROM personnes;
nom | coalesce
----------+----------
Lagaffe | Gaston
Fantasio | X
Prunelle | Léon
(3 rows)

Cette fonction est efficace également pour la concaténation précédente :


SELECT nom || ' ' || COALESCE(prenom, '') AS nom_complet FROM personnes;
nom_complet
----------------
Lagaffe Gaston
Fantasio
Prunelle Léon
(3 rows)

5.3 AGRÉGATS

• Regroupement de données
• Calculs d’agrégats

Comme son nom l’indique, l’agrégation permet de regrouper des données, qu’elles vien-
nent d’une ou de plusieurs colonnes. Le but est principalement de réaliser des calculs sur
les données des lignes regroupées.

201
https://dalibo.com/formations
SQL pour PostgreSQL

5.3.1 REGROUPEMENT DE DONNÉES

• Regroupement de données :
GROUP BY expression [, ...]
• Chaque groupe de données est ensuite représenté sur une seule ligne
• Permet d’appliquer des calculs sur les ensembles regroupés
– comptage, somme, moyenne, etc.

La clause GROUP BY permet de réaliser des regroupements de données. Les données re-
groupées sont alors représentées sur une seule ligne. Le principal intérêt de ces regroupe-
ments est de permettre de réaliser des calculs sur ces données.

5.3.2 CALCULS D'AGRÉGATS

• Effectuent un calcul sur un ensemble de valeurs


– somme, moyenne, etc.
• Retournent NULL si l’ensemble est vide
– sauf count()

Nous allons voir les différentes fonctions d’agrégats disponibles.

5.3.3 AGRÉGATS SIMPLES

• Comptage : COUNT(expression)
• compte les lignes : COUNT(*)
– compte les valeurs renseignées : COUNT(colonne)
• Valeur minimale : min(expression)
• Valeur maximale : max(expression)

La fonction count() permet de compter les éléments. La fonction est appelée de deux
façons.

La première forme consiste à utiliser count(*) qui revient à transmettre la ligne com-
plète à la fonction d’agrégat. Ainsi, toute ligne transmise à la fonction sera comptée,
même si elle n’est composée que de valeurs NULL. On rencontre parfois une forme du type
count(1), qui transmet une valeur arbitraire à la fonction, et qui permettait d’accélérer le
temps de traitement sur certains SGBD mais qui reste sans intérêt avec PostgreSQL.

202
5. PLUS LOIN AVEC SQL

La seconde forme consiste à utiliser une expression, par exemple le nom d’une colonne :
count(nom_colonne). Dans ce cas-là, seules les valeurs renseignées, donc non NULL,
seront prises en compte. Les valeurs NULL seront exclues du comptage.

La fonction min() permet de déterminer la valeur la plus petite d’un ensemble de valeurs
données. La fonction max() permet à l’inverse de déterminer la valeur la plus grande d’un
ensemble de valeurs données. Les valeurs NULL sont bien ignorées. Ces deux fonctions
permettent de travailler sur des données numériques, mais fonctionnent également sur
les autres types de données comme les chaînes de caractères.

La documentation de PostgreSQL permet d’obtenir la liste des fonctions d’agrégats


disponibles73 .

Exemples

Différences entre count(*) et count(colonne) :


CREATE TABLE test (x INTEGER);
-- insertion de cinq lignes dans la table test
INSERT INTO test (x) VALUES (1), (2), (2), (NULL), (NULL);

SELECT x, count(*) AS count_etoile, count(x) AS count_x FROM test GROUP BY x;


x | count_etoile | count_x
--------+--------------+---------
(null) | 2 | 0
1 | 1 | 1
2 | 2 | 2
(3 rows)

Déterminer la date de naissance de la personne la plus jeune :


SELECT MAX(date_naissance) FROM personnes;
max
------------
1957-02-28
(1 row)

73
http://docs.postgresql.fr/current/functions-aggregate.html

203
https://dalibo.com/formations
SQL pour PostgreSQL

5.3.4 CALCULS D'AGRÉGATS

• Moyenne : avg(expression)
• Somme : sql sum(expression)
• Écart-type : stddev(expression)
• Variance : variance(expression)

La fonction avg() permet d’obtenir la moyenne d’un ensemble de valeurs données. La


fonction sum() permet, quant à elle, d’obtenir la somme d’un ensemble de valeurs don-
nées. Enfin, les fonctions stddev() et variance() permettent d’obtenir respectivement
l’écart-type et la variance d’un ensemble de valeurs données.

Ces fonctions retournent NULL si aucune donnée n’est applicable. Elles ne prennent en
compte que des valeurs numériques.

La documentation de PostgreSQL permet d’obtenir la liste des fonctions d’agrégats


disponibles74 .

Exemples

Quel est le nombre total de bouteilles en stock par millésime ?


SELECT annee, sum(nombre) FROM stock GROUP BY annee ORDER BY annee;
annee | sum
-------+--------
1950 | 210967
1951 | 201977
1952 | 202183
...

Calcul de moyenne avec des valeurs NULL :


CREATE TABLE test (a int, b int);
INSERT INTO test VALUES (10,10);
INSERT INTO test VALUES (20,20);
INSERT INTO test VALUES (30,30);
INSERT INTO test VALUES (null,0);

SELECT avg(a), avg(b) FROM test;


avg | avg
---------------------+---------------------
20.0000000000000000 | 15.0000000000000000
(1 row)

74
https://docs.postgresql.fr/current/functions-aggregate.html

204
5. PLUS LOIN AVEC SQL

5.3.5 AGRÉGATS SUR PLUSIEURS COLONNES

• Possible d’avoir plusieurs paramètres sur la même fonction d’agrégat


• Quelques exemples
– pente : regr_slope(Y,X)
– intersection avec l’axe des ordonnées : regr_intercept(Y,X)
– indice de corrélation : corr (Y,X)

Une fonction d’agrégat peut aussi prendre plusieurs variables.

Par exemple concernant la méthode des « moindres carrés » :

• pente : regr_slope(Y,X)
• intersection avec l’axe des ordonnées : regr_intercept(Y,X)
• indice de corrélation : corr (Y,X)

Voici un exemple avec un nuage de points proches d’une fonction y=2x+5 :


CREATE TABLE test (x real, y real);
INSERT INTO test VALUES (0,5.01), (1,6.99), (2,9.03);

SELECT regr_slope(y,x) FROM test;


regr_slope
------------------
2.00999975204468
(1 ligne)

SELECT regr_intercept(y,x) FROM test;


regr_intercept
------------------
5.00000015894572
(1 ligne)

SELECT corr(y,x) FROM test;


corr
-------------------
0.999962873745297

205
https://dalibo.com/formations
SQL pour PostgreSQL

5.3.6 CLAUSE HAVING

• Filtrer sur des regroupements


– HAVING
• WHERE s’applique sur les lignes lues
• HAVING s’applique sur les lignes groupées

La clause HAVING permet de filtrer les résultats sur les regroupements réalisés par la clause
GROUP BY. Il est possible d’utiliser une fonction d’agrégat dans la clause HAVING.

Il faudra néanmoins faire attention à ne pas utiliser la clause HAVING comme clause de
filtrage des données lues par la requête. La clause HAVING ne doit permettre de filtrer
que les données traitées par la requête.

Ainsi, si l’on souhaite le nombre de vins rouge référencés dans le catalogue. La requête
va donc exclure toutes les données de la table vin qui ne correspondent pas au filtre
type_vin = 3. Pour réaliser cela, on utilisera la clause WHERE.

En revanche, si l’on souhaite connaître le nombre de vins par type de cépage si ce nombre
est supérieur à 2030, on utilisera la clause HAVING.

Exemples
SELECT type_vin_id, count(*)
FROM vin
GROUP BY type_vin_id
HAVING count(*) > 2030;
type_vin_id | count
-------------+-------
1 | 2031

Si la colonne correspondant à la fonction d’agrégat est renommée avec la clause AS, il n’est
pas possible d’utiliser le nouveau nom au sein de la clause HAVING. Par exemple :
SELECT type_vin_id, count(*) AS nombre
FROM vin
GROUP BY type_vin_id
HAVING nombre > 2030;

ERROR: column "nombre" does not exist

206
5. PLUS LOIN AVEC SQL

5.4 SOUS-REQUÊTES

• Corrélation requête/sous-requête
• Sous-requêtes retournant une seule ligne
• Sous-requêtes retournant une liste de valeur
• Sous-requêtes retournant un ensemble
• Sous-requêtes retournant un ensemble vide ou non-vide

5.4.1 CORRÉLATION REQUÊTE/SOUS-REQUÊTE

• Fait référence à la requête principale


• Peut utiliser une valeur issue de la requête principale

Une sous-requête peut faire référence à des variables de la requête principale. Ces vari-
ables seront ainsi transformées en constante à chaque évaluation de la sous-requête.

La corrélation requête/sous-requête permet notamment de créer des clauses de filtrage


dans la sous-requête en utilisant des éléments de la requête principale.

5.4.2 QU'EST-CE QU'UNE SOUS-REQUÊTE ?

• Une requête imbriquée dans une autre requête


• Le résultat de la requête principale dépend du résultat de la sous-requête
• Encadrée par des parenthèses : ( et )

Une sous-requête consiste à exécuter une requête à l’intérieur d’une autre requête. La
requête principale peut être une requête de sélection (SELECT) ou une requête de modi-
fication (INSERT, UPDATE, DELETE). La sous-requête est obligatoirement un SELECT.

Le résultat de la requête principale dépend du résultat de la sous-requête. La requête suiv-


ante effectue la sélection des colonnes d’une autre requête, qui est une sous-requête. La
sous-requête effectue une lecture de la table appellation. Son résultat est transformé
en un ensemble qui est nommé requete_appellation :
SELECT * FROM (SELECT libelle, region_id FROM appellation) requete_appellation;
libelle | region_id
-------------------------------------------+-----------
Ajaccio | 1
Aloxe-Corton | 2
...
207
https://dalibo.com/formations
SQL pour PostgreSQL

5.4.3 UTILISER UNE SEULE LIGNE

• La sous-requête ne retourne qu’une seule ligne


– sinon une erreur est levée
• Positionnée
– au niveau de la liste des expressions retournées par SELECT
– au niveau de la clause WHERE
– au niveau d’une clause HAVING

La sous-requête peut être positionnée au niveau de la liste des expressions retournées


par SELECT. La sous-requête est alors généralement un calcul d’agrégat qui ne donne en
résultat qu’une seule colonne sur une seule ligne. Ce type de sous-requête est peu per-
formant. Elle est en effet appelée pour chaque ligne retournée par la requête principale.

La requête suivante permet d’obtenir le cumul du nombre de bouteilles année par année.
SELECT annee,
sum(nombre) AS stock,
(SELECT sum(nombre)
FROM stock s
WHERE s.annee <= stock.annee) AS stock_cumule
FROM stock
GROUP BY annee
ORDER BY annee;
annee | stock | stock_cumule
-------+--------+--------------
1950 | 210967 | 210967
1951 | 201977 | 412944
1952 | 202183 | 615127
1953 | 202489 | 817616
1954 | 202041 | 1019657
...

Une telle sous-requête peut également être positionnée au niveau de la clause WHERE ou
de la clause HAVING.

Par exemple, pour retourner la liste des vins rouge :


SELECT *
FROM vin
WHERE type_vin_id = (SELECT id
FROM type_vin
WHERE libelle = 'rouge');

208
5. PLUS LOIN AVEC SQL

5.4.4 UTILISER UNE LISTE DE VALEURS

• La sous-requête retourne
– plusieurs lignes
– sur une seule colonne
• Positionnée
– avec une clause IN

Les sous-requêtes retournant une liste de valeur sont plus fréquemment utilisées. Ce
type de sous-requête permet de filtrer les résultats de la requête principale à partir des
résultats de la sous-requête.

5.4.5 CLAUSE IN

WHERE x IN (SELECT y FROM … )


• L’expression de gauche est évaluée et vérifiée avec la liste de valeurs de droite
• IN vaut true
– si l’expression de gauche correspond à un élément de la liste de droite
• IN vaut false
– si aucune correspondance n’est trouvée et la liste ne contient pas NULL
• IN vaut NULL
– si l’expression de gauche vaut NULL
– si aucune valeur ne correspond et la liste contient NULL
• équivalent : WHERE x = ANY (SELECT y…)

La clause IN dans la requête principale permet alors d’exploiter le résultat de la sous-


requête pour sélectionner les lignes dont une colonne correspond à une valeur retournée
par la sous-requête.

L’opérateur IN retourne true si la valeur de l’expression de gauche est trouvée au moins


une fois dans la liste de droite. La liste de droite peut contenir la valeur NULL dans ce cas :

SELECT 1 IN (1, 2, NULL) AS in;


in
----
t

Si aucune correspondance n’est trouvée entre l’expression de gauche et la liste de droite,


alors IN vaut false :

SELECT 1 IN (2, 4) AS in;


in
209
https://dalibo.com/formations
SQL pour PostgreSQL

----
f

Mais IN vaut NULL si aucune correspondance n’est trouvée et que la liste de droite contient
au moins une valeur NULL :
SELECT 1 IN (2, 4, NULL) AS in;
in
--------
(null)

IN vaut également NULL si l’expression de gauche vaut NULL :


SELECT NULL IN (2, 4) AS in;
in
--------
(null)

Exemples

La requête suivante permet de sélectionner les bouteilles du stock de la cave dont la con-
tenance est comprise entre 0,3 litre et 1 litre. Pour répondre à la question, la sous-requête
retourne les identifiants de contenant qui correspondent à la condition. La requête princi-
pale ne retient alors que les lignes dont la colonne contenant_id correspond à une valeur
d’identifiant retournée par la sous-requête.
SELECT *
FROM stock
WHERE contenant_id IN (SELECT id
FROM contenant
WHERE contenance
BETWEEN 0.3 AND 1.0);

Une variante assez rare existe, la clause = ANY :


SELECT *
FROM stock
WHERE contenant_id = ANY (SELECT id
FROM contenant
WHERE contenance
BETWEEN 0.3 AND 1.0);

210
5. PLUS LOIN AVEC SQL

5.4.6 CLAUSE NOT IN

WHERE x NOT IN (SELECT y FROM …)


• L’expression de droite est évaluée et vérifiée avec la liste de valeurs de gauche
• NOT IN vaut true
– si aucune correspondance n’est trouvée et la liste ne contient pas NULL
• NOT IN vaut false
– si l’expression de gauche correspond à un élément de la liste de droite
• NOT IN vaut NULL
– si l’expression de gauche vaut NULL
– si aucune valeur ne correspond et la liste contient NULL

À l’inverse, la clause NOT IN permet dans la requête principale de sélectionner les lignes
dont la colonne impliquée dans la condition ne correspond pas aux valeurs retournées
par la sous-requête.

La requête suivante permet de sélectionner les bouteilles du stock dont la contenance


n’est pas inférieure à 2 litres.

SELECT *
FROM stock
WHERE contenant_id NOT IN (SELECT id
FROM contenant
WHERE contenance < 2.0);

Il est à noter que les requêtes impliquant les clauses IN ou NOT IN peuvent généralement
être réécrites sous la forme d’une jointure.

De plus, les optimiseurs SQL parviennent difficilement à optimiser une requête impliquant
NOT IN. Il est préférable d’essayer de réécrire ces requêtes en utilisant une jointure.

Il existe un équivalent assez rarement rencontré, <> ALL :

SELECT *
FROM stock
WHERE contenant_id <> ALL (SELECT id
FROM contenant
WHERE contenance < 2.0);

Avec NOT IN, la gestion des valeurs NULL est à l’inverse de celle de la clause IN :

Si une correspondance est trouvée, NOT IN vaut false :

SELECT 1 NOT IN (1, 2, NULL) AS notin;


notin
-------
f
211
https://dalibo.com/formations
SQL pour PostgreSQL

Si aucune correspondance n’est trouvée, NOT IN vaut true :


SELECT 1 NOT IN (2, 4) AS notin;
notin
-------
t

Si aucune correspondance n’est trouvée mais que la liste de valeurs de droite contient au
moins un NULL, NOT IN vaut NULL :
SELECT 1 NOT IN (2, 4, NULL) AS notin;
notin
--------
(null)

Si l’expression de gauche vaut NULL, alors NOT IN vaut NULL également :


SELECT NULL IN (2, 4) AS notin;
notin
--------
(null)

Les sous-requêtes retournant des valeurs NULL posent souvent des problèmes avec NOT
IN. Il est préférable d’utiliser EXISTS ou NOT EXISTS pour ne pas avoir à se soucier des
valeurs NULL.

5.4.7 UTILISER UN ENSEMBLE

• La sous-requête retourne
– plusieurs lignes
– sur plusieurs colonnes
• Positionnée au niveau de la clause FROM
• Nommée avec un alias de table

La sous-requête peut être utilisée dans la clause FROM afin d’être utilisée comme une ta-
ble dans la requête principale. La sous-requête devra obligatoirement être nommée avec
un alias de table. Lorsqu’elles sont issues d’un calcul, les colonnes résultantes doivent
également être nommées avec un alias de colonne afin d’éviter toute confusion ou com-
portement incohérent.

La requête suivante permet de déterminer le nombre moyen de bouteilles par année :


SELECT AVG(nombre_total_annee) AS moyenne
FROM (SELECT annee, sum(nombre) AS nombre_total_annee
FROM stock
GROUP BY annee) stock_total_par_annee;

212
5. PLUS LOIN AVEC SQL

5.4.8 CLAUSE EXISTS

WHERE EXISTS (sous-requete)


• Intéressant avec une corrélation
• La clause EXISTS vérifie la présence ou l’absence de résultats
– vrai si l’ensemble est non vide
– faux si l’ensemble est vide

EXISTS présente peu d’intérêt sans corrélation entre la sous-requête et la requête princi-
pale.

Le prédicat EXISTS est en général plus performant que IN. Lorsqu’une requête utilisant IN
ne peut pas être réécrite sous la forme d’une jointure, il est recommandé d’utiliser EXISTS
en lieu et place de IN. Et à l’inverse, une clause NOT IN sera réécrite avec NOT EXISTS.

La requête suivante permet d’identifier les vins pour lesquels il y a au moins une bouteille
en stock :
SELECT *
FROM vin
WHERE EXISTS (SELECT *
FROM stock
WHERE vin_id = vin.id);

5.5 JOINTURES

• Conditions de jointure dans JOIN ou dans WHERE ?


• Produit cartésien
• Jointure interne
• Jointures externes
• Jointure ou sous-requête ?

Les jointures permettent d’écrire des requêtes qui impliquent plusieurs tables. Elles per-
mettent de combiner les colonnes de plusieurs tables selon des critères particuliers, ap-
pelés conditions de jointures.

Les jointures permettent de tirer parti du modèle de données dans lequel les tables sont
associées à l’aide de clés étrangères.

213
https://dalibo.com/formations
SQL pour PostgreSQL

5.5.1 CONDITIONS DE JOINTURE DANS JOIN OU DANS WHERE ?

• Jointure dans clause JOIN


– Séparation nette jointure et filtrage
– Plus lisible et maintenable
– Jointures externes propres
– Facilite le travail de l’optimiseur
• Jointure dans clause WHERE
– Historique

Bien qu’il soit possible de décrire une jointure interne sous la forme d’une requête SELECT
portant sur deux tables dont la condition de jointure est décrite dans la clause WHERE,
cette forme d’écriture n’est pas recommandée. Elle est essentiellement historique et se
retrouve surtout dans des projets migrés sans modification.

En effet, les conditions de jointures se trouvent mélangées avec les clauses de filtrage,
rendant ainsi la compréhension et la maintenance difficiles. Il arrive aussi que, noyé dans
les autres conditions de filtrage, l’utilisateur oublie la configuration de jointure, ce qui
aboutit à un produit cartésien, n’ayant rien à voir avec le résultat attendu, sans même
parler de la lenteur de la requête.

Il est recommandé d’utiliser la syntaxe SQL:92 et d’exprimer les jointures à l’aide de la


clause JOIN. D’ailleurs, cette syntaxe est la seule qui soit utilisable pour exprimer simple-
ment et efficacement une jointure externe. Cette syntaxe facilite la compréhension de
la requête mais facilite également le travail de l’optimiseur SQL qui peut déduire beau-
coup plus rapidement les jointures qu’en analysant la clause WHERE pour déterminer les
conditions de jointure et les tables auxquelles elles s’appliquent le cas échéant.

Comparer ces deux exemples d’une requête typique d’ERP pourtant simplifiée :

SELECT
clients.numero,
SUM(lignes_commandes.chiffre_affaire)
FROM
lignes_commandes
INNER JOIN commandes ON (lignes_commandes.commande_id = commandes.id)
INNER JOIN clients ON (commandes.client_id = clients.id)
INNER JOIN addresses ON (clients.adresse_id = addresses.id)
INNER JOIN pays ON (adresses.pays_id = pays.id)
WHERE
pays.code = 'FR'
AND addresses.ville = 'Strasbourg'
AND commandes.statut = 'LIVRÉ'
AND clients.type = 'PARTICULIER'

214
5. PLUS LOIN AVEC SQL

AND clients.actif IS TRUE


GROUP BY clients.numero ;

et :
SELECT
clients.numero,
SUM(lignes_commandes.chiffre_affaire)
FROM
lignes_commandes,
commandes,
clients,
addresses,
pays
WHERE
pays.code = 'FR'
AND lignes_commandes.commande_id = commandes.id
AND commandes.client_id = clients.id
AND commandes.statut = 'LIVRÉ'
AND clients.type = 'PARTICULIER'
AND clients.actif IS TRUE
AND clients.adresse_id = addresses.id
AND adresses.pays_id = pays.id
AND addresses.ville = 'Strasbourg'
GROUP BY clients.numero ;

5.5.2 PRODUIT CARTÉSIEN

• Clause CROSS JOIN


• Réalise toutes les combinaisons entre les lignes d’une table et les lignes d’une autre
• À éviter dans la mesure du possible
– peu de cas d’utilisation
– peu performant

Le produit cartésien peut être exprimé avec la clause de jointure CROSS JOIN :
-- préparation du jeu de données
CREATE TABLE t1 (i1 integer, v1 integer);
CREATE TABLE t2 (i2 integer, v2 integer);
INSERT INTO t1 (i1, v1) VALUES (0, 0), (1, 1);
INSERT INTO t2 (i2, v2) VALUES (2, 2), (3, 3);

-- requête CROSS JOIN


SELECT * FROM t1 CROSS JOIN t2;
i1 | v1 | i2 | v2
215
https://dalibo.com/formations
SQL pour PostgreSQL

----+----+----+----
0 | 0 | 2 | 2
0 | 0 | 3 | 3
1 | 1 | 2 | 2
1 | 1 | 3 | 3
(4 rows)

Ou plus simplement, en listant les deux tables dans la clause FROM sans indiquer de con-
dition de jointure :

SELECT * FROM t1, t2;


i1 | v1 | i2 | v2
----+----+----+----
0 | 0 | 2 | 2
0 | 0 | 3 | 3
1 | 1 | 2 | 2
1 | 1 | 3 | 3
(4 rows)

Voici un autre exemple utilisant aussi un NOT EXISTS :

CREATE TABLE sondes (id_sonde int, nom_sonde text);


CREATE TABLE releves_horaires (
id_sonde int,
heure_releve timestamptz check
(date_trunc('hour',heure_releve)=heure_releve),
valeur numeric);

INSERT INTO sondes VALUES (1,'sonde 1'),


(2, 'sonde 2'),
(3, 'sonde 3');

INSERT INTO releves_horaires VALUES


(1,'2013-01-01 12:00:00',10),
(1,'2013-01-01 13:00:00',11),
(1,'2013-01-01 14:00:00',12),
(2,'2013-01-01 12:00:00',10),
(2,'2013-01-01 13:00:00',12),
(2,'2013-01-01 14:00:00',12),
(3,'2013-01-01 12:00:00',10),
(3,'2013-01-01 14:00:00',10);

-- quels sont les relevés manquants entre 12h et 14h ?

SELECT id_sonde,
heures_releves
FROM sondes
CROSS JOIN generate_series('2013-01-01 12:00:00','2013-01-01 14:00:00',

216
5. PLUS LOIN AVEC SQL

interval '1 hour') series(heures_releves)


WHERE NOT EXISTS
(SELECT 1
FROM releves_horaires
WHERE releves_horaires.id_sonde=sondes.id_sonde
AND releves_horaires.heure_releve=series.heures_releves);

id_sonde | heures_releves
----------+------------------------
3 | 2013-01-01 13:00:00+01
(1 ligne)

5.5.3 JOINTURE INTERNE

• Clause INNER JOIN


– meilleure lisibilité
– facilite le travail de l’optimiseur
• Joint deux tables entre elles
– Selon une condition de jointure

Figure 3: Schéma de jointure interne

Une jointure interne est considérée comme un produit cartésien accompagné d’une clause
de jointure pour ne conserver que les lignes qui répondent à la condition de jointure. Les
SGBD réalisent néanmoins l’opération plus simplement.
217
https://dalibo.com/formations
SQL pour PostgreSQL

La condition de jointure est généralement une égalité, ce qui permet d’associer entre elles
les lignes de la table à gauche et de la table à droite dont les colonnes de condition de
jointure sont égales.

La jointure interne est exprimée à travers la clause INNER JOIN ou plus simplement JOIN.
En effet, si le type de jointure n’est pas spécifié, l’optimiseur considère la jointure comme
étant une jointure interne.

5.5.4 SYNTAXE D'UNE JOINTURE INTERNE

• Condition de jointure par prédicats :


table1 [INNER] JOIN table2 ON prédicat [...]
• Condition de jointure implicite par liste des colonnes impliquées :
table1 [INNER] JOIN table2 USING (colonne [, ...])
• Liste des colonnes de même nom (dangereux) :
table1 NATURAL [INNER] JOIN table2

La clause ON permet d’écrire les conditions de jointures sous la forme de prédicats tels
qu’on les retrouve dans une clause WHERE.

La clause USING permet de spécifier les colonnes sur lesquelles porte la jointure. Les
tables jointes devront posséder toutes les colonnes sur lesquelles portent la jointure. La
jointure sera réalisée en vérifiant l’égalité entre chaque colonne portant le même nom.

La clause NATURAL permet de réaliser la jointure entre deux tables en utilisant les colonnes
qui portent le même nom sur les deux tables comme condition de jointure. NATURAL
JOIN est fortement déconseillé car elle peut facilement entraîner des comportements
inattendus.

La requête suivante permet de joindre la table appellation avec la table region pour déter-
miner l’origine d’une appellation :
SELECT apl.libelle AS appellation, reg.libelle AS region
FROM appellation apl
JOIN region reg
ON (apl.region_id = reg.id);

218
5. PLUS LOIN AVEC SQL

5.5.5 JOINTURE EXTERNE

• Jointure externe à gauche


– ramène le résultat de la jointure interne
– ramène l’ensemble de la table de gauche qui ne peut être joint avec la table
de droite
– les attributs de la table de droite sont alors NULL

Figure 4: Schéma de jointure externe gauche

Il existe deux types de jointure externe : la jointure à gauche et la jointure à droite. Cela
ne concerne que l’ordre de la jointure, le traitement en lui- même est identique.

5.5.6 JOINTURE EXTERNE - 2

• Jointure externe à droite


– ramène le résultat de la jointure interne
– ramène l’ensemble de la table de droite qui ne peut être joint avec la table de
gauche
– les attributs de la table de gauche sont alors NULL

219
https://dalibo.com/formations
SQL pour PostgreSQL

Figure 5: Schéma de jointure externe droite

5.5.7 JOINTURE EXTERNE COMPLÈTE

• Ramène le résultat de la jointure interne


• Ramène l’ensemble de la table de gauche qui ne peut être joint avec la table de
droite
– les attributs de la table de droite sont alors NULL
• Ramène l’ensemble de la table de droite qui ne peut être joint avec la table de
gauche
– les attributs de la table de gauche sont alors NULL

5.5.8 SYNTAXE D'UNE JOINTURE EXTERNE À GAUCHE

• Condition de jointure par prédicats :


table1 LEFT [OUTER] JOIN table2 ON prédicat [...]
• Condition de jointure implicite par liste des colonnes impliquées :
table1 LEFT [OUTER] JOIN table2 USING (colonne [, ...])
• Liste des colonnes implicites :
table1 NATURAL LEFT [OUTER] JOIN table2

Il existe trois écritures différentes d’une jointure externe à gauche. La clause NATURAL
permet de réaliser la jointure entre deux tables en utilisant les colonnes qui portent le
même nom sur les deux tables comme condition de jointure.

220
5. PLUS LOIN AVEC SQL

Les voici en exemple :

• par prédicat :
SELECT article.art_titre, auteur.aut_nom
FROM article
LEFT JOIN auteur
ON (article.aut_id=auteur.aut_id);

• par liste de colonnes :


SELECT article.art_titre, auteur.aut_nom
FROM article
LEFT JOIN auteur
USING (aut_id);

5.5.9 SYNTAXE D'UNE JOINTURE EXTERNE À DROITE

• Condition de jointure par prédicats :


table1 RIGHT [OUTER] JOIN table2 ON prédicat [...]
• Condition de jointure implicite par liste des colonnes impliquées :
table1 RIGHT [OUTER] JOIN table2 USING (colonne [, ...])
• Liste des colonnes implicites :
table1 NATURAL RIGHT [OUTER] JOIN table2

Les jointures à droite sont moins fréquentes mais elles restent utilisées.

5.5.10 SYNTAXE D'UNE JOINTURE EXTERNE COMPLÈTE

• Condition de jointure par prédicats :


table1 FULL OUTER JOIN table2 ON prédicat [...]
• Condition de jointure implicite par liste des colonnes impliquées :
table1 FULL OUTER JOIN table2 USING (colonne [, ...])
• Liste des colonnes implicites :
table1 NATURAL FULL OUTER JOIN table2

221
https://dalibo.com/formations
SQL pour PostgreSQL

5.5.11 JOINTURE OU SOUS-REQUÊTE ?

• Jointures
– algorithmes très efficaces
– ne gèrent pas tous les cas
• Sous-requêtes
– parfois peu performantes
– répondent à des besoins non couverts par les jointures

Les sous-requêtes sont fréquemment utilisées mais elles sont moins performantes que
les jointures. Ces dernières permettent d’utiliser des optimisations très efficaces.

5.6 EXPRESSIONS CASE

• Équivalent à l’instruction switch en C ou Java


• Emprunté au langage Ada
• Retourne une valeur en fonction du résultat de tests

CASE permet de tester différents cas. Il s’utilise de la façon suivante :


SELECT
CASE WHEN col1=10 THEN 'dix'
WHEN col1>10 THEN 'supérieur à 10'
ELSE 'inférieur à 10'
END AS test
FROM t1;

5.6.1 CASE SIMPLE

CASE expression
WHEN valeur THEN expression
WHEN valeur THEN expression
(...)
ELSE expression
END

Il est possible de tester le résultat d’une expression avec CASE. Dans ce cas, chaque clause
WHEN reprendra la valeur à laquelle on souhaite associé une expression particulière :
CASE nom_region
WHEN 'Afrique' THEN 1

222
5. PLUS LOIN AVEC SQL

WHEN 'Amérique' THEN 2


WHEN 'Asie' THEN 3
WHEN 'Europe' THEN 4
ELSE 0
END

5.6.2 CASE SUR EXPRESSIONS

CASE WHEN expression THEN expression


WHEN expression THEN expression
(...)
ELSE expression
END

Une expression peut être évaluée pour chaque clause WHEN. Dans ce cas, l’expression CASE
retourne la première expression qui est vraie. Si une autre peut satisfaire la suivante, elle
ne sera pas évaluée.

Par exemple :
CASE WHEN salaire * prime < 1300 THEN salaire * prime
WHEN salaire * prime < 3000 THEN salaire
WHEN salaire * prime > 5000 THEN salaire * prime
END

5.6.3 SPÉCIFICITÉS DE CASE

• Comportement procédural
– les expressions sont évaluées dans l’ordre d’apparition
• Transtypage
– le type du retour de l’expression dépend du type de rang le plus élevé de
toute l’expression
• Imbrication
– des expressions CASE à l’intérieur d’autres expressions CASE
• Clause ELSE
– recommandé

Il est possible de placer plusieurs clauses WHEN. Elles sont évaluées dans leur ordre
d’apparition.
CASE nom_region
WHEN 'Afrique' THEN 1
223
https://dalibo.com/formations
SQL pour PostgreSQL

WHEN 'Amérique' THEN 2


/* l'expression suivante ne sera jamais évaluée */
WHEN 'Afrique' THEN 5
WHEN 'Asie' THEN 1
WHEN 'Europe' THEN 3
ELSE 0
END

Le type de données renvoyé par l’instruction CASE correspond au type indiqué par
l’expression au niveau des THEN et du ELSE. Ce doit être le même type. Si les types de
données ne correspondent pas, alors PostgreSQL retournera une erreur :
SELECT *,
CASE nom_region
WHEN 'Afrique' THEN 1
WHEN 'Amérique' THEN 2
WHEN 'Asie' THEN 1
WHEN 'Europe' THEN 3
ELSE 'inconnu'
END
FROM regions;
ERROR: invalid input syntax for integer: "inconnu"
LIGNE 7 : ELSE 'inconnu'

La clause ELSE n’est pas obligatoire mais fortement recommandé. En effet, si une expres-
sion CASE ne comporte pas de clause ELSE, alors la base de données ajoutera une clause
ELSE NULL à l’expression.

Ainsi l’expression suivante :


CASE
WHEN salaire < 1000 THEN 'bas'
WHEN salaire > 3000 THEN 'haut'
END

Sera implicitement transformée de la façon suivante :


CASE
WHEN salaire < 1000 THEN 'bas'
WHEN salaire > 3000 THEN 'haut'
ELSE NULL
END

224
5. PLUS LOIN AVEC SQL

5.7 OPÉRATEURS ENSEMBLISTES

• UNION
• INTERSECT
• EXCEPT

5.7.1 REGROUPEMENT DE DEUX ENSEMBLES

• Regroupement avec dédoublonnage :


requete_select1 UNION requete_select2
• Regroupement sans dédoublonnage :
requete_select1 UNION ALL requete_select2

L’opérateur ensembliste UNION permet de regrouper deux ensembles dans un même ré-
sultat.

Le dédoublonnage peut être particulièrement coûteux car il implique un tri des données.

Exemples

La requête suivante assemble les résultats de deux requêtes pour produire le résultat :
SELECT *
FROM appellation
WHERE region_id = 1
UNION ALL
SELECT *
FROM appellation
WHERE region_id = 3;

5.7.2 INTERSECTION DE DEUX ENSEMBLES

• Intersection de deux ensembles avec dédoublonnage :


requete_select1 INTERSECT requete_select2
• Intersection de deux ensembles sans dédoublonnage :
requete_select1 INTERSECT ALL requete_select2

L’opérateur ensembliste INTERSECT permet d’obtenir l’intersection du résultat de deux


requêtes.

Le dédoublonnage peut être particulièrement coûteux car il implique un tri des données.

Exemples
225
https://dalibo.com/formations
SQL pour PostgreSQL

L’exemple suivant n’a pas d’autre intérêt que de montrer le résultat de l’opérateur
INTERSECT sur deux ensembles simples :
SELECT *
FROM region
INTERSECT
SELECT *
FROM region
WHERE id = 3;

id | libelle
----+---------
3 | Alsace

5.7.3 DIFFÉRENCE ENTRE DEUX ENSEMBLES

• Différence entre deux ensembles avec dédoublonnage :


requete_select1 EXCEPT requete_select2
• Différence entre deux ensembles sans dédoublonnage :
requete_select1 EXCEPT ALL requete_select2

L’opérateur ensembliste EXCEPT est l’équivalent de l’opérateur MINUS d’Oracle. Il permet


d’obtenir la différence entre deux ensembles : toutes les lignes présentes dans les deux
ensembles sont exclues du résultat.

Le dédoublonnage peut être particulièrement coûteux car il implique un tri des données.

Exemples

L’exemple suivant n’a pas d’autre intérêt que de montrer le résultat de l’opérateur EXCEPT
sur deux ensembles simples. La première requête retourne l’ensemble des lignes de la
table region alors que la seconde requête retourne la ligne qui correspond au prédicat
id = 3. Cette ligne est ensuite retirée du résultat car elle est présente dans les deux
ensembles de gauche et de droite :
SELECT *
FROM region
EXCEPT
SELECT *
FROM region
WHERE id = 3;

id | libelle
----+----------------------------
11 | Cotes du Rhone

226
5. PLUS LOIN AVEC SQL

12 | Provence produit a Cassis.


10 | Beaujolais
19 | Savoie
7 | Languedoc-Roussillon
4 | Loire
6 | Provence
16 | Est
8 | Bordeaux
14 | Lyonnais
15 | Auvergne
2 | Bourgogne
17 | Forez
9 | Vignoble du Sud-Ouest
18 | Charente
13 | Champagne
5 | Jura
1 | Provence et Corse
(18 rows)

5.8 CONCLUSION

• Possibilité d’écrire des requêtes complexes


• C’est là où PostgreSQL est le plus performant

Le standard SQL va bien plus loin que ce que les requêtes simplistes laissent penser.
Utiliser des requêtes complexes permet de décharger l’application d’un travail conséquent
et le développeur de coder quelque chose qui existe déjà. Cela aide aussi la base de
données car il est plus simple d’optimiser une requête complexe qu’un grand nombre de
requêtes simplistes.

5.8.1 QUESTIONS

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

227
https://dalibo.com/formations
SQL pour PostgreSQL

5.9 TRAVAUX PRATIQUES

Figure 6: Schéma base tpc

1. Affichez, par pays, le nombre de fournisseurs.

Sortie attendue :

nom_pays | nombre
-------------------------------+--------
ARABIE SAOUDITE | 425
ARGENTINE | 416
(...)

2. Affichez, par continent (regions), le nombre de fournisseurs.

Sortie attendue :

nom_region | nombre
---------------------------+--------
Afrique | 1906
Moyen-Orient | 2113

228
5. PLUS LOIN AVEC SQL

Europe | 2094
Asie | 2002
Amérique | 1885

3. Affichez le nombre de commandes trié selon le nombre de lignes de commandes au


sein de chaque commande.

Sortie attendue :

num | count
-----+-------
1 | 13733
2 | 27816
3 | 27750
4 | 27967
5 | 27687
6 | 27876
7 | 13895

4. Pour les 30 premières commandes (selon la date de commande), affichez le prix total
de la commande, en appliquant la remise accordée sur chaque article commandé. La
sortie sera triée de la commande la plus chère à la commande la moins chère.

Sortie attendue :

numero_commande | prix_total
-----------------+------------
3 | 259600.00
40 | 258959.00
6 | 249072.00
69 | 211330.00
70 | 202101.00
4 | 196132.00
(...)

5. Affichez, par année, le total des ventes. La date de commande fait foi. La sortie
sera triée par année.

Sortie attendue :

annee | total_vente
-------+---------------
2005 | 3627568010.00
2006 | 3630975501.00
2007 | 3627112891.00
229
https://dalibo.com/formations
SQL pour PostgreSQL

(...)

6. Pour toutes les commandes, calculez le temps moyen de livraison, depuis la date
d’expédition. Le temps de livraison moyen sera exprimé en jours, arrondi à l’entier
supérieur (fonction ceil()).

Sortie attendue :

temps_moyen_livraison
-----------------------
8 jour(s)

7. Pour les 30 commandes les plus récentes (selon la date de commande), calculez le
temps moyen de livraison de chaque commande, depuis la date de commande. Le
temps de livraison moyen sera exprimé en jours, arrondi à l’entier supérieur (fonc-
tion ceil()).

Sortie attendue :

temps_moyen_livraison
-----------------------
38 jour(s)

8. Déterminez le taux de retour des marchandises (l’état à R indiquant qu’une marchan-


dise est retournée).

Sortie attendue :

taux_retour
-------------
24.29

9. Déterminez le mode d’expédition qui est le plus rapide, en moyenne.

Sortie attendue :

mode_expedition | delai
-----------------+--------------------
AIR | 7.4711070230494535

10. Un bug applicatif est soupçonné, déterminez s’il existe des commandes dont la date
de commande est postérieure à la date de livraison des articles.

Sortie attendue :

count
-------
2

230
5. PLUS LOIN AVEC SQL

11. Écrivez une requête qui corrige les données erronés en positionnant la date de com-
mande à la date de livraison la plus ancienne des marchandises. Vérifiez qu’elle soit
correcte. Cette requête permet de corriger des calculs de statistiques sur les délais
de livraison.

12. Écrivez une requête qui calcule le délai total maximal de livraison de la totalité d’une
commande donnée, depuis la date de la commande.

Sortie attendue pour la commande n°1 :

delai_max
-----------
102

13. Écrivez une requête pour déterminer les 10 commandes dont le délai de livraison,
entre la date de commande et la date de réception, est le plus important, pour
l’année 2011 uniquement.

Sortie attendue :

numero_commande | delai
-----------------+-------
413510 | 146
123587 | 143
224453 | 143
(...)

14. Un autre bug applicatif est détecté. Certaines commandes n’ont pas de lignes de
commandes. Écrivez une requête pour les retrouver.

-[ RECORD 1 ]------------------------
numero_commande | 91495
client_id | 93528
etat_commande | P
prix_total |
date_commande | 2007-07-07
priorite_commande | 5-NOT SPECIFIED
vendeur | Vendeur 000006761
priorite_expedition | 0
commentaire | xxxxxxxxxxxxx

15. Écrivez une requête pour supprimer ces commandes. Vérifiez le travail avant de
valider.
231
https://dalibo.com/formations
SQL pour PostgreSQL

16. Écrivez une requête pour déterminer les 20 pièces qui ont eu le plus gros volume
de commande.

Sortie attendue :

nom | sum
--------------------------------------------+--------
lemon black goldenrod seashell plum | 461.00
brown lavender dim white indian | 408.00
burlywood white chiffon blanched lemon | 398.00
(...)

17. Affichez les fournisseurs des 20 pièces qui ont été le plus commandées sur l’année
2011.

Sortie attendue :

nom | piece_id
--------------+----------
Supplier4395 | 191875
Supplier4397 | 191875
Supplier6916 | 191875
Supplier9434 | 191875
Supplier4164 | 11662
Supplier6665 | 11662
(...)

18. Affichez le pays qui a connu, en nombre, le plus de commandes sur l’année 2011.

Sortie attendue :

nom_pays | count
-----------------+-------
ARABIE SAOUDITE | 1074

19. Affichez pour les commandes passées en 2011, la liste des continents et la marge
brute d’exploitation réalisée par continents, triés dans l’ordre décroissant.

Sortie attendue :

nom_region | benefice
---------------------------+---------------
Moyen-Orient | 2008595508.00
(...)

232
5. PLUS LOIN AVEC SQL

20. Affichez le nom, le numéro de téléphone et le pays des fournisseurs qui ont un
commentaire contenant le mot clé Complaints :

Sortie attendue :

nom_fournisseur | telephone | nom_pays


-----------------+-----------------+-------------------------------
Supplier3873 | 10-741-199-8614 | IRAN, RÉPUBLIQUE ISLAMIQUE D'
(...)

21. Déterminez le top 10 des fournisseurs ayant eu le plus long délai de livraison, entre
la date de commande et la date de réception, pour l’année 2011 uniquement.

Sortie attendue :

fournisseur_id | nom_fournisseur | delai


----------------+-----------------+-------
9414 | Supplier9414 | 146
(...)

233
https://dalibo.com/formations
SQL pour PostgreSQL

5.10 TRAVAUX PRATIQUES (SOLUTIONS)

1. Affichez, par pays, le nombre de fournisseurs.


SELECT p.nom_pays, count(*)
FROM fournisseurs f
JOIN contacts c ON f.contact_id = c.contact_id
JOIN pays p ON c.code_pays = p.code_pays
GROUP BY p.nom_pays
;

2. Affichez, par continent, le nombre de fournisseurs.


SELECT r.nom_region, count(*)
FROM fournisseurs f
JOIN contacts c ON f.contact_id = c.contact_id
JOIN pays p ON c.code_pays = p.code_pays
JOIN regions r ON p.region_id = r.region_id
GROUP BY r.nom_region
;

3. Affichez le nombre de commandes trié selon le nombre de lignes de commandes au


sein de chaque commande.
SELECT
nombre_lignes_commandes,
count(*) AS nombre_total_commandes
FROM (
/* cette sous-requête permet de compter le nombre de lignes de commande de
chaque commande, et remonte cette information à la requête principale */
SELECT count(numero_ligne_commande) AS nombre_lignes_commandes
FROM lignes_commandes
GROUP BY numero_commande
) comm_agg
/* la requête principale aggrège et trie les données sur ce nombre de lignes
de commandes pour compter le nombre de commandes distinctes ayant le même
nombre de lignes de commandes */
GROUP BY nombre_lignes_commandes
ORDER BY nombre_lignes_commandes DESC
;

4. Pour les 30 premières commandes (selon la date de commande), affichez le prix total
de la commande, en appliquant la remise accordée sur chaque article commandé. La
sortie sera triée de la commande la plus chère à la commande la moins chère.
SELECT c.numero_commande, sum(quantite * prix_unitaire - remise) prix_total
FROM (
SELECT numero_commande, date_commande
FROM commandes

234
5. PLUS LOIN AVEC SQL

ORDER BY date_commande
LIMIT 30
) c
JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
GROUP BY c.numero_commande
ORDER BY sum(quantite * prix_unitaire - remise) DESC
;

5. Affichez, par année, le total des ventes. La date de commande fait foi. La sortie
sera triée par année.

SELECT
extract ('year' FROM date_commande),
sum(quantite * prix - remise) AS prix_total
FROM commandes c
JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
JOIN pieces p ON lc.piece_id = p.piece_id
GROUP BY extract ('year' FROM date_commande)
ORDER BY extract ('year' FROM date_commande)
;

6. Pour toutes les commandes, calculez le temps moyen de livraison, depuis la date
d’expédition. Le temps de livraison moyen sera exprimé en jours, arrondi à l’entier
supérieur (fonction ceil()).

SELECT ceil(avg(date_reception - date_expedition))::text || ' jour(s)'


FROM lignes_commandes lc
;

7. Pour les 30 commandes les plus récentes (selon la date de commande), calculez le
temps moyen de livraison de chaque commande, depuis la date de commande. Le
temps de livraison moyen sera exprimé en jours, arrondi à l’entier supérieur (fonc-
tion ceil()).

SELECT count(*), ceil(avg(date_reception - date_commande))::text || ' jour(s)'


FROM (
SELECT numero_commande, date_commande
FROM commandes
ORDER BY date_commande DESC
LIMIT 30
) c
JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande ;

Note : la colonne date_commande de la table commandes n’a pas de contrainte NOT NULL,
il est donc possible d’avoir des commandes sans date de commande renseignée. Dans ce
cas, ces commandes vont remonter par défaut en haut de la liste, puisque la clause ORDER
BY renvoie les NULL après les valeurs les plus grandes, et que l’on inverse le tri. Pour éviter
235
https://dalibo.com/formations
SQL pour PostgreSQL

que ces commandes ne faussent les résultats, il faut donc les exclure de la sous-requête,
de la façon suivante :
SELECT numero_commande, date_commande
FROM commandes
WHERE date_commande IS NOT NULL
ORDER BY date_commande DESC
LIMIT 30

8. Déterminez le taux de retour des marchandises (l’état à R indiquant qu’une marchan-


dise est retournée).
SELECT
round(
sum(
CASE etat_retour
WHEN 'R' THEN 1.0
ELSE 0.0
END
) / count(*)::numeric * 100,
2
)::text || ' %' AS taux_retour
FROM lignes_commandes
;

À partir de la version 9.4 de PostgreSQL, la clause FILTER des fonctions d’aggrégation


permet d’écrire une telle requête plus facilement :
SELECT
round(
count(*) FILTER (WHERE etat_retour = 'R') / count(*)::numeric * 100,
2
)::text || ' %' AS taux_retour
FROM lignes_commandes
;

9. Déterminez le mode d’expédition qui est le plus rapide, en moyenne.


SELECT mode_expedition, avg(date_reception - date_expedition)
FROM lignes_commandes lc
GROUP BY mode_expedition
ORDER BY avg(date_reception - date_expedition) ASC
LIMIT 1
;

10. Un bug applicatif est soupçonné, déterminez s’il existe des commandes dont la date
de commande est postérieure à la date d’expédition des articles.
SELECT count(*)
FROM commandes c

236
5. PLUS LOIN AVEC SQL

JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande


AND c.date_commande > lc.date_expedition
;

11. Écrivez une requête qui corrige les données erronés en positionnant la date de com-
mande à la date d’expédition la plus ancienne des marchandises. Vérifiez qu’elle soit
correcte. Cette requête permet de corriger des calculs de statistiques sur les délais
de livraison.

Afin de se protéger d’une erreur de manipulation, on ouvre une transaction :


BEGIN;

UPDATE commandes c_up


SET date_commande = (
SELECT min(date_expedition)
FROM commandes c
JOIN lignes_commandes lc ON lc.numero_commande = c.numero_commande
AND c.date_commande > lc.date_expedition
WHERE c.numero_commande = c_up.numero_commande
)
WHERE EXISTS (
SELECT 1
FROM commandes c2
JOIN lignes_commandes lc ON lc.numero_commande = c2.numero_commande
AND c2.date_commande > lc.date_expedition
WHERE c_up.numero_commande = c2.numero_commande
GROUP BY 1
)
;

La requête réalisée précédemment doit à présent retourner 0 :


SELECT count(*)
FROM commandes c
JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
AND c.date_commande > lc.date_expedition
;

Si c’est le cas, on valide la transaction :


COMMIT;

Si ce n’est pas le cas, il doit y avoir une erreur dans la transaction, on l’annule :
ROLLBACK;

12. Écrivez une requête qui calcule le délai total maximal de livraison de la totalité d’une
commande donnée, depuis la date de la commande.

Par exemple pour la commande dont le numéro de commande est le 1 :


237
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT max(date_reception - date_commande)


FROM commandes c
JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
WHERE c.numero_commande = 1
;

13. Écrivez une requête pour déterminer les 10 commandes dont le délai de livraison,
entre la date de commande et la date de réception, est le plus important, pour
l’année 2011 uniquement.
SELECT
c.numero_commande,
max(date_reception - date_commande)
FROM commandes c
JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
WHERE date_commande BETWEEN to_date('01/01/2011', 'DD/MM/YYYY')
AND to_date('31/12/2011', 'DD/MM/YYYY')
GROUP BY c.numero_commande
ORDER BY max(date_reception - date_commande) DESC
LIMIT 10
;

14. Un autre bug applicatif est détecté. Certaines commandes n’ont pas de lignes de
commandes. Écrivez une requête pour les retrouver.

Pour réaliser cette requête, il faut effectuer une jointure spéciale, nommée « Anti-jointure
». Il y a plusieurs façons d’écrire ce type de jointure. Les différentes méthodes sont don-
nées de la moins efficace à la plus efficace.

La version la moins performante est la suivante, avec NOT IN :


SELECT c.numero_commande
FROM commandes
WHERE numero_commande NOT IN (
SELECT numero_commande
FROM lignes_commandes
)
;

Il n’y a aucune corrélation entre la requête principale et la sous-requête. PostgreSQL doit


donc vérifier pour chaque ligne de commandes que numero_commande n’est pas présent
dans l’ensemble retourné par la sous-requête. Il est préférable d’éviter cette syntaxe.

Autre écriture possible, avec LEFT JOIN :


SELECT c.numero_commande
FROM commandes c
LEFT JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
/* c'est le filtre suivant qui permet de ne conserver que les lignes de la

238
5. PLUS LOIN AVEC SQL

table commandes qui n'ont PAS de correspondance avec la table


numero_commandes */
WHERE lc.numero_commande IS NULL
;

Enfin, l’écriture généralement préférée, tant pour la lisibilité que pour les performances,
avec NOT EXISTS :
SELECT c.numero_commande
FROM commandes c
WHERE NOT EXISTS (
SELECT 1
FROM lignes_commandes lc
WHERE lc.numero_commande = c.numero_commande
)
;

15. Écrivez une requête pour supprimer ces commandes. Vérifiez le travail avant de
valider.

Afin de se protéger d’une erreur de manipulation, on ouvre une transaction :


BEGIN;

La requête permettant de supprimer ces commandes est dérivée de la version NOT


EXISTS de la requête ayant permis de trouver le problème :
DELETE
FROM commandes c
WHERE NOT EXISTS (
SELECT 1
FROM lignes_commandes lc
WHERE lc.numero_commande = c.numero_commande
)
-- on peut renvoyer directement les numeros de commande qui ont été supprimés :
-- RETURNING numero_commande
;

Pour vérifier que le problème est corrigé :


SELECT count(*)
FROM commandes c
WHERE NOT EXISTS (
SELECT 1
FROM lignes_commandes lc
WHERE lc.numero_commande = c.numero_commande
)
;

Si la requête ci-dessus remonte 0, alors la transaction peut être validée :


239
https://dalibo.com/formations
SQL pour PostgreSQL

COMMIT;

16. Écrivez une requête pour déterminer les 20 pièces qui ont eu le plus gros volume
de commande.
SELECT p.nom,
sum(quantite)
FROM pieces p
JOIN lignes_commandes lc ON p.piece_id = lc.piece_id
GROUP BY p.nom
ORDER BY sum(quantite) DESC
LIMIT 20
;

17. Affichez les fournisseurs des 20 pièces qui ont été le plus commandées sur l’année
2011.
SELECT co.nom, max_p.piece_id, total_pieces
FROM (
/* cette sous-requête est sensiblement la même que celle de l'exercice
précédent, sauf que l'on remonte cette fois l'id de la piece plutôt
que son nom pour pouvoir faire la jointure avec pieces_fournisseurs, et
que l'on ajoute une jointure avec commandes pour pouvoir filtrer sur
l'année 2011 */
SELECT
p.piece_id,
sum(quantite) AS total_pieces
FROM pieces p
JOIN lignes_commandes lc ON p.piece_id = lc.piece_id
JOIN commandes c ON c.numero_commande = lc.numero_commande
WHERE date_commande BETWEEN to_date('01/01/2011', 'DD/MM/YYYY')
AND to_date('31/12/2011', 'DD/MM/YYYY')
GROUP BY p.piece_id
ORDER BY sum(quantite) DESC
LIMIT 20
) max_p
/* il faut passer par la table de liens pieces_fournisseurs pour récupérer
la liste des fournisseurs d'une piece */
JOIN pieces_fournisseurs pf ON max_p.piece_id = pf.piece_id
JOIN fournisseurs f ON f.fournisseur_id = pf.fournisseur_id
-- la jointure avec la table contact permet d'afficher le nom du fournisseur
JOIN contacts co ON f.contact_id = co.contact_id
;

18. Affichez le pays qui a connu, en nombre, le plus de commandes sur l’année 2011.
SELECT nom_pays,
count(c.numero_commande)
FROM commandes c

240
5. PLUS LOIN AVEC SQL

JOIN clients cl ON (c.client_id = cl.client_id)


JOIN contacts co ON (cl.contact_id = co.contact_id)
JOIN pays p ON (co.code_pays = p.code_pays)
WHERE date_commande BETWEEN to_date('01/01/2011', 'DD/MM/YYYY')
AND to_date('31/12/2011', 'DD/MM/YYYY')
GROUP BY p.nom_pays
ORDER BY count(c.numero_commande) DESC
LIMIT 1;

19. Affichez pour les commandes passées en 2011, la liste des régions et la marge brute
d’exploitation réalisée par régions, triés dans l’ordre décroissant.
SELECT
nom_region,
round(sum(quantite * prix - remise) - sum(quantite * cout_piece), 2)
AS marge_brute
FROM
commandes c
JOIN lignes_commandes lc ON lc.numero_commande = c.numero_commande
/* il faut passer par la table de liens pieces_fournisseurs pour récupérer
la liste des fournisseurs d'une piece - attention, la condition de
jointure entre lignes_commandes et pieces_fournisseurs porte sur deux
colonnes ! */
JOIN pieces_fournisseurs pf ON lc.piece_id = pf.piece_id
AND lc.fournisseur_id = pf.fournisseur_id
JOIN pieces p ON p.piece_id = pf.piece_id
JOIN fournisseurs f ON f.fournisseur_id = pf.fournisseur_id
JOIN clients cl ON c.client_id = cl.client_id
JOIN contacts co ON cl.contact_id = co.contact_id
JOIN pays pa ON co.code_pays = pa.code_pays
JOIN regions r ON r.region_id = pa.region_id
WHERE date_commande BETWEEN to_date('01/01/2011', 'DD/MM/YYYY')
AND to_date('31/12/2011', 'DD/MM/YYYY')
GROUP BY nom_region
ORDER BY sum(quantite * prix - remise) - sum(quantite * cout_piece) DESC
;

20. Affichez le nom, le numéro de téléphone et le pays des fournisseurs qui ont un
commentaire contenant le mot clé Complaints :
SELECT
nom,
telephone,
nom_pays
FROM
fournisseurs f
JOIN contacts c ON f.contact_id = c.contact_id
JOIN pays p ON c.code_pays = p.code_pays
241
https://dalibo.com/formations
SQL pour PostgreSQL

WHERE f.commentaire LIKE '%Complaints%'


;

21. Déterminez le top 10 des fournisseurs ayant eu le plus long délai de livraison, entre
la date de commande et la date de réception, pour l’année 2011 uniquement.
SELECT
f.fournisseur_id,
co.nom,
max(date_reception - date_commande)
FROM
lignes_commandes lc
JOIN commandes c ON c.numero_commande = lc.numero_commande
JOIN pieces_fournisseurs pf ON lc.piece_id = pf.piece_id
AND lc.fournisseur_id = pf.fournisseur_id
JOIN fournisseurs f ON pf.fournisseur_id = f.fournisseur_id
JOIN contacts co ON f.contact_id = co.contact_id
WHERE date_commande BETWEEN to_date('01/01/2011', 'DD/MM/YYYY')
AND to_date('31/12/2011', 'DD/MM/YYYY')
GROUP BY f.fournisseur_id, co.nom
ORDER BY max(date_reception - date_commande) DESC
LIMIT 10
;

242
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6 SQL AVANCÉ POUR LE TRANSACTIONNEL

6.0.1 PRÉAMBULE

• SQL et PostgreSQL proposent de nombreuses possibilités avancées


– normes SQL:99, 2003, 2008 et 2011
– parfois, extensions propres à PostgreSQL

La norme SQL a continué d’évoluer et a bénéficié d’un grand nombre d’améliorations.


Beaucoup de requêtes qu’il était difficile d’exprimer avec les premières incarnations de la
norme sont maintenant faciles à réaliser avec les dernières évolutions.

Ce module a pour objectif de voir les fonctionnalités pouvant être utiles pour développer
une application transactionnelle.

6.0.2 MENU

• LIMIT/OFFSET
• Jointures LATERAL
• UPSERT : INSERT ou UPDATE
• Common Table Expressions
• Serializable Snapshot Isolation

6.0.3 OBJECTIFS

• Aller au-delà de SQL:92


• Concevoir des requêtes simples pour résoudre des problèmes complexes

243
https://dalibo.com/formations
SQL pour PostgreSQL

6.1 LIMIT

• Clause LIMIT
• ou syntaxe en norme SQL : FETCH FIRST xx ROWS
• Utilisation :
– limite le nombre de lignes du résultat

La clause LIMIT, ou sa déclinaison normalisée par le comité ISO FETCH FIRST xx ROWS,
permet de limiter le nombre de lignes résultant d’une requête SQL. La syntaxe normalisée
vient de DB2 d’IBM et va être amenée à apparaître sur la plupart des bases de données.
La syntaxe LIMIT reste néanmoins disponible sur de nombreux SGBD et est plus concise.

6.1.1 LIMIT : EXEMPLE

SELECT *
FROM employes
LIMIT 2;

matricule | nom | service | salaire


-----------+----------+----------+----------
00000001 | Dupuis | | 10000.00
00000004 | Fantasio | Courrier | 4500.00
(2 lignes)

L’exemple ci-dessous s’appuie sur le jeu d’essai suivant :

SELECT *
FROM employes ;
matricule | nom | service | salaire
-----------+----------+-------------+----------
00000001 | Dupuis | | 10000.00
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
00000040 | Lebrac | Publication | 3000.00
(5 lignes)

Il faut faire attention au fait que ces fonctions ne permettent pas d’obtenir des résultats
stables si les données ne sont pas triées explicitement. En effet, le standard SQL ne
garantit en aucune façon l’ordre des résultats à moins d’employer la clause ORDER BY, et
que l’ensemble des champs sur lequel on trie soit unique et non null.

Si une ligne était modifiée, changeant sa position physique dans la table, le résultat de la

244
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

requête ne serait pas le même. Par exemple, en réalisant une mise à jour fictive de la ligne
correspondant au matricule 00000001 :
UPDATE employes
SET nom = nom
WHERE matricule = '00000001';

L’ordre du résultat n’est pas garanti :


SELECT *
FROM employes
LIMIT 2;
matricule | nom | service | salaire
-----------+----------+-------------+---------
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
(2 lignes)

L’application d’un critère de tri explicite permet d’obtenir la sortie souhaitée :


SELECT *
FROM employes
ORDER BY matricule
LIMIT 2;
matricule | nom | service | salaire
-----------+----------+----------+----------
00000001 | Dupuis | | 10000.00
00000004 | Fantasio | Courrier | 4500.00

6.1.2 OFFSET

• Clause OFFSET
– à utiliser avec LIMIT
• Utilité :
– pagination de résultat
– sauter les n premières lignes avant d’afficher le résultat

Ainsi, en reprenant le jeu d’essai utilisé précédemment :


SELECT * FROM employes ;
matricule | nom | service | salaire
-----------+----------+-------------+----------
00000001 | Dupuis | | 10000.00
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
245
https://dalibo.com/formations
SQL pour PostgreSQL

00000040 | Lebrac | Publication | 3000.00


(5 lignes)

6.1.3 OFFSET : EXEMPLE (1/2)

• Sans offset :
SELECT *
FROM employes
LIMIT 2
ORDER BY matricule;

matricule | nom | service | salaire


-----------+----------+----------+----------
00000001 | Dupuis | | 10000.00
00000004 | Fantasio | Courrier | 4500.00

6.1.4 OFFSET : EXEMPLE (2/2)

• En sautant les deux premières lignes :


SELECT *
FROM employes
ORDER BY matricule
LIMIT 2
OFFSET 2;

matricule | nom | service | salaire


-----------+----------+-------------+---------
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00

246
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6.1.5 OFFSET : PROBLÈMES

• OFFSET est problématique


– beaucoup de données lues
– temps de réponse dégradés
• Alternative possible
– utilisation d’un index sur le critère de tri
– critère de filtrage sur la page précédente
• Article sur le sujeta

Cependant, sur un jeu de données conséquent et une pagination importante, ce principe


de fonctionnement peut devenir contre-performant. En effet, la base de données devra
lire malgré tout les enregistrements qui n’apparaîtront pas dans le résultat de la requête,
simplement dans le but de les compter.

Soit la table posts suivante (téléchargeable sur https://dali.bo/tp_posts, à laquelle on


ajoute un index sur (id_article_id, id_post)) :
\d posts
Table « public.posts »
Colonne | Type | Collationnement | NULL-able | Par défaut
------------+--------------------------+-----------------+-----------+------------
id_article | integer | | |
id_post | integer | | |
ts | timestamp with time zone | | |
message | text | | |
Index :
"posts_id_article_id_post" btree (id_article, id_post)
"posts_ts_idx" btree (ts)

Si l’on souhaite récupérer les 10 premiers enregistrements :


SELECT *
FROM posts
WHERE id_article =12
ORDER BY id_post
LIMIT 10 ;

on obtient le plan d’exécution75 suivant :

QUERY PLAN
------------------------------------------------------------------------------
Limit (cost=0.43..18.26 rows=10 width=115)
(actual time=0.043..0.053 rows=10 loops=1)
a
https://use-the-index-luke.com/fr/no-offset
75
https://explain.dalibo.com/plan/xEs

247
https://dalibo.com/formations
SQL pour PostgreSQL

-> Index Scan using posts_id_article_id_post on posts


(cost=0.43..1745.88 rows=979 width=115)
(actual time=0.042..0.051 rows=10 loops=1)
Index Cond: (id_article = 12)
Planning Time: 0.204 ms
Execution Time: 0.066 ms

La requête est rapide car elle profite d’un index bien trié et elle ne lit que peu de données,
ce qui est bien.

En revanche, si l’on saute un nombre conséquent d’enregistrements grâce à la clause


OFFSET, la situation devient problématique :

SELECT *
FROM posts
WHERE id_article = 12
ORDER BY id_post
LIMIT 10
OFFSET 900 ;

Le plan76 n’est plus le même :

Limit (cost=1605.04..1622.86 rows=10 width=115)


(actual time=0.216..0.221 rows=10 loops=1)
-> Index Scan using posts_id_article_id_post on posts
(cost=0.43..1745.88 rows=979 width=115)
(actual time=0.018..0.194 rows=910 loops=1)
Index Cond: (id_article = 12)
Planning Time: 0.062 ms
Execution Time: 0.243 ms

Pour répondre à la requête, PostgreSQL choisit la lecture de l’ensemble des résultats, puis
leur tri, pour enfin appliquer la limite. En effet, LIMIT et OFFSET ne peuvent s’opèrer que
sur le résultat trié : il faut lire les 910 posts avant de pouvoir choisir les 10 derniers.

Le problème de ce plan est que, plus le jeu de données sera important, plus les temps de
réponse seront importants. Ils seront encore plus importants si le tri n’est pas utilisable
dans un index, ou si l’on déclenche un tri sur disque. Il faut donc trouver une solution
pour les minimiser.

Les problèmes de l’utilisation de la clause OFFSET sont parfaitement expliqués dans cet
article77 .
76
https://explain.dalibo.com/plan/V05
77
https://use-the-index-luke.com/fr/no-offset

248
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

Dans notre cas, le principe est d’abord de créer un index qui contient le critère ainsi que le
champ qui fixe la pagination (l’index existant convient). Puis on mémorise à quel post_id
la page précédente s’est arrêtée, pour le donner comme critère de filtrage (ici 12900). Il
suffit donc de récupérer les 10 articles pour lesquels id_article = 12 et id_post >
12900 :
EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE id_article = 12
AND id_post> 12900
ORDER BY id_post
LIMIT 10 ;

QUERY PLAN
----------------------------------------------------------------
Limit (cost=0.43..18.29 rows=10 width=115)
(actual time=0.018..0.024 rows=10 loops=1)
-> Index Scan using posts_id_article_id_post on posts
(cost=0.43..1743.02 rows=976 width=115)
(actual time=0.016..0.020 rows=10 loops=1)
Index Cond: ((id_article = 12) AND (id_post > 12900))
Planning Time: 0.111 ms
Execution Time: 0.039 ms

6.2 RETURNING

• Clause RETURNING
• Utilité :
– récupérer les enregistrements modifiés
– avec INSERT
– avec UPDATE
– avec DELETE

La clause RETURNING permet de récupérer les valeurs modifiées par un ordre DML. Ainsi,
la clause RETURNING associée à l’ordre INSERT permet d’obtenir une ou plusieurs colonnes
des lignes insérées.

249
https://dalibo.com/formations
SQL pour PostgreSQL

6.2.1 RETURNING : EXEMPLE

CREATE TABLE test_returning (id serial primary key, val integer);

INSERT INTO test_returning (val)


VALUES (10)
RETURNING id, val;

id | val
----+-----
1 | 10
(1 ligne)

Cela permet par exemple de récupérer la valeur de colonnes portant une valeur par défaut,
comme la valeur affectée par une séquence, comme sur l’exemple ci-dessus.

La clause RETURNING permet également de récupérer les valeurs des colonnes mises à
jour :
UPDATE test_returning
SET val = val + 10
WHERE id = 1
RETURNING id, val;
id | val
----+-----
1 | 20
(1 ligne)

Associée à l’ordre DELETE, il est possible d’obtenir les lignes supprimées :


DELETE FROM test_returning
WHERE val < 30
RETURNING id, val;
id | val
----+-----
1 | 20
(1 ligne)

250
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6.3 UPSERT

• INSERT ou UPDATE ?
– INSERT ... ON CONFLICT DO { NOTHING | UPDATE }
– À partir de la version 9.5
• Utilité :
– mettre à jour en cas de conflit sur un INSERT
– ne rien faire en cas de conflit sur un INSERT

L’implémentation de l’UPSERT peut poser des questions sur la concurrence d’accès.


L’implémentation de PostgreSQL de ON CONFLICT DO UPDATE est une opération atom-
ique, c’est-à-dire que PostgreSQL garantit qu’il n’y aura pas de conditions d’exécution qui
pourront amener à des erreurs. L’utilisation d’une contrainte d’unicité n’est pas étrangère
à cela, elle permet en effet de pouvoir vérifier que la ligne n’existe pas, et si elle existe
déjà, de verrouiller la ligne à mettre à jour de façon atomique.

En comparaison, plusieurs approches naïves présentent des problèmes de concurrences


d’accès. Les différentes approches sont décrites dans cet article de depesz78 . Elle
présente toutes des problèmes de race conditions qui peuvent entraîner des erreurs. Une
autre possibilité aurait été d’utiliser une CTE en écriture, mais elle présente également
les problèmes de concurrence d’accès décrits dans l’article.

Sur des traitements d’intégration de données, il s’agit d’un comportement qui n’est pas
toujours souhaitable. La norme SQL propose l’ordre MERGE pour palier à des problèmes de
ce type, mais il est peu probable de le voir rapidement implémenté dans PostgreSQL79 .
L’ordre INSERT s’est toutefois vu étendu avec PostgreSQL 9.5 pour gérer les conflits à
l’insertion.

Les exemples suivants s’appuient sur le jeu de données suivant :

\d employes
Table "public.employes"
Column | Type | Modifiers
-----------+--------------+-----------
matricule | character(8) | not null
nom | text | not null
service | text |
salaire | numeric(7,2) |
78
https://www.depesz.com/2012/06/10/why-is-upsert-so-complicated/
79
La solution actuelle semble techniquement meilleure et la solution actuelle a donc été choisie. Le wiki du projet
PostgreSQL montre que l’ordre MERGE a été étudié et qu’un certain nombre d’aspects cruciaux n’ont pas été spécifiés,
amenant le projet PostgreSQL à utiliser sa propre version. Voir la documentation : https://wiki.postgresql.org/wiki/
UPSERT#MERGE_disadvantages.

251
https://dalibo.com/formations
SQL pour PostgreSQL

Indexes:
"employes_pkey" PRIMARY KEY, btree (matricule)

SELECT * FROM employes ;


matricule | nom | service | salaire
-----------+----------+-------------+----------
00000001 | Dupuis | Direction | 10000.00
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
00000040 | Lebrac | Publication | 3000.00
(5 lignes)

6.3.1 UPSERT : PROBLÈME À RÉSOUDRE

• Insérer une ligne déjà existante provoque une erreur :


INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000001', 'Marsupilami', 'Direction', 50000.00);
ERROR: duplicate key value violates unique constraint
"employes_pkey"
DETAIL: Key (matricule)=(00000001) already exists.

Si l’on souhaite insérer une ligne contenant un matricule déjà existant, une erreur de clé
dupliquée est levée et toute la transaction est annulée.

6.3.2 ON CONFLICT DO NOTHING

• la clause ON CONFLICT DO NOTHING évite d’insérer une ligne existante :


=# INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000001', 'Marsupilami', 'Direction', 50000.00)
ON CONFLICT DO NOTHING;
INSERT 0 0

Les données n’ont pas été modifiées :


=# SELECT * FROM employes ;
matricule | nom | service | salaire
-----------+----------+-------------+----------
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00

252
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

00000020 | Lagaffe | Courrier | 3000.00


00000040 | Lebrac | Publication | 3000.00
00000001 | Dupuis | Direction | 10000.00
(5 rows)

La transaction est toujours valide.

6.3.3 ON CONFLICT DO NOTHING : SYNTAXE

INSERT ....
ON CONFLICT
DO NOTHING;

Il suffit d’indiquer à PostgreSQL de ne rien faire en cas de conflit sur une valeur dupliquée
avec la clause ON CONFLICT DO NOTHING placée à la fin de l’ordre INSERT qui peut poser
problème.

Dans ce cas, si une rupture d’unicité est détectée, alors PostgreSQL ignorera l’erreur, si-
lencieusement. En revanche, si une erreur apparaît sur une autre contrainte, l’erreur sera
levée.

En prenant l’exemple suivant :

CREATE TABLE test_upsert (


i serial PRIMARY KEY,
v text UNIQUE,
x integer CHECK (x > 0)
);

INSERT INTO test_upsert (v, x) VALUES ('x', 1);

L’insertion d’une valeur dupliquée provoque bien une erreur d’unicité :

INSERT INTO test_upsert (v, x) VALUES ('x', 1);


ERROR: duplicate key value violates unique constraint "test_upsert_v_key"

L’erreur d’unicité est bien ignorée si la ligne existe déjà, le résultat est INSERT 0 0 qui
indique qu’aucune ligne n’a été insérée :

INSERT INTO test_upsert (v, x)


VALUES ('x', 1)
ON CONFLICT DO NOTHING;
INSERT 0 0

L’insertion est aussi ignorée si l’on tente d’insérer des lignes rompant la contrainte
d’unicité mais ne comportant pas les mêmes valeurs pour d’autres colonnes :
253
https://dalibo.com/formations
SQL pour PostgreSQL

INSERT INTO test_upsert (v, x)


VALUES ('x', 4)
ON CONFLICT DO NOTHING;
INSERT 0 0

Si l’on insère une valeur interdite par la contrainte CHECK, une erreur est bien levée :

INSERT INTO test_upsert (v, x)


VALUES ('x', 0)
ON CONFLICT DO NOTHING;
ERROR: new row for relation "test_upsert" violates check constraint
"test_upsert_x_check"
DETAIL: Failing row contains (4, x, 0).

6.3.4 ON CONFLICT DO UPDATE

INSERT INTO employes (matricule, nom, service, salaire)


VALUES ('00000001', 'M. Pirate', 'Direction', 0.00)
ON CONFLICT (matricule)
DO UPDATE SET salaire = employes.salaire,
nom = excluded.nom
RETURNING *;

matricule | nom | service | salaire


-----------+-----------+-------------+----------
00000001 | M. Pirate | Direction | 50000.00

La clause ON CONFLICT permet de déterminer une colonne sur laquelle le conflit peut
arriver. Cette colonne ou ces colonnes doivent porter une contrainte d’unicité ou une
contrainte d’exclusion, c’est à dire une contrainte portée par un index. La clause DO
UPDATE associée fait référence aux valeurs rejetées par le conflit à l’aide de la pseudo-
table excluded. Les valeurs courantes sont accessibles en préfixant les colonnes avec le
nom de la table. L’exemple montre cela.

Avec la requête de l’exemple, on voit que le salaire du directeur n’a pas été modifié, mais
son nom l’a été :

SELECT * FROM employes ;


matricule | nom | service | salaire
-----------+-----------+-------------+----------
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
00000040 | Lebrac | Publication | 3000.00

254
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

00000001 | M. Pirate | Direction | 10000.00


(5 rows)

La clause ON CONFLICT permet également de définir une contrainte d’intégrité sur laque-
lle on réagit en cas de conflit :

INSERT INTO employes (matricule, nom, service, salaire)


VALUES ('00000001', 'Marsupilami', 'Direction', 50000.00)
ON CONFLICT ON CONSTRAINT employes_pkey
DO UPDATE SET salaire = excluded.salaire;

On remarque que seul le salaire du directeur a changé :

SELECT * FROM employes ;


matricule | nom | service | salaire
-----------+----------+-------------+----------
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
00000040 | Lebrac | Publication | 3000.00
00000001 | M. Pirate | Direction | 50000.00
(5 rows)

6.3.5 ON CONFLICT DO UPDATE

• Avec plusieurs lignes insérées :


INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000002', 'Moizelle Jeanne', 'Publication', 3000.00),
('00000040', 'Lebrac', 'Publication', 3100.00)
ON CONFLICT (matricule)
DO UPDATE SET salaire = employes.salaire,
nom = excluded.nom
RETURNING *;

matricule | nom | service | salaire


-----------+-----------------+-------------+----------
00000002 | Moizelle Jeanne | Publication | 3000.00
00000040 | Lebrac | Publication | 3000.00

Bien sûr, on peut insérer plusieurs lignes, INSERT ON CONFLICT réagira uniquement sur
les doublons :

La nouvelle employée, Moizelle Jeanne a été intégrée dans la tables des employés, et Lebrac
a été traité comme un doublon, en appliquant la règle de mise à jour vue plus haut : seul
le nom est mis à jour et le salaire est inchangé.
255
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT * FROM employes ;


matricule | nom | service | salaire
-----------+-----------------+-------------+----------
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
00000001 | M. Pirate | Direction | 50000.00
00000002 | Moizelle Jeanne | Publication | 3000.00
00000040 | Lebrac | Publication | 3000.00
(6 rows)

À noter que la clause SET salaire = employes.salaire est inutile, c’est ce que fait
PostgreSQL implicitement.

6.3.6 ON CONFLICT DO UPDATE : SYNTAXE

• colonne(s) portant(s) une contrainte d’unicité


• pseudo-table excluded
INSERT ....
ON CONFLICT (<colonne clé>)
DO UPDATE
SET colonne_a_modifier = excluded.colonne,
autre_colonne_a_modifier = excluded.autre_colonne,
...;

Si l’on choisit de réaliser une mise à jour plutôt que de générer une erreur, on utilisera
la clause ON CONFLICT DO UPDATE. Il faudra dans ce cas préciser la ou les colonnes qui
portent une contrainte d’unicité. Cette contrainte d’unicité permettra de détecter la du-
plication de valeur, PostgreSQL pourra alors appliquer la règle de mise à jour édictée.

La règle de mise à jour permet de définir très finement les colonnes à mettre à jour et les
colonnes à ne pas mettre à jour. Dans ce contexte, la pseudo-table excluded représente
l’ensemble rejeté par l’INSERT. Il faudra explicitement indiquer les colonnes dont la valeur
sera mise à jour à partir des valeurs que l’on tente d’insérer, reprise de la pseudo-table
excluded :
ON CONFLICT (...)
DO UPDATE
SET colonne = excluded.colonne,
autre_colonne = excluded.autre_colonne,
...

En alternative, il est possible d’indiquer un nom de contrainte plutôt que le nom d’une
colonne portant une contrainte d’unicité :

256
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

INSERT ....
ON CONFLICT ON CONSTRAINT nom_contrainte
DO UPDATE
SET colonne_a_modifier = excluded.colonne,
autre_colonne_a_modifier = excluded.autre_colonne,
...;

De plus amples informations quant à la syntaxe sont disponibles dans la documentation80


.

6.4 LATERAL

• Jointures LATERAL
– SQL:99
– PostgreSQL 9.3
– équivalent d’une boucle foreach
• Utilisations
– top-N à partir de plusieurs tables
– jointure avec une fonction retournant un ensemble

LATERAL apparaît dans la révision de la norme SQL de 1999. Elle permet d’appliquer une
requête ou une fonction sur le résultat d’une table.

6.4.1 LATERAL : AVEC UNE SOUS-REQUÊTE

• Jointure LATERAL
– équivalent de foreach
• Utilité :
– Top-N à partir de plusieurs tables
– exemple : afficher les 5 derniers messages des 5 derniers sujets actifs d’un forum

La clause LATERAL existe dans la norme SQL depuis plusieurs années. L’implémentation
de cette clause dans la plupart des SGBD reste cependant relativement récente.

Elle permet d’utiliser les données de la requête principale dans une sous-requête. La
sous-requête sera appliquée à chaque enregistrement retourné par la requête principale.

80
https://www.postgresql.org/docs/current/static/sql-insert.html

257
https://dalibo.com/formations
SQL pour PostgreSQL

6.4.2 LATERAL : EXEMPLE

SELECT titre,
top_5_messages.date_publication,
top_5_messages.extrait
FROM sujets,
LATERAL(SELECT date_publication,
substr(message, 0, 100) AS extrait
FROM messages
WHERE sujets.sujet_id = messages.sujet_id
ORDER BY date_publication DESC
LIMIT 5) top_5_messages
ORDER BY sujets.date_modification DESC,
top_5_messages.date_publication DESC
LIMIT 25;

L’exemple ci-dessus montre comment afficher les 5 derniers messages postés sur les 5
derniers sujets actifs d’un forum avec la clause LATERAL.

Une autre forme d’écriture emploie le mot clé JOIN, inutile dans cet exemple. Il peut
avoir son intérêt si l’on utilise une jointure externe (LEFT JOIN par exemple si un sujet
n’impliquait pas forcément la présence d’un message) :

SELECT titre, top_5_messages.date_publication, top_5_messages.extrait


FROM sujets
JOIN LATERAL(SELECT date_publication, substr(message, 0, 100) AS extrait
FROM messages
WHERE sujets.sujet_id = messages.sujet_id
ORDER BY date_publication DESC
LIMIT 5) top_5_messages
ON (true) -- condition de jointure toujours vraie
ORDER BY sujets.date_modification DESC, top_5_messages.date_publication DESC
LIMIT 25;

Il aurait été possible de réaliser cette requête par d’autres moyens, mais LATERAL permet
d’obtenir la requête la plus performante. Une autre approche quasiment aussi perfor-
mante aurait été de faire appel à une fonction retournant les 5 enregistrements souhaités.

À noter qu’une colonne date_modification a été ajouté à la table sujets afin de déter-
miner rapidement les derniers sujets modifiés. Sans cela, il faudrait parcourir l’ensemble
des sujets, récupérer la date de publication des derniers messages avec une jointure
LATERAL et récupérer les 5 derniers sujets actifs. Cela nécessite de lire beaucoup de
données. Un trigger positionné sur la table messages permettra d’entretenir la colonne
date_modification sur la table sujets sans difficulté. Il s’agit donc ici d’une entorse
aux règles de modélisation en vue d’optimiser les traitements.

258
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

Un index sur les colonnes sujet_id et date_publication permettra de minimiser les


accès pour cette requête :
CREATE INDEX ON messages (sujet_id, date_publication DESC);

6.4.3 LATERAL : PRINCIPE

Si nous n’avions pas la clause LATERAL, nous pourrions être tentés d’écrire la requête
suivante :
SELECT titre, top_5_messages.date_publication, top_5_messages.extrait
FROM sujets
JOIN (SELECT date_publication, substr(message, 0, 100) AS extrait
FROM messages
WHERE sujets.sujet_id = messages.sujet_id
ORDER BY date_message DESC
LIMIT 5) top_5_messages
ORDER BY sujets.date_modification DESC
LIMIT 25;

Cependant, la norme SQL interdit une telle construction, il n’est pas possible de référencer
la table principale dans une sous-requête. Mais avec la clause LATERAL, la sous-requête
peut faire appel à la table principale.

259
https://dalibo.com/formations
SQL pour PostgreSQL

6.4.4 LATERAL : AVEC UNE FONCTION

• Utilisation avec une fonction retournant un ensemble


– clause LATERAL optionnelle
• Utilité :
– extraire les données d’un tableau ou d’une structure JSON sous la forme tab-
ulaire
– utiliser une fonction métier qui retourne un ensemble X selon un ensemble
Y fourni

L’exemple ci-dessous montre qu’il est possible d’utiliser une fonction retournant un en-
semble (SRF pour Set Returning Functions).

6.4.5 LATERAL : EXEMPLE AVEC UNE FONCTION

SELECT titre,
top_5_messages.date_publication,
top_5_messages.extrait
FROM sujets,
get_top_5_messages(sujet_id) AS top_5_messages
ORDER BY sujets.date_modification DESC
LIMIT 25;

La fonction get_top_5_messages est la suivante :


CREATE OR REPLACE FUNCTION get_top_5_messages (p_sujet_id integer)
RETURNS TABLE (date_publication timestamp, extrait text)
AS $PROC$
BEGIN
RETURN QUERY SELECT date_publication, substr(message, 0, 100) AS extrait
FROM messages
WHERE messages.sujet_id = p_sujet_id
ORDER BY date_publication DESC
LIMIT 5;
END;
$PROC$ LANGUAGE plpgsql;

La clause LATERAL n’est pas obligatoire, mais elle s’utiliserait ainsi :


SELECT titre, top_5_messages.date_publication, top_5_messages.extrait
FROM sujets, LATERAL get_top_5_messages(sujet_id) AS top_5_messages
ORDER BY sujets.date_modification DESC LIMIT 25;

260
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6.5 COMMON TABLE EXPRESSIONS

• Common Table Expressions


– clauses WITH et WITH RECURSIVE
• Utilité :
– factoriser des sous-requêtes

6.5.1 CTE ET SELECT

• Utilité
– factoriser des sous-requêtes
– améliorer la lisibilité d’une requête

Les CTE permettent de factoriser la définition d’une sous-requête qui pourrait être ap-
pelée plusieurs fois.

Une CTE est exprimée avec la clause WITH. Cette clause permet de définir des vues
éphémères qui seront utilisées les unes après les autres et au final utilisées dans la re-
quête principale.

Avant la version 12, une CTE était forcément matérialisée. À partir de la version 12, ce
n’est plus le cas. Le seul moyen de s’en assurer revient à ajouter la clause MATERIALIZED.

6.5.2 CTE ET SELECT : EXEMPLE

WITH resultat AS (
/* requête complexe */
)
SELECT *
FROM resultat
WHERE nb < 5;

On utilise principalement une CTE pour factoriser la définition d’une sous-requête com-
mune, comme dans l’exemple ci-dessus.

Un autre exemple un peu plus complexe :


WITH resume_commandes AS (
SELECT c.numero_commande, c.client_id, quantite*prix_unitaire AS montant
FROM commandes c
JOIN lignes_commandes l
261
https://dalibo.com/formations
SQL pour PostgreSQL

ON (c.numero_commande = l.numero_commande)
WHERE date_commande BETWEEN '2014-01-01' AND '2014-12-31'
)
SELECT type_client, NULL AS pays, SUM(montant) AS montant_total_commande
FROM resume_commandes
JOIN clients
ON (resume_commandes.client_id = clients.client_id)
GROUP BY type_client
UNION ALL
SELECT NULL, code_pays AS pays, SUM(montant)
FROM resume_commandes r
JOIN clients cl
ON (r.client_id = cl.client_id)
JOIN contacts co
ON (cl.contact_id = co.contact_id)
GROUP BY code_pays;

Le plan d’exécution de la requête montre que la vue resume_commandes est exécutée une
seule fois et son résultat est utilisé par les deux opérations de regroupements définies
dans la requête principale :

QUERY PLAN
-------------------------------------------------------------------------------
Append (cost=244618.50..323855.66 rows=12 width=67)
CTE resume_commandes
-> Hash Join (cost=31886.90..174241.18 rows=1216034 width=26)
Hash Cond: (l.numero_commande = c.numero_commande)
-> Seq Scan on lignes_commandes l
(cost=0.00..73621.47 rows=3141947 width=18)
-> Hash (cost=25159.00..25159.00 rows=387032 width=16)
-> Seq Scan on commandes c
(cost=0.00..25159.00 rows=387032 width=16)
Filter: ((date_commande >= '2014-01-01'::date)
AND (date_commande <= '2014-12-31'::date))
-> HashAggregate (cost=70377.32..70377.36 rows=3 width=34)
Group Key: clients.type_client
-> Hash Join (cost=3765.00..64297.15 rows=1216034 width=34)
Hash Cond: (resume_commandes.client_id = clients.client_id)
-> CTE Scan on resume_commandes
(cost=0.00..24320.68 rows=1216034 width=40)
-> Hash (cost=2026.00..2026.00 rows=100000 width=10)
-> Seq Scan on clients
(cost=0.00..2026.00 rows=100000 width=10)
-> HashAggregate (cost=79236.89..79237.00 rows=9 width=35)
Group Key: co.code_pays
-> Hash Join (cost=12624.57..73156.72 rows=1216034 width=35)
Hash Cond: (r.client_id = cl.client_id)

262
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

-> CTE Scan on resume_commandes r


(cost=0.00..24320.68 rows=1216034 width=40)
-> Hash (cost=10885.57..10885.57 rows=100000 width=11)
-> Hash Join
(cost=3765.00..10885.57 rows=100000 width=11)
Hash Cond: (co.contact_id = cl.contact_id)
-> Seq Scan on contacts co
(cost=0.00..4143.05 rows=110005 width=11)
-> Hash (cost=2026.00..2026.00 rows=100000 width=16)
-> Seq Scan on clients cl
(cost=0.00..2026.00 rows=100000 width=16)

Si la requête avait été écrite sans CTE, donc en exprimant deux fois la même sous-requête,
le coût d’exécution aurait été multiplié par deux car il aurait fallu exécuter la sous-requête
deux fois au lieu d’une.

On utilise également les CTE pour améliorer la lisibilité des requêtes complexes, mais cela
peut poser des problèmes d’optimisations, comme cela sera discuté plus bas.

6.5.3 CTE ET SELECT : SYNTAXE

WITH nom_vue1 AS [ [ NOT ] MATERIALIZED ] (


<requête pour générer la vue 1>
)
SELECT *
FROM nom_vue1;

La syntaxe de définition d’une vue est donnée ci-dessus.

On peut néanmoins enchaîner plusieurs vues les unes à la suite des autres :
WITH nom_vue1 AS (
<requête pour générer la vue 1>
), nom_vue2 AS (
<requête pour générer la vue 2, pouvant utiliser la vue 1>
)
<requête principale utilisant vue 1 et/ou vue2>;

263
https://dalibo.com/formations
SQL pour PostgreSQL

6.5.4 CTE ET BARRIÈRE D'OPTIMISATION

• Attention, une CTE est une barrière d’optimisation !


– pas de transformations
– pas de propagation des prédicats
• Sauf à partir de la version 12
– clause MATERIALIZED pour obtenir cette barrière

Il faut néanmoins être vigilant car l’optimiseur n’inclut pas la définition des CTE dans la
requête principale quand il réalise les différentes passes d’optimisations.

Par exemple, sans CTE, si un prédicat appliqué dans la requête principale peut être re-
monté au niveau d’une sous-requête, l’optimiseur de PostgreSQL le réalisera :
EXPLAIN
SELECT MAX(date_embauche)
FROM (SELECT * FROM employes WHERE num_service = 4) e
WHERE e.date_embauche < '2006-01-01';

QUERY PLAN
------------------------------------------------------------------------------
Aggregate (cost=1.21..1.22 rows=1 width=4)
-> Seq Scan on employes (cost=0.00..1.21 rows=2 width=4)
Filter: ((date_embauche < '2006-01-01'::date) AND (num_service = 4))
(3 lignes)

Les deux prédicats num_service = 4 et date_embauche < '2006-01-01' ont été ap-
pliqués en même temps, réduisant ainsi le jeu de données à considérer dès le départ. En
anglais, on parle de predicate push-down.

Une requête équivalente basée sur une CTE ne permet pas d’appliquer le filtre au plus
tôt : ici le filtre inclus dans la CTE est appliqué, pas le second.
EXPLAIN
WITH e AS
(SELECT * FROM employes WHERE num_service = 4)
SELECT MAX(date_embauche)
FROM e
WHERE e.date_embauche < '2006-01-01';

QUERY PLAN
-----------------------------------------------------------------
Aggregate (cost=1.29..1.30 rows=1 width=4)
CTE e
-> Seq Scan on employes (cost=0.00..1.18 rows=5 width=43)
Filter: (num_service = 4)
-> CTE Scan on e (cost=0.00..0.11 rows=2 width=4)
Filter: (date_embauche < '2006-01-01'::date)

264
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

On peut se faire piéger également en voulant calculer trop de choses dans les CTE. Dans
cet autre exemple, on cherche à afficher les 7 commandes d’un client donné, le cumul des
valeurs des lignes par commande étant réalisé dans un CTE :

EXPLAIN ANALYZE
WITH nos_commandes AS
(
SELECT c.numero_commande, c.client_id, SUM(quantite*prix_unitaire) AS montant
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
GROUP BY 1,2
)
SELECT clients.client_id, type_client, nos_commandes.*
FROM nos_commandes
INNER JOIN clients
ON (nos_commandes.client_id = clients.client_id)
WHERE clients.client_id = 6845
;

QUERY PLAN
-----------------------------------------------------------------
Nested Loop (cost=154567.68..177117.90 rows=5000 width=58)
(actual time=7.757..5526.148 rows=7 loops=1)
CTE nos_commandes
-> GroupAggregate (cost=3.51..154567.39 rows=1000000 width=48)
(actual time=0.043..5076.121 rows=1000000 loops=1)
Group Key: c.numero_commande
-> Merge Join (cost=3.51..110641.89 rows=3142550 width=26)
(actual time=0.017..2511.385 rows=3142632 loops=1)
Merge Cond: (c.numero_commande = l.numero_commande)
-> Index Scan using commandes_pkey on commandes c
(cost=0.42..16290.72 rows=1000000 width=16)
(actual time=0.008..317.547 rows=1000000 loops=1)
-> Index Scan using lignes_commandes_pkey on lignes_commandes l
(cost=0.43..52570.08 rows=3142550 width=18)
(actual time=0.006..1030.420 rows=3142632 loops=1)
-> Index Scan using clients_pkey on clients
(cost=0.29..0.51 rows=1 width=10)
(actual time=0.009..0.009 rows=1 loops=1)
Index Cond: (client_id = 6845)
-> CTE Scan on nos_commandes (cost=0.00..22500.00 rows=5000 width=48)
(actual time=7.746..5526.128 rows=7 loops=1)
Filter: (client_id = 6845)
Rows Removed by Filter: 999993

Notez que la construction de la CTE fait un calcul sur l’intégralité des 5000 commandes
265
https://dalibo.com/formations
SQL pour PostgreSQL

et brasse un million de lignes. Puis, une fois connu le client_id, PostgreSQL parcourt
cette CTE pour en récupérer une seule ligne. C’est évidemment extrêmement coûteux et
dure plusieurs secondes.

Alors que sans la CTE, l’optimiseur se permet de faire la jointure avec les tables, donc à
filtrer sur le client demandé, et ne fait la somme des lignes qu’après, en quelques millisec-
ondes.

EXPLAIN ANALYZE
SELECT clients.client_id, type_client, nos_commandes.*
FROM
(
SELECT c.numero_commande, c.client_id, SUM(quantite*prix_unitaire) AS montant
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
GROUP BY 1,2
) AS nos_commandes
INNER JOIN clients
ON (nos_commandes.client_id = clients.client_id)
WHERE clients.client_id = 6845
;

QUERY PLAN
-----------------------------------------------------------------
Nested Loop (cost=12.83..13.40 rows=11 width=58)
(actual time=0.113..0.117 rows=7 loops=1)
-> Index Scan using clients_pkey on clients (cost=0.29..0.51 rows=1 width=10)
(actual time=0.007..0.007 rows=1 loops=1)
Index Cond: (client_id = 6845)
-> HashAggregate (cost=12.54..12.67 rows=11 width=48)
(actual time=0.106..0.108 rows=7 loops=1)
Group Key: c.numero_commande
-> Nested Loop (cost=0.85..12.19 rows=35 width=26)
(actual time=0.028..0.087 rows=23 loops=1)
-> Index Scan using commandes_clients_fkey on commandes c
(cost=0.42..1.82 rows=11 width=16)
(actual time=0.022..0.028 rows=7 loops=1)
Index Cond: (client_id = 6845)
-> Index Scan using lignes_commandes_pkey on lignes_commandes l
(cost=0.43..0.89 rows=5 width=18)
(actual time=0.006..0.007 rows=3 loops=7)
Index Cond: (numero_commande = c.numero_commande)

En plus d’améliorer la lisibilité et d’éviter la duplication de code, le mécanisme des CTE est
aussi un moyen contourner certaines limitations de l’optimiseur de PostgreSQL en vue de
contrôler précisément le plan d’exécution d’une requête.

266
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

Ce principe de fonctionnement a changé avec la version 12 de PostgreSQL. Par défaut, il


n’y a pas de matérialisation mais celle-ci peut être forcée avec l’option MATERIALIZED.

6.5.5 CTE EN ÉCRITURE

• CTE avec des requêtes en modification


– avec INSERT/UPDATE/DELETE
– et éventuellement RETURNING
– obligatoirement exécuté sur PostgreSQL
• Exemple d’utilisation :
– archiver des données
– partitionner les données d’une table
– débugger une requête complexe

6.5.6 CTE EN ÉCRITURE : EXEMPLE

WITH donnees_a_archiver AS (
DELETE FROM donnes_courantes
WHERE date < '2015-01-01'
RETURNING *
)
INSERT INTO donnes_archivees
SELECT * FROM donnees_a_archiver;

La requête d’exemple permet d’archiver des données dans une table dédiée à l’archivage
en utilisant une CTE en écriture. L’emploi de la clause RETURNING permet de récupérer
les lignes purgées.

Le même principe s’applique pour une table que l’on vient de partitionner. Les enreg-
istrements se trouvent initialement dans la table mère, il faut les répartir sur les différentes
partitions. On utilisera une requête reposant sur le même principe que la précédente.
L’ordre INSERT visera la table principale si l’on souhaite utiliser le trigger de partition
pour répartir les données. Il pourra également viser une partition donnée afin d’éviter le
surcoût du trigger de partition.

En plus de ce cas d’usage simple, il est possible d’utiliser cette fonctionnalité pour débug-
ger une requête complexe.
WITH sous-requete1 AS (

267
https://dalibo.com/formations
SQL pour PostgreSQL

),
debug_sous-requete1 AS (
INSERT INTO debug_sousrequete1
SELECT * FROM sous-requete1
), sous-requete2 AS (
SELECT ...
FROM sous-requete1
JOIN ....
WHERE ....
GROUP BY ...
),
debug_sous-requete2 AS (
INSERT INTO debug_sousrequete2
SELECT * FROM sous-requete2
)
SELECT *
FROM sous-requete2;

On peut également envisager une requête CTE en écriture pour émuler une requête
MERGE pour réaliser une intégration de données complexe, là où l’UPSERT ne serait pas
suffisant. Il faut toutefois avoir à l’esprit qu’une telle requête présente des problèmes
de concurrences d’accès, pouvant entraîner des résultats inattendus si elle est employée
alors que d’autres sessions modifient les données. On se contentera d’utiliser une telle
requête dans des traitements batchs.

Il est important de noter que sur PostgreSQL, chaque sous-partie d’une CTE qui exécute
une opération de mise à jour sera exécutée, même si elle n’est pas explicitement appelée.
Par exemple :
WITH del AS (DELETE FROM nom_table),
fonction_en_ecriture AS (SELECT * FROM fonction_en_ecriture())
SELECT 1;

supprimera l’intégralité des données de la table nom_table, mais n’appellera pas la fonc-
tion fonction_en_ecriture(), même si celle-ci effectue des écritures.

268
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6.5.7 CTE RÉCURSIVE

• SQL permet d’exprimer des récursions


– WITH RECURSIVE
• Utilité :
– récupérer une arborescence de menu hiérarchique
– parcourir des graphes (réseaux sociaux, etc.)

Le langage SQL permet de réaliser des récursions avec des CTE récursives. Son princi-
pal intérêt est de pouvoir parcourir des arborescences, comme par exemple des arbres
généalogiques, des arborescences de service ou des entrées de menus hiérarchiques.

Il permet également de réaliser des parcours de graphes, mais les possibilités en SQL sont
plus limitées de ce côté-là. En effet, SQL utilise un algorithme de type Breadth First (par-
cours en largeur) où PostgreSQL produit tout le niveau courant, et approfondit ensuite
la récursion. Ce fonctionnement est à l’opposé d’un algorithme Depth First (parcours en
profondeur) où chaque branche est explorée à fond individuellement avant de passer à
la branche suivante. Ce principe de fonctionnement de l’implémentation dans SQL peut
poser des problèmes sur des recherches de types réseaux sociaux où des bases de don-
nées orientées graphes, tel que Neo4J, seront bien plus efficaces. À noter que l’extension
pgRouting implémente des algorithmes de parcours de graphes plus efficace. Cela per-
met de rester dans PostgreSQL mais nécessite un certain formalisme et il faut avoir con-
science que pgRouting n’est pas l’outil le plus efficace car il génère un graphe en mémoire
à chaque requête à résoudre, qui est perdu après l’appel.

6.5.8 CTE RÉCURSIVE : EXEMPLE (1/2)

WITH RECURSIVE suite AS (


SELECT 1 AS valeur
UNION ALL
SELECT valeur + 1
FROM suite
WHERE valeur < 10
)
SELECT * FROM suite;

Voici le résultat de cette requête :

valeur
--------
1
269
https://dalibo.com/formations
SQL pour PostgreSQL

2
3
4
5
6
7
8
9
10
(10 rows)

L’exécution de cette requête commence avec le SELECT 1 AS valeur (la requête avant
le UNION ALL), d’où la première ligne avec la valeur 1. Puis PostgreSQL exécute le SELECT
valeur+1 FROM suite WHERE valeur < 10 tant que cette requête renvoie des lignes.
À la première exécution, il additionne 1 avec la valeur précédente (1), ce qui fait qu’il
renvoie 2. A la deuxième exécution, il additionne 1 avec la valeur précédente (2), ce qui
fait qu’il renvoie 3. Etc. La récursivité s’arrête quand la requête ne renvoie plus de ligne,
autrement dit quand la colonne vaut 10.

Cet exemple n’a aucun autre intérêt que de présenter la syntaxe permettant de réaliser
une récursion en langage SQL.

6.5.9 CTE RÉCURSIVE : PRINCIPE

• 1ère étape : initialisation de la récursion

270
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6.5.10 CTE RÉCURSIVE : PRINCIPE

• récursion : la requête s’appelle elle-même

271
https://dalibo.com/formations
SQL pour PostgreSQL

6.5.11 CTE RÉCURSIVE : EXEMPLE (2/2)

WITH RECURSIVE parcours_menu AS (


SELECT menu_id, libelle, parent_id,
libelle AS arborescence
FROM entrees_menu
WHERE libelle = 'Terminal'
AND parent_id IS NULL
UNION ALL
SELECT menu.menu_id, menu.libelle, menu.parent_id,
arborescence || '/' || menu.libelle
FROM entrees_menu menu
JOIN parcours_menu parent
ON (menu.parent_id = parent.menu_id)
)
SELECT * FROM parcours_menu;

Cet exemple suivant porte sur le parcours d’une arborescence de menu hiérarchique.

Une table entrees_menu est créée :


CREATE TABLE entrees_menu (menu_id serial primary key, libelle text not null,
parent_id integer);

Elle dispose du contenu suivant :


SELECT * FROM entrees_menu;
menu_id | libelle | parent_id
---------+----------------------------+-----------
1 | Fichier |
2 | Edition |
3 | Affichage |
4 | Terminal |
5 | Onglets |
6 | Ouvrir un onglet | 1
7 | Ouvrir un terminal | 1
8 | Fermer l'onglet | 1
9 | Fermer la fenêtre | 1
10 | Copier | 2
11 | Coller | 2
12 | Préférences | 2
13 | Général | 12
14 | Apparence | 12
15 | Titre | 13
16 | Commande | 13
17 | Police | 14

272
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

18 | Couleur | 14
19 | Afficher la barre d'outils | 3
20 | Plein écran | 3
21 | Modifier le titre | 4
22 | Définir l'encodage | 4
23 | Réinitialiser | 4
24 | UTF-8 | 22
25 | Europe occidentale | 22
26 | Europe centrale | 22
27 | ISO-8859-1 | 25
28 | ISO-8859-15 | 25
29 | WINDOWS-1252 | 25
30 | ISO-8859-2 | 26
31 | ISO-8859-3 | 26
32 | WINDOWS-1250 | 26
33 | Onglet précédent | 5
34 | Onglet suivant | 5
(34 rows)

Nous allons définir une CTE récursive qui va afficher l’arborescence du menu Terminal.
La récursion va donc commencer par chercher la ligne correspondant à cette entrée de
menu dans la table entrees_menu. Une colonne calculée arborescence est créée, elle
servira plus tard dans la récursion :

SELECT menu_id, libelle, parent_id, libelle AS arborescence


FROM entrees_menu
WHERE libelle = 'Terminal'
AND parent_id IS NULL

La requête qui réalisera la récursion est une jointure entre le résultat de l’itération précé-
dente, obtenu par la vue parcours_menu de la CTE, qui réalisera une jointure avec la ta-
ble entrees_menu sur la colonne entrees_menu.parent_id qui sera jointe à la colonne
menu_id de l’itération précédente.

La condition d’arrêt de la récursion n’a pas besoin d’être exprimée. En effet, les entrées
terminales des menus ne peuvent pas être jointes avec de nouvelles entrées de menu, car
il n’y a pas d’autre correspondance avec parent_id).

On obtient ainsi la requête CTE récursive présentée ci-dessus.

À titre d’exemple, voici l’implémentation du jeu des six degrés de Kevin Bacon en utilisant
pgRouting :

WITH dijkstra AS (
SELECT seq, id1 AS node, id2 AS edge, cost
FROM pgr_dijkstra('
SELECT f.film_id AS id,
273
https://dalibo.com/formations
SQL pour PostgreSQL

f.actor_id::integer AS source,
f2.actor_id::integer AS target,
1.0::float8 AS cost
FROM film_actor f
JOIN film_actor f2
ON (f.film_id = f2.film_id and f.actor_id <> f2.actor_id)'
, 29539, 29726, false, false)
)
SELECT *
FROM actors
JOIN dijkstra
on (dijkstra.node = actors.actor_id) ;

actor_id | actor_name | seq | node | edge | cost


----------+----------------+-----+-------+------+------
29539 | Kevin Bacon | 0 | 29539 | 1330 | 1
29625 | Robert De Niro | 1 | 29625 | 53 | 1
29726 | Al Pacino | 2 | 29726 | -1 | 0
(3 lignes)

6.6 CONCURRENCE D'ACCÈS

• Problèmes pouvant se poser :


– UPDATE perdu
– lecture non répétable
• Plusieurs solutions possibles
– versionnement des lignes
– SELECT FOR UPDATE
– SERIALIZABLE

Plusieurs problèmes de concurrences d’accès peuvent se poser quand plusieurs transac-


tions modifient les mêmes données en même temps.

Tout d’abord, des UPDATE peuvent être perdus, dans le cas où plusieurs transactions
lisent la même ligne, puis la mettent à jour sans concertation. Par exemple, si la transaction
1 ouvre une transaction et effectue une lecture d’une ligne donnée :
BEGIN TRANSACTION;
SELECT * FROM employes WHERE matricule = '00000004';

La transaction 2 effectue les mêmes traitements :


BEGIN TRANSACTION;
SELECT * FROM employes WHERE matricule = '00000004';

274
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

Après un traitement applicatif, la transaction 1 met les données à jour pour noter
l’augmentation de 5 % du salarié. La transaction est validée dans la foulée avec COMMIT :
UPDATE employes
SET salaire = <valeur récupérée préalablement * 1.05>
WHERE matricule = '00000004';
COMMIT;

Après un traitement applicatif, la transaction 2 met également les données à jour pour
noter une augmentation exceptionnelle de 100 € :
UPDATE employes
SET salaire = <valeur récupérée préalablement + 100>
WHERE matricule = '00000004';
COMMIT;

Le salarié a normalement droit à son augmentation de 100 € ET l’augmentation de 5 %,


or l’augmentation de 5 % a été perdue car écrasée par la transaction n°2. Ce problème
aurait pu être évité de trois façons différentes :

• en effectuant un UPDATE utilisant la valeur lue par l’ordre UPDATE,


• en verrouillant les données lues avec SELECT FOR UPDATE,
• en utilisant le niveau d’isolation SERIALIZABLE.

La première solution n’est pas toujours envisageable, il faut donc se tourner vers les deux
autres solutions.

Le problème des lectures sales (dirty reads) ne peut pas se poser car PostgreSQL
n’implémente pas le niveau d’isolation READ UNCOMMITTED. Si ce niveau d’isolation est
sélectionné, PostgreSQL utilise alors le niveau READ COMMITTED.

6.6.1 SELECT FOR UPDATE

• SELECT FOR UPDATE


• Utilité :
– « réserver » des lignes en vue de leur mise à jour
– éviter les problèmes de concurrence d’accès

L’ordre SELECT FOR UPDATE permet de lire des lignes tout en les réservant en posant un
verrou dessus en vue d’une future mise à jour. Le verrou permettra une lecture parallèle,
mais mettra toute mise à jour en attente.

Reprenons l’exemple précédent et utilisons SELECT FOR UPDATE pour voir si le problème
de concurrence d’accès peut être résolu.
275
https://dalibo.com/formations
SQL pour PostgreSQL

session 1

BEGIN TRANSACTION;
SELECT * FROM employes WHERE matricule = '00000004' FOR UPDATE;
matricule | nom | service | salaire
-----------+----------+----------+---------
00000004 | Fantasio | Courrier | 4500.00
(1 row)

La requête SELECT a retourné les données souhaitées.

session 2

BEGIN TRANSACTION;
SELECT * FROM employes WHERE matricule = '00000004' FOR UPDATE;

La requête SELECT ne rend pas la main, elle est mise en attente.

session 3

Une troisième session effectue une lecture, sans poser de verrou explicite :

SELECT * FROM employes WHERE matricule = '00000004';


matricule | nom | service | salaire
-----------+----------+----------+---------
00000004 | Fantasio | Courrier | 4500.00
(1 row)

Le SELECT n’a pas été bloqué par la session 1. Seule la session 2 est bloquée car elle
tente d’obtenir le même verrou.

session 1

L’application a effectué ses calculs et met à jour les données en appliquant l’augmentation
de 5 % :

UPDATE employes
SET salaire = 4725
WHERE matricule = '00000004';

Les données sont vérifiées :

SELECT * FROM employes WHERE matricule = '00000004';


matricule | nom | service | salaire
-----------+----------+----------+---------
00000004 | Fantasio | Courrier | 4725.00
(1 row)

Enfin, la transaction est validée :

COMMIT;

276
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

session 2

La session 2 a rendu la main, le temps d’attente a été important pour réaliser ces calculs
complexes :

matricule | nom | service | salaire


-----------+----------+----------+---------
00000004 | Fantasio | Courrier | 4725.00
(1 row)

Time: 128127,105 ms

Le salaire obtenu est bien le salaire mis à jour par la session 1. Sur cette base, l’application
applique l’augmentation de 100 € :

UPDATE employes
SET salaire = 4825.00
WHERE matricule = '00000004';

SELECT * FROM employes WHERE matricule = '00000004';


matricule | nom | service | salaire
-----------+----------+----------+---------
00000004 | Fantasio | Courrier | 4825.00

La transaction est validée :

COMMIT;

Les deux transactions ont donc été effectuée de manière sérialisée, l’augmentation de
100 € ET l’augmentation de 5 % ont été accordées à Fantasio. En contre-partie, l’une des
deux transactions concurrentes a été mise en attente afin de pouvoir sérialiser les trans-
actions. Cela implique de penser les traitements en verrouillant les ressources auxquelles
on souhaite accéder.

L’ordre SELECT FOR UPDATE dispose également d’une option NOWAIT qui permet d’annuler
la transaction courante si un verrou ne pouvait être acquis. Si l’on reprend les premières
étapes de l’exemple précédent :

session 1

BEGIN TRANSACTION;
SELECT * FROM employes WHERE matricule = '00000004' FOR UPDATE NOWAIT;
matricule | nom | service | salaire
-----------+----------+----------+---------
00000004 | Fantasio | Courrier | 4500.00
(1 row)

Aucun verrou préalable n’avait été posé, la requête SELECT a retourné les données
souhaitées.
277
https://dalibo.com/formations
SQL pour PostgreSQL

session 2

On effectue la même chose sur la session n°2 :


BEGIN TRANSACTION;
SELECT * FROM employes WHERE matricule = '00000004' FOR UPDATE NOWAIT;
ERROR: could not obtain lock on row in relation "employes"

Comme la session n°1 possède déjà un verrou sur la ligne qui nous intéresse, l’option
NOWAIT sur le SELECT a annulé la transaction.

Il faut maintenant effectuer un ROLLBACK explicite pour pouvoir recommencer les traite-
ments au risque d’obtenir le message suivant :

ERROR: current transaction is aborted, commands ignored until


end of transaction block

6.6.2 SKIP LOCKED

• SELECT FOR UPDATE SKIP LOCKED


– PostgreSQL 9.5
• Utilité :
– implémente des files d’attentes parallélisables

Une dernière fonctionnalité intéressante de SELECT FOR UPDATE, apparue avec Post-
greSQL 9.5, permet de mettre en oeuvre différents workers qui consomment des
données issues d’une table représentant une file d’attente. Il s’agit de la clause SKIP
LOCKED, dont le principe de fonctionnement est identique à son équivalent sous Oracle.

En prenant une table représentant la file d’attente suivante, peuplée avec des données
générées :
CREATE TABLE test_skiplocked (id serial primary key, val text);
INSERT INTO test_skiplocked (val) SELECT md5(i::text)
FROM generate_series(1, 1000) i;

Une première transaction est ouverte et tente d’obtenir un verrou sur les 10 premières
lignes :
BEGIN TRANSACTION;

SELECT *
FROM test_skiplocked
LIMIT 10
FOR UPDATE SKIP LOCKED;

278
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

id | val
----+----------------------------------
1 | c4ca4238a0b923820dcc509a6f75849b
2 | c81e728d9d4c2f636f067f89cc14862c
3 | eccbc87e4b5ce2fe28308fd9f2a7baf3
4 | a87ff679a2f3e71d9181a67b7542122c
5 | e4da3b7fbbce2345d7772b0674a318d5
6 | 1679091c5a880faf6fb5e6087eb1b2dc
7 | 8f14e45fceea167a5a36dedd4bea2543
8 | c9f0f895fb98ab9159f51fd0297e236d
9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
10 | d3d9446802a44259755d38e6d163e820
(10 rows)

Si on démarre une seconde transaction en parallèle, avec la première transaction toujours


ouverte, le fait d’exécuter la requête SELECT FOR UPDATE sans la clause SKIP LOCKED au-
rait pour effet de la mettre en attente. L’ordre SELECT rendra la main lorsque la transaction
#1 se terminera.

Avec la clause SKIP LOCKED, les 10 premières verrouillées par la transaction n°1 seront
passées et ce sont les 10 lignes suivantes qui seront verrouillées et retournées par l’ordre
SELECT :

BEGIN TRANSACTION;

SELECT *
FROM test_skiplocked
LIMIT 10
FOR UPDATE SKIP LOCKED;

id | val
----+----------------------------------
11 | 6512bd43d9caa6e02c990b0a82652dca
12 | c20ad4d76fe97759aa27a0c99bff6710
13 | c51ce410c124a10e0db5e4b97fc2af39
14 | aab3238922bcc25a6f606eb525ffdc56
15 | 9bf31c7ff062936a96d3c8bd1f8f2ff3
16 | c74d97b01eae257e44aa9d5bade97baf
17 | 70efdf2ec9b086079795c442636b55fb
18 | 6f4922f45568161a8cdf4ad2299f6d23
19 | 1f0e3dad99908345f7439f8ffabdffc4
20 | 98f13708210194c475687be6106a3b84
(10 rows)

Ensuite, la première transaction supprime les lignes verrouillées et valide la transaction :

DELETE FROM test_skiplocked


279
https://dalibo.com/formations
SQL pour PostgreSQL

WHERE id IN (...);
COMMIT;

De même pour la seconde transaction, qui aura traité d’autres lignes en parallèle de la
transaction #1.

6.7 SERIALIZABLE SNAPSHOT ISOLATION

SSI : Serializable Snapshot Isolation (9.1+)


• Chaque transaction est seule sur la base
• Si on ne peut maintenir l’illusion
– Une des transactions en cours est annulée
• Sans blocage
• On doit être capable de rejouer la transaction
• Toutes les transactions impliquées doivent être serializable
• default_transaction_isolation=serializable dans la configuration

PostgreSQL fournit depuis la version 9.1 un mode d’isolation appelé SERIALIZABLE.


Dans ce mode, toutes les transactions déclarées comme telles s’exécutent comme si elles
étaient seules sur la base. Dès que cette garantie ne peut plus être apportée, une des
transactions est annulée.

Toute transaction non déclarée comme SERIALIZABLE peut en théorie s’exécuter


n’importe quand, ce qui rend inutile le mode SERIALIZABLE sur les autres. C’est donc un
mode qui doit être mis en place globalement.

Voici un exemple.

Dans cet exemple, il y a des enregistrements avec une colonne couleur contenant ’blanc’
ou ’noir’. Deux utilisateurs essayent simultanément de convertir tous les enregistrements
vers une couleur unique, mais chacun dans une direction opposée. Un veut passer tous
les blancs en noir, et l’autre tous les noirs en blanc.

L’exemple peut être mis en place avec ces ordres :

create table points


(
id int not null primary key,
couleur text not null
);
insert into points
with x(id) as (select generate_series(1,10))

280
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

select id, case when id % 2 = 1 then 'noir'


else 'blanc' end from x;

Session 1 :
set default_transaction_isolation = 'serializable';
begin;
update points set couleur = 'noir'
where couleur = 'blanc';

Session 2 :
set default_transaction_isolation = 'serializable';
begin;
update points set couleur = 'blanc'
where couleur = 'noir';

À ce moment, une des deux transaction est condamnée à mourir.

Session 2 :
commit;

Le premier à valider gagne.


select * from points order by id;

id | couleur
----+-------
1 | blanc
2 | blanc
3 | blanc
4 | blanc
5 | blanc
6 | blanc
7 | blanc
8 | blanc
9 | blanc
10 | blanc
(10 rows)

Session 1 : Celle-ci s’est exécutée comme si elle était seule.


commit;

ERROR: could not serialize access


due to read/write dependencies
among transactions
DETAIL: Cancelled on identification
as a pivot, during commit attempt.
HINT: The transaction might succeed if retried.
281
https://dalibo.com/formations
SQL pour PostgreSQL

Une erreur de sérialisation. On annule et on réessaye.


rollback;
begin;
update points set couleur = 'noir'
where couleur = 'blanc';
commit;

Il n’y a pas de transaction concurrente pour gêner.


select * from points order by id;

id | couleur
----+-------
1 | noir
2 | noir
3 | noir
4 | noir
5 | noir
6 | noir
7 | noir
8 | noir
9 | noir
10 | noir
(10 rows)

La transaction s’est exécutée seule, après l’autre.

Le mode SERIALIZABLE permet de s’affranchir des SELECT FOR UPDATE qu’on écrit
habituellement, dans les applications en mode READ COMMITTED. Toutefois, il fait bien
plus que ça, puisqu’il réalise du verrouillage de prédicats. Un enregistrement qui « ap-
paraît » ultérieurement suite à une mise à jour réalisée par une transaction concurrente
déclenchera aussi une erreur de sérialisation. Il permet aussi de gérer les problèmes
ci-dessus avec plus de deux sessions.

Pour des exemples plus complets, le mieux est de consulter la documentation officielle81
.

81
https://wiki.postgresql.org/wiki/SSI/fr

282
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6.8 CONCLUSION

• SQL est un langage très riche


• Connaître les nouveautés des versions de la norme depuis 20 ans permet de
– gagner énormément de temps de développemment
– mais aussi de performance

283
https://dalibo.com/formations
SQL pour PostgreSQL

6.9 TRAVAUX PRATIQUES

Jointure latérale

Cette série de question utilise la base de TP magasin. La base magasin peut être
téléchargée depuis https://dali.bo/tp_magasin (dump de 96 Mo, pour 667 Mo sur le
disque au final). Elle s’importe de manière très classique (une erreur sur le schéma public
déjà présent est normale), ici dans une base nommée aussi magasin :
pg_restore -d magasin magasin.dump

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

Afficher les 10 derniers articles commandés.

Pour chacune des 10 dernières commandes passées, afficher le


premier article commandé.

CTE récursive

Cet exercice propose de manipuler des données généalogiques, disposées dans le schéma
genealogie de l’environnement de TP.

Voici la description de la table genealogie qui sera utilisée :

\d genealogie
Table "public.genealogie"
Column | Type | Modifiers
----------------+---------+---------------------------------------
id | integer | not null default +
| | nextval('genealogie_id_seq'::regclass)
nom | text |
prenom | text |
date_naissance | date |
pere | integer |
mere | integer |
Indexes:
"genealogie_pkey" PRIMARY KEY, btree (id)

À partir de la table genealogie, déterminer qui sont les descen-


dants de Fernand DEVAUX.

284
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

À l’inverse, déterminer qui sont les ancètres de Adèle TAIL-


LANDIER

Réseau social

Cet exercice est assez similaire au précédent et propose de manipuler des arborescences.
Les tables de travail sont disponibles dans le schéma socialnet.
Les tableaux et la fonction unnest peuvent être utiles pour résoudre plus facilement ce
problème.

La table personnes contient la liste de toutes les personnes d’un réseau social.

Table "public.personnes"
Column | Type | Modifiers
--------+---------+--------------------------------------------------------
id | integer | not null default nextval('personnes_id_seq'::regclass)
nom | text | not null
prenom | text | not null
Indexes:
"personnes_pkey" PRIMARY KEY, btree (id)

La table relation contient les connexions entre ces personnes.

Table "public.relation"
Column | Type | Modifiers
--------+---------+-----------
gauche | integer | not null
droite | integer | not null
Indexes:
"relation_droite_idx" btree (droite)
"relation_gauche_idx" btree (gauche)

Déterminer le niveau de connexions entre Sadry Luettgen et Yelsi


Kerluke et afficher le chemin de relation le plus court qui permet
de les connecter ensemble.

Dépendance de vues

Les dépendances entre objets est un problème classique dans les bases de données :

• dans quel ordre charger des tables selon les clés étrangères ?
• dans quel ordre recréer des vues ?
285
https://dalibo.com/formations
SQL pour PostgreSQL

• etc.

Le catalogue de PostgreSQL décrit l’ensemble des objets de la base de données. Deux


tables vont nous intéresser pour mener à bien cet exercice :

• pg_depend liste les dépendances entre objets


• pg_rewrite stocke les définitions des règles de réécritures des vues (RULES)
• pg_class liste les objets que l’on peut interroger comme une table, hormis les fonc-
tions retournant des ensembles

La définition d’une vue peut être obtenue à l’aide de la fonction pg_get_viewdef.

Pour plus d’informations sur ces tables, se référer à la documentation :

• Catalogue pg_depend82
• Catalogue pg_rewrite83
• Catalogue pg_class84
• Fonction d’information du catalogue système85

Retrouver les dépendances de la vue pilotes_brno. Déduisez


également l’ordre de suppression et de recréation des vues.

82
https://www.postgresql.org/docs/current/static/catalog-pg-depend.html
83
https://www.postgresql.org/docs/current/static/catalog-pg-rewrite.html
84
https://www.postgresql.org/docs/current/static/catalog-pg-class.html
85
https://www.postgresql.org/docs/current/static/functions-info.html#FUNCTIONS-INFO-CATALOG-TABLE

286
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

6.10 TRAVAUX PRATIQUES (SOLUTIONS)

Jointure latérale

Afficher les 10 derniers articles commandés.

Tout d’abord, nous positionnons le search_path pour chercher les objets du schéma ma-
gasin :
SET search_path = magasin;

On commence par afficher les 10 dernières commandes :


SELECT *
FROM commandes
ORDER BY numero_commande DESC
LIMIT 10;

Une simple jointure nous permet de retrouver les 10 derniers articles commandés :
SELECT lc.produit_id, p.nom
FROM commandes c
JOIN lignes_commandes lc
ON (c.numero_commande = lc.numero_commande)
JOIN produits p
ON (lc.produit_id = p.produit_id)
ORDER BY c.numero_commande DESC, numero_ligne_commande DESC
LIMIT 10;

Pour chacune des 10 dernières commandes passées, afficher le


premier article commandé.

La requête précédente peut être dérivée pour répondre à la question demandée. Ici, pour
chacune des dix dernières commandes, nous voulons récupérer le nom du dernier article
commandé, ce qui sera transcrit sous la forme d’une jointure latérale :
SELECT numero_commande, produit_id, nom
FROM commandes c,
LATERAL (SELECT p.produit_id, p.nom
FROM lignes_commandes lc
JOIN produits p
ON (lc.produit_id = p.produit_id)
WHERE (c.numero_commande = lc.numero_commande)
ORDER BY numero_ligne_commande ASC
LIMIT 1
) premier_article_par_commande
287
https://dalibo.com/formations
SQL pour PostgreSQL

ORDER BY c.numero_commande DESC


LIMIT 10;

CTE récursive

Cet exercice propose de manipuler des données généalogiques.

Tout d’abord, nous positionnons le search_path pour chercher les objets du schéma ge-
nealogie :

SET search_path = genealogie;

Voici la description de la table genealogie qui sera utilisée :

\d genealogie
Table "public.genealogie"
Column | Type | Modifiers
----------------+---------+---------------------------------------
id | integer | not null default +
| | nextval('genealogie_id_seq'::regclass)
nom | text |
prenom | text |
date_naissance | date |
pere | integer |
mere | integer |
Indexes:
"genealogie_pkey" PRIMARY KEY, btree (id)

À partir de la table genealogie, déterminer qui sont les descen-


dants de Fernand DEVAUX.

WITH RECURSIVE arbre_genealogique AS (


SELECT id, nom, prenom, date_naissance, pere, mere
FROM genealogie
WHERE nom = 'DEVAUX'
AND prenom = 'Fernand'
UNION ALL
SELECT g.*
FROM arbre_genealogique ancetre
JOIN genealogie g
ON (g.pere = ancetre.id OR g.mere = ancetre.id)
)
SELECT id, nom, prenom, date_naissance
FROM arbre_genealogique;

288
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

À l’inverse, déterminer qui sont les ancètres de Adèle TAIL-


LANDIER

WITH RECURSIVE arbre_genealogique AS (


SELECT id, nom, prenom, date_naissance, pere, mere
FROM genealogie
WHERE nom = 'TAILLANDIER'
AND prenom = 'Adèle'
UNION ALL
SELECT ancetre.id, ancetre.nom, ancetre.prenom, ancetre.date_naissance,
ancetre.pere, ancetre.mere
FROM arbre_genealogique descendant
JOIN genealogie ancetre
ON (descendant.pere = ancetre.id OR descendant.mere = ancetre.id)
)
SELECT id, nom, prenom, date_naissance
FROM arbre_genealogique;

Réseau social

Cet exercice est assez similaire au précédent.

Tout d’abord, nous positionnons le search_path pour chercher les objets du schéma so-
cialnet :
SET search_path = socialnet;
Les tableaux et la fonction unnest peuvent être utiles pour résoudre plus facilement ce
problème

La table personnes contient la liste de toutes les personnes d’un réseau social.

Table "public.personnes"
Column | Type | Modifiers
--------+---------+--------------------------------------------------------
id | integer | not null default nextval('personnes_id_seq'::regclass)
nom | text | not null
prenom | text | not null
Indexes:
"personnes_pkey" PRIMARY KEY, btree (id)

La table relation contient les connexions entre ces personnes.

Table "public.relation"
Column | Type | Modifiers
--------+---------+-----------
gauche | integer | not null
289
https://dalibo.com/formations
SQL pour PostgreSQL

droite | integer | not null


Indexes:
"relation_droite_idx" btree (droite)
"relation_gauche_idx" btree (gauche)

Déterminer le niveau de connexions entre Sadry Luettgen et Yelsi


Kerluke et afficher le chemin de relation le plus court qui permet
de les connecter ensemble.

La requête suivante permet de répondre à cette question :


WITH RECURSIVE connexions AS (
SELECT gauche, droite, ARRAY[gauche] AS personnes_connectees,0::integer AS level
FROM relation
WHERE gauche = 1
UNION ALL
SELECT p.gauche, p.droite, personnes_connectees || p.gauche, level + 1 AS level
FROM connexions c
JOIN relation p ON (c.droite = p.gauche)
WHERE level < 4
AND p.gauche <> ANY (personnes_connectees)
), plus_courte_connexion AS (
SELECT *
FROM connexions
WHERE gauche = (
SELECT id FROM personnes WHERE nom = 'Kerluke' AND prenom = 'Yelsi'
)
ORDER BY level ASC
LIMIT 1
)
SELECT list.id, p.nom, p.prenom, list.level - 1 AS level
FROM plus_courte_connexion,
unnest(personnes_connectees) WITH ORDINALITY AS list(id, level)
JOIN personnes p on (list.id = p.id)
ORDER BY list.level;
Cet exemple fonctionne sur une faible volumétrie, mais les limites des bases relation-
nelles sont rapidement atteintes sur de telles requêtes.
Une solution consisterait à implémenter un algorithme de parcours de graphe avec
pgRoutinga , mais cela nécessitera de présenter les données sous une forme particulière.
Pour les problématiques de traitement de graphe, notamment sur de grosses
volumétries, une base de données orientée graphe comme Neo4J sera probablement
plus adaptée.
a
https://docs.pgrouting.org/2.0/fr/src/kdijkstra/doc/index.html#pgr-kdijkstra

290
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

Dépendance de vues

Les dépendances entre objets est un problème classique dans les bases de données :

• dans quel ordre charger des tables selon les clés étrangères ?
• dans quel ordre recréer des vues ?
• etc.

Le catalogue de PostgreSQL décrit l’ensemble des objets de la base de données. Deux


tables vont nous intéresser pour mener à bien cet exercice :

• pg_depend liste les dépendances entre objets


• pg_rewrite stocke les définitions des règles de réécritures des vues (RULES)
• pg_class liste les objets que l’on peut interroger comme une table, hormis les fonc-
tions retournant des ensembles

La définition d’une vue peut être obtenue à l’aide de la fonction pg_get_viewdef.

Pour plus d’informations sur ces tables, se référer à la documentation :

• Catalogue pg_depend86
• Catalogue pg_rewrite87
• Catalogue pg_class88
• Fonction d’information du catalogue système89

Retrouver les dépendances de la vue pilotes_brno. Déduisez


également l’ordre de suppression et de recréation des vues.

Tout d’abord, nous positionnons le search_path pour chercher les objets du schéma
brno2015 :

SET search_path = brno2015;

Si la jointure entre pg_depend et pg_rewrite est possible pour l’objet de départ, alors
il s’agit probablement d’une vue. En discriminant sur les objets qui référencent la vue
pilotes_brno, nous arrivons à la requête de départ suivante :

SELECT DISTINCT pg_rewrite.ev_class as objid, refobjid as refobjid, 0 as depth


FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
WHERE refobjid = 'pilotes_brno'::regclass;

86
http://www.postgresql.org/docs/current/static/catalog-pg-depend.html
87
http://www.postgresql.org/docs/current/static/catalog-pg-rewrite.html
88
http://www.postgresql.org/docs/current/static/catalog-pg-class.html
89
http://www.postgresql.org/docs/current/static/functions-info.html#FUNCTIONS-INFO-CATALOG-TABLE

291
https://dalibo.com/formations
SQL pour PostgreSQL

La présence de doublons nous oblige à utiliser la clause DISTINCT.

Nous pouvons donc créer un graphe de dépendances à partir de cette requête de départ,
transformée en requête récursive :

WITH RECURSIVE graph AS (


SELECT distinct pg_rewrite.ev_class as objid, refobjid as refobjid, 0 as depth
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
WHERE refobjid = 'pilotes_brno'::regclass
UNION ALL
SELECT distinct pg_rewrite.ev_class as objid, pg_depend.refobjid as refobjid,
depth + 1 as depth
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
JOIN graph on pg_depend.refobjid = graph.objid
WHERE pg_rewrite.ev_class != graph.objid
)
SELECT * FROM graph;

Il faut maintenant résoudre les OID pour déterminer les noms des vues et leur schéma.
Pour cela, nous ajoutons une vue resolved telle que :

WITH RECURSIVE graph AS (


SELECT distinct pg_rewrite.ev_class as objid, refobjid as refobjid, 0 as depth
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
WHERE refobjid = 'pilotes_brno'::regclass
UNION ALL
SELECT distinct pg_rewrite.ev_class as objid, pg_depend.refobjid as refobjid,
depth + 1 as depth
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
JOIN graph on pg_depend.refobjid = graph.objid
WHERE pg_rewrite.ev_class != graph.objid
),
resolved AS (
SELECT n.nspname AS dependent_schema, d.relname as dependent,
n2.nspname AS dependee_schema, d2.relname as dependee,
depth
FROM graph
JOIN pg_class d ON d.oid = objid
JOIN pg_namespace n ON d.relnamespace = n.oid
JOIN pg_class d2 ON d2.oid = refobjid
JOIN pg_namespace n2 ON d2.relnamespace = n2.oid
)
SELECT * FROM resolved;

292
6. SQL AVANCÉ POUR LE TRANSACTIONNEL

Nous pouvons maintenant présenter les ordres de suppression et de recréation des vues,
dans le bon ordre. Les vues doivent être supprimées selon le numéro d’ordre décroissant
et recrées selon le numéro d’ordre croissant :
WITH RECURSIVE graph AS (
SELECT distinct pg_rewrite.ev_class as objid, refobjid as refobjid, 0 as depth
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
WHERE refobjid = 'pilotes_brno'::regclass
UNION ALL
SELECT distinct pg_rewrite.ev_class as objid, pg_depend.refobjid as refobjid,
depth + 1 as depth
FROM pg_depend
JOIN pg_rewrite ON pg_depend.objid = pg_rewrite.oid
JOIN graph on pg_depend.refobjid = graph.objid
WHERE pg_rewrite.ev_class != graph.objid
),
resolved AS (
SELECT n.nspname AS dependent_schema, d.relname as dependent,
n2.nspname AS dependee_schema, d2.relname as dependee,
d.oid as dependent_oid,
depth
FROM graph
JOIN pg_class d ON d.oid = objid
JOIN pg_namespace n ON d.relnamespace = n.oid
JOIN pg_class d2 ON d2.oid = refobjid
JOIN pg_namespace n2 ON d2.relnamespace = n2.oid
)
(SELECT 'DROP VIEW ' || dependent_schema || '.' || dependent || ';'
FROM resolved
GROUP BY dependent_schema, dependent
ORDER BY max(depth) DESC)
UNION ALL
(SELECT 'CREATE OR REPLACE VIEW ' || dependent_schema || '.' || dependent ||
' AS ' || pg_get_viewdef(dependent_oid)
FROM resolved
GROUP BY dependent_schema, dependent, dependent_oid
ORDER BY max(depth));

293
https://dalibo.com/formations
SQL pour PostgreSQL

7 TYPES DE BASE

• PostgreSQL offre un système de typage complet


– types standards
– types avancés propres à PostgreSQL

7.0.1 PRÉAMBULE

• SQL possède un typage fort


– le type employé décrit la donnée manipulée
– garantit l’intégrité des données
– primordial au niveau fonctionnel
– garantit les performances

7.0.2 MENU

• Qu’est-ce qu’un type ?


• Les types SQL standards
– numériques
– temporels
– textuels et binaires
• Les types avancés de PostgreSQL

7.0.3 OBJECTIFS

• Comprendre le système de typage de PostgreSQL


• Savoir choisir le type adapté à une donnée
• Être capable d’utiliser les types avancés à bon escient

294
7. TYPES DE BASE

7.1 LES TYPES DE DONNÉES

• Qu’est-ce qu’un type ?


• Représentation physique
• Impacts sur l’intégrité
• Impacts fonctionnels

7.1.1 QU'EST-CE QU'UN TYPE ?

• Un type définit :
– les valeurs que peut prendre une donnée
– les opérateurs applicables à cette donnée

7.1.2 IMPACT SUR LES PERFORMANCES

• Choisir le bon type pour :


– optimiser les performances
– optimiser le stockage

7.1.3 IMPACTS SUR L'INTÉGRITÉ

• Le bon type de données garantit l’intégrité des données :


– la bonne représentation
– le bon intervalle de valeur

Le choix du type employé pour stocker une donnée est primordial pour garantir l’intégrité
des données.

Par exemple, sur une base de données mal conçue, il peut arriver que les dates soient
stockées sous la forme d’une chaîne de caractère. Ainsi, une date malformée ou invalide
pourra être enregistrée dans la base de données, passant outre les mécanismes de con-
trôle d’intégrité de la base de données. Si une date est stockée dans une colonne de type
date, alors ces problèmes ne se posent pas :
postgres=# create table test_date (dt date);
CREATE TABLE

295
https://dalibo.com/formations
SQL pour PostgreSQL

postgres=# insert into test_date values ('2015-0717');


ERROR: invalid input syntax for type date: "2015-0717"
LINE 1: insert into test_date values ('2015-0717');
^
postgres=# insert into test_date values ('2015-02-30');
ERROR: date/time field value out of range: "2015-02-30"
LINE 1: insert into test_date values ('2015-02-30');

postgres=# insert into test_date values ('2015-07-17');


INSERT 0 1

7.1.4 IMPACTS FONCTIONNELS

• Un type de données offre des opérateurs spécifiques :


– comparaison
– manipulation
• Exemple: une date est-elle comprise entre deux dates données ?

7.2 TYPES NUMÉRIQUES

• Entiers
• Flottants
• Précision fixée

7.2.1 TYPES NUMÉRIQUES : ENTIERS

• 3 types entiers :
– smallint : 2 octets
– integer : 4 octets
– bigint : 8 octets
• Valeur exacte
• Signé
• Utilisation :
– véritable entier
– clé technique

296
7. TYPES DE BASE

7.2.2 TYPES NUMÉRIQUES : FLOTTANTS

• 2 types flottants :
– real/float4
– double precision/float8
• Données numériques « floues »
– valeurs non exactes
• Utilisation :
– stockage des données issues de capteurs

7.2.3 TYPES NUMÉRIQUES : NUMERIC

• 1 type
– numeric(.., ..)
• Type exact
– mais calcul lent
• Précision choisie : totale, partie décimale
• Utilisation :
– données financières
– calculs exacts
• Déconseillé pour :
– clés primaires
– données non exactes (ex : résultats de capteurs)

7.2.4 OPÉRATIONS SUR LES NUMÉRIQUES

• Indexable : >, >=, =, <=,<


• +, -, /, *, modulo (%), puissance (^)
• Pour les entiers :
– AND, OR, XOR (&, |, #)
– décalage de bits (shifting): >>, <<
• Attention aux conversions (casts) / promotions !

Tout les types numériques sont indexables avec des indexes standards btree, permettant
la recherche avec les opérateurs d’égalité / inégalité. Pour les entiers, il est possible de
réaliser des opérations bit-à-bit :
297
https://dalibo.com/formations
SQL pour PostgreSQL

postgres=# select 2 | 4;
?column?
----------
6
(1 ligne)

postgres=# select 7 & 3;


?column?
----------
3
(1 ligne)

Il faut toutefois être vigilant face aux opérations de cast implicites et de promotions des
types numériques. Par exemple, les deux requêtes suivantes ramèneront le même résul-
tat, mais l’une sera capable d’utiliser un éventuel index sur id, l’autre non :
postgres=# explain select * from t1 where id = 10::int4;
QUERY PLAN
-------------------------------------------------------------------------
Bitmap Heap Scan on t1 (cost=4.67..52.52 rows=50 width=4)
Recheck Cond: (id = 10)
-> Bitmap Index Scan on t1_id_idx (cost=0.00..4.66 rows=50 width=0)
Index Cond: (id = 10)
(4 lignes)

postgres=# explain select * from t1 where id = 10::numeric;


QUERY PLAN
-----------------------------------------------------
Seq Scan on t1 (cost=0.00..195.00 rows=50 width=4)
Filter: ((id)::numeric = 10::numeric)
(2 lignes)

Cela peut paraître contre-intuitif, mais le cast est réalisé dans ce sens pour ne pas perdre
d’information. Par exemple, si la valeur numérique cherchée n’est pas un entier. Il faut
donc faire spécialement attention aux types utilisés côté applicatif. Avec un ORM tel
Hibernate, il peut être tentant de faire correspondre un BigInt à un numeric côté SQL,
ce qui engendrera des casts implicites, et potentiellement des indexes non utilisés.

298
7. TYPES DE BASE

7.2.5 CHOIX D'UN TYPE NUMÉRIQUE

• integer ou biginteger :
– identifiants (clés primaires et autre)
– nombres entiers
• numeric :
– valeurs décimales exactes
– performance non critique
• float, real :
– valeurs flottantes, non exactes
– performance demandée : SUM(), AVG(), etc.

Pour les identifiants, il est préférable d’utiliser des entiers ou grands entiers. En effet, il
n’est pas nécessaire de s’encombrer du bagage technique et de la pénalité en performance
dû à l’utilisation de numeric. Contrairement à d’autres SGBD, PostgreSQL ne transforme
pas un numeric sans partie décimale en entier, et celui-ci souffre donc des performances
inhérentes au type numeric.

De même, lorsque les valeurs sont entières, il faut utiliser le type adéquat.

Pour les nombres décimaux, lorsque la performance n’est pas critique, préférer le type
numeric: il est beaucoup plus simple de raisonner sur ceux-ci et leur précision que de
garder à l’esprit les subtilités du standard IEEE 754 définissant les opérations sur les flot-
tants. Dans le cas de données décimales nécessitant une précision exacte, il est impératif
d’utiliser le type numeric.

Les nombres flottants (float et real) ne devraient être utilisés que lorsque les implica-
tions en terme de perte de précision sont intégrées, et que la performance d’un type
numeric devient gênante. En pratique, cela est généralement le cas lors d’opérations
d’agrégations.

Pour bien montrer les subtilités des types float, et les risques auquels ils nous exposent,
considérons l’exemple suivant, en créant une table contenant 25000 fois la valeur 0.4,
stockée soit en float soit en numeric :

postgres=# create table t_float as (


select 0.04::float as cf,
0.04::numeric as cn
from generate_series(1, 25000)
);
SELECT 25000
postgres=# select sum(cn), sum(cf) from t_float ;
sum | sum
---------+-----------------
299
https://dalibo.com/formations
SQL pour PostgreSQL

1000.00 | 999.99999999967
(1 ligne)

Si l’on considère la performance de ces opérations, on remarque des temps d’exécution


bien différents :
postgres=# select sum(cn) from t_float ;
sum
---------
1000.00
(1 ligne)

Temps : 10,611 ms
postgres=# select sum(cf) from t_float ;
sum
-----------------
999.99999999967
(1 ligne)

Temps : 6,434 ms

Pour aller (beaucoup) plus loin, le document suivant détaille le comportement des flot-
tants selon le standard IEEE : https://docs.oracle.com/cd/E19957-01/806-3568/ncg_
goldberg.html

7.3 TYPES TEMPORELS

• Date
• Date & heure
– …avec ou sans fuseau

7.3.1 TYPES TEMPORELS : DATE

• date
– représente une date, sans heure
– affichage format ISO : YYYY-MM-DD
• Utilisation :
– stockage d’une date lorsque la composante heure n’est pas utilisée
• Cas déconseillés :
– stockage d’une date lorsque la composante heure est utilisée

300
7. TYPES DE BASE

# SELECT now()::date ;

now
------------
2019-11-13

7.3.2 TYPES TEMPORELS : TIME

• time
– représente une heure sans date
– affichage format ISO HH24:MI:SS
• Peu de cas d’utilisation
• À éviter :
– stockage d’une date et de la composante heure dans deux colonnes

# SELECT now()::time ;

now
-----------------
15:19:39.947677

7.3.3 TYPES TEMPORELS : TIMESTAMP

• timestamp (without time zone !)


– représente une date et une heure
– fuseau horaire non précisé
• Utilisation :
– stockage d’une date et d’une heure

# SELECT now()::timestamp ;

now
----------------------------
2019-11-13 15:20:54.222233

Le nom réel est timestamp without time zone. Comme on va le voir, il faut lui préférer
le type timestamptz.

301
https://dalibo.com/formations
SQL pour PostgreSQL

7.3.4 TYPES TEMPORELS : TIMESTAMP WITH TIME ZONE

• timestamp with time zone = timestamptz


– représente une date et une heure
– fuseau horaire inclus
– affichage : 2019-11-13 15:33:00.824096+01
• Utilisation :
– stockage d’une date et d’une heure, cadre mondial
– à préférer à timestamp without time zone

Ces deux exemples ont été exécutés à quelques secondes d’intervalle sur des instances
en France (heure d’hiver) et au Brésil :
# SHOW timezone;

TimeZone
--------------
Europe/Paris

# SELECT now() ;
now
-------------------------------
2019-11-13 15:32:09.615455+01

# SHOW timezone;

TimeZone
-------------
Brazil/West
(1 ligne)

# SELECT now() ;
now
-------------------------------
2019-11-13 10:32:39.536972-04

# SET timezone to 'Europe/Paris' ;

# SELECT now() ;
now
-------------------------------
2019-11-13 15:33:00.824096+01

302
7. TYPES DE BASE

On préférera presque tout le temps le type timestamptz à timestamp (sans fuseau


horaire), ne serait-ce qu’à cause des heures d’été et d’hiver. Les deux types occupent
8 octets.

7.3.5 TYPES TEMPORELS : INTERVAL

• interval
– représente une durée
• Utilisation :
– exprimer une durée
– dans une requête, pour modifier une date/heure existante

7.3.6 CHOIX D'UN TYPE TEMPOREL

• Préférer les types avec timezone


– Toujours plus simple à gérer au début qu’à la fin
• Considérer les types range pour tout couple « début / fin »
• Utiliser interval /generate_series

De manière générale, il est beaucoup plus simple de gérer des dates avec timezone côté
base. En effet, dans le cas où une seule timezone est gérée, les clients ne verront pas la
différence. Si en revanche les besoins évoluent, il sera beaucoup plus simple de gérer les
différentes timezones à ce moment là.

Les points suivants concernent plus de la modélisation que des types de données à propre-
ment parler, mais il est important de considérer les types ranges dès lors que l’on souhaite
stocker un couple « date de début / date de fin ». Nous aurons l’occasion de revenir sur
ces types dans la suite de ce module.

Enfin, une problématique assez commune consiste à vouloir effectuer des jointures con-
tre une table de dates de références. Une (mauvaise) solution à ce problème consiste
à stocker ces dates dans une table. Il est beaucoup plus avantageux en terme de main-
tenance de ne pas stocker ces dates, mais de les générer à la volée. Par exemple, pour
générer tous les jours de janvier 2015 :
postgres=# select * from generate_series(
'2015-01-01',
'2015-01-31',
'1 day'::interval
303
https://dalibo.com/formations
SQL pour PostgreSQL

);
generate_series
------------------------
2015-01-01 00:00:00+01
2015-01-02 00:00:00+01
2015-01-03 00:00:00+01
2015-01-04 00:00:00+01
2015-01-05 00:00:00+01
2015-01-06 00:00:00+01
2015-01-07 00:00:00+01
2015-01-08 00:00:00+01
2015-01-09 00:00:00+01
2015-01-10 00:00:00+01
2015-01-11 00:00:00+01
2015-01-12 00:00:00+01
2015-01-13 00:00:00+01
2015-01-14 00:00:00+01
2015-01-15 00:00:00+01
2015-01-16 00:00:00+01
2015-01-17 00:00:00+01
2015-01-18 00:00:00+01
2015-01-19 00:00:00+01
2015-01-20 00:00:00+01
2015-01-21 00:00:00+01
2015-01-22 00:00:00+01
2015-01-23 00:00:00+01
2015-01-24 00:00:00+01
2015-01-25 00:00:00+01
2015-01-26 00:00:00+01
2015-01-27 00:00:00+01
2015-01-28 00:00:00+01
2015-01-29 00:00:00+01
2015-01-30 00:00:00+01
2015-01-31 00:00:00+01

7.4 TYPES CHAÎNES

• Texte à longueur variable


• Binaires

En général on choisira une chaîne de longueur variable. Nous ne parlerons pas ici du type
char (à tailel fixe), d’utilisation très restreinte.

304
7. TYPES DE BASE

7.4.1 TYPES CHAÎNES : CARACTÈRES

• varchar(_n_), text
• Représentent une chaîne de caractères
• Valident l’encodage
• Valident la longueur maximale de la chaîne (contrainte !)
• Utilisation :
– stocker des chaînes de caractères non binaires

7.4.2 TYPES CHAÎNES : BINAIRES

• bytea
• Stockage de données binaires
– encodage en hexadécimal ou séquence d’échappement
• Utilisation :
– stockage de courtes données binaires
• Cas déconseillés :
– stockage de fichiers binaires

Le type bytea permet de stocker des données binaires dans une base de données Post-
greSQL.

7.4.3 QUEL TYPE CHOISIR ?

• varchar (sans limite) ou text (non standard)


• Implémenter la limite avec une contrainte
– plus simple à modifier
CREATE TABLE t1 (c1 varchar CHECK (length(c1) < 10))

En règle générale, il est recommandé d’utiliser un champ de type varchar tout court, et
de vérifier la longueur au niveau d’une contrainte. En effet, il sera plus simple de modifier
celle-ci par la suite, en modifiant uniquement la contrainte. De plus, la contrainte permet
plus de possibilités, comme par exemple d’imposer une longueur minimale.

305
https://dalibo.com/formations
SQL pour PostgreSQL

7.4.4 COLLATION

• L’ordre de tri dépend des langues & de conventions variables


• Collation par colonne / index / requête
• SELECT * FROM mots ORDER BY t COLLATE "C" ;
• CREATE TABLE messages (
id int,
fr TEXT COLLATE "fr_FR.utf8",
de TEXT COLLATE "de_DE.utf8" );

L’ordre de tri des chaînes de caractère (« collation ») peut varier suivant le contenu d’une
colonne. Rien que parmi les langues européennes, il existe des spécificités propres à
chacune, et même à différents pays pour une même langue. Si l’ordre des lettres est une
convention courante, il existe de nombreuses variations propres à chacune (comme é, à,
æ, ö, ß, å, ñ…), avec des règles de tri propres. Certaines lettres peuvent être assimilées
à une combinaison d’autres lettres. De plus, la place relative des majuscules, celles des
chiffres, ou des caractères non alphanumérique est une pure affaire de convention.

La collation dépend de l’encodage (la manière de stocker les caractères), de nos jours
généralement UTF890 (standard Unicode). PostgreSQL utilise par défaut UTF8 et il est
chaudement conseillé de ne pas changer cela.

La collation par défaut dans une base est définie à sa création, et est visible avec \l
(ci-dessous pour une installation en français). Le type de caractères est généralement
identique.

# \l
Liste des bases de données
Nom | Propriétaire | Encodage | Collationnement | Type caract. |...
-----------+--------------+----------+-----------------+--------------+
pgbench | pgbench | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 |
postgres | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 |
template0 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | ...
template1 | postgres | UTF8 | fr_FR.UTF-8 | fr_FR.UTF-8 | ...

Parmi les collations que l’on peut rencontrer, il y a par exemple en_US.UTF-8 (la collation
par défaut de beaucoup d’installations), ou C, basée sur les caractères ASCII et les valeurs
des octets. De vieilles installations peuvent encore contenir fr_FR.iso885915@euro.

Si le tri par défaut ne convient pas, on peut le changer à la volée dans la requête SQL, au
besoin après avoir créé la collation.

Exemple avec du français :


90
https://fr.wikipedia.org/wiki/UTF-8

306
7. TYPES DE BASE

CREATE TABLE mots (t text) ;

INSERT INTO mots


VALUES ('A'),('a'),('aa'),('z'),('ä'),('å'),('Å'),('aa'),('æ'),('ae'),('af'), ('ß'), ('ss') ;

SELECT * FROM mots ORDER BY t ; -- sous-entendu, ordre par défaut en français ici

t
----
a
A
å
Å
ä
aa
aa
ae
æ
af
ss
ß
z

Noter que les caractères « æ » et « ß » sont correctement assimilés à « ae » et « ss ». (Ce


serait aussi le cas avec en_US.utf8 ou de_DE.utf8).

Avec la collation C, l’ordre est plus basique, soit celui des codes UTF-8 :

SELECT * FROM mots ORDER BY t COLLATE "C" ;

t
----
A
a
aa
aa
ae
af
ss
z
Å
ß
ä
å
æ

Un intérêt de la collation C est qu’elle est plus simple et se repose sur la glibc du système,
ce qui lui permet d’être souvent plus rapide qu’une des collations ci-dessus. Il suffit donc
307
https://dalibo.com/formations
SQL pour PostgreSQL

parfois de remplacer ORDER BY champ_texte par ORDER BY champ_text COLLATE "C",


à condition bien sûr que l’ordre ASCII convienne.

Il est possible d’indiquer dans la définition de chaque colonne quelle doit être sa collation
par défaut :

Pour du danois :
-- La collation doit exister sur le système d'exploitation
CREATE COLLATION IF NOT EXISTS "da_DK" (locale='da_DK.utf8');

ALTER TABLE mots ALTER COLUMN t TYPE text COLLATE "da_DK" ;

SELECT * FROM mots ORDER BY t ; -- ordre danois

t
----
A
a
ae
af
ss
ß
z
æ
ä
Å
å
aa

Dans cette langue, les majuscules viennent traditionnellement avant les minuscules, et
« å» et « aa » viennent après le « z ».

Avec une collation précisée dans la requête, un index peut ne pas être utilisable. En effet,
par défaut, il est trié sur disque dans l’ordre de la collation de la colonne. Un index peut
cependant se voir affecter une collation différente de celle de la colonne, par exemple
pour un affichage ou une interrogation dans plusieurs langues :
CREATE INDEX ON mots (t); -- collation par défaut de la colonne
CREATE INDEX ON mots (t COLLATE "de_DE.utf8"); -- tri allemand

La collation n’est pas qu’une question d’affichage. Le tri joue aussi dans la sélection quand
il y a des inégalités, et le français et le danois revoient ici des résultats différents :
SELECT * FROM mots WHERE t > 'z' COLLATE "fr_FR";

t
---
(0 ligne)

308
7. TYPES DE BASE

SELECT * FROM mots WHERE t > 'z' COLLATE "da_DK";

t
----
aa
ä
å
Å
aa
æ
(6 lignes)

7.4.5 COLLATION & SOURCES

Source des collations :


• le système : installations séparées nécessaires, différences entre OS
• (>= v 10) : librairie externe ICU
CREATE COLLATION danois (provider = icu, locale = 'da-x-icu') ;

Des collations comme en_US.UTF-8 ou fr_FR.UTF-8 sont dépendantes des locales in-
stallées sur la machine. Cela implique qu’elles peuvent subtilement différer entre deux
systèmes, même entre deux versions d’un même système d’exploitation ! De plus, la lo-
cale voulue n’est pas forcément présente, et son mode d’installation dépend du système
d’exploitation et de sa distribution…

Pour éliminer ces problèmes tout en améliorant la flexibilité, PostgreSQL 10 a introduit


les collations ICU, c’est-à-dire standardisées et versionnées dans une librairie séparée. En
pratique, les paquets des distributions l’installent automatiquement avec PostgreSQL. Les
collations linguistiques sont donc immédiatement disponibles via ICU :
CREATE COLLATION danois (provider = icu, locale = 'da-x-icu') ;

La librairie ICU fournit d’autres collations plus spécifiques liées à un contexte, par exemple
l’ordre d’un annuaire ou l’ordre suivant la casse. Par exemple, cette collation très pratique
tient compte de la valeur des chiffres (« tri naturel ») :
CREATE COLLATION nombres (provider = icu, locale = 'fr-u-kn-kr-latn-digit');

SELECT * FROM
(VALUES ('1 sou'),('01 sou'),('02 sous'),('2 sous'),
('10 sous'),('0100 sous') ) AS n(n)
ORDER BY n COLLATE nombres ;

n
-----------
309
https://dalibo.com/formations
SQL pour PostgreSQL

01 sou
1 sou
02 sous
2 sous
10 sous
0100 sous

Alors que, par défaut, « 02 » précéderait « 1 » :


SELECT * FROM
(VALUES ('1 sou'),('01 sou'),('02 sous'),('2 sous'),
('10 sous'),('0100 sous') ) AS n(n)
ORDER BY n ; -- tri avec la locale par défaut

n
-----------
0100 sous
01 sou
02 sous
10 sous
1 sou
2 sous

Pour d’autres exemples et les détails, voir ce billet de Peter Eisentraut91 et la documen-
tation officielle92 .

Pour voir les collations disponibles, consulter pg_collation :


SELECT collname, collcollate, collprovider, collversion
FROM pg_collation WHERE collname LIKE 'fr%' ;

collname | collcollate | collprovider | collversion


-------------+-------------+--------------+-------------
fr-BE-x-icu | fr-BE | i | 153.80
fr-BF-x-icu | fr-BF | i | 153.80
fr-CA-x-icu | fr-CA | i | 153.80.32.1
fr-x-icu | fr | i | 153.80
...
fr_FR | fr_FR.utf8 | c | ¤
fr_FR.utf8 | fr_FR.utf8 | c | ¤
fr_LU | fr_LU.utf8 | c | ¤
fr_LU.utf8 | fr_LU.utf8 | c | ¤
(57 lignes)

Les collations installées dans la base sont visibles avec \dO sous psql :
=# \dO
Liste des collationnements
91
https://blog.2ndquadrant.com/icu-support-postgresql-10/
92
https://docs.postgresql.fr/current/collation.html

310
7. TYPES DE BASE

Schéma | Nom | Collationnement | … | Fournisseur | …


--------+--------------+-----------------------+---+-------------+----
public | belge | fr-BE-x-icu | … | icu | …
public | chiffres_fin | fr-u-kn-kr-latn-digit | … | icu | …
public | da_DK | da_DK.utf8 | … | libc | …
public | danois | da-x-icu | … | icu | …
public | de_DE | de_DE.utf8 | … | libc | …
public | de_phonebook | de-u-co-phonebk | … | icu | …
public | es_ES | es_ES.utf8 | … | libc | …
public | espagnol | es-x-icu | … | icu | …
public | fr_FR | fr_FR.utf8 | … | libc | …
public | français | fr-FR-x-icu | … | icu | …

7.5 TYPES AVANCÉS

• PostgreSQL propose des types plus avancés


• De nombreuses extensions !
– faiblement structurés (JSON...)
– intervalle
– géométriques
– tableaux

7.5.1 TYPES FAIBLEMENT STRUCTURÉS

• PostgreSQL propose plusieurs types faiblement structurés :


– hstore (clé/valeur historique)
– JSON
– XML

311
https://dalibo.com/formations
SQL pour PostgreSQL

7.5.2 JSON

• json
– stockage sous forme d’une chaîne de caractère
– valide un document JSON sans modification
• jsonb (PG > 9.4)
– stockage binaire optimisé
– beaucoup plus de fonctions (dont jsonpath en v12)
– à préférer

7.5.3 XML

• xml
– stocke un document XML
– valide sa structure
• Quelques opérateurs disponibles

7.6 TYPES INTERVALLE DE VALEURS

• Représentation d’intervalle
– utilisable avec plusieurs types : entiers, dates, timestamps, etc.
– contrainte d’exclusion

7.6.1 RANGE

• range
• exprime un intervalle
– entre deux bornes
– incluses ou non
– notation américaine !
• pour plusieurs types de bases
– int, bigint, numeric
– date, timestamp, timestamp with timezone

312
7. TYPES DE BASE

7.6.2 CONTRAINTE D'EXCLUSION

• Contrainte d’exclusion
• Utilisation :
– éviter le chevauchement de deux intervalles (range)
• Performance :
– s’appuie sur un index

7.7 TYPES GÉOMÉTRIQUES

• Plusieurs types natifs 2D :


– point, ligne, segment, polygone, cercle
• Utilisation :
– stockage de géométries simples, sans référentiel de projection
• Pour la géographie :
– extension PostGIS

7.8 TYPES UTILISATEURS

• Plusieurs types définissables par l’utilisateur


– types composites
– domaines
– enums

7.8.1 TYPES COMPOSITES

• Regroupe plusieurs attributs


– la création d’une table implique la création d’un type composite associé
• Utilisation :
– déclarer un tableau de données composites
– en PL/pgSQL, déclarer une variable de type enregistrement

Les types composites sont assez difficiles à utiliser, car ils nécessitent d’adapter la syn-
taxe spécifiquement au type composite. S’il ne s’agit que de regrouper quelques attributs
ensemble, autant les lister simplement dans la déclaration de la table.
313
https://dalibo.com/formations
SQL pour PostgreSQL

En revanche, il peut être intéressant pour stocker un tableau de données composites dans
une table.

7.8.2 TYPE ÉNUMÉRATION

• Ensemble fini de valeurs possibles


– uniquement des chaînes de caractères
– 63 caractères maximum
• Équivalent des énumérations des autres langages
• Utilisation :
– listes courtes figées (statuts...)
– évite des jointures

Référence :

• Type énumération93

93
https://docs.postgresql.fr/current/datatype-enum.html

314
8. TYPES AVANCÉS

8 TYPES AVANCÉS

PostgreSQL offre des types avancés :


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

8.1 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 PostreSQL :
– hstore : clé/valeur
– json : JSON, stockage texte, validation syntaxique, fonctions d’extraction
– jsonb : JSON, stockage binaire, accès rapide, fonctions d’extraction, de re-
quêtage, indexation avancée

Ces types sont utilisés quand le modèle relationnel n’est pas assez souple, donc s’il est
nécessaire d’ajouter dynamiquement des colonnes à la table suivant les besoins du client,
ou si le détail des attributs d’une entité n’est pas connu (modélisation géographique par
exemple), etc.

La solution traditionnelle est de créer des tables entité/attribut de ce format :


CREATE TABLE attributs_sup (entite int, attribut text, valeur text);

On y stocke dans entite la clé de l’enregistrement de la table principale, dans attribut


la colonne supplémentaire, et dans valeur la valeur de cette attribut. Ce modèle présente
l’avantage évident de résoudre le problème. Les défauts sont par contre nombreux :

• Les attributs d’une ligne peuvent être totalement éparpillés dans la table
attributs_sup : récupérer n’importe quelle information demandera donc
des accès à de nombreux blocs différents.
• Il faudra plusieurs requêtes (au moins deux) pour récupérer le détail d’un enreg-
istrement, avec du code plus lourd côté client pour fusionner le résultat des deux re-
315
https://dalibo.com/formations
SQL pour PostgreSQL

quêtes, ou bien une requête effectuant des jointures (autant que d’attributs, sachant
que le nombre de jointures complexifie énormément le travail de l’optimiseur SQL)
pour retourner directement l’enregistrement complet.

Toute recherche complexe est très inefficace : une recherche multi-critères sur ce
schéma va être extrêmement peu performante. Les statistiques sur les valeurs d’un
attribut deviennent nettement moins faciles à estimer pour PostgreSQL. Quant aux
contraintes d’intégrité entre valeurs, elles deviennent pour le moins complexes à gérer.

Les types hstore, json et jsonb permettent de résoudre le problème autrement. Ils
permettent de stocker les différentes entités dans un seul champ pour chaque ligne de
l’entité. L’accès aux attributs se fait par une syntaxe ou des fonctions spécifiques.

Il n’y a même pas besoin de créer une table des attributs séparée : le mécanisme du
« TOAST » permet de déporter les champs volumineux (texte, JSON, hstore…) dans
une table séparée gérée par PostgreSQL, éventuellement en les compressant, et cela de
manière totalement transparente. On y gagne donc en simplicité de développement.

8.2 HSTORE

Stocker des données non structurées


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

hstore est une extension, fournie en « contrib ». Elle est donc systématiquement
disponible. L’installer permet d’utiliser le type de même nom. On peut ainsi stocker un
ensemble de clés/valeurs, exclusivement textes, dans un unique champ.

Ces champs sont indexables et peuvent recevoir des contraintes d’intégrité (unicité, non
recouvrement…).

Les hstore ne permettent par contre qu’un modèle « plat ». Il s’agit d’un pur stockage clé-
valeur. Si vous avez besoin de stocker des informations davantage orientées document,
vous devrez vous tourner vers un type JSON.

Ce type perd donc de son intérêt depuis que PostgreSQL 9.4 a apporté le type jsonb. Il
lui reste sa simplicité d’utilisation.

316
8. TYPES AVANCÉS

8.2.1 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

Les ordres précédents installent l’extension, créent une table avec un champ de type
hstore, insèrent trois lignes, avec des attributs variant sur chacune, indexent l’ensemble
avec un index GiST, et enfin recherchent les lignes où l’attribut carnivore possède la
valeur t.

# SELECT * FROM animaux ;

nom | caract
--------+-----------------------------------
canari | "vole"=>"oui", "pattes"=>"2"
loup | "pattes"=>"4", "carnivore"=>"oui"
carpe | "eau"=>"douce"

Les différentes fonctions disponibles sont bien sûr dans la documentation94 .

Par exemple :

# UPDATE animaux SET caract = caract||'poil=>t'::hstore


WHERE nom = 'loup' ;

# SELECT * FROM animaux WHERE caract@>'carnivore=>oui';

nom | caract
------+--------------------------------------------------
loup | "poil"=>"t", "pattes"=>"4", "carnivore"=>"oui"

Il est possible de convertir un hstore en tableau :

# SELECT hstore_to_matrix(caract) FROM animaux


WHERE caract->'vole' = 'oui';

94
https://docs.postgresql.fr/current/hstore.html

317
https://dalibo.com/formations
SQL pour PostgreSQL

hstore_to_matrix
-------------------------
{{vole,oui},{pattes,2}}

ou en JSON :
# SELECT caract::jsonb FROM animaux
WHERE (caract->'pattes')::int > 2;

caract
----------------------------------------------------
{"pattes": "4", "poil": "t", "carnivore": "oui"}

L’indexation de ces champs peut se faire avec divers types d’index. Un index unique
n’est possible qu’avec un index B-tree classique. Les index GIN ou GiST sont utiles
pour rechercher des valeurs d’un attribut. Les index hash ne sont utiles que pour des
recherches d’égalité d’un champ entier ; par contre ils sont très compacts. (Rappelons
que les index hash sont inutilisables avant PostgreSQL 10).

8.3 JSON

{
"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 (PG 12+)

Le format JSON95 est devenu extrêmement populaire. Au-delà d’un simple stockage
clé/valeur, il permet de stocker des tableaux, ou des hiérarchies, de manière plus sim-
ple et lisible qu’en XML. Par exemple, pour décrire une personne, on peut utiliser cette
structure :
{
"firstName": "Jean",
"lastName": "Dupont",
95
https://fr.wikipedia.org/wiki/JavaScript_Object_Notation

318
8. TYPES AVANCÉS

"isAlive": true,
"age": 27,
"address": {
"streetAddress": "43 rue du Faubourg Montmartre",
"city": "Paris",
"state": "",
"postalCode": "75002"
},
"phoneNumbers": [
{
"type": "personnel",
"number": "06 12 34 56 78"
},
{
"type": "bureau",
"number": "07 89 10 11 12"
}
],
"children": [],
"spouse": null
}

Même si le type json est apparu dans PostgreSQL 9.2, le JSON n’est vraiment utilisable
que depuis PostgreSQL 9.4, et le type jsonb (binaire). Les opérateurs SQL/JSON path96
ont été ajoutés dans PostgreSQL 12, suite à l’introduction du JSON dans le standard
SQL:2016. Ils permettent de spécifier des parties d’un champ JSON.

8.3.1 TYPE JSON

• Type texte
• Validation du format JSON
• Fonctions de manipulation JSON
– Mais ré-analyse du champ pour chaque appel de fonction
– Indexation comme un simple texte
• => Réservé au stockage à l’identique
– Sinon préférer jsonb

Le type natif json, dans PostgreSQL, n’est rien d’autre qu’un habillage autour du type
texte. Il valide à chaque insertion/modification que la donnée fournie est une syntaxe
JSON valide.
96
https://paquier.xyz/postgresql-2/postgres-12-jsonpath/

319
https://dalibo.com/formations
SQL pour PostgreSQL

Le stockage est exactement le même qu’une chaîne de texte, et utilise le mécanisme du


« TOAST », qui compresse au besoin les plus grands champs, de manière transparente
pour l’utilisateur.

Toutefois, le fait que la donnée soit validée comme du JSON permet d’utiliser des fonc-
tions de manipulation, comme l’extraction d’un attribut, la conversion d’un JSON en en-
registrement, de façon systématique sur un champ sans craindre d’erreur.

On préférera généralement le type binaire jsonb, pour les performances et ses fonction-
nalités supplémentaires. Au final, l’intérêt de json est surtout de conserver un objet JSON
sous sa forme originale.

Les exemples suivants avec jsonb sont aussi applicables à json. La plupart des fonctions
et opérateurs existent dans les deux versions.

8.3.2 TYPE JSONB

• JSON au format Binaire


• Indexation GIN
• Gestion du langage JSON Path (v12+)

Le type jsonb permet de stocker les données dans un format binaire optimisé. Ainsi, il
n’est plus nécessaire de désérialiser l’intégralité du document pour accéder à une pro-
priété.

Pour un exemple extrême (document JSON d’une centaine de Mo), voici une comparaison
des performances entre les deux types json et jsonb pour la récupération d’un champ
sur 1300 lignes :
EXPLAIN (ANALYZE, BUFFERS) SELECT document->'id' FROM test_json;

QUERY PLAN
---------------------------------------------------------------------
Seq Scan on test_json (cost=0.00..26.38 rows=1310 width=32)
(actual time=893.454..912.168 rows=1 loops=1)
Buffers: shared hit=170
Planning time: 0.021 ms
Execution time: 912.194 ms

EXPLAIN (ANALYZE, BUFFERS) SELECT document->'id' FROM test_jsonb;

QUERY PLAN
--------------------------------------------------------------------
Seq Scan on test_jsonb (cost=0.00..26.38 rows=1310 width=32)
(actual time=77.707..84.148 rows=1 loops=1)

320
8. TYPES AVANCÉS

Buffers: shared hit=170


Planning time: 0.026 ms
Execution time: 84.177 ms

8.3.3 JSON : EXEMPLE D'UTILISATION

CREATE TABLE personnes (datas jsonb );


INSERT INTO personnes (datas) VALUES ('
{
"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": [],
"spouse": null
}
');

Un champ de type jsonb (ou json) accepte tout champ JSON directement.

8.3.4 JSON : AFFICHAGE DE CHAMPS

SELECT datas->>'firstName' AS prenom_texte,


datas->'address' AS addr_json
FROM personnes ;
SELECT datas #>> '{address,city}' AS villes FROM personnes ;
SELECT jsonb_array_elements (datas->'phoneNumbers')->>'number' FROM personnes;

Le type json dispose de nombreuses fonctions de manipulation et d’extraction. Les opéra-


teurs ->> et -> renvoient respectivement une valeur au format texte, et au format JSON.
# SELECT datas->>'firstName' AS prenom,
datas->'address' AS addr
FROM personnes
\gdesc
321
https://dalibo.com/formations
SQL pour PostgreSQL

Column | Type
--------+-------
prenom | text
addr | jsonb

# \g

prenom | addr
---------+-------------------------------------------------------
Jean | { +
| "streetAddress": "43 rue du Faubourg Montmartre",+
| "city": "Paris", +
| "state": "", d
| "postalCode": "75002" +
| }
Georges | { +
| "streetAddress": "27 rue des Moulins", +
| "city": "Châteauneuf", +
| "postalCode": "45990" +
| }

L’équivalent existe avec des chemins, avec #> et #>> :


# SELECT datas #>> '{address,city}' AS villes FROM personnes ;

villes
-------------
Paris
Châteauneuf

jsonb_array_elements permet de parcourir des tableaux de JSON :


# SELECT jsonb_array_elements (datas->'phoneNumbers')->>'number'
AS numero
FROM personnes ;

numero
----------------
06 12 34 56 78
07 89 10 11 12
06 21 34 56 87
07 98 10 11 13

322
8. TYPES AVANCÉS

8.3.5 CONVERSIONS JSONB / RELATIONNEL

• Construire un objet JSON depuis un ensemble :


– json_object_agg()
• Construire un ensemble de tuples depuis un objet JSON :
– jsonb_each(), jsonb_to_record()
• Manipuler des tableaux :
– jsonb_array_elements(), jsonb_to_recordset()

Les fonctions permettant de construire du jsonb, ou de le manipuler de manière en-


sembliste permettent une très forte souplesse. Il est aussi possible de déstructurer des
tableaux, mais il est compliqué de requêter sur leur contenu.

Par exemple, si l’on souhaite filtrer des documents de la sorte pour ne ramener que ceux
dont une catégorie est categorie :

{
"id": 3,
"sous_document": {
"label": "mon_sous_document",
"mon_tableau": [
{"categorie": "categorie"},
{"categorie": "unique"}
]
}
}

CREATE TABLE json_table (id serial, document jsonb);


INSERT INTO json_table (document) VALUES ('
{
"id": 3,
"sous_document": {
"label": "mon_sous_document",
"mon_tableau": [
{"categorie": "categorie"},
{"categorie": "unique"}
]
}
}
');

SELECT document->'id'
FROM json_table j,
LATERAL jsonb_array_elements(document #> '{sous_document, mon_tableau}')
AS elements_tableau
WHERE elements_tableau->>'categorie' = 'categorie';
323
https://dalibo.com/formations
SQL pour PostgreSQL

Ce type de requête devient rapidement compliqué à écrire, et n’est pas indexable.

8.3.6 JSONB : INDEXATION

• Index fonctionnel sur un champ précis :


CREATE INDEX idx_prs_nom ON personnes ((datas->>'lastName')) ;
• Indexation « schemaless » grâce au GIN :
CREATE INDEX idx_prs ON personnes USING gin(datas jsonb_path_ops) ;

On peut créer des index sur certaines propriétés en passant par des index fonctionnels.
L’index ci-dessus permettra d’accélérer des requêtes utilisant une clause WHERE sur
jsonb_extract_path_text(datas,'ouvrages') uniquement. Pour un champ fréquem-
ment utilisé pour les recherches, c’est le plus efficace. C’est d’ailleurs tout ce qui est pos-
sible avec un type json (avec la fonction json_extract_path_text).

Mais le gros avantage des jsonb réside dans la capacité de tirer parti des fonctionnalités
avancées de PostgreSQL, notamment les index GIN. Deux classes d’opérateurs permet-
tent d’en tirer parti. L’opérateur par défaut supporte plus d’opérations, mais il est souvent
suffisant, et plus efficace, de choisir l’opérateur jsonb_path_ops (voir les détails97 ).

CREATE INDEX idx_prs ON personnes USING gin(datas jsonb_path_ops) ;

jsonb_path_ops supporte notamment l’opérateur « contient » (@>) :

# EXPLAIN (ANALYZE) SELECT datas->>'firstName' FROM personnes


WHERE datas @> '{"lastName": "Dupont"}'::jsonb ;

QUERY PLAN
--------------------------------------------------------------------
Bitmap Heap Scan on personnes (cost=2.01..3.02 rows=1 width=32)
(actual time=0.018..0.019 rows=1 loops=1)
Recheck Cond: (datas @> '{"lastName": "Dupont"}'::jsonb)
Heap Blocks: exact=1
-> Bitmap Index Scan on idx_prs (cost=0.00..2.01 rows=1 width=0)
(actual time=0.010..0.010 rows=1 loops=1)
Index Cond: (datas @> '{"lastName": "Dupont"}'::jsonb)
Planning Time: 0.052 ms
Execution Time: 0.104 ms

Il n’est en revanche pas possible de faire des recherches sur des opérateurs B-tree clas-
siques (<, <=, >, >=), ou sur le contenu de tableaux. On est obligé pour cela de revenir
au monde relationnel, et l’indexation devra alors utiliser des index fonctionnels sur les
97
https://docs.postgresql.fr/current/datatype-json.html#JSON-INDEXING

324
8. TYPES AVANCÉS

clés que l’on souhaite indexer. Il est donc préférable d’utiliser les opérateurs spécifiques,
comme « contient » (@>).

8.3.7 SQL/JSON & JSONPATH

• SQL:2016 introduit SQL/JSON et le langage JSON Path


• JSON Path :
– langage de recherche pour JSON
– concis, flexible, plus rapide
– inclus dans PostreSQL 12 pour l’essentiel
– exemple :
SELECT jsonb_path_query (datas, '$.phoneNumbers[*] ? (@.type == "bureau") ')
FROM personnes ;

JSON path facilite la recherche et le parcours dans les documents JSON complexes. Il
évite de parcourir manuellement les nœuds du JSON.

Par exemple, une recherche peut se faire ainsi :


SELECT datas->>'firstName' AS prenom
FROM personnes
WHERE datas @@ '$.lastName == "Durand"' ;

prenom
----------
Georges

Mais l’intérêt est d’extraire facilement des parties d’un tableau :


SELECT jsonb_path_query (datas, '$.phoneNumbers[*] ? (@.type == "bureau") ')
FROM personnes ;

jsonb_path_query
------------------------------------------------
{"type": "bureau", "number": "07 89 10 11 12"}
{"type": "bureau", "number": "07 98 10 11 13"}

On trouvera d’autres exemples dans la présentation de Postgres Pro dédié à la fonction-


nalité lors la parution de PostgreSQL 1298 , ou dans un billet de Michael Paquier99 .

98
https://www.postgresql.eu/events/pgconfeu2019/sessions/session/2555/slides/221/jsonpath-pgconfeu-2019.
pdf
99
https://paquier.xyz/postgresql-2/postgres-12-jsonpath/

325
https://dalibo.com/formations
SQL pour PostgreSQL

8.3.8 EXTENSION JSQUERY

• Fournit un « langage de requête » sur JSON


– Similaire aux jsonpath (PG 12+), mais dès PG 9.4
• Indexation GIN
SELECT document->'id'
FROM json_table j
WHERE j.document @@ 'sous_document.mon_tableau.#.categorie = categorie' ;

L’extension jsquery fournit un opérateur @@ (« correspond à la requête jsquery »), simi-


laire à l’opérateur @@ de la recherche plein texte. Celui-ci permet de faire des requêtes
évoluées sur un document JSON, optimisable facilement grâce à la méthode d’indexation
supportée.

jsquery permet de requêter directement sur des champs imbriqués, en utilisant même des
jokers pour certaines parties.

Le langage en lui-même est relativement riche, et fournit un système de hints pour pallier
à certains problèmes de la collecte de statistiques, qui devrait être améliorée dans le futur.

Il supporte aussi les opérateurs différents de l’égalité :


SELECT *
FROM json_table j
WHERE j.document @@ 'ville.population > 10000';

Le périmètre est très proche des expressions jsonpath apparues dans PostgreSQL 12, qui,
elles, se basent sur le standard SQL:2016. Les auteurs sont d’ailleurs les mêmes. Voir cet
article pour les détails100 , ou le dépôt github101 . La communauté fournit des paquets.

8.4 XML

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

Le type xml, inclus de base, vérifie que le XML inséré est un document « bien formé », ou
constitue des fragments de contenu (« content »). L’encodage UTF-8 est impératif. Il y a
100
https://habr.com/en/company/postgrespro/blog/500440/
101
https://github.com/akorotkov/jsquery

326
8. TYPES AVANCÉS

quelques limitations par rapport aux dernières versions du standard, XPath et XQuery102
. Le stockage se fait en texte, donc bénéficie du mécanisme de compression TOAST.

Il existe quelques opérateurs et fonctions de validation et de manipulations, décrites dans


la documentation du type xml103 ou celle des fonctions104 . Par contre, une simple
comparaison est impossible et l’indexation est donc impossible directement. Il faudra
passer par une expression XPath.

À titre d’exemple : XMLPARSE convertit une chaîne en document XML, XMLSERIALIZE


procède à l’opération inverse.

CREATE TABLE liste_cd (catalogue xml) ;


\d liste_cd

Table « public.liste_cd »
Colonne | Type | Collationnement | NULL-able | Par défaut
-----------+------+-----------------+-----------+------------
catalogue | xml | | |

INSERT INTO liste_cd


SELECT XMLPARSE ( DOCUMENT
$$<?xml version="1.0" encoding="UTF-8"?>
<CATALOG>
<CD>
<TITLE>The Times They Are a-Changin'</TITLE>
<ARTIST>Bob Dylan</ARTIST>
<COUNTRY>USA</COUNTRY>
<YEAR>1964</YEAR>
</CD>
<CD>
<TITLE>Olympia 1961</TITLE>
<ARTIST>Jacques Brel</ARTIST>
<COUNTRY>France</COUNTRY>
<YEAR>1962</YEAR>
</CD>
</CATALOG> $$ ) ;
--- Noter le $$ pour délimiter une chaîne contenant une apostrophe

102
https://docs.postgresql.fr/current/xml-limits-conformance.html
103
https://docs.postgresql.fr/current/datatype-xml.html
104
https://docs.postgresql.fr/current/functions-xml.html

327
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT XMLSERIALIZE (DOCUMENT catalogue AS text) FROM liste_cd;

xmlserialize
--------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?> +
<CATALOG> +
<CD> +
<TITLE>The Times They Are a-Changin'</TITLE>+
<ARTIST>Bob Dylan</ARTIST> +
<COUNTRY>USA</COUNTRY> +
<YEAR>1964</YEAR> +
</CD> +
<CD> +
<TITLE>Olympia 1961</TITLE> +
<ARTIST>Jacques Brel</ARTIST> +
<COUNTRY>France</COUNTRY> +
<YEAR>1962</YEAR> +
</CD> +
</CATALOG>
(1 ligne)

Il existe aussi query_to_xml pour convertir un résultat de requête en XML, xmlagg pour
agréger des champs XML, ou xpath pour extraire des nœuds suivant une expression
XPath 1.0.

NB : l’extension xml2105 est dépréciée et ne doit pas être utilisée dans les nouveaux
projets.

8.5 OBJETS BINAIRES

Deux méthodes pour stocker des objets binaires :


• bytea : type binaire
• Large Objects
– Se manipulent plutôt comme des fichiers

PostgreSQL permet de stocker des données au format binaire, potentiellement de


n’importe quel type, par exemple des images ou des PDF.
105
https://docs.postgresql.fr/current/xml2.html

328
8. TYPES AVANCÉS

La volumétrie peut donc devenir énorme, surtout si les binaires sont modifiés : le mode
de fonctionnement de PostgreSQL aura tendance à les dupliquer. Cela aura un impact sur
la fragmentation, la quantité de journaux, la taille des sauvegardes, et toutes les opéra-
tions de maintenance. Ce qui est intéressant à conserver dans une base sont des don-
nées qu’il faudra rechercher, et l’on recherche rarement au sein d’un gros binaire. En
général, l’essentiel des données binaires que l’on voudrait confier à une base peut se con-
tenter d’un stockage classique, PostgreSQL ne contenant qu’un chemin ou une URL vers
le fichier réel.

On dispose de deux méthodes différentes pour gérer les données binaires :

• bytea : un type comme un autre ;


• Large Object : des objets séparés, à gérer indépendamment des tables.

8.5.1 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');

Voici un exemple :

CREATE TABLE demo_bytea(a bytea);


INSERT INTO demo_bytea VALUES ('bonjour'::bytea);
SELECT * FROM demo_bytea ;
a
------------------
\x626f6e6a6f7572

Nous avons inséré la chaîne de caractère « bonjour » dans le champ bytea, en fait sa
représentation binaire dans l’encodage courant (UTF-8). Si nous interrogeons la table,
nous voyons la représentation textuelle du champ bytea. Elle commence par \x pour
indiquer un encodage de type hex. Ensuite, chaque paire de valeurs hexadécimales
représente un octet.

Un second format d’affichage est disponible : escape :


329
https://dalibo.com/formations
SQL pour PostgreSQL

SET bytea_output = escape ;


SELECT * FROM demo_bytea ;

a
---------
bonjour

INSERT INTO demo_bytea VALUES ('journée'::bytea);


SELECT * FROM demo_bytea ;

a
----------------
bonjour
journ\303\251e

Le format de sortie escape ne protège donc que les valeurs qui ne sont pas représentables
en ASCII 7 bits. Ce format peut être plus compact pour des données textuelles essentielle-
ment en alphabet latin sans accent, où le plus gros des caractères n’aura pas besoin d’être
protégé.

Le format hex est bien plus efficace à convertir, ce qui en fait le choix par défaut depuis
PostgreSQL 9.0. (Il est donc primordial que les librairies clientes, Java par exemple,
soient d’une version assez récente pour comprendre les deux formats. Sinon il faut forcer
bytea_output à escape, sous peine de corruption.)

Pour charger directement un fichier, on peut notamment utiliser la fonction pg_read_binary_file,


exécutée par le serveur PostreSQL :
INSERT INTO demo_bytea (a)
SELECT pg_read_binary_file ('/chemin/fichier');

En théorie, un bytea peut contenir 1 Go. En pratique, on se limitera à nettement moins,


ne serait-ce que parce pg_dump tombe en erreur quand il doit exporter des bytea de plus
de 500 Mo environ (le décodage double le nombre d’octets et dépasse cette limite de
1 Go).

La documentation officielle106 liste les fonctions pour encoder, décoder, extraire, hacher...
les bytea.

106
https://docs.postgresql.fr/current/functions-binarystring.html

330
8. TYPES AVANCÉS

8.5.2 LARGE OBJECT

• Objet indépendant des tables


• Identifié par un OID
– à stocker dans les tables
• Suppression manuelle : trigger, batch (extensions)
• Fonction de manipulation, modification
– lo_create, lo_import, lo_seek, lo_open, lo_read, lo_write…
• Maxi 4 To (à éviter aussi…)

Un large object est un objet totalement décorrélé des tables. (il est stocké en fait dans la
table système pg_largeobject). Le code doit donc gérer cet objet séparément :

• créer le large object et stocker ce qu’on souhaite dedans ;


• stocker la référence à ce large object dans une table (avec le type lob) ;
• interroger l’objet séparément de la table ;
• le supprimer explicitement quand il n’est plus référencé : il ne disparaîtra pas au-
tomatiquement !

Le large object nécessite donc un plus gros investissement au niveau du code.

En contrepartie, il a les avantages suivant :

• une taille jusqu’à 4 To, ce qui n’est tout de même pas conseillé ;
• la possibilité d’accéder à une partie directement (par exemple les octets de 152000
à 153020), ce qui permet de le transférer par parties sans le charger en mémoire
(notamment, le driver JDBC de PostgreSQL fournit une classe LargeObject107 ) ;
• de ne modifier que cette partie sans tout réécrire.

Il y a plusieurs méthodes pour nettoyer les large objets devenu inutiles :

• appeler la fonction lo_unlink dans le code client — au risque d’oublier ;


• utiliser la fonction trigger lo_manage fournie par le module contrib lo : (voir docu-
mentation108 , si les large objects ne sont jamais référencés plus d’une fois ;
• appeler régulièrement le programme vacuumlo (là encore un contrib109 ) : il liste
tous les large objects référencés dans la base, puis supprime les autres. Ce traitement
est bien sûr un peu lourd.

Voir la documentation110 pour les détails.

107
https://jdbc.postgresql.org/documentation/head/largeobjects.html
108
https://docs.postgresql.fr/current/lo.html
109
https://docs.postgresql.fr/current/vacuumlo.html
110
https://docs.postgresql.fr/current/largeobjects.html

331
https://dalibo.com/formations
SQL pour PostgreSQL

8.6 TRAVAUX PRATIQUES

Les TP sur le types hstore et JSON utilisent la base cave. La base cave peut être
téléchargée depuis https://dali.bo/tp_cave (dump de 2,6 Mo, pour 71 Mo sur le disque
au final) et importée ainsi :

$ psql -c "CREATE ROLE caviste LOGIN PASSWORD 'caviste'"


$ psql -c "CREATE DATABASE cave OWNER caviste"
$ pg_restore -d cave cave.dump # Une erreur sur un schéma 'public' existant est normale

8.6.1 HSTORE (OPTIONNEL)

Pour ce TP, il est fortement conseillé d’aller regarder la documentation officielle du type
hstore sur https://docs.postgresql.fr/current/hstore.html.

But : Obtenir une version dénormalisée de la table stock : elle contiendra une colonne
de type hstore contenant l’année, l’appellation, la région, le récoltant, le type, et le con-
tenant :

vin_id integer
nombre integer
attributs hstore

Ce genre de table n’est évidemment pas destiné à une application transactionnelle: on


n’aurait aucun moyen de garantir l’intégrité des données de cette colonne. Cette colonne
va nous permettre d’écrire une recherche multi-critères efficace sur nos stocks.

Afficher les informations à stocker avec la requête suivante :

SELECT stock.vin_id,
stock.annee,
stock.nombre,
recoltant.nom AS recoltant,
appellation.libelle AS appellation,
region.libelle AS region,
type_vin.libelle AS type_vin,
contenant.contenance,
contenant.libelle as contenant
FROM stock
JOIN vin ON (stock.vin_id=vin.id)
JOIN recoltant ON (vin.recoltant_id=recoltant.id)
JOIN appellation ON (vin.appellation_id=appellation.id)
JOIN region ON (appellation.region_id=region.id)
JOIN type_vin ON (vin.type_vin_id=type_vin.id)

332
8. TYPES AVANCÉS

JOIN contenant ON (stock.contenant_id=contenant.id)


LIMIT 10;

(LIMIT 10 est là juste pour éviter de ramener tous les enregistrements).

Créer une table stock_denorm (vin_id int, nombre int,


attributs hstore) et y copier le contenu de la requête.
Une des écritures possibles passe par la génération d’un tableau,
ce qui permet de passer tous les éléments au constructeur de
hstore sans se soucier de formatage de chaîne de caractères.
(Voir la documentation.)

Créer un index sur le champ attributs pour accélérer les


recherches.

Rechercher le nombre de bouteilles (attribut bouteille) en stock


de vin blanc (attribut type_vin) d’Alsace (attribut region).
Quel est le temps d’exécution de la requête ?
Combien de buffers accédés ?

Refaire la même requête sur la table initiale. Qu’en conclure ?

8.6.2 JSONB

Nous allons créer une table dénormalisée contenant uniquement un champs JSON.

Pour chaque vin, le document JSON aura la structure suivante :


{
vin: {
recoltant: {
nom: text,
adressse: text
},
appellation: {
libelle: text,
region: text
},
type_vin: text
},
stocks: [{
333
https://dalibo.com/formations
SQL pour PostgreSQL

contenant: {
contenance: real,
libelle: text
},
annee: integer,
nombre: integer
}]
}

Pour écrire une requête permettant de générer ces documents, nous allons procéder par
étapes.

La requête suivante permet de générer les parties vin du docu-


ment, avec recoltant et appellation.
Créer un document JSONB pour chaque ligne de vin grâce à la
fonction jsonb_build_object.

SELECT
recoltant.nom,
recoltant.adresse,
appellation.libelle,
region.libelle,
type_vin.libelle
FROM vin
INNER JOIN recoltant on vin.recoltant_id = recoltant.id
INNER JOIN appellation on vin.appellation_id = appellation.id
INNER JOIN region on region.id = appellation.region_id
INNER JOIN type_vin on vin.type_vin_id = type_vin.id;

Écrire une requête permettant de générer la partie stocks du


document, soit un document JSON pour chaque ligne de la table
stock, incluant le contenant.

Fusionner les requêtes précédentes pour générer un document


complet pour chaque ligne de la table vin. Créer une table
stock_jsonb avec un unique champ JSONB rassemblant ces
documents.

Calculer la taille de la table, et la comparer à la taille du reste de


la base.

334
8. TYPES AVANCÉS

Depuis cette nouvelle table, renvoyer l’ensemble des récoltants


de la région Beaujolais.

Renvoyer l’ensemble des vins pour lesquels au moins une


bouteille entre 1992 et 1995 existe. (la difficulté est d’extraire
les différents stocks d’une même ligne de la table)

Indexer le document jsonb en utilisant un index de type GIN.

Peut-on réécrire les deux requêtes précédentes pour utiliser


l’index ?

8.6.3 LARGE OBJECTS

Créer une table fichiers avec un texte et une colonne permet-


tant de référencer des Large Objects.

Importer un fichier local à l’aide de psql dans un large object.


Noter l’oid retourné.

Importer un fichier du serveur à l’aide de psql dans un large ob-


ject.

Afficher le contenu de ces différents fichiers à l’aide de psql.

Les sauvegarder dans des fichiers locaux.

335
https://dalibo.com/formations
SQL pour PostgreSQL

8.7 TRAVAUX PRATIQUES (SOLUTIONS)

8.7.1 HSTORE

Afficher les informations à stocker avec la requête suivante :

SELECT stock.vin_id,
stock.annee,
stock.nombre,
recoltant.nom AS recoltant,
appellation.libelle AS appellation,
region.libelle AS region,
type_vin.libelle AS type_vin,
contenant.contenance,
contenant.libelle as contenant
FROM stock
JOIN vin ON (stock.vin_id=vin.id)
JOIN recoltant ON (vin.recoltant_id=recoltant.id)
JOIN appellation ON (vin.appellation_id=appellation.id)
JOIN region ON (appellation.region_id=region.id)
JOIN type_vin ON (vin.type_vin_id=type_vin.id)
JOIN contenant ON (stock.contenant_id=contenant.id)
LIMIT 10;

(LIMIT 10 est là juste pour éviter de ramener tous les enregistrements).

Créer une table stock_denorm (vin_id int, nombre int,


attributs hstore) et y copier le contenu de la requête.
Une des écritures possibles passe par la génération d’un tableau,
ce qui permet de passer tous les éléments au constructeur de
hstore sans se soucier de formatage de chaîne de caractères.
(Voir la documentation.)

Une remarque toutefois : les éléments du tableau doivent tous être de même type, d’où
la conversion en text des quelques éléments entiers. C’est aussi une limitation du type
hstore : il ne supporte que les attributs texte.

Cela donne :
CREATE EXTENSION hstore;

CREATE TABLE stock_denorm AS


SELECT stock.vin_id,
stock.nombre,

336
8. TYPES AVANCÉS

hstore(ARRAY['annee', stock.annee::text,
'recoltant', recoltant.nom,
'appellation',appellation.libelle,
'region',region.libelle,
'type_vin',type_vin.libelle,
'contenance',contenant.contenance::text,
'contenant',contenant.libelle]) AS attributs
FROM stock
JOIN vin ON (stock.vin_id=vin.id)
JOIN recoltant ON (vin.recoltant_id=recoltant.id)
JOIN appellation ON (vin.appellation_id=appellation.id)
JOIN region ON (appellation.region_id=region.id)
JOIN type_vin ON (vin.type_vin_id=type_vin.id)
JOIN contenant ON (stock.contenant_id=contenant.id);

Et l’on n’oublie pas les statistiques :


ANALYZE stock_denorm;

Créer un index sur le champ attributs pour accélérer les


recherches.

CREATE INDEX idx_stock_denorm on stock_denorm USING gin (attributs );

Rechercher le nombre de bouteilles (attribut bouteille) en stock


de vin blanc (attribut type_vin) d’Alsace (attribut region).
Quel est le temps d’exécution de la requête ?
Combien de buffers accédés ?

Attention au A majuscule de Alsace, les hstore sont sensibles à la casse !


EXPLAIN (ANALYZE,BUFFERS)
SELECT *
FROM stock_denorm
WHERE attributs @>
'type_vin=>blanc, region=>Alsace, contenant=>bouteille';

QUERY PLAN
--------------------------------------------------------------------------------
Bitmap Heap Scan on stock_denorm (cost=64.70..374.93 rows=91 width=193)
(actual time=64.370..68.526 rows=1680 loops=1)
Recheck Cond: (attributs @> '"region"=>"Alsace", "type_vin"=>"blanc",
"contenant"=>"bouteille"'::hstore)
Heap Blocks: exact=1256
Buffers: shared hit=1353
-> Bitmap Index Scan on idx_stock_denorm
337
https://dalibo.com/formations
SQL pour PostgreSQL

(cost=0.00..64.68 rows=91 width=0)


(actual time=63.912..63.912 rows=1680 loops=1)
Index Cond: (attributs @> '"region"=>"Alsace", "type_vin"=>"blanc",
"contenant"=>"bouteille"'::hstore)
Buffers: shared hit=97
Planning time: 0.210 ms
Execution time: 68.927 ms

Refaire la même requête sur la table initiale. Qu’en conclure ?

EXPLAIN (ANALYZE,BUFFERS)
SELECT stock.vin_id,
stock.annee,
stock.nombre,
recoltant.nom AS recoltant,
appellation.libelle AS appellation,
region.libelle AS region,
type_vin.libelle AS type_vin,
contenant.contenance,
contenant.libelle as contenant
FROM stock
JOIN vin ON (stock.vin_id=vin.id)
JOIN recoltant ON (vin.recoltant_id=recoltant.id)
JOIN appellation ON (vin.appellation_id=appellation.id)
JOIN region ON (appellation.region_id=region.id)
JOIN type_vin ON (vin.type_vin_id=type_vin.id)
JOIN contenant ON (stock.contenant_id=contenant.id)
WHERE type_vin.libelle='blanc' AND region.libelle='Alsace'
AND contenant.libelle = 'bouteille';

QUERY PLAN
--------------------------------------------------------------------------------
Nested Loop (cost=11.64..873.33 rows=531 width=75)
(actual time=0.416..24.779 rows=1680 loops=1)
Join Filter: (stock.contenant_id = contenant.id)
Rows Removed by Join Filter: 3360
Buffers: shared hit=6292
-> Seq Scan on contenant (cost=0.00..1.04 rows=1 width=16)
(actual time=0.014..0.018 rows=1 loops=1)
Filter: (libelle = 'bouteille'::text)
Rows Removed by Filter: 2
Buffers: shared hit=1
-> Nested Loop (cost=11.64..852.38 rows=1593 width=67)
(actual time=0.392..22.162 rows=5040 loops=1)
Buffers: shared hit=6291
-> Hash Join (cost=11.23..138.40 rows=106 width=55)
(actual time=0.366..5.717 rows=336 loops=1)

338
8. TYPES AVANCÉS

Hash Cond: (vin.recoltant_id = recoltant.id)


Buffers: shared hit=43
-> Hash Join (cost=10.07..135.78 rows=106 width=40)
(actual time=0.337..5.289 rows=336 loops=1)
Hash Cond: (vin.type_vin_id = type_vin.id)
Buffers: shared hit=42
-> Hash Join (cost=9.02..132.48 rows=319 width=39)
(actual time=0.322..4.714 rows=1006 loops=1)
Hash Cond: (vin.appellation_id = appellation.id)
Buffers: shared hit=41
-> Seq Scan on vin
(cost=0.00..97.53 rows=6053 width=16)
(actual time=0.011..1.384 rows=6053 loops=1)
Buffers: shared hit=37
-> Hash (cost=8.81..8.81 rows=17 width=31)
(actual time=0.299..0.299 rows=53 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 4kB
Buffers: shared hit=4
-> Hash Join
(cost=1.25..8.81 rows=17 width=31)
(actual time=0.033..0.257 rows=53 loops=1)
Hash Cond:
(appellation.region_id = region.id)
Buffers: shared hit=4
-> Seq Scan on appellation
(cost=0.00..6.19 rows=319 width=24)
(actual time=0.010..0.074 rows=319
loops=1)
Buffers: shared hit=3
-> Hash
(cost=1.24..1.24 rows=1 width=15)
(actual time=0.013..0.013 rows=1
loops=1)
Buckets: 1024 Batches: 1
Memory Usage: 1kB
Buffers: shared hit=1
-> Seq Scan on region
(cost=0.00..1.24 rows=1 width=15)
(actual time=0.005..0.012 rows=1
loops=1)
Filter: (libelle =
'Alsace'::text)
Rows Removed by Filter: 18
Buffers: shared hit=1
-> Hash (cost=1.04..1.04 rows=1 width=9)
(actual time=0.008..0.008 rows=1 loops=1)

339
https://dalibo.com/formations
SQL pour PostgreSQL

Buckets: 1024 Batches: 1 Memory Usage: 1kB


Buffers: shared hit=1
-> Seq Scan on type_vin
(cost=0.00..1.04 rows=1 width=9)
(actual time=0.005..0.007 rows=1 loops=1)
Filter: (libelle = 'blanc'::text)
Rows Removed by Filter: 2
Buffers: shared hit=1
-> Hash (cost=1.07..1.07 rows=7 width=23)
(actual time=0.017..0.017 rows=7 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 1kB
Buffers: shared hit=1
-> Seq Scan on recoltant
(cost=0.00..1.07 rows=7 width=23)
(actual time=0.004..0.009 rows=7 loops=1)
Buffers: shared hit=1
-> Index Scan using idx_stock_vin_annee on stock
(cost=0.42..6.59 rows=15 width=16)
(actual time=0.013..0.038 rows=15 loops=336)
Index Cond: (vin_id = vin.id)
Buffers: shared hit=6248
Planning time: 4.341 ms
Execution time: 25.232 ms
(53 lignes)

La requête sur le schéma normalisé est ici plus rapide. On constate tout de même qu’elle
accède à 6300 buffers, contre 1300 à la requête dénormalisée, soit 4 fois plus de don-
nées. Un test identique exécuté sur des données hors du cache donne environ 80 ms
pour la requête sur la table dénormalisée, contre près d’une seconde pour les tables nor-
malisées. Ce genre de transformation est très utile lorsque le schéma ne se prête pas à
une normalisation, et lorsque le volume de données à manipuler est trop important pour
tenir en mémoire. Les tables dénormalisées avec hstore se prêtent aussi bien mieux aux
recherches multi-critères.

8.7.2 JSONB

Pour chaque vin, le document JSON aura la structure suivante :


{
vin: {
recoltant: {
nom: text,
adressse: text
},
appellation: {

340
8. TYPES AVANCÉS

libelle: text,
region: text
},
type_vin: text
},
stocks: [{
contenant: {
contenance: real,
libelle: text
},
annee: integer,
nombre: integer
}]
}

La requête suivante permet de générer les parties vin du docu-


ment, avec recoltant et appellation.
Créer un document JSONB pour chaque ligne de vin grâce à la
fonction jsonb_build_object.

SELECT
recoltant.nom,
recoltant.adresse,
appellation.libelle,
region.libelle,
type_vin.libelle
FROM vin
INNER JOIN recoltant on vin.recoltant_id = recoltant.id
INNER JOIN appellation on vin.appellation_id = appellation.id
INNER JOIN region on region.id = appellation.region_id
INNER JOIN type_vin on vin.type_vin_id = type_vin.id;

SELECT
jsonb_build_object(
'recoltant',
json_build_object('nom', recoltant.nom, 'adresse',
recoltant.adresse
),
'appellation',
jsonb_build_object('libelle', appellation.libelle, 'region', region.libelle),
'type_vin', type_vin.libelle
)
FROM vin
INNER JOIN recoltant on vin.recoltant_id = recoltant.id
INNER JOIN appellation on vin.appellation_id = appellation.id
INNER JOIN region on region.id = appellation.region_id
341
https://dalibo.com/formations
SQL pour PostgreSQL

INNER JOIN type_vin on vin.type_vin_id = type_vin.id ;

Écrire une requête permettant de générer la partie stocks du


document, soit un document JSON pour chaque ligne de la table
stock, incluant le contenant.

La partie stocks du document est un peu plus compliquée, et nécessite l’utilisation de


fonctions d’agrégations.
SELECT json_build_object(
'contenant',
json_build_object('contenance', contenant.contenance, 'libelle',
contenant.libelle),
'annee', stock.annee,
'nombre', stock.nombre)
FROM stock join contenant on stock.contenant_id = contenant.id;

Pour un vin donné, le tableau stock ressemble à cela :


SELECT json_agg(json_build_object(
'contenant',
json_build_object('contenance', contenant.contenance, 'libelle',
contenant.libelle),
'annee', stock.annee,
'nombre', stock.nombre))
FROM stock
INNER JOIN contenant on stock.contenant_id = contenant.id
WHERE vin_id = 1
GROUP BY vin_id;

Fusionner les requêtes précédentes pour générer un document


complet pour chaque ligne de la table vin. Créer une table
stock_jsonb avec un unique champ JSONB rassemblant ces
documents.

On assemble les deux parties précédentes :


CREATE TABLE stock_jsonb AS (
SELECT
json_build_object(
'vin',
json_build_object(
'recoltant',
json_build_object('nom', recoltant.nom, 'adresse', recoltant.adresse),
'appellation',

342
8. TYPES AVANCÉS

json_build_object('libelle', appellation.libelle, 'region',


region.libelle),
'type_vin', type_vin.libelle),
'stocks',
json_agg(json_build_object(
'contenant',
json_build_object('contenance', contenant.contenance, 'libelle',
contenant.libelle),
'annee', stock.annee,
'nombre', stock.nombre)))::jsonb as document
FROM vin
INNER JOIN recoltant on vin.recoltant_id = recoltant.id
INNER JOIN appellation on vin.appellation_id = appellation.id
INNER JOIN region on region.id = appellation.region_id
INNER JOIN type_vin on vin.type_vin_id = type_vin.id
INNER JOIN stock on stock.vin_id = vin.id
INNER JOIN contenant on stock.contenant_id = contenant.id
GROUP BY vin_id, recoltant.id, region.id, appellation.id, type_vin.id
);

Calculer la taille de la table, et la comparer à la taille du reste de


la base.

La table avec JSON contient toutes les mêmes informations que l’ensemble des tables
normalisées de la base cave (à l’exception des id). Elle occupe en revanche une place
beaucoup moins importante, puisque les documents individuels vont pouvoir être com-
pressés en utilisant le mécanisme TOAST. De plus, on économise les 26 octets par ligne
de toutes les autres tables.

Elle est même plus petite que la seule table stock :


\d+
Liste des relations
Schéma | Nom | Type | Propriétaire | Persistence | Taille | Description
--------+--------------------+----------+--------------+-------------+------------+-------------

public | stock | table | caviste | permanent | 36 MB |
public | stock_jsonb | table | postgres | permanent | 12 MB |

Depuis cette nouvelle table, renvoyer l’ensemble des récoltants


de la région Beaujolais.

SELECT DISTINCT document #> '{vin, recoltant, nom}'


FROM stock_jsonb
WHERE document #>> '{vin, appellation, region}' = 'Beaujolais';
343
https://dalibo.com/formations
SQL pour PostgreSQL

Renvoyer l’ensemble des vins pour lesquels au moins une


bouteille entre 1992 et 1995 existe. (la difficulté est d’extraire
les différents stocks d’une même ligne de la table)

La fonction jsonb_array_elements permet de convertir les différents éléments du doc-


ument stocks en autant de lignes. La clause LATERAL permet de l’appeler une fois pour
chaque ligne :
SELECT DISTINCT document #> '{vin, recoltant, nom}'
FROM stock_jsonb,
LATERAL jsonb_array_elements(document #> '{stocks}') as stock
WHERE (stock->'annee')::text::integer BETWEEN 1992 AND 1995;

Indexer le document jsonb en utilisant un index de type GIN.

CREATE INDEX ON stock_jsonb USING gin (document jsonb_path_ops);

Peut-on réécrire les deux requêtes précédentes pour utiliser


l’index ?

Pour la première requête, on peut utiliser l’opérateur « contient » pour passer par l’index :
SELECT DISTINCT document #> '{vin, recoltant, nom}'
FROM stock_jsonb
WHERE document @> '{"vin": {"appellation": {"region": "Beaujolais"}}}';

La seconde ne peut malheureusement pas être réécrite pour tirer partie de l’index.

La dénormalisation vers un champs externe n’est pas vraiment possible, puisqu’il y a


plusieurs stocks par ligne.

8.7.3 LARGE OBJECTS

Créer une table fichiers avec un texte et une colonne permet-


tant 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',oid_retourné);

344
8. TYPES AVANCÉS

Importer un fichier du serveur à l’aide de psql dans un large ob-


ject.

psql -c "INSERT INTO fichiers SELECT 'postgresql.conf', \


lo_import('/var/lib/pgsql/11/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';"

345
https://dalibo.com/formations
SQL pour PostgreSQL

9 SQL POUR L'ANALYSE DE DONNÉES

9.1 PRÉAMBULE

• Analyser des données est facile avec PostgreSQL


– opérations d’agrégation disponibles
– fonctions OLAP avancées

9.1.1 MENU

• agrégation de données
• clause FILTER
• fonctions window
• GROUPING SETS, ROLLUP, CUBE
• WITHIN GROUPS

9.1.2 OBJECTIFS

• Écrire des requêtes encore plus complexes


• Analyser les données en amont
– pour ne récupérer que le résultat

9.2 AGRÉGATS

• SQL dispose de fonctions de calcul d’agrégats


• Utilité :
– calcul de sommes, moyennes, valeur minimale et maximale
– nombreuses fonctions statistiques disponibles

À l’aide des fonctions de calcul d’agrégats, on peut réaliser un certain nombre de calculs
permettant d’analyser les données d’une table.

La plupart des exemples utilisent une table employes définie telle que :

346
9. SQL POUR L’ANALYSE DE DONNÉES

CREATE TABLE employes (


matricule char(8) primary key,
nom text not null,
service text,
salaire numeric(7,2)
);

INSERT INTO employes (matricule, nom, service, salaire)


VALUES ('00000001', 'Dupuis', 'Direction', 10000.00);
INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000004', 'Fantasio', 'Courrier', 4500.00);
INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000006', 'Prunelle', 'Publication', 4000.00);
INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000020', 'Lagaffe', 'Courrier', 3000.00);
INSERT INTO employes (matricule, nom, service, salaire)
VALUES ('00000040', 'Lebrac', 'Publication', 3000.00);

SELECT * FROM employes ;


matricule | nom | service | salaire
-----------+----------+-------------+----------
00000001 | Dupuis | Direction | 10000.00
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
00000040 | Lebrac | Publication | 3000.00
(5 lignes)

Ainsi, on peut déduire le salaire moyen avec la fonction avg(), les salaires maximum et
minimum versés par la société avec les fonctions max() et min(), ainsi que la somme
totale des salaires versés avec la fonction sum() :

SELECT avg(salaire) AS salaire_moyen,


max(salaire) AS salaire_maximum,
min(salaire) AS salaire_minimum,
sum(salaire) AS somme_salaires
FROM employes;
salaire_moyen | salaire_maximum | salaire_minimum | somme_salaires
-----------------------+-----------------+-----------------+----------------
4900.0000000000000000 | 10000.00 | 3000.00 | 24500.00

La base de données réalise les calculs sur l’ensemble des données de la table et n’affiche
que le résultat du calcul.

Si l’on applique un filtre sur les données, par exemple pour ne prendre en compte que le
service Courrier, alors PostgreSQL réalise le calcul uniquement sur les données issues de
la lecture :
347
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT avg(salaire) AS salaire_moyen,


max(salaire) AS salaire_maximum,
min(salaire) AS salaire_minimum,
sum(salaire) AS somme_salaires
FROM employes
WHERE service = 'Courrier';
salaire_moyen | salaire_maximum | salaire_minimum | somme_salaires
-----------------------+-----------------+-----------------+----------------
3750.0000000000000000 | 4500.00 | 3000.00 | 7500.00
(1 ligne)

En revanche, il n’est pas possible de référencer d’autres colonnes pour les afficher à côté
du résultat d’un calcul d’agrégation à moins de les utiliser comme critère de regroupe-
ment :
SELECT avg(salaire), nom FROM employes;
ERROR: column "employes.nom" must appear in the GROUP BY clause or be used in
an aggregate function
LIGNE 1 : SELECT avg(salaire), nom FROM employes;
^

9.2.1 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

L’opérateur d’agrégat GROUP BY indique à la base de données que l’on souhaite regrouper
les données selon les mêmes valeurs d’une colonne.

348
9. SQL POUR L’ANALYSE DE DONNÉES

Des calculs pourront être réalisés sur les données agrégées selon le critère de regroupe-
ment donné. Le résultat sera alors représenté en n’affichant que les colonnes de regroupe-
ment puis les valeurs calculées par les fonctions d’agrégation :

9.2.2 GROUP BY : PRINCIPE

349
https://dalibo.com/formations
SQL pour PostgreSQL

L’agrégation est ici réalisée sur la colonne service. En guise de calcul d’agrégation, une
somme est réalisée sur les salaires payés dans chaque service.

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

SQL permet depuis le début de réaliser des calculs d’agrégation. Pour cela, la base de
données observe les critères de regroupement définis dans la clause GROUP BY de la re-
quête et effectue l’opération sur l’ensemble des lignes qui correspondent au critère de
regroupement.

On peut bien entendu combiner plusieurs opérations d’agrégations :


SELECT service,
sum(salaire) salaires_par_service,
avg(salaire) AS salaire_moyen_service
FROM employes
GROUP BY service;

service | salaires_par_service | salaire_moyen_service


-------------+----------------------+------------------------
Courrier | 7500.00 | 3750.0000000000000000
Direction | 10000.00 | 10000.0000000000000000
Publication | 7000.00 | 3500.0000000000000000
(3 lignes)

On peut combiner le résultat de deux requêtes d’agrégation avec UNION ALL, si les en-
sembles retournées sont de même type :
SELECT service,
sum(salaire) AS salaires_par_service
FROM employes GROUP BY service
UNION ALL
SELECT 'Total' AS service,

350
9. SQL POUR L’ANALYSE DE DONNÉES

sum(salaire) AS salaires_par_service
FROM employes;

service | salaires_par_service
-------------+----------------------
Courrier | 7500.00
Direction | 10000.00
Publication | 7000.00
Total | 24500.00
(4 lignes)

On le verra plus loin, cette dernière requête peut être écrite plus simplement avec les
GROUPING SETS, mais qui nécessitent au minimum PostgreSQL 9.5.

9.2.4 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

Les fonctions array_agg, string_agg et xmlagg permettent d’agréger des éléments dans
un tableau, dans une chaîne ou dans une arborescence XML. Autant l’ordre dans lequel
les données sont utilisées n’a pas d’importance lorsque l’on réalise un calcul d’agrégat
classique, autant cet ordre va influencer la façon dont les données seront produites par
les trois fonctions citées plus haut. En effet, le tableau généré par array_agg est composé
d’éléments ordonnés, de même que la chaîne de caractères ou l’arborescence XML.

9.2.5 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
351
https://dalibo.com/formations
SQL pour PostgreSQL

(3 lignes)

La requête suivante permet d’obtenir, pour chaque service, la liste des employés dans un
tableau, trié par ordre alphabétique :
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)

Il est possible de réaliser la même chose mais pour obtenir un tableau plutôt qu’une chaîne
de caractère :
SELECT service,
array_agg(nom ORDER BY nom) AS liste_employes
FROM employes
GROUP BY service;
service | liste_employes
-------------+--------------------
Courrier | {Fantasio,Lagaffe}
Direction | {Dupuis}
Publication | {Lebrac,Prunelle}

9.3 CLAUSE FILTER

• Clause FILTER
• Utilité :
– filtrer les données sur les agrégats
– évite les expressions CASE complexes
• SQL:2003
• Intégré dans la version 9.4

La clause FILTER permet de remplacer des expressions complexes écrites avec CASE et
donc de simplifier l’écriture de requêtes réalisant un filtrage dans une fonction d’agrégat.

352
9. SQL POUR L’ANALYSE DE DONNÉES

9.3.1 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);

Avec cette syntaxe, dès que l’on a besoin d’avoir de multiples filtres ou de filtres plus
complexes, la requête devient très rapidement peu lisible et difficile à maintenir. Le risque
d’erreur est également élevé.

9.3.2 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);

L’exemple suivant montre l’utilisation de la clause FILTER et son équivalent écrit avec une
expression CASE :
sql=# SELECT count(*) AS compte_pays,
count(*) FILTER (WHERE r.nom_region='Europe') AS compte_pays_europeens,
count(CASE WHEN r.nom_region='Europe' THEN 1 END)
AS oldschool_compte_pays_europeens
FROM pays p
JOIN regions r
ON (p.region_id = r.region_id);
compte_pays | compte_pays_europeens | oldschool_compte_pays_europeens
-------------+-----------------------+---------------------------------
25 | 5 | 5
(1 ligne)

353
https://dalibo.com/formations
SQL pour PostgreSQL

9.4 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

PostgreSQL supporte les fonctions de fenêtrage depuis la version 8.4. Elles apportent
des fonctionnalités analytiques à PostgreSQL, et permettent d’écrire beaucoup plus sim-
plement certaines requêtes.

Prenons un exemple.

SELECT service, AVG(salaire)


FROM employe
GROUP BY service

SELECT service, id_employe, salaire,


AVG(salaire) OVER (
PARTITION BY service
ORDER BY age
ROWS BETWEEN 2 PRECEEDING AND 2 FOLLOWING
)
FROM employes

354
9. SQL POUR L’ANALYSE DE DONNÉES

9.4.1 REGROUPEMENT

• Regroupement
– clause OVER (PARTITION BY ...)
• Utilité :
– plusieurs critères de regroupement différents
– avec des fonctions de calcul d’agrégats

La clause OVER permet de définir la façon dont les données sont regroupées - uniquement
pour la colonne définie - avec la clause PARTITION BY.

Les exemples vont utiliser cette table employes :


exemple=# SELECT * FROM employes ;
matricule | nom | service | salaire
-----------+----------+-------------+----------
00000001 | Dupuis | Direction | 10000.00
00000004 | Fantasio | Courrier | 4500.00
00000006 | Prunelle | Publication | 4000.00
00000020 | Lagaffe | Courrier | 3000.00
00000040 | Lebrac | Publication | 3000.00
(5 lignes)

355
https://dalibo.com/formations
SQL pour PostgreSQL

9.4.2 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

Les calculs réalisés par cette requête sont identiques à ceux réalisés avec une agrégation
utilisant GROUP BY. La principale différence est que l’on évite de ici de perdre le détail des
données tout en disposant des données agrégées dans le résultat de la requête.

9.4.3 REGROUPEMENT : PRINCIPE

SUM(salaire) OVER (PARTITION BY service)

Entouré de noir, le critère de regroupement et entouré de rouge, les données sur


lesquelles sont appliqués le calcul d’agrégat.

356
9. SQL POUR L’ANALYSE DE DONNÉES

9.4.4 REGROUPEMENT : SYNTAXE

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

Le terme PARTITION BY permet d’indiquer les critères de regroupement de la fenêtre sur


laquelle on souhaite travailler.

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

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

La fonction row_number() permet de numéroter les lignes selon un critère de tri défini
dans la clause OVER.

L’ordre de tri de la clause OVER n’influence pas l’ordre de tri explicite d’une requête :
357
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT row_number() OVER (ORDER BY matricule),


matricule, nom
FROM employes
ORDER BY nom;
row_number | matricule | nom
------------+-----------+----------
1 | 00000001 | Dupuis
2 | 00000004 | Fantasio
4 | 00000020 | Lagaffe
5 | 00000040 | Lebrac
3 | 00000006 | Prunelle
(5 lignes)

On dispose aussi de fonctions de classement, pour déterminer par exemple les employés
les moins bien payés :
SELECT matricule, nom, salaire, service,
rank() OVER (ORDER BY salaire),
dense_rank() OVER (ORDER BY salaire)
FROM employes ;
matricule | nom | salaire | service | rank | dense_rank
-----------+----------+----------+-------------+------+------------
00000020 | Lagaffe | 3000.00 | Courrier | 1 | 1
00000040 | Lebrac | 3000.00 | Publication | 1 | 1
00000006 | Prunelle | 4000.00 | Publication | 3 | 2
00000004 | Fantasio | 4500.00 | Courrier | 4 | 3
00000001 | Dupuis | 10000.00 | Direction | 5 | 4
(5 lignes)

La fonction de fenêtrage rank() renvoie le classement en autorisant des trous dans la


numérotation, et dense_rank() le classement sans trous.

9.4.7 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

358
9. SQL POUR L’ANALYSE DE DONNÉES

00000040 | 3000.00 | 24500.00

9.4.8 TRI : PRINCIPE

SUM(salaire) OVER (ORDER BY matricule)

Lorsque l’on utilise une clause de tri, la portion de données visible par l’opérateur
d’agrégat correspond aux données comprises entre la première ligne examinée et la ligne
courante. La fenêtre est définie selon le critère RANGE BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW.

Nous verrons plus loin que nous pouvons modifier ce comportement.

359
https://dalibo.com/formations
SQL pour PostgreSQL

9.4.9 TRI : SYNTAXE

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

Le terme ORDER BY permet d’indiquer les critères de tri de la fenêtre sur laquelle on
souhaite travailler.

9.4.10 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

Il est possible de combiner les clauses de fenêtrage PARTITION BY et ORDER BY. Cela
permet d’isoler des jeux de données entre eux avec la clause PARTITION BY, tout en ap-
pliquant un critère de tri avec la clause ORDER BY. Beaucoup d’applications sont possibles
si l’on associe à cela les nombreuses fonctions analytiques disponibles.

9.4.11 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
(...)

360
9. SQL POUR L’ANALYSE DE DONNÉES

Si l’on applique les deux clauses PARTITION BY et ORDER BY à une fonction de fenêtrage,
alors le critère de tri est appliqué dans la partition et chaque partition est indépendante
l’une de l’autre.

Voici un extrait plus complet du résultat de la requête présentée ci-dessus :

continent | pays | population | rang_pop


---------------------------+--------------------+------------+-----------
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
Afrique | Afrique du Sud | 52.8 | 5
Afrique | Tanzanie | 49.3 | 6
Afrique | Kenya | 44.4 | 7
Afrique | Algérie | 39.2 | 8
Afrique | Ouganda | 37.6 | 9
Afrique | Maroc | 33.0 | 10
Afrique | Ghana | 25.9 | 11
Afrique | Mozambique | 25.8 | 12
Afrique | Madagascar | 22.9 | 13
Afrique | Côte-d'Ivoire | 20.3 | 14
Afrique | Niger | 17.8 | 15
Afrique | Burkina Faso | 16.9 | 16
Afrique | Zimbabwe | 14.1 | 17
Afrique | Soudan | 14.1 | 17
Afrique | Tunisie | 11.0 | 19
Amérique du Nord | États-Unis | 320.1 | 1
Amérique du Nord | Canada | 35.2 | 2
Amérique latine. Caraïbes | Brésil | 200.4 | 1
Amérique latine. Caraïbes | Mexique | 122.3 | 2
Amérique latine. Caraïbes | Colombie | 48.3 | 3
Amérique latine. Caraïbes | Argentine | 41.4 | 4
Amérique latine. Caraïbes | Pérou | 30.4 | 5
Amérique latine. Caraïbes | Venezuela | 30.4 | 5
Amérique latine. Caraïbes | Chili | 17.6 | 7
Amérique latine. Caraïbes | Équateur | 15.7 | 8
Amérique latine. Caraïbes | Guatemala | 15.5 | 9
Amérique latine. Caraïbes | Cuba | 11.3 | 10
(...)
361
https://dalibo.com/formations
SQL pour PostgreSQL

9.4.12 REGROUPEMENT ET TRI : PRINCIPE

OVER (PARTITION BY continent


ORDER BY population DESC)

9.4.13 REGROUPEMENT ET TRI : SYNTAXE

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

Cette construction ne pose aucune difficulté syntaxique. La norme impose de placer la


clause PARTITION BY avant la clause ORDER BY, c’est la seule chose à retenir au niveau
de la syntaxe.

362
9. SQL POUR L’ANALYSE DE DONNÉES

9.4.14 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

Sans les fonctions analytiques, il était difficile en SQL d’écrire des requêtes nécessitant
de faire appel à des données provenant d’autres lignes que la ligne courante.

Par exemple, pour renvoyer la liste détaillée de tous les employés ET le salaire le plus
élevé du service auquel il appartient, on peut utiliser la fonction first_value() :
SELECT matricule, nom, salaire, service,
first_value(salaire) OVER (PARTITION BY service ORDER BY salaire DESC)
AS salaire_maximum_service
FROM employes ;
matricule | nom | salaire | service | salaire_maximum_service
-----------+----------+----------+-------------+-------------------------
00000004 | Fantasio | 4500.00 | Courrier | 4500.00
00000020 | Lagaffe | 3000.00 | Courrier | 4500.00
00000001 | Dupuis | 10000.00 | Direction | 10000.00
00000006 | Prunelle | 4000.00 | Publication | 4000.00
00000040 | Lebrac | 3000.00 | Publication | 4000.00
(5 lignes)

Il existe également les fonctions suivantes :

• last_value(colonne) : renvoie la dernière valeur pour la colonne ;


• nth(colonne, n) : renvoie la n-ème valeur (en comptant à partir de 1) pour la
colonne ;
• lag(colonne, n) : renvoie la valeur située en n-ème position avant la ligne en
cours pour la colonne ;
• lead(colonne, n) : renvoie la valeur située en n-ème position après la ligne en
cours pour la colonne ;
– pour ces deux fonctions, le n est facultatif et vaut 1 par défaut ;
– ces deux fonctions acceptent un 3ème argument facultatif spécifiant la valeur
à renvoyer si aucune valeur n’est trouvée en n-ème position avant ou après.
Par défaut, NULL sera renvoyé.

363
https://dalibo.com/formations
SQL pour PostgreSQL

9.4.15 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

La construction lead(colonne) est équivalente à lead(colonne, 1). De même, la con-


struction lag(colonne) est équivalente à lag(colonne, 1). Il s’agit d’un raccourci pour
utiliser la valeur précédente ou la valeur suivante d’une colonne dans la fenêtre définie.

9.4.16 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

La requête présentée en exemple ne s’appuie que sur un jeu réduit de données afin de
montrer un résultat compréhensible.

9.4.17 LEAD() ET LAG() : PRINCIPE

lag(population) OVER (PARTITION BY continent


ORDER BY population DESC)

364
9. SQL POUR L’ANALYSE DE DONNÉES

NULL est renvoyé lorsque la valeur n’est pas accessible dans la fenêtre de données, comme
par exemple si l’on souhaite utiliser la valeur d’une colonne appartenant à la ligne précé-
dant la première ligne de la partition.

9.4.18 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

Utilisé avec ORDER BY et PARTITION BY, la fonction first_value() permet par exemple
d’obtenir le salaire le plus élevé d’un service :
SELECT matricule, nom, salaire, service,
first_value(salaire) OVER (PARTITION BY service ORDER BY salaire DESC)
AS salaire_maximum_service
FROM employes ;

matricule | nom | salaire | service | salaire_maximum_service


-----------+----------+----------+-------------+-------------------------
00000004 | Fantasio | 4500.00 | Courrier | 4500.00
00000020 | Lagaffe | 3000.00 | Courrier | 4500.00
00000001 | Dupuis | 10000.00 | Direction | 10000.00
00000006 | Prunelle | 4000.00 | Publication | 4000.00
00000040 | Lebrac | 3000.00 | Publication | 4000.00
(5 lignes)

9.4.19 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


-----------------+-----------+------------+-------------
365
https://dalibo.com/formations
SQL pour PostgreSQL

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
Lorsque que la clause ORDER BY est utilisée pour définir une fenêtre, la fenêtre visible
depuis la ligne courante commence par défaut à la première ligne de résultat et s’arrête
à la ligne courante.

Par exemple, si l’on exécute la même requête en utilisant last_value() plutôt que
first_value(), on récupère à chaque fois la valeur de la colonne sur la ligne courante :
SELECT pays, continent, population,
last_value(population) OVER (PARTITION BY continent
ORDER BY population DESC)
FROM population;

pays | continent | population | last_value


-----------------------+-----------+------------+------------
Chine | Asie | 1385.6 | 1385.6
Iraq | Asie | 33.8 | 33.8
Ouzbékistan | Asie | 28.9 | 28.9
Arabie Saoudite | Asie | 28.8 | 28.8
France métropolitaine | Europe | 64.3 | 64.3
Finlande | Europe | 5.4 | 5.4
Lettonie | Europe | 2.1 | 2.1
(7 rows)

Il est alors nécessaire de redéfinir le comportement de la fenêtre visible pour que la fonc-
tion se comporte comme attendu, en utilisant RANGE BETWEEN UNBOUNDED PRECEDING
AND UNBOUNDED FOLLOWING - cet aspect sera décrit dans la section sur les possibilités de
modification de la définition de la fenêtre.

366
9. SQL POUR L’ANALYSE DE DONNÉES

9.4.20 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);

Il arrive que l’on ait besoin d’utiliser plusieurs fonctions de fenêtrage au sein d’une même
requête qui utilisent la même définition de fenêtre (même clause PARTITION BY et/ou
ORDER BY). Afin d’éviter de dupliquer cette clause, il est possible de définir une fenêtre
nommée et de l’utiliser à plusieurs endroits de la requête. Par exemple, l’exemple précé-
dent des fonctions de classement pourrait s’écrire :
SELECT matricule, nom, salaire, service,
rank() OVER w,
dense_rank() OVER w
FROM employes
WINDOW w AS (ORDER BY salaire);
matricule | nom | salaire | service | rank | dense_rank
-----------+----------+----------+-------------+------+------------
00000020 | Lagaffe | 3000.00 | Courrier | 1 | 1
00000040 | Lebrac | 3000.00 | Publication | 1 | 1
00000006 | Prunelle | 4000.00 | Publication | 3 | 2
00000004 | Fantasio | 4500.00 | Courrier | 4 | 3
00000001 | Dupuis | 10000.00 | Direction | 5 | 4
(5 lignes)

À noter qu’il est possible de définir de multiples définitions de fenêtres au sein d’une
même requête, et qu’une définition de fenêtre peut surcharger la clause ORDER BY si la
définition parente ne l’a pas définie. Par exemple, la requête SQL suivante est correcte :
SELECT matricule, nom, salaire, service,
rank() OVER w_asc,
dense_rank() OVER w_desc
FROM employes
WINDOW w AS (PARTITION BY service),
w_asc AS (w ORDER BY salaire),
w_desc AS (w ORDER BY salaire DESC);

367
https://dalibo.com/formations
SQL pour PostgreSQL

9.4.21 CLAUSE WINDOW : SYNTAXE

SELECT fonction_agregat OVER nom,


fonction_agregat_2 OVER nom ...
...
FROM <liste_tables>
WHERE <predicats>
WINDOW nom AS (PARTITION BY ... ORDER BY ...)

9.4.22 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

9.4.23 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

368
9. SQL POUR L’ANALYSE DE DONNÉES

9.4.24 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

9.4.25 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

Ceci n’est disponible que depuis la version 11.

369
https://dalibo.com/formations
SQL pour PostgreSQL

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

Ceci n’est disponible que depuis la version 11.

9.4.27 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

370
9. SQL POUR L’ANALYSE DE DONNÉES

9.5 WITHIN GROUP

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

La clause WITHIN GROUP est une nouvelle clause pour les agrégats utilisant des fonctions
dont les données doivent être triées. Quelques fonctions ont été ajoutées pour profiter
au mieux de cette nouvelle clause.

9.5.1 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

Cet exemple permet d’afficher le continent, la médiane de la population par continent et


la population du pays le moins peuplé parmi les 5% de pays les plus peuplés de chaque
continent.

Pour rappel, la table contient les données suivantes :


postgres=# SELECT * FROM population ORDER BY continent, population;
pays | population | superficie | densite | continent
-----------------------+------------+------------+---------+----------
Tunisie | 11.0 | 164 | 67 | Afrique
Zimbabwe | 14.1 | 391 | 36 | Afrique
Soudan | 14.1 | 197 | 72 | Afrique
Burkina Faso | 16.9 | 274 | 62 | Afrique
(...)
371
https://dalibo.com/formations
SQL pour PostgreSQL

En ajoutant le support de cette clause, PostgreSQL améliore son support de la norme SQL
2008 et permet le développement d’analyses statistiques plus élaborées.

9.6 GROUPING SETS

• GROUPING SETS/ROLLUP/CUBE
• Extension de GROUP BY
• PostgreSQL 9.5
• 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

Les GROUPING SETS permettent de définir plusieurs clauses d’agrégation GROUP BY. Les ré-
sultats seront présentés comme si plusieurs requêtes d’agrégation avec les clauses GROUP
BY mentionnées étaient assemblées avec UNION ALL.

9.6.1 GROUPING SETS : JEU DE DONNÉES

CREATE TABLE stock AS SELECT * FROM (


VALUES ('ecrous', 'est', 50),
('ecrous', 'ouest', 0),
('ecrous', 'sud', 40),

372
9. SQL POUR L’ANALYSE DE DONNÉES

('clous', 'est', 70),


('clous', 'nord', 0),
('vis', 'ouest', 50),
('vis', 'sud', 50),
('vis', 'nord', 60)
) AS VALUES(piece, region, quantite);

9.6.2 GROUPING SETS : EXEMPLE VISUEL

9.6.3 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

373
https://dalibo.com/formations
SQL pour PostgreSQL

9.6.4 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;

Le comportement de la clause GROUPING SETS peut être émulée avec deux requêtes util-
isant chacune une clause GROUP BY sur les colonnes de regroupement souhaitées.

Cependant, le plan d’exécution de la requête équivalente conduit à deux lectures et peut


être particulièrement coûteux si le jeu de données sur lequel on souhaite réaliser les agré-
gations est important :

EXPLAIN SELECT piece,NULL as region,sum(quantite)


FROM stock
GROUP BY piece
UNION ALL
SELECT NULL, region,sum(quantite)
FROM STOCK
GROUP BY region;

QUERY PLAN
-------------------------------------------------------------------------
Append (cost=1.12..2.38 rows=7 width=44)
-> HashAggregate (cost=1.12..1.15 rows=3 width=45)
Group Key: stock.piece
-> Seq Scan on stock (cost=0.00..1.08 rows=8 width=9)
-> HashAggregate (cost=1.12..1.16 rows=4 width=44)
Group Key: stock_1.region
-> Seq Scan on stock stock_1 (cost=0.00..1.08 rows=8 width=8)

La requête utilisant la clause GROUPING SETS propose un plan bien plus efficace :

EXPLAIN SELECT piece,region,sum(quantite)


FROM stock GROUP BY GROUPING SETS (piece,region);
QUERY PLAN
------------------------------------------------------------------
GroupAggregate (cost=1.20..1.58 rows=14 width=17)
Group Key: piece
Sort Key: region
Group Key: region

374
9. SQL POUR L’ANALYSE DE DONNÉES

-> Sort (cost=1.20..1.22 rows=8 width=13)


Sort Key: piece
-> Seq Scan on stock (cost=0.00..1.08 rows=8 width=13)

9.6.5 ROLLUP

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

La clause ROLLUP est une fonctionnalité d’analyse type OLAP du langage SQL. Elle s’utilise
dans la clause GROUP BY, tout comme GROUPING SETS

9.6.6 ROLLUP : EXEMPLE VISUEL

375
https://dalibo.com/formations
SQL pour PostgreSQL

9.6.7 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));

Sur une requête un peu plus intéressante, effectuant des statistiques sur des ventes :

SELECT type_client, code_pays, SUM(quantite*prix_unitaire) AS montant


FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients cl
ON (c.client_id = cl.client_id)
JOIN contacts co
ON (cl.contact_id = co.contact_id)
WHERE date_commande BETWEEN '2014-01-01' AND '2014-12-31'
GROUP BY ROLLUP (type_client, code_pays);

Elle produit le résultat suivant :

type_client | code_pays | montant


-------------+-----------+---------------
A | CA | 6273168.32
A | CN | 7928641.50
A | DE | 6642061.57
A | DZ | 6404425.16
A | FR | 55261295.52
A | IN | 7224008.95
A | PE | 7356239.93
A | RU | 6766644.98
A | US | 7700691.07
A | | 111557177.00
(...)
P | RU | 287605812.99
P | US | 296424154.49
P | | 4692152751.08
| | 5217862160.65

Une fonction GROUPING, associée à ROLLUP, permet de déterminer si la ligne courante


correspond à un regroupement donné. Elle est de la forme d’un masque de bit converti
au format décimal :

SELECT row_number()
OVER ( ORDER BY grouping(piece,region)) AS ligne,

376
9. SQL POUR L’ANALYSE DE DONNÉES

grouping(piece,region)::bit(2) AS g,
piece,
region,
sum(quantite)
FROM stock
GROUP BY CUBE (piece,region)
ORDER BY g ;

ligne | g | piece | region | sum


-------+----+--------+--------+-----
1 | 00 | clous | est | 150
2 | 00 | clous | nord | 10
3 | 00 | ecrous | est | 110
4 | 00 | ecrous | ouest | 10
5 | 00 | ecrous | sud | 90
6 | 00 | vis | nord | 130
7 | 00 | vis | ouest | 110
8 | 00 | vis | sud | 110
9 | 01 | vis | | 350
10 | 01 | ecrous | | 210
11 | 01 | clous | | 160
12 | 10 | | ouest | 120
13 | 10 | | sud | 200
14 | 10 | | est | 260
15 | 10 | | nord | 140
16 | 11 | | | 720

Voici un autre exemple :

SELECT COALESCE(service,
CASE
WHEN GROUPING(service) = 0 THEN 'Unknown' ELSE 'Total'
END) AS service,
sum(salaire) AS salaires_service, count(*) AS nb_employes
FROM employes
GROUP BY ROLLUP (service);
service | salaires_service | nb_employes
-------------+------------------+-------------
Courrier | 7500.00 | 2
Direction | 50000.00 | 1
Publication | 7000.00 | 2
Total | 64500.00 | 5
(4 rows)

Ou appliqué à l’exemple un peu plus complexe :

SELECT COALESCE(type_client,
CASE
377
https://dalibo.com/formations
SQL pour PostgreSQL

WHEN GROUPING(type_client) = 0 THEN 'Unknown' ELSE 'Total'


END) AS type_client,
COALESCE(code_pays,
CASE
WHEN GROUPING(code_pays) = 0 THEN 'Unknown' ELSE 'Total'
END) AS code_pays,
SUM(quantite*prix_unitaire) AS montant
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients cl
ON (c.client_id = cl.client_id)
JOIN contacts co
ON (cl.contact_id = co.contact_id)
WHERE date_commande BETWEEN '2014-01-01' AND '2014-12-31'
GROUP BY ROLLUP (type_client, code_pays);

type_client | code_pays | montant


-------------+-----------+---------------
A | CA | 6273168.32
A | CN | 7928641.50
A | DE | 6642061.57
A | DZ | 6404425.16
A | FR | 55261295.52
A | IN | 7224008.95
A | PE | 7356239.93
A | RU | 6766644.98
A | US | 7700691.07
A | Total | 111557177.00
(...)
P | US | 296424154.49
P | Total | 4692152751.08
Total | Total | 5217862160.65

378
9. SQL POUR L’ANALYSE DE DONNÉES

9.6.8 CUBE

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

La clause CUBE est une autre fonctionnalité d’analyse type OLAP du langage SQL. Tout
comme ROLLUP, elle s’utilise dans la clause GROUP BY.

9.6.9 CUBE : EXEMPLE VISUEL

9.6.10 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)
);
379
https://dalibo.com/formations
SQL pour PostgreSQL

Elle permet de réaliser des regroupements sur l’ensemble des combinaisons possibles des
clauses de regroupement indiquées. Pour de plus amples détails, se référer à cet article
Wikipédia111 .

En reprenant la requête de l’exemple précédent :

SELECT type_client,
code_pays,
SUM(quantite*prix_unitaire) AS montant
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients cl
ON (c.client_id = cl.client_id)
JOIN contacts co
ON (cl.contact_id = co.contact_id)
WHERE date_commande BETWEEN '2014-01-01' AND '2014-12-31'
GROUP BY CUBE (type_client, code_pays);

Elle retournera le résultat suivant :

type_client | code_pays | montant


-------------+-----------+---------------
A | CA | 6273168.32
A | CN | 7928641.50
A | DE | 6642061.57
A | DZ | 6404425.16
A | FR | 55261295.52
A | IN | 7224008.95
A | PE | 7356239.93
A | RU | 6766644.98
A | US | 7700691.07
A | | 111557177.00
E | CA | 28457655.81
E | CN | 25537539.68
E | DE | 25508815.68
E | DZ | 24821750.17
E | FR | 209402443.24
E | IN | 26788642.27
E | PE | 24541974.54
E | RU | 25397116.39
E | US | 23696294.79
111
https://en.wikipedia.org/wiki/OLAP_cube

380
9. SQL POUR L’ANALYSE DE DONNÉES

E | | 414152232.57
P | CA | 292975985.52
P | CN | 287795272.87
P | DE | 287337725.21
P | DZ | 302501132.54
P | FR | 2341977444.49
P | IN | 295256262.73
P | PE | 300278960.24
P | RU | 287605812.99
P | US | 296424154.49
P | | 4692152751.08
| | 5217862160.65
| CA | 327706809.65
| CN | 321261454.05
| DE | 319488602.46
| DZ | 333727307.87
| FR | 2606641183.25
| IN | 329268913.95
| PE | 332177174.71
| RU | 319769574.36
| US | 327821140.35

Dans ce genre de contexte, lorsque le regroupement est réalisé sur l’ensemble des valeurs
d’un critère de regroupement, alors la valeur qui apparaît est NULL pour la colonne cor-
respondante. Si la colonne possède des valeurs NULL légitimes, il est alors difficile de
les distinguer. On utilise alors la fonction GROUPING() qui permet de déterminer si le re-
groupement porte sur l’ensemble des valeurs de la colonne. L’exemple suivant montre
une requête qui exploite cette fonction :

SELECT GROUPING(type_client,code_pays)::bit(2),
GROUPING(type_client)::boolean g_type_cli,
GROUPING(code_pays)::boolean g_code_pays,
type_client,
code_pays,
SUM(quantite*prix_unitaire) AS montant
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients cl
ON (c.client_id = cl.client_id)
JOIN contacts co
ON (cl.contact_id = co.contact_id)
381
https://dalibo.com/formations
SQL pour PostgreSQL

WHERE date_commande BETWEEN '2014-01-01' AND '2014-12-31'


GROUP BY CUBE (type_client, code_pays);

Elle produit le résultat suivant :

grouping | g_type_cli | g_code_pays | type_client | code_pays | montant


----------+------------+-------------+-------------+-----------+---------------
00 | f | f | A | CA | 6273168.32
00 | f | f | A | CN | 7928641.50
00 | f | f | A | DE | 6642061.57
00 | f | f | A | DZ | 6404425.16
00 | f | f | A | FR | 55261295.52
00 | f | f | A | IN | 7224008.95
00 | f | f | A | PE | 7356239.93
00 | f | f | A | RU | 6766644.98
00 | f | f | A | US | 7700691.07
01 | f | t | A | | 111557177.00
(...)
01 | f | t | P | | 4692152751.08
11 | t | t | | | 5217862160.65
10 | t | f | | CA | 327706809.65
10 | t | f | | CN | 321261454.05
10 | t | f | | DE | 319488602.46
10 | t | f | | DZ | 333727307.87
10 | t | f | | FR | 2606641183.25
10 | t | f | | IN | 329268913.95
10 | t | f | | PE | 332177174.71
10 | t | f | | RU | 319769574.36
10 | t | f | | US | 327821140.35
(40 rows)

L’application sera alors à même de gérer la présentation des résultats en fonction des
valeurs de grouping ou g_type_client et g_code_pays.

382
9. SQL POUR L’ANALYSE DE DONNÉES

9.7 TRAVAUX PRATIQUES

Le schéma brno2015 dispose d’une table pilotes ainsi que les résultats tour par tour de la
course de MotoGP de Brno (CZ) de la saison 2015.

La table brno2015 indique pour chaque tour, pour chaque pilote, le temps réalisé dans le
tour :

Table "public.brno_2015"
Column | Type | Modifiers
-----------+----------+-----------
no_tour | integer |
no_pilote | integer |
lap_time | interval |

Une table pilotes permet de connaître les détails d’un pilote :

Table "public.pilotes"
Column | Type | Modifiers
-------------+---------+-----------
no | integer |
nom | text |
nationalite | text |
ecurie | text |
moto | text |

Précisions sur les données à manipuler : la course est réalisée en plusieurs tours; certains
coureurs n’ont pas terminé la course, leur relevé de tours s’arrête donc brutalement.

Agrégation

1. Quel est le pilote qui a le moins gros écart entre son meilleur tour et son moins bon
tour ?

2. Déterminer quel est le pilote le plus régulier (écart-type).

Window Functions

3. Afficher la place sur le podium pour chaque coureur.

4. À partir de la requête précédente, afficher également la différence du temps de


chaque coureur par rapport à celui de la première place.

5. Pour chaque tour, afficher :

• le nom du pilote ;
• son rang dans le tour ;
383
https://dalibo.com/formations
SQL pour PostgreSQL

• son temps depuis le début de la course ;


• dans le tour, la différence de temps par rapport au premier.

6. Pour chaque coureur, quel est son meilleur tour et quelle place avait-il sur ce tour ?

7. Déterminer quels sont les coureurs ayant terminé la course qui ont gardé la même
position tout au long de la course.

8. En quelle position a terminé le coureur qui a doublé le plus de personnes ? Combien


de personnes a-t-il doublées ?

Grouping Sets

Ce TP nécessite PostgreSQL 9.5 ou supérieur. Il s’appuie sur les tables présentes dans le
schéma magasin.

9. En une seule requête, afficher le montant total des commandes par année et pays
et le montant total des commandes uniquement par année.

10. Ajouter également le montant total des commandes depuis le début de l’activité.

11. Ajouter également le montant total des commandes par pays.

384
9. SQL POUR L’ANALYSE DE DONNÉES

9.8 TRAVAUX PRATIQUES (SOLUTIONS)

Le schéma brno2015 dispose d’une table pilotes ainsi que les résultats tour par tour de la
course de MotoGP de Brno (CZ) de la saison 2015.

La table brno2015 indique pour chaque tour, pour chaque pilote, le temps réalisé dans le
tour :

Table "public.brno_2015"
Column | Type | Modifiers
-----------+----------+-----------
no_tour | integer |
no_pilote | integer |
lap_time | interval |

Une table pilotes permet de connaître les détails d’un pilote :

Table "public.pilotes"
Column | Type | Modifiers
-------------+---------+-----------
no | integer |
nom | text |
nationalite | text |
ecurie | text |
moto | text |

Précisions sur les données à manipuler : la course est réalisée en plusieurs tours; certains
coureurs n’ont pas terminé la course, leur relevé de tours s’arrête donc brutalement.

Agrégation

Tout d’abord, nous positionnons le search_path pour chercher les objets du schéma
brno2015 :

SET search_path = brno2015;

1. Quel est le pilote qui a le moins gros écart entre son meilleur tour et son moins bon
tour ?

Le coureur :

SELECT nom, max(lap_time) - min(lap_time) as ecart


FROM brno_2015
JOIN pilotes
ON (no_pilote = no)
GROUP BY 1
385
https://dalibo.com/formations
SQL pour PostgreSQL

ORDER BY 2
LIMIT 1;

La requête donne le résultat suivant :

nom | ecart
-----------------+--------------
Jorge LORENZO | 00:00:04.661
(1 row)

2. Déterminer quel est le pilote le plus régulier (écart-type).

Nous excluons le premier tour car il s’agit d’une course avec départ arrêté, donc ce tour
est plus lent que les autres, ici d’au moins 8 secondes :
SELECT nom, stddev(extract (epoch from lap_time)) as stddev
FROM brno_2015
JOIN pilotes
ON (no_pilote = no)
WHERE no_tour > 1
GROUP BY 1
ORDER BY 2
LIMIT 1;

Le résultat montre le coureur qui a abandonné en premier :

nom | stddev
-----------------+-------------------
Alex DE ANGELIS | 0.130107647741847
(1 row)

On s’aperçoit qu’Alex De Angelis n’a pas terminé la course. Il semble donc plus intéressant
de ne prendre en compte que les pilotes qui ont terminé la course et toujours en excluant
le premier tour (il y a 22 tours sur cette course, on peut le positionner soit en dur dans
la requête, soit avec un sous-select permettant de déterminer le nombre maximum de
tours) :
SELECT nom, stddev(extract (epoch from lap_time)) as stddev
FROM brno_2015
JOIN pilotes
ON (no_pilote = no)
WHERE no_tour > 1
AND no_pilote in (SELECT no_pilote FROM brno_2015 WHERE no_tour=22)
GROUP BY 1
ORDER BY 2
LIMIT 1;

Le pilote 19 a donc été le plus régulier :

386
9. SQL POUR L’ANALYSE DE DONNÉES

nom | stddev
-----------------+-------------------
Alvaro BAUTISTA | 0.222825823492654

Window Functions

Si ce n’est pas déjà fait, nous positionnons le search_path pour chercher les objets du
schéma brno2015 :
SET search_path = brno2015;

3. Afficher la place sur le podium pour chaque coureur.

Les coureurs qui ne franchissent pas la ligne d’arrivée sont dans le classement malgré tout.
Il faut donc tenir compte de cela dans l’affichage des résultats.
SELECT rank() OVER (ORDER BY max_lap desc, total_time asc) AS rang,
nom, ecurie, total_time
FROM (SELECT no_pilote,
sum(lap_time) over (PARTITION BY no_pilote) as total_time,
max(no_tour) over (PARTITION BY no_pilote) as max_lap
FROM brno_2015
) AS race_data
JOIN pilotes
ON (race_data.no_pilote = pilotes.no)
GROUP BY nom, ecurie, max_lap, total_time
ORDER BY max_lap desc, total_time asc;

La requête affiche le résultat suivant :

rang | nom | ecurie | total_time


------+------------------+-----------------------------+--------------
1 | Jorge LORENZO | Movistar Yamaha MotoGP | 00:42:53.042
2 | Marc MARQUEZ | Repsol Honda Team | 00:42:57.504
3 | Valentino ROSSI | Movistar Yamaha MotoGP | 00:43:03.439
4 | Andrea IANNONE | Ducati Team | 00:43:06.113
5 | Dani PEDROSA | Repsol Honda Team | 00:43:08.692
6 | Andrea DOVIZIOSO | Ducati Team | 00:43:08.767
7 | Bradley SMITH | Monster Yamaha Tech 3 | 00:43:14.863
8 | Pol ESPARGARO | Monster Yamaha Tech 3 | 00:43:16.282
9 | Aleix ESPARGARO | Team SUZUKI ECSTAR | 00:43:36.826
10 | Danilo PETRUCCI | Octo Pramac Racing | 00:43:38.303
11 | Yonny HERNANDEZ | Octo Pramac Racing | 00:43:43.015
12 | Scott REDDING | EG 0,0 Marc VDS | 00:43:43.216
13 | Alvaro BAUTISTA | Aprilia Racing Team Gresini | 00:43:47.479
14 | Stefan BRADL | Aprilia Racing Team Gresini | 00:43:47.666
387
https://dalibo.com/formations
SQL pour PostgreSQL

15 | Loris BAZ | Forward Racing | 00:43:53.358


16 | Hector BARBERA | Avintia Racing | 00:43:54.637
17 | Nicky HAYDEN | Aspar MotoGP Team | 00:43:55.43
18 | Mike DI MEGLIO | Avintia Racing | 00:43:58.986
19 | Jack MILLER | CWM LCR Honda | 00:44:04.449
20 | Claudio CORTI | Forward Racing | 00:44:43.075
21 | Karel ABRAHAM | AB Motoracing | 00:44:55.697
22 | Maverick VIÑALES | Team SUZUKI ECSTAR | 00:29:31.557
23 | Cal CRUTCHLOW | CWM LCR Honda | 00:27:38.315
24 | Eugene LAVERTY | Aspar MotoGP Team | 00:08:04.096
25 | Alex DE ANGELIS | E-Motion IodaRacing Team | 00:06:05.782
(25 rows)

4. À partir de la requête précédente, afficher également la différence du temps de


chaque coureur par rapport à celui de la première place.

La requête n’est pas beaucoup modifiée, seule la fonction first_value() est utilisée
pour déterminer le temps du vainqueur, temps qui sera ensuite retranché au temps du
coureur courant.

SELECT rank() OVER (ORDER BY max_lap desc, total_time asc) AS rang,


nom, ecurie, total_time,
total_time - first_value(total_time)
OVER (ORDER BY max_lap desc, total_time asc) AS difference
FROM (SELECT no_pilote,
sum(lap_time) over (PARTITION BY no_pilote) as total_time,
max(no_tour) over (PARTITION BY no_pilote) as max_lap
FROM brno_2015
) AS race_data
JOIN pilotes
ON (race_data.no_pilote = pilotes.no)
GROUP BY nom, ecurie, max_lap, total_time
ORDER BY max_lap desc, total_time asc;

La requête affiche le résultat suivant :

r | nom | ecurie | total_time | difference


--+-----------------+----------------------+-------------+---------------
1| Jorge LORENZO | Movistar Yamaha [...]|00:42:53.042 | 00:00:00
2| Marc MARQUEZ | Repsol Honda Team |00:42:57.504 | 00:00:04.462
3| Valentino ROSSI | Movistar Yamaha [...]|00:43:03.439 | 00:00:10.397
4| Andrea IANNONE | Ducati Team |00:43:06.113 | 00:00:13.071
5| Dani PEDROSA | Repsol Honda Team |00:43:08.692 | 00:00:15.65
6| Andrea DOVIZIOSO| Ducati Team |00:43:08.767 | 00:00:15.725

388
9. SQL POUR L’ANALYSE DE DONNÉES

7| Bradley SMITH | Monster Yamaha Tech 3|00:43:14.863 | 00:00:21.821


8| Pol ESPARGARO | Monster Yamaha Tech 3|00:43:16.282 | 00:00:23.24
9| Aleix ESPARGARO | Team SUZUKI ECSTAR |00:43:36.826 | 00:00:43.784
10| Danilo PETRUCCI | Octo Pramac Racing |00:43:38.303 | 00:00:45.261
11| Yonny HERNANDEZ | Octo Pramac Racing |00:43:43.015 | 00:00:49.973
12| Scott REDDING | EG 0,0 Marc VDS |00:43:43.216 | 00:00:50.174
13| Alvaro BAUTISTA | Aprilia Racing [...] |00:43:47.479 | 00:00:54.437
14| Stefan BRADL | Aprilia Racing [...] |00:43:47.666 | 00:00:54.624
15| Loris BAZ | Forward Racing |00:43:53.358 | 00:01:00.316
16| Hector BARBERA | Avintia Racing |00:43:54.637 | 00:01:01.595
17| Nicky HAYDEN | Aspar MotoGP Team |00:43:55.43 | 00:01:02.388
18| Mike DI MEGLIO | Avintia Racing |00:43:58.986 | 00:01:05.944
19| Jack MILLER | CWM LCR Honda |00:44:04.449 | 00:01:11.407
20| Claudio CORTI | Forward Racing |00:44:43.075 | 00:01:50.033
21| Karel ABRAHAM | AB Motoracing |00:44:55.697 | 00:02:02.655
22| Maverick VIÑALES| Team SUZUKI ECSTAR |00:29:31.557 | -00:13:21.485
23| Cal CRUTCHLOW | CWM LCR Honda |00:27:38.315 | -00:15:14.727
24| Eugene LAVERTY | Aspar MotoGP Team |00:08:04.096 | -00:34:48.946
25| Alex DE ANGELIS | E-Motion Ioda[...] |00:06:05.782 | -00:36:47.26
(25 rows)

5. Pour chaque tour, afficher :

• le nom du pilote ;
• son rang dans le tour ;
• son temps depuis le début de la course ;
• dans le tour, la différence de temps par rapport au premier.

Pour construire cette requête, nous avons besoin d’obtenir le temps cumulé tour après
tour pour chaque coureur. Nous commençons donc par écrire une première requête :

SELECT *,
SUM(lap_time)
OVER (PARTITION BY no_pilote ORDER BY no_tour) AS temps_tour_glissant
FROM brno_2015

Elle retourne le résultat suivant :

no_tour | no_pilote | lap_time | temps_tour_glissant


---------+-----------+--------------+---------------------
1 | 4 | 00:02:02.209 | 00:02:02.209
2 | 4 | 00:01:57.57 | 00:03:59.779
3 | 4 | 00:01:57.021 | 00:05:56.8
389
https://dalibo.com/formations
SQL pour PostgreSQL

4 | 4 | 00:01:56.943 | 00:07:53.743
5 | 4 | 00:01:57.012 | 00:09:50.755
6 | 4 | 00:01:57.011 | 00:11:47.766
7 | 4 | 00:01:57.313 | 00:13:45.079
8 | 4 | 00:01:57.95 | 00:15:43.029
9 | 4 | 00:01:57.296 | 00:17:40.325
10 | 4 | 00:01:57.295 | 00:19:37.62
11 | 4 | 00:01:57.185 | 00:21:34.805
12 | 4 | 00:01:57.45 | 00:23:32.255
13 | 4 | 00:01:57.457 | 00:25:29.712
14 | 4 | 00:01:57.362 | 00:27:27.074
15 | 4 | 00:01:57.482 | 00:29:24.556
16 | 4 | 00:01:57.358 | 00:31:21.914
17 | 4 | 00:01:57.617 | 00:33:19.531
18 | 4 | 00:01:57.594 | 00:35:17.125
19 | 4 | 00:01:57.412 | 00:37:14.537
20 | 4 | 00:01:57.786 | 00:39:12.323
21 | 4 | 00:01:58.087 | 00:41:10.41
22 | 4 | 00:01:58.357 | 00:43:08.767
(...)

Cette requête de base est ensuite utilisée dans une CTE qui sera utilisée par la requête
répondant à la question de départ. La colonne temps_tour_glissant est utilisée pour
calculer le rang du pilote dans la course, est affiché et le temps cumulé du meilleur pilote
est récupéré avec la fonction first_value :

WITH temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum(lap_time)
OVER (PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant
FROM brno_2015
ORDER BY no_pilote, no_tour
)

SELECT no_tour, nom,


rank() OVER (PARTITION BY no_tour
ORDER BY temps_tour_glissant ASC
) as place_course,
temps_tour_glissant,
temps_tour_glissant - first_value(temps_tour_glissant)
OVER (PARTITION BY no_tour

390
9. SQL POUR L’ANALYSE DE DONNÉES

ORDER BY temps_tour_glissant asc


) AS difference
FROM temps_glissant t
JOIN pilotes p ON p.no = t.no_pilote;

On pouvait également utiliser une simple sous-requête pour obtenir le même résultat :

SELECT no_tour,
nom,
rank()
OVER (PARTITION BY no_tour
ORDER BY temps_tour_glissant ASC
) AS place_course,
temps_tour_glissant,
temps_tour_glissant - first_value(temps_tour_glissant)
OVER (PARTITION BY no_tour
ORDER BY temps_tour_glissant asc
) AS difference
FROM (
SELECT *, SUM(lap_time)
OVER (PARTITION BY no_pilote
ORDER BY no_tour)
AS temps_tour_glissant
FROM brno_2015) course
JOIN pilotes
ON (pilotes.no = course.no_pilote)
ORDER BY no_tour;

La requête fournit le résultat suivant :

no.| nom | place_c. | temps_tour_glissant | difference


---+------------------+----------+---------------------+--------------
1 | Jorge LORENZO | 1 | 00:02:00.83 | 00:00:00
1 | Marc MARQUEZ | 2 | 00:02:01.058 | 00:00:00.228
1 | Andrea DOVIZIOSO | 3 | 00:02:02.209 | 00:00:01.379
1 | Valentino ROSSI | 4 | 00:02:02.329 | 00:00:01.499
1 | Andrea IANNONE | 5 | 00:02:02.597 | 00:00:01.767
1 | Bradley SMITH | 6 | 00:02:02.861 | 00:00:02.031
1 | Pol ESPARGARO | 7 | 00:02:03.239 | 00:00:02.409
(...)
2 | Jorge LORENZO | 1 | 00:03:57.073 | 00:00:00
2 | Marc MARQUEZ | 2 | 00:03:57.509 | 00:00:00.436
2 | Valentino ROSSI | 3 | 00:03:59.696 | 00:00:02.623
2 | Andrea DOVIZIOSO | 4 | 00:03:59.779 | 00:00:02.706
2 | Andrea IANNONE | 5 | 00:03:59.9 | 00:00:02.827
391
https://dalibo.com/formations
SQL pour PostgreSQL

2 | Bradley SMITH | 6 | 00:04:00.355 | 00:00:03.282


2 | Pol ESPARGARO | 7 | 00:04:00.87 | 00:00:03.797
2 | Maverick VIÑALES | 8 | 00:04:01.187 | 00:00:04.114
(...)
(498 rows)

6. Pour chaque coureur, quel est son meilleur tour et quelle place avait-il sur ce tour ?

Il est ici nécessaire de sélectionner pour chaque tour le temps du meilleur tour. On peut
alors sélectionner les tours pour lequels le temps du tour est égal au meilleur temps :

WITH temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum(lap_time)
OVER (PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant
FROM brno_2015
ORDER BY no_pilote, no_tour
),

classement_tour AS (
SELECT no_tour, no_pilote, lap_time,
rank() OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
) as place_course,
temps_tour_glissant,
min(lap_time) OVER (PARTITION BY no_pilote) as meilleur_temps
FROM temps_glissant
)

SELECT no_tour, nom, place_course, lap_time


FROM classement_tour t
JOIN pilotes p ON p.no = t.no_pilote
WHERE lap_time = meilleur_temps;

Ce qui donne le résultat suivant :

no_tour | nom | place_course | lap_time


---------+------------------+--------------+--------------
4 | Jorge LORENZO | 1 | 00:01:56.169
4 | Marc MARQUEZ | 2 | 00:01:56.048
4 | Valentino ROSSI | 3 | 00:01:56.747
6 | Andrea IANNONE | 5 | 00:01:56.86
6 | Dani PEDROSA | 7 | 00:01:56.975

392
9. SQL POUR L’ANALYSE DE DONNÉES

4 | Andrea DOVIZIOSO | 4 | 00:01:56.943


3 | Bradley SMITH | 6 | 00:01:57.25
17 | Pol ESPARGARO | 8 | 00:01:57.454
4 | Aleix ESPARGARO | 12 | 00:01:57.844
4 | Danilo PETRUCCI | 11 | 00:01:58.121
9 | Yonny HERNANDEZ | 14 | 00:01:58.53
2 | Scott REDDING | 14 | 00:01:57.976
3 | Alvaro BAUTISTA | 21 | 00:01:58.71
3 | Stefan BRADL | 16 | 00:01:58.38
3 | Loris BAZ | 19 | 00:01:58.679
2 | Hector BARBERA | 15 | 00:01:58.405
2 | Nicky HAYDEN | 16 | 00:01:58.338
3 | Mike DI MEGLIO | 18 | 00:01:58.943
4 | Jack MILLER | 22 | 00:01:59.007
2 | Claudio CORTI | 24 | 00:02:00.377
14 | Karel ABRAHAM | 23 | 00:02:01.716
3 | Maverick VIÑALES | 8 | 00:01:57.436
3 | Cal CRUTCHLOW | 11 | 00:01:57.652
3 | Eugene LAVERTY | 20 | 00:01:58.977
3 | Alex DE ANGELIS | 23 | 00:01:59.257
(25 rows)

7. Déterminer quels sont les coureurs ayant terminé la course qui ont gardé la même
position tout au long de la course.

WITH nb_tour AS (
SELECT max(no_tour) FROM brno_2015
),
temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum(lap_time) OVER (
PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant,
max(no_tour) OVER (PARTITION BY no_pilote) as total_tour
FROM brno_2015
),
classement_tour AS (
SELECT no_tour, no_pilote, lap_time, total_tour,
rank() OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
) as place_course
393
https://dalibo.com/formations
SQL pour PostgreSQL

FROM temps_glissant
)
SELECT no_pilote
FROM classement_tour t
JOIN nb_tour n ON n.max = t.total_tour
GROUP BY no_pilote
HAVING count(DISTINCT place_course) = 1;

Elle retourne le résultat suivant :

no_pilote
-----------
93
99
(2 lignes)

8. En quelle position a terminé le coureur qui a doublé le plus de personnes. Combien


de personnes a-t-il doublées ?

WITH temps_glissant AS (
SELECT no_tour, no_pilote, lap_time,
sum(lap_time) OVER (
PARTITION BY no_pilote
ORDER BY no_tour
) as temps_tour_glissant
FROM brno_2015
),
classement_tour AS (
SELECT no_tour, no_pilote, lap_time,
rank() OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
) as place_course,
temps_tour_glissant
FROM temps_glissant
),
depassement AS (
SELECT no_pilote,
last_value(place_course) OVER (PARTITION BY no_pilote) as rang,
CASE
WHEN lag(place_course) OVER (
PARTITION BY no_pilote
ORDER BY no_tour
) - place_course < 0
THEN 0
ELSE lag(place_course) OVER (
PARTITION BY no_pilote

394
9. SQL POUR L’ANALYSE DE DONNÉES

ORDER BY no_tour
) - place_course
END AS depasse
FROM classement_tour t
)

SELECT no_pilote, rang, sum(depasse)


FROM depassement
GROUP BY no_pilote, rang
ORDER BY sum(depasse) DESC
LIMIT 1;

Grouping Sets

La suite de ce TP est maintenant réalisé avec la base de formation habituelle. Attention,


ce TP nécessite l’emploi d’une version 9.5 ou supérieure de PostgreSQL.

Tout d’abord, nous positionnons le search_path pour chercher les objets du schéma
magasin :
SET search_path = magasin;

9. En une seule requête, afficher le montant total des commandes par année et pays
et le montant total des commandes uniquement par année.
SELECT extract('year' from date_commande) AS annee, code_pays,
SUM(quantite*prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY GROUPING SETS (
(extract('year' from date_commande), code_pays),
(extract('year' from date_commande))
);

Le résultat attendu est :

annee | code_pays | montant_total_commande


-------+-----------+------------------------
2003 | DE | 49634.24
2003 | FR | 10003.98
2003 | | 59638.22
2008 | CA | 1016082.18
2008 | CN | 801662.75
395
https://dalibo.com/formations
SQL pour PostgreSQL

2008 | DE | 694787.87
2008 | DZ | 663045.33
2008 | FR | 5860607.27
2008 | IN | 741850.87
2008 | PE | 1167825.32
2008 | RU | 577164.50
2008 | US | 928661.06
2008 | | 12451687.15
(...)

10. Ajouter également le montant total des commandes depuis le début de l’activité.

L’opérateur de regroupement ROLL UP amène le niveau d’agrégation sans regroupement


:

SELECT extract('year' from date_commande) AS annee, code_pays,


SUM(quantite*prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY ROLLUP (extract('year' from date_commande), code_pays);

11. Ajouter également le montant total des commandes par pays.

Cette fois, l’opérateur CUBE permet d’obtenir l’ensemble de ces informations :

SELECT extract('year' from date_commande) AS annee, code_pays,


SUM(quantite*prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY CUBE (extract('year' from date_commande), code_pays);

12. À partir de la requête précédente, ajouter une colonne par critère de regroupement,
de type booléen, qui est positionnée à true lorsque le regroupement est réalisé sur
l’ensemble des valeurs de la colonne.

Ces colonnes booléennes permettent d’indiquer à l’application comment gérer la présen-


tation des résultats.

396
9. SQL POUR L’ANALYSE DE DONNÉES

SELECT grouping(extract('year' from date_commande))::boolean AS g_annee,


grouping(code_pays)::boolean AS g_pays,
extract('year' from date_commande) AS annee,
code_pays,
SUM(quantite*prix_unitaire) AS montant_total_commande
FROM commandes c
JOIN lignes_commandes l
ON (c.numero_commande = l.numero_commande)
JOIN clients
ON (c.client_id = clients.client_id)
JOIN contacts co
ON (clients.contact_id = co.contact_id)
GROUP BY CUBE (extract('year' from date_commande), code_pays);

397
https://dalibo.com/formations
SQL pour PostgreSQL

10 SQL : CE QU'IL NE FAUT PAS FAIRE

398
10. SQL : CE QU’IL NE FAUT PAS FAIRE

10.1 DES MAUVAISES PRATIQUES

• Modélisation
• Écriture de requêtes
• Conception de l’application

Cette partie présente différents problèmes fréquemment rencontrés et leurs solutions.


Elles ont trait aussi bien à des problèmes courants qu’à des mauvaises pratiques.

10.2 PROBLÈMES DE MODÉLISATION

• Rappels sur le modèle relationnel


• Formes normales
• Atomicité !

10.2.1 QUE VEUT DIRE « RELATIONNEL » ?

• PostgreSQL est un SGBD-R, un système de gestion de bases de données relation-


nel
• Le schéma est d’une importance capitale
• « Relationnel » n’est pas « relation entre tables »
• Les tables SONT les relations (entre attributs)

Contrairement à une idée assez fréquemment répandue, le terme relationnel ne désigne


pas le fait que les tables soient liées entre elles. Les « tables » SONT les relations. On fait
référence ici à l’algèbre relationnelle, inventée en 1970 par Edgar Frank Codd.

Les bases de données dites relationnelles n’implémentent habituellement pas exactement


cet algèbre, mais en sont très proches. Le langage SQL, entre autres, ne respecte pas
l’algèbre relationnelle. Le sujet étant vaste et complexe, il ne sera pas abordé ici. Si vous
voulez approfondir le sujet, le livre Introduction aux bases de données de Chris J. Date, est
un des meilleurs ouvrages sur l’algèbre relationnelle et les déficiences du langage SQL à
ce sujet.

399
https://dalibo.com/formations
SQL pour PostgreSQL

10.2.2 QUELQUES RAPPELS SUR LE MODÈLE RELATIONNEL

• Le but est de modéliser un ensemble de faits


• Le modèle relationnel a été introduit à l’époque des bases de données hiérar-
chiques
– Pointeur : incohérence à terme
– Formalisme : relations, modélisation évitant les incohérences suite à modifi-
cation
– Formes normales
• Un modèle n’est qu’un modèle : il ne traduit pas la réalité, simplement ce qu’on
souhaite en représenter
• Identifier rapidement les problèmes les plus évidents

Le modèle relationnel est apparu suite à un constat : les bases de données de l’époque
(hiérarchiques) reposaient sur la notion de pointeur. Une mise à jour pouvait donc facile-
ment casser le modèle : doublons simples, données pointant sur du « vide », doublons
incohérents entre eux, etc.

Le modèle relationnel a donc été proposé pour remédier à tous ces problèmes. Un sys-
tème relationnel repose sur le concept de relation (table en SQL). Une relation est un
ensemble de faits. Chaque fait est identifié par un identifiant (clé naturelle). Le fait lie
cet identifiant à un certain nombre d’attributs. Une relation ne peut donc pas avoir de
doublon.

La modélisation relationnelle étant un vaste sujet en soi, nous n’allons pas tout détailler
ici, mais plutôt rappeler les points les plus importants.

10.2.3 FORMES NORMALES

Il existe une définition mathématique précise de chacune des 7 formes normales.


• La troisième forme normale peut toujours être atteinte
• La forme suivante (forme normale de Boyce-Codd, ou FNBC) ne peut pas toujours
être atteinte
• La cible est donc habituellement la 3FN
• Chris Date :
– « Chaque attribut dépend de la clé, de TOUTE la clé, et QUE de la clé »
– « The key, the whole key, nothing but the key »

Une relation (table) est en troisième forme normale si tous les attributs (colonnes) dépen-
dent de la clé (primaire), de toute la clé (pas d’un sous-ensemble de ses colonnes), et de

400
10. SQL : CE QU’IL NE FAUT PAS FAIRE

rien d’autre que de la clé (une colonne supplémentaire).

Si vos tables vérifient déjà ces trois points, votre modélisation est probablement assez
bonne.

Voir l’article wikipedia112 présentant l’ensemble des formes normales.

10.3 ATOMICITÉ

• Un attribut (colonne) doit être atomique :


– Modifier l’attribut sans en toucher un autre
– Donnée correcte (délicat !)
– Recherche efficace : accédé en entier dans une clause WHERE
• Non respect = violation de la première forme normale

L’exemple suivant utilise une table voiture.

10.3.1 ATOMICITÉ - MAUVAIS EXEMPLE

Immatriculation Modèle Caractéristiques

NH-415-DG twingo 4 roues motrices,toit ouvrant, climatisation


EO-538-WR clio boite automatique,abs,climatisation

INSERT INTO voitures


VALUES ('AD-057-GD','clio','toit ouvrant,abs');

Cette modélisation viole la première forme normale (atomicité des attributs). Si on


recherche toutes les voitures vertes, on va devoir utiliser une clause WHERE de ce type :
SELECT * FROM voitures
WHERE caracteristiques LIKE '%abs%'

ce qui sera évidemment très inefficace.

Par ailleurs, on n’a évidemment aucun contrôle sur ce qui est mis dans le champ
caractéristiques, ce qui est la garantie de données incohérentes au bout de quelques
jours (heures ?) d’utilisation. Par exemple, rien n’empêche d’ajouter une ligne avec des
caractéristiques similaires légèrement différentes, comme « ABS », « boîte automatique ».
112
https://fr.wikipedia.org/wiki/Forme_normale_(bases_de_donn%C3%A9es_relationnelles)

401
https://dalibo.com/formations
SQL pour PostgreSQL

Ce modèle ne permet donc pas d’assurer la cohérence des données.

10.3.2 ATOMICITÉ - PROPOSITIONS

• Champs dédiés :
Column | Type | Description
----------------+---------+------------------------------------
immatriculation | text | Clé primaire
modele | text |
couleur | color | Couleur vehicule (bleu,rouge,vert)
abs | boolean | Option anti-blocage des roues
type_roue | boolean | tole/aluminium
motricite | boolean | 2 roues motrices / 4 roues motrices
• Plusieurs valeurs : contrainte CHECK/enum/table de référence
• Beaucoup de champs : clé/valeur (plusieurs formes possibles)

Une alternative plus fiable est de rajouter des colonnes boolean quatre_roues_motrices,
boolean abs, varchar couleur. C’est ce qui est à privilégier si le nombre de caractéris-
tiques est fixe et pas trop important.

Dans le cas où un simple booléen ne suffit pas, un champ avec une contrainte est possible.
Il y a plusieurs méthodes :

• une contrainte simple :


ALTER TABLE voitures ADD COLUMN couleur text
CHECK (couleur IN ('rouge','bleu','vert')) ;

• un type « énumération113 » :
CREATE TYPE color AS ENUM ('bleu', 'rouge', 'vert') ;
ALTER TABLE voitures ADD COLUMN couleur color ;

(Les énumérations ne sont pas adaptées à des modifications fréquentes et nécessitent


parfois un transtypage vers du text).

• une table de référence avec contrainte, c’est le plus flexible :


CREATE TABLE couleurs (
couleur_id int PRIMARY KEY,
couleur text
) ;
ALTER TABLE voitures ADD COLUMN couleur_id REFERENCES couleurs ;
113
https://docs.postgresql.fr/current/datatype-enum.html

402
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Ce modèle facilite les recherches et assure la cohérence. L’indexation est facilitée, et les
performances ne sont pas dégradées, bien au contraire.

Dans le cas où le nombre de propriétés n’est pas aussi bien défini qu’ici, ou est grand,
même un modèle clé-valeur dans une associée vaut mieux que l’accumulation de pro-
priétés dans un champ texte. Même une simple table des caractéristiques est plus flexible
(voir le TP).

Un modèle clé/valeur existe sous plusieurs variantes (table associée, champs hstore ou
JSON...) et a ses propres inconvénients, mais il offre au moins plus de flexibilité et de
possibilités d’indexation ou de validation des données. Ce sujet est traité plus loin.

10.4 CONTRAINTES ABSENTE

• Parfois (souvent ?) ignorées pour diverses raisons :


– faux gains de performance
– flexibilité du modèle de données
– compatibilité avec d’autres SGBD (MySQL/MyISAM...)
– commodité de développement

Les contraintes d’intégrité et notamment les clés étrangères sont parfois absentes des
modèles de données. Les problématiques de performance et de flexibilité sont souvent
mises en avant, alors que les contraintes sont justement une aide pour l’optimisation de re-
quêtes par le planificateur, mais surtout une garantie contre de très coûteuses corruption
de données logiques.

L’absence de contraintes a souvent des conséquences catastrophiques.

10.4.1 CONSÉQUENCES DE L'ABSENCE DE CONTRAINTES

• Conséquences
– problèmes d’intégrité des données
– fonctions de vérification de cohérence des données
• Les contraintes sont utiles à l’optimiseur :
– déterminent l’unicité des valeurs
– éradiquent des lectures de tables inutiles sur des LEFT JOIN
– utilisent les contraintes CHECK pour exclure une partition
403
https://dalibo.com/formations
SQL pour PostgreSQL

De plus, l’absence de contraintes va également entraîner des problèmes d’intégrité des


données. Il est par exemple très compliqué de se prémunir efficacement contre une race
condition114 en l’absence de clé étrangère.

Imaginez le scénario suivant :

• la transaction x1 s’assure que la donnée est présente dans la table t1 ;


• la transaction x2 supprime la donnée précédente dans la table t1 ;
• la transaction x1 insère une ligne dans la table t2 faisant référence à la ligne de t1
qu’elle pense encore présente.

Ce cas est très facilement gérable pour un moteur de base de donnée si une clé étrangère
existe. Redévelopper ces mêmes contrôles dans la couche applicative sera toujours plus
coûteux en terme de performance, voire impossible à faire dans certains cas sans passer
par la base de donnée elle-même (multiples serveurs applicatifs accédant à la même base
de donnée).

Il peut s’ensuivre des calculs d’agrégats faux et des problèmes applicatifs de toute sorte.
Souvent, plutôt que de corriger le modèle de données, des fonctions de vérification de la
cohérence des données seront mises en place, entraînant ainsi un travail supplémentaire
pour trouver et corriger les incohérences.

Lorsque ces problèmes d’intégrité seront détectés, il s’en suivra également la création
de procédures de vérification de cohérence des données qui vont aussi alourdir les
développements, entraînant ainsi un travail supplémentaire pour trouver et corriger les
incohérences. Ce qui a été gagné d’un côté est perdu de l’autre, mais sous une forme
différente.

De plus, les contraintes d’intégrité sont des informations qui garantissent non seulement
la cohérence des données mais qui vont également influencer l’optimiseur dans ses choix
de plans d’exécution.

Parmi les informations utilisées par l’optimiseur, les contraintes d’unicité permettent de
déterminer sans difficulté la répartition des valeurs stockées dans une colonne : chaque
valeur est simplement unique. L’utilisation des index sur ces colonnes sera donc prob-
ablement favorisée. Les contraintes d’intégrité permettent également à l’optimiseur de
pouvoir éliminer des jointures inutiles avec un LEFT JOIN. Enfin, les contraintes CHECK sur
des tables partitionnées permettent de cibler les lectures sur certaines partitions seule-
ment, et donc d’exclure les partitions inutiles.

114
Situation où deux sessions ou plus modifient des données en tables au même moment.

404
10. SQL : CE QU’IL NE FAUT PAS FAIRE

10.4.2 SUSPENSION DES CONTRAINTES LE TEMPS D'UNE TRANSACTION

• Solution :
– contraintes DEFERRABLE !

Parfois, les clés étrangères sont supprimées simplement parce que des transactions sont
en erreur car des données sont insérées dans une table fille sans avoir alimenté la table
mère. Des identifiants de clés étrangères de la table fille sont absents de la table mère,
entraînant l’arrêt en erreur de la transaction. Il est possible de contourner cela en différant
la vérification des contraintes d’intégrité à la fin de la transaction

Une contrainte DEFERRABLE associée à un SET CONSTRAINT … DEFERRED n’est vérifiée


que lors du COMMIT. Elle ne gêne donc pas le développeur, qui peut insérer les don-
nées dans l’ordre qu’il veut ou insérer temporairement des données incohérentes. Ce
qui compte est que la situation soit saine à la fin de la transaction, quand les données
seront enregistrées et deviendront visibles par les autres sessions.

L’exemple ci-dessous montre l’utilisation de la vérification des contraintes d’intégrité en


fin de transaction.
CREATE TABLE mere (id integer, t text);

CREATE TABLE fille (id integer, mere_id integer, t text);

ALTER TABLE mere ADD CONSTRAINT pk_mere PRIMARY KEY (id);

ALTER TABLE fille


ADD CONSTRAINT fk_mere_fille
FOREIGN KEY (mere_id)
REFERENCES mere (id)
MATCH FULL
ON UPDATE NO ACTION
ON DELETE CASCADE
DEFERRABLE;

La transaction insère d’abord les données dans la table fille, puis ensuite dans la table
mère :
BEGIN ;
SET CONSTRAINTS ALL DEFERRED ;

INSERT INTO fille (id, mere_id, t) VALUES (1, 1, 'val1');


INSERT INTO fille (id, mere_id, t) VALUES (2, 2, 'val2');

INSERT INTO mere (id, t) VALUES (1, 'val1'), (2, 'val2');

COMMIT;
405
https://dalibo.com/formations
SQL pour PostgreSQL

Sans le SET CONSTRAINTS ALL DEFERRED, le premier ordre serait tombé en erreur.

10.5 STOCKAGE ENTITÉ-CLÉ-VALEUR

• Entité-Attribut-Valeur (ou Entité-Clé-Valeur)


• Quel but ?
– flexibilité du modèle de données
– adapter sans délai ni surcoût le modèle de données
• Conséquences :
– création d’une table : identifiant / nom_attribut / valeur
– requêtes abominables et coûteuses

Le modèle relationnel a été critiqué depuis sa création pour son manque de souplesse
pour ajouter de nouveaux attributs ou pour proposer plusieurs attributs sans pour autant
nécessiter de redévelopper l’application.

La solution souvent retenue est d’utiliser une table « à tout faire » entité-attribut-valeur
qui est associée à une autre table de la base de données. Techniquement, une telle table
comporte trois colonnes. La première est un identifiant généré qui permet de référencer
la table mère. Les deux autres colonnes stockent le nom de l’attribut représenté et la
valeur représentée.

Ainsi, pour reprendre l’exemple des informations de contacts pour un individu,


une table personnes permet de stocker un identifiant de personne. Une table
personne_attributs permet d’associer des données à un identifiant de personne.
Le type de données de la colonne est souvent prévu largement pour faire tenir tout
type d’informations, mais sous forme textuelle. Les données ne peuvent donc pas être
validées.
CREATE TABLE personnes (id SERIAL PRIMARY KEY);

CREATE TABLE personne_attributs (


id_pers INTEGER NOT NULL,
nom_attr varchar(20) NOT NULL,
val_attr varchar(100) NOT NULL
);

INSERT INTO personnes (id) VALUES (nextval('personnes_id_seq')) RETURNING id;


id
----
1

406
10. SQL : CE QU’IL NE FAUT PAS FAIRE

INSERT INTO personne_attributs (id_pers, nom_attr, val_attr)


VALUES (1, 'nom', 'Prunelle'),
(1, 'prenom', 'Léon');
(...)

Un tel modèle peut sembler souple mais pose plusieurs problèmes. Le premier concerne
l’intégrité des données. Il n’est pas possible de garantir la présence d’un attribut comme
on le ferait avec une contrainte NOT NULL. Si l’on souhaite stocker des données dans un
autre format qu’une chaîne de caractère, pour bénéficier des contrôles de la base de don-
nées sur ce type, la seule solution est de créer autant de colonnes d’attributs qu’il y a
de types de données à représenter. Ces colonnes ne permettront pas d’utiliser des con-
traintes CHECK pour garantir la cohérence des valeurs stockées avec ce qui est attendu,
car les attributs peuvent stocker n’importe quelle donnée.

10.5.1 STOCKAGE ENTITÉ-CLÉ-VALEUR : EXEMPLE

Comment lister tous les DBA ?

id_pers nom_attr val_attr

1 nom Prunelle
1 prenom Léon
1 telephone 0123456789
1 fonction dba

10.5.2 STOCKAGE ENTITÉ-CLÉ-VALEUR : REQUÊTE ASSOCIÉE

SELECT id, att_nom.val_attr nom , att_prenom.val_attr prenom,att_telephone.val_attr tel


FROM personnes p
JOIN personne_attributs att_nom
ON (p.id=att_nom.id_pers AND att_nom.nom_attr='nom')
JOIN personne_attributs att_prenom
ON (p.id=att_prenom.id_pers AND att_prenom.nom_attr='prenom')
JOIN personne_attributs att_telephone
ON (p.id=att_telephone.id_pers AND att_telephone.nom_attr='telephone')
JOIN personne_attributs att_fonction
ON (p.id=att_fonction.id_pers AND att_fonction.nom_attr='fonction')
WHERE att_fonction.val_attr='dba';
407
https://dalibo.com/formations
SQL pour PostgreSQL

Les requêtes SQL qui permettent de récupérer les données requises dans l’application
sont également particulièrement lourdes à écrire et à maintenir, à moins de récupérer les
données attribut par attribut.

Des problèmes de performances vont donc très rapidement se poser. Cette représenta-
tion des données entraîne souvent l’effondrement des performances d’une base de don-
nées relationnelle. Les requêtes sont difficilement optimisables et nécessitent de réaliser
beaucoup d’entrées-sorties disque, car les données sont éparpillées un peu partout dans
la table.

10.5.3 STOCKAGE ENTITÉ-CLÉ-VALEUR, HSTORE, JSON

• Solutions :
– revenir sur la conception du modèle de données
– utiliser un type de données plus adapté : hstore, jsonb
• On économise jointures et place disque.

Lorsque de telles solutions sont déployées pour stocker des données transactionnelles,
il vaut mieux revenir à un modèle de données traditionnel qui permet de typer correcte-
ment les données, de mettre en place les contraintes d’intégrité adéquates et d’écrire des
requêtes SQL efficaces.

Dans d’autres cas où le nombre de champs est vraiment élevé et variable, il vaut mieux
utiliser un type de données de PostgreSQL qui est approprié, comme hstore qui permet
de stocker des données sous la forme clé->valeur. On conserve ainsi l’intégrité des
données (on n’a qu’une ligne par personne), on évite de très nombreuses jointures source
d’erreurs et de ralentissements, et même de la place disque.

De plus, ce type de données peut être indexé pour garantir de bons temps de réponses
des requêtes qui nécessitent des recherches sur certaines clés ou certaines valeurs.

Voici l’exemple précédent revu avec l’extension hstore :


CREATE EXTENSION hstore;
CREATE TABLE personnes (id SERIAL PRIMARY KEY, attributs hstore);

INSERT INTO personnes (attributs) VALUES ('nom=>Prunelle, prenom=>Léon');


INSERT INTO personnes (attributs) VALUES ('prenom=>Gaston,nom=>Lagaffe');
INSERT INTO personnes (attributs) VALUES ('nom=>DeMaesmaker');

SELECT * FROM personnes;


id | attributs
----+--------------------------------------

408
10. SQL : CE QU’IL NE FAUT PAS FAIRE

1 | "nom"=>"Prunelle", "prenom"=>"Léon"
2 | "nom"=>"Lagaffe", "prenom"=>"Gaston"
3 | "nom"=>"DeMaesmaker"
(3 rows)

SELECT id, attributs->'prenom' AS prenom FROM personnes;


id | prenom
----+----------
1 | Léon
2 | Gaston
3 |
(3 rows)

SELECT id, attributs->'nom' AS nom FROM personnes;


id | nom
----+-------------
1 | Prunelle
2 | Lagaffe
3 | DeMaesmaker
(3 rows)

Le principe du JSON est similaire.

10.6 ATTRIBUTS MULTI-COLONNES

• Pourquoi
– stocker plusieurs attributs pour une même ligne
– exemple : les différents numéros de téléphone d’une personne
• Pratique courante
– ex : telephone_1, telephone_2
• Conséquences
– et s’il faut rajouter encore une colonne ?
– maîtrise de l’unicité des valeurs ?
– requêtes complexes à maintenir
• Solutions
– créer une table dépendante
– ou un type tableau

Dans certains cas, le modèle de données doit être étendu pour pouvoir stocker des don-
nées complémentaires. Un exemple typique est une table qui stocke les informations
pour contacter une personne. Une table personnes ou contacts possède une colonne
409
https://dalibo.com/formations
SQL pour PostgreSQL

telephone qui permet de stocker le numéro de téléphone d’une personne. Or, une per-
sonne peut disposer de plusieurs numéros. Le premier réflexe est souvent de créer une
seconde colonne telephone_2 pour stocker un numéro de téléphone complémentaire.
S’en suit une colonne telephone_3 voire telephone_4 en fonction des besoins.

Dans de tels cas, les requêtes deviennent plus complexes à maintenir et il est difficile
de garantir l’unicité des valeurs stockées pour une personne car l’écriture des contraintes
d’intégrité devient de plus en plus complexe au fur et à mesure que l’on ajoute une colonne
pour stocker un numéro.

La solution la plus pérenne pour gérer ce cas de figure est de créer une table de dépen-
dance qui est dédiée au stockage des numéros de téléphone. Ainsi, la table personnes ne
portera plus de colonnes telephone, mais une table telephones portera un identifiant
référençant une personne et un numéro de téléphone. Ainsi, si une personne dispose de
trois, quatre... numéros de téléphone, la table telephones comportera autant de lignes
qu’il y a de numéros pour une personne.

Les différents numéros de téléphone seront obtenus par jointure entre la table personnes
et la table telephones. L’application se chargera de l’affichage.

Ci-dessous, un exemple d’implémentation du problème où une table telephonesdans


laquelle plusieurs numéros seront stockés sur plusieurs lignes plutôt que dans plusieurs
colonnes.

CREATE TABLE personnes (


per_id SERIAL PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
pnom VARCHAR(50) NOT NULL,
...
);

CREATE TABLE telephones (


per_id INTEGER NOT NULL,
numero VARCHAR(20),
PRIMARY KEY (per_id, numero),
FOREIGN KEY (per_id) REFERENCES personnes (per_id)
);

L’unicité des valeurs sera garantie à l’aide d’une contrainte d’unicité posée sur l’identifiant
per_id et le numéro de téléphone.

Une autre solution consiste à utiliser un tableau pour représenter cette information. D’un
point de vue conceptuel, le lien entre une personne et son ou ses numéros de téléphone
est plus une « composition » qu’une réelle « relation » : le numéro de téléphone ne nous
intéresse pas en tant que tel, mais uniquement en tant que détail d’une personne. On

410
10. SQL : CE QU’IL NE FAUT PAS FAIRE

n’accédera jamais à un numéro de téléphone séparément : la table telephones donnée


plus haut n’a pas de clé « naturelle », un simple rattachement à la table personnes par
l’identifiant de la personne. Sans même parler de partitionnement, on gagnerait donc en
performances en stockant directement les numéros de téléphone dans la table personnes,
ce qui est parfaitement faisable sous PostgreSQL :
CREATE TABLE personnes (
per_id SERIAL PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
pnom VARCHAR(50) NOT NULL,
numero VARCHAR(20)[]
);

-- Ajout d'une personne


INSERT INTO personnes (nom, pnom, numero)
VALUES ('Simpson', 'Omer', '{0607080910}');

SELECT *
FROM personnes;
per_id | nom | pnom | numero
--------+---------+------+--------------
1 | Simpson | Omer | {0607080910}
(1 ligne)

-- Ajout d'un numéro de téléphone pour une personne donnée :


UPDATE personnes
SET numero = numero || '{0102030420}'
WHERE per_id = 1;

-- Vérification de l'ajout :
SELECT *
FROM personnes;
per_id | nom | pnom | numero
--------+---------+------+-------------------------
1 | Simpson | Omer | {0607080910,0102030420}

-- Séparation des éléments du tableau :


SELECT per_id, nom, pnom, unnest(numero) AS numero
FROM personnes;
per_id | nom | pnom | numero
--------+---------+------+------------
1 | Simpson | Omer | 0607080910
1 | Simpson | Omer | 0102030420
(2 lignes)

411
https://dalibo.com/formations
SQL pour PostgreSQL

10.7 NOMBREUSES LIGNES DE PEU DE COLONNES

• Énormément de lignes, peu de colonnes


– Cas typique : séries temporelles
• Volumétrie augmentée par les entêtes
• Regrouper les valeurs dans un ARRAY ou un type composite
• Partitionner

Certaines applications, typiquement celles récupérant des données temporelles, stockent


peu de colonnes (parfois juste date, capteur, valeur...) mais énormément de lignes.

Dans le modèle MVCC de PostgreSQL, chaque ligne utilise au bas mot 23 octets pour
stocker xmin, xmax et les autres informations de maintenance de la ligne. On peut donc
se retrouver avec un overhead représentant la majorité de la table. Cela peut avoir un fort
impact sur la volumétrie :

CREATE TABLE valeurs_capteur (d timestamp, v smallint);


-- soit 8 + 2 = 10 octets de données utiles par ligne

-- 100 valeurs chaque seconde pendant 100 000 s = 10 millions de lignes


INSERT INTO valeurs_capteur (d, v)
SELECT current_timestamp + (i%100000) * interval '1 s',
(random()*200)::smallint
FROM generate_series (1,10000000) i ;

SELECT pg_size_pretty(pg_relation_size ('valeurs_capteur')) ;


pg_size_pretty
----------------
422 MB
-- dont seulement 10 octets * 10 Mlignes = 100 Mo de données utiles

Il est parfois possible de regrouper les valeurs sur une même ligne au sein d’un ARRAY, ici
pour chaque seconde :

CREATE TABLE valeurs_capteur_2 (d timestamp, tv smallint[]);

INSERT INTO valeurs_capteur_2


SELECT current_timestamp+ (i%100000) * interval '1 s' ,
array_agg((random()*200)::smallint)
FROM generate_series (1,10000000) i
GROUP BY 1 ;

SELECT pg_size_pretty(pg_relation_size ('valeurs_capteur_2'));


pg_size_pretty
----------------
25 MB

412
10. SQL : CE QU’IL NE FAUT PAS FAIRE

-- soit par ligne :


-- 23 octets d'entête + 8 pour la date + 100 * 2 octets de valeurs smallint

Dans cet exemple, on économise la plupart des entêtes de ligne, mais aussi les données re-
dondantes (la date), et le coût de l’alignement des champs. Avec suffisamment de valeurs
à stocker, une partie des données peut même se retrouver compressée dans la partie
TOAST de la table.

La récupération des données se fait de manière à peine moins simple :


SELECT unnest(tv) FROM valeurs_capteur_2
WHERE d = '2018-06-15 22:07:47.651295' ;

L’indexation des valeurs à l’intérieur du tableau nécessite un index GIN :


CREATE INDEX tvx ON valeurs_capteur_2 USING gin(tv);

EXPLAIN (ANALYZE) SELECT * FROM valeurs_capteur_2 WHERE '{199}' && tv ;


QUERY PLAN
---------------------------------------------------------------------------------
Bitmap Heap Scan on valeurs_capteur_2 (cost=311.60..1134.20 rows=40000 width=232)
(actual time=8.299..20.460 rows=39792 loops=1)
Recheck Cond: ('{199}'::smallint[] && tv)
Heap Blocks: exact=3226
-> Bitmap Index Scan on tvx (cost=0.00..301.60 rows=40000 width=0)
(actual time=7.723..7.723 rows=39792 loops=1)
Index Cond: ('{199}'::smallint[] && tv)
Planning time: 0.214 ms
Execution time: 22.386 ms

Évidemment cette technique est à réserver aux cas où les données mises en tableau sont
insérées et mises à jour ensemble.

Le maniement des tableaux est détaillé dans la documentation officielle115 .

Tout cela est détaillé et mesuré dans ce billet de Julien Rouhaud116 . Il évoque aussi le
cas de structures plus complexes : au lieu d’un hstore ou d’un ARRAY, on peut utiliser un
type qui regroupe les différentes valeurs.

Une autre option, complémentaire, est le partitionnement. Il peut être géré manuellement
(tables générées par l’applicatif, par date et/ou par source de données...) ou profiter des
deux modes de partitionnement de PostgreSQL. Il n’affectera pas la volumétrie totale mais
permet de gérer des partitions plus maniables. Il a aussi l’intérêt de ne pas nécessiter de
modification du code pour lire les données.

115
https://www.postgresql.org/docs/current/static/arrays.html
116
https://rjuju.github.io/postgresql/2016/09/16/minimizing-tuple-overhead.html

413
https://dalibo.com/formations
SQL pour PostgreSQL

10.8 TABLES AUX TRÈS NOMBREUSES COLONNES

Tables à plusieurs dizaines, voire centaines de colonnes :


• Les entités sont certainement trop grosses dans la modélisation
• Il y a probablement dépendance entre certaines colonnes (Only the key)
• On accède à beaucoup d’attributs inutiles (tout est stocké au même endroit)

Il arrive régulièrement de rencontrer des tables ayant énormément de colonnes (souvent à


NULL d’ailleurs). Cela signifie qu’on modélise une entité ayant tous ces attributs (centaines
d’attributs). Il est très possible que cette entité soit en fait composée de « sous-entités »,
qu’on pourrait modéliser séparément. On peut évidemment trouver des cas particuliers
contraires, mais une table de ce type reste un bon indice.

Surtout si vous trouvez dans les dernières colonnes des attributs comme attribut_supplementaire_1…

10.9 CHOIX DES TYPES DE DONNÉES

• Objectif
– représenter des valeurs décimales
• Pratique courante
– utiliser le type float ou double
• Problèmes :
– types flottants = approximation de la valeur représentée
– erreurs d’arrondis
– résultats faux
• Solutions
– numeric(x, y) pour les calculs précis (financiers notamment)

Certaines applications scientifiques se contentent de types flottants standards car ils per-
mettent d’encoder des valeurs plus importantes que les types entiers standards. Néan-
moins, les types flottants sont peu précis, notamment pour les applications financières
où une erreur d’arrondi n’est pas envisageable.

Exemple montrant une perte de précision dans les calculs :


test=# CREATE TABLE comptes (compte_id SERIAL PRIMARY KEY, solde FLOAT);
CREATE TABLE

test=# INSERT INTO comptes (solde) VALUES (100000000.1), (10.1), (10000.2),


(100000000000000.1);
INSERT 0 4

414
10. SQL : CE QU’IL NE FAUT PAS FAIRE

test=# SELECT SUM(solde) FROM comptes;


sum
-----------------
100000100010010
(1 row)

L’utilisation du type numeric permet d’éviter la perte de précision :


test=# CREATE TABLE comptes (compte_id SERIAL PRIMARY KEY, solde NUMERIC);
CREATE TABLE

test=# INSERT INTO comptes (solde) VALUES (100000000.1), (10.1), (10000.2),


(100000000000000.1);
INSERT 0 4

test=# SELECT SUM(solde) FROM comptes;


sum
-------------------
100000100010010.5
(1 row)

10.10 COLONNE DE TYPE VARIABLE

Plus rarement, on rencontre aussi :


• Une colonne de type varchar contenant
– quelquefois un entier
– quelquefois une date
– un NULL
– une chaîne autre
– etc.
• À éviter comme la peste !
• Plusieurs sens = plusieurs champs

On rencontre parfois ce genre de choses :

Immatriculation Camion Numero de tournee

TP-108-AX 12
TF-112-IR ANNULÉE

avec bien sûr une table tournée décrivant la tournée elle-même, avec une clé technique
415
https://dalibo.com/formations
SQL pour PostgreSQL

numérique.

Cela pose un gros problème de modélisation : la colonne a un type de contenu qui


dépend de l’information qu’elle contient. On va aussi avoir un problème de performance
en joignant cette chaîne à la clé numérique de la table tournée. Le moteur n’aura que
deux choix : convertir la chaîne en numérique, avec une exception à la clé en essayant de
convertir « ANNULÉE », ou bien (ce qu’il fera) convertir le numérique de la table tournee
en chaîne. Cette dernière méthode rendra l’accès à l’identifiant de tournée par index im-
possible. D’où un parcours complet (Seq Scan) de la table tournée à chaque accès et des
performances qui décroissent au fur et à mesure que la table grossit.

La solution est une supplémentaire (un booléen tournee_ok par exemple).

Un autre classique est le champ date stocké au format texte. Le format correct de cette
date ne peut être garanti par la base, ce qui mène systématiquement à des erreurs de con-
version si un humain est impliqué. Dans un environnement international où l’on mélange
DD-MM-YYYY et MM-DD-YYYY, un rattrapge manuel est même illusoire. Les calculs de
date sont évidemment impossibles.

10.11 PROBLÈMES COURANTS D'ÉCRITURE DE REQUÊTES

• Utilisation de NULL
• Ordre implicite des colonnes
• Requêtes spaghetti
• Moteur de recherche avec LIKE

Le langage SQL est généralement méconnu, ce qui amène à l’écriture de requêtes peu
performantes, voire peu pérennes.

10.12 NULL

• NULL signifie habituellement :


– Valeur non renseignée
– Valeur inconnue
• Absence d’information
• Une table remplie de NULL est habituellement signe d’un problème de modélisa-
tion.
• NOT NULL recommandé

416
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Une table qui contient majoritairement des valeurs NULL contient bien peu de faits utilis-
ables. La plupart du temps, c’est une table dans laquelle on stocke beaucoup de choses
n’ayant que peu de rapport entre elles, les champs étant renseignés suivant le type de
chaque « chose ». C’est donc le plus souvent un signe de mauvaise modélisation. Cette
table aurait certainement dû être éclatée en plusieurs tables, chacune représentant une
des relations qu’on veut modéliser.

Il est donc recommandé que tous les attributs d’une table portent une contrainte NOT
NULL. Quelques colonnes peuvent ne pas porter ce type de contraintes, mais elles doivent
être une exception. En effet, le comportement de la base de données est souvent source
de problèmes lorsqu’une valeur NULL entre en jeu. Par exemple, la concaténation d’une
chaîne de caractères avec une valeur NULL retourne une valeur NULL, car elle est propagée
dans les calculs. D’autres types de problèmes apparaissent également pour les prédicats.

Il faut avoir à l’esprit cette citation de Chris Date :

« La valeur NULL telle qu’elle est implémentée dans SQL peut


poser plus de problèmes qu’elle n’en résout. Son comportement
est parfois étrange et est source de nombreuses erreurs et de
confusions. »

Il ne ne s’agit pas de remplacer ce NULL par des valeurs « magiques » (par exemple -1 pour
« Non renseigné » , cela ne ferait que complexifier le code) mais de se demander si NULL a
une vraie signification.

10.13 ORDRE IMPLICITE DES COLONNES

• Objectif
– s’économiser d’écrire la liste des colonnes dans une requête
• Problèmes
– si l’ordre des colonnes change, les résultats changent
– résultats faux
– données corrompues
• Solutions
– nommer les colonnes impliquées

Le langage SQL permet de s’appuyer sur l’ordre physique des colonnes d’une table. Or,
faire confiance à la base de données pour conserver cet ordre physique peut entraîner
de graves problèmes applicatifs en cas de changements. Dans le meilleur des cas,
417
https://dalibo.com/formations
SQL pour PostgreSQL

l’application ne fonctionnera plus, ce qui permet d’éviter les corruptions de données


silencieuses, où une colonne prend des valeurs destinées normalement à être stockées
dans une autre colonne. Si l’application continue de fonctionner, elle va générer des
résultats faux et des incohérences d’affichage.

Par exemple, l’ordre des colonnes peut changer notamment lorsque certains ETL sont
utilisés pour modifier le type d’une colonne varchar(10) en varchar(11). Par exemple,
pour la colonne username, l’ETL Kettle génère les ordres suivants :

ALTER TABLE utilisateurs ADD COLUMN username_KTL VARCHAR(11);


UPDATE utilisateurs SET username_KTL=username;
ALTER TABLE utilisateurs DROP COLUMN username;
ALTER TABLE utilisateurs RENAME username_KTL TO username

Il génère des ordres SQL inutiles et consommateurs d’entrées/sorties disques car il doit
générer des ordres SQL compris par tous les SGBD du marché. Or, tous les SGBD ne
permettent pas de changer le type d’une colonne aussi simplement que dans PostgreSQL.
PostgreSQL, lui, ne permet pas de changer l’ordre d’apparition des colonnes.

C’est pourquoi il est préférable de lister explicitement les colonnes dans les ordres INSERT
et SELECT, afin de garder un ordre d’insertion déterministe.

Exemples

Exemple de modification du schéma pouvant entraîner des problèmes d’insertion si les


colonnes ne sont pas listées explicitement :

CREATE TABLE insere (id integer PRIMARY KEY, col1 varchar(5), col2 integer);

INSERT INTO insere VALUES (1, 'XX', 10);

SELECT * FROM insere ;


id | col1 | col2
----+------+------
1 | XX | 10

ALTER TABLE insere ADD COLUMN col1_tmp varchar(6);


UPDATE insere SET col1_tmp = col1;
ALTER TABLE insere DROP COLUMN col1;
ALTER TABLE insere RENAME COLUMN col1_tmp TO col1;

INSERT INTO insere VALUES (2, 'XXX', 10);


ERROR: invalid input syntax for integer: "XXX"
LINE 1: INSERT INTO insere VALUES (2, 'XXX', 10);
^

INSERT INTO insere (id, col1, col2) VALUES (2, 'XXX', 10);

418
10. SQL : CE QU’IL NE FAUT PAS FAIRE

SELECT * FROM insere ;


id | col2 | col1
----+------+------
1 | 10 | XX
2 | 10 | XXX

L’utilisation de SELECT * à la place d’une liste explicite est une erreur similaire. Le nombre
de colonnes peut brutalement varier. De plus, toutes les colonnes sont rarement utilisées
dans un tel cas, ce qui provoque un gaspillage de ressources.

10.14 CODE SPAGHETTI

Le problème est similaire à tout autre langage :


• Code spaghetti pour le SQL
– Écriture d’une requête à partir d’une autre requête
– Ou évolution d’une requête au fil du temps avec des ajouts
• Non optimisable
• Vite ingérable
– Ne pas la patcher !
– Ne pas hésiter à reprendre la requête à zéro, en repensant sa sémantique
– Souvent, un changement de spécification est un changement de sens, au
niveau relationnel, de la requête

Un exemple (sous Oracle) :


SELECT Article.datem AS Article_1_9,
Article.degre_alcool AS Article_1_10,
Article.id AS Article_1_19,
Article.iddf_categor AS Article_1_20,
Article.iddp_clsvtel AS Article_1_21,
Article.iddp_cdelist AS Article_1_22,
Article.iddf_cd_prix AS Article_1_23,
Article.iddp_agreage AS Article_1_24,
Article.iddp_codelec AS Article_1_25,
Article.idda_compo AS Article_1_26,
Article.iddp_comptex AS Article_1_27,
Article.iddp_cmptmat AS Article_1_28,
Article.idda_articleparent AS Article_1_29,
Article.iddp_danger AS Article_1_30,
Article.iddf_fabric AS Article_1_33,
Article.iddp_marqcom AS Article_1_34,
Article.iddp_nomdoua AS Article_1_35,
419
https://dalibo.com/formations
SQL pour PostgreSQL

Article.iddp_pays AS Article_1_37,
Article.iddp_recept AS Article_1_40,
Article.idda_unalvte AS Article_1_42,
Article.iddb_sitecl AS Article_1_43,
Article.lib_caisse AS Article_1_49,
Article.lib_com AS Article_1_50,
Article.maj_en_attente AS Article_1_61,
Article.qte_stk AS Article_1_63,
Article.ref_tech AS Article_1_64,
1 AS Article_1_70,
CASE
WHEN (SELECT COUNT(MA.id)
FROM da_majart MA
join da_majmas MM
ON MM.id = MA.idda_majmas
join gt_tmtprg TMT
ON TMT.id = MM.idgt_tmtprg
join gt_prog PROG
ON PROG.id = TMT.idgt_prog
WHERE idda_article = Article.id
AND TO_DATE(TO_CHAR(PROG.date_lancement, 'DDMMYYYY')
|| TO_CHAR(PROG.heure_lancement, ' HH24:MI:SS'),
'DDMMYYYY HH24:MI:SS') >= SYSDATE) >= 1 THEN 1
ELSE 0
END AS Article_1_74,
Article.iddp_compnat AS Article_2_0,
Article.iddp_modven AS Article_2_1,
Article.iddp_nature AS Article_2_2,
Article.iddp_preclin AS Article_2_3,
Article.iddp_raybala AS Article_2_4,
Article.iddp_sensgrt AS Article_2_5,
Article.iddp_tcdtfl AS Article_2_6,
Article.iddp_unite AS Article_2_8,
Article.idda_untgrat AS Article_2_9,
Article.idda_unpoids AS Article_2_10,
Article.iddp_unilogi AS Article_2_11,
ArticleComplement.datem AS ArticleComplement_5_6,
ArticleComplement.extgar_depl AS ArticleComplement_5_9,
ArticleComplement.extgar_mo AS ArticleComplement_5_10,
ArticleComplement.extgar_piece AS ArticleComplement_5_11,
ArticleComplement.id AS ArticleComplement_5_20,
ArticleComplement.iddf_collect AS ArticleComplement_5_22,
ArticleComplement.iddp_gpdtcul AS ArticleComplement_5_23,
ArticleComplement.iddp_support AS ArticleComplement_5_25,
ArticleComplement.iddp_typcarb AS ArticleComplement_5_27,
ArticleComplement.mt_ext_gar AS ArticleComplement_5_36,

420
10. SQL : CE QU’IL NE FAUT PAS FAIRE

ArticleComplement.pres_cpt AS ArticleComplement_5_44,
GenreProduitCulturel.code AS GenreProduitCulturel_6_0,
Collection.libelle AS Collection_8_1,
Gtin.date_dern_vte AS Gtin_10_0,
Gtin.gtin AS Gtin_10_1,
Gtin.id AS Gtin_10_3,
Fabricant.code AS Fabricant_14_0,
Fabricant.nom AS Fabricant_14_2,
ClassificationVenteLocale.niveau1 AS ClassificationVenteL_16_2,
ClassificationVenteLocale.niveau2 AS ClassificationVenteL_16_3,
ClassificationVenteLocale.niveau3 AS ClassificationVenteL_16_4,
ClassificationVenteLocale.niveau4 AS ClassificationVenteL_16_5,
MarqueCommerciale.code AS MarqueCommerciale_18_0,
MarqueCommerciale.libellelong AS MarqueCommerciale_18_4,
Composition.code AS Composition_20_0,
CompositionTextile.code AS CompositionTextile_21_0,
AssoArticleInterfaceBalance.datem AS AssoArticleInterface_23_0,
AssoArticleInterfaceBalance.lib_envoi AS AssoArticleInterface_23_3,
AssoArticleInterfaceCaisse.datem AS AssoArticleInterface_24_0,
AssoArticleInterfaceCaisse.lib_envoi AS AssoArticleInterface_24_3,
NULL AS TypeTraitement_25_0,
NULL AS TypeTraitement_25_1,
RayonBalance.code AS RayonBalance_31_0,
RayonBalance.max_cde_article AS RayonBalance_31_5,
RayonBalance.min_cde_article AS RayonBalance_31_6,
TypeTare.code AS TypeTare_32_0,
GrilleDePrix.datem AS GrilleDePrix_34_1,
GrilleDePrix.libelle AS GrilleDePrix_34_3,
FicheAgreage.code AS FicheAgreage_38_0,
Codelec.iddp_periact AS Codelec_40_1,
Codelec.libelle AS Codelec_40_2,
Codelec.niveau1 AS Codelec_40_3,
Codelec.niveau2 AS Codelec_40_4,
Codelec.niveau3 AS Codelec_40_5,
Codelec.niveau4 AS Codelec_40_6,
PerimetreActivite.code AS PerimetreActivite_41_0,
DonneesPersonnalisablesCodelec.gestionreftech AS DonneesPersonnalisab_42_0,
ClassificationArticleInterne.id AS ClassificationArticl_43_0,
ClassificationArticleInterne.niveau1 AS ClassificationArticl_43_2,
DossierCommercial.id AS DossierCommercial_52_0,
DossierCommercial.codefourndc AS DossierCommercial_52_1,
DossierCommercial.anneedc AS DossierCommercial_52_3,
DossierCommercial.codeclassdc AS DossierCommercial_52_4,
DossierCommercial.numversiondc AS DossierCommercial_52_5,
DossierCommercial.indice AS DossierCommercial_52_6,
DossierCommercial.code_ss_classement AS DossierCommercial_52_7,

421
https://dalibo.com/formations
SQL pour PostgreSQL

OrigineNegociation.code AS OrigineNegociation_53_0,
MotifBlocageInformation.libellelong AS MotifBlocageInformat_54_3,
ArbreLogistique.id AS ArbreLogistique_63_1,
ArbreLogistique.codesap AS ArbreLogistique_63_5,
Fournisseur.code AS Fournisseur_66_0,
Fournisseur.nom AS Fournisseur_66_2,
Filiere.code AS Filiere_67_0,
Filiere.nom AS Filiere_67_2,
ValorisationAchat.val_ach_patc AS Valorisation_74_3,
LienPrixVente.code AS LienPrixVente_76_0,
LienPrixVente.datem AS LienPrixVente_76_1,
LienGratuite.code AS LienGratuite_78_0,
LienGratuite.datem AS LienGratuite_78_1,
LienCoordonnable.code AS LienCoordonnable_79_0,
LienCoordonnable.datem AS LienCoordonnable_79_1,
LienStatistique.code AS LienStatistique_81_0,
LienStatistique.datem AS LienStatistique_81_1
FROM da_article Article
join (SELECT idarticle,
poids,
ROW_NUMBER()
over (
PARTITION BY RNA.id
ORDER BY INNERSEARCH.poids) RN,
titre,
nom,
prenom
FROM da_article RNA
join (SELECT idarticle,
pkg_db_indexation.CALCULPOIDSMOTS(chaine,
'foire vins%') AS POIDS,
DECODE(index_clerecherche, 'Piste.titre', chaine,
'') AS TITRE,
DECODE(index_clerecherche, 'Artiste.nom_prenom',
SUBSTR(chaine, 0, INSTR(chaine, '_') - 1),
'') AS NOM,
DECODE(index_clerecherche, 'Artiste.nom_prenom',
SUBSTR(chaine, INSTR(chaine, '_') + 1),
'') AS PRENOM
FROM ((SELECT index_idenreg AS IDARTICLE,
C.cde_art AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =

422
10. SQL : CE QU’IL NE FAUT PAS FAIRE

'Article.codeArticle'
join da_article C
ON id = index_idenreg
WHERE mots_mot = 'foire'
INTERSECT
SELECT index_idenreg AS IDARTICLE,
C.cde_art AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'Article.codeArticle'
join da_article C
ON id = index_idenreg
WHERE mots_mot LIKE 'vins%'
AND 1 = 1)
UNION ALL
(SELECT index_idenreg AS IDARTICLE,
C.cde_art_bal AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'Article.codeArticleBalance'
join da_article C
ON id = index_idenreg
WHERE mots_mot = 'foire'
INTERSECT
SELECT index_idenreg AS IDARTICLE,
C.cde_art_bal AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'Article.codeArticleBalance'
join da_article C
ON id = index_idenreg
WHERE mots_mot LIKE 'vins%'
AND 1 = 1)
UNION ALL
(SELECT index_idenreg AS IDARTICLE,
C.lib_com AS CHAINE,
index_clerecherche

423
https://dalibo.com/formations
SQL pour PostgreSQL

FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'Article.libelleCommercial'
join da_article C
ON id = index_idenreg
WHERE mots_mot = 'foire'
INTERSECT
SELECT index_idenreg AS IDARTICLE,
C.lib_com AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'Article.libelleCommercial'
join da_article C
ON id = index_idenreg
WHERE mots_mot LIKE 'vins%'
AND 1 = 1)
UNION ALL
(SELECT idda_article AS IDARTICLE,
C.gtin AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'Gtin.gtin'
join da_gtin C
ON id = index_idenreg
WHERE mots_mot = 'foire'
INTERSECT
SELECT idda_article AS IDARTICLE,
C.gtin AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'Gtin.gtin'
join da_gtin C
ON id = index_idenreg
WHERE mots_mot LIKE 'vins%'
AND 1 = 1)

424
10. SQL : CE QU’IL NE FAUT PAS FAIRE

UNION ALL
(SELECT idda_article AS IDARTICLE,
C.ref_frn AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'ArbreLogistique.referenceFournisseur'
join da_arblogi C
ON id = index_idenreg
WHERE mots_mot = 'foire'
INTERSECT
SELECT idda_article AS IDARTICLE,
C.ref_frn AS CHAINE,
index_clerecherche
FROM cstd_mots M
join cstd_index I
ON I.mots_id = M.mots_id
AND index_clerecherche =
'ArbreLogistique.referenceFournisseur'
join da_arblogi C
ON id = index_idenreg
WHERE mots_mot LIKE 'vins%'
AND 1 = 1))) INNERSEARCH
ON INNERSEARCH.idarticle = RNA.id) SEARCHMC
ON SEARCHMC.idarticle = Article.id
AND 1 = 1
left join da_artcmpl ArticleComplement
ON Article.id = ArticleComplement.idda_article
left join dp_gpdtcul GenreProduitCulturel
ON ArticleComplement.iddp_gpdtcul = GenreProduitCulturel.id
left join df_collect Collection
ON ArticleComplement.iddf_collect = Collection.id
left join da_gtin Gtin
ON Article.id = Gtin.idda_article
AND Gtin.principal = 1
AND Gtin.db_suplog = 0
left join df_fabric Fabricant
ON Article.iddf_fabric = Fabricant.id
left join dp_clsvtel ClassificationVenteLocale
ON Article.iddp_clsvtel = ClassificationVenteLocale.id
left join dp_marqcom MarqueCommerciale
ON Article.iddp_marqcom = MarqueCommerciale.id
left join da_compo Composition
ON Composition.id = Article.idda_compo

425
https://dalibo.com/formations
SQL pour PostgreSQL

left join dp_comptex CompositionTextile


ON CompositionTextile.id = Article.iddp_comptex
left join da_arttrai AssoArticleInterfaceBalance
ON AssoArticleInterfaceBalance.idda_article = Article.id
AND AssoArticleInterfaceBalance.iddp_tinterf = 1
left join da_arttrai AssoArticleInterfaceCaisse
ON AssoArticleInterfaceCaisse.idda_article = Article.id
AND AssoArticleInterfaceCaisse.iddp_tinterf = 4
left join dp_raybala RayonBalance
ON Article.iddp_raybala = RayonBalance.id
left join dp_valdico TypeTare
ON TypeTare.id = RayonBalance.iddp_typtare
left join df_categor Categorie
ON Categorie.id = Article.iddf_categor
left join df_grille GrilleDePrix
ON GrilleDePrix.id = Categorie.iddf_grille
left join dp_agreage FicheAgreage
ON FicheAgreage.id = Article.iddp_agreage
join dp_codelec Codelec
ON Article.iddp_codelec = Codelec.id
left join dp_periact PerimetreActivite
ON PerimetreActivite.id = Codelec.iddp_periact
left join dp_perscod DonneesPersonnalisablesCodelec
ON Codelec.id = DonneesPersonnalisablesCodelec.iddp_codelec
AND DonneesPersonnalisablesCodelec.db_suplog = 0
AND DonneesPersonnalisablesCodelec.iddb_sitecl = 1012124
left join dp_clsart ClassificationArticleInterne
ON DonneesPersonnalisablesCodelec.iddp_clsart =
ClassificationArticleInterne.id
left join da_artdeno ArticleDenormalise
ON Article.id = ArticleDenormalise.idda_article
left join df_clasmnt ClassementFournisseur
ON ArticleDenormalise.iddf_clasmnt = ClassementFournisseur.id
left join tr_dosclas DossierDeClassement
ON ClassementFournisseur.id = DossierDeClassement.iddf_clasmnt
AND DossierDeClassement.date_deb <= '2013-09-27'
AND COALESCE(DossierDeClassement.date_fin,
TO_DATE('31129999', 'DDMMYYYY')) >= '2013-09-27'
left join tr_doscomm DossierCommercial
ON DossierDeClassement.idtr_doscomm = DossierCommercial.id
left join dp_valdico OrigineNegociation
ON DossierCommercial.iddp_dossref = OrigineNegociation.id
left join dp_motbloc MotifBlocageInformation
ON MotifBlocageInformation.id = ArticleDenormalise.idda_motinf
left join da_arblogi ArbreLogistique
ON Article.id = ArbreLogistique.idda_article

426
10. SQL : CE QU’IL NE FAUT PAS FAIRE

AND ArbreLogistique.princ = 1
AND ArbreLogistique.db_suplog = 0
left join df_filiere Filiere
ON ArbreLogistique.iddf_filiere = Filiere.id
left join df_fourn Fournisseur
ON Filiere.iddf_fourn = Fournisseur.id
left join od_dosal dossierALValo
ON dossierALValo.idda_arblogi = ArbreLogistique.id
AND dossierALValo.idod_dossier IS NULL
left join tt_val_dal valoDossier
ON valoDossier.idod_dosal = dossierALValo.id
AND valoDossier.estarecalculer = 0
left join tt_valo ValorisationAchat
ON ValorisationAchat.idtt_val_dal = valoDossier.id
AND ValorisationAchat.date_modif_retro IS NULL
AND ValorisationAchat.date_debut_achat <= '2013-09-27'
AND COALESCE(ValorisationAchat.date_fin_achat,
TO_DATE('31129999', 'DDMMYYYY')) >= '2013-09-27'
AND ValorisationAchat.val_ach_pab IS NOT NULL
left join da_lienart assoALPXVT
ON assoALPXVT.idda_article = Article.id
AND assoALPXVT.iddp_typlien = 14893
left join da_lien LienPrixVente
ON LienPrixVente.id = assoALPXVT.idda_lien
left join da_lienart assoALGRAT
ON assoALGRAT.idda_article = Article.id
AND assoALGRAT.iddp_typlien = 14894
left join da_lien LienGratuite
ON LienGratuite.id = assoALGRAT.idda_lien
left join da_lienart assoALCOOR
ON assoALCOOR.idda_article = Article.id
AND assoALCOOR.iddp_typlien = 14899
left join da_lien LienCoordonnable
ON LienCoordonnable.id = assoALCOOR.idda_lien
left join da_lienal assoALSTAT
ON assoALSTAT.idda_arblogi = ArbreLogistique.id
AND assoALSTAT.iddp_typlien = 14897
left join da_lien LienStatistique
ON LienStatistique.id = assoALSTAT.idda_lien WHERE
SEARCHMC.rn = 1
AND ( ValorisationAchat.id IS NULL
OR ValorisationAchat.date_debut_achat = (
SELECT MAX(VALMAX.date_debut_achat)
FROM tt_valo VALMAX
WHERE VALMAX.idtt_val_dal = ValorisationAchat.idtt_val_dal
AND VALMAX.date_modif_retro IS NULL

427
https://dalibo.com/formations
SQL pour PostgreSQL

AND VALMAX.val_ach_pab IS NOT NULL


AND VALMAX.date_debut_achat <= '2013-09-27') )
AND ( Article.id IN (SELECT A.id
FROM da_article A
join du_ucutiar AssoUcUtiAr
ON AssoUcUtiAr.idda_article = A.id
join du_asucuti AssoUcUti
ON AssoUcUti.id = AssoUcUtiAr.iddu_asucuti
WHERE ( AssoUcUti.iddu_uti IN ( 90000000000022 ) )
AND a.iddb_sitecl = 1012124) )
AND Article.db_suplog = 0
ORDER BY SEARCHMC.poids ASC

Comprendre un tel monstre implique souvent de l’imprimer pour acquérir une vision glob-
ale et prendre des notes :

428
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Ce code a été généré initialement par Hibernate, puis édité plusieurs fois à la main.

429
https://dalibo.com/formations
SQL pour PostgreSQL

10.15 RECHERCHE TEXTUELLE

• Objectif
– ajouter un moteur de recherche à l’application
• Pratique courante
– utiliser l’opérateur LIKE
• Problèmes
– requiert des index spécialisés
– recherche uniquement le terme exact
• Solutions
– pg_trgm
– Full Text Search

Les bases de données qui stockent des données textuelles ont souvent pour but de per-
mettre des recherches sur ces données textuelles.

La première solution envisagée lorsque le besoin se fait sentir est d’utiliser l’opérateur
LIKE. Il permet en effet de réaliser des recherches de motif sur une colonne stockant
des données textuelles. C’est une solution simple et qui peut s’avérer simpliste dans de
nombreux cas.

Tout d’abord, les recherches de type LIKE '%motif%' ne peuvent généralement pas tirer
partie d’un index btree normal. Cela étant dit, l’extension pg_trgm permet d’optimiser ces
recherches à l’aide d’un index GiST ou GIN. Elle fait partie des extensions standard et ne
nécessite pas d’adaptation du code.

Exemples

L’exemple ci-dessous montre l’utilisation du module pg_trgm pour accélérer une


recherche avec LIKE '%motif%' :
CREATE INDEX idx_appellation_libelle ON appellation
USING btree (libelle varchar_pattern_ops);

EXPLAIN SELECT * FROM appellation WHERE libelle LIKE '%wur%';


QUERY PLAN
------------------------------------------------------------
Seq Scan on appellation (cost=0.00..6.99 rows=3 width=24)
Filter: (libelle ~~ '%wur%'::text)

CREATE EXTENSION pg_trgm;

CREATE INDEX idx_appellation_libelle_trgm ON appellation


USING gist (libelle gist_trgm_ops);

430
10. SQL : CE QU’IL NE FAUT PAS FAIRE

EXPLAIN SELECT * FROM appellation WHERE libelle LIKE '%wur%';


QUERY PLAN

-----------------------------------------------------------------------------
Bitmap Heap Scan on appellation (cost=4.27..7.41 rows=3 width=24)
Recheck Cond: (libelle ~~ '%wur%'::text)
-> Bitmap Index Scan on idx_appellation_libelle_trgm (cost=0.00..4.27...)
Index Cond: (libelle ~~ '%wur%'::text)

Mais cette solution n’offre pas la même souplesse que la recherche plein texte, en anglais
Full Text Search, de PostgreSQL. Elle est cependant plus complexe à mettre en œuvre et
possède une syntaxe spécifique.

10.16 CONCLUSION

• La base est là pour vous aider


• Le modèle relationnel doit être compris et appliqué
• Avant de contourner un problème, chercher s’il n’existe pas une fonctionnalité
dédiée

431
https://dalibo.com/formations
SQL pour PostgreSQL

10.17 TRAVAUX PRATIQUES

Ce TP utilise les tables voitures et voitures_ecv. Les deux tables voitures


et voitures_ecv peuvent être téléchargées sur https://dali.bo/tp_voitures. Elles
s’importent dans n’importe quelle base de manière classique (un message sur un schéma
public préexistant est normal) :
pg_restore -d nomdelabasecible voitures.dump

Ne pas oublier d’effectuer un VACUUM ANALYZE.

10.17.1 NORMALISATION DE SCHÉMA

La table voitures viole la première forme normale (attribut répétitif, non atomique). De
plus elle n’a pas de clé primaire.

Renommer la table en voitures_orig.


Ne pas la supprimer (nous en aurons besoin plus tard).

Écrire des requêtes permettant d’éclater cette table en trois ta-


bles :
voitures, caracteristiques et caracteristiques_voitures.
(La fonction regexp_split_to_table permettra de séparer les
champs de caractéristiques.)

Mettre en place les contraintes d’intégrité : clé primaire sur


chaque table, et clés étrangères.
Ne pas prévoir encore d’index supplémentaire.
Attention : la table de départ contient des immatriculations en
doublon !

Tenter d’insérer une voiture avec les caractéristiques « ABS » (ma-


jusucules) et « phares LED ».

Comparer les performances entre les deux modèles pour une


recherche des voitures ayant un toit ouvrant.

432
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Les plans sont-ils les mêmes si l’on cherche une caractéristique


qui n’existe pas ?

Indexer la colonne de clé étrangère caracteristiques_voitures.carateristique


et voir ce que devient le plan de la dernière requête.

Rechercher une voitures possédant les 3 options ABS, toit ou-


vrant et 4 roues motrices, et voir le plan.

10.17.2 ENTITÉ-CLÉ-VALEUR

Une autre version de la table voiture existe aussi dans cette base au format « entité/-
clé/valeur » c’est la table voitures_ecv. Sa clé primaire est entite (immatriculation) /
cle (caractéristique). En pratique il n’y a que des booléens.

Afficher toutes les caractéristiques d’une voiture au hasard (par


exemple ZY-745-KT).

Trouver toutes les caractéristiques de toutes les voitures ayant


un toit ouvrant dans voitures_ecv. Trier par immatriculation.
Quel est le plan d’exécution ?

hstore est une extension qui permet de stocker des clés/valeur dans un champ. Sa doc-
umentation est sur le site du projet117 .

Installer l’extension hstore.


Convertir cette table pour qu’elle utilise une ligne par immatricu-
lation, avec les caractéristiques dans un champ hstore.

Rechercher la voiture précédente.

117
https://docs.postgresql.fr/current/hstore.html

433
https://dalibo.com/formations
SQL pour PostgreSQL

Insérer une voiture avec les caractéristiques couleur=>vert et


phares=>LED.

Définir un index de type GiST sur ce champ hstore.


Retrouver la voiture insérée par ses caractéristiques.

10.17.3 INDEXATION DE CHAMPS TABLEAU

Il est possible, si on peut réécrire la requête, d’obtenir de bonnes performances avec


la première table voitures_orig. En effet, PostgreSQL sait indexer des tableaux et
des fonctions. Il saurait donc indexer un tableau résultat d’une fonction sur le champ
caracteristiques.

Trouver cette fonction dans la documentation de PostgreSQL


(chercher dans les fonctions de découpage de chaîne de
caractères).

Définir un index fonctionnel sur le résultat de cette fonction, de


type GIN.

Rechercher toutes les voitures avec toit ouvrant et voir le plan.

10.17.4 PAGINATION ET INDEX

La pagination est une fonctionnalité que l’on retrouve de plus en plus souvent, surtout
depuis que les applications web ont pris une place prépondérante.

Nous allons utiliser une version simplifiée d’une table de forum. La table posts peut être
téléchargée depuis https://dali.bo/tp_posts (dump de 358 Mo, 758 Mo sur disque), et
restaurée dans n’importe quelle base de manière classique (un message sur un schéma
public préexistant est normal) :

pg_restore -d nomdelabasecible posts.dump

Ne pas oublier d’effectuer ensuite un VACUUM ANALYZE.

434
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Nous voulons afficher le plus rapidement possible les messages (posts) associés à un arti-
cle : les 10 premiers, puis du 11 au 20, etc. Nous allons examiner les différentes stratégies
possibles.

La table contient 5 000 articles de 1000 posts, d’au plus 200 signes.

La description de la table est :

# \d posts
Table « public.posts »
Colonne | Type | Collationnement | NULL-able | Par défaut
------------+--------------------------+-----------------+-----------+------------
id_article | integer | | |
id_post | integer | | |
ts | timestamp with time zone | | |
message | text | | |
Index :
"posts_ts_idx" btree (ts)

Pour la clarté des plans, désactiver le JIT et le parallélisme dans votre session :
SET jit to off ;
SET max_parallel_workers_per_gather TO 0 ;

Écrire une requête permettant de récupérer les 10 premiers posts


de l’article d’id_article=12, triés dans l’ordre de id_post. Il n’y
a pas d’index, la requête va être très lente.

Créer un index permettant d’améliorer cette requête.

Utiliser les clauses LIMIT et OFFSET pour récupérer les 10 posts


suivants. Puis du post 901 au 921. Que constate-t-on sur le plan
d’exécution ?

Trouver une réécriture de la requête pour trouver directement les


posts 901 à 911 une fois connu le post 900 récupéré au travers
de la pagination.

435
https://dalibo.com/formations
SQL pour PostgreSQL

10.17.5 CLAUSES WHERE ET PIÈGES

Nous utilisons toujours la table posts. Nous allons maintenant manipuler le champ ts,
de type timestamp. Ce champ est indexé.

La requête
SELECT * FROM posts WHERE to_char(ts,'YYYYMM')='201302'
retourne tous les enregistrements de février 2013. Examiner
son plan d’exécution. Où est le problème ?

Réécrire la clause WHERE avec une inégalité de dates pour utiliser


l’index sur ts.

Plus compliqué : retourner tous les posts ayant eu lieu un


dimanche, en 2013, en passant par un index et en une seule
requête.
(Indice : il est possible de générer la liste de tous les di-
manches de l’année 2013 avec generate_series('2013-01-06
00:00:00','2014-01-01 00:00:00', INTERVAL '7 days'))

On cherche un article à peu près au tiers de la liste avec la requête


suivante. Pourquoi est-elle si lente ?

SELECT * FROM posts


WHERE id_article =
(SELECT max(id_article) * 0.333
FROM posts
) ;

436
10. SQL : CE QU’IL NE FAUT PAS FAIRE

10.18 TRAVAUX PRATIQUES (SOLUTIONS)

10.18.1 NORMALISATION DE SCHÉMA

Renommer la table en voitures_orig.


Ne pas la supprimer (nous en aurons besoin plus tard).

ALTER TABLE voitures rename TO voitures_orig;

Écrire des requêtes permettant d’éclater cette table en trois ta-


bles :
voitures, caracteristiques et caracteristiques_voitures.
(La fonction regexp_split_to_table permettra de séparer les
champs de caractéristiques.)

CREATE TABLE voitures AS


SELECT DISTINCT ON (immatriculation) immatriculation, modele
FROM voitures_orig ;

ALTER TABLE voitures ADD PRIMARY KEY (immatriculation);

CREATE TABLE caracteristiques


AS SELECT *
FROM (
SELECT DISTINCT
regexp_split_to_table(caracteristiques,',') caracteristique
FROM voitures_orig)
AS tmp
WHERE caracteristique <> '' ;

ALTER TABLE caracteristiques ADD PRIMARY KEY (caracteristique);

CREATE TABLE caracteristiques_voitures


AS SELECT DISTINCT *
FROM (
SELECT
immatriculation,
regexp_split_to_table(caracteristiques,',') caracteristique
FROM voitures_orig
)
AS tmp
WHERE caracteristique <> '';

VACUUM ANALYZE ;
437
https://dalibo.com/formations
SQL pour PostgreSQL

\d+
Liste des relations
Schéma | Nom | Type | Propriétaire | Persistenz | Taille | ..
--------+---------------------------+-------+--------------+------------+---------+---
public | caracteristiques | table | postgres | permanent | 48 kB |
public | caracteristiques_voitures | table | postgres | permanent | 3208 kB |
public | voitures | table | postgres | permanent | 4952 kB |
public | voitures_ecv | table | postgres | permanent | 3336 kB |
public | voitures_orig | table | postgres | permanent | 5736 kB |

Mettre en place les contraintes d’intégrité : clé primaire sur


chaque table, et clés étrangères.
Ne pas prévoir encore d’index supplémentaire.
Attention : la table de départ contient des immatriculations en
doublon !

Sur caracteristiques_voitures, la clé primaire comprend les deux colonnes, et donc


interdit qu’une même caractéristique soit présente deux fois sur la même voiture :
ALTER TABLE caracteristiques_voitures
ADD PRIMARY KEY (immatriculation,caracteristique);

Clé étrangère de cette table vers les deux autres tables :

ALTER TABLE caracteristiques_voitures


ADD FOREIGN KEY (immatriculation)
REFERENCES voitures(immatriculation);

ALTER TABLE caracteristiques_voitures


ADD FOREIGN KEY (caracteristique)
REFERENCES caracteristiques(caracteristique);

Tenter d’insérer une Clio avec les caractéristiques « ABS » (ma-


jusucules) et « phares LED ».

En toute rigueur il faut le faire dans une transaction :


BEGIN ;
INSERT INTO voitures VALUES ('AA-007-JB','clio') ;
INSERT INTO caracteristiques_voitures (immatriculation, caracteristique)
VALUES ('AA-007-JB','ABS') ;
INSERT INTO caracteristiques_voitures (immatriculation, caracteristique)

438
10. SQL : CE QU’IL NE FAUT PAS FAIRE

VALUES ('AA-007-JB','phares LED') ;


COMMIT ;

Évidemment, cela échoue :

ERROR: insert or update on table "caracteristiques_voitures" violates foreign key


constraint "caracteristiques_voitures_caracteristique_fkey"
DÉTAIL : Key (caracteristique)=(ABS) is not present in table "caracteristiques".

ERROR: insert or update on table "caracteristiques_voitures" violates foreign key


constraint "caracteristiques_voitures_immatriculation_fkey"
DÉTAIL : Key (immatriculation)=(AA-007-JB) is not present in table "voitures".

En cas d’erreur, c’est exactement ce que l’on veut.

Pour que l’insertion fonctionne, il faut corriger la casse de « ABS » et déclarer la nouvelle
propriété :
BEGIN ;
INSERT INTO voitures VALUES ('AA-007-JB','clio') ;
INSERT INTO caracteristiques VALUES ('phares LED') ;
INSERT INTO caracteristiques_voitures (immatriculation, caracteristique)
VALUES ('AA-007-JB','abs') ;
INSERT INTO caracteristiques_voitures (immatriculation, caracteristique)
VALUES ('AA-007-JB','phares LED') ;
COMMIT ;

Comparer les performances d’une recherche des voitures ayant


un toit ouvrant avec l’ancien et le nouveau modèle.

La version la plus simple est :


SELECT * FROM voitures_orig
WHERE caracteristiques like '%toit ouvrant%';

Plus rigoureusement ([[:>:]] et [[:<:]] indiquent des frontières de mots.), on préfér-


eras :
EXPLAIN ANALYZE
SELECT * FROM voitures_orig
WHERE caracteristiques ~ E'[[:<:]]toit ouvrant[[:>:]]';

QUERY PLAN
--------------------------------------------------------------------------
Seq Scan on voitures_orig (cost=0.00..1962.00 rows=8419 width=25)
(actual time=0.030..92.226 rows=8358 loops=1)
Filter: (caracteristiques ~ '[[:<:]]toit ouvrant[[:>:]]'::text)
439
https://dalibo.com/formations
SQL pour PostgreSQL

Rows Removed by Filter: 91642


Planning Time: 0.658 ms
Execution Time: 92.512 ms

Toute la table a été parcourue, 91 642 lignes ont été rejetées, 8358 retenues (~8 %). Les
estimations statistiques sont correctes.

NB : pour la lisibilité, les plans n’utilisent pas l’option BUFFERS d’EXPLAIN. Si on l’active,
on pourra vérifier que tous les accès se font bien dans le cache de PostgreSQL (shared
hits).

Avec le nouveau schéma on peut écrire la requête simplement avec une simple jointure :
SELECT *
FROM voitures
INNER JOIN caracteristiques_voitures
ON ( caracteristiques_voitures.immatriculation=voitures.immatriculation)
WHERE caracteristique = 'toit ouvrant' ;

Il n’y a pas doublement de lignes si une caractéristique est en double car la clé primaire
l’interdit. Sans cette contrainte, une autre écriture serait nécessaire :
SELECT *
FROM voitures
WHERE EXISTS (
SELECT 1 FROM caracteristiques_voitures
WHERE caracteristiques_voitures.immatriculation=voitures.immatriculation
AND caracteristique = 'toit ouvrant'
) ;

Dans les deux cas, on obtient ce plan118 :

QUERY PLAN
----------------------------------------------------------------------
Hash Join (cost=1225.80..3102.17 rows=8329 width=16)
(actual time=6.307..31.811 rows=8358 loops=1)
Hash Cond: (voitures.immatriculation = caracteristiques_voitures.immatriculation)
-> Seq Scan on voitures (cost=0.00..1613.89 rows=99989 width=16)
(actual time=0.019..10.432 rows=99989 loops=1)
-> Hash (cost=1121.69..1121.69 rows=8329 width=10)
(actual time=6.278..6.279 rows=8358 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 577kB
-> Seq Scan on caracteristiques_voitures
(cost=0.00..1121.69 rows=8329 width=10)
(actual time=0.004..5.077 rows=8358 loops=1)
118
https://explain.dalibo.com/plan/lz

440
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Filter: (caracteristique = 'toit ouvrant'::text)


Rows Removed by Filter: 49697
Planning Time: 0.351 ms
Execution Time: 32.155 ms

Le temps d’exécution est ici plus court malgré un parcours complet de voitures.
PostgreSQL prévoit correctement qu’il ramènera 10 % de cette table, ce qui n’est pas
si discriminant et justifie fréquemment un Seq Scan, surtout que voitures est petite.
caracteristiques_voitures est aussi parcourue entièrement : faute d’index, il n’y a
pas d’autre moyen.

Les plans sont-ils les mêmes si une caractéristique cherchée


n’existe pas ?

Si on cherche une option rare ou n’existant pas, le plan change119 :

EXPLAIN ANALYZE
SELECT *
FROM voitures
INNER JOIN caracteristiques_voitures
ON ( caracteristiques_voitures.immatriculation=voitures.immatriculation)
WHERE caracteristique = 'ordinateur de bord' ;

QUERY PLAN
---------------------------------------------------------------------
Nested Loop (cost=0.42..1130.12 rows=1 width=16)
(actual time=4.849..4.850 rows=0 loops=1)
-> Seq Scan on caracteristiques_voitures (cost=0.00..1121.69 rows=1 width=10)
(actual time=4.848..4.848 rows=0 loops=1)
Filter: (caracteristique = 'ordinateur de bord'::text)
Rows Removed by Filter: 58055
-> Index Scan using voitures_pkey on voitures (cost=0.42..8.44 rows=1 width=16)
(never executed)
Index Cond: (immatriculation = caracteristiques_voitures.immatriculation)
Planning Time: 0.337 ms
Execution Time: 4.872 ms

Avec un seul résultat attendu, ce qui est beaucoup plus discriminant, l’utilisation de l’index
sur voitures devient pertinente.

Avec l’ancien schéma, on doit toujours lire la table voitures_orig en entier.


119
https://explain.dalibo.com/plan/Hij

441
https://dalibo.com/formations
SQL pour PostgreSQL

Indexer la colonne de clé étrangère caracteristiques_voitures.carateristique


et voir ce que devient le plan de la dernière requête.

CREATE INDEX ON caracteristiques_voitures (caracteristique) ;

Le plan d’exécution120 devient foudroyant, puisque la table caracteristiques_voitures


n’est plus intégralement lue :

EXPLAIN ANALYZE
SELECT *
FROM voitures
INNER JOIN caracteristiques_voitures
ON ( caracteristiques_voitures.immatriculation=voitures.immatriculation)
WHERE caracteristique = 'ordinateur de bord' ;

QUERY PLAN
---------------------------------------------------------------------
Nested Loop (cost=0.83..16.78 rows=1 width=16)
(actual time=0.010..0.011 rows=0 loops=1)
-> Index Scan using caracteristiques_voitures_caracteristique_idx
on caracteristiques_voitures
(cost=0.41..8.35 rows=1 width=10)
(actual time=0.010..0.010 rows=0 loops=1)
Index Cond: (caracteristique = 'ordinateur de bord'::text)
-> Index Scan using voitures_pkey on voitures (cost=0.42..8.44 rows=1 width=16)
(never executed)
Index Cond: (immatriculation = caracteristiques_voitures.immatriculation)
Planning Time: 0.268 ms
Execution Time: 0.035 ms

Avec voitures_orig, il existerait aussi des méthodes d’indexation mais elles sont plus
lourdes (index GIN...).

Rechercher une voitures possédant les 3 options ABS, toit ou-


vrant et 4 roues motrices, et voir le plan.

Si on recherche plusieurs options en même temps, l’optimiseur peut améliorer les choses
en prenant en compte la fréquence de chaque option pour restreindre plus efficacement
les recherches. Le plan devient121 :
120
https://explain.dalibo.com/plan/577
121
https://explain.dalibo.com/plan/O0H

442
10. SQL : CE QU’IL NE FAUT PAS FAIRE

EXPLAIN (ANALYZE, COSTS OFF)


SELECT *
FROM voitures
JOIN caracteristiques_voitures AS cr1 USING (immatriculation)
JOIN caracteristiques_voitures AS cr2 USING (immatriculation)
JOIN caracteristiques_voitures AS cr3 USING (immatriculation)
WHERE cr1.caracteristique = 'toit ouvrant'
AND cr2.caracteristique = 'abs'
AND cr3.caracteristique='4 roues motrices' ;

QUERY PLAN
--------------------------------------------------------------------------------
Nested Loop
-> Hash Join
Hash Cond: (cr2.immatriculation = cr1.immatriculation)
-> Bitmap Heap Scan on caracteristiques_voitures cr2
Recheck Cond: (caracteristique = 'abs'::text)
-> Bitmap Index Scan on caracteristiques_voitures_caracteristique_idx
Index Cond: (caracteristique = 'abs'::text)
-> Hash
-> Hash Join
Hash Cond: (cr1.immatriculation = cr3.immatriculation)
-> Bitmap Heap Scan on caracteristiques_voitures cr1
Recheck Cond: (caracteristique = 'toit ouvrant'::text)
-> Bitmap Index Scan
on caracteristiques_voitures_caracteristique_idx
Index Cond: (caracteristique = 'toit ouvrant'::text)
-> Hash
-> Bitmap Heap Scan on caracteristiques_voitures cr3
Recheck Cond: (caracteristique =
'4 roues motrices'::text)
-> Bitmap Index Scan
on caracteristiques_voitures_caracteristique_idx
Index Cond: (caracteristique =
'4 roues motrices'::text)
-> Index Scan using voitures_pkey on voitures
Index Cond: (immatriculation = cr1.immatriculation)

Ce plan parcoure deux index, joins leurs résultats, fait de même avec le résultat de l’index
pour la 3è caractéristique, puis opère la jointure finale avec la table principale par l’index
sur immatriculation (un plan complet indiquerait une estimation de 56 lignes de résultat,
même si le résultat final est de 461 lignes).
443
https://dalibo.com/formations
SQL pour PostgreSQL

Mais les problématiques de performances ne sont pas le plus important dans ce cas. Ce
qu’on gagne réellement, c’est la garantie que les caractéristiques ne seront que celles
existant dans la table caractéristique, ce qui évite d’avoir à réparer la base plus tard.

10.18.2 ENTITÉ-CLÉ-VALEUR

Afficher toutes les caractéristiques d’une voiture au hasard

SELECT * FROM voitures_ecv


WHERE entite = 'ZY-745-KT' ;

entite | cle | valeur


-----------+-----------------------+--------
ZY-745-KT | climatisation | t
ZY-745-KT | jantes aluminium | t
ZY-745-KT | regulateur de vitesse | t
ZY-745-KT | toit ouvrant | t

Trouver toutes les caractéristiques de toutes les voitures ayant


un toit ouvrant dans voitures_ecv. Trier par immatriculation.
Quel est le plan d’exécution ?

Autrement dit : on sélectionne toutes les voitures avec un toit ouvrant, et l’on veut toutes
les caractéristiques de ces voitures. Cela nécessite d’appeler deux fois la table.

Là encore une jointure de la table avec elle-même sur entite serait possible, mais serait
dangereuse dans les cas où il y a énormément de propriétés. On préférera encore la
version avec EXISTS, et PostgreSQL en fera spontanément une jointure122 :
EXPLAIN ANALYZE
SELECT * FROM voitures_ecv
WHERE EXISTS (
SELECT 1 FROM voitures_ecv test
WHERE test.entite=voitures_ecv.entite
AND cle = 'toit ouvrant' AND valeur = true
)
ORDER BY entite ;

QUERY PLAN
---------------------------------------------------------------------
Sort (cost=3468.93..3507.74 rows=15527 width=25)
122
https://explain.dalibo.com/plan/nn2

444
10. SQL : CE QU’IL NE FAUT PAS FAIRE

(actual time=29.854..30.692 rows=17782 loops=1)


Sort Key: voitures_ecv.entite
Sort Method: quicksort Memory: 2109kB
-> Hash Join (cost=1243.09..2388.05 rows=15527 width=25)
(actual time=6.915..23.964 rows=17782 loops=1)
Hash Cond: (voitures_ecv.entite = test.entite)
-> Seq Scan on voitures_ecv (cost=0.00..992.55 rows=58055 width=25)
(actual time=0.006..4.242 rows=58055 loops=1)
-> Hash (cost=1137.69..1137.69 rows=8432 width=10)
(actual time=6.899..6.899 rows=8358 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 471kB
-> Seq Scan on voitures_ecv test
(cost=0.00..1137.69 rows=8432 width=10)
(actual time=0.005..5.615 rows=8358 loops=1)
Filter: (valeur AND (cle = 'toit ouvrant'::text))
Rows Removed by Filter: 49697
Planning Time: 0.239 ms
Execution Time: 31.321 ms

Installer l’extension hstore.


Convertir cette table pour qu’elle utilise une ligne par immatricu-
lation, avec les caractéristiques dans un champ hstore.
Une méthode simple est de récupérer les lignes d’une même im-
matriculation avec la fonction array_agg puis de convertir sim-
plement en champ hstore.

hstore est normalement présente sur toutes les installations (ou alors l’administrateur a
négligé d’installer le paquet contrib). Il suffit donc d’une déclaration.

CREATE EXTENSION hstore;

CREATE TABLE voitures_hstore


AS
SELECT entite AS immatriculation,
hstore(array_agg(cle),array_agg(valeur)::text[]) AS caracteristiques
FROM voitures_ecv group by entite;

ALTER TABLE voitures_hstore ADD PRIMARY KEY (immatriculation);

Rechercher la voiture précédente.

445
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT * FROM voitures_hstore


WHERE immatriculation = 'ZY-745-KT' \gx

-[ RECORD 1 ]----+--------------------------------------------------------------
immatriculation | ZY-745-KT
caracteristiques | "toit ouvrant"=>"true", "climatisation"=>"true",
| "jantes aluminium"=>"true", "regulateur de vitesse"=>"true"

L’accès à une caractéristique se fait ainsi (attention aux espaces) :


SELECT immatriculation, caracteristiques -> 'climatisation'
FROM voitures_hstore
WHERE immatriculation = 'ZY-745-KT' ;

Insérer une voiture avec les caractéristiques couleur=>vert et


phares=>LED.

INSERT INTO voitures_hstore


VALUES ('XX-4456-ZT', 'couleur=>vert, phares=>LED'::hstore) ;

Définir un index de type GiST sur ce champ hstore.


Retrouver la voiture insérée par ses caractéristiques.

Les index B-tree classiques sont inadaptés aux types complexes, on préfère donc un index
GiST :
CREATE INDEX voitures_hstore_caracteristiques
ON voitures_hstore
USING gist (caracteristiques);

L’opérateur @> signifie « contient » :


SELECT *
FROM voitures_hstore
WHERE caracteristiques @> 'couleur=>vert' AND caracteristiques @> 'phares=>LED' ;

QUERY PLAN
---------------------------------------------------------------------
Index Scan using voitures_hstore_caracteristiques on voitures_hstore
(cost=0.28..2.30 rows=1 width=55) (actual time=0.033..0.033 rows=1 loops=1)
Index Cond: ((caracteristiques @> '"couleur"=>"vert"'::hstore)
AND (caracteristiques @> '"phares"=>"LED"'::hstore))
Buffers: shared hit=4
Planning Time: 0.055 ms
Execution Time: 0.047 ms

446
10. SQL : CE QU’IL NE FAUT PAS FAIRE

10.18.3 INDEXATION DE CHAMPS TABLEAU

Trouver cette fonction (chercher dans les fonctions de découpage


de chaîne de caractères, dans la documentation de PostgreSQL)

La fonction est regexp_split_to_array :


SELECT immatriculation, modele,
regexp_split_to_array(caracteristiques,',')
FROM voitures_orig
LIMIT 10;

immatriculation | modele | regexp_split_to_array


-----------------+--------+-----------------------------------------
WW-649-AI | twingo | {"regulateur de vitesse"}
QZ-533-JD | clio | {"4 roues motrices","jantes aluminium"}
YY-854-LE | megane | {climatisation}
QD-761-QV | twingo | {""}
LV-277-QC | megane | {abs,"jantes aluminium"}
ZI-003-BQ | kangoo | {"boite automatique",climatisation}
WT-817-IK | megane | {""}
JK-791-XB | megane | {""}
WW-019-EK | megane | {""}
BZ-544-OS | twingo | {""}

La syntaye {} est la représentation texte d’un tableau.

Définir un index fonctionnel sur le résultat de cette fonction, de


type GIN.

CREATE INDEX idx_voitures_array ON voitures_orig


USING gin (regexp_split_to_array(caracteristiques,','));

Rechercher toutes les voitures avec toit ouvrant et voir le plan.

EXPLAIN ANALYZE
SELECT * FROM voitures_orig
WHERE regexp_split_to_array(caracteristiques,',') @> '{"toit ouvrant"}';

QUERY PLAN
--------------------------------------------------------------------------------
Bitmap Heap Scan on voitures_orig (cost=8.87..387.37 rows=500 width=25)
(actual time=0.707..2.756 rows=8358 loops=1)
447
https://dalibo.com/formations
SQL pour PostgreSQL

Recheck Cond: (regexp_split_to_array(caracteristiques, ','::text)


@> '{"toit ouvrant"}'::text[])
Heap Blocks: exact=712
-> Bitmap Index Scan on idx_voitures_array (cost=0.00..8.75 rows=500 width=0)
(actual time=0.631..0.631 rows=8358 loops=1)
Index Cond: (regexp_split_to_array(caracteristiques, ','::text)
@> '{"toit ouvrant"}'::text[])
Planning Time: 0.129 ms
Execution Time: 3.028 ms

Noter que les estimations de statistiques sont plus délicates sur un résultat de fonction.

10.18.4 PAGINATION ET INDEX

Écrire une requête permettant de récupérer les 10 premiers posts


de l’article d’id_article=12, triés dans l’ordre de id_post. Il n’y
a pas d’index, la requête va être lente.

EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE id_article =12
ORDER BY id_post
LIMIT 10 ;

Le plan123 est un parcours complet de la table, rejetant 4 999 000 lignes et en gardant
1000 lignes, suivi d’un tri :

QUERY PLAN
------------------------------------------------------------------------------
Limit (cost=153694.51..153694.53 rows=10 width=115)
(actual time=500.525..500.528 rows=10 loops=1)
-> Sort (cost=153694.51..153696.95 rows=979 width=115)
(actual time=500.524..500.525 rows=10 loops=1)
Sort Key: id_post
Sort Method: top-N heapsort Memory: 27kB
-> Seq Scan on posts (cost=0.00..153673.35 rows=979 width=115)
(actual time=1.300..500.442 rows=1000 loops=1)
Filter: (id_article = 12)
Rows Removed by Filter: 4999000
123
https://explain.dalibo.com/plan/xEs

448
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Planning Time: 0.089 ms


Execution Time: 500.549 ms

Créer un index permettant d’améliorer cette requête.

Un index sur id_article améliorerait déjà les choses. Mais comme on trie sur id_post,
il est intéressant de rajouter aussi cette colonne dans l’index :

CREATE INDEX posts_id_article_id_post ON posts (id_article, id_post);

Testons cet index :

EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE id_article =12
ORDER BY id_post
LIMIT 10 ;

Le plan124 devient :

QUERY PLAN
---------------------------------------------------------
Limit (cost=0.43..18.26 rows=10 width=115)
(actual time=0.043..0.053 rows=10 loops=1)
-> Index Scan using posts_id_article_id_post on posts
(cost=0.43..1745.88 rows=979 width=115)
(actual time=0.042..0.051 rows=10 loops=1)
Index Cond: (id_article = 12)
Planning Time: 0.204 ms
Execution Time: 0.066 ms

C’est beaucoup plus rapide : l’index trouve tout de suite les lignes de l’article cherché, et
retourne les enregistrements directement triés par id_post. On évite de parcourir toute
la table, et il n’y a même pas d’étape de tri (qui serait certes très rapide sur 10 lignes).

Utiliser les clauses LIMIT et OFFSET pour récupérer les 10 posts


suivants. Puis du post 901 au 921. Que constate-t-on sur le plan
d’exécution ?

Les posts 11 à 20 se trouvent rapidement :


124
https://explain.dalibo.com/plan/Fgy

449
https://dalibo.com/formations
SQL pour PostgreSQL

EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE id_article = 12
ORDER BY id_post
LIMIT 10
OFFSET 10;

QUERY PLAN
---------------------------------------------------------
Limit (cost=18.26..36.09 rows=10 width=115)
(actual time=0.020..0.023 rows=10 loops=1)
-> Index Scan using posts_id_article_id_post on posts
(cost=0.43..1745.88 rows=979 width=115)
(actual time=0.017..0.021 rows=20 loops=1)
Index Cond: (id_article = 12)
Planning Time: 0.061 ms
Execution Time: 0.036 ms

Tout va bien. La requête est à peine plus coûteuse. Noter que l’index a ramené 20 lignes
et non 10.

À partir du post 900 :


EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE id_article = 12
ORDER BY id_post
LIMIT 10
OFFSET 900 ;

Le plan125 reste similaire :

QUERY PLAN
---------------------------------------------------------
Limit (cost=1605.04..1622.86 rows=10 width=115)
(actual time=0.216..0.221 rows=10 loops=1)
-> Index Scan using posts_id_article_id_post on posts
(cost=0.43..1745.88 rows=979 width=115)
(actual time=0.018..0.194 rows=910 loops=1)
Index Cond: (id_article = 12)
Planning Time: 0.062 ms
Execution Time: 0.243 ms
125
https://explain.dalibo.com/plan/V05

450
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Cette requête est 4 fois plus lente. Si une exécution unitaire ne pose pas encore problème,
des demandes très répétées poseraient problème. Noter que l’index ramène 910 lignes !
Dans notre exemple idéalisée, les posts sont bien rangés ensemble, et souvent présents
dans les mêmes blocs. C’est très différent dans une table qui beaucoup vécu.

Trouver une réécriture de la requête pour trouver directement les


posts 901 à 911 une fois connu le post 900 récupéré au travers
de la pagination.

Pour se mettre dans la condition du test, récupérons l’enregistrement 900 :


SELECT id_article, id_post
FROM posts
WHERE id_article = 12
ORDER BY id_post
LIMIT 1
OFFSET 899 ;

id_article | id_post
------------+---------
12 | 12900

(La valeur retournée peut différer sur une autre base.)

Il suffit donc de récupérer les 10 articles pour lesquels id_article = 12 et id_post >
12900 :
EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE id_article = 12
AND id_post> 12900
ORDER BY id_post
LIMIT 10;

QUERY PLAN
----------------------------------------------------------------
Limit (cost=0.43..18.29 rows=10 width=115)
(actual time=0.018..0.024 rows=10 loops=1)
-> Index Scan using posts_id_article_id_post on posts
(cost=0.43..1743.02 rows=976 width=115)
(actual time=0.016..0.020 rows=10 loops=1)
Index Cond: ((id_article = 12) AND (id_post > 12900))
Planning Time: 0.111 ms
Execution Time: 0.039 ms
451
https://dalibo.com/formations
SQL pour PostgreSQL

Nous sommes de retour à des temps d’exécution très faibles. Ajouter la condition sur
le id_post permet de limiter à la source le nombre de lignes à récupérer. L’index n’en
renvoie bien que 10.

L’avantage de cette technique par rapport à l’offset est que le temps d’une requête ne
variera que l’on chercher la première ou la millième page.

L’inconvénient est qu’il faut mémoriser l’id_post où l’on s’est arrêté sur la page précé-
dente.

10.18.5 CLAUSES WHERE ET PIÈGES

Nous allons maintenant manipuler le champ ts (de type timestamp) de la table posts.

La requête
SELECT * FROM posts WHERE to_char(ts,'YYYYMM')='201302'
retourne tous les enregistrements de février 2013. Examiner
son plan d’exécution. Où est le problème ?

EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE to_char(ts,'YYYYMM')='201302' ;

Le plan126 est un parcours complet de la table :

QUERY PLAN
---------------------------------------------------------------------
Seq Scan on posts (cost=0.00..187728.49 rows=50000 width=269)
(actual time=0.380..14163.371 rows=18234 loops=1)
Filter: (to_char(ts, 'YYYYMM'::text) = '201302'::text)
Rows Removed by Filter: 9981766
Total runtime: 14166.265 ms

C’est normal : PostgreSQL ne peut pas deviner que to_char(ts,'YYYYMM')='201302'


veut dire « toutes les dates du mois de février 2013 ». Une fonction est pour lui une boîte
noire, et il ne voit pas le lien entre le résultat attendu et les données qu’il va lire.

Ceci est une des causes les plus habituelles de ralentissement de requêtes : une fonction
est appliquée à une colonne, ce qui rend le filtre incompatible avec l’utilisation d’un index.

126
https://explain.dalibo.com/plan/ATT

452
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Réécrire la clause WHERE avec une inégalité de dates.

C’est à nous d’indiquer une clause WHERE au moteur qu’il puisse directement appliquer sur
notre date :

EXPLAIN ANALYZE
SELECT *
FROM posts
WHERE ts >= '2013-02-01'
AND ts < '2013-03-01' ;

Le plan127 montre que l’index est maintenant utilisable :

QUERY PLAN
----------------------------------------------------------------------------------
Index Scan using posts_ts_idx on posts (cost=0.43..998.95 rows=20165 width=115)
(actual time=0.050..5.907 rows=20160 loops=1)
Index Cond: ((ts >= '2013-02-01 00:00:00+01'::timestamp with time zone)
AND (ts < '2013-03-01 00:00:00+01'::timestamp with time zone))
Planning Time: 0.095 ms
Execution Time: 6.526 ms

Noter la conversion automatique du critère en timestamp with time zone.

Plus compliqué : retourner tous les posts ayant eu lieu un


dimanche, en 2013, en passant par un index et en une seule
requête.
(Indice : il est possible de générer la liste de tous les di-
manches de l’année 2013 avec generate_series('2013-01-06
00:00:00','2014-01-01 00:00:00', INTERVAL '7 days'))

Construisons cette requête morceau par morceau. Listons tous les dimanches de 2013
(le premier dimanche est le 6 janvier) :

SELECT generate_series(
'2013-01-06 00:00:00',
'2013-12-31 00:00:00',
INTERVAL '7 days'
) ;

S’il faut calculer le premier dimanche de l’année, cela peut se faire ainsi :
127
https://explain.dalibo.com/plan/GDY

453
https://dalibo.com/formations
SQL pour PostgreSQL

WITH premiersjours AS (
SELECT '2000-01-01'::timestamp + i * interval '1 year' AS jan1
FROM generate_series(1, 30) i
),
dimanches AS (
SELECT jan1,
jan1
+ mod(13-extract(dow FROM (jan1 - interval '1 day'))::int, 7)
* interval '1 day'
AS dim1
FROM premiersjours
)
SELECT jan1, dim1
FROM dimanches ;

On n’a encore que des dates à minuit. Il faut calculer les heures de début et de fin de
chaque dimanche :

SELECT i AS debut,
i + INTERVAL '1 day' AS fin
FROM generate_series(
'2013-01-06 00:00:00',
'2013-12-31 00:00:00',
INTERVAL '7 days'
) g(i) ;

debut | fin
------------------------+------------------------
2013-01-06 00:00:00+01 | 2013-01-07 00:00:00+01
2013-01-13 00:00:00+01 | 2013-01-14 00:00:00+01
...
2013-12-29 00:00:00+01 | 2013-12-30 00:00:00+01

Il ne nous reste plus qu’à joindre ces deux ensembles. Comme clause de jointure, on teste
la présence de la date du post dans un des intervalles des dimanches :

EXPLAIN ANALYZE
WITH dimanches AS (
SELECT i AS debut,
i + INTERVAL '1 day' AS fin
FROM generate_series(
'2013-01-06 00:00:00',
'2013-12-31 00:00:00',
INTERVAL '7 days'
) g(i)
)
SELECT posts.*

454
10. SQL : CE QU’IL NE FAUT PAS FAIRE

FROM posts
JOIN dimanches
ON (posts.ts >= dimanches.debut AND posts.ts < dimanches.fin) ;

Le plan128 devient :

QUERY PLAN
------------------------------------------------------------------------------
Nested Loop (cost=0.44..17086517.00 rows=555555556 width=115)
(actual time=0.038..12.978 rows=37440 loops=1)
-> Function Scan on generate_series g (cost=0.00..10.00 rows=1000 width=8)
(actual time=0.016..0.031 rows=52 loops=1)
-> Index Scan using posts_ts_idx on posts
(cost=0.43..11530.95 rows=555556 width=115)
(actual time=0.009..0.168 rows=720 loops=52)
Index Cond: ((ts >= g.i) AND (ts < (g.i + '1 day'::interval)))
Planning Time: 0.131 ms
Execution Time: 14.178 ms

PostgreSQL génère les 52 lignes d’intervalles (noter qu’il ne sait pas estimer le résultat de
cette fonction), puis fait 52 appels à l’index (noter le loops=52). C’est efficace.

Attention : des inéqui-jointures entraînent forcément des nested loops (pour chaque ligne
d’une table, on va chercher les lignes d’une autre table). Sur de grands volumes, ce ne peut
pas être efficace. Ici, tout va bien parce que la liste des dimanches est raisonnablement
courte.

On cherche un article à peu près au tiers de la liste avec la requête


suivante. Pourquoi est-elle si lente ?

SELECT * FROM posts


WHERE id_article =
(SELECT max(id_article) * 0.333
FROM posts
) ;

Le plan129 est :

QUERY PLAN
--------------------------------------------------------------------------------
Seq Scan on posts (cost=0.48..166135.48 rows=25000 width=115)
(actual time=333.363..1000.696 rows=1000 loops=1)
128
https://explain.dalibo.com/plan/nN5
129
https://explain.dalibo.com/plan/6GI

455
https://dalibo.com/formations
SQL pour PostgreSQL

Filter: ((id_article)::numeric = $1)


Rows Removed by Filter: 4999000
InitPlan 2 (returns $1)
-> Result (cost=0.46..0.48 rows=1 width=32)
(actual time=0.016..0.017 rows=1 loops=1)
InitPlan 1 (returns $0)
-> Limit (cost=0.43..0.46 rows=1 width=4)
(actual time=0.012..0.014 rows=1 loops=1)
-> Index Only Scan Backward using posts_id_article_id_post
on posts posts_1
(cost=0.43..142352.43 rows=5000000 width=4)
(actual time=0.012..0.012 rows=1 loops=1)
Index Cond: (id_article IS NOT NULL)
Heap Fetches: 0
Planning Time: 0.097 ms
Execution Time: 1000.753 ms

Ce plan indique une recherche du numéro d’article maximal (il est dans l’index ; noter
que PostgreSQL restreint à une valeur non vide), puis il calcule la valeur correspondant
au tiers et la met dans $1. Tout ceci est rapide. La partie lente est le Seq Scan pour
retrouver cette valeur, avec un filtre et non par l’index.

Le problème est visible sur le filtre même :

Filter: ((id_article)::numeric = $1)

(id_article)::numeric signifie que tous les id_article (des entiers) sont convertis en
numeric pour ensuite être comparés au $1. Or une conversion est une fonction, ce qui
rend l’index inutilisable. En fait, notre problème est que $1 n’est pas un entier !

SELECT max(id_article) * 0.333


FROM posts
\gdesc

Column | Type
----------+---------
?column? | numeric

La conversion du critère en int peut se faire à plusieurs endroits. Par exemple :

SELECT * FROM posts


WHERE id_article =
(SELECT max(id_article) * 0.333
FROM posts
)::int ;

456
10. SQL : CE QU’IL NE FAUT PAS FAIRE

Et l’index est donc utilisable immédiatement :

QUERY PLAN
-------------------------------------------------------------------------------
Index Scan using posts_id_article_id_post on posts
(cost=0.91..1796.42 rows=1007 width=115)
(actual time=0.031..0.198 rows=1000 loops=1)
Index Cond: (id_article = ($1)::integer)
InitPlan 2 (returns $1)
-> Result (cost=0.46..0.48 rows=1 width=32) (...)
InitPlan 1 (returns $0)
-> Limit (cost=0.43..0.46 rows=1 width=4) (...)
-> Index Only Scan Backward using posts_id_article_id_post
on posts posts_1 (...)
Index Cond: (id_article IS NOT NULL)
Heap Fetches: 0
Planning Time: 0.105 ms
Execution Time: 0.245 ms

Si l’on avait fait le calcul avec / 3 au lieu de * 0.333, on n’aurait pas eu le problème, car
la division de deux entiers donne un entier.

Attention donc à la cohérence des types dans vos critères. Le problème peut se rencontrer
même en joignant des int et des bigint !

457
https://dalibo.com/formations
SQL pour PostgreSQL

11 PL/PGSQL : LES BASES

458
11. PL/PGSQL : LES BASES

11.1 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

Ce module présente la programmation PL/pgSQL. Il commence par décrire les routines


stockées et les différents langages disponibles. Puis il aborde les bases du langage
PL/pgSQL, autrement dit :

• comment installer PL/pgSQL dans une base PostgreSQL ;


• comment créer un squelette de fonction ;
• comment déclarer des variables ;
• comment utiliser les instructions de base du langage ;
• comment créer et manipuler des structures ;
• comment passer une valeur de retour de la fonction à l’appelant.

11.1.1 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

11.1.2 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

459
https://dalibo.com/formations
SQL pour PostgreSQL

11.2 INTRODUCTION

11.2.1 QU'EST-CE QU'UN PL ?

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

PL est l’acronyme de « Procedural Languages ». En dehors du C et du SQL, tous les lan-


gages acceptés par PostgreSQL sont des PL.

Par défaut, trois langages sont installés et activés : C, SQL et PL/pgSQL.

11.2.2 QUELS LANGAGES SONT DISPONIBLES ?

• Nombreux langages disponibles


• PL/pgSQL, PL/Tcl, PL/Perl, PL/Python dans le core
• d’autres sous forme d’extensions tierces (PL/java, PL/R, etc.)

Les quatre langages PL supportés nativement sont décrits en détail dans la documentation
officielle :

• PL/PgSQL130
• PL/Tcl131 (existe en version trusted et untrusted)
• PL/Perl132 (existe en version trusted et untrusted)
• PL/Python133 (uniquement en version untrusted)

D’autres langages PL sont accessibles en tant qu’extensions tierces. Les plus stables sont
également mentionnés dans la documentation134 .

Une liste plus large est par ailleurs disponible sur le wiki PostgreSQL135 . Il en ressort
qu’au moins 18 langages sont disponibles, dont 10 installables en production. De plus, il
est possible d’en ajouter d’autres, comme décrit dans la documentation136 .

130
https://docs.postgresql.fr/current/plpgsql.html
131
https://docs.postgresql.fr/current/pltcl.html
132
https://docs.postgresql.fr/current/plperl.html
133
https://docs.postgresql.fr/current/plpython.html
134
https://docs.postgresql.fr/current/external-pl.html
135
https://wiki.postgresql.org/wiki/PL_Matrix
136
https://docs.postgresql.fr/current/plhandler.html

460
11. PL/PGSQL : LES BASES

11.2.3 DIFFÉRENCES ENTRE TRUSTED ET UNTRUSTED

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

Les langages de confiance ne peuvent accèder qu’à la base de données. Ils ne peuvent pas
accéder aux autres bases, aux systèmes de fichiers, au réseau, etc. Ils sont donc confinés,
ce qui les rend moins facilement utilisables pour compromettre le système. PL/pgSQL est
l’exemple typique. Mais de ce fait, ils offrent moins de possibilités que les autres langages.

Seuls les superutilisateurs peuvent créer une routine dans un langage untrusted. Par con-
tre, ils peuvent ensuite donner les droits d’exécution à ces routines aux autres utilisateurs.

11.2.4 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

Il peut y avoir de nombreuses raisons différentes à l’utilisation d’un langage PL. Simpli-
fier et centraliser des traitements clients directement dans la base est l’argument le plus
fréquent. Par exemple, une insertion complexe dans plusieurs tables, avec mise en place
d’identifiants pour liens entre ces tables, peut évidemment être écrite côté client. Il est
quelquefois plus pratique de l’écrire sous forme de PL. On y gagne :

• la centralisation du code : si plusieurs applications ont potentiellement be-


soin d’écrire le traitement, cela réduit d’autant les risques de bugs et facilite la
maintenance ;
• les performances : le code s’exécute localement, directement dans le moteur de la
base. Il n’y a donc pas tous les changements de contexte et échanges de messages
réseaux dus à l’exécution de nombreux ordres SQL consécutifs ;
• la simplicité : suivant le besoin, un langage PL peut être bien plus pratique que le
langage client.

Il est par exemple très simple d’écrire un traitement d’insertion/mise à jour en PL/pgSQL,
le langage étant créé pour simplifier ce genre de traitements, et la gestion des exceptions
461
https://dalibo.com/formations
SQL pour PostgreSQL

pouvant s’y produire. Si vous avez besoin de réaliser du traitement de chaîne puissant, ou
de la manipulation de fichiers, PL/Perl ou PL/Python seront probablement des options
plus intéressantes car plus performantes.

La grande variété des différents langages PL supportés par PostgreSQL permet normale-
ment d’en trouver un correspondant aux besoins et aux langages déjà maîtrisés dans
l’entreprise.

Les langages PL permettent donc de rajouter une couche d’abstraction et d’effectuer des
traitements avancés directement en base.

11.2.5 INTÉRÊTS DE PL/PGSQL EN PARTICULIER

• Structure inspirée de l’ADA, donc proche du Pascal


• Ajout de structures de contrôle au langage SQL
• Peut effectuer des traitements complexes
• Hérite de tous les types, fonctions et opérateurs définis par les utilisateurs
• Trusted
• Facile à utiliser

La plupart des gens ont eu l’occasion de faire du Pascal ou de l’ADA, et sont donc familiers
avec la syntaxe de PL/pgSQL. Cette syntaxe est d’ailleurs très proche de celle de PLSQL
d’Oracle.

Elle permet d’écrire des requêtes directement dans le code PL sans déclaration préalable,
sans appel à des méthodes complexes, ni rien de cette sorte. Le code SQL est mélangé
naturellement au code PL, et on a donc un sur-ensemble de SQL qui est procédural.

PL/pgSQL étant intégré à PostgreSQL, il hérite de tous les types déclarés dans le moteur,
même ceux rajoutés par l’utilisateur. Il peut les manipuler de façon transparente.

PL/pgSQL est trusted. Tous les utilisateurs peuvent donc créer des routines dans ce lan-
gage (par défaut). Vous pouvez toujours soit supprimer le langage, soit retirer les droits à
un utilisateur sur ce langage (via la commande SQL REVOKE).

PL/pgSQL est donc raisonnablement facile à utiliser : il y a peu de complications, peu de


pièges, il dispose d’une gestion des erreurs évoluée (gestion d’exceptions).

462
11. PL/PGSQL : LES BASES

11.2.6 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 un gros défaut :
– pas spécialisés dans le traitement de requêtes

Les langages PL « autres », comme PL/Perl et PL/Python (les deux plus utilisés après
PL/pgSQL), sont bien plus évolués que PL/PgSQL. Par exemple, ils sont bien plus effi-
caces en matière de traitement de chaînes de caractères, ils possèdent des structures
avancées comme des tables de hachage, permettent l’utilisation de variables statiques
pour maintenir des caches, voire, pour leur version untrusted, peuvent effectuer des ap-
pels systèmes. Dans ce cas, il devient possible d’appeler un Webservice par exemple, ou
d’écrire des données dans un fichier externe.

Il existe des langages PL spécialisés. Le plus emblématique d’entre eux est PL/R. R est
un langage utilisé par les statisticiens pour manipuler de gros jeux de données. PL/R
permet donc d’effectuer ces traitements R directement en base, traitements qui seraient
très pénibles à écrire dans d’autres langages.

Il existe aussi un langage qui est, du moins sur le papier, plus rapide que tous les langages
cités précédemment : vous pouvez écrire des procédures stockées en C, directement.
Elles seront compilées à l’extérieur de PosgreSQL, en respectant un certain formalisme,
puis seront chargées en indiquant la bibliothèque C qui les contient et leurs paramètres et
types de retour. Attention, toute erreur dans le code C est susceptible d’accéder à toute
la mémoire visible par le processus PostgreSQL qui l’exécute, et donc de corrompre les
données. Il est donc conseillé de ne faire ceci qu’en dernière extrémité.

Le gros défaut est simple et commun à tous ces langages : vous utilisez par exemple
PL/Perl. Perl n’est pas spécialement conçu pour s’exécuter en tant que langage de procé-
dures stockées. Ce que vous utilisez quand vous écrivez du PL/Perl est donc du code
Perl, avec quelques fonctions supplémentaires (préfixées par spi) pour accéder à la base
de données. L’accès aux données est donc rapide, mais assez fastidieux au niveau syntax-
ique, comparé à PL/pgSQL.

Un autre problème des langages PL (autre que C et PL/pgSQL), est que ces langages
n’ont pas les mêmes types natifs que PostgreSQL, et s’exécutent dans un interpréteur
relativement séparé. Les performances sont donc moindres que PL/pgSQL et C, pour
les traitements dont le plus consommateur est l’accès aux données. Souvent, le temps
de traitement dans un de ces langages plus évolués est tout de même meilleur grâce au
temps gagné par les autres fonctionnalités (la possibilité d’utiliser un cache, ou une table
463
https://dalibo.com/formations
SQL pour PostgreSQL

de hachage par exemple).

11.2.7 ROUTINES / PROCÉDURES STOCKÉES / FONCTIONS

• Procédure stockée
– ne renvoit pas de données
– permet le contrôle transactionnel
– disponible à partir de la version 11
• Fonction
– peut renvoyer des données
– utilisable dans un SELECT
– peut être de type TRIGGER, agrégat, fenêtrage
• Routine
– terme utilisé pour signifier procédure ou fonction

Les programmes écrits à l’aide des langages PL sont habituellement enregistrés sous forme
de routines :

• procédures ;
• fonctions ;
• fonctions trigger ;
• fonctions d’agrégat ;
• fonctions de fenêtrage (window functions).

Le code source de ces objets est stocké dans la table pg_proc du catalogue.

Les procédures, apparues avec PostgreSQL 11, sont très similaires aux fonctions. Les
principales différences entre les deux sont :

• Les fonctions doivent déclarer des arguments en sortie (RETURNS ou arguments OUT).
Il est possible d’utiliser void pour une fonction sans argument de sortie ; c’était
d’ailleurs la méthode utilisée pour émuler le comportement d’une procédure avant
leur introduction avec PostgreSQL 11.
• Les procédures offrent le support du contrôle transactionnel, c’est-à-dire la capacité
de valider (COMMIT) ou annuler (ROLLBACK) les modifications effectuées jusqu’à ce
point par la procédure.
• Les procédures sont appelées exclusivement par la commande SQL CALL ; les
fonctions peuvent être appelées dans la plupart des ordres DML/DQL (notamment
SELECT), mais pas par CALL.
• Les fonctions peuvent être déclarées de telle manière qu’elles peuvent être utilisées
dans des rôles spécifiques (TRIGGER, agrégat ou fonction de fenêtrage).

464
11. PL/PGSQL : LES BASES

11.3 INSTALLATION

• SQL, C et PL/pgSQL compilés et installés par défaut


– Paquets Debian et Red Hat du PGDG pour beaucoup de langages :
yum install postgresql12-plperl
apt install postgresql-plython3-12
• Autres langages à compiler explicitement

Pour savoir si PL/Perl ou PL/Python a été compilé, on peut le demander à pg_config :

> pg_config --configure


'--prefix=/usr/local/pgsql-10_icu' '--enable-thread-safety'
'--with-openssl' '--with-libxml' '--enable-nls' '--with-perl' '--enable-debug'
'ICU_CFLAGS=-I/usr/local/include/unicode/'
'ICU_LIBS=-L/usr/local/lib -licui18n -licuuc -licudata' '--with-icu'

Si besoin, les emplacements exacts d’installation des bibliothèques peuvent être


récupérés à l’aide des options --libdir et --pkglibdir de pg_config.

Cependant, dans les paquets fournis par le PGDG pour Red Hat ou Debian, il faudra in-
staller explicitement le paquet dédié à plperl (yum install postgresql12-plperl ou
apt install postgresql-plperl-12).

Ainsi, la bibliothèque plperl.so que l’on trouvera dans ces répertoires contiendra les
fonctions qui permettent l’utilisation du langage PL/Perl. Elle est chargée par le moteur
à la première utilisation d’une procédure utilisant ce langage.

De même pour Python 3 (yum install postgresql12-plpython3 ou apt install


postgresql-plython3-12).

11.3.1 ACTIVER/DÉSACTIVER UN LANGAGE

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


CREATE EXTENSION plperl ;
• Supprimer l’extension désactive le langage :
DROP EXTENSION plperl ;

Le langage est activé uniquement dans la base dans laquelle la commande est lancée.
S’il faut l’activer sur plusieurs bases, il sera nécessaire d’exécuter cet ordre SQL sur les
différentes bases ciblées.
465
https://dalibo.com/formations
SQL pour PostgreSQL

Activer un langage dans la base modèle template1 l’activera aussi pour toutes les bases
créées par la suite.

11.3.2 LANGAGE DÉJÀ INSTALLÉ ?

• Interroger le catalogue système pg_language


– ou utiliser \dx avec psql
• Il contient une ligne par langage installé
• Un langage peut avoir lanpltrusted à false

Voici un exemple d’interrogation de pg_language :


SELECT lanname, lanpltrusted
FROM pg_language
WHERE lanname='plpgsql';

lanname | lanpltrusted
---------+--------------
plpgsql | t
(1 ligne)

Si un langage est trusted, tous les utilisateurs peuvent créer des procédures dans ce lan-
gage. Sinon seuls les superutilisateurs le peuvent. Il existe par exemple deux variantes de
PL/Perl : PL/Perl et PL/PerlU. La seconde est la variante untrusted et est un Perl « com-
plet ». La version trusted n’a pas le droit d’ouvrir des fichiers, des sockets, ou autres appels
systèmes qui seraient dangereux.

SQL, PL/pgSQL, PL/Tcl, PL/Perl sont trusted.

C, PL/TclU, PL/PerlU, et PL/PythonU (et les variantes spécifiques aux versions


PL/Python2U et PL/Python3U) sont untrusted.

Il est à noter que les langages PL sont généralement installés par le biais d’extensions :

base=# \dx
Liste des extensions installées
Nom | Version | Schéma | Description
-------------+---------+------------+---------------------------------------
plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language

466
11. PL/PGSQL : LES BASES

11.4 EXEMPLES DE FONCTIONS ET PROCÉDURES

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

SELECT addition (1,2);

addition
----------
3

11.4.1 EXEMPLE DE FONCTION

Même fonction en SQL pur :


CREATE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE sql
IMMUTABLE
AS '
SELECT entier1 + entier2;
' ;

Les fonctions simples peuvent être écrites en SQL pur. L’avantage principal est qu’elle
peuvent fréquemment être intégrées dans les requêtes, et qu’elles ne sont pas pour
l’optimiseur des « boîtes noires ». La syntaxe est plus claire, mais bien plus limitée.

467
https://dalibo.com/formations
SQL pour PostgreSQL

11.4.2 EXEMPLE DE FONCTION 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 ;

Dans cet exemple, on récupère l’estimation du nombre de lignes actives d’une table
passée en paramètres.

L’intérêt majeur du PL/pgSQL et du SQL sur les autres langages est la facilité d’accès aux
données. Ici, un simple SELECT <champ> INTO <variable> suffit à récupérer une valeur
depuis une table dans une variable.

SELECT nb_lignes_table ('public', 'pgbench_accounts');

nb_lignes_table
-----------------
10000000

11.4.3 EXEMPLE DE PROCÉDURE

CREATE OR REPLACE PROCEDURE vide_tables_une_fois_sur_deux (dry_run BOOLEAN)


AS '
BEGIN
TRUNCATE TABLE pgbench_accounts ;
TRUNCATE TABLE pgbench_history ;
TRUNCATE TABLE pgbench_branches ;
TRUNCATE TABLE pgbench_tellers ;
IF dry_run THEN
ROLLBACK ;
END IF ;
END ;
' LANGUAGE plpgsql ;

468
11. PL/PGSQL : LES BASES

Cette procédure tronque des tables de la base d’exemple pgbench, et annule si dry_run
est vrai.

Les procédures sont récentes dans PostgreSQL (à partir de la version 11). Elles sont à
utiliser quand on n’attend pas de résultat en retour. Surtout, elles permettent de gérer
les transactions (COMMIT, ROLLBACK), ce qui ne peut se faire dans des fonctions, même si
celles-ci peuvent modifier les données.

11.5 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, généralement DML
SELECT mafonction('arg1', 'arg2');
INSERT INTO matable SELECT mafonction(macolonne) FROM ma_table2;
CALL maprocedure( mafonction() );

Demander l’exécution d’une procédure se fait en utilisant un ordre SQL spécifique : CALL.
L’usage en est extrêmement simple, puisque l’ordre ne sert qu’à invoquer une procédure.
Ainsi, la commande suivante :
CALL public.ma_procedure('test', 1);

Permettrait d’invoquer une procédure nommée ma_procedure, existant dans le schéma


public, avec deux arguments d’entrée :

• une chaîne de caractères ;


• un entier.

Les fonctions ne sont quant à elles pas directement compatibles avec la commande CALL,
il faut les invoquer dans le contexte d’une commande SQL. Elles sont le plus couramment
appelées depuis des commandes de type DML (SELECT, INSERT, etc.), mais on peut aussi
les trouver dans d’autres commandes. Voici quelques exemples :

• dans un SELECT (la fonction ne doit renvoyer qu’une seule ligne) :


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

• dans un SELECT, en passant en argument les valeurs d’une colonne d’une table :
SELECT ma_fonction(ma_colonne) FROM ma_table;

• dans le FROM d’un SELECT, la fonction renvoit ici généralement plusieurs lignes
(SETOF), et un résultat de type RECORD :
469
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT result FROM ma_fonction() AS f(result);

• dans un INSERT pour générer la valeur à insérer :

INSERT INTO ma_table(ma_colonne) VALUES ( ma_fonction() );

• dans une création d’index (index fonctionnel, la fonction sera réellement appelée
lors des mises à jour de l’index... attention la fonction doit être déclarée IMMUTABLE) :

CREATE INDEX ON ma_table ( ma_fonction(ma_colonne) );

• appel d’une fonction en paramètre d’une autre fonction ou d’une procédure, par
exemple ici le résultat de la fonction ma_fonction() (qui doit renvoyer une seule
ligne) est passé en argument d’entrée de la procédure ma_procedure() :

CALL ma_procedure( ma_fonction() );

Par ailleurs, certaines fonctions sont spécialisées et ne peuvent être invoquées que dans
le contexte pour lequel elles ont été conçues (fonctions trigger, d’agrégat, de fenêtrage,
etc.).

11.6 CRÉATION ET STRUCTURE D'UNE FONCTION OU PROCÉ-


DURE

• Créer une fonction : CREATE FUNCTION


• Créer une procédure : CREATE PROCEDURE

Voici la syntaxe complète pour une fonction (d’après https://www.postgresql.org/docs/


current/sql-createfunction.html) :

CREATE [ OR REPLACE ] FUNCTION


name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
[ RETURNS rettype
| RETURNS TABLE ( column_name column_type [, ...] ) ]
{ LANGUAGE lang_name
| TRANSFORM { FOR TYPE type_name } [, ... ]
| WINDOW
| IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
| CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT
| [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
| PARALLEL { UNSAFE | RESTRICTED | SAFE }
| COST execution_cost
| ROWS result_rows
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'

470
11. PL/PGSQL : LES BASES

| AS 'obj_file', 'link_symbol'
} ...

Voici la syntaxe complète pour une procédure (d’après https://www.postgresql.org/docs/


current/sql-createprocedure.html) :

CREATE [ OR REPLACE ] PROCEDURE


name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
{ LANGUAGE lang_name
| TRANSFORM { FOR TYPE type_name } [, ... ]
| [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
| SET configuration_parameter { TO value | = value | FROM CURRENT }
| AS 'definition'
| AS 'obj_file', 'link_symbol'
} ...

Nous allons décrire les différentes clauses ci-dessous.

11.6.1 ARGUMENTS

• Préciser les arguments


[ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ]
[, ...] ]
• argmode : entrée (IN), sortie (OUT), entrée/sortie (INOUT), nombre variant
(VARIADIC)
• argname : nom (libre et optionnel)
• argtype : type (parmi tous les types de base et les types utilisateur)
• valeur par défaut : clause DEFAULT

Si le mode de l’argument est omis, IN est la valeur implicite.

L’option VARIADIC permet de définir une fonction avec un nombre d’arguments libres à
condition de respecter le type de l’argument (comme printf en C par exemple). Seul
un argument OUT peut suivre un argument VARIADIC : l’argument VARIADIC doit être le
dernier de la liste des paramètres en entrée puisque tous les paramètres en entrée suivant
seront considérées comme faisant partie du tableau variadic. Seuls les arguments IN et
VARIADIC sont utilisables avec une fonction déclarée renvoyant une table (clause RETURNS
TABLE). S’il y a plusieurs paramètres en OUT, un enregistrement composite de tous ces
types est renvoyé (c’est donc équivalent sémantiquement à un RETURNS TABLE).

Les procédures ne supportent pas les arguments OUT, seulement IN et INOUT.

La clause DEFAULT permet de rendre les paramètres optionnels. Après le premier


471
https://dalibo.com/formations
SQL pour PostgreSQL

paramètre ayant une valeur par défaut, tous les paramètres qui suivent doivent avoir une
valeur par défaut. Pour rendre le paramètre optionnel, il doit être le dernier argument ou
alors les paramètres suivants doivent aussi avoir une valeur par défaut.

11.6.2 TYPE EN RETOUR

• Fonctions uniquement !
• Il faut aussi indiquer un type de retour :
[ RETURNS rettype | RETURNS TABLE ( col_nom type [, ...] ) ]
• sauf si un ou plusieurs paramètres sont en mode OUT ou INOUT
• rettype : type de la valeur en retour (parmi tous les types de base et les types
utilisateurs)
• void est un type de retour valide
• Il est aussi possible d’indiquer un type TABLE
• Peut renvoyer plusieurs lignes : clause SETOF

Le type de retour est obligatoire pour les fonctions et interdit pour les procédures.

Avant la version 11, il n’était pas possible de créer une procédure mais il était possible de
créer une fonction se comportant globalement comme une procédure en utilisant le type
de retour void.

Il est possible de renvoyer plusieurs colonnes grâce à la clause TABLE et plusieurs lignes
grâce à la clause SETOF.

11.6.3 LANGAGE

• Le langage de la routine doit être précisé


LANGUAGE nomlang
• Nous étudierons SQL et plpgsql
• Aussi : plpython3u, plperl, pl/R...

Il n’y a pas de langage par défaut. Il est donc nécessaire de le spécifier à chaque création
d’une routine.

Ici ce sera surtout : LANGUAGE plpgsql.

Une routine en pur SQL indiquera LANGUAGE sql. On rencontrera aussi plperl,
plpython3u, etc. en fonction des besoins.

472
11. PL/PGSQL : LES BASES

11.6.4 MODE

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

On peut indiquer à PostgreSQL le niveau de volatilité (ou de stabilité) d’une fonction. Ceci
permet d’aider PostgreSQL à optimiser les requêtes utilisant ces fonctions, mais aussi
d’interdire leur utilisation dans certains contextes.

Une fonction est IMMUTABLE (immuable) si son exécution ne dépend que de ses
paramètres. Elle ne doit donc dépendre ni du contenu de la base (pas de SELECT, ni de
modification de donnée de quelque sorte), ni d’aucun autre élément qui ne soit pas un
de ses paramètres. Les fonctions arithmétiques simples (+, *, abs...) sont immuables.

À l’inverse, now() n’est évidemment pas immuable. Une fonction sélectionnant des don-
nées d’une table non plus. to_char() n’est pas non plus immuable, car son comportement
dépend des paramètres de session, par exemple to_char(timestamp with time zone,
text) dépend du paramètre de session timezone…

Une fonction est STABLE si son exécution donne toujours le même résultat sur toute la
durée d’un ordre SQL, pour les mêmes paramètres en entrée. Cela signifie que la fonction
ne modifie pas les données de la base. Une fonction n’exécutant que des SELECT sur des
tables (pas des fonctions !) sera stable. to_char() est stable. L’optimiseur peut réduire
ainsi le nombre d’appels sans que ce soit en pratique toujours le cas.

Une fonction est VOLATILE dans tous les autres cas. random() est volatile. Une fonction
volatile peut même modifier les donneés. Une fonction non déclarée comme stable ou
immuable est volatile par défaut.

La volatilité des fonctions intégrées à PostgreSQL est déjà définie. C’est au développeur
de préciser la volatilité des fonctions qu’il écrit. Ce n’est pas forcément évident. Une
erreur peut poser des problèmes quand le plan est mis en cache, ou, on le verra, dans des
index.

Quelle importance cela a-t-il ?

Prenons une table d’exemple sur les heures de l’année 2020 :


-- Une ligne par heure dans l année, 8784 lignes
CREATE TABLE heures
473
https://dalibo.com/formations
SQL pour PostgreSQL

AS
SELECT i, '2020-01-01 00:00:00+01:00'::timestamptz + i * interval '1 hour' AS t
FROM generate_series (1,366*24) i;

Définissons une fonction un peu naïve ramenant le premier jour du mois, volatile faute
de mieux :

CREATE OR REPLACE FUNCTION premierjourdumois(t timestamptz)


RETURNS timestamptz
LANGUAGE plpgsql
VOLATILE
AS $$
BEGIN
RAISE notice 'appel premierjourdumois' ; -- trace des appels
RETURN date_trunc ('month', t);
END $$ ;

Demandons juste le plan d’un appel ne portant que sur le dernier jour :

EXPLAIN SELECT * FROM heures


WHERE t > premierjourdumois('2020-12-31 00:00:00+02:00'::timestamptz)
LIMIT 10 ;

QUERY PLAN
-------------------------------------------------------------------------
Limit (cost=0.00..8.04 rows=10 width=12)
-> Seq Scan on heures (cost=0.00..2353.80 rows=2928 width=12)
Filter: (t > premierjourdumois(
'2020-12-30 23:00:00+01'::timestamp with time zone))

Le nombre de lignes attendues (2928) est le tiers de la table, alors que nous deman-
dons que le dernier mois. Il s’agit de l’estimation forfaitaire que PostgreSQL utilise faute
d’informations sur ce que va retourner la fonction.

Demander à voir le résultat mène à l’affichage de milliers de NOTICE : la fonction est ap-
pelée à chaque ligne. En effet, une fonction volatile sera systématiquement exécutée à
chaque appel, et, selon le plan, ce peut être pour chaque ligne parcourue !

Cette fonction n’appelle que des fonctions, sans effet de bord. Déclarons-la donc stable :

ALTER FUNCTION premierjourdumois(timestamp with time zone) STABLE ;

Une fonction stable peut en théorie être remplacée par son résultat pendant l’exécution
de la requête. Mais c’est impossible de le faire plus tôt, car on ne sait pas forcément
dans quel contexte la fonction va être appelée. En cas de requête préparée, par exemple,
les paramètres de la session ou les données de la base peuvent même changer entre la
planification et l’exécution.

474
11. PL/PGSQL : LES BASES

Dans notre cas, le même EXPLAIN simple mène à ceci :

NOTICE: appel premierjourdumois


QUERY PLAN
-------------------------------------------------------------------------
Limit (cost=0.00..32.60 rows=10 width=12)
-> Seq Scan on heures (cost=0.00..2347.50 rows=720 width=12)
Filter: (t > premierjourdumois(
'2020-12-30 23:00:00+01'::timestamp with time zone))

Comme il s’agit d’un simple EXPLAIN, la requête n’est pas exécutée. Or le message NOTICE
est renvoyé : la fonction est donc exécutée pour une simple planification. Un appel unique
suffit, puisque la valeur d’une fonction stable ne change pas pendant toute la durée de
la requête pour les mêmes paramètres (ici une constante). Cet appel permet d’affiner
la volumétrie des valeurs attendues, ce qui peut avoir un impact énorme. Cependant, à
l’exécution, les NOTICE apparaîtront pour indiquer que la fonction est appelée à chaque
ligne.

Pour qu’un seul appel soit effectué pour toute la requête, il faudrait déclarer la fonction
comme immuable, ce qui serait faux, puisqu’elle dépend implicitement du fuseau horaire.
Dans l’idéal, une fonction immuable peut être remplacée par son résultat avant même
la planification d’une requête l’utilisant. C’est le cas avec les calculs arithmétiques par
exemple :

EXPLAIN SELECT * FROM heures


WHERE i > abs(364*24) AND t > '2020-06-01'::date + interval '57 hours' ;

La valeur est substituée très tôt, ce qui permet de les comparer aux statistiques :

Seq Scan on heures (cost=0.00..179.40 rows=13 width=12)


Filter: ((i > 8736) AND (t > '2020-06-03 09:00:00'::timestamp without time zone))

Pour forcer un appel unique quand on sait que la fonction renverra une constante, du
moins le temps de la requête, même si elle est volatile, une astuce est de signifier à
l’optimiseur qu’il n’y aura qu’une seule valeur de comparaison, même si on ne sait pas
laquelle :

EXPLAIN (ANALYZE) SELECT * FROM heures


WHERE t > (SELECT premierjourdumois('2020-12-31 00:00:00+02:00'::timestamptz)) ;

NOTICE: appel premierjourdumois


QUERY PLAN
--------------------------------------------------------------------------------
Seq Scan on heures (cost=0.26..157.76 rows=2920 width=12)
(actual time=1.090..1.206 rows=721 loops=1)
475
https://dalibo.com/formations
SQL pour PostgreSQL

Filter: (t > $0)


Rows Removed by Filter: 8039
InitPlan 1 (returns $0)
-> Result (cost=0.00..0.26 rows=1 width=8)
(actual time=0.138..0.139 rows=1 loops=1)
Planning Time: 0.058 ms
Execution Time: 1.328 ms

On note qu’il n’y a qu’un appel. On comprend donc l’intérêt de se poser la question à
l’écriture de chaque fonction.

La volatilité est encore plus importante quand il s’agit de créer des fonctions sur index :
CREATE INDEX ON heures (premierjourdumois( t )) ;
ERROR: functions in index expression must be marked IMMUTABLE

Ceci n’est possible que si la fonction est immuable. En effet, si le résultat de la fonction
dépend de l’état de la base ou d’autres paramètres, la fonction exécutée au moment de
la création de la clé d’index pourrait ne plus retourner le même résultat quand viendra le
moment de l’interroger. PostgreSQL n’acceptera donc que les fonctions immuables dans
la déclaration des index fonctionnels.

Déclarer hâtivement une fonction comme immuable juste pour pouvoir l’utiliser comme
un index est dangereux : en cas d’erreur, les résultats d’une requête peuvent alors dépen-
dre du plan d’exécution, selon que les index seront utilisés ou pas ! Cela est particulière-
ment fréquent quand les fuseaux horaires ou les dictionnaires sont impliqués. Vérifiez
bien que vous n’utilisez que des fonctions immuables dans les index fonctionnels, les
pièges sont nombreux.

Par exemple, si l’on veut une version immuable de la fonction précédente, il faut fixer
le fuseau horaire dans l’appel à date_trunc. En effet, la seule version immuable de
date_trunc n’accepte que des timestamp sans fuseau, et en renvoie un.
CREATE OR REPLACE FUNCTION premierjourdumois_utc(t timestamptz)
RETURNS timestamptz
LANGUAGE plpgsql
IMMUTABLE
AS $$
DECLARE
jour1 timestamp ; --sans TZ
BEGIN
jour1 := date_trunc ('month', (t at time zone 'UTC')::timestamp) ;
RETURN jour1 AT TIME ZONE 'UTC';
END $$ ;

Testons avec une date dans les dernières heures de septembre en Alaska, qui correspond

476
11. PL/PGSQL : LES BASES

à une date déjà en octobre en temps universel, et par exemple aussi au Japon :

\x

SET timezone TO 'US/Alaska';

SELECT d,
d AT TIME ZONE 'UTC' AS d_en_utc,
premierjourdumois_utc (d),
premierjourdumois_utc (d) AT TIME ZONE 'UTC' as pjm_en_utc
FROM (SELECT '2020-09-30 18:00:00-08'::timestamptz AS d) x;

-[ RECORD 1 ]---------+-----------------------
d | 2020-09-30 18:00:00-08
d_en_utc | 2020-10-01 02:00:00
premierjourdumois_utc | 2020-09-30 16:00:00-08
pjm_en_utc | 2020-10-01 00:00:00

SET timezone TO 'Japan';

SELECT d,
d AT TIME ZONE 'UTC' AS d_en_utc,
premierjourdumois_utc (d),
premierjourdumois_utc (d) AT TIME ZONE 'UTC' as pjm_en_utc
FROM (SELECT '2020-09-30 18:00:00-08'::timestamptz AS d) x;

-[ RECORD 1 ]---------+-----------------------
d | 2020-10-01 11:00:00+09
d_en_utc | 2020-10-01 02:00:00
premierjourdumois_utc | 2020-10-01 09:00:00+09
pjm_en_utc | 2020-10-01 00:00:00

Malgré les différences d’affichage dues au fuseau horaire, c’est bien le même moment (la
première seconde d’octobre en temps universel) qui est retourné par la fonction.

Pour une fonction aussi simple, la version SQL est même préférable :

CREATE OR REPLACE FUNCTION premierjourdumois_utc(t timestamptz)


RETURNS timestamptz
LANGUAGE sql
IMMUTABLE
AS $$
SELECT (date_trunc ('month', (t at time zone 'UTC')::timestamp)) AT TIME ZONE 'UTC';
$$ ;

Enfin, la volatilité a également son importance lors d’autres opérations d’optimisation,


comme l’exclusion de partitions. Seules les fonctions immuables sont compatibles avec
477
https://dalibo.com/formations
SQL pour PostgreSQL

le partition pruning effectué à la planification, mais les fonctions stable sont éligibles au
dynamic partition pruning (à l’exécution) apparu avec PostgreSQL 11.

11.6.5 GESTION DES VALEURS NULL

• Fonctions uniquement !
• Précision sur la façon dont la fonction gère les valeurs NULL :
CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT
• CALLED ON NULL INPUT
– fonction appelée même si certains arguments sont NULL
• RETURNS NULL ON NULL INPUT ou STRICT
– la fonction renvoie NULL à chaque fois qu’au moins un argument est NULL

Si une fonction est définie comme STRICT et qu’un des arguments d’entrée est NULL, Post-
greSQL n’exécute même pas la fonction et utilise NULL comme résultat.

Dans la logique relationnelle, NULL signifie « la valeur est inconnue ». La plupart du temps,
il est logique qu’une fonction ayant un paramètre à une valeur inconnue retourne aussi
une valeur inconnue, ce qui fait que cette optimisation est très souvent pertinente.

On gagne à la fois en temps d’exécution, mais aussi en simplicité du code (il n’y a pas à
gérer les cas NULL pour une fonction dans laquelle NULL ne doit jamais être injecté).

11.6.6 POLITIQUE DE SÉCURITÉ

[EXTERNAL] SECURITY INVOKER | [EXTERNAL] SECURITY DEFINER


• Permet de déterminer l’utilisateur avec lequel sera exécutée la fonction
• Le « sudo de la base de données »
– potentiellement dangereux

Une fonction SECURITY INVOKER s’exécute avec les droits de l’appelant. C’est le mode
par défaut.

Une fonction SECURITY DEFINER s’exécute avec les droits du créateur. Cela permet, au
travers d’une fonction, de permettre à un utilisateur d’outrepasser ses droits de façon
contrôlée.

Bien sûr, une fonction SECURITY DEFINER doit faire l’objet d’encore plus d’attention
qu’une fonction normale. Elle peut facilement constituer un trou béant dans la sécurité
de votre base.

478
11. PL/PGSQL : LES BASES

Deux points importants sont à noter pour SECURITY DEFINER :

• Par défaut, toute fonction est exécutable par public. La première chose à faire est
donc de révoquer ce droit.
• Il faut se protéger des variables de session qui pourraient être utilisées pour modifier
le comportement de la fonction, en particulier le search_path. Il doit donc impéra-
tivement être positionné en dur dans cette fonction (soit d’emblée, avec un SET
dans la fonction, soit en positionnant un SET dans le CREATE FUNCTION).

Le mot clé EXTERNAL est facultatif, et n’est là que pour être en conformité avec la norme
SQL. En effet, dans PostgreSQL, on peut modifier le security definer pour toutes les fonc-
tions, qu’elles soient externes ou pas.

11.6.7 CODE À EXÉCUTER

• Précision du code à exécuter : AS 'definition' | AS 'obj_file',


'link_symbol'
• Premier cas
– chaîne « definition » contenant le code réel de la routine
• Deuxième cas
– obj_file est le nom de la bibliothèque
– link_symbol est le nom de la fonction dans le code source C

link_symbol n’est à utiliser que quand le nom de la routine diffère du nom de la fonction
C qui l’implémente.

11.6.8 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

COST est représenté en unité de cpu_operator_cost (100 par défaut).

ROWS vaut par défaut 1000 pour les fonctions SETOF. Pour les autres fonctions, la valeur
de ce paramètre est ignorée et remplacée par 1.
479
https://dalibo.com/formations
SQL pour PostgreSQL

Ces deux paramètres ne modifient pas le comportement de la fonction. Ils ne servent que
pour aider l’optimiseur de requête à estimer le coût d’appel à la fonction, afin de savoir, si
plusieurs plans sont possibles, lequel est le moins coûteux par rapport au nombre d’appels
de la fonction et au nombre d’enregistrements qu’elle retourne.

11.6.9 PARALLÉLISATION

• Fonctions uniquement !
• PARALLEL [UNSAFE | RESTRICTED | SAFE]
– la fonction peut-elle être exécutée en parallèle ?

PARALLEL UNSAFE indique que la fonction ne peut pas être exécutée dans le mode
parallèle. La présence d’une fonction de ce type dans une requête SQL force un plan
d’exécution en série. C’est la valeur par défaut.

Une fonction est non parallélisable si elle modifie l’état d’une base ou si elle fait des
changements sur la transaction.

PARALLEL RESTRICTED indique que la fonction peut être exécutée en mode parallèle mais
l’exécution est restreinte au processus principal d’exécution.

Une fonction peut être déclarée comme restreinte si elle accède aux tables temporaires,
à l’état de connexion des clients, aux curseurs, aux requêtes préparées.

PARALLEL SAFE indique que la fonction s’exécute correctement dans le mode parallèle
sans restriction.

En général, si une fonction est marquée sûre ou restreinte à la parallélisation alors qu’elle
ne l’est pas, elle pourrait renvoyer des erreurs ou fournir de mauvaises réponses lorsqu’elle
est utilisée dans une requête parallèle.

En cas de doute, les fonctions doivent être marquées comme UNSAFE, ce qui correspond
à la valeur par défaut.

480
11. PL/PGSQL : LES BASES

11.6.10 EXEMPLE DE FONCTION PL/PERL

• Permet d’insérer une facture associée à un client


• Si le client n’existe pas, une entrée est créée

Voici l’exemple de la fonction :

CREATE OR REPLACE FUNCTION


public.demo_insert_perl(nom_client text, titre_facture text)
RETURNS integer
LANGUAGE plperl
STRICT
AS $function$
use strict;
my ($nom_client, $titre_facture)=@_;
my $rv;
my $id_facture;
my $id_client;

# Le client existe t'il ?


$rv = spi_exec_query('SELECT id_client FROM mes_clients WHERE nom_client = '
. quote_literal($nom_client)
);
# Sinon on le crée :
if ($rv->{processed} == 0)
{
$rv = spi_exec_query('INSERT INTO mes_clients (nom_client) VALUES ('
. quote_literal($nom_client) . ') RETURNING id_client'
);
}
# Dans les deux cas, l'id client est dans $rv :
$id_client=$rv->{rows}[0]->{id_client};

# Insérons maintenant la facture


$rv = spi_exec_query(
'INSERT INTO mes_factures (titre_facture, id_client) VALUES ('
. quote_literal($titre_facture) . ", $id_client ) RETURNING id_facture"
);

$id_facture = $rv->{rows}[0]->{id_facture};

return $id_facture;
$function$ ;

Cette fonction n’est pas parfaite, elle ne protège pas de tout. Il est tout à fait possible
d’avoir une insertion concurrente entre le SELECT et le INSERT par exemple.
481
https://dalibo.com/formations
SQL pour PostgreSQL

Il est clair que l’accès aux données est malaisé en PL/Perl, comme dans la plupart des
langages, puisqu’ils ne sont pas prévus spécifiquement pour cette tâche. Par contre, on
dispose de toute la puissance de Perl pour les traitements de chaîne, les appels système…

PL/Perl, c’est :

• Perl, moins les fonctions pouvant accéder à autre chose qu’à PostgreSQL (il faut
utiliser PL/PerlU pour passer outre cette limitation)
• Un bloc de code anonyme appelé par PostgreSQL
• Des fonctions d’accès à la base, spi_*

11.6.11 EXEMPLE DE FONCTION PL/PGSQL

• Même fonction en PL/pgSQL


• 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

Pour éviter les conflits avec les objets de la base, il est conseillé de préfixer les variables.
CREATE OR REPLACE FUNCTION
public.demo_insert_plpgsql(p_nom_client text, p_titre_facture text)
RETURNS integer
LANGUAGE plpgsql
STRICT
AS $function$
DECLARE
v_id_facture int;
v_id_client int;
BEGIN
-- Le client existe t'il ?
SELECT id_client
INTO v_id_client
FROM mes_clients
WHERE nom_client = p_nom_client;

-- Sinon on le crée :
IF NOT FOUND THEN
INSERT INTO mes_clients (nom_client)
VALUES (p_nom_client)
RETURNING id_client INTO v_id_client;
END IF;

482
11. PL/PGSQL : LES BASES

-- Dans les deux cas, l'id client est maintenant dans v_id_client

-- Insérons maintenant la facture


INSERT INTO mes_factures (titre_facture, id_client)
VALUES (p_titre_facture, v_id_client)
RETURNING id_facture INTO v_id_facture;

return v_id_facture;
END;
$function$ ;

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

Le langage PL/pgSQL n’est pas sensible à la casse, tout comme SQL (sauf les noms de
colonnes, si vous les mettez entre des guillemets doubles).

L’opérateur de comparaison est =, l’opérateur d’affectation :=

11.6.13 STRUCTURE - SUITE

• DECLARE
– pour la déclaration des variables locales
• BEGIN
– pour indiquer le début du code de la routine
• END
– pour en indiquer la fin
• Instructions séparées par des points-virgules
• Commentaires commençant par -- ou compris entre /* et */
483
https://dalibo.com/formations
SQL pour PostgreSQL

Une routine est composée d’un bloc de déclaration des variables locales et d’un bloc de
code. Le bloc de déclaration commence par le mot clé DECLARE et se termine avec le mot
clé BEGIN. Ce mot clé est celui qui débute le bloc de code. La fin est indiquée par le mot
clé END.

Toutes les instructions se terminent avec des points-virgules. Attention, DECLARE, BEGIN
et END ne sont pas des instructions.

Il est possible d’ajouter des commentaires. -- indique le début d’un commentaire qui se
terminera en fin de ligne. Pour être plus précis dans la délimitation, il est aussi possible
d’utiliser la notation C : /* est le début d’un commentaire et */ la fin.

11.6.14 AJOUT DE 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

Indiquer le nom d’un label ainsi :

<<mon_label>>
-- le code (blocs DECLARE, BEGIN-END, et EXCEPTION)

ou bien (pour une boucle)

[ <<mon_label>> ]
LOOP
ordres …
END LOOP [ mon_label ];

Bien sûr, il est aussi possible d’utiliser des labels pour des boucles FOR, WHILE, FOREACH.

On sort d’un bloc ou d’une boucle avec la commande EXIT, on peut aussi utiliser CONTINUE
pour passer à l’exécution suivante d’une boucle sans terminer l’itération courante.

Par exemple :

EXIT [mon_label] WHEN compteur > 1;

484
11. PL/PGSQL : LES BASES

11.6.15 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

Une routine est surchargeable. La seule façon de les différencier est de prendre en compte
les arguments (nombre et type). Les noms des arguments peuvent être indiqués mais ils
seront ignorés.

Deux routines identiques aux arguments près (on parle de prototype) ne sont pas iden-
tiques, mais bien deux routines distinctes.

CREATE OR REPLACE a principalement pour but de modifier le code d’une routine, mais il
est aussi possible de modifier les méta-données.

11.6.16 MODIFICATION DES MÉTA-DONNÉES D'UNE ROUTINE

• ALTER FUNCTION / ALTER PROCEDURE / ALTER ROUTINE


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

Toutes les méta-données discutées plus haut sont modifiables avec un ALTER.

11.6.17 SUPPRESSION D'UNE ROUTINE

• DROP FUNCTION / DROP PROCEDURE / DROP ROUTINE


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

La suppression se fait avec l’ordre DROP.

Une fonction pouvant exister en plusieurs exemplaires, avec le même nom et des argu-
ments de type différents, il faudra parfois parfois préciser ces derniers.

485
https://dalibo.com/formations
SQL pour PostgreSQL

11.6.18 UTILISATION DES GUILLEMETS

• L’utilisation des guillemets devient très rapidement complexe


• Surtout lorsque le source se place directement entre guillemets
– doublage de tous les guillemets du code source
• Utilisation de $$ à la place des guillemets qui entourent les sources
– ou de n’importe quel autre marqueur

Par exemple, on peut reprendre la syntaxe de déclaration de la fonction addition()


précédente en utilisant cette méthode :
CREATE FUNCTION addition(entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $ma_fonction_addition$
DECLARE
resultat integer;
BEGIN
resultat := entier1 + entier2;
RETURN resultat;
END
$ma_fonction_addition$;

Autre exemple, si on prend un extrait de code réalisant une concaténation de chaînes de


caractères contenant des guillements, la syntaxe traditionnelle donnerait :
requete := requete || '' AND vin LIKE ''''bordeaux%'''' AND xyz''

On est obligé de multiplier les guillements pour les protéger, le code devient difficile à
lire.

En voilà une simplification grâce aux dollars :


requete := requete || $sql$ AND vin LIKE 'bordeaux%' AND xyz$sql$

Si vous avez besoin de mettre entre guillemets du texte qui inclut $$, vous pouvez utiliser
$Q$, et ainsi de suite. Le plus simple étant de définir un marqueur de fin de routine plus
complexe, par exemple incluant le nom de la fonction…

486
11. PL/PGSQL : LES BASES

11.7 DÉCLARATIONS

• Types natifs de PostgreSQL intégralement supportés


• Quelques types spécifiques à PL/pgSQL

En dehors des types natifs de PostgreSQL, PL/pgSQL y ajoute des types spécifiques pour
faciliter l’écriture des routines.

11.7.1 DÉCLARATION DE PARAMÈTRES

• Déclarés en dehors du code de la routine


• Possible d’associer directement un nom au type
• Les arguments sans nom peuvent être nommés avec l’argument ALIAS FOR dans
la partie DECLARE

Il est aussi possible d’utiliser une notation numérotée : le premier argument a pour nom
$1, le deuxième $2, etc.

11.7.2 DÉCLARATION DE VARIABLES

• Variables déclarées dans le source, dans la partie DECLARE :


DECLARE
var1 integer;
var2 integer := 5;
var3 integer NOT NULL DEFAULT 1;
var4 text COLLATE "fr_FR";

En PL/pgSQL, pour utiliser une variable dans le corps de la routine (entre le BEGIN et le
END), il est obligatoire de l’avoir déclarée précédemment :

• soit dans la liste des arguments (IN, INOUT ou OUT) ;


• soit dans la section DECLARE.

La déclaration doit impérativement préciser le nom et le type de la variable.

En option, il est également possible de préciser :

• sa valeur initiale (si rien n’est précisé, ce sera NULL par défaut) :
answer integer := 42;

• sa valeur par défaut, si on veut autre chose que NULL :


487
https://dalibo.com/formations
SQL pour PostgreSQL

answer integer DEFAULT 42;

• une contrainte NOT NULL (dans ce cas, il faut impérativement un défault différent
de NULL, et toute éventuelle affectation ultérieure de NULL à la variable provoquera
une erreur) :
answer integer NOT NULL DEFAULT 42;

• le collationnement à utiliser, pour les variables de type chaîne de caractères :


question text COLLATE "en_GB";

11.7.3 DÉCLARATION DE CONSTANTES

• Clause supplémentaire CONSTANT :


DECLARE
valeur_fixe CONSTANT integer := 12;
ma_version CONSTANT text := '1.12';

L’option CONSTANT permet de définir une variable pour laquelle il sera alors impossible
d’assigner une valeur dans le reste de la routine.

11.7.4 RÉCUPÉRATION D'UN TYPE

• 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

Cela permet d’écrire des routines plus génériques.

488
11. PL/PGSQL : LES BASES

11.7.5 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...;

11.7.6 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;
...
$$

L’utilisation de %ROWTYPE permet de définir une variable qui contient la structure d’un
enregistrement de la table spécifiée. %ROWTYPE n’est pas obligatoire, il est néanmoins
préférable d’utiliser cette forme, bien plus portable. En effet, dans PostgreSQL, toute
création de table crée un type associé de même nom, le nom de la table seul est donc
suffisant.

11.7.7 TYPE RECORD

• Identique au type ROW


– sauf que son type n’est connu que lors de son affectation
• Une variable de type RECORD peut changer de type au cours de l’exécution de la
routine, suivant les affectations réalisées

RECORD est beaucoup utilisé pour manipuler des curseurs : cela évite de devoir se préoc-
cuper de déclarer un type correspondant exactement aux colonnes de la requête associée
à chaque curseur.

489
https://dalibo.com/formations
SQL pour PostgreSQL

11.7.8 TYPE RECORD - EXEMPLE

CREATE FUNCTION ma_fonction () RETURNS integer


AS $$
DECLARE
ligne RECORD;
...
BEGIN
SELECT INTO ligne * FROM ma_premiere_table;
-- traitement de la ligne
FOR ligne IN SELECT * FROM ma_deuxieme_table LOOP
-- traitement de cette nouvelle ligne
...
$$

11.7.9 MODE D'UN PARAMÈTRE : IN, OUT, INOUT

• Trois modes de paramètres


– IN : en entrée
– OUT : en sortie
– INOUT : en entrée et en sortie
• Fonction à plusieurs paramètres OUT : identique à une fonction qui renvoie un
ROWTYPE pour un type composite créé préalablement
• Pas besoin de l’expression RETURN ou RETURN NEXT dans une fonction avec
paramètre(s) OUT
– il suffit d’affecter une valeur à ces paramètres

RETURN est inutile avec des paramètres OUT parce que c’est la valeur des paramètres OUT
à la fin de la fonction qui est retournée.

Dans le cas d’un RETURN NEXT, cela signifie que la fonction retourne un SETOF
d’enregistrements. Chaque appel à RETURN NEXT retourne donc un enregistrement
composé d’une copie de toutes les variables, au moment de l’appel à RETURN NEXT.

490
11. PL/PGSQL : LES BASES

11.8 INSTRUCTIONS

• Concernent les opérations sur la base de données, comme une extraction ou mod-
ification
• Ou d’autres expressions, comme des calculs, comparaisons, etc.
• Toute expression écrite en PL/pgSQL sera passée à la commande SELECT pour
l’interprétation par le moteur

Par expression, on entend par exemple des choses comme :


IF myvar > 0 THEN
myvar2 := 1 / myvar;
END IF;

Dans ce cas, l’expression myvar > 0 sera préparée par le moteur en de la façon suivante :

PREPARE statement_name(integer, integer) AS SELECT $1 > $2;

Puis cette requête préparée sera exécutée en lui passant en paramètre la valeur de myvar
et la constante 0.

Si myvar est supérieur à 0, il en sera ensuite de même pour l’instruction suivante :

PREPARE statement_name(integer, integer) AS SELECT $1 / $2;

11.8.1 AFFECTATION D'UNE VALEUR À UNE VARIABLE

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

Privilégiez la première écriture pour la lisibilité, la seconde écriture est moins claire et
n’apporte rien puisqu’il s’agit ici d’une affectation de constante.

À noter que l’écriture suivante est également possible pour une affectation :
ma_variable := une_colonne FROM ma_table WHERE id = 5;

Cette méthode profite du fait que toutes les expressions du code PL/pgSQL vont être
passées au moteur SQL de PostgreSQL dans un SELECT pour être résolues. Cela va fonc-
tionner, mais c’est très peu lisible, et donc non recommandé.

491
https://dalibo.com/formations
SQL pour PostgreSQL

11.8.2 EXÉCUTION D'UNE REQUÊTE

• Affectation de la ligne renvoyée dans une variable de type RECORD ou ROW : SELECT
* INTO ma_variable_ligne FROM ma_table...;
• Si plusieurs enregistrements renvoyés : seul le premier est récupéré
• Pour contrôler qu’un seul enregistrement est renvoyé :
– remplacer INTO par INTO STRICT
• Pour récupérer plus d’un enregistrement : écrire une boucle
• L’ordre est statique : on ne peut pas faire varier les colonnes retournées, la clause
WHERE, les tables…

Dans le cas du type ROW, la définition de la ligne doit correspondre parfaitement à la défi-
nition de la ligne renvoyée. Utiliser un type RECORD permet d’éviter ce type de problème.
La variable obtient directement le type ROW de la ligne renvoyée.

11.8.3 FONCTION RENVOYANT UN ENSEMBLE

• Doit renvoyer un ensemble d’un type SETOF


• Chaque ligne sera récupérée par l’instruction RETURN NEXT

11.8.4 EXEMPLE D'UNE FONCTION SETOF

• Exemple :
CREATE FUNCTION liste_entier (limite integer)
RETURNS SETOF integer
AS $$
BEGIN
FOR i IN 1..limite LOOP
RETURN NEXT i;
END LOOP;
END
$$ LANGUAGE plpgsql;

Ceci n’est qu’un exemple qui tente de reproduire la fonction generate_series. En pra-
tique, il est inutile et même contre-productif de créer ses propres fonctions pour réaliser
en moins bien ce que le moteur sait déjà faire.

Ainsi, la fonction native generate_series() possède plus d’options, gère d’autres types

492
11. PL/PGSQL : LES BASES

comme des timestamps, etc. Programmée en C, une fonction intégrée est aussi générale-
ment plus rapide.

11.8.5 EXÉCUTION D'UNE FONCTION SETOF

• Exécution :
ma_base=# SELECT * FROM liste_entier(5);

liste_entier
--------------
1
2
3
4
5
(5 lignes)

11.8.6 EXÉCUTION D'UNE REQUÊTE SANS RÉSULTAT

• PERFORM : exécution de la requête en direct


PERFORM * FROM ma_table WHERE une_colonne>0;
• Permet l’exécution
– d’un INSERT, UPDATE, DELETE (si la clause RETURNING n’est pas utilisée)
– ou même SELECT, si le résultat importe peu
– d’une autre fonction sans en récupérer de résultat
• Affectation de la variable FOUND si une ligne est affectée par l’instruction
• Utiliser ROW_COUNT pour obtenir le nombre de lignes affectées

On peut déterminer qu’aucune ligne n’a été trouvé par la requête en utilisant la variable
FOUND :
PERFORM * FROM ma_table WHERE une_colonne>0;
IF NOT FOUND THEN
...
END IF;

Pour appeler une fonction, il suffit d’utiliser PERFORM de la manière suivante :


PERFORM mafonction(argument1);
493
https://dalibo.com/formations
SQL pour PostgreSQL

Pour récupérer le nombre de lignes affectées lar l’instruction exécutée, il faut récupérer
la variable de diagnostic ROW_COUNT :
GET DIAGNOSTICS variable = ROW_COUNT;

Il est à noter que le ROW_COUNT récupéré ainsi s’applique à l’ordre SQL précédent, quel
qu’il soit :

• PERFORM ;
• EXECUTE ;
• ou même à un ordre statique directement dans le code PL/pgSQL.

11.8.7 EXÉCUTION D'UNE REQUÊTE - EXECUTE - 1

• Instruction :
EXECUTE '<chaine>' [INTO [STRICT] cible];
• Exécute la requête comprise dans la variable chaîne
• La variable chaine peut être construite à partir d’autres variables
• Cible contient le résultat de l’exécution de la requête dans le cas d’un résultat sur
une seule ligne
• Mot clé USING pour indiquer la valeur de certains arguments

11.8.8 EXÉCUTION D'UNE REQUÊTE - EXECUTE - 2

• Sans STRICT
– cible contient la première ligne d’un résultat multi-lignes
– ou NULL s’il n’y a pas de résultat
• Avec STRICT
– une exception est levée si le résultat ne contient aucune ligne
(NO_DATA_FOUND) ou en contient plusieurs (TOO_MANY_ROWS)
• GET DIAGNOSTICS integer_var = ROW_COUNT

Nous verrons comment traiter les exceptions plus loin.

494
11. PL/PGSQL : LES BASES

11.8.9 POUR CONSTRUIRE UNE REQUÊTE

• Fonction quote_ident
– pour mettre entre guillemets un identifiant d’un objet PostgreSQL (table,
colonne, etc.)
• Fonction quote_literal
– pour mettre entre guillemets une valeur (chaîne de caractères)
• Fonction quote_nullable
– pour mettre entre guillemets une valeur (chaîne de caractères), sauf NULL
qui sera alors renvoyé sans les guillemets
• || à utiliser pour concaténer tous les morceaux de la requête
• ou fonction format(...), équivalent de sprintf en C

La fonction format est l’équivalent de la fonction sprintf en C : elle formate une chaine
en fonction d’un patron et de valeurs à appliquer à ses paramètres et la retourne. Les
types de paramètre reconnus par format sont :

• %I : est remplacé par un identifiant d’objet. C’est l’équivalent de la fonction


quote_ident. L’objet en question est entouré en double-guillemet si nécessaire ;
• %L : est remplacé par une valeur littérale. C’est l’équivalent de la fonction
quote_literal. Des simple-guillemet sont ajoutés à la valeur et celle-ci est
correctement échappée si nécessaire ;
• %s : est remplacé par la valeur donnée sans autre forme de transformation ;
• %% : est remplacé par un simple %.

Voici un exemple d’utilisation de cette fonction, utilisant des paramètre positionnels :


SELECT format(
'SELECT %I FROM %I WHERE %1$I=%3$L',
'MaColonne',
'ma_table',
$$l'été$$
);
format
-------------------------------------------------------------
SELECT "MaColonne" FROM ma_table WHERE "MaColonne"='l''été'

495
https://dalibo.com/formations
SQL pour PostgreSQL

11.8.10 EXÉCUTION D'UNE REQUÊTE - EXECUTE - 3

• EXECUTE command-string [ INTO [STRICT] target ] [ USING expression


[, ... ] ];
• Permet de créer une requête dynamique avec des variables de substitution
• Beaucoup plus lisible que des quote_nullable
EXECUTE 'SELECT count(*) FROM mytable
WHERE inserted_by = $1 AND inserted <= $2'
INTO c
USING checked_user, checked_date;
• Le nombre de paramètres de la requête doit être fixe, ainsi que leur type
• Ne concerne pas les identifiants !

11.9 STRUCTURES DE CONTRÔLES

• But du PL : les traitements procéduraux

11.9.1 TESTS IF/THEN/ELSE/END IF - 1

IF condition THEN
instructions
[ELSEIF condition THEN
instructions]
[ELSEIF condition THEN
instructions]
[ELSE
instructions]
END IF

Ce dernier est l’équivalent d’un CASE en C pour une vérification de plusieurs alternatives.

496
11. PL/PGSQL : LES BASES

11.9.2 TESTS IF/THEN/ELSE/END IF - 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;

11.9.3 TESTS CASE

Deux possibilités :
• 1ère :
CASE variable
WHEN expression THEN instructions
ELSE instructions
END CASE
• 2nde :
CASE
WHEN expression-booléene THEN instructions
ELSE instructions
END CASE

Quelques exemples :
CASE x
WHEN 1, 2 THEN
msg := 'un ou deux';
ELSE
msg := 'autre valeur que un ou deux';
END CASE;

CASE
WHEN x BETWEEN 0 AND 10 THEN
msg := 'la valeur est entre 0 et 10';
WHEN x BETWEEN 11 AND 20 THEN
msg := 'la valeur est entre 11 et 20';
END CASE;

497
https://dalibo.com/formations
SQL pour PostgreSQL

11.9.4 BOUCLE LOOP/EXIT/CONTINUE - 1

• Créer une boucle (label possible)


– LOOP / END LOOP :
• Sortir de la boucle
– EXIT [label] [WHEN expression_booléenne]
• Commencer une nouvelle itération de la boucle
– CONTINUE [label] [WHEN expression_booléenne]

11.9.5 BOUCLE LOOP/EXIT/CONTINUE - 2

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

Cette boucle incrémente le résultat de 1 à chaque itération tant que la valeur du résultat
est inférieure à 50. Ensuite, le résultat est incrémenté de 1 à deux reprises pour chaque
tout de boucle, on incrémente donc de 2 par tour de boucle. Arrivé à 100, la procédure
sort de la boucle.

11.9.6 BOUCLE WHILE

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

498
11. PL/PGSQL : LES BASES

11.9.7 BOUCLE FOR - 1

• Synopsis :
FOR variable in [REVERSE] entier1..entier2 [BY incrément]
LOOP
instructions
END LOOP;
• variable va obtenir les différentes valeurs entre entier1 et entier2
• Label possible.

11.9.8 BOUCLE FOR - 2

• L’option BY permet d’augmenter l’incrémentation


FOR variable in 1..10 BY 5...
• L’option REVERSE permet de faire défiler les valeurs en ordre inverse
FOR variable in REVERSE 10..1 ...

11.9.9 BOUCLE FOR... IN... LOOP

• Permet de boucler dans les lignes résultats d’une requête


• Exemple :
FOR ligne IN SELECT * FROM ma_table LOOP
instructions
END LOOP;
• Label possible
• ligne de type RECORD, ROW ou liste de variables séparées par des virgules
• Utilise un curseur en interne

Exemple :
FOR a, b, c, d IN SELECT col_a, col_b, col_c, col_d FROM ma_table
LOOP
-- instructions
END LOOP;

499
https://dalibo.com/formations
SQL pour PostgreSQL

11.9.10 BOUCLE FOREACH

• Permet de boucler sur les éléments d’un tableau


• Syntaxe :
FOREACH variable [SLICE n] IN ARRAY expression LOOP
instructions
END LOOP
• 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 vari-
able
• label possible

Voici deux exemples permettant d’illustrer l’utilité de SLICE :

• sans SLICE :
do $$
declare a int[] := ARRAY[[1,2],[3,4],[5,6]];
b int;
begin
foreach b in array a loop
raise info 'var: %', b;
end loop;
end $$;
INFO: var: 1
INFO: var: 2
INFO: var: 3
INFO: var: 4
INFO: var: 5
INFO: var: 6

• Avec SLICE :
do $$
declare a int[] := ARRAY[[1,2],[3,4],[5,6]];
b int[];
begin
foreach b slice 1 in array a loop
raise info 'var: %', b;
end loop;
end $$;
INFO: var: {1,2}
INFO: var: {3,4}
INFO: var: {5,6}

500
11. PL/PGSQL : LES BASES

11.10 RETOUR D'UNE FONCTION

• RETURN [expression]
• Renvoie cette expression à la requête appelante
• expression optionnelle si argument(s) déclarés OUT
– RETURN lui-même optionnel si argument(s) déclarés OUT

11.10.1 RETURN NEXT

• Fonction SETOF, aussi appelé fonction SRF (Set Returning Function)


• Fonctionne avec des types scalaires (normaux) et des types composites
• RETURN NEXT renvoie une ligne du SETOF
• Cette fonction s’appelle de cette façon :
SELECT * FROM ma_fonction();
• expression de renvoi optionnelle si argument de mode OUT

Tout est conservé en mémoire jusqu’à la fin de la fonction. Donc, si beaucoup de données
sont renvoyées, cela pourrait occasionner quelques lenteurs.

Par ailleurs, il est possible d’appeler une SRF par :


SELECT ma_fonction();

Dans ce cas, on récupère un résultat d’une seule colonne, de type composite.

11.10.2 RETURN QUERY

• Fonctionne comme RETURN NEXT


• RETURN QUERY la_requete
• RETURN QUERY EXECUTE chaine_requete

Par ce mécanisme, on peut très simplement produire une fonction retournant le résultat
d’une requête complexe fabriquée à partir de quelques paramètres.

501
https://dalibo.com/formations
SQL pour PostgreSQL

11.11 CONCLUSION

• Ajoute un grand nombre de structure de contrôle (test, boucle, etc.)


• Facile à utiliser et à comprendre
• Attention à la compatibilité ascendante

11.11.1 POUR ALLER PLUS LOIN

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

La documentation officielle sur le langage PL/pgSQL peut être consultée en français à


cette adresse137 .

11.11.2 QUESTIONS

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

137
https://docs.postgresql.fr/current/plpgsql.html

502
11. PL/PGSQL : LES BASES

11.12 TRAVAUX PRATIQUES

11.12.1 HELLO

Écrire une fonction hello qui renvoie la chaîne de caractère «


Hello World! » en SQL.

Écrire une fonction hello_pl qui renvoie la chaîne de caractère


« Hello World! » en PL/pgSQL.

Comparer les coûts des deux plans d’exécutions de ces requêtes.


Expliquer les coûts.

11.12.2 DIVISION

Écrire en PL/pgSQL une fonction de division appelée division.


Elle acceptera en entrée deux arguments de type entier et ren-
verra un nombre réel (numeric).

Écrire cette même fonction en SQL.

Comment corriger le problème de la division par zéro ? Écrire


cette nouvelle fonction dans les deux langages.
(Conseil : dans ce genre de calcul impossible, il est possible
d’utiliser la constante NaN (Not A Number) ).

503
https://dalibo.com/formations
SQL pour PostgreSQL

11.12.3 SELECT SUR DES TABLES DANS LES FONCTIONS

Ce TP utilise les tables de la base employes_services.

Le script de création de la base peut être téléchargé depuis https://dali.bo/tp_employes_


services. Il ne fait que 3,5 ko. Le chargement se fait de manière classique :
$ psql < employes_services.sql

Les quelques tables occupent environ 80 Mo sur le disque.

Créer une fonction qui ramène le nombre d’employés embauchés


une année donnée (à partir du champ employes.date_embauche).

Utiliser la fonction generate_series() pour lister le nombre


d’embauches pour chaque année entre 2000 et 2010.

Créer une fonction qui fait la même chose avec deux années en
paramètres une boucle FOR … LOOP, RETURNS TABLE et RETURN
NEXT.

11.12.4 MULTIPLICATION

Écrire une fonction de multiplication dont les arguments sont des


chiffres en toute lettre, inférieurs ou égaux à « neuf ». Par exem-
ple, multiplication ('deux','trois') doit renvoyer 6.

Si ce n’est déjà fait, faire en sorte que multiplication appelle


une autre fonction pour faire la conversion de texte en chiffre, et
n’effectue que le calcul.

Essayer de multiplier « deux » par 4. Qu’obtient-on et pourquoi ?

504
11. PL/PGSQL : LES BASES

Corriger la fonction pour tomber en erreur si un argument est


numérique (utiliser RAISE EXCEPTION <message>).

11.12.5 SALUTATIONS

Écrire une fonction en PL/pgSQL qui prend en argument le nom


de l’utilisateur, puis lui dit « Bonjour » ou « Bonsoir » suivant
l’heure de la journée. Utiliser la fonction to_char().

Écrire la même fonction avec un paramètre OUT.

Pour calculer l’heure courante, utiliser plutôt la fonction extract.

Réécrire la fonction en SQL.

11.12.6 INVERSION DE CHAÎNE

Écrire une fonction inverser qui inverse une chaîne (pour « toto »
en entrée, afficher « otot » en sortie), à l’aide d’une boucle WHILE
et des fonctions char_length et substring.

505
https://dalibo.com/formations
SQL pour PostgreSQL

11.12.7 JOURS FÉRIÉS

Le calcul de la date de Pâques est complexe138 . On peut écrire la fonction suivante :


CREATE OR REPLACE FUNCTION paques(annee integer)
RETURNS date
AS $$
DECLARE
a integer;
b integer;
r date;
BEGIN
a := (19*(annee % 19) + 24) % 30;
b := (2*(annee % 4) + 4*(annee % 7) + 6*a + 5) % 7;
SELECT (annee::text||'-03-31')::date + (a+b-9) INTO r;
RETURN r;
END;
$$
LANGUAGE plpgsql;

Principe : Soit m l’année. On calcule successivement :

• le reste de m/19 : c’est la valeur de a.


• le reste de m/4 : c’est la valeur de b.
• le reste de m/7 : c’est la valeur de c.
• le reste de (19a + p)/30 : c’est la valeur de d.
• le reste de (2b + 4c + 6d + q)/7 : c’est la valeur de e.

Les valeurs de p et de q varient de 100 ans en 100 ans. De 2000 à 2100, p vaut 24, q
vaut 5. La date de Pâques est le (22 + d + e) mars ou le (d + e - 9) avril.

Afficher les dates de Pâques de 2018 à 2022.

Écrire une fonction qui calcule la date de l’Ascension, soit le jeudi


de la sixième semaine après Pâques. Pour simplifier, on peut aussi
considérer que l’Ascension se déroule 39 jours après Pâques.

Enfin, écrire une fonction qui renvoie tous les jours fériés d’une
année (libellé et date).
Prévoir un paramètre supplémentaire pour l’Alsace-Moselle, où le
Vendredi saint (précédant le dimanche de Pâques) et le 26 décem-

138
https://fr.wikipedia.org/wiki/Calcul_de_la_date_de_P%C3%A2ques

506
11. PL/PGSQL : LES BASES

bre sont aussi fériés.


Cette fonction doit renvoyer plusieurs lignes : utiliser RETURN
NEXT. Plusieurs variantes sont possibles : avec SETOF record,
avec des paramètres OUT, ou avec RETURNS TABLE (libelle,
jour).
Enfin, il est possible d’utiliser RETURN QUERY.

507
https://dalibo.com/formations
SQL pour PostgreSQL

11.13 TRAVAUX PRATIQUES (SOLUTIONS)

11.13.1 HELLO

Écrire une fonction hello qui renvoie la chaîne de caractère «


Hello World! » en SQL.

CREATE OR REPLACE FUNCTION hello()


RETURNS text
AS $BODY$
SELECT 'hello world !'::text;
$BODY$
LANGUAGE SQL;

Écrire une fonction hello_pl qui renvoie la chaîne de caractère


« Hello World! » en PL/pgSQL.

CREATE OR REPLACE FUNCTION hello_pl()


RETURNS text
AS $BODY$
BEGIN
RETURN 'hello world !';
END
$BODY$
LANGUAGE plpgsql;

Comparer les coûts des deux plans d’exécutions de ces requêtes.


Expliquer les coûts.

Requêtage :

EXPLAIN SELECT hello();

QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=32)

EXPLAIN SELECT hello_pl();

QUERY PLAN
------------------------------------------
Result (cost=0.00..0.26 rows=1 width=32)

508
11. PL/PGSQL : LES BASES

Par défaut, si on ne précise pas le coût (COST) d’une fonction, cette dernière a un coût par
défaut de 100. Ce coût est à multiplier par la valeur du paramètre cpu_operator_cost,
par défaut à 0.0025. Le coût total d’appel de la fonction hello_pl est donc par défaut
de :

100*cpu_operator_cost + cpu_tuple_cost

Ce n’est pas valable pour la fonction en SQL pur, qui est ici intégrée à la requête.

11.13.2 DIVISION

Écrire en PL/pgSQL une fonction de division appelée division.


Elle acceptera en entrée deux arguments de type entier et ren-
verra un nombre réel (numeric).

Attention, sous PostgreSQL, la division de deux entiers est par défaut entière : il faut donc
transtyper.

CREATE OR REPLACE FUNCTION division (arg1 integer, arg2 integer)


RETURNS numeric
AS $BODY$
BEGIN
RETURN arg1::numeric / arg2::numeric;
END
$BODY$
LANGUAGE plpgsql;

SELECT division (3,2) ;

division
--------------------
1.5000000000000000

Écrire cette même fonction en SQL.

CREATE OR REPLACE FUNCTION division_sql (a integer, b integer)


RETURNS numeric
AS $$
SELECT a::numeric / b::numeric;
$$
LANGUAGE SQL;
509
https://dalibo.com/formations
SQL pour PostgreSQL

Comment corriger le problème de la division par zéro ? Écrire


cette nouvelle fonction dans les deux langages.
(Conseil : dans ce genre de calcul impossible, il est possible
d’utiliser la constante NaN (Not A Number) ).

Le problème se présente ainsi :


SELECT division(1,0);
ERROR: division by zero
CONTEXTE : PL/pgSQL function division(integer,integer) line 3 at RETURN

Pour la version en PL :
CREATE OR REPLACE FUNCTION division(arg1 integer, arg2 integer)
RETURNS numeric
AS $BODY$
BEGIN
IF arg2 = 0 THEN
RETURN 'NaN';
ELSE
RETURN arg1::numeric / arg2::numeric;
END IF;
END $BODY$
LANGUAGE plpgsql;

SELECT division (3,0) ;

division
----------
NaN

Pour la version en SQL :


CREATE OR REPLACE FUNCTION division_sql(a integer, b integer)
RETURNS numeric
AS $$
SELECT CASE $2
WHEN 0 THEN 'NaN'
ELSE $1::numeric / $2::numeric
END;
$$
LANGUAGE SQL;

510
11. PL/PGSQL : LES BASES

11.13.3 SELECT SUR DES TABLES DANS LES FONCTIONS

Créer une fonction qui ramène le nombre d’employés embauchés


une année donnée (à partir du champ employes.date_embauche).

CREATE OR REPLACE FUNCTION nb_embauches (v_annee integer)


RETURNS integer
AS $BODY$
DECLARE
nb integer;
BEGIN
SELECT count(*)
INTO nb
FROM employes
WHERE extract (year from date_embauche) = v_annee ;
RETURN nb;
END
$BODY$
LANGUAGE plpgsql ;

Test :
SELECT nb_embauches (2006);

nb_embauches
--------------
9

Utiliser la fonction generate_series() pour lister le nombre


d’embauches pour chaque année entre 2000 et 2010.

SELECT n, nb_embauches (n)


FROM generate_series (2000,2010) n
ORDER BY n;

n | nb_embauches
------+--------------
2000 | 2
2001 | 0
2002 | 0
2003 | 1
2004 | 0
2005 | 2
2006 | 9
2007 | 0
511
https://dalibo.com/formations
SQL pour PostgreSQL

2008 | 0
2009 | 0
2010 | 0

Créer une fonction qui fait la même chose avec deux années en
paramètres une boucle FOR … LOOP, RETURNS TABLE et RETURN
NEXT.

CREATE OR REPLACE FUNCTION nb_embauches (v_anneedeb int, v_anneefin int)


RETURNS TABLE (annee int, nombre_embauches int)
AS $BODY$
BEGIN
FOR i in v_anneedeb..v_anneefin
LOOP
SELECT i, nb_embauches (i)
INTO annee, nombre_embauches ;
RETURN NEXT ;
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;

Le nom de la fonction a été choisi identique à la précédente, mais avec des paramètres
différents. Cela ne gêne pas le requêtage :
SELECT * FROM nb_embauches (2006,2010);

annee | nombre_embauches
-------+------------------
2006 | 9
2007 | 0
2008 | 0
2009 | 0
2010 | 0

512
11. PL/PGSQL : LES BASES

11.13.4 MULTIPLICATION

Écrire une fonction de multiplication dont les arguments sont des


chiffres en toute lettre, inférieurs ou égaux à « neuf ». Par exem-
ple, multiplication ('deux','trois') doit renvoyer 6.

CREATE OR REPLACE FUNCTION multiplication (arg1 text, arg2 text)


RETURNS integer
AS $BODY$
DECLARE
a1 integer;
a2 integer;
BEGIN
IF arg1 = 'zéro' THEN
a1 := 0;
ELSEIF arg1 = 'un' THEN
a1 := 1;
ELSEIF arg1 = 'deux' THEN
a1 := 2;
ELSEIF arg1 = 'trois' THEN
a1 := 3;
ELSEIF arg1 = 'quatre' THEN
a1 := 4;
ELSEIF arg1 = 'cinq' THEN
a1 := 5;
ELSEIF arg1 = 'six' THEN
a1 := 6;
ELSEIF arg1 = 'sept' THEN
a1 := 7;
ELSEIF arg1 = 'huit' THEN
a1 := 8;
ELSEIF arg1 = 'neuf' THEN
a1 := 9;
END IF;

IF arg2 = 'zéro' THEN


a2 := 0;
ELSEIF arg2 = 'un' THEN
a2 := 1;
ELSEIF arg2 = 'deux' THEN
a2 := 2;
ELSEIF arg2 = 'trois' THEN
a2 := 3;
ELSEIF arg2 = 'quatre' THEN
a2 := 4;
513
https://dalibo.com/formations
SQL pour PostgreSQL

ELSEIF arg2 = 'cinq' THEN


a2 := 5;
ELSEIF arg2 = 'six' THEN
a2 := 6;
ELSEIF arg2 = 'sept' THEN
a2 := 7;
ELSEIF arg2 = 'huit' THEN
a2 := 8;
ELSEIF arg2 = 'neuf' THEN
a2 := 9;
END IF;

RETURN a1*a2;
END
$BODY$
LANGUAGE plpgsql;

Test :

SELECT multiplication('deux', 'trois');

multiplication
----------------
6

SELECT multiplication('deux', 'quatre');

multiplication
----------------
8

Si ce n’est déjà fait, créer une autre fonction pour faire la con-
version de texte en chiffre, que multiplication appellera avant
d’effectuer le calcul.

CREATE OR REPLACE FUNCTION texte_vers_entier(arg text)


RETURNS integer AS $BODY$
DECLARE
ret integer;
BEGIN
IF arg = 'zéro' THEN
ret := 0;
ELSEIF arg = 'un' THEN
ret := 1;
ELSEIF arg = 'deux' THEN
ret := 2;
ELSEIF arg = 'trois' THEN

514
11. PL/PGSQL : LES BASES

ret := 3;
ELSEIF arg = 'quatre' THEN
ret := 4;
ELSEIF arg = 'cinq' THEN
ret := 5;
ELSEIF arg = 'six' THEN
ret := 6;
ELSEIF arg = 'sept' THEN
ret := 7;
ELSEIF arg = 'huit' THEN
ret := 8;
ELSEIF arg = 'neuf' THEN
ret := 9;
END IF;

RETURN ret;
END
$BODY$
LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION multiplication(arg1 text, arg2 text)


RETURNS integer
AS $BODY$
DECLARE
a1 integer;
a2 integer;
BEGIN
a1 := texte_vers_entier(arg1);
a2 := texte_vers_entier(arg2);
RETURN a1*a2;
END
$BODY$
LANGUAGE plpgsql;

Essayer de multiplier « deux » par 4. Qu’obtient-on et pourquoi ?

SELECT multiplication('deux', 4::text);


multiplication
----------------

Par défaut, les variables internes à la fonction valent NULL. Rien n’est prévu pour affecter
le second argument, on obtient donc NULL en résultat.

Corriger la fonction pour tomber en erreur si un argument est


numérique (utiliser RAISE EXCEPTION <message>).

515
https://dalibo.com/formations
SQL pour PostgreSQL

CREATE OR REPLACE FUNCTION texte_vers_entier(arg text)


RETURNS integer AS $BODY$
DECLARE
ret integer;
BEGIN
IF arg = 'zéro' THEN
ret := 0;
ELSEIF arg = 'un' THEN
ret := 1;
ELSEIF arg = 'deux' THEN
ret := 2;
ELSEIF arg = 'trois' THEN
ret := 3;
ELSEIF arg = 'quatre' THEN
ret := 4;
ELSEIF arg = 'cinq' THEN
ret := 5;
ELSEIF arg = 'six' THEN
ret := 6;
ELSEIF arg = 'sept' THEN
ret := 7;
ELSEIF arg = 'huit' THEN
ret := 8;
ELSEIF arg = 'neuf' THEN
ret := 9;
ELSE
RAISE EXCEPTION 'argument "%" invalide', arg;
ret := NULL;
END IF;

RETURN ret;
END
$BODY$
LANGUAGE plpgsql;

SELECT multiplication('deux', 4::text);


ERROR: argument "4" invalide
CONTEXTE : PL/pgSQL function texte_vers_entier(text) line 26 at RAISE
PL/pgSQL function multiplication(text,text) line 7 at assignment

516
11. PL/PGSQL : LES BASES

11.13.5 SALUTATIONS

Écrire une fonction en PL/pgSQL qui prend en argument le nom


de l’utilisateur, puis lui dit « Bonjour » ou « Bonsoir » suivant
l’heure de la journée. Utiliser la fonction to_char().

CREATE OR REPLACE FUNCTION salutation(utilisateur text)


RETURNS text
AS $BODY$
DECLARE
heure integer;
libelle text;
BEGIN
heure := to_char(now(), 'HH24');
IF heure > 12
THEN
libelle := 'Bonsoir';
ELSE
libelle := 'Bonjour';
END IF;

RETURN libelle||' '||utilisateur||' !';


END
$BODY$
LANGUAGE plpgsql;

Test :
SELECT salutation ('Guillaume');

salutation
---------------------
Bonsoir Guillaume !

Écrire la même fonction avec un paramètre OUT.

CREATE OR REPLACE FUNCTION salutation(IN utilisateur text, OUT message text)


AS $BODY$
DECLARE
heure integer;
libelle text;
BEGIN
heure := to_char(now(), 'HH24');
IF heure > 12
THEN
libelle := 'Bonsoir';
517
https://dalibo.com/formations
SQL pour PostgreSQL

ELSE
libelle := 'Bonjour';
END IF;

message := libelle||' '||utilisateur||' !';


END
$BODY$
LANGUAGE plpgsql;

Elle s’utilise de la même manière :


SELECT salutation ('Guillaume');

salutation
---------------------
Bonsoir Guillaume !

Pour calculer l’heure courante, utiliser plutôt la fonction extract.

CREATE OR REPLACE FUNCTION salutation(IN utilisateur text, OUT message text)


AS $BODY$
DECLARE
heure integer;
libelle text;
BEGIN
SELECT INTO heure extract(hour from now())::int;
IF heure > 12
THEN
libelle := 'Bonsoir';
ELSE
libelle := 'Bonjour';
END IF;

message := libelle||' '||utilisateur||' !';


END
$BODY$
LANGUAGE plpgsql;

Réécrire la fonction en SQL.

Le CASE … WHEN remplace aisément un IF … THEN :


CREATE OR REPLACE FUNCTION salutation_sql(nom text)
RETURNS text
AS $$
SELECT CASE extract(hour from now()) > 12
WHEN 't' THEN 'Bonsoir '|| nom

518
11. PL/PGSQL : LES BASES

ELSE 'Bonjour '|| nom


END::text;
$$ LANGUAGE SQL;

11.13.6 INVERSION DE CHAÎNE

Écrire une fonction inverser qui inverse une chaîne (pour « toto
» en entrée, afficher « otot » en sortie), à l’aide d’une boucle WHILE
et des fonctions char_length et substring.

CREATE OR REPLACE FUNCTION inverser(str_in varchar)


RETURNS varchar
AS $$
DECLARE
str_out varchar ; -- à renvoyer
position integer ;
BEGIN
-- Initialisation de str_out, sinon sa valeur reste à NULL
str_out := '';
-- Position initialisée ç la longueur de la chaîne
position := char_length(str_in);
-- La chaîne est traitée ç l'envers
-- Boucle: Inverse l'ordre des caractères d'une chaîne de caractères
WHILE position > 0 LOOP
-- la chaîne donnée en argument est parcourue
-- à l'envers,
-- et les caractères sont extraits individuellement
str_out := str_out || substring(str_in, position, 1);
position := position - 1;
END LOOP;
RETURN str_out;
END;
$$
LANGUAGE plpgsql;

SELECT inverser (' toto ') ;

inverser
----------
otot

519
https://dalibo.com/formations
SQL pour PostgreSQL

11.13.7 JOURS FÉRIÉS

La fonction suivante calcule la date de Pâques d’une année :


CREATE OR REPLACE FUNCTION paques(annee integer)
RETURNS date
AS $$
DECLARE
a integer;
b integer;
r date;
BEGIN
a := (19*(annee % 19) + 24) % 30;
b := (2*(annee % 4) + 4*(annee % 7) + 6*a + 5) % 7;
SELECT (annee::text||'-03-31')::date + (a+b-9) INTO r;
RETURN r;
END;
$$
LANGUAGE plpgsql;

Afficher les dates du dimanche de Pâques de 2018 à 2022.

SELECT paques (n) FROM generate_series (2018, 2022) n ;

paques
------------
2018-04-01
2019-04-21
2020-04-12
2021-04-04
2022-04-17

Écrire une fonction qui calcule la date de l’Ascension, soit le jeudi


de la sixième semaine après Pâques. Pour simplifier, on peut aussi
considérer que l’Ascension se déroule 39 jours après Pâques.

Version complexe :
CREATE OR REPLACE FUNCTION ascension(annee integer)
RETURNS date
AS $$
DECLARE
r date;
BEGIN
SELECT paques(annee)::date + 40 INTO r;

520
11. PL/PGSQL : LES BASES

SELECT r + (4 - extract(dow from r))::integer INTO r;


RETURN r;
END;
$$
LANGUAGE plpgsql;

Version simple :
CREATE OR REPLACE FUNCTION ascension(annee integer)
RETURNS date
AS $$
SELECT (paques (annee) + INTERVAL '39 days')::date ;
$$
LANGUAGE sql;

Test :
SELECT paques (n), ascension(n) FROM generate_series (2018, 2022) n ;

paques | ascension
------------+------------
2018-04-01 | 2018-05-10
2019-04-21 | 2019-05-30
2020-04-12 | 2020-05-21
2021-04-04 | 2021-05-13
2022-04-17 | 2022-05-26

Enfin, écrire une fonction qui renvoie tous les jours fériés d’une
année (libellé et date).
Prévoir un paramètre supplémentaire pour l’Alsace-Moselle, où le
Vendredi saint (précédant le dimanche de Pâques) et le 26 décem-
bre sont aussi fériés.
Cette fonction doit renvoyer plusieurs lignes : utiliser RETURN
NEXT. Plusieurs variantes sont possibles : avec SETOF record,
avec des paramètres OUT, ou avec RETURNS TABLE (libelle,
jour).
Enfin, il est possible d’utiliser RETURN QUERY.

Version avec SETOF record :


CREATE OR REPLACE FUNCTION vacances(annee integer, alsace_moselle boolean DEFAULT false)
RETURNS SETOF record
AS $$
DECLARE
f integer;
521
https://dalibo.com/formations
SQL pour PostgreSQL

r record;
BEGIN
SELECT 'Jour de l''an'::text, (annee::text||'-01-01')::date INTO r;
RETURN NEXT r;
SELECT 'Pâques'::text, paques(annee)::date + 1 INTO r;
RETURN NEXT r;
SELECT 'Ascension'::text, ascension(annee)::date INTO r;
RETURN NEXT r;
SELECT 'Fête du travail'::text, (annee::text||'-05-01')::date INTO r;
RETURN NEXT r;
SELECT 'Victoire 1945'::text, (annee::text||'-05-08')::date INTO r;
RETURN NEXT r;
SELECT 'Fête nationale'::text, (annee::text||'-07-14')::date INTO r;
RETURN NEXT r;
SELECT 'Assomption'::text, (annee::text||'-08-15')::date INTO r;
RETURN NEXT r;
SELECT 'La toussaint'::text, (annee::text||'-11-01')::date INTO r;
RETURN NEXT r;
SELECT 'Armistice 1918'::text, (annee::text||'-11-11')::date INTO r;
RETURN NEXT r;
SELECT 'Noël'::text, (annee::text||'-12-25')::date INTO r;
RETURN NEXT r;
IF alsace_moselle THEN
SELECT 'Vendredi saint'::text, paques(annee)::date - 2 INTO r;
RETURN NEXT r;
SELECT 'Lendemain de Noël'::text, (annee::text||'-12-26')::date INTO r;
RETURN NEXT r;
END IF;

RETURN;
END;
$$
LANGUAGE plpgsql;

Le requêtage implique de nommer les colonnes :

SELECT *
FROM vacances(2020, true) AS (libelle text, jour date)
ORDER BY jour ;

libelle | jour
--------------------+------------
Jour de l'an | 2020-01-01
Vendredi saint | 2020-04-10
Pâques | 2020-04-13
Fête du travail | 2020-05-01

522
11. PL/PGSQL : LES BASES

Victoire 1945 | 2020-05-08


Ascension | 2020-05-21
Fête nationale | 2020-07-14
Assomption | 2020-08-15
La toussaint | 2020-11-01
Armistice 1918 | 2020-11-11
Noël | 2020-12-25
Lendemain de Noël | 2020-12-26

Version avec paramètres OUT :

Une autre forme d’écriture possible consiste à indiquer les deux colonnes de retour
comme des paramètres OUT :

CREATE OR REPLACE FUNCTION vacances(


annee integer,
alsace_moselle boolean DEFAULT false,
OUT libelle text,
OUT jour date)
RETURNS SETOF record
LANGUAGE plpgsql
AS $function$
DECLARE
f integer;
r record;
BEGIN
SELECT 'Jour de l''an'::text, (annee::text||'-01-01')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Pâques'::text, paques(annee)::date + 1 INTO libelle, jour;
RETURN NEXT;
SELECT 'Ascension'::text, ascension(annee)::date INTO libelle, jour;
RETURN NEXT;
SELECT 'Fête du travail'::text, (annee::text||'-05-01')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Victoire 1945'::text, (annee::text||'-05-08')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Fête nationale'::text, (annee::text||'-07-14')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Assomption'::text, (annee::text||'-08-15')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'La toussaint'::text, (annee::text||'-11-01')::date
523
https://dalibo.com/formations
SQL pour PostgreSQL

INTO libelle, jour;


RETURN NEXT;
SELECT 'Armistice 1918'::text, (annee::text||'-11-11')::date
INTO libelle, jour;
RETURN NEXT;
SELECT 'Noël'::text, (annee::text||'-12-25')::date INTO libelle, jour;
RETURN NEXT;
IF alsace_moselle THEN
SELECT 'Vendredi saint'::text, paques(annee)::date - 2 INTO libelle, jour;
RETURN NEXT;
SELECT 'Lendemain de Noël'::text, (annee::text||'-12-26')::date
INTO libelle, jour;
RETURN NEXT;
END IF;

RETURN;
END;
$function$;

La fonction s’utilise alors de façon simple :

SELECT *
FROM vacances(2020)
ORDER BY jour ;

libelle | jour
-----------------+------------
Jour de l'an | 2020-01-01
Pâques | 2020-04-13
Fête du travail | 2020-05-01
Victoire 1945 | 2020-05-08
Ascension | 2020-05-21
Fête nationale | 2020-07-14
Assomption | 2020-08-15
La toussaint | 2020-11-01
Armistice 1918 | 2020-11-11
Noël | 2020-12-25

Version avec RETURNS TABLE :

Seule la déclaration en début diffère de la version avec les paramètres OUT :

CREATE OR REPLACE FUNCTION vacances(


annee integer,alsace_moselle boolean DEFAULT false)
RETURNS TABLE (libelle text, jour date)
LANGUAGE plpgsql

524
11. PL/PGSQL : LES BASES

AS $function$

L’utilisation est aussi simple que la version précédente.

Version avec RETURN QUERY :

C’est peut-être la version la plus compacte :


CREATE OR REPLACE FUNCTION vacances(annee integer,alsace_moselle boolean DEFAULT false)
RETURNS TABLE (libelle text, jour date)
LANGUAGE plpgsql
AS $function$
BEGIN
RETURN QUERY SELECT 'Jour de l''an'::text, (annee::text||'-01-01')::date ;
RETURN QUERY SELECT 'Pâques'::text, paques(annee)::date + 1 ;
RETURN QUERY SELECT 'Ascension'::text, ascension(annee)::date ;
RETURN QUERY SELECT 'Fête du travail'::text, (annee::text||'-05-01')::date ;
RETURN QUERY SELECT 'Victoire 1945'::text, (annee::text||'-05-08')::date ;
RETURN QUERY SELECT 'Fête nationale'::text, (annee::text||'-07-14')::date ;
RETURN QUERY SELECT 'Assomption'::text, (annee::text||'-08-15')::date ;
RETURN QUERY SELECT 'La toussaint'::text, (annee::text||'-11-01')::date ;
RETURN QUERY SELECT 'Armistice 1918'::text, (annee::text||'-11-11')::date ;
RETURN QUERY SELECT 'Noël'::text, (annee::text||'-12-25')::date ;
IF alsace_moselle THEN
RETURN QUERY SELECT 'Vendredi saint'::text, paques(annee)::date - 2 ;
RETURN QUERY SELECT 'Lendemain de Noël'::text, (annee::text||'-12-26')::date ;
END IF;
RETURN;
END;
$function$;

525
https://dalibo.com/formations
SQL pour PostgreSQL

12 PL/PGSQL AVANCÉ

526
12. PL/PGSQL AVANCÉ

12.1 PRÉAMBULE

12.1.1 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

12.1.2 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

12.2 ROUTINES VARIADIC

527
https://dalibo.com/formations
SQL pour PostgreSQL

12.2.1 ROUTINES VARIADIC : INTRODUCTION

• Permet de créer des routines avec un nombre d’arguments variables


• ... mais du même type

L’utilisation du mot clé VARIADIC dans la déclaration des routines permet d’utiliser un
nombre variable d’arguments dans la mesure où tous les arguments optionnels sont du
même type de données. Ces arguments sont passés à la fonction sous forme de tableau
d’arguments du même type.

VARIADIC tableau text[]

Il n’est pas possible d’utiliser d’autres arguments en entrée à la suite d’un paramètre
VARIADIC.

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

Quelques explications sur cette fonction :

• SQL est un langage de routines stockées


– une routine SQL ne contient que des ordres SQL exécutés séquentiellements
– le résultat de la fonction est le résultat du dernier ordre
• generate_subscript() prend un tableau en premier paramètre et la dimension de
ce tableau (un tableau peut avoir plusieurs dimensions), et elle retourne une série
d’entiers allant du premier au dernier indice du tableau dans cette dimension
• g(i) est un alias : generate_subscripts est une SRF (set-returning function,
retourne un SETOF), g est donc le nom de l’alias de table, et i le nom de l’alias de
colonne.

528
12. PL/PGSQL AVANCÉ

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

En PL/pgSQL, il est possible d’utiliser une boucle FOREACH pour parcourir directement le
tableau des arguments optionnels.
CREATE OR REPLACE FUNCTION pluspetit(VARIADIC liste numeric[])
RETURNS numeric
LANGUAGE plpgsql
AS $function$
DECLARE
courant numeric;
plus_petit numeric;
BEGIN
FOREACH courant IN ARRAY liste LOOP
IF plus_petit IS NULL OR courant < plus_petit THEN
plus_petit := courant;
END IF;
END LOOP;
RETURN plus_petit;
END
$function$;

Auparavant, il fallait développer le tableau avec la fonction unnest() pour réaliser la


même opération.
CREATE OR REPLACE FUNCTION pluspetit(VARIADIC liste numeric[])
RETURNS numeric
LANGUAGE plpgsql
AS $function$
DECLARE
courant numeric;
plus_petit numeric;
BEGIN
FOR courant IN SELECT unnest(liste) LOOP
IF plus_petit IS NULL OR courant < plus_petit THEN
plus_petit := courant;
END IF;
END LOOP;
RETURN plus_petit;
END
$function$;

529
https://dalibo.com/formations
SQL pour PostgreSQL

12.3 ROUTINES POLYMORPHES

12.3.1 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

Pour pouvoir utiliser la même fonction en utilisant des types différents, il est nécessaire
de la redéfinir avec les différents types autorisés en entrée. Par exemple, pour autoriser
l’utilisation de données de type integer ou float en entrée et retournés par une même
fonction, il faut la dupliquer.
CREATE OR REPLACE FUNCTION
addition(var1 integer, var2 integer)
RETURNS integer
AS $$
DECLARE
somme integer;
BEGIN
somme := var1 + var2;
RETURN somme;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION


addition(var1 float, var2 float)
RETURNS float
AS $$
DECLARE
somme float;
BEGIN
somme := var1 + var2;
RETURN somme;
END;
$$ LANGUAGE plpgsql;

L’utilisation de types polymorphes permet d’éviter ce genre de duplications fastidieuses.

530
12. PL/PGSQL AVANCÉ

12.3.2 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

12.3.3 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

12.3.4 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;

531
https://dalibo.com/formations
SQL pour PostgreSQL

12.3.5 ROUTINES POLYMORPHES : TESTS

# SELECT addition(1, 3);


addition
----------
4
(1 row)

# SELECT addition(1.3, 3.5);


addition
----------
4.8
(1 row)

L’opérateur + étant défini pour les entiers comme pour les numeric, la fonction ne pose
aucun problème pour ces deux types de données, et retourne une donnée du même type
que les données d’entrée.

12.3.6 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

Le typage n’étant connu qu’à l’exécution, c’est aussi à ce moment que se déclenchent les
erreurs.

De même, l’affectation du type unique pour tous les éléments se fait sur la base du premier
élément, ainsi :

# SELECT addition(1, 3.5);


ERROR: function addition(integer, numeric) does not exist
LIGNE 1 : SELECT addition(1, 3.5);
^

532
12. PL/PGSQL AVANCÉ

ASTUCE : No function matches the given name and argument types.


You might need to add explicit type casts.

génère une erreur car du premier argument est déduit le type integer, ce qui n’est évide-
ment pas le cas du deuxième. Il peut donc être nécessaire d’utiliser une conversion ex-
plicite pour résoudre ce genre de problématique.

# SELECT addition(1::numeric, 3.5);


addition
----------
4.5
(1 row)

12.4 FONCTIONS TRIGGER

12.4.1 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

Un trigger est une spécification précisant que la base de données doit exécuter une fonc-
tion particulière quand un certain type d’opération est traité. Les fonctions trigger peu-
vent être définies pour s’exécuter avant ou après une commande INSERT, UPDATE, DELETE
ou TRUNCATE.

La fonction trigger doit être définie avant que le trigger lui-même puisse être créé. La
fonction trigger doit être déclarée comme une fonction ne prenant aucun argument et
retournant un type trigger.

Une fois qu’une fonction trigger est créée, le trigger est créé avec CREATE TRIGGER. La
même fonction trigger est utilisable par plusieurs triggers.

Un trigger TRUNCATE ne peut utiliser que le mode par instruction, contrairement aux autres
triggers pour lesquels vous avez le choix entre « par ligne » et « par instruction ».

Enfin, l’instruction COPY est traitée comme s’il s’agissait d’une commande INSERT.
533
https://dalibo.com/formations
SQL pour PostgreSQL

À noter que les problématiques de visibilité et de volatilité depuis un trigger sont assez
complexes dès lors que l’on lit ou modifie les données. Voir la documentation139 pour
plus de détails à ce sujet.

12.4.2 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

12.4.3 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

12.4.4 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

139
https://docs.postgresql.fr/current/trigger-datachanges.html

534
12. PL/PGSQL AVANCÉ

12.4.5 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

Vous pourriez aussi rencontrer dans du code la variable TG_RELNAME. C’est aussi le nom de
la table qui a déclenché le trigger. Attention, cette variable est obsolète, il est préférable
d’utiliser maintenant TG_TABLE_NAME.

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

La fonction trigger est déclarée sans arguments mais il est possible de lui en passer dans
la déclaration du trigger. Dans ce cas, il faut utiliser les deux variables ci-dessus pour y
accéder. Attention, tous les arguments sont convertis en texte. Il faut donc se cantonner
à des informations simples, sous peine de compliquer le code.

CREATE OR REPLACE FUNCTION verifier_somme()


RETURNS trigger AS $$
DECLARE
fact_limit integer;
arg_color varchar;
BEGIN
fact_limit := TG_ARGV[0];

IF NEW.somme > fact_limit THEN


RAISE NOTICE 'La facture % necessite une verification. '
'La somme % depasse la limite autorisee de %.',
NEW.idfact, NEW.somme, fact_limit;
END IF;

NEW.datecreate := current_timestamp;

return NEW;
535
https://dalibo.com/formations
SQL pour PostgreSQL

END;
$$
LANGUAGE plpgsql;

CREATE TRIGGER trig_verifier_debit


BEFORE INSERT OR UPDATE ON test
FOR EACH ROW
EXECUTE PROCEDURE verifier_somme(400);

CREATE TRIGGER trig_verifier_credit


BEFORE INSERT OR UPDATE ON test
FOR EACH ROW
EXECUTE PROCEDURE verifier_somme(800);

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

Une fonction trigger retourne le type spécial trigger. Pour cette raison, ces fonctions ne
peuvent être utilisées que dans le contexte d’un ou plusieurs triggers. Pour pouvoir être
utilisée comme valeur de retour dans la fonction (avec RETURN), une variable doit être de
structure identique à celle de la table sur laquelle le trigger a été déclenché. Les variables
spéciales OLD (ancienne valeur avant application de l’action à l’origine du déclenchement)
et NEW (nouvelle valeur après application de l’action) sont également disponibles, utilis-
ables et même modifiables.

La valeur de retour d’un trigger de type ligne (ROW) déclenché avant l’opération (BEFORE)
peut changer complètement l’effet de la commande ayant déclenché le trigger. Par exem-
ple, il est possible d’annuler complètement l’action sans erreur (et d’empêcher également
tout déclenchement ultérieur d’autres triggers pour cette même action) en retournant
NULL. Il est également possible de changer les valeurs de la nouvelle ligne créée par une
action INSERT ou UPDATE en retournant une des valeurs différentes de NEW (ou en modi-
fiant NEW directement). Attention, dans le cas d’une fonction trigger BEFORE déclenchée

536
12. PL/PGSQL AVANCÉ

par une action DELETE, in faut prendre en compte que NEW contient NULL, en conséquence
RETURN NEW; provoquera l’annulation du DELETE ! Dans ce cas, si on désire laisser l’action
inchangée, la convention est de faire un RETURN OLD;.

En revanche, la valeur de retour utilisée n’a pas d’effet dans les cas des triggers ROW et
AFTER, et des triggers STATEMENT. À noter que bien que la valeur de retour soit ignorée
dans ce cas, il est possible d’annuler l’action d’un trigger de type ligne intervenant après
l’opération ou d’un trigger à l’instruction en remontant une erreur à l’exécution de la fonc-
tion.

12.4.8 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);

12.4.9 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;

537
https://dalibo.com/formations
SQL pour PostgreSQL

12.4.10 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 trans-
action
• CREATE TRIGGER view_insert INSTEAD OF INSERT ON my_view

• On peut ne déclencher un trigger que si une condition est vérifiée. Cela simplifie
souvent le code du trigger, et gagne en performances : plus besoin pour le moteur
d’aller exécuter la fonction.
• On peut ne déclencher un trigger que si une colonne spécifique a été modifiée. Il
ne s’agit donc que de triggers sur UPDATE. Encore un moyen de simplifier le code et
de gagner en performances en évitant les déclenchements inutiles.
• On peut créer un trigger en le déclarant comme étant un trigger de contrainte. Il
peut alors être deferrable, deferred, comme tout autre contrainte, c’est-à-dire n’être
exécuté qu’au moment de la validation de la transaction, ce qui permet de ne vérifier
les contraintes implémentées par le trigger qu’au moment de la validation finale.
• On peut créer un trigger sur une vue. C’est un trigger INSTEAD OF, qui permet de
programmer de façon efficace les INSERT/UPDATE/DELETE/TRUNCATE sur les vues.
Auparavant, il fallait passer par le système de règles (RULES), complexe et sujet à
erreurs.

12.4.11 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();

Dans le cas d’un trigger en mode instruction, il n’est pas possible d’utiliser les variables
OLD et NEW car elles ciblent une seule ligne. Pour cela, le standard SQL parle de tables de
transition.

538
12. PL/PGSQL AVANCÉ

La version 10 de PostgreSQL permet donc de rattraper le retard à ce sujet par rapport au


standard SQL et SQL Server.

Voici un exemple de leur utilisation.

Nous allons créer une table t1 qui aura le trigger et une table archives qui a pour but de
récupérer les enregistrements supprimés de la table t1.
CREATE TABLE t1 (c1 integer, c2 text);

CREATE TABLE archives (id integer GENERATED ALWAYS AS IDENTITY,


dlog timestamp DEFAULT now(),
t1_c1 integer, t1_c2 text);

Maintenant, il faut créer le code de la procédure stockée :


CREATE OR REPLACE FUNCTION log_delete() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO archives (t1_c1, t1_c2) SELECT c1, c2 FROM oldtable;
RETURN null;
END
$$;

Et ajouter le trigger sur la table t1 :


CREATE TRIGGER tr1
AFTER DELETE ON t1
REFERENCING OLD TABLE AS oldtable
FOR EACH STATEMENT
EXECUTE PROCEDURE log_delete();

Maintenant, insérons un million de ligne dans t1 et supprimons-les :


INSERT INTO t1 SELECT i, 'Ligne '||i FROM generate_series(1, 1000000) i;

DELETE FROM t1;


Time: 2141.871 ms (00:02.142)

La suppression avec le trigger prend 2 secondes. Il est possible de connaître le temps à


supprimer les lignes et le temps à exécuter le trigger en utilisant l’ordre EXPLAIN ANALYZE :
TRUNCATE archives;

INSERT INTO t1 SELECT i, 'Ligne '||i FROM generate_series(1, 1000000) i;

EXPLAIN (ANALYZE) DELETE FROM t1;


QUERY PLAN
--------------------------------------------------------------------------
Delete on t1 (cost=0.00..14241.98 rows=796798 width=6)
(actual time=781.612..781.612 rows=0 loops=1)
-> Seq Scan on t1 (cost=0.00..14241.98 rows=796798 width=6)
539
https://dalibo.com/formations
SQL pour PostgreSQL

(actual time=0.113..104.328 rows=1000000 loops=1)


Planning time: 0.079 ms
Trigger tr1: time=1501.688 calls=1
Execution time: 2287.907 ms
(5 rows)

Donc la suppression des lignes met 0,7 seconde alors que l’exécution du trigger met 1,5
seconde.

Pour comparer, voici l’ancienne façon de faire (configuration d’un trigger en mode ligne) :

CREATE OR REPLACE FUNCTION log_delete() RETURNS trigger LANGUAGE plpgsql AS $$


BEGIN
INSERT INTO archives (t1_c1, t1_c2) VALUES (old.c1, old.c2);
RETURN null;
END
$$;

DROP TRIGGER tr1 ON t1;

CREATE TRIGGER tr1


AFTER DELETE ON t1
FOR EACH ROW
EXECUTE PROCEDURE log_delete();

TRUNCATE archives;

TRUNCATE t1;

INSERT INTO t1 SELECT i, 'Ligne '||i FROM generate_series(1, 1000000) i;

DELETE FROM t1;


Time: 8445.697 ms (00:08.446)

TRUNCATE archives;

INSERT INTO t1 SELECT i, 'Ligne '||i FROM generate_series(1, 1000000) i;

EXPLAIN (ANALYZE) DELETE FROM t1;


QUERY PLAN
--------------------------------------------------------------------------
Delete on t1 (cost=0.00..14241.98 rows=796798 width=6)
(actual time=1049.420..1049.420 rows=0 loops=1)
-> Seq Scan on t1 (cost=0.00..14241.98 rows=796798 width=6)
(actual time=0.061..121.701 rows=1000000 loops=1)
Planning time: 0.096 ms
Trigger tr1: time=7709.725 calls=1000000

540
12. PL/PGSQL AVANCÉ

Execution time: 8825.958 ms


(5 rows)

Donc avec un trigger en mode ligne, la suppression du million de lignes met presque
9 secondes à s’exécuter, dont 7,7 pour l’exécution du trigger. Sur le trigger en mode
instruction, il faut compter 2,2 secondes, dont 1,5 sur le trigger. Les tables de transition
nous permettent de gagner en performance.

Le gros intérêt des tables de transition est le gain en performance que cela apporte.

12.5 CURSEURS

12.5.1 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

À noter que la notion de curseur existe aussi en SQL pur, sans passer par une routine
PL/pgSQL. On les crée en utilisant la commande DECLARE, et les règles de manipulation
sont légèrement différentes (on peut par exemple créer un curseur WITH HOLD, qui per-
sistera après la fin de la transaction). Voir la documentation pour plus d’informations à ce
sujet : https://docs.postgresql.fr/current/sql-declare.html

541
https://dalibo.com/formations
SQL pour PostgreSQL

12.5.2 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;

La première forme permet la création d’un curseur non lié à une requête.

12.5.3 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

Voici un exemple de lien entre une requête et un curseur :


OPEN curseur FOR SELECT * FROM ma_table;

Et voici un exemple d’utilisation d’une requête dynamique :


OPEN curseur FOR EXECUTE 'SELECT * FROM ' || quote_ident(TG_TABLE_NAME);

12.5.4 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;

542
12. PL/PGSQL AVANCÉ

12.5.5 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

12.5.6 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

12.5.7 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;

Attention, ces différentes syntaxes ne modifient pas les données dans le curseur en
mémoire, mais font réellement la modification dans la table. L’emplacement actuel du
curseur est utilisé ici pour identifier la ligne correspondante à mettre à jour.

543
https://dalibo.com/formations
SQL pour PostgreSQL

12.5.8 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

12.5.9 CURSEURS : RENVOI D'UN CURSEUR

• Fonction renvoyant une valeur de type refcursor


• Permet donc de renvoyer plusieurs valeurs

Voici un exemple d’utilisation d’une référence de curseur retournée par une fonction :

CREATE FUNCTION consult_all_stock(refcursor) RETURNS refcursor AS $$


BEGIN
OPEN $1 FOR SELECT * FROM stock;
RETURN $1;
END;
$$ LANGUAGE plpgsql;

-- doit être dans une transaction pour utiliser les curseurs.


BEGIN;

SELECT * FROM consult_all_stock('cursor_a');

FETCH ALL FROM cursor_a;


COMMIT;

12.6 CONTRÔLE TRANSACTIONNEL

• Procédures uniquement !
• COMMIT et ROLLBACK
• Pas de BEGIN
– automatique après la fin d’une transaction
• Ne fonctionne pas à l’intérieur d’une transaction
• Incompatible avec une clause EXCEPTION

Voici un exemple avec COMMIT ou ROLLBACK suivant que le nombre est pair ou impair :

544
12. PL/PGSQL AVANCÉ

CREATE TABLE test1 (a int) ;

CREATE OR REPLACE PROCEDURE transaction_test1()


LANGUAGE plpgsql
AS $$
BEGIN
FOR i IN 0..5 LOOP
INSERT INTO test1 (a) VALUES (i);
IF i % 2 = 0 THEN
COMMIT;
ELSE
ROLLBACK;
END IF;
END LOOP;
END
$$;

CALL transaction_test1();

SELECT * FROM test1;


a | b
---+---
0 |
2 |
4 |
6 |
8 |
(5 lignes)

Une exemple plus fréquemment utilisé est celui d’une procédure effectuant un traitement
de modification des données par lots, et donc faisant un COMMIT à intervalle régulier.

Noter qu’il n’y a pas de BEGIN explicite dans la gestion des transactions. Après un COMMIT
ou un ROLLBACK, un BEGIN est immédiatement exécuté.

On ne peut pas imbriquer des transactions :

BEGIN ; CALL transaction_test1() ;

ERROR: invalid transaction termination


CONTEXTE : PL/pgSQL function transaction_test1() line 6 at COMMIT

On ne peut pas utiliser en même temps une clause EXCEPTION et le contrôle transaction-
nel :

DO LANGUAGE plpgsql $$
BEGIN
BEGIN
545
https://dalibo.com/formations
SQL pour PostgreSQL

INSERT INTO test1 (a) VALUES (1);


COMMIT;
INSERT INTO test1 (a) VALUES (1/0);
COMMIT;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'caught division_by_zero';
END;
END;
$$;

ERREUR: cannot commit while a subtransaction is active


CONTEXTE : fonction PL/pgSQL inline_code_block, ligne 5 à COMMIT

12.7 GESTION DES ERREURS

12.7.1 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 ex-
ceptions

12.7.2 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.

546
12. PL/PGSQL AVANCÉ

END

12.7.3 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

12.7.4 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 anci-
enne valeur

12.7.5 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 cer-
taines 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.
547
https://dalibo.com/formations
SQL pour PostgreSQL

• Les erreurs sont contenues dans des classes d’erreurs plus génériques, qui peuvent
aussi être utilisées

Toutes les erreurs sont référencées dans la documentation140

Attention, des codes d’erreurs nouveaux apparaissent à chaque version.

La classe data_exception contient de nombreuses erreurs, comme datetime_field_overflow,


invalid_escape_character, invalid_binary_representation… On peut donc, dans la déclara-
tion de l’exception, intercepter toutes les erreurs de type data_exception d’un coup, ou
une par une.

L’instruction GET STACKED DIAGNOSTICS permet d’avoir une vision plus précise de
l’erreur récupéré par le bloc de traitement des exceptions. La liste de toutes les
informations que l’on peut collecter est disponible dans la documentation141 .

La démonstration ci-dessous montre comment elle peut être utilisée.

# CREATE TABLE t5(c1 integer PRIMARY KEY);


CREATE TABLE
# INSERT INTO t5 VALUES (1);
INSERT 0 1
# CREATE OR REPLACE FUNCTION test(INT4) RETURNS void AS $$
DECLARE
v_state TEXT;
v_msg TEXT;
v_detail TEXT;
v_hint TEXT;
v_context TEXT;
BEGIN
BEGIN
INSERT INTO t5 (c1) VALUES ($1);
EXCEPTION WHEN others THEN
GET STACKED DIAGNOSTICS
v_state = RETURNED_SQLSTATE,
v_msg = MESSAGE_TEXT,
v_detail = PG_EXCEPTION_DETAIL,
v_hint = PG_EXCEPTION_HINT,
v_context = PG_EXCEPTION_CONTEXT;
raise notice E'Et une exception :
state  : %
message: %
detail : %
hint : %
context: %', v_state, v_msg, v_detail, v_hint, v_context;

140
https://docs.postgresql.fr/current/errcodes-appendix.html
141
https://docs.postgresql.fr/current/plpgsql-control-structures.html#plpgsql-exception-diagnostics-values

548
12. PL/PGSQL AVANCÉ

END;
RETURN;
END;
$$ LANGUAGE plpgsql;
# SELECT test(2);
test
------

(1 row)

# SELECT test(2);
NOTICE: Et une exception :
state : 23505
message: duplicate key value violates unique constraint "t5_pkey"
detail : Key (c1)=(2) already exists.
hint :
context: SQL statement "INSERT INTO t5 (c1) VALUES ($1)"
PL/pgSQL function test(integer) line 10 at SQL statement
test
------

(1 row)

12.7.6 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

Il convient de noter qu’un message envoyé de cette manière ne fera pas partie de
l’éventuel résultat d’une fonction, et ne sera donc pas exploitable en SQL. Pour cela, il
faut utiliser l’instruction RETURN avec un type de retour approprié.

Le traitement des messages de ce type et leur destination d’envoi sont contrôlés par le
serveur à l’aide des paramètres log_min_messages et client_min_messages.

549
https://dalibo.com/formations
SQL pour PostgreSQL

12.7.7 MESSAGES D'ERREURS : RAISE - 2

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

Les autres niveaux pour RAISE ne sont que des messages, sans déclenchement
d’exception.

12.7.8 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.

12.7.9 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

550
12. PL/PGSQL AVANCÉ

12.7.10 MESSAGES D'ERREURS : RAISE EXCEPTION - 2

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

Le rôle d’une exception est d’intercepter une erreur pour exécuter un traitement per-
mettant soit de corriger l’erreur, soit de remonter une erreur pertinente. Intercepter un
problème pour retourner « erreur interne » n’est pas une bonne idée.

12.7.11 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

Démonstration en plusieurs étapes :

# CREATE TABLE ma_table (


id integer unique
);
CREATE TABLE

# CREATE OR REPLACE FUNCTION public.demo_exception()


RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
INSERT INTO ma_table VALUES (1);
-- Va déclencher une erreur de violation de contrainte d'unicité
INSERT INTO ma_table VALUES (1);
END
$function$;
CREATE FUNCTION

# SELECT demo_exception();
ERROR: duplicate key value violates unique constraint "ma_table_id_key"
DETAIL: Key (id)=(1) already exists.
CONTEXT: SQL statement "INSERT INTO ma_table VALUES (1)"
PL/pgSQL function demo_exception() line 6 at SQL statement
551
https://dalibo.com/formations
SQL pour PostgreSQL

Une exception a été remontée avec un message explicite.


# SELECT * FROM ma_table ;
a
---
(0 row)

La fonction a bien été annulée.

12.7.12 FLUX DES ERREURS DANS DU CODE PL - 2

• Les erreurs remontent


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

# CREATE OR REPLACE FUNCTION public.demo_exception()


RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
INSERT INTO ma_table VALUES (1);
-- Va déclencher une erreur de violation de contrainte d'unicité
INSERT INTO ma_table VALUES (1);
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'violation d''unicite, mais celle-ci n''est pas grave';
RAISE NOTICE 'erreur: %',sqlerrm;
END
$function$;
CREATE FUNCTION

# SELECT demo_exception();
NOTICE: violation d'unicite, mais celle-ci n'est pas grave
NOTICE: erreur: duplicate key value violates unique constraint "ma_table_id_key"
demo_exception
----------------

(1 row)

L’erreur est bien devenue un message de niveau NOTICE.


# SELECT * FROM ma_table ;
a
---
(0 row)

La table n’en reste pas moins vide pour autant puisque le bloc a été annulé.

552
12. PL/PGSQL AVANCÉ

12.7.13 FLUX DES ERREURS DANS DU CODE PL - 3

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

Voici une nouvelle version de la fonction :


# CREATE OR REPLACE FUNCTION public.demo_exception()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
INSERT INTO ma_table VALUES (1);
-- L'operation suivante pourrait échouer.
-- Il ne faut pas perdre le travail effectué jusqu'à ici
BEGIN
-- Va déclencher une erreur de violation de contrainte d'unicité
INSERT INTO ma_table VALUES (1);
EXCEPTION WHEN unique_violation THEN
-- Cette exception est bien celle du bloc imbriqué
RAISE NOTICE 'violation d''unicite, mais celle-ci n''est pas grave';
RAISE NOTICE 'erreur: %',sqlerrm;
END; -- Fin du bloc imbriqué
END
$function$;
CREATE FUNCTION

# SELECT demo_exception();
NOTICE: violation d'unicite, mais celle-ci n'est pas grave
NOTICE: erreur: duplicate key value violates unique constraint "ma_table_id_key"
demo_exception
----------------

(1 row)

En apparence, le résultat est identique.


# SELECT * FROM ma_table ;
a
---
1
(1 row)

Mais cette fois-ci, le bloc BEGIN parent n’a pas eu d’exception, il s’est donc bien terminé.

553
https://dalibo.com/formations
SQL pour PostgreSQL

12.7.14 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

On commence par ajouter une contrainte sur la colonne pour empêcher les valeurs
supérieures ou égales à 10 :

# ALTER TABLE ma_table ADD CHECK (id < 10 ) ;


ALTER TABLE

Puis, on recrée la fonction de façon à ce qu’elle déclenche cette erreur dans le bloc le plus
bas, et la gère uniquement dans le bloc parent :

CREATE OR REPLACE FUNCTION public.demo_exception()


RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
BEGIN
INSERT INTO ma_table VALUES (1);
-- L'operation suivante pourrait échouer.
-- Il ne faut pas perdre le travail effectué jusqu'à ici
BEGIN
-- Va déclencher une erreur de violation de check (col < 10)
INSERT INTO ma_table VALUES (100);
EXCEPTION WHEN unique_violation THEN
-- Cette exception est bien celle du bloc imbriqué
RAISE NOTICE 'violation d''unicite, mais celle-ci n''est pas grave';
RAISE NOTICE 'erreur: %',sqlerrm;
END; -- Fin du bloc imbriqué
EXCEPTION WHEN check_violation THEN
RAISE NOTICE 'violation de contrainte check';
RAISE EXCEPTION 'mais on va remonter une exception à l''appelant, '
'juste pour le montrer';
END
$function$;

Exécutons la fonction :

# SELECT demo_exception();
ERROR: duplicate key value violates unique constraint "ma_table_id_key"
DETAIL: Key (id)=(1) already exists.
CONTEXT: SQL statement "INSERT INTO ma_table VALUES (1)"
PL/pgSQL function demo_exception() line 4 at SQL statement

C’est normal, nous avons toujours l’enregistrement à 1 du test précédent. L’exception

554
12. PL/PGSQL AVANCÉ

se déclenche donc dans le bloc parent, sans espoir d’interception: nous n’avons pas
d’exception pour lui.

Nettoyons donc la table, pour reprendre le test :


# TRUNCATE ma_table ;
TRUNCATE TABLE
# SELECT demo_exception();
NOTICE: violation de contrainte check
ERREUR: mais on va remonter une exception à l'appelant, juste pour le montrer
CONTEXT: PL/pgSQL function demo_exception() line 17 at RAISE

Le gestionnaire d’exception qui intercepte l’erreur est bien ici celui de l’appelant. Par
ailleurs, comme nous retournons nous-même une exception, la requête ne retourne pas
de résultat, mais une erreur : il n’y a plus personne pour récupérer l’exception, c’est donc
PostgreSQL lui-même qui s’en charge.

12.8 SÉCURITÉ

12.8.1 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

12.8.2 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 ]

555
https://dalibo.com/formations
SQL pour PostgreSQL

12.8.3 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 ]

12.8.4 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 de l’utilisateur qui en est le propriétaire
– équivalent du sudo Unix
• Il faut impérativement sécuriser les variables d’environnement (surtout le search
path) en SECURITY DEFINER

Exemple d’une fonction en SECURITY DEFINER avec un search path sécurisé :


CREATE OR REPLACE FUNCTION instance_is_in_backup ( )
RETURNS BOOLEAN AS $$
DECLARE is_exists BOOLEAN;
BEGIN
-- Set a secure search_path: trusted schemas, then 'pg_temp'.
PERFORM pg_catalog.set_config('search_path', 'pg_temp', true);
SELECT ((pg_stat_file('backup_label')).modification IS NOT NULL)
INTO is_exists;
RETURN is_exists;
EXCEPTION
WHEN undefined_file THEN
RETURN false;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

556
12. PL/PGSQL AVANCÉ

12.8.5 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

Certains utilisateurs créent des vues pour filtrer des lignes, afin de restreindre la visibil-
ité sur certaines données. Or, cela peut se révéler dangereux si un utilisateur malinten-
tionné a la possibilité de créer une fonction car il peut facilement contourner cette sécu-
rité si cette option n’est pas utilisée, notamment en jouant sur des paramètres de fonction
comme COST, qui permet d’indiquer au planificateur un coût estimé pour la fonction.

En indiquant un coût extrêmement faible, le planificateur aura tendance à réécrire la


requête, et à déplacer l’exécution de la fonction dans le code même de la vue, avant
l’application des filtres restreignant l’accès aux données : la fonction a donc accès a tout
le contenu de la table, et peut faire fuiter des données normalement inaccessibles, par
exemple à travers l’utilisation de la commande RAISE.

L’option security_barrier des vues dans PostgreSQL bloque ce comportement du plan-


ificateur, mais en conséquence empêche le choix de plans d’exécutions potentiellement
plus performants. Déclarer une fonction avec l’option LEAKPROOF permet d’indiquer à
PostgreSQL que celle-ci ne peut pas occasionner de fuite d’informations. Ainsi, le planifi-
cateur de PostgreSQL sait qu’il peut en optimiser l’exécution. Cette option n’est accessi-
ble qu’aux superutilisateurs.

12.8.6 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

557
https://dalibo.com/formations
SQL pour PostgreSQL

12.8.7 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;
:

La méta-commande psql \df+ public.addition permet également d’obtenir cette in-


formation.

12.8.8 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

Voici un exemple simple :


CREATE TABLE ma_table_secrete1 (b integer, a integer);
INSERT INTO ma_table_secrete1 SELECT i,i from generate_series(1,20) i;

CREATE OR REPLACE FUNCTION demo_injection ( param1 text, value1 text )


RETURNS SETOF ma_table_secrete1
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
-- Cette fonction prend un nom de colonne variable
-- et l'utilise dans une clause WHERE
-- Il faut donc une requête dynamique
-- Par contre, mon utilisateur 'normal' qui appelle
-- n'a droit qu'aux enregistrements où a<10

558
12. PL/PGSQL AVANCÉ

DECLARE
ma_requete text;
ma_ligne record;
BEGIN
ma_requete := 'SELECT * FROM ma_table_secrete1 WHERE ' || param1 || ' = ' ||
value1 || ' AND a < 10';
RETURN QUERY EXECUTE ma_requete;
END
$function$;

# SELECT * from demo_injection ('b','2');


a | b
---+---
2 | 2
(1 row)

# SELECT * from demo_injection ('a','20');


a | b
---+---
(0 row)

Tout va bien, elle effectue ce qui est demandé.

Par contre, elle effectue aussi ce qui n’est pas prévu :

# SELECT * from demo_injection ('1=1 --','');


a | b
-----+-----
1 | 1
2 | 2
3 | 3
4 | 4
5 | 5
6 | 6
7 | 7
8 | 8
9 | 9
10 | 10
11 | 11
12 | 12
13 | 13
14 | 14
15 | 15
16 | 16
17 | 17
18 | 18
19 | 19
20 | 20
559
https://dalibo.com/formations
SQL pour PostgreSQL

(20 lignes)

Cet exemple est évidemment simplifié.

Une règle demeure : ne jamais faire confiance aux paramètres d’une fonction. Au mini-
mum, un quote_ident pour param1 et un quote_literal pour val1 étaient obligatoires,
pour se protéger de ce genre de problèmes.

12.9 OPTIMISATION

12.9.1 FONCTIONS IMMUABLES, 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

Les fonctions de ce type sont susceptibles de renvoyer un résultat différent à chaque


appel, comme par exemple random() ou setval().

Toute fonction ayant des effets de bords doit être qualifiée volatile dans le but d’éviter
que PostgreSQL utilise un résultat intermédiaire déjà calculé et évite ainsi d’exécuter le
code de la fonction.

À noter qu’il est possible de « forcer » le pré-calcul du résultat d’une fonction volatile dans
une requête SQL en utilisant une sous-requête. Par exemple, dans l’exemple suivant,
random() est exécutée pour chaque ligne de la table ma_table, et renverra donc une
valeur différente par ligne :
SELECT random() FROM ma_table;

Par contre, en utilisant une sous-requête, l’optimiseur va pré-calculer le résultat de


random()... l’exécution sera donc plus rapide, mais le résultat différent, puisque la même
valeur sera affichée pour toutes les lignes !
SELECT ( SELECT random() ) FROM ma_table;

560
12. PL/PGSQL AVANCÉ

12.9.2 FONCTIONS IMMUABLES, STABLES OU VOLATILES - 2

• immutable : fonctions déterministes, dont le résultat peut être précalculé avant


de planifier la requête.

Certaines fonctions que l’on écrit sont déterministes. C’est-à-dire qu’à paramètre(s) iden-
tique(s), le résultat est identique.

Le résultat de telles fonctions est alors remplaçable par son résultat avant même de com-
mencer à planifier la requête.

Voici un exemple qui utilise cette particularité :


create function factorielle (a integer) returns bigint as
$$
declare
result bigint;
begin
if a=1 then
return 1;
else
return a*(factorielle(a-1));
end if;
end;
$$
language plpgsql immutable;

# CREATE TABLE test (a bigint UNIQUE);


CREATE TABLE
# INSERT INTO test SELECT generate_series(1,1000000);
INSERT 0 1000000
# ANALYZE test;
# EXPLAIN ANALYZE SELECT * FROM test WHERE a < factorielle(12);
QUERY PLAN
--------------------------------------------------------------------
Seq Scan on test (cost=0.00..16925.00 rows=1000000 width=8)
(actual time=0.032..130.921 rows=1000000 loops=1)
Filter: (a < '479001600'::bigint)
Planning time: 896.039 ms
Execution time: 169.954 ms
(4 rows)

La fonction est exécutée une fois, remplacée par sa constante, et la requête est ensuite
planifiée.

Si on déclare la fonction comme stable :


# EXPLAIN ANALYZE SELECT * FROM test WHERE a < factorielle(12);
QUERY PLAN
561
https://dalibo.com/formations
SQL pour PostgreSQL

----------------------------------------------------------
Index Only Scan using test_a_key on test
(cost=0.68..28480.67 rows=1000000 width=8)
(actual time=0.137..115.592 rows=1000000 loops=1)
Index Cond: (a < factorielle(12))
Heap Fetches: 0
Planning time: 4.682 ms
Execution time: 153.762 ms
(5 rows)

La requête est planifiée sans connaître factorielle(12), donc avec une hypothèse très
approximative sur la cardinalité. factorielle(12) est calculé, et la requête est exécutée.
Grâce au Index Only Scan, le requête s’effectue rapidement.

Si on déclare la fonction comme volatile :


# EXPLAIN ANALYZE SELECT * FROM test WHERE a < factorielle(12);
QUERY PLAN
-----------------------------------------------------------------------
Seq Scan on test (cost=0.00..266925.00 rows=333333 width=8)
(actual time=1.005..57519.702 rows=1000000 loops=1)
Filter: (a < factorielle(12))
Planning time: 0.388 ms
Execution time: 57573.508 ms
(4 rows)

La requête est planifiée, et factorielle(12) est calculé pour chaque enregistrement de la


table, car on ne sait pas si elle retourne toujours le même résultat.

12.9.3 FONCTIONS IMMUABLES, STABLES OU VOLATILES - 3

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

Ces fonctions retournent la même valeur pour la même requête SQL, mais peuvent re-
tourner une valeur différente dans la prochaine instruction.

Il s’agit typiquement de fonctions dont le traitement dépend d’autres valeurs dans la base
de données, ou bien de réglages de configuration. Les fonctions comme to_char(),
to_date() sont STABLE et non IMMUTABLE car des paramètres de configuration (locale
utilisée pour to_char(), timezone pour les fonctions temporelles, etc.) pourraient influer
sur le résultat.

À noter au passage que les fonctions de la famille de current_timestamp (et donc le


fréquemment utilisé now()) renvoient de plus une valeur constante au sein d’une même
transaction.

562
12. PL/PGSQL AVANCÉ

PostgreSQL refusera de déclarer comme STABLE toute fonction modifiant des données :
elle ne peut pas être stable si elle modifie la base.

12.9.4 OPTIMISATION : RIGUEUR

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

Les fonctions définies comme STRICT ou RETURNS NULL ON NULL INPUT annule
l’exécution de la requête si l’un des paramètres passés est NULL. Dans ce cas, la fonction
est considérée comme ayant renvoyé NULL.

Si l’on reprend l’exemple de la fonction factorielle() :


create or replace function factorielle (a integer) returns bigint as
$$
declare
result bigint;
begin
if a=1 then
return 1;
else
return a*(factorielle(a-1));
end if;
end;
$$
language plpgsql immutable STRICT;

on obtient le résultat suivant si elle est exécutée avec la valeur NULL passée en paramètre :
# EXPLAIN ANALYZE SELECT * FROM test WHERE a < factorielle(NULL);
QUERY PLAN
---------------------------------------------------
Result (cost=0.00..0.00 rows=0 width=8)
(actual time=0.002..0.002 rows=0 loops=1)
One-Time Filter: false
Planning time: 0.100 ms
Execution time: 0.039 ms
(4 rows)

563
https://dalibo.com/formations
SQL pour PostgreSQL

12.9.5 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 unique-
ment.
• À utiliser avec parcimonie
• Un bloc BEGIN imbriqué a un coût aussi
– un SAVEPOINT est créé à chaque fois.

12.9.6 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

Avant la version 9.2, un plan générique (indépendant des paramètres de l’ordre SQL) était
systématiquement généré et utilisé. Ce système permet de gagner du temps d’exécution
si la requête est réutilisée plusieurs fois, et qu’elle est coûteuse à planifier.

Toutefois, un plan générique n’est pas forcément idéal dans toutes les situations, et peut
conduire à des mauvaises performances.

Par exemple :

SELECT * FROM ma_table WHERE col_pk = param_function ;

est un excellent candidat à être écrit statiquement : le plan sera toujours le même : on
attaque l’index de la clé primaire pour trouver l’enregistrement.

SELECT * FROM ma_table WHERE col_timestamp > param_function ;

est un moins bon candidat : le plan, idéalement, dépend de param_function : on ne par-


court pas la même fraction de la table suivant la valeur de param_function.

Par défaut, un plan générique ne sera utilisé dès la première exécution d’une requête
statique que si celle-ci ne dépend d’aucun paramètre. Dans le cas contraire, cela ne se
produira qu’au bout de plusieurs exécutions de la requête, et seulement si le planificateur
détermine que les plans spécifiques utilisés n’apportent pas d’avantage par rapport au
plan générique.

564
12. PL/PGSQL AVANCÉ

12.9.7 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

L’écriture d’une requête dynamique est par contre un peu plus pénible, puisqu’il faut fab-
riquer un ordre SQL, puis le passer en paramètre à EXECUTE, avec tous les quote_* que
cela implique pour en protéger les paramètres.

Pour se faciliter la vie, on peut utiliser EXECUTE query USING param1, param2 …, qui
est même quelquefois plus lisible que la syntaxe en dur : les paramètres de la requête
sont clairement identifiés dans cette syntaxe.

Par contre, la syntaxe USING n’est utilisable que si le nombre de paramètres est fixe.

12.9.8 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

La limite est difficile à placer, il s’agit de faire un compromis entre le temps de plani-
fication d’une requête (quelques dizaines de microsecondes pour une requête basique
à potentiellement plusieurs secondes si on dépasse la dizaine de jointures) et le temps
d’exécution.

Dans le doute, réalisez un test de performance de la fonction sur un jeu de données


représentatif.

565
https://dalibo.com/formations
SQL pour PostgreSQL

12.10 OUTILS

• Deux outils disponibles


– un debugger
– un pseudo-profiler

Tous les outils d’administration PostgreSQL permettent d’écrire des routines stockées en
PL/pgSQL, la plupart avec les fonctionnalités habituelles (comme le surlignage des mots
clés, l’indentation automatique, etc.).

Par contre, pour aller plus loin, l’offre est restreinte. Il existe tout de même un debugger
qui fonctionne avec pgAdmin 4, sous la forme d’une extension.

12.10.1 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 est un outil initialement créé par Dave Page et Korry Douglas au sein
d’EnterpriseDB, repris par la communauté. Il est proposé sous license libre (Artistic 2.0).

Il fonctionne grâce à des hooks implémentés dans la version 8.2 de PostgreSQL.

Il est assez peu connu, ce qui explique que peu l’utilisent. Seul l’outil d’installation « one-
click installer » l’installe par défaut. Pour tous les autres systèmes, cela réclame une compi-
lation supplémentaire. Cette compilation est d’ailleurs peu aisée étant donné qu’il n’utilise
pas le système pgxs.

566
12. PL/PGSQL AVANCÉ

12.10.2 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

Voici les étapes à réaliser pour compiler pldebugger en prenant pour hypothèse que les
sources de PostgreSQL sont disponibles dans le répertoire /usr/src/postgresql-10 et
qu’ils ont été préconfigurés avec la commande ./configure :

• Se placer dans le répertoire contrib des sources de PostgreSQL :

$ cd /usr/src/postgresql-10/contrib

• Cloner le dépôt git :

$ git clone git://git.postgresql.org/git/pldebugger.git


Cloning into 'pldebugger'...
remote: Counting objects: 441, done.
remote: Compressing objects: 100% (337/337), done.
remote: Total 441 (delta 282), reused 171 (delta 104)
Receiving objects: 100% (441/441), 170.24 KiB, done.
Resolving deltas: 100% (282/282), done.

• Se placer dans le nouveau répertoire pldebugger :

$ cd pldebugger

• Compiler pldebugger :

$ make

• Installer pldebugger :

# make install

L’installation copie le fichier plugin_debugger.so dans le répertoire des bibliothèques


partagées de PostgreSQL. L’installation copie ensuite les fichiers SQL et de contrôle de
l’extension pldbgapi dans le répertoire extension du répertoire share de PostgreSQL.

567
https://dalibo.com/formations
SQL pour PostgreSQL

12.10.3 PLDEBUGGER - ACTIVATION

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

La configuration du paramètre shared_preload_libraries permet au démarrage


de PostgreSQL de laisser la bibliothèque plugin_debugger s’accrocher aux hooks de
l’interpréteur PL/pgSQL. Du coup, pour que la modification de ce paramètre soit prise
en compte, il faut redémarrer PostgreSQL.

L’interaction avec pldebugger se fait par l’intermédiaire de procédures stockées. Il faut


donc au préalable créer ces procédures stockées dans la base contenant les procédures
PL/pgSQL à débugguer. Cela se fait en créant l’extension :
$ psql
psql (13.0)
Type "help" for help.

postgres# create extension pldbgapi;


CREATE EXTENSION

12.10.4 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;

auto_explain est une « contrib » officielle de PostgreSQL (et non une exten-
sion). Il permet de tracer le plan d’une requête. En général, on ne trace ainsi
que les requêtes dont la durée d’exécution dépasse la durée configurée avec le
paramètre’auto_explain.log_min_duration. Par défaut, ce paramètre est à -1 pour
ne tracer aucun plan.

Comme dans un EXPLAIN classique, on peut activer toutes les options (par exemple
ANALYZE ou TIMING avec, respectivement SET auto_explain.log_analyze TO true;

568
12. PL/PGSQL AVANCÉ

et SET auto_explain.log_timing TO true;) mais l’impact en performance peut être


important même pour les requêtes qui ne seront pas tracées.

D’autres options existent, qui reprennent les paramètres habituels d’EXPLAIN, notam-
ment auto_explain.log_buffers et auto_explain.log_settings (voir la documenta-
tion142 ).

L’exemple suivant utilise deux fonctions imbriquées mais cela marche pour une simple
requête :

CREATE OR REPLACE FUNCTION table_nb_indexes (tabname IN text, nbi OUT int)


RETURNS int
LANGUAGE plpgsql
AS $$
BEGIN
SELECT COUNT(*) INTO nbi
FROM pg_index i INNER JOIN pg_class c ON (c.oid=indrelid)
WHERE relname LIKE tabname ;
RETURN ;
END ;
$$
;
CREATE OR REPLACE FUNCTION table_nb_col_indexes
(tabname IN text, nb_cols OUT int, nb_indexes OUT int)
RETURNS record
LANGUAGE plpgsql
AS $$
BEGIN
SELECT COUNT(*) INTO nb_cols
FROM pg_attribute
WHERE attname LIKE tabname ;

SELECT nbi INTO nb_indexes FROM table_nb_indexes (tabname) ;

RETURN ;
END ;
$$
;

Chargement dans la session d’auto_explain (si pas déjà présent dans shared_preload_libraries) :

LOAD 'auto_explain' ;

Activation pour toutes les requêtes, avec les options ANALYZE et BUFFERS, puis affichage
dans la console (si la sortie dans les traces ne suffit pas) :

142
https://docs.postgresql.fr/current/auto-explain.html

569
https://dalibo.com/formations
SQL pour PostgreSQL

SET auto_explain.log_min_duration TO 0 ;
SET auto_explain.log_analyze TO on ;
SET auto_explain.log_buffers TO on ;
SET client_min_messages TO log ;

Test de la première fonction : le plan s’affiche, mais les compteurs (ici juste shared hit), ne
concernent que la fonction dans son ensemble.

postgres=# SELECT * FROM table_nb_col_indexes ('pg_class') ;

LOG: duration: 2.208 ms plan:


Query Text: SELECT * FROM table_nb_col_indexes ('pg_class') ;
Function Scan on table_nb_col_indexes (cost=0.25..0.26 rows=1 width=8)
(actual time=2.203..2.203 rows=1 loops=1)
Buffers: shared hit=294

nb_cols | nb_indexes
---------+------------
0 | 3

En activant auto_explain.log_nested_statements, on voit clairement les plans de


chaque requête exécutée :
SET auto_explain.log_nested_statements TO on ;

postgres=# SELECT * FROM table_nb_col_indexes ('pg_class') ;

LOG: duration: 0.235 ms plan:


Query Text: SELECT COUNT(*) FROM pg_attribute
WHERE attname LIKE tabname
Aggregate (cost=65.95..65.96 rows=1 width=8)
(actual time=0.234..0.234 rows=1 loops=1)
Buffers: shared hit=24
-> Index Only Scan using pg_attribute_relid_attnam_index on pg_attribute
(cost=0.28..65.94 rows=1 width=0)
(actual time=0.233..0.233 rows=0 loops=1)
Index Cond: ((attname >= 'pg'::text) AND (attname < 'ph'::text))
Filter: (attname ~~ 'pg_class'::text)
Heap Fetches: 0
Buffers: shared hit=24

LOG: duration: 0.102 ms plan:


Query Text: SELECT COUNT(*) FROM pg_index i

570
12. PL/PGSQL AVANCÉ

INNER JOIN pg_class c ON (c.oid=indrelid)


WHERE relname LIKE tabname
Aggregate (cost=24.48..24.49 rows=1 width=8)
(actual time=0.100..0.100 rows=1 loops=1)
Buffers: shared hit=18
-> Nested Loop (cost=0.14..24.47 rows=1 width=0)
(actual time=0.096..0.099 rows=3 loops=1)
Buffers: shared hit=18
-> Seq Scan on pg_class c (cost=0.00..23.30 rows=1 width=4)
(actual time=0.091..0.093 rows=1 loops=1)
Filter: (relname ~~ 'pg_class'::text)
Rows Removed by Filter: 580
Buffers: shared hit=16
-> Index Only Scan using pg_index_indrelid_index on pg_index i
(cost=0.14..1.16 rows=1 width=4)
(actual time=0.003..0.004 rows=3 loops=1)
Index Cond: (indrelid = c.oid)
Heap Fetches: 0
Buffers: shared hit=2

LOG: duration: 0.703 ms plan:


Query Text: SELECT nbi FROM table_nb_indexes (tabname)
Function Scan on table_nb_indexes (cost=0.25..0.26 rows=1 width=4)
(actual time=0.702..0.702 rows=1 loops=1)
Buffers: shared hit=26

LOG: duration: 1.524 ms plan:


Query Text: SELECT * FROM table_nb_col_indexes ('pg_class') ;
Function Scan on table_nb_col_indexes (cost=0.25..0.26 rows=1 width=8)
(actual time=1.520..1.520 rows=1 loops=1)
Buffers: shared hit=59

nb_cols | nb_indexes
---------+------------
0 | 3

Cet exemple permet de mettre le doigt sur un petit problème de performance dans la
fonction : le _ est interprété comme critère de recherche. En modifiant le paramètre on
peut constater le changement de plan au niveau des index :
571
https://dalibo.com/formations
SQL pour PostgreSQL

postgres=# SELECT * FROM table_nb_col_indexes ('pg\_class') ;

LOG: duration: 0.141 ms plan:


Query Text: SELECT COUNT(*) FROM pg_attribute
WHERE attname LIKE tabname
Aggregate (cost=56.28..56.29 rows=1 width=8)
(actual time=0.140..0.140 rows=1 loops=1)
Buffers: shared hit=24
-> Index Only Scan using pg_attribute_relid_attnam_index on pg_attribute
(cost=0.28..56.28 rows=1 width=0)
(actual time=0.138..0.138 rows=0 loops=1)
Index Cond: (attname = 'pg_class'::text)
Filter: (attname ~~ 'pg\_class'::text)
Heap Fetches: 0
Buffers: shared hit=24

LOG: duration: 0.026 ms plan:


Query Text: SELECT COUNT(*) FROM pg_index i
INNER JOIN pg_class c ON (c.oid=indrelid)
WHERE relname LIKE tabname
Aggregate (cost=3.47..3.48 rows=1 width=8) (actual time=0.024..0.024 rows=1 loops=1)
Buffers: shared hit=8
-> Nested Loop (cost=0.42..3.47 rows=1 width=0) (…)
Buffers: shared hit=8
-> Index Scan using pg_class_relname_nsp_index on pg_class c
(cost=0.28..2.29 rows=1 width=4)
(actual time=0.017..0.018 rows=1 loops=1)
Index Cond: (relname = 'pg_class'::text)
Filter: (relname ~~ 'pg\_class'::text)
Buffers: shared hit=6
-> Index Only Scan using pg_index_indrelid_index on pg_index i (…)
Index Cond: (indrelid = c.oid)
Heap Fetches: 0
Buffers: shared hit=2

LOG: duration: 0.414 ms plan:


Query Text: SELECT nbi FROM table_nb_indexes (tabname)
Function Scan on table_nb_indexes (cost=0.25..0.26 rows=1 width=4)
(actual time=0.412..0.412 rows=1 loops=1)

572
12. PL/PGSQL AVANCÉ

Buffers: shared hit=16

LOG: duration: 1.046 ms plan:


Query Text: SELECT * FROM table_nb_col_indexes ('pg\_class') ;
Function Scan on table_nb_col_indexes (cost=0.25..0.26 rows=1 width=8)
(actual time=1.042..1.043 rows=1 loops=1)
Buffers: shared hit=56

nb_cols | nb_indexes
---------+------------
0 | 3

Pour les procédures, il est possible de mettre en place cette trace avec ALTER PROCEDURE
… SET auto_explain.log_min_duration = 0. Cela ne fonctionne pas pour les fonc-
tions.

pgBadger est capable de lire les plans tracés par auto_explain, de les intégrer à son
rapport et d’inclure un lien vers depesz.com143 pour une version plus lisible.

12.10.5 PLDEBUGGER - UTILISATION

• Via pgAdmin

Le menu contextuel pour accéder au débuggage d’une fonction :

143
https://explain.depesz.com/

573
https://dalibo.com/formations
SQL pour PostgreSQL

La fenêtre du débugger :

12.10.6 LOG_FUNCTIONS

• Créé par Dalibo


• License BSD
• Compilation nécessaire

log_functions est un outil créé par Guillaume Lelarge au sein de Dalibo. Il est proposé
sous license libre (BSD).

574
12. PL/PGSQL AVANCÉ

12.10.7 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

Voici les étapes à réaliser pour compiler log_functions en prenant pour hypothèse que les
sources de PostgreSQL sont disponibles dans le répertoire /home/guillaume/postgresql-9.1.4
et qu’ils ont été préconfigurés avec la commande ./configure :

• Se placer dans le répertoire contrib des sources de PostgreSQL :

$ cd /home/guillaume/postgresql-9.1.4/contrib

• Récupérer le dépôt git de log_functions :

$ git://github.com/gleu/log_functions.git
Cloning into 'log_functions'...
remote: Counting objects: 24, done.
remote: Compressing objects: 100% (15/15), done.
remote: Total 24 (delta 8), reused 24 (delta 8)
Receiving objects: 100% (24/24), 11.71 KiB, done.
Resolving deltas: 100% (8/8), done.

• Se placer dans le nouveau répertoire log_functions :

$ cd log_functions

• Compiler log_functions :

$ make

• Installer log_functions :

$ make install

L’installation copie le fichier log_functions.o dans le répertoire des bibliothèques


partagées de PostgreSQL.

Si la version de PostgreSQL est supérieure ou égale à la 9.2, alors l’installation est plus
simple et les sources de PostgreSQL ne sont plus nécessaires.

Téléchargement de log_functions :

wget http://api.pgxn.org/dist/log_functions/1.0.0/log_functions-1.0.0.zip

puis décompression et installation de l’extension :

unzip log_functions-1.0.0.zip
cd log_functions-1.0.0/
make USE_PGXS=1 && make USE_PGXS=1 install
575
https://dalibo.com/formations
SQL pour PostgreSQL

L’installation copie aussi le fichier log_functions.so dans le répertoire des bibliothèques


partagées de PostgreSQL.

12.10.8 LOG_FUNCTIONS - ACTIVATION

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

Le module log_functions est activable de deux façons.

La première consiste à demander à PostgreSQL de le charger au démarrage. Pour cela,


il faut configurer la variable shared_preload_libraries, puis redémarrer PostgreSQL
pour que le changement soit pris en compte.

La deuxième manière de l’activer est de l’activer seulement au moment où son utilisation


s’avère nécessaire. Il faut utiliser pour cela la commande LOAD en précisant le module à
charger.

La première méthode a un coût en terme de performances car le module s’exécute à


chaque exécution d’une procédure stockée écrite en PL/pgSQL. La deuxième méth-
ode rend l’utilisation du profiler un peu plus complexe. Le choix est donc laissé à
l’administrateur.

12.10.9 LOG_FUNCTIONS - CONFIGURATION

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

Les informations de profilage récupérées par log_functions sont envoyées dans les traces
de PostgreSQL. Comme cela va générer plus d’écriture, et donc plus de lenteurs, il est
possible de configurer chaque trace.

La configuration se fait soit dans le fichier postgresql.conf soit avec l’instruction SET.

Voici la liste des paramètres et leur utilité :

576
12. PL/PGSQL AVANCÉ

• log_functions.log_declare, à mettre à true pour tracer le moment où PL/pgSQL


exécute la partie DECLARE d’une procédure stockée ;
• log_functions.log_function_begin, à mettre à true pour tracer le moment où
PL/pgSQL exécute la partie BEGIN d’une procédure stockée ;
• log_functions.log_function_end, à mettre à true pour tracer le moment où
PL/pgSQL exécute la partie END d’une procédure stockée ;
• log_functions.log_statement_begin, à mettre à true pour tracer le moment où
PL/pgSQL commence l’exécution d’une instruction dans une procédure stockée ;
• log_functions.log_statement_end, à mettre à true pour tracer le moment où
PL/pgSQL termine l’exécution d’une instruction dans une procédure stockée.

Par défaut, seuls log_statement_begin et log_statement_end sont à false pour éviter


la génération de traces trop importantes.

12.10.10 LOG_FUNCTIONS - UTILISATION

• Exécuter des procédures stockées en PL/pgSQL


• Lire les journaux applicatifs
– grep très utile

Voici un exemple d’utilisation de cet outil :


b2# SELECT incremente(4);
incremente
------------
5
(1 row)

b2# LOAD 'log_functions';


LOAD
b2# SET client_min_messages TO log;
LOG: duration: 0.136 ms statement: set client_min_messages to log;
SET
b2# SELECT incremente(4);
LOG: log_functions, DECLARE, incremente
LOG: log_functions, BEGIN, incremente
CONTEXT: PL/pgSQL function "incremente" during function entry
LOG: valeur de b : 5
LOG: log_functions, END, incremente
CONTEXT: PL/pgSQL function "incremente" during function exit
LOG: duration: 118.332 ms statement: select incremente(4);
incremente
------------
577
https://dalibo.com/formations
SQL pour PostgreSQL

5
(1 row)

12.11 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

12.11.1 POUR ALLER PLUS LOIN

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

Quelques liens utiles dans la documentation de PostgreSQL :

• Chapitre 40. PL/pgSQL - Langage de procédures SQL144


• Annexe A. Codes d’erreurs de PostgreSQL145

12.11.2 QUESTIONS

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

144
https://docs.postgresql.fr/current/plpgsql.html
145
https://docs.postgresql.fr/current/errcodes-appendix.html

578
12. PL/PGSQL AVANCÉ

12.12 TRAVAUX PRATIQUES

TP2.1

Ré-écrire la fonction de division pour tracer le problème de division par zéro (vous pouvez
aussi utiliser les exceptions).

TP2.2

Tracer dans une table toutes les modifications du champ nombre dans stock. On veut
conserver l’ancienne et la nouvelle valeur. On veut aussi savoir qui a fait la modification
et quand.

Interdire la suppression des lignes dans stock. Afficher un message dans les logs dans ce
cas.

Afficher aussi un message NOTICE quand nombre devient inférieur à 5, et WARNING quand
il vaut 0.

TP2.3

Interdire à tout le monde, sauf un compte admin, l’accès à la table des logs précédemment
créée .

En conséquence, le trigger fonctionne-t-il ? Le cas échéant, le modifier pour qu’il fonc-


tionne.

TP2.4

Lire toute la table stock avec un curseur.

Afficher dans les journaux applicatifs toutes les paires (vin_id, contenant_id) pour
chaque nombre supérieur à l’argument de la fonction.

TP2.5

Ré-écrire la fonction nb_bouteilles du TP précédent de façon à ce qu’elle prenne désor-


mais en paramètre d’entrée une liste variable d’années à traiter.

579
https://dalibo.com/formations
SQL pour PostgreSQL

12.13 TRAVAUX PRATIQUES (SOLUTIONS)

TP2.1 Solution :

CREATE OR REPLACE FUNCTION division(arg1 integer, arg2 integer)


RETURNS float4 AS
$BODY$
BEGIN
RETURN arg1::float4/arg2::float4;
EXCEPTION WHEN OTHERS THEN
-- attention, division par zéro
RAISE LOG 'attention, [%]: %', SQLSTATE, SQLERRM;
RETURN 'NaN';
END $BODY$
LANGUAGE 'plpgsql' VOLATILE;

Requêtage :

cave=# SET client_min_messages TO log;


SET
cave=# SELECT division(1,5);
division
----------
0.2
(1 ligne)

cave=# SELECT division(1,0);


LOG: attention, [22012]: division par zéro
division
----------
NaN
(1 ligne)

TP2.2 Solution :

1.

La table de log :

CREATE TABLE log_stock (


id serial,
utilisateur text,
dateheure timestamp,
operation char(1),
vin_id integer,
contenant_id integer,
annee integer,

580
12. PL/PGSQL AVANCÉ

anciennevaleur integer,
nouvellevaleur integer);

La fonction trigger :

CREATE OR REPLACE FUNCTION log_stock_nombre()


RETURNS TRIGGER AS
$BODY$
DECLARE
v_requete text;
v_operation char(1);
v_vinid integer;
v_contenantid integer;
v_annee integer;
v_anciennevaleur integer;
v_nouvellevaleur integer;
v_atracer boolean := false;
BEGIN

-- ce test a pour but de vérifier que le contenu de nombre a bien changé


-- c'est forcément le cas dans une insertion et dans une suppression
-- mais il faut tester dans le cas d'une mise à jour en se méfiant
-- des valeurs NULL
v_operation := substr(TG_OP, 1, 1);
IF TG_OP = 'INSERT'
THEN
-- cas de l'insertion
v_atracer := true;
v_vinid := NEW.vin_id;
v_contenantid := NEW.contenant_id;
v_annee := NEW.annee;
v_anciennevaleur := NULL;
v_nouvellevaleur := NEW.nombre;
ELSEIF TG_OP = 'UPDATE'
THEN
-- cas de la mise à jour
v_atracer := OLD.nombre != NEW.nombre;
v_vinid := NEW.vin_id;
v_contenantid := NEW.contenant_id;
v_annee := NEW.annee;
v_anciennevaleur := OLD.nombre;
v_nouvellevaleur := NEW.nombre;
ELSEIF TG_OP = 'DELETE'
THEN
-- cas de la suppression
v_atracer := true;
v_vinid := OLD.vin_id;
581
https://dalibo.com/formations
SQL pour PostgreSQL

v_contenantid := OLD.contenant_id;
v_annee := NEW.annee;
v_anciennevaleur := OLD.nombre;
v_nouvellevaleur := NULL;
END IF;

IF v_atracer
THEN
INSERT INTO log_stock
(utilisateur, dateheure, operation, vin_id, contenant_id,
annee, anciennevaleur, nouvellevaleur)
VALUES
(current_user, now(), v_operation, v_vinid, v_contenantid,
v_annee, v_anciennevaleur, v_nouvellevaleur);
END IF;

RETURN NEW;

END $BODY$
LANGUAGE 'plpgsql' VOLATILE;

Le trigger :

CREATE TRIGGER log_stock_nombre_trig


AFTER INSERT OR UPDATE OR DELETE
ON stock
FOR EACH ROW
EXECUTE PROCEDURE log_stock_nombre();

2.

On commence par supprimer le trigger :

DROP TRIGGER log_stock_nombre_trig ON stock;

La fonction trigger :

CREATE OR REPLACE FUNCTION log_stock_nombre()


RETURNS TRIGGER AS
$BODY$
DECLARE
v_requete text;
v_operation char(1);
v_vinid integer;
v_contenantid integer;
v_annee integer;
v_anciennevaleur integer;
v_nouvellevaleur integer;
v_atracer boolean := false;

582
12. PL/PGSQL AVANCÉ

BEGIN

v_operation := substr(TG_OP, 1, 1);


IF TG_OP = 'INSERT'
THEN
-- cas de l'insertion
v_atracer := true;
v_vinid := NEW.vin_id;
v_contenantid := NEW.contenant_id;
v_annee := NEW.annee;
v_anciennevaleur := NULL;
v_nouvellevaleur := NEW.nombre;
ELSEIF TG_OP = 'UPDATE'
THEN
-- cas de la mise à jour
v_atracer := OLD.nombre != NEW.nombre;
v_vinid := NEW.vin_id;
v_contenantid := NEW.contenant_id;
v_annee := NEW.annee;
v_anciennevaleur := OLD.nombre;
v_nouvellevaleur := NEW.nombre;
END IF;

IF v_atracer
THEN
INSERT INTO log_stock
(utilisateur, dateheure, operation, vin_id, contenant_id,
anciennevaleur, nouvellevaleur)
VALUES
(current_user, now(), v_operation, v_vinid, v_contenantid,
v_anciennevaleur, v_nouvellevaleur);
END IF;

RETURN NEW;

END $BODY$
LANGUAGE 'plpgsql' VOLATILE;

Le trigger :

CREATE TRIGGER trace_nombre_de_stock


AFTER INSERT OR UPDATE
ON stock
FOR EACH ROW
EXECUTE PROCEDURE log_stock_nombre();

La deuxième fonction trigger :


583
https://dalibo.com/formations
SQL pour PostgreSQL

CREATE OR REPLACE FUNCTION empeche_suppr_stock()


RETURNS TRIGGER AS
$BODY$
BEGIN

IF TG_OP = 'DELETE'
THEN
RAISE WARNING 'Tentative de suppression du stock (%, %, %)',
OLD.vin_id, OLD.contenant_id, OLD.annee;
RETURN NULL;
ELSE
RETURN NEW;
END IF;

END $BODY$
LANGUAGE 'plpgsql' VOLATILE;

Le deuxième trigger :

CREATE TRIGGER empeche_suppr_stock_trig


BEFORE DELETE
ON stock
FOR EACH ROW
EXECUTE PROCEDURE empeche_suppr_stock();

3.

La fonction trigger :

CREATE OR REPLACE FUNCTION log_stock_nombre()


RETURNS TRIGGER AS
$BODY$
DECLARE
v_requete text;
v_operation char(1);
v_vinid integer;
v_contenantid integer;
v_annee integer;
v_anciennevaleur integer;
v_nouvellevaleur integer;
v_atracer boolean := false;
BEGIN

v_operation := substr(TG_OP, 1, 1);


IF TG_OP = 'INSERT'
THEN
-- cas de l'insertion
v_atracer := true;

584
12. PL/PGSQL AVANCÉ

v_vinid := NEW.vin_id;
v_contenantid := NEW.contenant_id;
v_annee := NEW.annee;
v_anciennevaleur := NULL;
v_nouvellevaleur := NEW.nombre;
ELSEIF TG_OP = 'UPDATE'
THEN
-- cas de la mise à jour
v_atracer := OLD.nombre != NEW.nombre;
v_vinid := NEW.vin_id;
v_contenantid := NEW.contenant_id;
v_annee := NEW.annee;
v_anciennevaleur := OLD.nombre;
v_nouvellevaleur := NEW.nombre;
END IF;

IF v_nouvellevaleur < 1
THEN
RAISE WARNING 'Il ne reste plus que % bouteilles dans le stock (%, %, %)',
v_nouvellevaleur, OLD.vin_id, OLD.contenant_id, OLD.annee;
ELSEIF v_nouvellevaleur < 5
THEN
RAISE LOG 'Il ne reste plus que % bouteilles dans le stock (%, %, %)',
v_nouvellevaleur, OLD.vin_id, OLD.contenant_id, OLD.annee;
END IF;

IF v_atracer
THEN
INSERT INTO log_stock
(utilisateur, dateheure, operation, vin_id, contenant_id,
annee, anciennevaleur, nouvellevaleur)
VALUES
(current_user, now(), v_operation, v_vinid, v_contenantid,
v_annee, v_anciennevaleur, v_nouvellevaleur);
END IF;

RETURN NEW;

END $BODY$
LANGUAGE 'plpgsql' VOLATILE;

Requêtage :

Faire des INSERT, DELETE, UPDATE pour jouer avec.

TP2.3 Solution :
585
https://dalibo.com/formations
SQL pour PostgreSQL

CREATE ROLE admin;


ALTER TABLE log_stock OWNER TO admin;
ALTER TABLE log_stock_id_seq OWNER TO admin;
REVOKE ALL ON TABLE log_stock FROM public;

cave=> insert into stock (vin_id, contenant_id, annee, nombre)


values (3,1,2020,10);
ERROR: permission denied for relation log_stock
CONTEXT: SQL statement "INSERT INTO log_stock
(utilisateur, dateheure, operation, vin_id, contenant_id,
annee, anciennevaleur, nouvellevaleur)
VALUES
(current_user, now(), v_operation, v_vinid, v_contenantid,
v_annee, v_anciennevaleur, v_nouvellevaleur)"
PL/pgSQL function log_stock_nombre() line 45 at SQL statement

ALTER FUNCTION log_stock_nombre() OWNER TO admin;


ALTER FUNCTION log_stock_nombre() SECURITY DEFINER;

cave=> insert into stock (vin_id, contenant_id, annee, nombre)


values (3,1,2020,10);
INSERT 0 1

Que constatez-vous dans log_stock ? (un petit indice : regardez l’utilisateur)

TP2.4 Solution :

CREATE OR REPLACE FUNCTION verif_nombre(maxnombre integer)


RETURNS integer AS
$BODY$
DECLARE
v_curseur refcursor;
v_resultat stock%ROWTYPE;
v_index integer;
BEGIN

v_index := 0;
OPEN v_curseur FOR SELECT * FROM stock WHERE nombre > maxnombre;
LOOP
FETCH v_curseur INTO v_resultat;
IF NOT FOUND THEN
EXIT;
END IF;
v_index := v_index + 1;
RAISE NOTICE 'nombre de (%, %) : % (supérieur à %)',
v_resultat.vin_id, v_resultat.contenant_id, v_resultat.nombre, maxnombre;

586
12. PL/PGSQL AVANCÉ

END LOOP;

RETURN v_index;

END $BODY$
LANGUAGE 'plpgsql' VOLATILE;

Requêtage:

SELECT verif_nombre(16);
INFO: nombre de (6535, 3) : 17 (supérieur à 16)
INFO: nombre de (6538, 3) : 17 (supérieur à 16)
INFO: nombre de (6541, 3) : 17 (supérieur à 16)
[...]
INFO: nombre de (6692, 3) : 18 (supérieur à 16)
INFO: nombre de (6699, 3) : 17 (supérieur à 16)
verif_nombre
--------------
107935
(1 ligne)

TP2.5

CREATE OR REPLACE FUNCTION


nb_bouteilles(v_typevin text, VARIADIC v_annees integer[])
RETURNS SETOF record
AS $BODY$
DECLARE
resultat record;
i integer;
BEGIN
FOREACH i IN ARRAY v_annees
LOOP
SELECT INTO resultat i, nb_bouteilles(v_typevin, i);
RETURN NEXT resultat;
END LOOP;
RETURN;
END
$BODY$
LANGUAGE plpgsql;

Exécution:

-- ancienne fonction
cave=# SELECT * FROM nb_bouteilles('blanc', 1990, 1995)
AS (annee integer, nb integer);
annee | nb
587
https://dalibo.com/formations
SQL pour PostgreSQL

-------+------
1990 | 5608
1991 | 5642
1992 | 5621
1993 | 5581
1994 | 5614
1995 | 5599
(6 lignes)

cave=# SELECT * FROM nb_bouteilles('blanc', 1990, 1992, 1994)


AS (annee integer, nb integer);
annee | nb
-------+------
1990 | 5608
1992 | 5621
1994 | 5614
(3 lignes)

cave=# SELECT * FROM nb_bouteilles('blanc', 1993, 1991)


AS (annee integer, nb integer);
annee | nb
-------+------
1993 | 5581
1991 | 5642
(2 lignes)

588
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

13 EXTENSIONS POSTGRESQL POUR L'UTILISATEUR

13.1 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

Les extensions sont un gros point fort de PostgreSQL. Elles permettent de rajouter des
fonctionnalités, aussi bien pour les utilisateurs que pour les administrateurs, sur tous les
sujets : fonctions utilitaires, types supplémentaires, outils d’administration avancés, voire
applications quasi-complètes. Certaines sont intégrées par le projet, mais n’importe qui
peut en proposer et en intégrer une.
589
https://dalibo.com/formations
SQL pour PostgreSQL

13.2 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

Une extension est un objet du catalogue, englobant d’autres objets. On peut la comparer
à un paquetage Linux.

Une extension peut provenir d’un projet séparé de PostgreSQL (PostGIS, par exemple, ou
le Foreign Data Wrapper Oracle).

Les extensions les plus simples peuvent se limiter à quelques objets en SQL, certaines
sont en PL/pgSQL, beaucoup sont en C. Dans ce dernier cas, il faut être conscient que la
stabilité du serveur est encore plus en jeu !

13.2.1 INSTALLATION DES EXTENSIONS

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

Au niveau du système d’exploitation, une extension nécessite des objets (binaires,


scripts…) dans l’arborescence de PostgreSQL. De nombreuses extensions sont déjà
fournies sous forme de paquets dans les distributions courantes ou par le PGDG. Dans
certains cas, il faudra aller sur le site du projet et l’installer soi-même, ce qui peut
nécessiter une compilation.

L’extension doit être ensuite déclarée dans chaque base où elle est jugée nécessaire avec
CREATE EXTENSION nom_extension. Les scripts fournis avec l’extension vont alors créer
les objets nécessaires (vues, procédures, tables…). En cas de désinstallation avec DROP

590
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

EXTENSION, ils seront supprimés. Une extension peut avoir besoin d’autres extensions : à
partir de la version 9.6 existe l’option CASCADE pour les installer automatiquement.

Le mécanisme couvre aussi la mise à jour des extensions : ALTER EXTENSION UPDATE
permet de mettre à jour une extension dans PostgreSQL suite à la mise de ses binaires.
Cela peut être nécessaire si elle contient des tables à mettre à jour, par exemple. Les
versions des extensions disponibles sur le système et celles installées dans la base en
cours sont visibles dans la vue pg_available_extensions.

Les extensions peuvent être exportées et importées par pg_dump/pg_restore. Un ex-


port par pg_dump contient un CREATE EXTENSION nom_extension, ce qui permettra de
recréer d’éventuelles tables, et le contenu de ces tables. Une mise à jour de version ma-
jeure, par exemple, permettra donc de migrer les extensions dans leur dernière version
installée sur le serveur (changement de prototypes de fonctions, nouvelles vues, etc.).

Sous psql, les extensions présentes dans la base sont visibles avec \dx :

 # \dx
Liste des extensions installées
Nom | Version | Schéma | Description
--------------------+---------+------------+--------------------------------------------------
amcheck | 1.2 | public | functions for verifying relation integrity
file_fdw | 1.0 | public | foreign-data wrapper for flat file access
hstore | 1.6 | public | data type for storing sets of (key, value) pairs
pageinspect | 1.7 | public | inspect the contents of database pages at...
pg_buffercache | 1.3 | public | examine the shared buffer cache
pg_prewarm | 1.2 | public | prewarm relation data
pg_rational | 0.0.1 | public | bigint fractions
pg_stat_statements | 1.7 | public | track execution statistics of all SQL statements...
plpgsql | 1.0 | pg_catalog | PL/pgSQL procedural language
plpython3u | 1.0 | pg_catalog | PL/Python3U untrusted procedural language
postgres_fdw | 1.0 | public | foreign-data wrapper for remote PostgreSQL servers
unaccent | 1.1 | public | text search dictionary that removes accents

13.3 CONTRIBS

Ce sont des 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
591
https://dalibo.com/formations
SQL pour PostgreSQL

• documentées : Chapitre F : « Modules supplémentaires fournis »

Une « contrib » est habituellement une extension, sauf quelques exceptions qui ne créent
pas d’objets de catalogue (auto_explain par exemple). Elles sont fournies directement
dans l’arborescence de PostgreSQL, et suivent donc strictement son rythme de révision.
Leur compatibilité est ainsi garantie. Les distributions les proposent parfois dans des pa-
quets séparés (postgresql-contrib-9.6, postgresql13-contrib…), dont l’installation
est fortement conseillée.

Il s’agit soit de fonctionnalités qui n’intéressent pas tout le monde (hstore, pg_trgm,
pgstattuple…), ou en cours de stabilisation (comme l’autovacuum avant PostgreSQL 8.1),
ou à l’inverse de dépréciation (xml2).

La documentation des contribs est dans le chapitre F des annexes146 , et est donc
fréquemment oubliée par les nouveaux utilisateurs.

13.4 QUELQUES EXTENSIONS

...plus ou moins connues

13.4.1 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

Fourni avec PostgreSQL, vous permet de chiffrer vos données147 :

• directement ;
• avec une clé PGP (gérée par exemple avec GnuPG), ce qui est préférable ;
• selon divers algorithmes courants ;
• différemment selon chaque ligne/champ.

Voici un exemple de code:


146
https://docs.postgresql.fr/current/contrib.html
147
https://docs.postgresql.fr/current/pgcrypto.html

592
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

CREATE EXTENSION pgcrypto;


UPDATE utilisateurs SET mdp = crypt('mon nouveau mot de passe',gen_salt('md5'));
INSERT INTO table_secrete (encrypted)
VALUES (pgp_sym_encrypt('mon secret','motdepasse'));

L’appel à gen_salt permet de rajouter une partie aléatoire à la chaîne à chiffrer, ce qui
évite que la même chaîne chiffrée deux fois retourne le même résultat. Cela limite donc
les attaques par dictionnaire.

La base effectuant le (dé)chiffrement, cela évite certains allers-retours. Il est préférable


que la clé de déchiffrement ne soit pas dans l’instance, et soit connue et fournie par
l’applicatif. La communication avec cet applicatif doit être sécurisée par SSL pour que
les clés et données ne transitent pas en clair.

Un gros inconvénient des données chiffrées dans la table est l’impossibilité complète de
les indexer, même avec un index fonctionnel : les données déchiffrées seraient en clair
dans cet index ! Une recherche implique donc de parcourir et déchiffrer chaque ligne...

13.4.2 POSTGIS

• Un projet totalement indépendant


• Licence GPL (logiciel libre)
• Extension de PostgreSQL aux types géométriques/géographiques
• 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 ? »

PostGIS permet donc d’écrire des requêtes de ce type :


SELECT restaurants.geom, restaurants.name FROM restaurants
WHERE EXISTS (SELECT 1 FROM routes
WHERE ST_DWithin(restaurants.geom, routes.geom, 3000)
AND route.name = 'Nationale 12')

Il fournit les fonctions d’indexation qui permettent d’accéder rapidement aux objets
géométriques, au moyen d’index GiST. La requête ci-dessous n’a évidemment pas besoin
de parcourir tous les restaurants à la recherche de ceux correspondant aux critères de
recherche.

593
https://dalibo.com/formations
SQL pour PostgreSQL

13.4.3 POSTGIS (SUITE)

• De nombreuses fonctionnalités avancées :


– Support des coordonnées géodésiques
– Projections, reprojections dans systèmes de coordonnées locaux (Lambert93
en France par exemple)
– 3D, extrusions, routage, rasters
– Opérateurs d’analyse géométrique : enveloppe convexe, simplification…
– Intégré aux principaux serveurs de carte, ETL, outils de manipulation
• Utilisé par IGN, BRGM, AirBNB, Mappy, Openstreetmap, Agence de l’eau…

PostGIS est également respectueux des normes : Open Geospatial Consortium’s “Simple
Features for SQL Specification”

Voir la liste complète des fonctionnalités148 .

C’est donc une extension très avancée de PostgreSQL. Elle est avant tout utilisée par des
spécialistes du domaine Géospatial, mais peut être utilisée aussi dans des projets moins
complexes.

13.4.4 CITEXT

Champ texte insensible à la casse :


• Beaucoup utilisé pour compatibilité avec SQL Server/MySQL
• Les fonctions de comparaison et tri deviennent insensibles à la casse
• Nécessite une conversion de casse à chaque comparaison
• Plus lent que le type texte
CREATE EXTENSION citext;
CREATE TABLE ma_table (col_insensible citext);

Ce type est très utile, par exemple dans le cas d’un portage d’une application de SQL
Server, ou MySQL, vers PostgreSQL : ces deux moteurs sont habituellement paramétrés
pour être insensibles à la casse.

Il suffit pour en profiter de créer l’extension citext, puis manipuler le type citext.

Les limitations sont les suivantes:

• Les performances sont moins bonnes sur les colonnes citext, surtout en l’absence
d’index, à cause des conversions de casse
• La maintenance de l’index, s’il y en a un, est plus coûteuse
148
https://postgis.net/features

594
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

• On ne peut pas donner de limite de taille comme avec un type varchar. Cette limi-
tation peut être contournée avec une contrainte CHECK, ou un DOMAIN.

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

CREATE EXTENSION hstore ;


CREATE TABLE demo_hstore(id serial, meta hstore);
INSERT INTO demo_hstore (meta) VALUES ('river=>t');
INSERT INTO demo_hstore (meta) VALUES ('road=>t,secondary=>t');
INSERT INTO demo_hstore (meta) VALUES ('road=>t,primary=>t');
CREATE INDEX idxhstore ON demo_hstore USING gist (meta);
SELECT * FROM demo_hstore WHERE meta@>'river=>t';

id | meta
----+--------------
15 | "river"=>"t"

13.4.6 MAIS ENCORE...

• uuid-ossp : gérer des UUID


• unaccent : supprime des accents
• pg_anonymizer : masquer des données sensibles

595
https://dalibo.com/formations
SQL pour PostgreSQL

13.4.7 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 !

Les extensions permettent de diffuser des bibliothèques de fonction pour la compatibilité


avec du code d’autres produits : orafce est un exemple bien connu.

Pour éviter de maintenir un fork complet de PostgreSQL, certains éditeurs offrent leur
produit sous forme d’extension, souvent avec une version communautaire intégrant les
principales fonctionnalités. Par exemple :

• Citus permet du sharding ;


• TimescaleDB gère les séries temporelles.

Face à des extensions extérieures, on gardera à l’esprit qu’il s’agit d’un produit supplémen-
taire à maîtriser et administrer, et l’on cherchera d’abord à tirer le maximum du PostgreSQL
communautaire.

13.5 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.

SQL et PL/pgSQL ne sont pas les seuls langages utilisables au niveau d’un serveur Post-
greSQL. PL/pgSQL est installé par défaut en tant qu’extension. Il est possible de rajouter
les langages python, perl, R, etc. et de coder des fonctions dans ces langages. Ces lan-
gages ne sont pas fournis par l’installation standard de PostgreSQL. Une installation via
les paquets du système d’exploitation est sans doute le plus simple.

596
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

13.6 ACCÈS DISTANTS

Accès à des bases distantes


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

Les accès distants à d’autres bases de données sont généralement disponibles par des
extensions. L’extension dblink permet d’accéder à une autre instance PostgreSQL mais
elle est ancienne, et l’on préférera le foreign data wrapper postgresql_fdw, disponible dans
les contribs. D’autres FDW sont des projets extérieurs : ora_fdw, mysql_fdw, etc.

Une solution de sharding n’est pas encore intégrée à PostgreSQL mais des outils existent :
PL/Proxy fournit des fonctions pour répartir des accès mais implique de refondre le code.
Citus est une extension plus récente et plus transparente.

13.7 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
• Nombreux projets externes

Tous ces modules permettent de manipuler une facette de PostgreSQL à laquelle on n’a
normalement pas accès. Leur utilisation est parfois très spécialisée et pointue.

En plus des contribs listés ci-dessus, de nombreux projets externes existent : toastinfo,
pg_stat_kcache, pg_qualstats, PoWa, pg_wait_sampling, hypopg...
597
https://dalibo.com/formations
SQL pour PostgreSQL

13.8 PGXN

PostgreSQL eXtension Network :


• Site web : pgxn.orga
– 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

Le site PGXN fournit une vitrine à de nombreux projets gravitant autour de PostgreSQL.

PGXN a de nombreux avantages, dont celui de demander aux projets participants de re-
specter un certain cahier des charges permettant l’installation automatisée des modules
hébergés. Ceci peut par exemple être réalisé avec le client pgxn fourni :

> pgxn search --dist fdw


multicdr_fdw 1.2.2
MultiCDR *FDW* =================== Foreign Data Wrapper for representing
CDR files stream as an external SQL table. CDR files from a directory
can be read into a table with a specified field-to-column...

redis_fdw 1.0.0
Redis *FDW* for PostgreSQL 9.1+ ============================== This
PostgreSQL extension implements a Foreign Data Wrapper (*FDW*) for the
Redis key/value database: http://redis.io/ This code is...

jdbc_fdw 1.0.0
Also,since the JVM being used in jdbc *fdw* is created only once for the
entire psql session,therefore,the first query issued that uses jdbc
*fdw* shall set the value of maximum heap size of the JVM(if...

mysql_fdw 2.1.2
... This PostgreSQL extension implements a Foreign Data Wrapper (*FDW*)
for [MySQL][1]. Please note that this version of mysql_fdw only works
a
https://pgxn.org/

598
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

with PostgreSQL Version 9.3 and greater, for previous version...

www_fdw 0.1.8
... library contains a PostgreSQL extension, a Foreign Data Wrapper
(*FDW*) handler of PostgreSQL which provides easy way for interacting
with different web-services.

mongo_fdw 2.0.0
MongoDB *FDW* for PostgreSQL 9.2 ============================== This
PostgreSQL extension implements a Foreign Data Wrapper (*FDW*) for
MongoDB.

firebird_fdw 0.1.0
... -
http://www.postgresql.org/docs/current/interactive/postgres-*fdw*.html *
Other FDWs - https://wiki.postgresql.org/wiki/*Fdw* -
http://pgxn.org/tag/*fdw*/

json_fdw 1.0.0
... This PostgreSQL extension implements a Foreign Data Wrapper (*FDW*)
for JSON files. The extension doesn't require any data to be loaded into
the database, and supports analytic queries against array...

postgres_fdw 1.0.0
This port provides a read-only Postgres *FDW* to PostgreSQL servers in
the 9.2 series. It is a port of the official postgres_fdw contrib module
available in PostgreSQL version 9.3 and later.

osm_fdw 3.0.0
... "Openstreetmap pbf foreign data wrapper") (*FDW*) for reading
[Openstreetmap PBF](http://wiki.openstreetmap.org/wiki/PBF_Format
"Openstreetmap PBF") file format (*.osm.pbf) ## Requirements *...

odbc_fdw 0.1.0
ODBC *FDW* (beta) for PostgreSQL 9.1+
=================================== This PostgreSQL extension implements
a Foreign Data Wrapper (*FDW*) for remote databases using Open Database
Connectivity(ODBC)...

599
https://dalibo.com/formations
SQL pour PostgreSQL

couchdb_fdw 0.1.0
CouchDB *FDW* (beta) for PostgreSQL 9.1+
====================================== This PostgreSQL extension
implements a Foreign Data Wrapper (*FDW*) for the CouchDB document-
oriented database...

treasuredata_fdw 1.2.14
## INSERT INTO statement This *FDW* supports `INSERT INTO` statement.
With `atomic_import` is `false`, the *FDW* imports INSERTed rows as
follows.

twitter_fdw 1.1.1
Installation ------------ $ make && make install $ psql -c "CREATE
EXTENSION twitter_fdw" db The CREATE EXTENSION statement creates not
only *FDW* handlers but also Data Wrapper, Foreign Server, User...

ldap_fdw 0.1.1
... is an initial working on a PostgreSQL's Foreign Data Wrapper (*FDW*)
to query LDAP servers. By all means use it, but do so entirely at your
own risk! You have been warned! Do you like to use it in...

git_fdw 1.0.2
# PostgreSQL Git Foreign Data Wrapper [![Build Status](https://travis-
ci.org/franckverrot/git_fdw.svg?branch=master)](https://travis-
ci.org/franckverrot/git_fdw) git\_fdw is a Git Foreign Data...

oracle_fdw 2.0.0
Foreign Data Wrapper for Oracle ===============================
oracle_fdw is a PostgreSQL extension that provides a Foreign Data
Wrapper for easy and efficient access to Oracle databases, including...

foreign_table_exposer 1.0.0
# foreign_table_exposer This PostgreSQL extension exposes foreign tables
like a normal table with rewriting Query tree. Some BI tools can't
detect foreign tables since they don't consider them when...

cstore_fdw 1.6.0
cstore_fdw ========== [![Build Status](https://travis-
ci.org/citusdata/cstore_fdw.svg?branch=master)][status] [![Coverage](htt

600
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

p://img.shields.io/coveralls/citusdata/cstore_fdw/master.svg)][coverage]
...

multicorn 1.3.5
[![PGXN version](https://badge.fury.io/pg/multicorn.svg)](https://badge.
fury.io/pg/multicorn) [![Build
Status](https://jenkins.dalibo.info/buildStatus/public/Multicorn)]()
Multicorn =========...

tds_fdw 1.0.7
# TDS Foreign data wrapper * **Author:** Geoff Montee * **Name:**
tds_fdw * **File:** tds_fdw/README.md ## About This is a [PostgreSQL
foreign data...

pmpp 1.2.3
... Having foreign server definitions and user mappings makes for
cleaner function invocations.

file_textarray_fdw 1.0.1
### File Text Array Foreign Data Wrapper for PostgreSQL This *FDW* is
similar to the provided file_fdw, except that instead of the foreign
table having named fields to match the fields in the data...

floatfile 1.3.0
Also I'd need to compare the performance of this vs an *FDW*. If I do
switch to an *FDW*, I'll probably use [Andrew Dunstan's
`file_text_array_fdw`](https://github.com/adunstan/file_text_array_fdw)
as a...

pg_pathman 1.4.13
... event handling; * Non-blocking concurrent table partitioning; *
*FDW* support (foreign partitions); * Various GUC toggles and
configurable settings.

Pour peu que le Instant Client d’Oracle soit installé, on peut par exemple lancer :

> pgxn install oracle_fdw


INFO: best version: oracle_fdw 1.1.0
INFO: saving /tmp/tmpihaor2is/oracle_fdw-1.1.0.zip
INFO: unpacking: /tmp/tmpihaor2is/oracle_fdw-1.1.0.zip
601
https://dalibo.com/formations
SQL pour PostgreSQL

INFO: building extension


gcc -O3 -O0 -Wall -Wmissing-prototypes -Wpointer-arith [...]
[...]
INFO: installing extension
/usr/bin/mkdir -p '/opt/postgres/lib'
/usr/bin/mkdir -p '/opt/postgres/share/extension'
/usr/bin/mkdir -p '/opt/postgres/share/extension'
/usr/bin/mkdir -p '/opt/postgres/share/doc/extension'
/usr/bin/install -c -m 755 oracle_fdw.so '/opt/postgres/lib/oracle_fdw.so'
/usr/bin/install -c -m 644 oracle_fdw.control '/opt/postgres/share/extension/'
/usr/bin/install -c -m 644 oracle_fdw--1.1.sql\oracle_fdw--1.0--1.1.sql
'/opt/postgres/share/extension/'
/usr/bin/install -c -m 644 README.oracle_fdw \
'/opt/postgres/share/doc/extension/'
Attention : le fait qu’un projet soit hébergé sur PGXN n’est absolument pas une vali-
dation de la part du projet PostgreSQL. De nombreux projets hébergés sur PGXN sont
encore en phase de développement, voire abandonnés. Il faut avoir le même recul que
pour n’importe quel autre brique libre.

13.9 CONCLUSION

• Un nombre toujours plus important d’extension permettant d’étendre les possibil-


ités de PostgreSQL
• Certains modules de contribs sont inclus dans le cœur de PostgreSQL lorsqu’ils
sont considérés comme matures et utiles au moteur.
• Un site central pour les extensions PGXN.org, mais toutes n’y sont pas
référencées.

Cette possibilité d’étendre les fonctionnalités de PostgreSQL est vraiment un atout ma-
jeur du projet PostgreSQL. Cela permet de tester des fonctionnalités sans avoir à toucher
au moteur de PostgreSQL et risquer des états instables. Une fois l’extension mature, elle
peut être intégrée directement dans le code de PostgreSQL si elle est considérée utile au
moteur.

602
13. EXTENSIONS POSTGRESQL POUR L’UTILISATEUR

13.9.1 QUESTIONS

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

603
https://dalibo.com/formations
SQL pour PostgreSQL

14 PARTITIONNEMENT

• Ses principes et intérêts


• Historique
• Les différents types

14.1 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
– purge par partitions entières

Maintenir de très grosses tables peut devenir fastidieux voire impossible : VACUUM FULL
trop long, voire impossibles faute d’espace disque suffisant, autovacuum pas assez réac-
tif, réindexation interminable… Il est aussi aberrant de conserver beaucoup de données
d’archives dans des tables lourdement sollicitées pour les données récentes.

Le partitionnement consiste à séparer une même table en plusieurs sous-tables (parti-


tions) manipulables en tant que tables à part entières.

Maintenance

La maintenance s’effectue sur les partitions plutôt que sur l’ensemble complet des don-
nées. En particulier, un VACUUM FULL ou une réindexation peuvent s’effectuer partition
par partition, ce qui permet de limiter interruptions en production, et lisser la charge.
pg_dump ne sait pas paralléliser la sauvegarde d’une table volumineuse et non partition-
née, mais parallélise celle de différentes partitions d’une même table.

C’est aussi un moyen de déplacer une partie des données dans un autre tablespace pour
des raisons de place, ou pour déporter les parties les moins utilisées de la table vers des
disques plus lents et moins chers.

Parcours complet de partitions

Certaines requêtes (notamment décisionnelles) ramènent tant de lignes ou ont des


critères si complexes qu’un parcours complet de la table est souvent privilégié par
l’optimiseur.

Un partitionnement, souvent par date, permet de ne parcourir qu’une ou quelques parti-


tions au lieu de l’ensemble des données. C’est le rôle de l’optimiseur de choisir la partition

604
14. PARTITIONNEMENT

(partition pruning), par exemple celle de l’année en cours, ou des mois sélectionnés.

Suppression des partitions

La suppression de données parmi un gros volume peut poser des problèmes d’accès con-
currents ou de performance, par exemple dans le cas de purges.

En configurant judicieusement les partitions, on peut résoudre cette problématique en


supprimant une partition (DROP TABLE nompartition ;), ou en la détachant (ALTER
TABLE table_partitionnee DETACH PARTITION nompartition ;) pour l’archiver (et
la réattacher au besoin) ou la supprimer ultérieurement.

La difficulté d’un système de partitionnement consiste à partitionner avec un impact min-


imal sur la maintenance du code par rapport à une table classique.

14.2 PARTITIONNEMENT APPLICATIF

• Intégralement géré au niveau applicatif


• Complexité pour le développeur
• Intégrité des liens entre les données ?
• Réinvention de la roue

L’application peut gérer le partitionnement elle-même, par exemple en créant des tables
numérotées par mois, année… Le moteur de PostgreSQL ne voit que des tables classiques
et ne peut assurer l’intégrité entre ces données.

C’est au développeur de réinventer la roue : choix de la table, gestion des index… La


lecture des données qui concerne plusieurs tables peut devenir délicat.

Ce modèle extrêmement fréquent est bien sûr à éviter.

14.3 HISTORIQUE DU PARTITIONNEMENT SUR POSTGRESQL

• Partitionnement par héritage (historique, < v10)


• Partitionnement déclaratif (>=v10, préférer v12)

Un partitionnement entièrement géré par le moteur, n’existe réellement que depuis la


version 10 de PostgreSQL. Il a été grandement amélioré en versions 11 et 12, en fonc-
tionnalités comme en performances.
605
https://dalibo.com/formations
SQL pour PostgreSQL

Jusqu’en 9.6, n’existaient que le partitionnement dit par héritage, nettement moins flexi-
ble, et celui géré intégralement par l’applicatif.

14.4 PARTITIONNEMENT PAR HÉRITAGE

• Table principale :
– table mère définie normalement
• Tables filles :
– CREATE TABLE primates(debout boolean) INHERITS (mammiferes) ;
– héritent des propriétés de la table mère
– mais pas des contraintes, index et droits
• Insertion applicative ou par trigger (lent !)

PostgreSQL permet de créer des tables qui héritent les unes des autres.

L’héritage d’une table mère transmet les propriétés suivantes à la table fille :

• les colonnes, avec le type et les valeurs par défaut ;


• les contraintes CHECK.

Les tables filles peuvent ajouter leurs propres colonnes. Par exemple :
CREATE TABLE animaux (nom text PRIMARY KEY);

INSERT INTO animaux VALUES ('Éponge');


INSERT INTO animaux VALUES ('Ver de terre');

CREATE TABLE cephalopodes (nb_tentacules integer)


INHERITS (animaux);

INSERT INTO cephalopodes VALUES ('Poulpe', 8);

CREATE TABLE vertebres (


nb_membres integer
)
INHERITS (animaux);

CREATE TABLE tetrapodes () INHERITS (vertebres);

ALTER TABLE ONLY tetrapodes


ALTER COLUMN nb_membres
SET DEFAULT 4;

606
14. PARTITIONNEMENT

CREATE TABLE poissons (eau_douce boolean)


INHERITS (tetrapodes);

INSERT INTO poissons (nom, eau_douce) VALUES ('Requin', false);


INSERT INTO poissons (nom, nb_membres, eau_douce) VALUES ('Anguille', 0, false);

La table poissons possède les champs des tables dont elles héritent :

\d+ poissons

Table "public.poissons"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
------------+---------+-----------+----------+---------+----------+--------------+-------------
nom | text | | not null | | extended | |
nb_membres | integer | | | 4 | plain | |
eau_douce | boolean | | | | plain | |
Inherits: tetrapodes
Access method: heap

On peut créer toute une hiérarchie avec des branches parallèles, chacune avec ses
colonnes propres :

CREATE TABLE reptiles (venimeux boolean)


INHERITS (tetrapodes);

INSERT INTO reptiles VALUES ('Crocodile', 4, false);


INSERT INTO reptiles VALUES ('Cobra', 0, true);

CREATE TABLE mammiferes () INHERITS (tetrapodes);

CREATE TABLE cetartiodactyles (


cornes boolean,
bosse boolean
)
INHERITS (mammiferes);

INSERT INTO cetartiodactyles VALUES ('Girafe', 4, true, false);


INSERT INTO cetartiodactyles VALUES ('Chameau', 4, false, true);

CREATE TABLE primates (debout boolean)


INHERITS (mammiferes);

INSERT INTO primates (nom, debout) VALUES ('Chimpanzé', false);


INSERT INTO primates (nom, debout) VALUES ('Homme', true);

\d+ primates

Table "public.primates"
607
https://dalibo.com/formations
SQL pour PostgreSQL

Column | Type | Collation | Nullable | Default | Storage | Stats target | Description


------------+---------+-----------+----------+---------+----------+--------------+-------------
nom | text | | not null | | extended | |
nb_membres | integer | | | 4 | plain | |
debout | boolean | | | | plain | |
Inherits: mammiferes
Access method: heap

On remarquera que la clé primaire manque. En effet, l’héritage ne transmet pas :

• les contraintes d’unicité et référentielles ;


• les index ;
• les droits.

Chaque table possède ses propres lignes :

SELECT * FROM poissons ;

nom | nb_membres | eau_douce


----------+------------+-----------
Requin | 4 | f
Anguille | 0 | f
(2 lignes)

Par défaut une table affiche aussi le contenu de ses tables filles et les colonnes com-
munes :

SELECT * FROM animaux ORDER BY 1 ;

nom
--------------
Anguille
Chameau
Chimpanzé
Cobra
Crocodile
Éponge
Girafe
Homme
Poulpe
Requin
Ver de terre
(11 lignes)

SELECT * FROM tetrapodes ORDER BY 1 ;

nom | nb_membres
-----------+------------

608
14. PARTITIONNEMENT

Anguille | 0
Chameau | 4
Chimpanzé | 4
Cobra | 0
Crocodile | 4
Girafe | 4
Homme | 4
Requin | 4
(8 lignes)

EXPLAIN SELECT * FROM tetrapodes ORDER BY 1 ;

QUERY PLAN
---------------------------------------------------------------------------------
Sort (cost=420.67..433.12 rows=4982 width=36)
Sort Key: tetrapodes.nom
-> Append (cost=0.00..114.71 rows=4982 width=36)
-> Seq Scan on tetrapodes (cost=0.00..0.00 rows=1 width=36)
-> Seq Scan on poissons (cost=0.00..22.50 rows=1250 width=36)
-> Seq Scan on reptiles (cost=0.00..22.50 rows=1250 width=36)
-> Seq Scan on mammiferes (cost=0.00..0.00 rows=1 width=36)
-> Seq Scan on cetartiodactyles (cost=0.00..22.30 rows=1230 width=36)
-> Seq Scan on primates (cost=0.00..22.50 rows=1250 width=36)

Pour ne consulter que le contenu de la table sans ses filles :


SELECT * FROM ONLY animaux ;
nom
--------------
Éponge
Ver de terre
(2 lignes)

En conséquence, on a bien affaire à des tables indépendantes. Rien n’empêche d’avoir


des doublons entre la table mère et la table fille. Cela empêche aussi bien sûr la mise en
place de clé étrangère, puisqu’une clé étrangère s’appuie sur une contrainte d’unicité de
la table référencée. Lors d’une insertion, voire d’une mise à jour, le choix de la table cible
se fait par l’application ou un trigger sur la table mère.

Il faut être vigilant à bien recréer les contraintes et index manquants ainsi qu’à attribuer
les droits sur les objets de manière adéquate. L’une des erreurs les plus fréquentes est
d’oublier de créer les contraintes, index et droits qui n’ont pas été transmis.

Ce type de partitionnement est un héritage des débuts de PostgreSQL, à l’époque de la


mode des « bases de donnée objet ». Dans la pratique, dans les versions antérieures à
la version 10, l’héritage était utilisé pour mettre en place le partitionnement. La mainte-
609
https://dalibo.com/formations
SQL pour PostgreSQL

nance des index, des contraintes et la nécessité d’un trigger pour aiguiller les insertions
vers la bonne table fille, ne facilitaient pas la maintenance. Les performances en écritures
étaient bien en-deçà des tables classiques ou du nouveau partitionnement déclaratif (voir
comparaison plus bas). S’il fonctionne toujours sur les versions récentes de PostgreSQL,
il est déconseillé pour les nouveaux développements.

14.5 PARTITIONNEMENT DÉCLARATIF

• À partir de la version 10
• Mise en place et administration simplifiées car intégrées au moteur
• Gestion automatique des lectures et écritures
• Partitions
– attacher/détacher une partition
– contrainte implicite de partitionnement
– expression possible pour la clé de partitionnement
– sous-partitions possibles
– partition par défaut

La version 10 apporte un nouveau système de partitionnement se basant sur une infras-


tructure qui existait déjà dans PostgreSQL. C’est le système à privilégier de nos jours.

Le but est de simplifier la mise en place et l’administration des tables partitionnées. Des
clauses spécialisées ont été ajoutées aux ordres SQL déjà existants, comme CREATE
TABLE et ALTER TABLE, pour attacher (ATTACH PARTITION) et détacher des partitions
(DETACH PARTITION).

Au niveau de la simplification de la mise en place par rapport à l’ancien partitionnement


par héritage, on peut noter qu’il n’est plus nécessaire de créer une fonction trigger et
d’ajouter des triggers pour gérer les insertions et mises à jour. Le routage est géré de
façon automatique en fonction de la définition des partitions. Si les données insérées ne
trouvent pas de partition cible, l’insertion est tout simplement en erreur (sauf si une parti-
tion par défaut a été définie, à partir de la version 11). Du fait de ce routage automatique,
les insertions se révèlent aussi plus rapides.

610
14. PARTITIONNEMENT

14.5.1 PARTITIONNEMENT PAR LISTE

• Liste de valeurs par partition


• Créer une table partitionnée :
CREATE TABLE t1(c1 integer, c2 text) PARTITION BY LIST (c1);
• Ajouter une partition :
CREATE TABLE t1_a PARTITION OF t1 FOR VALUES IN (1, 2, 3);
• Attacher la partition :
ALTER TABLE t1 ATTACH PARTITION t1_a FOR VALUES IN (1, 2, 3);
• Détacher la partition :
ALTER TABLE t1 DETACH PARTITION t1_a;

Exemple complet :

Création de la table principale et des partitions :

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


CREATE TABLE

postgres=# CREATE TABLE t1_a PARTITION OF t1 FOR VALUES IN (1, 2, 3);


CREATE TABLE

postgres=# CREATE TABLE t1_b PARTITION OF t1 FOR VALUES IN (4, 5);


CREATE TABLE

Insertion de données :

postgres=# INSERT INTO t1 VALUES (0);


ERROR: no PARTITION OF relation "t1" found for row
DETAIL: Partition key of the failing row contains (c1) = (0).

postgres=# INSERT INTO t1 VALUES (1);


INSERT 0 1

postgres=# INSERT INTO t1 VALUES (2);


INSERT 0 1

postgres=# INSERT INTO t1 VALUES (5);


INSERT 0 1

postgres=# INSERT INTO t1 VALUES (6);


ERROR: no PARTITION OF relation "t1" found for row
DETAIL: Partition key of the failing row contains (c1) = (6).

Lors de l’insertion, les données sont correctement redirigées vers leur partition.

Si aucune partition correspondant à la clé insérée n’est trouvée et qu’aucune partition par
611
https://dalibo.com/formations
SQL pour PostgreSQL

défaut n’est déclarée (version 11), une erreur se produit.

14.5.2 PARTITIONNEMENT PAR INTERVALLE

• Intervalle de valeurs par partition


• Créer une table partitionnée :
CREATE TABLE t2(c1 integer, c2 text) PARTITION BY RANGE (c1);
• Ajouter une partition :
CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES FROM (1) TO (100);
• Détacher une partition :
ALTER TABLE t2 DETACH PARTITION t2_1;

Exemple complet :

Création de la table principale et d’une partition :


postgres=# CREATE TABLE t2(c1 integer, c2 text) PARTITION BY RANGE (c1);
CREATE TABLE

postgres=# CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES FROM (1) to (100);
CREATE TABLE

Insertion de données :
postgres=# INSERT INTO t2 VALUES (0);
ERROR: no PARTITION OF relation "t2" found for row
DETAIL: Partition key of the failing row contains (c1) = (0).

postgres=# INSERT INTO t2 VALUES (1);


INSERT 0 1

postgres=# INSERT INTO t2 VALUES (2);


INSERT 0 1

postgres=# INSERT INTO t2 VALUES (5);


INSERT 0 1

postgres=# INSERT INTO t2 VALUES (101);


ERROR: no PARTITION OF relation "t2" found for row
DETAIL: Partition key of the failing row contains (c1) = (101).

Lors de l’insertion, les données sont correctement redirigées vers leur partition.

Si aucune partition correspondant à la clé insérée n’est trouvée et qu’aucune partition par
défaut n’est déclarée, une erreur se produit.

612
14. PARTITIONNEMENT

14.5.3 PARTITIONNEMENT PAR HACHAGE

• À partir de la version 11
• Hachage de valeurs par partition
• Créer une table partitionnée :
CREATE TABLE t3(c1 integer, c2 text) PARTITION BY HASH (c1);
• Ajouter les partitions :
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);

Exemple complet :

Création de la table principale et d’une partition :


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

postgres=# CREATE TABLE t3_1 PARTITION OF t3 FOR VALUES WITH (modulus 3, remainder 0);
CREATE TABLE

postgres=# CREATE TABLE t3_2 PARTITION OF t3 FOR VALUES WITH (modulus 3, remainder 1);
CREATE TABLE

postgres=# CREATE TABLE t3_3 PARTITION OF t3 FOR VALUES WITH (modulus 3, remainder 2);
CREATE TABLE

Insertion de données :
postgres=# INSERT INTO t3 SELECT generate_series(1, 1000000);
INSERT 0 1000000
postgres=# SELECT relname, count(*)
FROM t3 JOIN pg_class ON t3.tableoid=pg_class.oid
GROUP BY 1;

relname | count
---------+--------
t3_1 | 333263
t3_2 | 333497
t3_3 | 333240
(3 rows)
613
https://dalibo.com/formations
SQL pour PostgreSQL

Lors de l’insertion, les données sont correctement redirigées vers leur partition.

14.5.4 CLÉ DE PARTITIONNEMENT MULTI-COLONNES

• Clé sur plusieurs colonnes acceptée


– si partitionnement par intervalle ou hash, pas pour liste
• Créer une table partitionnée avec une clé multi-colonnes :
CREATE TABLE t1(c1 integer, c2 text, c3 date)
PARTITION BY RANGE (c1, c3);
• Ajouter une partition :
CREATE TABLE t1_a PARTITION OF t1
FOR VALUES FROM (1,'2017-08-10') TO (100, '2017-08-11');

Quand on utilise le partitionnement par intervalle, il est possible de créer les partitions en
utilisant plusieurs colonnes.

On profitera de l’exemple ci-dessous pour montrer l’utilisation conjointe de tablespaces


différents.

Commençons par créer les tablespaces :


postgres=# CREATE TABLESPACE ts0 LOCATION '/tablespaces/ts0';
CREATE TABLESPACE

postgres=# CREATE TABLESPACE ts1 LOCATION '/tablespaces/ts1';


CREATE TABLESPACE

postgres=# CREATE TABLESPACE ts2 LOCATION '/tablespaces/ts2';


CREATE TABLESPACE

postgres=# CREATE TABLESPACE ts3 LOCATION '/tablespaces/ts3';


CREATE TABLESPACE

Créons maintenant la table partitionnée et deux partitions :


postgres=# CREATE TABLE t2(c1 integer, c2 text, c3 date not null)
PARTITION BY RANGE (c1, c3);
CREATE TABLE

postgres=# CREATE TABLE t2_1 PARTITION OF t2


FOR VALUES FROM (1,'2017-08-10') TO (100, '2017-08-11')
TABLESPACE ts1;
CREATE TABLE

postgres=# CREATE TABLE t2_2 PARTITION OF t2

614
14. PARTITIONNEMENT

FOR VALUES FROM (100,'2017-08-11') TO (200, '2017-08-12')


TABLESPACE ts2;
CREATE TABLE

Si les valeurs sont bien comprises dans les bornes :


postgres=# INSERT INTO t2 VALUES (1, 'test', '2017-08-10');
INSERT 0 1

postgres=# INSERT INTO t2 VALUES (150, 'test2', '2017-08-11');


INSERT 0 1

Si la valeur pour c1 est trop petite :


postgres=# INSERT INTO t2 VALUES (0, 'test', '2017-08-10');
ERROR: no partition of relation "t2" found for row
DÉTAIL : Partition key of the failing row contains (c1, c3) = (0, 2017-08-10).

Si la valeur pour c3 (colonne de type date) est antérieure :


postgres=# INSERT INTO t2 VALUES (1, 'test', '2017-08-09');
ERROR: no partition of relation "t2" found for row
DÉTAIL : Partition key of the failing row contains (c1, c3) = (1, 2017-08-09).

Les valeurs spéciales MINVALUE et MAXVALUE permettent de ne pas indiquer de valeur


de seuil limite. Les partitions t2_0 et t2_3 pourront par exemple être déclarées comme
suit et permettront d’insérer les lignes qui étaient ci-dessus en erreur.
postgres=# CREATE TABLE t2_0 PARTITION OF t2
FOR VALUES FROM (MINVALUE, MINVALUE) TO (1,'2017-08-10')
TABLESPACE ts0;

postgres=# CREATE TABLE t2_3 PARTITION OF t2


FOR VALUES FROM (200,'2017-08-12') TO (MAXVALUE, MAXVALUE)
TABLESPACE ts3;

Enfin, on peut consulter la table pg_class afin de vérifier la présence des différentes
partitions :
postgres=# ANALYZE t2;
ANALYZE

postgres=# SELECT relname,relispartition,relkind,reltuples


FROM pg_class WHERE relname LIKE 't2%';
relname | relispartition | relkind | reltuples
---------+----------------+---------+-----------
t2 | f | p | 0
t2_0 | t | r | 2
t2_1 | t | r | 1
t2_2 | t | r | 1
615
https://dalibo.com/formations
SQL pour PostgreSQL

t2_3 | t | r | 0
(5 lignes)

14.5.5 PERFORMANCES EN INSERTION

t1 (non partitionnée) :
INSERT INTO t1 select i, 'toto'
FROM generate_series(0, 9999999) i;
Time: 10097.098 ms (00:10.097)
t2 (nouveau partitionnement) :
INSERT INTO t2 select i, 'toto'
FROM generate_series(0, 9999999) i;
Time: 11448.867 ms (00:11.449)
t3 (ancien partitionnement par héritage) :
INSERT INTO t3 select i, 'toto'
FROM generate_series(0, 9999999) i;
Time: 125351.918 ms (02:05.352)

La table t1 est une table non partitionnée. Elle a été créée comme suit :
CREATE TABLE t1 (c1 integer, c2 text);

La table t2 est une table partitionnée utilisant les nouvelles fonctionnalités de la version
10 de PostgreSQL :
CREATE TABLE t2 (c1 integer, c2 text) PARTITION BY RANGE (c1);
CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES FROM ( 0) TO ( 1000000);
CREATE TABLE t2_2 PARTITION OF t2 FOR VALUES FROM (1000000) TO ( 2000000);
CREATE TABLE t2_3 PARTITION OF t2 FOR VALUES FROM (2000000) TO ( 3000000);
CREATE TABLE t2_4 PARTITION OF t2 FOR VALUES FROM (3000000) TO ( 4000000);
CREATE TABLE t2_5 PARTITION OF t2 FOR VALUES FROM (4000000) TO ( 5000000);
CREATE TABLE t2_6 PARTITION OF t2 FOR VALUES FROM (5000000) TO ( 6000000);
CREATE TABLE t2_7 PARTITION OF t2 FOR VALUES FROM (6000000) TO ( 7000000);
CREATE TABLE t2_8 PARTITION OF t2 FOR VALUES FROM (7000000) TO ( 8000000);
CREATE TABLE t2_9 PARTITION OF t2 FOR VALUES FROM (8000000) TO ( 9000000);
CREATE TABLE t2_0 PARTITION OF t2 FOR VALUES FROM (9000000) TO (10000000);

Enfin, la table t3 est une table utilisant l’ancienne méthode de partitionnement :


CREATE TABLE t3 (c1 integer, c2 text);
CREATE TABLE t3_1 (CHECK (c1 BETWEEN 0 AND 1000000)) INHERITS (t3);
CREATE TABLE t3_2 (CHECK (c1 BETWEEN 1000000 AND 2000000)) INHERITS (t3);
CREATE TABLE t3_3 (CHECK (c1 BETWEEN 2000000 AND 3000000)) INHERITS (t3);
CREATE TABLE t3_4 (CHECK (c1 BETWEEN 3000000 AND 4000000)) INHERITS (t3);

616
14. PARTITIONNEMENT

CREATE TABLE t3_5 (CHECK (c1 BETWEEN 4000000 AND 5000000)) INHERITS (t3);
CREATE TABLE t3_6 (CHECK (c1 BETWEEN 5000000 AND 6000000)) INHERITS (t3);
CREATE TABLE t3_7 (CHECK (c1 BETWEEN 6000000 AND 7000000)) INHERITS (t3);
CREATE TABLE t3_8 (CHECK (c1 BETWEEN 7000000 AND 8000000)) INHERITS (t3);
CREATE TABLE t3_9 (CHECK (c1 BETWEEN 8000000 AND 9000000)) INHERITS (t3);
CREATE TABLE t3_0 (CHECK (c1 BETWEEN 9000000 AND 10000000)) INHERITS (t3);

CREATE OR REPLACE FUNCTION insert_into() RETURNS TRIGGER


LANGUAGE plpgsql
AS $FUNC$
BEGIN
IF NEW.c1 BETWEEN 0 AND 1000000 THEN
INSERT INTO t3_1 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 1000000 AND 2000000 THEN
INSERT INTO t3_2 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 2000000 AND 3000000 THEN
INSERT INTO t3_3 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 3000000 AND 4000000 THEN
INSERT INTO t3_4 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 4000000 AND 5000000 THEN
INSERT INTO t3_5 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 5000000 AND 6000000 THEN
INSERT INTO t3_6 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 6000000 AND 7000000 THEN
INSERT INTO t3_7 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 7000000 AND 8000000 THEN
INSERT INTO t3_8 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 8000000 AND 9000000 THEN
INSERT INTO t3_9 VALUES (NEW.*);
ELSIF NEW.c1 BETWEEN 9000000 AND 10000000 THEN
INSERT INTO t3_0 VALUES (NEW.*);
END IF;
RETURN NULL;
END;
$FUNC$;

CREATE TRIGGER tr_insert_t3 BEFORE INSERT ON t3 FOR EACH ROW EXECUTE PROCEDURE insert_into();

617
https://dalibo.com/formations
SQL pour PostgreSQL

14.5.6 FONCTIONS DE GESTION

• Disponibles à partir de la v12


• pg_partition_root() : récupérer la racine d’une partition
• pg_partition_ancestors() : parents d’une partition
• pg_partition_tree() : liste entière des partitions
• \dP

Voici le jeu de tests pour l’exemple qui suivra :


CREATE TABLE logs (dreception timestamptz, contenu text)
PARTITION BY RANGE(dreception);
CREATE TABLE logs_2018 PARTITION OF logs
FOR VALUES FROM ('2018-01-01') TO ('2018-12-31')
PARTITION BY range(dreception);
CREATE TABLE logs_201801 PARTITION OF logs_2018
FOR VALUES FROM ('2018-01-01') TO ('2018-01-31');
CREATE TABLE logs_201802 PARTITION OF logs_2018
FOR VALUES FROM ('2018-02-01') TO ('2018-02-28');
CREATE TABLE logs_2019 PARTITION OF logs
FOR VALUES FROM ('2019-01-01') TO ('2019-12-31')
PARTITION BY range(dreception);
CREATE TABLE logs_201901 PARTITION OF logs_2019
FOR VALUES FROM ('2019-01-01') TO ('2019-01-31');

Et voici le test des différentes fonctions :

postgres=# SELECT pg_partition_root('logs_2019');

pg_partition_root
-------------------
logs
(1 row)

postgres=# SELECT pg_partition_root('logs_201901');

pg_partition_root
-------------------
logs
(1 row)

postgres=# SELECT pg_partition_ancestors('logs_2018');

pg_partition_ancestors

618
14. PARTITIONNEMENT

------------------------
logs_2018
logs
(2 rows)

postgres=# SELECT pg_partition_ancestors('logs_201901');

pg_partition_ancestors
------------------------
logs_201901
logs_2019
logs
(3 rows)

postgres=# SELECT * FROM pg_partition_tree('logs');

relid | parentrelid | isleaf | level


-------------+-------------+--------+-------
logs | | f | 0
logs_2018 | logs | f | 1
logs_2019 | logs | f | 1
logs_201801 | logs_2018 | t | 2
logs_201802 | logs_2018 | t | 2
logs_201901 | logs_2019 | t | 2
(6 rows)

Sous psql, \d affichera toutes les tables, partitions comprises. À partir de la version 12
du client, \dP affiche uniquement les tables et index partitionnés :

=# \dP
Liste des relations partitionnées
Schéma | Nom | Propriétaire | Type | Table
--------+----------+--------------+--------------------+----------
public | bigtable | postgres | table partitionnée |
public | bigidx | postgres | index partitionné | bigtable

619
https://dalibo.com/formations
SQL pour PostgreSQL

14.5.7 INTÉRÊTS DU PARTITIONNEMENT DÉCLARATIF

• Souple
• Performant
• Intégration au moteur

Par rapport à l’ancien système, le partitionnement déclaratif n’a que des avantages : ra-
pidité d’insertion, souplesse dans le choix du partitionnement, intégration au moteur (ce
qui garantit l’intégrité des données)…

14.5.8 LIMITATIONS DU PARTITIONNEMENT DÉCLARATIF

• Temps de planification ! Max ~ 100 partitions


• Pas d’héritage multiple, schéma fixe
• En cas d’attachement d’une partition :
– vérification du respect de la contrainte (Seq Scan de la table)
– sauf si ajout au préalable d’une contrainte CHECK identique
• Limitations de la v10 :
– Pas de partition par défaut
– Pas de propagation des index
– Pas de PK ou UK...
• Limitations des v10 et v11 :
– Pas de clé étrangère vers une table partitionnée
• Limitations des v10, v11 et v12 :
– Pas de triggers BEFORE UPDATE ... FOR EACH ROW
• Possibilité de contourner en travaillant par partition

Les partitions ont forcément le même schéma de données que leur partition mère.

Il n’y a pas de notion d’héritage multiple.

Toute donnée doit pouvoir être placée dans une partition. Dans le cas contraire, la don-
née ne sera pas placée dans la table mère (contrairement à l’ancien partitionnement par
héritage). À la place, une erreur sera générée :

ERROR: no partition of relation "t2" found for row

Néanmoins, à partir de la version 11, il est possible de définir une partition par défaut. Si
on reprend l’exemple de t1 :
TRUNCATE t1;

620
14. PARTITIONNEMENT

INSERT INTO t1 VALUES (2);

INSERT INTO t1 SELECT generate_series(1, 10);


ERROR: no partition of relation "t1" found for row
DETAIL: Partition key of the failing row contains (c1) = (6).

CREATE TABLE t1_def PARTITION OF t1 DEFAULT;


INSERT INTO t1 SELECT generate_series(1, 10);
SELECT relname, count(*) FROM t1 JOIN pg_class ON t1.tableoid=pg_class.oid GROUP BY 1;

relname | count
---------+-------
t1_b | 2
t1_a | 4
t1_def | 5
(3 rows)

Une limitation sérieuse du partitionnement tient au temps de planification qui augmente


très vite avec le nombre de partitions, même petites. On considère qu’il ne faut pas dé-
passer 100 partitions. Pour contourner cette limite, il reste possible de manipuler directe-
ment les sous-partitions s’il est facile de trouver leur nom.

En version 10, il n’était pas possible d’ajouter un index à la table mère, sous peine de voir
l’erreur suivante apparaître :

ERROR: cannot create index on partitioned table "t1"

Ceci implique qu’il n’est pas possible en version 10 de mettre une clé primaire, et une
contrainte unique sur une table partitionnée. De ce fait, il n’est pas non plus possible d’y
faire pointer une clé étrangère.

À partir de la version 11, les index sont propagés de la table mère aux partitions : tout in-
dex créé sur la table partitionnée sera automatiquement créé sur les partitions existantes.
Toute nouvelle partition disposera des index de la table partitionnée. La suppression d’un
index se fait sur la table partitionnée et concernera toutes les partitions. Il n’est pas pos-
sible de supprimer un index d’une seule partition.

En v11, on peut donc créer une clé primaire ou unique sur une table partitionnée, mais
aussi une clé étrangère d’une table partitionnée vers une table normale. Par contre, il
reste impossible de créer une clé étrangère vers une table partitionnée. Par exemple, si
lignes_commandes et commandes_id sont partitionnées, poser une clé étrangère entre
les deux échouera :

ALTER TABLE lignes_ventes


ADD CONSTRAINT lignes_ventes_ventes_fk
FOREIGN KEY (vente_id) REFERENCES ventes(vente_id) ;
621
https://dalibo.com/formations
SQL pour PostgreSQL

ERROR: cannot reference partitioned table "ventes"

La colonne lignes_ventes.commande_id peut tout de même être indexée en v11. Il


reste possible d’ajouter des contraintes entre les partitions une à une, si les clés de parti-
tionnement sont compatibles.

En cas d’attachement d’une partition avec ATTACH PARTITION, la partition sera complète-
ment parcourue pour vérifier qu’elle correspond bien au critère de partitionnement. Il est
conseillé d’ajouter une contrainte CHECK adéquate pour réduire la durée du verrou lié au
rattachement.

Il est possible d’attacher comme partition une table distante (déclarée avec postgres_fdw
notamment). En v10, cette partition ne sera cependant accessible qu’en lecture si elle est
accédée via la table mère. Pour un accès en écriture, il faudra modifier la table distante
directement, avec un risque d’incohérence si sa contrainte n’est pas la même que celle
déclarée dans son rattachement au partitionnement. Cette restriction est levée en v11 :
le routage des insertions ou des mises à jour se fait bien. Cependant, même en v11,
la propagation d’index ne fonctionne pas sur les tables distantes (elles ne peuvent en
posséder) et on ne peut ajouter de table distante à une table partitionnée avec des index.

Les triggers de lignes ne se propagent pas en v10. En v11, on peut créer des trigger
AFTER UPDATE ... FOR EACH ROW mais les BEFORE UPDATE ... FOR EACH ROW ne peu-
vent toujours pas être créés sur la table mère. Il reste là encore la possibilité de les créer
partition par partition au besoin. À partir de la version 13, les triggers BEFORE UPDATE
... FOR EACH ROW sont possibles complètement, mais il ne permettent pas de modifier
la partition de destination.

Enfin, la version 10 ne permet pas de faire une mise à jour (UPDATE) d’une ligne où la clé
de partitionnement est modifiée de telle façon que la ligne doit changer de partition. Il
faut faire un DELETE et un INSERT à la place. La version 11 gère mieux ce cas en déplaçant
directement la ligne dans la bonne partition.

622
14. PARTITIONNEMENT

14.6 EXTENSIONS & OUTILS

• Outil : pg_partman
• Extensions dédiées à un domaine :
– timescaledb
– citus

Le logiciel pg_partman est un complément aux systèmes de partitionnement de Post-


greSQL. Il est apparu pour automatiser le partitionnement par héritage. Même avec le
partitionnement déclaratif, il permet de simplifier la maintenance du partitionnement
lorsque les tables sont partitionnées sur une échelle temporelle ou de valeurs (partition-
nement par range). Il se présente sous forme d’extension PostgreSQL, son code source
est disponible sur ce dépôt github149 .

timescaledb est une extension spécialisée dans les séries temporelles. Basée sur le par-
titionnement par héritage, elle vaut surtout pour sa technique de compression, ses util-
itaires. La version communautaire sur Github150 ne comprend pas tout ce qu’offre la
version commerciale.

citus est une autre extension commerciale. Le principe est de partitionner agressivement
les tables sur plusieurs instances, et d’utiliser simultanément les processeurs, disques de
toutes ces instances (sharding). Citus gère la distribution des requêtes, mais pas la main-
tenance des instances supplémentaires. Là encore, la version libre151 est utilisable mais
ne permet pas tout ce que peut la version commerciale. L’éditeur Citusdata a été racheté
par Microsoft.

149
https://github.com/pgpartman/pg_partman
150
https://github.com/timescale/timescaledb
151
https://github.com/citusdata/citus

623
https://dalibo.com/formations
SQL pour PostgreSQL

14.7 TRAVAUX PRATIQUES

14.7.1 PARTITIONNEMENT

Nous travaillons sur la base cave. La base cave peut être téléchargée depuis https://dali.
bo/tp_cave (dump de 2,6 Mo, pour 71 Mo sur le disque au final) et importée ainsi :
$ psql -c "CREATE ROLE caviste LOGIN PASSWORD 'caviste'"
$ psql -c "CREATE DATABASE cave OWNER caviste"
$ pg_restore -d cave cave.dump # Une erreur sur un schéma 'public' existant est normale

Nous allons partitionner la table stock sur l’année.

Pour nous simplifier la vie, nous allons limiter le nombre d’années dans stock (cela nous
évitera la création de 50 partitions) :
-- Création de lignes en 2001-2005
INSERT INTO stock SELECT vin_id, contenant_id, 2001 + annee % 5, sum(nombre)
FROM stock GROUP BY vin_id, contenant_id, 2001 + annee % 5;
-- purge des lignes prédédentes
DELETE FROM stock WHERE annee<2001;

Nous n’avons maintenant que des bouteilles des années 2001 à 2005.

Renommez stock en stock_old.


Créer une table partitionnée stock vide, sans index pour le mo-
ment.

Créer les partitions de stock, avec la contrainte d’année :


stock_2001 à stock_2005.

Insérer tous les enregistrements venant de l’ancienne table


stock.

Passer les statistiques pour être sûr des plans à partir de main-
tenant (nous avons modifié beaucoup d’objets).

Vérifier la présence d’enregistrements dans stock_2001 (syntaxe


SELECT ONLY).
Vérifier qu’il n’y en a aucun dans stock.

624
14. PARTITIONNEMENT

Vérifier qu’une requête sur stock sur 2002 ne parcourt qu’une


seule partition.

Remettre en place les index présents dans la table stock origi-


nale.
Il se peut que d’autres index ne servent à rien (ils ne seront dans
ce cas pas présents dans la correction).

Quel est le plan pour la récupération du stock des bouteilles du


vin_id 1725, année 2003 ?

Essayer de changer l’année de ce même enregistrement de stock


(la même que la précédente). Pourquoi cela échoue-t-il ?

Supprimer les enregistrements de 2004 pour vin_id = 1725.


Retenter la mise à jour.

Pour vider complètement le stock de 2001, supprimer la partition


stock_2001.

Tenter d’ajouter au stock une centaine de bouteilles de 2006.


Pourquoi cela échoue-t-il ?

Créer une partition par défaut pour recevoir de tels enreg-


istrements.
Retenter l’ajout.

Tenter de créer la partition pour l’année 2006. Pourquoi cela


échoue-t-il ?

625
https://dalibo.com/formations
SQL pour PostgreSQL

Pour créer la partition sur 2006, au sein d’une seule transaction :


- détacher la partition par défaut ;
- y déplacer les enregistrements mentionnés ;
- ré-attacher la partition par défaut.

626
14. PARTITIONNEMENT

14.8 TRAVAUX PRATIQUES (SOLUTIONS)

14.8.1 PARTITIONNEMENT

Pour nous simplifier la vie, nous allons limiter le nombre d’années dans stock (cela nous
évitera la création de 50 partitions).
INSERT INTO stock
SELECT vin_id, contenant_id, 2001 + annee % 5, sum(nombre)
FROM stock
GROUP BY vin_id, contenant_id, 2001 + annee % 5 ;

DELETE FROM stock WHERE annee<2001 ;

Nous n’avons maintenant que des bouteilles des années 2001 à 2005.

Renommez stock en stock_old.


Créer une table partitionnée stock vide, sans index pour le mo-
ment.

ALTER TABLE stock RENAME TO stock_old;


CREATE TABLE stock(LIKE stock_old) PARTITION BY LIST (annee);

Créer les partitions de stock, avec la contrainte d’année :


stock_2001 à stock_2005.

CREATE TABLE stock_2001 PARTITION of stock FOR VALUES IN (2001) ;


CREATE TABLE stock_2002 PARTITION of stock FOR VALUES IN (2002) ;
CREATE TABLE stock_2003 PARTITION of stock FOR VALUES IN (2003) ;
CREATE TABLE stock_2004 PARTITION of stock FOR VALUES IN (2004) ;
CREATE TABLE stock_2005 PARTITION of stock FOR VALUES IN (2005) ;

Insérer tous les enregistrements venant de l’ancienne table


stock.

INSERT INTO stock SELECT * FROM stock_old;

Passer les statistiques pour être sûr des plans à partir de main-
tenant (nous avons modifié beaucoup d’objets).

ANALYZE;

627
https://dalibo.com/formations
SQL pour PostgreSQL

Vérifier la présence d’enregistrements dans stock_2001 (syntaxe


SELECT ONLY).
Vérifier qu’il n’y en a aucun dans stock.

SELECT count(*) FROM stock_2001;


SELECT count(*) FROM ONLY stock;

Vérifier qu’une requête sur stock sur 2002 ne parcourt qu’une


seule partition.

EXPLAIN ANALYZE SELECT * FROM stock WHERE annee=2002;

QUERY PLAN
------------------------------------------------------------------------------
Append (cost=0.00..417.36 rows=18192 width=16) (...)
-> Seq Scan on stock_2002 (cost=0.00..326.40 rows=18192 width=16) (...)
Filter: (annee = 2002)
Planning Time: 0.912 ms
Execution Time: 21.518 ms

Remettre en place les index présents dans la table stock origi-


nale.
Il se peut que d’autres index ne servent à rien (ils ne seront dans
ce cas pas présents dans la correction).

CREATE UNIQUE INDEX ON stock (vin_id,contenant_id,annee);

Les autres index ne servent à rien sur les partitions : idx_stock_annee est évidemment
inutile, mais idx_stock_vin_annee aussi, puisqu’il est inclus dans l’index unique que nous
venons de créer.

Quel est le plan pour la récupération du stock des bouteilles du


vin_id 1725, année 2003 ?

EXPLAIN ANALYZE SELECT * FROM stock WHERE vin_id=1725 AND annee=2003 ;

Append (cost=0.29..4.36 rows=3 width=16) (...)


-> Index Scan using stock_2003_vin_id_contenant_id_annee_idx on stock_2003 (...)
Index Cond: ((vin_id = 1725) AND (annee = 2003))
Planning Time: 1.634 ms
Execution Time: 0.166 ms

628
14. PARTITIONNEMENT

Essayer de changer l’année de ce même enregistrement de stock


(la même que la précédente). Pourquoi cela échoue-t-il ?

UPDATE stock SET annee=2004 WHERE annee=2003 and vin_id=1725 ;

ERROR: duplicate key value violates unique constraint "stock_2004_vin_id_contenant_id_annee_idx"


DETAIL: Key (vin_id, contenant_id, annee)=(1725, 1, 2004) already exists.

C’est une violation de contrainte unique, qui est une erreur normale : nous avons déjà un
enregistrement de stock pour ce vin pour l’année 2004.

Supprimer les enregistrements de 2004 pour vin_id = 1725.


Retenter la mise à jour.

DELETE FROM stock WHERE annee=2004 and vin_id=1725;

UPDATE stock SET annee=2004 WHERE annee=2003 and vin_id=1725 ;

Pour vider complètement le stock de 2001, supprimer la partition


stock_2001.

DROP TABLE stock_2001 ;

Tenter d’ajouter au stock une centaine de bouteilles de 2006.


Pourquoi cela échoue-t-il ?

INSERT INTO stock (vin_id, contenant_id, annee, nombre) VALUES (1, 1, 2006, 100) ;

ERROR: no partition of relation "stock" found for row


DETAIL: Partition key of the failing row contains (annee) = (2006).

Il n’existe pas de partition définie pour l’année 2006, cela échoue donc.

Créer une partition par défaut pour recevoir de tels enreg-


istrements.
Retenter l’ajout.

CREATE TABLE stock_default PARTITION OF stock DEFAULT ;

INSERT INTO stock (vin_id, contenant_id, annee, nombre) VALUES (1, 1, 2006, 100) ;

Tenter de créer la partition pour l’année 2006. Pourquoi cela


échoue-t-il ?

629
https://dalibo.com/formations
SQL pour PostgreSQL

CREATE TABLE stock_2006 PARTITION of stock FOR VALUES IN (2006) ;

ERROR: updated partition constraint for default partition "stock_default"


would be violated by some row

Cela échoue car des enregistrements présents dans la partition par défaut répondent à
cette nouvelle contrainte de partitionnement.

Pour créer la partition sur 2006, au sein d’une seule transaction :


- détacher la partition par défaut ;
- y déplacer les enregistrements mentionnés ;
- ré-attacher la partition par défaut.

BEGIN ;

ALTER TABLE stock DETACH PARTITION stock_default;

CREATE TABLE stock_2006 PARTITION of stock FOR VALUES IN (2006) ;

INSERT INTO stock SELECT * FROM stock_default WHERE annee = 2006 ;

DELETE FROM stock_default WHERE annee = 2006 ;

ALTER TABLE stock ATTACH PARTITION stock_default DEFAULT ;

COMMIT ;

630
15. CONNEXIONS DISTANTES

15 CONNEXIONS DISTANTES

15.1 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

Il existe principalement 3 méthodes pour accéder à des données externes à la base sous
PostgreSQL.

Les Foreign Data Wrappers (norme SQL/MED) sont la méthode recommandée pour ac-
céder à un objet distant. Elle permet l’accès à de nombreuses sources de données.

Historiquement, on utilisait dblink, qui ne fournit cette fonctionnalité que de PostgreSQL


à PostgreSQL, et de façon moins performante.

PL/Proxy est un cas d’utilisation très différent : il s’agit de distribuer des appels de fonc-
tions PL sur plusieurs nœuds.

Le sharding n’est pas intégré de manière simple à PostgreSQL dans sa version commu-
nautaire. Il est déjà possible d’en faire une version primitive avec des partitions basées
sur des tables distantes passant par des foreign data wrappers, mais ce n’est qu’un début.
Des éditeurs proposent des extensions, propriétaires ou expérimentales, ou des forks de
PostgreSQL dédiés. Comme souvent, il faut se poser la question du besoin réel par rap-
port à un PostgreSQL bien optimisé avant d’utiliser des outils qui vont ajouter une couche
supplémentaire de complexité dans votre infrastructure.

15.2 FOREIGN DATA WRAPPERS

PostgreSQL supporte SQL/MED :


• Management of External Data
• Extension de la norme SQL ISO
• Données externes présentées comme des tables
• En lecture/écriture (si supporté par le driver et à partir de PostgreSQL 9.3)
– PostgreSQL, Oracle, MySQL (lecture/écriture)
– fichier CSV, fichier fixe (en lecture)
– ODBC, JDBC, Multicorn
631
https://dalibo.com/formations
SQL pour PostgreSQL

– CouchDB, Redis (NoSQL)

SQL/MED est un des tomes de la norme SQL, traitant de l’accès aux données externes
(Management of External Data).

Elle fournit donc un certain nombre d’éléments conceptuels, et de syntaxe, permettant la


déclaration d’accès à des données externes. Ces données externes sont bien sûr présen-
tées comme des tables.

PostgreSQL suit cette norme et est ainsi capable de requêter des tables distantes à travers
des connecteurs FDW (Foreign Data Wrapper). Les seuls connecteurs livrés par défaut
sont file_fdw (fichier plat) et postgres_fdw (à partir de la 9.3) qui permet de se con-
necter à un autre serveur PostgreSQL.

Les deux wrappers les plus aboutis sont sans conteste ceux pour PostgreSQL (c’est un con-
trib) et Oracle (qui supporte jusqu’aux objets géométriques). Ces deux drivers supportent
les écritures sur la base distante.

De nombreux drivers spécialisés existent, entre autres pour accéder à des bases NoSQL
comme MongDB, CouchDB ou Redis.

Il existe aussi des drivers génériques :

• ODBC : utilisation de driver ODBC


• JDBC : utilisation de driver JDBC
• Multicorn : accès aux données au travers d’une API Python, permettant donc
d’écrire facilement un accès pour un nouveau type de service

La liste complète des Foreign Data Wrapper disponibles pour PostgreSQL peut être con-
sultée à cette adresse152 . Rappelons que leur présence sur PGXN n’est en aucun cas un
gage de maturité et de stabilité : testez soigneusement !

15.3 SQL/MED : UTILISATION

CREATE EXTENSION file_fdw;


CREATE SERVER file FOREIGN DATA WRAPPER file_fdw ;

```sql
CREATE FOREIGN TABLE statistical_data (f1 numeric, f2 numeric)
SERVER file OPTIONS (filename '/tmp/statistical_data.csv',
format 'csv', delimiter ';') ;
IMPORT FOREIGN SCHEMA remote_schema FROM SERVER server_name INTO local_schema ;
152
https://pgxn.org/tag/foreign%20data%20wrapper/

632
15. CONNEXIONS DISTANTES

Quel que soit le driver, la création d’un accès se fait en 3 étapes minimum :

• Installation du driver : aucun foreign data wrapper n’est présent par défaut. Il se peut
que vous ayez d’abord à l’installer sur le serveur.
• Création du serveur : permet de spécifier un certain nombre d’informations
génériques à un serveur distant, qu’on n’aura pas à repréciser pour chaque objet
de ce serveur.
• Création de la table distante : l’objet qu’on souhaite rendre visible

Éventuellement, on peut vouloir créer un USER MAPPING, mettant en correspondance les


utilisateurs locaux et distants. Il s’agit habituellement de renseigner les paramètres de
connexion d’un utilisateur (ou groupe d’utilisateurs) local à une base de donnée distante :
login, mot de passe, etc.

La fastidieuse étape de création des tables distantes peut être remplacée par l’ordre
IMPORT FOREIGN SCHEMA. Disponible à partir de la version 9.5, il permet l’import d’un
schéma complet.

15.4 SQL/MED : POSTGRESQL

• Ajouter le FDW
• Ajouter un serveur
• Ajouter une table distante
• Lire la table distante
• Écrire dans la table distante
• Analyser la table distante
• Plus lent qu’une table locale, surtout pour les patterns d’accès complexes

Nous créons une table sur un serveur distant. Par simplicité, nous utiliserons le même
serveur mais une base différente. Créons cette base et cette table :
dalibo=# CREATE DATABASE distante;
CREATE DATABASE

dalibo=# \c distante
You are now connected to database "distante" as user "dalibo".

distante=# CREATE TABLE personnes (id integer, nom text);


CREATE TABLE

distante=# INSERT INTO personnes (id, nom) VALUES (1, 'alice'),


(2, 'bertrand'), (3, 'charlotte'), (4, 'david');
633
https://dalibo.com/formations
SQL pour PostgreSQL

INSERT 0 4

distante=# ANALYZE personnes;


ANALYZE

Maintenant nous pouvons revenir à notre base d’origine et mettre en place la relation
avec le « serveur distant » :

distante=# \c dalibo
You are now connected to database "dalibo" as user "dalibo".

dalibo=# CREATE EXTENSION postgres_fdw;


CREATE EXTENSION

dalibo=# CREATE SERVER serveur_distant FOREIGN DATA WRAPPER postgres_fdw


OPTIONS (HOST 'localhost',PORT '5432', DBNAME 'distante');
CREATE SERVER

dalibo=# CREATE USER MAPPING FOR dalibo SERVER serveur_distant


OPTIONS (user 'dalibo', password 'mon_mdp');
CREATE USER MAPPING

dalibo=# CREATE FOREIGN TABLE personnes (id integer, nom text)


SERVER serveur_distant;
CREATE FOREIGN TABLE

Et c’est tout ! Nous pouvons désormais utiliser la table distante personnes comme si elle
était une table locale de notre base.

dalibo=# SELECT * FROM personnes;


id | nom
----+-----------
1 | alice
2 | bertrand
3 | charlotte
4 | david
(4 rows)

dalibo=# EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM personnes;


QUERY PLAN
----------------------------------------------------------------------------
Foreign Scan on public.personnes (cost=100.00..150.95 rows=1365 width=36)
(actual time=0.655..0.657 rows=4 loops=1)
Output: id, nom
Remote SQL: SELECT id, nom FROM public.personnes
Total runtime: 1.197 ms
(4 rows)

634
15. CONNEXIONS DISTANTES

En plus, si nous filtrons notre requête, le filtre est exécuté sur le serveur distant, réduisant
considérablement le trafic réseau et le traitement associé.

dalibo=# EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM personnes WHERE id = 3;


QUERY PLAN
----------------------------------------------------------------------------
Foreign Scan on public.personnes (cost=100.00..127.20 rows=7 width=36)
(actual time=1.778..1.779 rows=1 loops=1)
Output: id, nom
Remote SQL: SELECT id, nom FROM public.personnes WHERE ((id = 3))
Total runtime: 2.240 ms
(4 rows)

À partir de la 9.3, il est possible d’écrire vers ces tables aussi, à condition que le connecteur
FDW le permette.

En utilisant l’exemple de la section précédente, on note qu’il y a un aller-retour entre la


sélection des lignes à modifier et la modification de ces lignes :

dalibo=# EXPLAIN (ANALYZE, VERBOSE) UPDATE personnes


SET nom = 'agathe' WHERE id = 1;
QUERY PLAN
-------------------------------------------------------------------------------
Update on public.personnes (cost=100.00..140.35 rows=12 width=10)
(actual time=2.086..2.086 rows=0 loops=1)
Remote SQL: UPDATE public.personnes SET nom = $2 WHERE ctid = $1
-> Foreign Scan on public.personnes (cost=100.00..140.35 rows=12 width=10)
(actual time=1.040..1.042 rows=1 loops=1)
Output: id, 'agathe'::text, ctid
Remote SQL: SELECT id, ctid FROM public.personnes WHERE ((id = 1))
FOR UPDATE
Total runtime: 2.660 ms
(6 rows)

dalibo=# SELECT * FROM personnes;


id | nom
----+-----------
2 | bertrand
3 | charlotte
4 | david
1 | agathe
(4 rows)

On peut aussi constater que l’écriture distante respecte les transactions :

dalibo=# BEGIN;
BEGIN

635
https://dalibo.com/formations
SQL pour PostgreSQL

dalibo=# DELETE FROM personnes WHERE id=2;


DELETE 1

dalibo=# SELECT * FROM personnes;


id | nom
----+-----------
3 | charlotte
4 | david
1 | agathe
(3 rows)

dalibo=# ROLLBACK;
ROLLBACK

dalibo=# SELECT * FROM personnes;


id | nom
----+-----------
2 | bertrand
3 | charlotte
4 | david
1 | agathe
(4 rows)

Attention à ne pas perdre de vue qu’une table distante n’est pas une table locale. L’accès
à ses données est plus lent, surtout quand on souhaite récupérer de manière répétitive
peu d’enregistrements : on a systématiquement une latence réseau, éventuellement un
parsing de la requête envoyée au serveur distant, etc.

Les jointures ne sont pas « poussées » au serveur distant avant PostgreSQL 9.6 et pour
des bases PostgreSQL. Un accès par Nested Loop (boucle imbriquée entre les deux tables)
est habituellement inenvisageable entre deux tables distantes : la boucle interne (celle
qui en local serait un accès à une table par index) entraînerait une requête individuelle
par itération, ce qui serait horriblement peu performant.

Les tables distantes sont donc à réserver à des accès intermittents. Il ne faut pas les utiliser
pour développer une application transactionnelle par exemple. Noter qu’entre serveurs
PostgreSQL, chaque version améliore les performances (notamment pour « pousser » le
maximum d’informations et de critères au serveur distant).

636
15. CONNEXIONS DISTANTES

15.5 SQL/MED : PERFORMANCES

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


• postgresql_fdw : fetch_size

Pour améliorer les performances lors de l’utilisation de foreign data wrapper, une pratique
courante est de faire une vue matérialisée de l’objet distant. Les données sont récupérées
en bloc et cette vue matérialisée peut être indexée. C’est une sorte de mise en cache.
Évidemment cela ne convient pas à toutes les applications.

La documentation de postgresql_fdw153 mentionne plusieurs paramètres, et le plus in-


téressant pour des requêtes de gros volume est fetch_size : la valeur par défaut n’est
que de 100, et l’augmenter permet de réduire les aller-retours à travers le réseau.

15.6 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.

Cette fonctionnalité utilise le mécanisme d’héritage de PostgreSQL (depuis la version 9.5).

Exemple d’une table locale qui hérite d’une table distante

La table parent (ici une table distante) sera la table fgn_stock_londre et la table enfant
sera la table local_stock (locale). Ainsi la lecture de la table fgn_stock_londre retourn-
era les enregistrements de la table fgn_stock_londre et de la table local_stock.

Sur l’instance distante :

Créer une table stock_londre sur l’instance distante dans la base nommée « cave » et
insérer des valeurs :
CREATE TABLE stock_londre (c1 int);
INSERT INTO stock_londre VALUES (1),(2),(4),(5);

Sur l’instance locale :

Créer le serveur et la correspondance des droits :


153
https://docs.postgresql.fr/current/postgres-fdw.html

637
https://dalibo.com/formations
SQL pour PostgreSQL

CREATE EXTENSION postgres_fdw ;

CREATE SERVER pgdistant


FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '192.168.0.42', port '5432', dbname 'cave');

CREATE USER MAPPING FOR mon_utilisateur


SERVER pgdistant
OPTIONS (user 'utilisateur_distant', password 'mdp_utilisateur_distant');

Créer une table distante fgn_stock_londre correspondant à la table stock_londre de


l’autre instance :
CREATE FOREIGN TABLE fgn_stock_londre (c1 int) SERVER pgdistant
OPTIONS (schema_name 'public' , table_name 'stock_londre');

On peut bien lire les données :


SELECT tableoid::regclass,* FROM fgn_stock_londre;
tableoid | c1
------------------+----
fgn_stock_londre | 1
fgn_stock_londre | 2
fgn_stock_londre | 4
fgn_stock_londre | 5
(4 lignes)

Voici le plan d’exécution associé :


EXPLAIN ANALYZE SELECT * FROM fgn_stock_londre;

QUERY PLAN
----------------------------------------------------------------------------
Foreign Scan on fgn_stock_londre (cost=100.00..197.75 rows=2925 width=4)
(actual time=0.388..0.389 rows=4 loops=1)

Créer une table local_stock sur l’instance locale qui va hériter de la table mère :
CREATE TABLE local_stock () INHERITS (fgn_stock_londre);

On insère des valeurs dans la table local_stock :


INSERT INTO local_stock VALUES (10),(15);
INSERT 0 2

La table local_stock ne contient bien que 2 valeurs :


SELECT * FROM local_stock ;
c1
----
10

638
15. CONNEXIONS DISTANTES

15
(2 lignes)

En revanche, la table fgn_stock_londre ne contient plus 4 valeurs mais 6 valeurs :


SELECT tableoid::regclass,* FROM fgn_stock_londre;
tableoid | c1
------------------+----
fgn_stock_londre | 1
fgn_stock_londre | 2
fgn_stock_londre | 4
fgn_stock_londre | 5
local_stock | 10
local_stock | 15
(6 lignes)

Dans le plan d’exécution on remarque bien la lecture des deux tables :


EXPLAIN ANALYZE SELECT * FROM fgn_stock_londre;
QUERY PLAN
-------------------------------------------------------------------------
Append (cost=100.00..233.25 rows=5475 width=4)
(actual time=0.438..0.444 rows=6 loops=1)
-> Foreign Scan on fgn_stock_londre
(cost=100.00..197.75 rows=2925 width=4)
(actual time=0.438..0.438 rows=4 loops=1)
-> Seq Scan on local_stock (cost=0.00..35.50 rows=2550 width=4)
(actual time=0.004..0.005 rows=2 loops=1)
Planning time: 0.066 ms
Execution time: 0.821 ms
(5 lignes)

Note : Les données de la table stock_londre sur l’instance distante n’ont pas été modi-
fiées.

Exemple d’une table distante qui hérite d’une table locale

La table parent sera la table master_stock et la table fille (ici distante) sera la table
fgn_stock_londre. Ainsi une lecture de la table master_stock retournera les valeurs
de la table master_stock et de la table fgn_stock_londre, sachant qu’une lecture
de la table fgn_stock_londre retourne les valeurs de la table fgn_stock_londre et
local_stock. Une lecture de la table master_stock retournera les valeurs des 3 tables :
master_stock, fgn_stock_londre, local_stock.

Créer une table master_stock, insérer des valeurs dedans :


CREATE TABLE master_stock (LIKE fgn_stock_londre);
INSERT INTO master_stock VALUES (100),(200);

639
https://dalibo.com/formations
SQL pour PostgreSQL

SELECT tableoid::regclass,* FROM master_stock;


tableoid | c1
--------------+-----
master_stock | 100
master_stock | 200
(2 rows)

Modifier la table fgn_stock_londre pour qu’elle hérite de la table master_stock :

ALTER TABLE fgn_stock_londre INHERIT master_stock ;

La lecture de la table master_stock nous montre bien les valeurs des 3 tables :

SELECT tableoid::regclass,* FROM master_stock ;


tableoid | c1
------------------+-----
master_stock | 100
master_stock | 200
fgn_stock_londre | 1
fgn_stock_londre | 2
fgn_stock_londre | 4
fgn_stock_londre | 5
local_stock | 10
local_stock | 15
(8 lignes)

Le plan d’exécution confirme bien la lecture des 3 tables :

EXPLAIN ANALYSE SELECT * FROM master_stock ;


QUERY PLAN
--------------------------------------------------------------------------
Append (cost=0.00..236.80 rows=5730 width=4)
(actual time=0.004..0.440 rows=8 loops=1)
-> Seq Scan on master_stock (cost=0.00..3.55 rows=255 width=4)
(actual time=0.003..0.003 rows=2 loops=1)
-> Foreign Scan on fgn_stock_londre
(cost=100.00..197.75 rows=2925 width=4)
(actual time=0.430..0.430 rows=4 loops=1)
-> Seq Scan on local_stock (cost=0.00..35.50 rows=2550 width=4)
(actual time=0.003..0.004 rows=2 loops=1)
Planning time: 0.073 ms
Execution time: 0.865 ms
(6 lignes)

Dans cet exemple, on a un héritage « imbriqué » :

• la table master_stock est parent de la table distante fgn_stock_londre


• la table distante fgn_stock_londre est enfant de la table master_stock et parent
de la table local_stock

640
15. CONNEXIONS DISTANTES

• ma table local_stock est enfant de la table distante fgn_stock_londre

master_stock
��fgn_stock_londre => stock_londre
��local_stock

Créons un index sur master_stock et ajoutons des données dans la table master_stock :
CREATE INDEX fgn_idx ON master_stock(c1);
INSERT INTO master_stock (SELECT generate_series(1,10000));

Maintenant effectuons une simple requête de sélection :


SELECT tableoid::regclass,* FROM master_stock WHERE c1=10;

tableoid | c1
--------------+----
master_stock | 10
local_stock | 10
(2 lignes)

Étudions le plan d’exécution associé :


EXPLAIN ANALYZE SELECT tableoid::regclass,* FROM master_stock WHERE c1=10;
QUERY PLAN
-------------------------------------------------------------------------------
Result (cost=0.29..192.44 rows=27 width=8)
(actual time=0.010..0.485 rows=2 loops=1)
-> Append (cost=0.29..192.44 rows=27 width=8)
(actual time=0.009..0.483 rows=2 loops=1)
-> Index Scan using fgn_idx on master_stock
(cost=0.29..8.30 rows=1 width=8)
(actual time=0.009..0.010 rows=1 loops=1)
Index Cond: (c1 = 10)
-> Foreign Scan on fgn_stock_londre
(cost=100.00..142.26 rows=13 width=8)
(actual time=0.466..0.466 rows=0 loops=1)
-> Seq Scan on local_stock (cost=0.00..41.88 rows=13 width=8)
(actual time=0.007..0.007 rows=1 loops=1)
Filter: (c1 = 10)
Rows Removed by Filter: 1

L’index ne se fait que sur master_stock.

En ajoutant l’option ONLY après la clause FROM, on demande au moteur de n’afficher que
la table master_stock et pas les tables filles :
SELECT tableoid::regclass,* FROM ONLY master_stock WHERE c1=10;

tableoid | c1
641
https://dalibo.com/formations
SQL pour PostgreSQL

--------------+----
master_stock | 10
(1 ligne)

Attention, si on supprime les données sur la table parent, la suppression se fait aussi sur
les tables filles :
BEGIN;
DELETE FROM master_stock;
-- [DELETE 10008]
SELECT * FROM master_stock ;

c1
----
(0 ligne)

ROLLBACK;

En revanche avec l’option ONLY, on ne supprime que les données de la table parent :
BEGIN;
DELETE FROM ONLY master_stock;
-- [DELETE 10002]
ROLLBACK;

Enfin, si nous ajoutons une contrainte CHECK sur la table étrangère, l’exclusion de partition
basées sur ces contraintes s’appliquent naturellement :
ALTER TABLE fgn_stock_londre ADD CHECK (c1 < 100);
ALTER TABLE local_stock ADD CHECK (c1 < 100);
--local_stock hérite de fgn_stock_londre !

EXPLAIN (ANALYZE,verbose) SELECT tableoid::regclass,*


FROM master_stock WHERE c1=200;
QUERY PLAN
-------------------------------------------------------------
Result (cost=0.29..8.32 rows=2 width=8)
(actual time=0.009..0.011 rows=2 loops=1)
Output: (master_stock.tableoid)::regclass, master_stock.c1
-> Append (cost=0.29..8.32 rows=2 width=8)
(actual time=0.008..0.009 rows=2 loops=1)
-> Index Scan using fgn_idx on public.master_stock
(cost=0.29..8.32 rows=2 width=8)
(actual time=0.008..0.008 rows=2 loops=1)
Output: master_stock.tableoid, master_stock.c1
Index Cond: (master_stock.c1 = 200)
Planning time: 0.157 ms
Execution time: 0.025 ms
(8 rows)

642
15. CONNEXIONS DISTANTES

Attention : La contrainte CHECK sur fgn_stock_londre est locale seulement. Si cette


contrainte n’existe pas sur la table distants, le résultat de la requête pourra alors être
faux !

Sur le serveur distant :


INSERT INTO stock_londre VALUES (200);

Sur le serveur local :


SELECT tableoid::regclass,* FROM master_stock WHERE c1=200;
tableoid | c1
--------------+-----
master_stock | 200
master_stock | 200
(2 rows)

ALTER TABLE fgn_stock_londre DROP CONSTRAINT fgn_stock_londre_c1_check;

SELECT tableoid::regclass,* FROM master_stock WHERE c1=200;


tableoid | c1
------------------+-----
master_stock | 200
master_stock | 200
fgn_stock_londre | 200

15.7 DBLINK

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

Documentation officielle154 .

Le module dblink de PostgreSQL n’est pas aussi riche que son homonyme dans d’autres
SGBD concurrents bien connus.

Cette extension est un peu ancienne. On préférera utiliser à la place le Foreign Data Wrap-
per pour PostgreSQL (une autre contrib de plus en plus puissante au fil des versions).
dblink a encore l’intérêt d’émuler des transactions autonomes ou d’appeler des fonctions
sur le serveur distant, ce qui est impossible avec postgres_fdw.
154
https://docs.postgresql.fr/current/contrib-dblink-function.html

643
https://dalibo.com/formations
SQL pour PostgreSQL

15.8 PL/PROXY : PRÉSENTATION

• Possibilité de distribuer les requêtes


• Utile pour le « partionnement horizontal »
• Uniquement si votre application n’utilise que des appels de fonction à la base

Une fonction PL/Proxy peut se connecter à plusieurs hôtes distants simultanément !

PostgreSQL propose 3 modes d’exécution des fonctions PL/Proxy :

• ANY : la fonction est exécuté sur un seul noeud au hasard


• ALL : la fonction est exécuté sur tous les noeuds
• EXACT : la fonction est exécutée sur un noeud défini dans le corps de la fonction

On peut mettre en place un ensemble de fonctions PL/Proxy pour « découper » une table
volumineuse et le répartir sur plusieurs instances PostgreSQL.

Le PL/Proxy offre alors la possibilité de développer une couche d’astraction transparente


pour l’utilisateur final qui peut alors consulter et manipuler les données comme si elles se
trouvaient dans une seule table sur une seule instance PostgreSQL.

644
15. CONNEXIONS DISTANTES

15.9 TRAVAUX PRATIQUES

15.9.1 FOREIGN DATA WRAPPER SUR UN FICHIER

Avec le foreign data wrapper file_fdw, créez une table étrangère


qui présente les champs du fichier /etc/passwd sous forme de
table. Vérifiez son bon fonctionnement avec un SELECT.

15.9.2 FOREIGN DATA WRAPPER SUR UNE AUTRE BASE

Accédez à la table stock de votre voisin à travers une table dis-


tante (via postgres_fdw) : configuration du pg_hba.conf, instal-
lation de l’extension, création du serveur, de la table, du map-
ping pour les droits. Visualisez l’accès par un EXPLAIN ANALYZE
VERBOSE SELECT ....

645
https://dalibo.com/formations
SQL pour PostgreSQL

15.10 TRAVAUX PRATIQUES (SOLUTIONS)

15.10.1 FOREIGN DATA WRAPPER SUR UN FICHIER

Avec le foreign data wrapper file_fdw, créez une table étrangère


qui présente les champs du fichier /etc/passwd sous forme de
table. Vérifiez son bon fonctionnement avec un SELECT.

CREATE EXTENSION file_fdw;


CREATE SERVER files FOREIGN DATA WRAPPER file_fdw;
CREATE FOREIGN TABLE passwd (
login text,
passwd text,
uid int,
gid int,
username text,
homedir text,
shell text)
SERVER files
OPTIONS (filename '/etc/passwd', format 'csv', delimiter ':');

15.10.2 FOREIGN DATA WRAPPER SUR UNE AUTRE BASE

Accédez à la table stock de votre voisin à travers une table dis-


tante (via postgres_fdw) : configuration du pg_hba.conf, instal-
lation de l’extension, création du serveur, de la table, du map-
ping pour les droits. Visualisez l’accès par un EXPLAIN ANALYZE
VERBOSE SELECT ....

Tout d’abord, vérifiez qu’avec psql vous arrivez à vous connecter chez lui. Sinon, vérifiez
son listen_addresses, son fichier pg_hba.conf, son firewall.

Une fois que la connexion avec psql fonctionne, vous pouvez entamer la création de la
table étrangère stock_remote chez votre voisin. Attention, si vous avez fait le TP parti-
tionnement précédemment, accédez plutôt à stock_old.

Installez d’abord le foreign data wrapper :

CREATE EXTENSION postgres_fdw ;

Créez le foreign server (déclaration du serveur de votre voisin). Ajustez les options pour
correspondre à votre environnement :

646
15. CONNEXIONS DISTANTES

CREATE SERVER serveur_voisin


FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host '192.168.0.18', port '5432', dbname 'cave');

Créez un user mapping, c’est-à-dire une correspondance entre votre utilisateur local et
l’utilisateur distant :
CREATE USER MAPPING FOR mon_utilisateur
SERVER serveur_voisin
OPTIONS (user 'utilisateur_distant', password 'mdp_utilisateur_distant');

Puis créez votre foreign table :


CREATE FOREIGN TABLE stock_voisin (
vin_id integer, contenant_id integer, annee integer, nombre integer)
SERVER serveur_voisin
OPTIONS (schema_name 'public', table_name 'stock_old');

Vérifiez le bon fonctionnement :


SELECT * FROM stock_voisin WHERE vin_id=12;

Vérifiez le plan :
EXPLAIN ANALYZE VERBOSE SELECT * FROM stock_voisin WHERE vin_id=12;

Il faut l’option VERBOSE pour voir la requête envoyée au serveur distant. Vous constatez
que le prédicat sur vin_id a été transmis, ce qui est le principal avantage de cette implé-
mentation sur les DBLinks.

647
https://dalibo.com/formations
SQL pour PostgreSQL

16 FONCTIONNALITÉS AVANCÉES POUR LA PERFOR-


MANCE

16.1 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 per-
formances.

16.2 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

Un table unlogged est une table non journalisée. Comme la journalisation est responsable
de la durabilité, une table non journalisée n’a pas cette garantie.
Un crash système, un arrêt d’urgence peuvent entraîner la corruption de cette table.
Comme elle n’est pas journalisée, il n’est pas possible de la corriger au redémarrage. De
ce fait, dans l’impossibilité de dire si elle est ou non corrompue, la table est systéma-
tiquement remise à zéro au redémarrage après un crash.

Le fait qu’elle ne soit pas journalisée implique aussi que ses données ne sont pas ré-
pliquées vers des serveurs secondaires, puisque la réplication native de PostgreSQL utilise
les journaux de transactions. Pour la même raison, une restauration de sauvegarde PITR
ne restaurera pas le contenu de la table.

Les contraintes doivent être respectées même si la table unlogged est vidée : une table
normale ne peut donc avoir de clé étrangère pointant vers une table unlogged. Mais les
deux tables peuvent être unlogged.

À part ces limitations, les tables unlogged se comportent exactement comme les autres.
Leur intérêt principal est d’être en moyenne 5 fois plus rapides à la mise à jour. Elles sont

648
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

donc à réserver à des cas d’utilisation particuliers, comme par exemple :

• table de spooling/staging ;
• table de cache/session applicative ;
• table de travail partagée entre sessions ;
• table de travail systématiquement reconstruite avant utilisation dans le flux appli-
catif ;
• et de manière générale toute table contenant des données dont on peut accepter
la perte sans impact opérationnel ou dont on peut regénérer aisément les données.

Les tables unlogged ne doivent pas être confondues avec les tables temporaires (non jour-
nalisées et visibles uniquement dans la session qui les a créées). L’abus des tables tem-
poraires a tendance à générer de la fragmentation dans les tables système ; les tables
unlogged sont une alternative dans certains cas.

16.2.1 UNLOGGED TABLES : MISE EN PLACE

CREATE UNLOGGED TABLE ma_table (col1 int…) ;

Une table unlogged se crée exactement comme une table journalisée classique, excepté
qu’on rajoute le mot UNLOGGED dans la création.

16.2.2 BASCULE D'UNE TABLE EN/DEPUIS UNLOGGED

• ALTER TABLE SET UNLOGGED


– réécriture
• ALTER TABLE SET LOGGED
– passage du contenu dans les WAL !

Depuis la version 9.5, on peut basculer une table à volonté de normale à unlogged et
vice-versa.

Dans le premier cas, la table doit tout de même être réécrite comme pour un VACUUM
FULL, pour des raisons techniques, mais sans qu’il y ait d’écriture dans les journaux à ce
moment, ni par la suite. Ce peut être long pour une grosse table, et il faudra voir si le gain
par la suite le justifie.

Dans le second, la réécriture a aussi lieu, et tout le contenu de la table est journalisé (c’est
indispensable pour la réplication notamment), ce qui génère énormément de journaux et
peut prendre du temps.
649
https://dalibo.com/formations
SQL pour PostgreSQL

Par exemple, une table modifiée de manière répétée pendant un batch, peut être définie
unlogged pour des raisons de performance, puis basculée en logged en fin de traitement
pour pérenniser son contenu.

16.3 JIT : LA COMPILATION À LA VOLÉE

• Compilation Just In Time des requêtes


• Utilise le compilateur LLVM
• Vérifier que l’installation est fonctionnelle
• Disponible en v11 (désactivé par défaut)
• Activé par défaut en v12

Une des nouveautés les plus visibles et techniquement pointues de la v11 est la « com-
pilation à la volée » (Just In Time compilation, ou JIT) de certaines expressions dans les
requêtes SQL. Le JIT n’est activé par défaut qu’à partir de la version 12.

Dans certaines requêtes, l’essentiel du temps est passé à décoder des enregistrements
(tuple deforming), à analyser des clauses WHERE, à effectuer des calculs. En conséquence,
l’idée du JIT est de transformer tout ou partie de la requête en un programme natif di-
rectement exécuté par le processeur.

Cette compilation est une opération lourde qui ne sera effectuée que pour des requêtes
qui en valent le coup.

Le JIT de PostgreSQL s’appuie actuellement sur la chaîne de compilation LLVM,


choisie pour sa flexibilité. L’utilisation nécessite un PostgreSQL compilé avec l’option
--with-llvm et l’installation des bibliothèques de LLVM. Avec les paquets du PGDG, les
dépendances sont en place sur Debian/Ubuntu. Sur CentOS/Red Hat 7, il y a un paquet
supplémentaire à installer, qui lui-même nécessite des paquets du dépôt EPEL :

# yum install epel-release


# yum install postgresql12-llvmjit

Sur CentOS/Red Hat 8, le dépôt EPEL n’est pas nécessaire. Les systèmes CentOS/Red Hat
6 ne permettent pas d’utiliser le JIT.

Si PostgreSQL ne trouve pas les bibliothèques nécessaires, il ne renvoie pas d’erreur et


continue sans tenter de JIT. Pour tester si le JIT est fonctionnel sur votre machine, il doit
apparaître dans un plan quand on force son utilisation ainsi :
SET jit=on;
SET jit_above_cost TO 0 ;
EXPLAIN (ANALYZE) SELECT 1;

650
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

QUERY PLAN
-------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4) (… rows=1 loops=1)
Planning Time: 0.069 ms
JIT:
Functions: 1
Options: Inlining false, Optimization false, Expressions true,
Deforming true
Timing: Generation 0.123 ms, Inlining 0.000 ms, Optimization 0.187 ms,
Emission 2.778 ms, Total 3.088 ms
Execution Time: 3.952 ms

La documentation officielle est assez accessible : https://doc.postgresql.fr/current/jit.


html

16.3.1 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

Le JIT ne peut pas encore compiler toute une requête. La version actuelle se concentre
sur des goulots d’étranglement classiques :

• le décodage des enregistrements (tuple deforming) pour en extraire les champs in-
téressants ;
• les évaluations d’expressions, notamment dans les clauses WHERE pour filtrer les
lignes ;
• les agrégats, les GROUP BY...

Les jointures ne sont pas (encore ?) concernées par le JIT.

Le code résultant est utilisable plus efficacement avec les processeurs actuels qui utilisent
les pipelines et les prédictions de branchement.

Pour les détails, on peut consulter notamment cette conférence très technique au FOS-
DEM 2018155 par l’auteur principal du JIT, Andres Freund.

155
https://archive.fosdem.org/2018/schedule/event/jiting_postgresql_using_llvm/

651
https://dalibo.com/formations
SQL pour PostgreSQL

16.3.2 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 !

De l’avis même de son auteur, l’algorithme de déclenchement du JIT est « naïf ». Quatre
paramètres existent (hors débogage).

jit = on (défaut à partir de la v12) active le JIT si l’environnement technique évoqué


plus haut le permet.

La compilation n’a cependant lieu que pour un coût de requête calculé d’au moins
jit_above_cost (par défaut 100 000, une valeur élevée). Puis, si le coût atteint
jit_inline_above_cost (500 000), certaines fonctions utilisées par la requête et
supportées par le JIT sont intégrées dans la compilation. Si jit_optimize_above_cost
(500 000) est atteint, une optimisation du code compilé est également effectuée. Ces
deux dernières opérations étant longues, elles ne le sont que pour des coûts assez
importants.

Ces seuils sont à comparer avec les coûts des requêtes, qui incluent les entrées-sorties,
donc pas seulement le coût CPU. Ces seuils sont un peu arbitraires et nécessiteront sans
doute un certain tuning en fonction de vos requêtes et de vos processeurs.

Des contre-performances dues au JIT ont déjà été observées, menant à monter les seuils.
Le JIT est trop jeune pour que les développeurs de PostgreSQL eux-mêmes aient des
règles d’ajustement des valeurs des différents paramètres.

Un exemple de plan d’exécution sur une grosse table donne :

# EXPLAIN (ANALYZE) SELECT sum(x), count(id)


FROM bigtable WHERE id + 2 > 500000 ;

QUERY PLAN
-------------------------------------------------------------------------------
Finalize Aggregate (cost=3403866.94..3403866.95 rows=1 width=16) (…)
-> Gather (cost=3403866.19..3403866.90 rows=7 width=16)
(actual time=11778.983..11784.235 rows=8 loops=1)
Workers Planned: 7
Workers Launched: 7
-> Partial Aggregate (cost=3402866.19..3402866.20 rows=1 width=16)(…)

652
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

-> Parallel Seq Scan on bigtable (…)


Filter: ((id + 2) > 500000)
Rows Removed by Filter: 62500
Planning Time: 0.047 ms
JIT:
Functions: 42
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 5.611 ms, Inlining 422.019 ms, Optimization 229.956 ms,
Emission 125.768 ms, Total 783.354 ms
Execution Time: 11785.276 ms

Le plan d’exécution est complété, à la fin, des informations suivantes :

• le nombre de fonctions concernées ;


• les temps de génération, d’inclusion des fonctions, d’optimisation du code compilé...

Dans l’exemple ci-dessus, on peut constater que ces coûts ne sont pas négligeables
par rapport au temps total. Il reste à voir si ce temps perdu est récupéré sur le temps
d’exécution de la requête… ce qui en pratique n’a rien d’évident.

Sans JIT, la durée de la requête était d’environ 17 s.

16.3.3 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

Vu son coût élevé, le JIT n’a d’intérêt que pour les requêtes utilisant beaucoup le CPU et
où il est le facteur limitant.

Ce seront donc surtout des requêtes analytiques agrégeant beaucoup de lignes, com-
prenant beaucoup de calculs et filtres, et non les petites requêtes d’un ERP.

Il n’y a pas non plus de mise en cache du code compilé.

Si gain il y a, il est relativement modeste en deçà de quelques millions de lignes, et devient


de plus en plus important au fur et à mesure que la volumétrie augmente, à condition bien
sûr que d’autres limites n’apparaissent pas (bande passante...).

Documentation officielle : https://docs.postgresql.fr/current/jit-decision.html


653
https://dalibo.com/formations
SQL pour PostgreSQL

16.4 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…

L’indexation FTS est un des cas les plus fréquents d’utilisation non-relationnelle d’une
base de données : les utilisateurs ont souvent besoin de pouvoir rechercher une informa-
tion qu’ils ne connaissent pas parfaitement, d’une façon floue :

• recherche d’un produit/article par rapport à sa description ;


• recherche dans le contenu de livres/documents…

PostgreSQL doit donc permettre de rechercher de façon efficace dans un champ texte.
L’avantage de cette solution est d’être intégrée au SGBD. Le moteur de recherche est
donc toujours parfaitement à jour avec le contenu de la base, puisqu’il est intégré avec le
reste des transactions.

Le principe est de décomposer le texte en « lexèmes » propres à chaque langue. Cela


implique donc une certaine forme de normalisation, et permettent aussi de tenir compte
de dictionnaires de synonymes. Le dictionnaire inclue aussi les termes courants inutiles à
indexer (stop words) propres à la langue (le, la, et, the, and, der, daß…).

Décomposition et recherche en plein texte utilisent des fonctions et opérateurs dédiés,


ce qui nécessite donc une adaptation du code. Ce qui suit n’est qu’un premier aperçu. La
recherche plein texte est un chapitre entier de la documentation officielle156 .

Adrien Nayrat a donné une excellente conférence sur le sujet au PGDay France 2017 à
Toulouse157 (slides158 ).

156
https://docs.postgresql.fr/current/textsearch.html
157
https://www.youtube.com/embed/9S5dBqMbw8A
158
https://2017.pgday.fr/slides/nayrat_Le_Full_Text_Search_dans_PostgreSQL.pdf

654
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

16.4.1 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

to_tsvector analyse un texte et le décompose en lexèmes, et non en mots. Les chiffres


indiquent ici les positions et ouvrent la possibilité à des scores de proximité. Mais des
indications de poids sont possibles.

Autre exemple de décomposition d’une phrase :

# SHOW default_text_search_config ;
default_text_search_config
----------------------------
pg_catalog.french

# SELECT to_tsvector ('La documentation de PostgreSQL est \


sur https://www.postgresql.org/') ;
to_tsvector
----------------------------------------------------
'document':2 'postgresql':4 'www.postgresql.org':7

Les mots courts et le verbe « être » sont repérés comme termes trop courants, la casse
est ignorée, même l’URL est décomposée en protocole et hôte. On peut voir en détail
comment la FTS a procédé :

# SELECT description, token, dictionary, lexemes


FROM ts_debug('La documentation de PostgreSQL est sur https://www.postgresql.org/');

description | token | dictionary | lexemes


-----------------+--------------------+-------------+----------------------
Word, all ASCII | La | french_stem | {}
Space symbols | | ¤ | ¤
Word, all ASCII | documentation | french_stem | {document}
Space symbols | | ¤ | ¤
655
https://dalibo.com/formations
SQL pour PostgreSQL

Word, all ASCII | de | french_stem | {}


Space symbols | | ¤ | ¤
Word, all ASCII | PostgreSQL | french_stem | {postgresql}
Space symbols | | ¤ | ¤
Word, all ASCII | est | french_stem | {}
Space symbols | | ¤ | ¤
Word, all ASCII | sur | french_stem | {}
Space symbols | | ¤ | ¤
Protocol head | https:// | ¤ | ¤
Host | www.postgresql.org | simple | {www.postgresql.org}
Space symbols | / | ¤ | ¤

Si l’on se trompe de langue, les termes courants sont mal repérés (et la recherche sera
inefficace) :

# SELECT to_tsvector ('english',


'La documentation de PostgreSQL est sur https://www.postgresql.org/');
to_tsvector
----------------------------------------------------------------------------------
'de':3 'document':2 'est':5 'la':1 'postgresql':4 'sur':6 'www.postgresql.org':7

Pour construire un critère de recherche, to_tsquery est nécessaire :

SELECT * FROM textes


WHERE to_tsvector('french',contenu) @@ to_tsquery('Valjean & Cosette');

Les termes à chercher peuvent être combinés par &, | (ou), ! (négation), <-> (mots suc-
cessifs), <N> (séparés par N lexèmes). @@ est l’opérateur de correspondance. Il y en a
d’autres159 .

Il existe une fonction phraseto_tsquery pour donner une phrase entière comme critère,
laquelle sera décomposée en lexèmes :

# SELECT livre, contenu FROM textes


WHERE
livre ILIKE 'Les Misérables Tome V%'
AND ( to_tsvector ('french',contenu)
@@ phraseto_tsquery('c''est la fautes de Voltaire')
OR to_tsvector ('french',contenu)
@@ phraseto_tsquery('nous sommes tombés à terre')
);
livre | contenu
159
https://docs.postgresql.fr/current/functions-textsearch.html

656
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

-------------------------------------------------+----------------------------

Les misérables Tome V Jean Valjean, Hugo, Victor | Je suis tombé par terre,
Les misérables Tome V Jean Valjean, Hugo, Victor | C'est la faute à Voltaire,

16.4.2 FULL TEXT SEARCH : DICTIONNAIRES

• Configurations liées à la langue


– basées sur des dictionnaires
– dictionnaires filtrants (unaccent)
– synonymes
• Extensible grâce à des sources extérieures
• Configuration par défaut : default_text_search_config

Les lexèmes, les termes courants, la manière de décomposer un terme… sont fortement
liés à la langue.

Des configurations toutes prêtes sont fournies par PostgreSQL pour certaines langues :

# \dF
Liste des configurations de la recherche de texte
Schéma | Nom | Description
------------+------------+---------------------------------------
pg_catalog | arabic | configuration for arabic language
pg_catalog | danish | configuration for danish language
pg_catalog | dutch | configuration for dutch language
pg_catalog | english | configuration for english language
pg_catalog | finnish | configuration for finnish language
pg_catalog | french | configuration for french language
pg_catalog | german | configuration for german language
pg_catalog | hungarian | configuration for hungarian language
pg_catalog | indonesian | configuration for indonesian language
pg_catalog | irish | configuration for irish language
pg_catalog | italian | configuration for italian language
pg_catalog | lithuanian | configuration for lithuanian language
pg_catalog | nepali | configuration for nepali language
pg_catalog | norwegian | configuration for norwegian language
pg_catalog | portuguese | configuration for portuguese language
pg_catalog | romanian | configuration for romanian language
657
https://dalibo.com/formations
SQL pour PostgreSQL

pg_catalog | russian | configuration for russian language


pg_catalog | simple | simple configuration
pg_catalog | spanish | configuration for spanish language
pg_catalog | swedish | configuration for swedish language
pg_catalog | tamil | configuration for tamil language
pg_catalog | turkish | configuration for turkish language

La recherche plein textes est donc directement utilisable pour le français ou l’anglais
et beaucoup d’autres langues européennes. La configuration par défaut dépend du
paramètre default_text_search_config, même s’il est conseillé de toujours passer ex-
plicitement la configuration aux fonctions. Ce paramètre peut être modifié globalement,
par session ou par un ALTER DATABASE SET.

En demandant le détail de la configuration french, on peut voir qu’elle se base sur des
« dictionnaires » pour chaque type d’élément qui peut être rencontré : mots, phrases mais
aussi URL, entiers…

# \dF+ french
Configuration « pg_catalog.french » de la recherche de texte
Analyseur : « pg_catalog.default »
Jeton | Dictionnaires
-----------------+---------------
asciihword | french_stem
asciiword | french_stem
email | simple
file | simple
float | simple
host | simple
hword | french_stem
hword_asciipart | french_stem
hword_numpart | simple
hword_part | french_stem
int | simple
numhword | simple
numword | simple
sfloat | simple
uint | simple
url | simple
url_path | simple
version | simple
word | french_stem

658
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

On peut lister ces dictionnaires :

# \dFd
Liste des dictionnaires de la recherche de texte
Schéma | Nom | Description
------------+-----------------+---------------------------------------------

pg_catalog | english_stem | snowball stemmer for english language

pg_catalog | french_stem | snowball stemmer for french language

pg_catalog | simple | simple dictionary: just lower case
| and check for stopword

Ces dictionnaires sont de type « Snowball160 », incluant notamment des algorithmes


différents pour chaque langue. Le dictionnaire simple n’est pas lié à une langue et corre-
spond à une simple décomposition après passage en minuscule et recherche de termes
courants anglais : c’est suffisant pour des éléments comme les URL.

D’autres dictionnaires peuvent être combinés aux existants pour créer une nouvelle con-
figuration. Le principe est que les dictionnaires reconnaissent certains éléments, et trans-
mettent aux suivants ce qu’ils n’ont pas reconnu. Les dictionnaires précédents, de type
Snowball, reconnaissent tout et doivent donc être placés en fin de liste.

Par exemple, la contrib unaccent permet de faire une configuration négligeant les
accents161 . La contrib dict_int fournit un dictionnaire qui réduit la précision des nom-
bres162 pour réduire la taille de l’index. La contrib dict_xsyn permet de créer un diction-
naire pour gérer une liste de synonymes163 . Mais les dictionnaires de synonymes peuvent
être gérés manuellement164 . Les fichiers adéquats sont déjà présents ou à ajouter dans
$SHAREDIR/tsearch_data/ (par exemple /usr/pgsql-12/share/tsearch_data sur
Red Hat/CentOS ou /usr/share/postgresql/12/tsearch_data sur Debian).

Donc par exemple, en utilisant le fichier d’exemple $SHAREDIR/tsearch_data/synonym_sample.syn,


dont le contenu est :

postgresql pgsql
postgre pgsql
gogle googl
160
https://snowballstem.org/
161
https://docs.postgresql.fr/current/unaccent.html
162
https://docs.postgresql.fr/current/dict-int.html
163
https://docs.postgresql.fr/current/dict-xsyn.html
164
https://docs.postgresql.fr/current/textsearch-dictionaries.html#textsearch-synonym-dictionary

659
https://dalibo.com/formations
SQL pour PostgreSQL

indices index*

on peut définir un dictionnaire de synonymes, créer une nouvelle configuration reprenant


french, et y insérer le nouveau dictionnaire en premier élément :
# CREATE TEXT SEARCH DICTIONARY messynonymes
(template=synonym, synonyms='synonym_sample');

# CREATE TEXT SEARCH CONFIGURATION french2 (copy=french);

# ALTER TEXT SEARCH CONFIGURATION french2


ALTER MAPPING FOR asciiword,hword,asciihword,word
WITH messynonymes, french_stem ;

# SELECT to_tsvector ('french2',


'PostgreSQL s''abrège en pgsql ou Postgres') ;

to_tsvector
-------------------------
'abreg':3 'pgsql':1,5,7

Les trois versions de « PostgreSQL » ont été reconnues.

Pour une analyse plus fine, on peut ajouter d’autres dictionnaires linguistiques depuis des
sources extérieures (Ispell, OpenOffice…). Ce n’est pas intégré par défaut à PostgreSQL
mais la procédure est dans la documentation165 .

Des « thesaurus » peuvent être même être créés pour remplacer des expressions par des
synonymes (et identifier par exemple « le meilleur SGBD » et « PostgreSQL »).

16.4.3 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

Sans indexation, une recherche FTS fonctionne, mais parcourra entièrement la table.
L’indexation est possible, avec GIN ou GiST. On peut stocker le vecteur résultat de
to_tsvector dans une autre colonne de la table, et c’est elle qui sera indexée. Jusque
PostgreSQL 11, il est nécessaire de le faire manuellement, ou d’écrire un trigger pour
cela. Avec PostgreSQL 12, on peut utiliser une colonne générée (il est nécessaire de
préciser la configuration FTS) :
165
https://docs.postgresql.fr/current/textsearch-dictionaries.html

660
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

ALTER TABLE textes


ADD COLUMN vecteur tsvector
GENERATED ALWAYS AS (to_tsvector ('french', contenu)) STORED ;

Les critères de recherche porteront sur la colonne vecteur :

# SELECT * FROM textes WHERE vecteur @@ to_tsquery ('french','Roméo <2> Juliette');

Plus simplement, il peut suffire de créer directement un index fonctionnel sur


to_tsvector ('french', contenu). Ceci devra bien sûr être le critère de recherche
exact, sinon l’index ne sera pas utilisé :

# CREATE INDEX idx_fts ON public.textes


USING gin (to_tsvector('french'::regconfig, contenu))

# SELECT * FROM textes


WHERE to_tsvector ('french', contenu) @@ to_tsquery ('french','Roméo <2> Juliette');

Voici un exemple complet de mise en place de FTS :

• Création d’une configuration de dictionnaire dédiée avec dictionnaire français, sans


accent, dans une table de dépêches :

CREATE TEXT SEARCH CONFIGURATION depeches (COPY= french);

CREATE EXTENSION unaccent ;

ALTER TEXT SEARCH CONFIGURATION depeches ALTER MAPPING FOR


hword, hword_part, word WITH unaccent,french_stem;

• Ajout d’une colonne vectorisée à la table depeche, avec des poids différents pour
le titre et le texte, ici gérée manuellement avec un trigger.

CREATE TABLE depeche (id int, titre text, texte text) ;

ALTER TABLE depeche ADD vect_depeche tsvector;

UPDATE depeche
SET vect_depeche =
(setweight(to_tsvector('depeches',coalesce(titre,'')), 'A') ||
setweight(to_tsvector('depeches',coalesce(texte,'')), 'C'));

CREATE FUNCTION to_vectdepeche( )


RETURNS trigger
LANGUAGE plpgsql
-- common options: IMMUTABLE STABLE STRICT SECURITY DEFINER
AS $function$
BEGIN
NEW.vect_depeche :=
661
https://dalibo.com/formations
SQL pour PostgreSQL

setweight(to_tsvector('depeches',coalesce(NEW.titre,'')), 'A') ||
setweight(to_tsvector('depeches',coalesce(NEW.texte,'')), 'C');
return NEW;
END
$function$;

CREATE TRIGGER trg_depeche before INSERT OR update ON depeche


FOR EACH ROW execute procedure to_vectdepeche();

• Création de l’index associé au vecteur :


CREATE INDEX idx_gin_texte ON depeche USING gin(vect_depeche);

• Collecte des statistiques sur la table :


ANALYZE depeche ;

• Utilisation basique :
SELECT titre,texte FROM depeche WHERE vect_depeche @@
to_tsquery('depeches','varicelle');
SELECT titre,texte FROM depeche WHERE vect_depeche @@
to_tsquery('depeches','varicelle & médecin');

• Tri par pertinenence :


SELECT titre,texte
FROM depeche
WHERE vect_depeche @@ to_tsquery('depeches','varicelle & médecin')
ORDER BY ts_rank_cd(vect_depeche, to_tsquery('depeches','varicelle & médecin'));

• Cette requête peut s’écrire aussi ainsi :


SELECT titre,ts_rank_cd(vect_depeche,query) AS rank
FROM depeche, to_tsquery('depeches','varicelle & médecin') query
WHERE query@@vect_depeche
ORDER BY rank DESC ;

16.4.4 FULL TEXT SEARCH SUR DU JSON

• Vectorisation possible des JSON


• Version 10

Depuis la version 10 de PostgreSQL, une recherche FTS est directement possible sur des
champs JSON. Voici un exemple :
CREATE TABLE commandes (info jsonb);

INSERT INTO commandes (info)

662
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

VALUES
(
'{ "client": "Jean Dupont",
"articles": {"produit": "Enveloppes A4","qté": 24}}'
),
(
'{ "client": "Jeanne Durand",
"articles": {"produit": "Imprimante","qté": 1}}'
),
(
'{ "client": "Benoît Delaporte",
"items": {"produit": "Rame papier normal A4","qté": 5}}'
),
(
'{ "client": "Lucie Dumoulin",
"items": {"produit": "Pochette Papier dessin A3","qté": 5}}'
);

La décomposition par FTS donne :


# SELECT to_tsvector('french', info) FROM commandes ;
to_tsvector
------------------------------------------------
'a4':5 'dupont':2 'envelopp':4 'jean':1
'durand':2 'imprim':4 'jeann':1
'a4':4 'benoît':6 'delaport':7 'normal':3 'papi':2 'ram':1
'a3':4 'dessin':3 'dumoulin':7 'luc':6 'papi':2 'pochet':1

Une recherche sur « papier » donne :


# 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"}

Plus d’information chez Depesz : Full Text Search support for json and jsonb166 .

166
https://www.depesz.com/2017/04/04/waiting-for-postgresql-10-full-text-search-support-for-json-and-jsonb/

663
https://dalibo.com/formations
SQL pour PostgreSQL

16.5 TRAVAUX PRATIQUES

16.5.1 TABLES NON JOURNALISÉES

Afficher le nom du journal de transaction courant.

Créer une base pgbench vierge, de taille 80 (environ 1,2 Go). Les
tables doivent être en mode unlogged.

Afficher la liste des objets unlogged dans la base pgbench.

Afficher le nom du journal de transaction courant. Que s’est-il


passé ?

Passer l’ensemble des tables de la base pgbench en mode logged.

Afficher le nom du journal de transaction courant. Que s’est-il


passé ?

Repasser toutes les tables de la base pgbench en mode unlogged.

Afficher le nom du journal de transaction courant. Que s’est-il


passé ?

Réinitialiser la base pgbench toujours avec une taille 80 mais avec


les tables en mode logged.
Que constate-t-on ?

Réinitialiser la base pgbench mais avec une taille de 10. Les tables
doivent être en mode unlogged.

664
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

Compter le nombre de lignes dans la table pgbench_accounts.

Simuler un crash de l’instance PostgreSQL.

Redémarrer l’instance PostgreSQL.

Compter le nombre de lignes dans la table pgbench_accounts.


Que constate-t-on ?

16.5.2 INDEXATION FULL TEXT

Vous aurez besoin de la base textes. La base est disponible en deux versions : complète
sur https://dali.bo/tp_gutenberg (dump de 0,5 Go, table de 21 millions de lignes dans
3 Go) ou https://dali.bo/tp_gutenberg10 pour un extrait d’un dizième. Le dump peut se
charger dans une base préexistante avec pg_restore et créera juste une table nommée
textes.

Ce TP utilise la version complète de la base textes basée sur le projet Gutenberg. Un


index GIN va permettre d’utiliser la Full Text Search sur la table textes.

Créer un index GIN sur le vecteur du champ contenu (fonction


to_tsvector).

Quelle est la taille de cet index ?

Quelle performance pour trouver « Fantine » (personnage des


Misérables de Victor Hugo) dans la table ? Le résultat contient-
il bien « Fantine » ?

Trouver les lignes qui contiennent à la fois les mots « affaire » et


« couteau » et voir le plan.

665
https://dalibo.com/formations
SQL pour PostgreSQL

16.6 TRAVAUX PRATIQUES (SOLUTIONS)

16.6.1 TABLES NON JOURNALISÉES

Afficher le nom du journal de transaction courant.

postgres=# SELECT pg_walfile_name(pg_current_wal_lsn());


pg_walfile_name
--------------------------
000000010000000100000024

Créer une base pgbench vierge, de taille 80 (environ 1,2 Go). Les
tables doivent être en mode unlogged.

$ createdb pgbench

$ /usr/pgsql-13/bin/pgbench -i -s 80 --unlogged-tables pgbench


dropping old tables...
NOTICE: table "pgbench_accounts" does not exist, skipping
NOTICE: table "pgbench_branches" does not exist, skipping
NOTICE: table "pgbench_history" does not exist, skipping
NOTICE: table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
8000000 of 8000000 tuples (100%) done (elapsed 4.93 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 8.84 s (drop tables 0.00 s, create tables 0.01 s, client-side generate 5.02 s,
vacuum 1.79 s, primary keys 2.02 s).

Afficher la liste des objets unlogged dans la base pgbench.

$ psql -c "SELECT relname FROM pg_class WHERE relpersistence = 'u';" -d pgbench


relname
-----------------------
pgbench_accounts
pgbench_branches
pgbench_history
pgbench_tellers
pgbench_branches_pkey
pgbench_tellers_pkey
pgbench_accounts_pkey

Les 3 objets avec le suffixe pkey correspondent aux clés primaires des tables créées par
pgbench. Comme elles dépendent des tables, elles sont également en mode unlogged.

666
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

Afficher le nom du journal de transaction courant. Que s’est-il


passé ?

postgres=# SELECT pg_walfile_name(pg_current_wal_lsn());


pg_walfile_name
--------------------------
000000010000000100000024

Comme l’initialisation de pgbench a été réalisée en mode unlogged, aucune information


concernant les tables et les données qu’elles contiennent n’a été inscrite dans les journaux
de transaction. Donc le journal de transaction est toujours le même.

Passer l’ensemble des tables de la base pgbench en mode logged.

pgbench=# ALTER TABLE pgbench_accounts SET LOGGED;


ALTER TABLE
pgbench=# ALTER TABLE pgbench_branches SET LOGGED;
ALTER TABLE
pgbench=# ALTER TABLE pgbench_history SET LOGGED;
ALTER TABLE
pgbench=# ALTER TABLE pgbench_tellers SET LOGGED;
ALTER TABLE

Afficher le nom du journal de transaction courant. Que s’est-il


passé ?

pgbench=# SELECT pg_walfile_name(pg_current_wal_lsn());


pg_walfile_name
--------------------------
000000010000000100000077

Comme toutes les tables de la base pgbench ont été passées en mode logged, une réécri-
ture de celles-ci a eu lieu (comme pour un VACUUM FULL). Cette réécriture additionnée
au mode logged a entraîné une forte écriture dans les journaux de transaction. Dans
notre cas, 83 journaux de transaction ont été consommés, soit approximativement 1,3
Go d’utilisé sur disque.

Il faut donc faire particulièrement attention à la quantité de journaux de transaction qui


peut être générée lors du passage d’une table du mode unlogged à logged.

Repasser toutes les tables de la base pgbench en mode unlogged.

pgbench=# ALTER TABLE pgbench_accounts SET UNLOGGED;


ALTER TABLE
667
https://dalibo.com/formations
SQL pour PostgreSQL

pgbench=# ALTER TABLE pgbench_branches SET UNLOGGED;


ALTER TABLE
pgbench=# ALTER TABLE pgbench_history SET UNLOGGED;
ALTER TABLE
pgbench=# ALTER TABLE pgbench_tellers SET UNLOGGED;
ALTER TABLE

Afficher le nom du journal de transaction courant. Que s’est-il


passé ?

pgbench=# SELECT pg_walfile_name(pg_current_wal_lsn());


pg_walfile_name
--------------------------
000000010000000100000077

Le processus est le même que précedemment, mais, lors de la réécriture des tables, au-
cune information n’est stockée dans les journaux de transaction.

Réinitialiser la base pgbench toujours avec une taille 80 mais avec


les tables en mode logged.
Que constate-t-on ?

$ /usr/pgsql-13/bin/pgbench -i -s 80 -d pgbench
dropping old tables...
creating tables...
generating data (client-side)...
8000000 of 8000000 tuples (100%) done (elapsed 9.96 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 16.60 s (drop tables 0.11 s, create tables 0.00 s, client-side generate 10.12 s,
vacuum 2.87 s, primary keys 3.49 s).

On constate que le temps mis par pgbench pour initialiser sa base est beaucoup plus long
en mode logged que unlogged. On passe de 8,84 secondes en unlogged à 16,60 secondes
en mode logged. Cette augmentation du temps de traitement est due à l’écriture dans les
journaux de transaction.

Réinitialiser la base pgbench mais avec une taille de 10. Les tables
doivent être en mode unlogged.

$ /usr/pgsql-13/bin/pgbench -i -s 10 -d pgbench --unlogged-tables


dropping old tables...
creating tables...
generating data (client-side)...
1000000 of 1000000 tuples (100%) done (elapsed 0.60 s, remaining 0.00 s)

668
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

vacuuming...
creating primary keys...
done in 1.24 s (drop tables 0.02 s, create tables 0.02 s, client-side generate 0.62 s,
vacuum 0.27 s, primary keys 0.31 s).

Compter le nombre de lignes dans la table pgbench_accounts.

$ psql -c "SELECT count(*) FROM pgbench_accounts;" -d pgbench


count
---------
1000000

Simuler un crash de l’instance PostgreSQL.

$ ps -ef | grep postmaster


postgres 697 1 0 14:32 ? 00:00:00 /usr/pgsql-13/bin/postmaster -D ...

$ kill -9 697

Ne faites jamais cela en production, bien sûr !

Redémarrer l’instance PostgreSQL.

$ /usr/pgsql-13/bin/pg_ctl -D /var/lib/pgsql/13/data start

Compter le nombre de lignes dans la table pgbench_accounts.


Que constate-t-on ?

$ psql -c "SELECT count(*) FROM pgbench_accounts;" -d pgbench


count
-------
0

Lors d’un crash, PostgreSQL remet tous les objets unlogged à zéro.

16.6.2 INDEXATION FULL TEXT

Créer un index GIN sur le vecteur du champ contenu (fonction


to_tsvector).

textes=# CREATE INDEX idx_fts ON textes


USING gin (to_tsvector('french',contenu));
CREATE INDEX

669
https://dalibo.com/formations
SQL pour PostgreSQL

Quelle est la taille de cet index ?

La table « pèse » 3 Go (même si on pourrait la stocker de manière beaucoup plus efficace).


L’index GIN est lui-même assez lourd dans la configuration par défaut :
textes=# SELECT pg_size_pretty(pg_relation_size('idx_fts'));
pg_size_pretty
----------------
593 MB
(1 ligne)

Quelle performance pour trouver « Fantine » (personnage des


Misérables de Victor Hugo) dans la table ? Le résultat contient-
il bien « Fantine » ?

textes=# EXPLAIN (ANALYZE,BUFFERS) SELECT * FROM textes


WHERE to_tsvector('french',contenu) @@ to_tsquery('french','fantine');

QUERY PLAN
-------------------------------------------------------------------------------
Bitmap Heap Scan on textes (cost=107.94..36936.16 rows=9799 width=123)
(actual time=0.423..1.149 rows=326 loops=1)
Recheck Cond: (to_tsvector('french'::regconfig, contenu)
@@ '''fantin'''::tsquery)
Heap Blocks: exact=155
Buffers: shared hit=159
-> Bitmap Index Scan on idx_fts (cost=0.00..105.49 rows=9799 width=0)
(actual time=0.210..0.211 rows=326 loops=1)
Index Cond: (to_tsvector('french'::regconfig, contenu)
@@ '''fantin'''::tsquery)
Buffers: shared hit=4
Planning Time: 1.248 ms
Execution Time: 1.298 ms

On constate donc que le Full Text Search est très efficace du moins pour le Full Text Search +
GIN : trouver 1 mot parmi plus de 100 millions avec 300 enregistrements correspondants
dure 1,5 ms (cache chaud).

Si l’on compare avec une recherche par trigramme (extension pg_trgm et index GIN), c’est
bien meilleur. À l’inverse, les trigrammes permettent des recherches floues (orthographe
approximative), des recherches sur autre chose que des mots, et ne nécessitent pas de
modification de code.

670
16. FONCTIONNALITÉS AVANCÉES POUR LA PERFORMANCE

Par contre, la recherche n’est pas exacte, « Fantin » est fréquemment trouvé. En fait, le
plan montre que c’est le vrai critère retourné par to_tsquery('french','fantine') et
transformé en 'fantin'::tsquery. Si l’on tient à ce critère précis il faudra ajouter une
clause plus classique contenu LIKE '%Fantine%' pour filtrer le résultat après que le FTS
ait « dégrossi » la recherche.

Trouver les lignes qui contiennent à la fois les mots « affaire » et


« couteau » et voir le plan.

10 lignes sont ramenées en quelques millisecondes :


EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM textes
WHERE to_tsvector('french',contenu) @@ to_tsquery('french','affaire & couteau')
;

QUERY PLAN
-------------------------------------------------------------------------------
Bitmap Heap Scan on textes (cost=36.22..154.87 rows=28 width=123)
(actual time=6.642..6.672 rows=10 loops=1)
Recheck Cond: (to_tsvector('french'::regconfig, contenu)
@@ '''affair'' & ''couteau'''::tsquery)
Heap Blocks: exact=10
Buffers: shared hit=53
-> Bitmap Index Scan on idx_fts (cost=0.00..36.21 rows=28 width=0)
(actual time=6.624..6.624 rows=10 loops=1)
Index Cond: (to_tsvector('french'::regconfig, contenu)
@@ '''affair'' & ''couteau'''::tsquery)
Buffers: shared hit=43
Planning Time: 0.519 ms
Execution Time: 6.761 ms

Noter que les pluriels « couteaux » et « affaires » figurent parmi les résultats puisque la
recherche porte sur les léxèmes 'affair'' & ''couteau'.

671
https://dalibo.com/formations
SQL pour PostgreSQL

17 POOLING

672
17. POOLING

17.1 AU MENU

• Concepts
• Pool de connexion avec PgBouncer

Ce module permet d’aborder le pooling.

Ce qui suit ne portera que sur un unique serveur, et n’aborde pas le sujet de la répartition
de charge.

Nous étudierons principalement un logiciel : PgBouncer.

17.1.1 OBJECTIFS

• Savoir ce qu’est un pool de connexion ?


• Avantage, inconvénients & limites
• Savoir mettre en place un pooler de connexion avec PgBouncer

17.2 POOL DE CONNEXION

• Qu’est ce qu’un pool de connexion ?


• Présentation
• Avantages et inconvénients
• Mise en œuvre avec PgBouncer

Dans cette partie, nous allons étudier la théorie des poolers de connexion. La partie
suivante sera la mise en pratique avec l’outil PgBouncer.

673
https://dalibo.com/formations
SQL pour PostgreSQL

17.2.1 SERVEUR DE POOL DE CONNEXIONS

17.2.2 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ée
• Différents poolers :
– intégrés aux applicatifs
– service séparé (où ?)

Un serveur de pool de connexions s’intercale entre les clients et le système de gestion


de bases de données. Les clients ne se connectent plus directement sur le SGBD pour
accéder aux bases. Ils passent par le pooler qui se fait passer pour le serveur de bases
de données. Le pooler maintient alors des connexions vers le SGBD et en gère lui-même
l’attribution aux utilisateurs.

Chaque connexion au SGBD est définie par deux paramètres : le rôle de connexion et
la base de donnée. Ainsi, une connexion maintenue par le pooler ne sera attribuée à un
utilisateur que si ce couple rôle/base de donnée est le même.

674
17. POOLING

Les conditions de création de connexions au SGBD sont donc définies dans la configura-
tion du pooler.

Un pooler peut se présenter sous différentes formes :

• comme brique logicielle incorporée dans le code applicatif sur les serveurs
d’applications (fourni par Hibernate ou Apache Tomcat, par exemple) ;
• comme service séparé, démarré sur un serveur et écoutant sur un port donné, où
les clients se connecteront pour accéder à la base de donnée voulue (exemples :
PgBouncer, pgPool)

Nous nous consacrons dans ce module aux pools de connexions accessibles à travers un
service.

Noter qu’il ne faut pas confondre un pooler avec un outil de répartition de charge (même
si un pooler peut également permettre la répartition de charge, comme PgPool).

L’emplacement d’un pooler se décide au cas par cas selon l’architecture. Il peut aussi
bien se trouver intégré à l’application, et lui être dédié, qu’être sur le serveur de base de
données et servir plusieurs applications, voire se trouver sur une troisième machine. Il
faut aussi réfléchir à ce qui se passera en cas de bascule entre deux instances.

17.2.3 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)
– Saturation temporaires des connexions sur l’instance

Le maintien des connexions entre le pooler et le SGBD apporte un gain non négligeable
lors de l’établissement des connexions. Effectivement, pour chaque nouvelle connexion
à PostgreSQL, nous avons :

• la création d’un nouveau processus ;


• l’allocation des ressources mémoires utiles à la session ;
• le positionnement des paramètres de session de l’utilisateur.

Tout ceci engendre une consommation processeur.


675
https://dalibo.com/formations
SQL pour PostgreSQL

Ce travail peut durer plusieurs dizaines, voire centaines de millisecondes. Cette latence
induite peut alors devenir un réel goulot d’étranglement dans certains contextes. Or, une
connexion déjà active maintenue dans un pool peut être attribuée à une nouvelle session
immédiatement : cette latence est donc de facto fortement limitée par le pooler.

En fonction du mode de fonctionnement, de la configuration et du type de pooler choisi,


sa transparence vis-à-vis de l’application et son impact sur les performances seront dif-
férents.

De plus, cette position privilégiée entre les utilisateurs et le SGBD permet au pooler de
contrôler et centraliser les connexions vers le ou les SGBD. Effectivement, les applications
pointant sur le serveur de pool de connexions, le SGBD peut être situé n’importe où, voire
sur plusieurs serveurs différents. Le pooler peut aiguiller les connexions vers un serveur
différent en fonction de la base de données demandée. Certains poolers peuvent détecter
une panne d’un serveur et aiguiller vers un autre. En cas de switchover, failover, évolution
ou déplacement du SGBD, il peut suffire de reconfigurer le pooler.

Enfin, les sessions entrantes peuvent être mises en attente si plus aucune connexion n’est
disponible et qu’elles ne peuvent pas en créer de nouvelle. On évite donc de lever immé-
diatement une erreur, ce qui est le comportement par défaut de PostgreSQL.

Pour la base de données, le pooler est une application comme une autre.

Si la configuration le permet (pg_hba.conf), il est possible de se connecter à une instance


aussi bien via le pooler que directement selon l’utilisation (application, batch, administra-
tion…)

17.2.4 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

Les fonctionnalités de PostgreSQL utilisables au travers d’un pooler varient suivant son

676
17. POOLING

mode de fonctionnement du pooler (par requêtes, transactions ou sessions). Nous ver-


rons que plus la mutualisation est importante, plus les restrictions apparaissent.

Un pooler est un élément en plus entre l’application et vos données, donc il aura un coût
en performances. Il ajoute notamment une certaine latence. On n’introduit donc pas un
pooler sans avoir identifié un problème. Si la configuration est bien faite, cet impact est
normalement négligeable, ou en tout cas sera compensé par des gains au niveau de la
base de données, ou en administration.

Comme dans tout système de proxy, un des points délicats de la configuration est
l’authentification, avec certaines restrictions.

Un pooler est un élément en plus dans votre architecture. Il la rend donc plus complexe
et y ajoute ses propres besoins en administration, supervision et ses propres modes de
défaillance. Si vous faites passer toutes vos connexions par un pooler, celui-ci devient un
nouveau point de défaillance possible (SPOF). Une redondance est bien sûr possible mais
complique à nouveau les choses.

677
https://dalibo.com/formations
SQL pour PostgreSQL

17.3 POOLING DE SESSIONS

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

Un pool de connexion par session attribue une connexion au SGBD à un unique utilisateur
pendant toute la durée de sa session. Si aucune connexion à PostgreSQL n’est disponible,
une nouvelle connexion est alors créée, dans la limite exprimée dans la configuration du
pooler. Si cette limite est atteinte, la session est mise en attente ou une erreur est levée.

17.3.1 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 active au pooler égal au nombre de connexions actives
au SGBD

L’intérêt d’un pool de connexion en mode session est principalement de conserver les
connexions ouvertes vers le SGBD. On économise ainsi le temps d’établissement de la
connexion pour les nouvelles sessions entrantes si une connexion est déjà disponible.
Dans ce cas, le pooler permet d’avoir un comportement de type pre-fork côté SGBD.

678
17. POOLING

L’autre intérêt est de ne pas rejeter une connexion, même s’il n’y a plus de connexions
possibles au SGBD. Contrairement au comportement de PostgreSQL, les connexions sont
placées en attente si elles ne peuvent pas être satisfaites immédiatement.

Ce mode de fonctionnement est très simple et robuste, c’est le plus transparent vis-à-vis
des sessions clientes, avec un impact quasi nul sur le code applicatif.

Aucune optimisation du temps de travail côté SGBD n’est donc possible. S’il peut être
intéressant de limiter le nombre de sessions ouvertes sur le pooler, il sera en revanche
impossible d’avoir plus de sessions ouvertes sur le pooler que de connexions disponibles
sur le SGDB.

679
https://dalibo.com/formations
SQL pour PostgreSQL

17.4 POOLING DE TRANSACTIONS

Multiplexe les transactions des utilisateurs sur une ou plusieurs connexions.

Dans le schéma présenté ici, chaque bloc représente une transaction délimitée par une
instruction BEGIN, suivie plus tard d’un COMMIT ou d’un ROLLBACK. Chaque zone colorée
représente une requête au sein de la transaction.

Un pool de connexions par transactions multiplexe les transactions des utilisateurs en-
tre une ou plusieurs connexions au SGBD. Une transaction est débutée sur la première
connexion à la base qui soit inactive (idle). Toutes les requêtes d’une transaction sont
envoyées sur la même connexion.

Ce schéma suppose que le pool accorde la première connexion disponible en partant du


haut dans l’ordre où les transactions se présentent.

17.4.1 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
– interdit les requêtes préparées

680
17. POOLING

– période de non activité des sessions toujours possible

Les intérêts d’un pool de connexion en mode transaction sont multiples en plus de cu-
muler ceux d’un pool de connexion par session.

Il est désormais possible de partager une même connexion au SGBD entre plusieurs ses-
sions utilisateurs. En effet, il existe de nombreux contextes où une session a un taux
d’occupation relativement faible : requêtes très simples et exécutées très rapidement,
génération des requêtes globalement plus lente que la base de données, couche applica-
tive avec des temps de traitement des données reçues plus importants que l’exécution
côté SGBD, etc.

Avoir la capacité de multiplexer les transactions de plusieurs sessions entre plusieurs con-
nexions permet ainsi de limiter le nombre de connexions à la base en optimisant leur taux
d’occupation. Cet économie de connexions côté SGBD a plusieurs avantages :

• moins de connexions à gérer par le serveur, qui est donc plus disponible pour les
connexions actives ;
• moins de connexions, donc économie de mémoire, devenue disponible pour les re-
quêtes ;
• possibilité d’avoir un plus grand nombre de clients connectés côté pooler sans pour
autant atteindre un nombre critique de connexions côté SGBD.

En revanche, avec ce mode de fonctionnement, le pool de connexions n’assure pas aux


client connectés que leurs requêtes et transactions iront toujours vers la même connexion,
bien au contraire ! Ainsi, si l’application utilise des requêtes préparées (c’est-à-dire en
trois phases PREPARE, BIND, EXECUTE), la commande PREPARE pourrait être envoyée sur
une connexion alors que les commandes EXECUTE pourraient être dirigées vers d’autres
connexions, menant leur exécution tout droit à une erreur.

Seules les requêtes au sein d’une même transaction sont assurées d’être exécutées sur la
même connexion. Ainsi, au début de cette transaction, la connexion est alors réservée
exclusivement à l’utilisateur propriétaire de la transaction. Même si ce dernier est inactif
dans sa transaction (idle in transaction), la connexion reste malgré tout inaccessible
aux autres sessions.

Ce type de pool de connexion a donc un impact non négligeable à prendre en compte lors
du développement.

681
https://dalibo.com/formations
SQL pour PostgreSQL

17.5 POOLING DE REQUÊTES

• Un pool de connexions en mode requêtes multiplexe toutes les requêtes sur une
ou plusieurs connexions

Un pool de connexions par requêtes multiplexe les requêtes des utilisateurs entre une ou
plusieurs connexions au SGBD.

Dans le schéma présenté ici, chaque bloc coloré représente une requête. Elles sont
placées exactement aux mêmes instants que dans le schéma présentant le pool de con-
nexion en mode transactions.

17.5.1 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 !

Les intérêts d’un pool de connexions en mode requêtes sont les mêmes que pour un pool
de connexion en mode de transactions. Cependant, dans ce mode, toutes les requêtes

682
17. POOLING

des clients sont multiplexées à travers les différentes connexions disponibles et inactives.

Ainsi, il est désormais possible d’optimiser encore plus le temps de travail des connex-
ions au SGBD, supprimant la possibilité de bloquer une connexion dans un état idle in
transaction. Nous sommes alors capable de partager une même connexion avec en-
core plus de clients, augmentant ainsi le nombre de sessions disponibles sur le pool de
connexions tout en conservant un nombre limité de connexions côté SGBD.

En revanche, si les avantages sont les mêmes que ceux d’un pooler de connexion en mode
transactions, les limitations sont elles aussi plus importantes. Il n’est effectivement plus
possible d’utiliser des transactions, en plus des requêtes préparées !

683
https://dalibo.com/formations
SQL pour PostgreSQL

17.6 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

Deux projets sous licence BSD coexistent dans l’écosystème de PostgreSQL pour mettre
en œuvre un pool de connexion : PgBouncer et PgPool-II.

PgPool-II167 est le projet le plus ancien, développé et maintenu principalement par SRA
OSS168 . Ce projet est un véritable couteau suisse capable d’effectuer bien plus que du
pooling (répartition de charge, bascules…). Malheureusement, cette polyvalence a un
coût important en terme de fonctionnalités et complexités. PgPool n’est effectivement
capable de travailler qu’en tant que pool de connexion par session.

PgBouncer169 est un projet créé par Skype. Il a pour objectifs :

• de n’agir qu’en tant que pool de connexion ;


• d’être le plus léger possible ;
• d’avoir les meilleures performances possibles ;
• d’avoir le plus de fonctionnalités possibles sur son cœur de métier.

PgBouncer étant le plus évolué des deux, nous allons le mettre en œuvre dans les pages
suivantes.

17.6.1 PGBOUNCER : FONCTIONNALITÉS

• Techniquement : un démon
• Disponible sous Unix & Windows
• Modes sessions / transactions / requêtes
• 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
167
https://www.pgpool.net/
168
https://www.sraoss.co.jp/index_en.php
169
https://www.pgbouncer.org/

684
17. POOLING

PgBouncer est techniquement assez simple : il s’agit d’un simple démon, auxquelles les ap-
plicatifs se connectent (en croyant avoir affaire à PostgreSQL), et qui retransmet requêtes
et données.

PgBouncer dispose de nombreuses fonctionnalités, toutes liées au pooling de connexions.


La majorité de ces fonctionnalités ne sont pas disponibles avec PgPool.

À l’inverse de ce dernier, PgBouncer n’offre pas de répartition de charge. Ses créateurs


renvoient vers des outils au niveau TCP comme HAProxy. De même, pour les bascules
d’un serveur à l’autre, ils conseillent plutôt de s’appuyer sur le niveau DNS.

Ce qui suit n’est qu’un extrait de la documentation de référence, assez courte : https:
//www.pgbouncer.org/config.html. La FAQ170 est également à lire.

17.6.2 PGBOUNCER : INSTALLATION

• Par les paquets fournis par le PGDG :


– yum|dnf install pgbouncer
– apt install pgbouncer
• Installation par les sources
– Dépôt pgbouncera

PgBouncer est disponible sous la forme d’un paquet binaire sur les principales distribu-
tions Linux et les dépôts du PGDG. Il est possible de recompiler depuis les sources.

Sous Windows, le projet fournit une archive171 à décompresser.

17.6.3 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]

Les paquets binaires créent un fichier de configuration /etc/pgbouncer/pgbouncer.ini.

Une ligne de configuration concerne un seul paramètre, avec le format suivant :


170
https://www.pgbouncer.org/faq.html
a
http://www.pgbouncer.org/install.html
171
https://github.com/pgbouncer/pgbouncer/releases/

685
https://dalibo.com/formations
SQL pour PostgreSQL

parametre = valeur

PgBouncer n’accepte pas que l’utilisateur spécifie une unité pour les valeurs. L’unité prise
en compte par défaut est la seconde.

Il y a plusieurs sections :

• les bases de données ([databases]), où on spécifie pour chaque base la chaîne de


connexion à utiliser ;
• les utilisateurs ([users]), pour des propriétés liées aux utilisateurs ;
• le moteur ([pgbouncer]), où se fait tout le reste de la configuration de PgBouncer.

17.6.4 PGBOUNCER : CONNEXIONS

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

PgBouncer accepte les connexions en mode socket Unix et via TCP/IP. Les paramètres
disponibles ressemblent beaucoup à ce que PostgreSQL propose.

listen_addr correspond aux interfaces réseaux sur lesquels PgBouncer va écouter. Il


est par défaut configuré à la boucle locale, mais vous pouvez ajouter les autres interfaces
disponibles, ou tout simplement * pour écouter sur toutes les interfaces. listen_port
précise le port de connexion : traditionnellement, c’est 6432, mais on peut le changer,
par exemple à 5432 pour que la configuration de connexion des clients reste identique.
Si PostgreSQL se trouve sur le même serveur et que vous voulez utiliser le port 5432
pour PgBouncer, il faudra bien sûr changer le port de connexion de PostgreSQL.

Pour une connexion uniquement en local par la socket Unix, il est possible d’indiquer
où le fichier socket doit être créé (paramètre unix_socket_dir : /tmp sur Red Hat/-
CentOS, /var/run/postgresql sur Debian et dérivés), quel groupe doit lui être affecté
(unix_socket_group) et les droits du fichier (unix_socket_mode). Si un groupe est in-
diqué, il est nécessaire que l’utilisateur détenteur du processus pgbouncer soit membre
de ce groupe.

Cela est pris en compte par les paquets binaires d’installation.

PgBouncer supporte également le chiffrement TLS.

686
17. POOLING

17.6.5 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, connect_query
– client_encoding, datestyle, timezone…
• Base par défaut :
* = host=ip1 port=5432 dbname=data0
• auth_hba_file : équivalent à pg_hba.conf

Lorsque l’utilisateur cherche à se connecter à PostgreSQL, il va indiquer l’adresse IP du


serveur où est installé PgBouncer et le numéro de port où écoute PgBouncer. Il va aussi
indiquer d’autres informations comme la base qu’il veut utiliser, le nom d’utilisateur pour
la connexion, son mot de passe, etc.

Lorsque PgBouncer reçoit cette requête de connexion, il extrait le nom de la base et va


chercher dans la section [databases] si cette base de données est indiquée. Si oui, il rem-
placera tous les paramètres de connexion qu’il trouve dans son fichier de configuration
et établira la connexion entre ce client et cette base. Si jamais la base n’est pas indiquée,
il cherchera s’il existe une base de connexion par défaut (nom *) et l’utilisera dans ce cas.

Exemples de chaîne de connexion :

prod = host=p1 port=5432 dbname=erp pool_size=40 pool_mode=transaction


prod = host=p1 port=5432 dbname=erp pool_size=10 pool_mode=session

Il est donc possible de faire beaucoup de chose :

• n’accéder qu’à un serveur dont les bases sont décrites ;


• accéder à différents serveurs PostgreSQL depuis un même serveur de pooling, suiv-
ant le nom de la base ou de l’utilisateur ;
• remplacer l’utilisateur de connexion ;
• etc.

Néanmoins, les variables user et password sont très peu utilisées.


La chaîne de connexion est du type libpq mais tout ce qu’accepte la libpq n’est pas for-
cément accepté par PgBouncer (notamment pas de variable service, pas de possibilité
d’utiliser directement le fichier standard .pgpass).

On peut configurer un fichier auth_hba_file de même format que pg_hba.conf pour


filtrer les accès au niveau du pooler (en plus des bases).
687
https://dalibo.com/formations
SQL pour PostgreSQL

17.6.6 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 n’a pas accès à l’authentification de PostgreSQL. De plus, son rôle est de don-
ner accès à des connexions déjà ouvertes à des clients. PgBouncer doit donc s’authentifier
auprès de PostgreSQL à la place des clients, et vérifier lui-même les mots de passe de ces
clients. (Ce mécanisme ne dispense évidemment pas les clients de fournir les mots de
passe.)

La première méthode, et la plus simple, est de déclarer les utilisateurs dans le fichier
pointé par le paramètre auth_file, par défaut userlist.txt. Les utilisateurs et mots de
passe y sont stockés comme ci-dessous selon le type d’authentification, obligatoirement
encadrés avec des guillemets doubles.

"guillaume" "supersecret"
"marc" "md59fa7827a30a483125ca3b7218bad6fee"
"pgbench" "SCRAM-SHA-256$4096:Rqk+MWaDN9rKXOLuoj8eCw==$ry5DD2Ptk…+6do76FN/ys="

Le type d’authentification est plus limité que ce que PostgreSQL propose. Le type trust
indique que l’utilisateur sera accepté par PgBouncer quel que soit le mot de passe qu’il
fournit ; il faut que le serveur PostgreSQL soit configuré de la même façon. Cela est
bien sur déconseillé. auth_type peut prendre les valeurs md5 ou scram-sha-256 pour
autoriser des mots de passe chiffrés. Pour des raisons de compatibilité descendante, md5
permet aussi d’utiliser scram-sha-256.

Les paramètres de configuration admin_users et stats_users permettent d’indiquer la


liste d’utilisateurs pouvant se connecter à PgBouncer directement pour obtenir des com-
mandes de contrôle sur PgBouncer ainsi que des statistiques d’activité. Ils peuvent être
déclarés dans le fichier des mots de passe avec un mot de passe arbitraire en clair.

userlist.txt est évidemment un fichier dont les accès doivent être les plus restreints
possibles.

688
17. POOLING

17.6.7 PGBOUNCER : AUTHENTIFICATION PAR DÉLÉGATION

• Créer un rôle dédié


• Copier son hash de mot de passe (MD5) 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

La maintenance du fichier de mots de passe peut vite devenir fastidieuse. Il est possible
de déléguer un rôle à la recherche des mots de passe avec le paramètre auth_user (à
poser globalement ou au niveau de la base).
prod = host=p1 port=5432 dbname=erp pool_mode=transaction auth_user=frontend

Ce rôle se connectera et ira valider dans l’instance le hash du mot de passe du client. Il
sera donc inutile de déclarer d’autres rôles dans userlist.txt.

Il n’y aura pas de problème avec l’authentification MD5. Par contre, le principe même de
SCRAM-SHA-256 interdit de passer par un proxy. Le mot de passe de l’utilisateur dédié
devra donc forcément être encodé en MD5.

Exemple de configuration :
SET password_encryption = 'md5' ;
CREATE ROLE frontend PASSWORD 'pass' LOGIN ;
SELECT rolpassword FROM pg_authid WHERE rolname = 'frontend' \gx

Le hachage obtenu (ici en MD5) est recopié dans userlist.txt :


"frontend" "md5b935ea59a93354a09864a11ff102b548"

Le paramètre auth_query définit la requête à exécuter pour ensuite comparer les résul-
tats avec les identifiants de connexion. Par défaut, il s’agit simplement de requêter la vue
pg_shadow :
auth_query = SELECT usename, passwd FROM pg_shadow WHERE usename=$1

D’autres variantes sont possibles, comme une requête plus élaborée sur pg_authid, ou
une fonction avec les bons droits de consultation avec une clause SECURITY DEFINER (la
documentation donne un exemple172 ). Il faut évidemment que l’utilisateur choisi ait les
droits nécessaires, et cela dans toutes les bases impliquées. La mise en place de cette con-
figuration est facilement source d’erreur, il faut bien surveiller les traces de PostgreSQL
et PgBouncer.
172
http://www.pgbouncer.org/config.html#example

689
https://dalibo.com/formations
SQL pour PostgreSQL

17.6.8 PGBOUNCER : POOLER

• Mode du multiplexage
– pool_mode (session)
• Nombre de connexions
– max_client_conn (100)
– default_pool_size (20)
– min_pool_size (0)
– reserve_pool_size (0)
• À la connexion
– ignore_startup_parameter (attention à PGOPTIONS)
• À la déconnexion
– server_reset_query

PgBouncer accepte les différents modes de pooling : à la session, à la transaction et à la


requête. Par défaut, il est en mode session mais il est possible de changer le mode avec
le paramètre pool_mode dans la chaîne de connexion de la base.

PostgreSQL dispose d’un nombre de connexions maximum, max_connections. Celui-ci


est un compromis entre le nombre de requêtes simultanément actives, leur complexité,
le nombre de CPU, le nombre de processus gérables par l’OS… L’utilisation d’un pooler
en multiplexage se justifie notamment quand des centaines, voire milliers, de connexions
simultanées sont nécessaires, celles-ci étant inactives la plus grande partie du temps.

Le paramètre de configuration max_client_conn permet d’indiquer le nombre maximum


de connexions clients à PgBouncer. Sa valeur par défaut est de 100, tout comme le
max_connections de PostgreSQL. Si vous essayez de dépasser ce nombre, vous obtien-
drez le message suivant :
ERROR: no more connections allowed

default_pool_size est le nombre maximum de connexions PgBouncer/PostgreSQL


pour un pool (un pool étant un couple utilisateur/base de données côté PgBouncer).
Noter qu’il est possible de le personnaliser base par base dans la section [databases]
avec pool_size.

S’il y a trop de demandes de connexion pour le pool, les transactions sont mises en
attente. Ce peut être ce que l’on veut pour ne pas trop charger le serveur, mais de-
venir intolérable pour l’application. Une « réserve » de connexions peut être définie avec
reserve_pool_size : ces connexions sont utilisables dans une situation grave, c’est-à-
dire si des connexions se retrouvent à attendre plus de reserve_pool_timeout secondes.

690
17. POOLING

À l’inverse, pour faciliter les montées en charge rapides, min_pool_size définit un nom-
bre de connexions qui seront immédiatement ouvertes dès que le pool voit sa première
connexion.

PgBouncer utilise des descripteurs de fichiers pour les connexions. Le nombre de de-
scripteurs peut être bien plus important que ce que n’autorise par défaut le système
d’exploitation. Le maximum théorique est de :
max_client_conn + (max_pool_size * nombre de bases * nombre d’utilisateurs)

Le cas échéant il faudra reconfigurer le nombre de descripteurs disponibles sur un système


d’exploitation Linux, ou il faudra le faire au niveau du service PgBouncer.

Lorsqu’un client se connecte, il peut utiliser des paramètres de connexion que PgBouncer
ne connaît pas ou ne sait pas gérer. Si PgBouncer détecte un paramètre de connexion
qu’il ne connaît pas, il rejette purement et simplement la connexion. Le paramètre
ignore_startup_parameters permet de changer ce comportement, d’ignorer le
paramètre et de procéder à la connexion. Par exemple, une variable d’environnement
PGOPTIONS interdit la connexion depuis psql, il faudra donc définir :

ignore_startup_parameters = options

ce qui malheureusement réduit à néant l’intérêt de $PGOPTIONS.

En fin de connexion du client, comme la connexion vers PostgreSQL peut être réu-
tilisée par un autre client, il est nécessaire de réinitialiser la session : enlever la
configuration de session, supprimer les tables temporaires, supprimer les curseurs, etc.
Pour cela, PgBouncer exécute une liste de requêtes configurables avec le paramètre
server_reset_query. Par défaut il s’agit de DISCARD ALL, ce qui suffira généralement,
mais vous pouvez ajouter d’autres requêtes au besoin.

17.6.9 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
691
https://dalibo.com/formations
SQL pour PostgreSQL

• D’une requête
– query_timeout = 0

PgBouncer dispose d’un grand nombre de paramètres de durée de vie. Ils permettent
d’éviter de conserver des connexions trop longues, notamment si elles sont inactives.
C’est un avantage sur PostgreSQL qui ne dispose pas de ce type de paramétrage. Les
paramètres en client_* concernent les connexions entre le client et PgBouncer, ceux
en server_* concernent les connexions entre PgBouncer et PostgreSQL.

Il faut faire bien attention à ces configurations. Par exemple, activer client_idle_timeout
(pour couper les connexions inactives) peut couper brutalement la connexion à une
application cliente qui ne s’y attend pas.

17.6.10 PGBOUNCER : TRACES

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

PgBouncer dispose de quelques options de configuration pour les traces.

Le paramètre logfile indique l’emplacement (par défaut /var/log/pgbouncer sur


Red Hat/CentOS, /var/log/postgres sur Debian et dérivés). Mais on peut rediriger
vers syslog.

Ensuite, il est possible de configurer les événements tracés, notamment les connexions
(avec log_connections) et les déconnexions (avec log_disconnections).

Par défaut, log_stats est activé : PgBouncer trace alors les statistiques sur les dernières
60 secondes (paramètresstats_period).
2020-11-30 19:10:07.839 CET [290804] LOG stats: 54 xacts/s, 380 queries/s,
in 23993 B/s, out 10128 B/s, xact 304456 us, query 43274 us, wait 14685821 us

692
17. POOLING

17.6.11 PGBOUNCER : ADMINISTRATION

• Pseudo-base pgbouncer
• Administration
– RELOAD, PAUSE, SUSPEND, RESUME, SHUTDOWN
• Supervision
– SHOW CONFIG|DATABASES|POOLS|CLIENTS|...
– ...|SERVERS|STATS|FDS|SOCKETS|...
– ...|ACTIVE_SOCKETS|LISTS|MEM

Si on essaie de se connecter via PgBouncer à une base de données pgbouncer, PgBouncer


n’établit aucune connexion à une base de données PostgreSQL, mais peut répondre à
quelques ordres d’administration. Il faut pour cela se connecter avec un utilisateur au-
torisé (déclaration par les paramètres admin_users et stats_users).

Les utilisateurs « administrateurs » ont le droit d’exécuter des instructions de con-


trôle, comme recharger la configuration (RELOAD), mettre le système en pause (PAUSE),
supprimer la pause (RESUME), forcer une déconnexion/reconnexion dès que possible
(RECONNECT, le plus propre en cas de modification de configuration), tuer toutes les
sessions d’une base (KILL), arrêter PgBouncer (SHUTDOWN), etc.

Les utilisateurs statistiques peuvent récupérer des informations sur l’activité de Pg-
Bouncer : statistiques sur les bases, les pools de connexions, les clients, les serveurs, etc.
avec SHOW STATS, SHOW STATS_AVERAGE, SHOW TOTALS, SHOW MEM, etc.

Toutes ces informations sont utilisées notamment par la sonde Nagios check_postgres173
pour permettre une supervision de cet outil.

Pour vous connecter à cette pseudo-base, vous pouvez passer par l’utilisateur pgbouncer.
# sudo -iu pgbouncer psql -h /var/run/postgresql -p 6432 pgbouncer
psql (13.1 (Ubuntu 13.1-1.pgdg20.04+1), serveur 1.14.0/bouncer)

pgbouncer=# SHOW help ;


NOTICE: Console usage
DÉTAIL :
SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION
SHOW FDS|SOCKETS|ACTIVE_SOCKETS|LISTS|MEM
SHOW DNS_HOSTS|DNS_ZONES
SHOW STATS|STATS_TOTALS|STATS_AVERAGES|TOTALS
SET key = arg
RELOAD
PAUSE [<db>]
RESUME [<db>]
173
https://github.com/bucardo/check_postgres

693
https://dalibo.com/formations
SQL pour PostgreSQL

DISABLE <db>
ENABLE <db>
RECONNECT [<db>]
KILL <db>
SUSPEND
SHUTDOWN

pgbouncer=# SHOW DATABASES \gx


-[ RECORD 1 ]-------+--------------------------------------
name | pgbench_1000_sur_server3
host | 192.168.74.5
port | 13002
database | pgbench_1000
force_user |
pool_size | 10
reserve_pool | 7
pool_mode | session
max_connections | 0
current_connections | 17
paused | 0
disabled | 0
-[ RECORD 2 ]-------+--------------------------------------

pgbouncer=# SHOW POOLS \gx


-[ RECORD 1 ]-------------------------------------
database | pgbench_1000_sur_server3
user | pgbench
cl_active | 10
cl_waiting | 80
sv_active | 10
sv_idle | 0
sv_used | 0
sv_tested | 0
sv_login | 0
maxwait | 0
maxwait_us | 835428
pool_mode | session
-[ RECORD 2 ]-------------------------------------
database | pgbouncer
user | pgbouncer
cl_active | 1
cl_waiting | 0
sv_active | 0
sv_idle | 0
sv_used | 0
sv_tested | 0

694
17. POOLING

sv_login | 0
maxwait | 0
maxwait_us | 0
pool_mode | statement

pgbouncer=# SHOW STATS \gx


-[ RECORD 1 ]-----+-------------------------
database | pgbench_1000_sur_server3
total_xact_count | 16444
total_query_count | 109711
total_received | 6862181
total_sent | 3041536
total_xact_time | 8885633095
total_query_time | 8873756132
total_wait_time | 14123238083
avg_xact_count | 103
avg_query_count | 667
avg_recv | 41542
avg_sent | 17673
avg_xact_time | 97189
avg_query_time | 14894
avg_wait_time | 64038262
-[ RECORD 2 ]-----+-------------------------
database | pgbouncer
total_xact_count | 1
total_query_count | 1
total_received | 0
total_sent | 0
total_xact_time | 0
total_query_time | 0
total_wait_time | 0
avg_xact_count | 0
avg_query_count | 0
avg_recv | 0
avg_sent | 0
avg_xact_time | 0
avg_query_time | 0
avg_wait_time | 0

pgbouncer=# SHOW MEM ;


name | size | used | free | memtotal
--------------+------+------+------+----------
user_cache | 360 | 11 | 39 | 18000
db_cache | 208 | 5 | 73 | 16224
pool_cache | 480 | 2 | 48 | 24000

695
https://dalibo.com/formations
SQL pour PostgreSQL

server_cache | 560 | 17 | 33 | 28000


client_cache | 560 | 91 | 1509 | 896000
iobuf_cache | 4112 | 74 | 1526 | 6579200

17.7 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é

17.7.1 QUESTIONS

SELECT * FROM questions ;

696
17. POOLING

17.8 TRAVAUX PRATIQUES

Créer un rôle PostgreSQL nommé pooler avec un mot de passe.

Pour mieux suivre les traces, activer log_connections et


log_disconnections, et passer log_min_duration_statement
à 0.

Installer PgBouncer.
Configurer /etc/pgbouncer/pgbouncer.ini pour pouvoir se
connecter à n’importe quelle base du serveur via PgBouncer
(port 6432).
Ajouter pooler dans /etc/pgbouncer/userlist.txt.
L’authentification doit être md5.
Ne pas oublier pg_hba.conf.
Suivre le contenu de /var/log/pgbouncer/pgbouncer.log.
Se connecter par l’intermédiaire du pooler sur une base locale.

Activer l’accès à la pseudo-base pgbouncer pour les utilisateurs


postgres et pooler.
Laisser la session ouverte pour suivre les connexions en cours.

17.8.1 POOLING PAR SESSION

Ouvrir deux connexions sur le pooler. Combien de connexions


sont-elles ouvertes côté serveur ?

17.8.2 POOLING PAR TRANSACTION

Passer PgBouncer en pooling par transaction. Bien vérifier qu’il


n’y a plus de connexions ouvertes.

697
https://dalibo.com/formations
SQL pour PostgreSQL

Rouvrir deux connexions via PgBouncer.


Cette fois, combien de connexions sont ouvertes côté serveur ?

Successivement et à chaque fois dans une transaction, créer une


table dans une des sessions ouvertes, puis dans l’autre insérer
des données. Suivre le nombre de connexions ouvertes.
Recommencer avec des transactions simultanées.

17.8.3 POOLING PAR REQUÊTE

Passer le pooler en mode pooling par requête et tenter d’ouvrir


une transaction.

Repasser PgBouncer en pooling par session.

17.8.4 PGBENCH

Créer une base nommée bench appartenant à pooler.


Avec pgbench, l’initialiser avec un scale factor de 100.

Lancer des tests (lectures uniquement, avec --select) de 60 sec-


ondes avec 80 connexions :
une fois sur le pooler, et une fois directement sur le serveur. Com-
parer les performances.

Refaire ce test en demandant d’ouvrir et fermer les connexions


(-C), sur le serveur puis sur le pooler. Effectuer un SHOW POOLS
pendant ce dernier test.

698
17. POOLING

17.9 TRAVAUX PRATIQUES (SOLUTIONS)

Créer un rôle PostgreSQL nommé pooler avec un mot de passe.

Les connexions se feront avec l’utilisateur pooler que nous allons créer avec le (trop évi-
dent) mot de passe « pooler » :
$ createuser --login --pwprompt --echo pooler
Saisir le mot de passe pour le nouveau rôle :
Le saisir de nouveau :

CREATE ROLE pooler PASSWORD 'md52a1394e4bcb2e9370746790c13ac33ac'
NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN;

(NB : le hash sera beaucoup plus complexe si le chiffrage SCRAM-SHA-256 est activé,
mais cela ne change rien au principe.)

Pour mieux suivre les traces, activer log_connections et


log_disconnections, et passer log_min_duration_statement
à 0.

PostgreSQL trace les rejets de connexion, mais, dans notre cas, il est intéressant de suivre
aussi les connexions abouties.

Dans postgresql.conf :
log_connections = on
log_disconnections = on
log_min_duration_statement = 0

Puis on recharge la configuration :


sudo systemctl reload postgresql-13

En cas de problème, le suivi des connexions dans /var/lib/pgsql/13/data/log peut


être très pratique.

Installer PgBouncer.
Configurer /etc/pgbouncer/pgbouncer.ini pour pouvoir se
connecter à n’importe quelle base du serveur via PgBouncer
(port 6432).
Ajouter pooler dans /etc/pgbouncer/userlist.txt.
L’authentification doit être md5.
Ne pas oublier pg_hba.conf.

699
https://dalibo.com/formations
SQL pour PostgreSQL

Suivre le contenu de /var/log/pgbouncer/pgbouncer.log.


Se connecter par l’intermédiaire du pooler sur une base locale.

L’installation est simple :


sudo yum install pgbouncer

La configuration se fait dans /etc/pgbouncer/pgbouncer.ini.

Dans la section [databases] on spécifie la chaîne de connexion à l’instance, pour toute


base :
* = host=127.0.0.1 port=5432

Il faut ajouter l’utilisateur au fichier /etc/pgbouncer/userlist.txt. La syntaxe est


de la forme "user" "hachage du mot de passe". La commande createuser l’a ren-
voyé ci-dessus, mais généralement il faudra aller interroger la vue pg_shadow ou la table
pg_authid de l’instance PostgreSQL :
SELECT usename,passwd FROM pg_shadow WHERE usename = 'pooler';

usename | passwd
---------+-------------------------------------
pooler | md52a1394e4bcb2e9370746790c13ac33ac

Le fichier /etc/pgbouncer/userlist.txt contiendra donc :


"pooler" "md52a1394e4bcb2e9370746790c13ac33ac"

Il vaut mieux que seul l’utilisateur système dédié (pgbouncer sur Red Hat/CentOS) voit
ce fichier :
sudo chown pgbouncer: userlist.txt

De plus il faut préciser dans pgbouncer.ini que nous fournissons des mots de passe
hachés :
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

Si ce n’est pas déjà possible, il faut autoriser l’accès de pooler en local à l’instance Post-
greSQL. Du point de vue de PostgreSQL, les connexions se feront depuis 127.0.0.1 (IP du
pooler). Ajouter cette ligne dans le fichier pg_hba.conf et recharger la configuration de
l’instance :
host all pooler 127.0.0.1/32 md5

sudo systemctl reload postgresql-13

Enfin, on peut démarrer le pooler :


sudo systemctl restart pgbouncer

700
17. POOLING

Dans une autre session, on peut suivre les tentatives de connexion :


sudo tail -f /var/log/pgbouncer/pgbouncer.log

La connexion directement au pooler doit fonctionner :

psql -h 127.0.0.1 -p 6432 -U pooler -d postgres


Mot de passe pour l'utilisateur pooler :
psql (13.0)
Saisissez « help » pour l'aide.

postgres=>

Dans pgbouncer.log :
2020-12-02 08:42:35.917 UTC [2208] LOG C-0x152a490: postgres/pooler@127.0.0.1:55096
login attempt: db=postgres user=pooler tls=no

Noter qu’en cas d’erreur de mot de passe, l’échec apparaîtra dans ce dernier fichier, et
pas dans postgresql.log.

Activer l’accès à la pseudo-base pgbouncer pour les utilisateurs


postgres et pooler.
Laisser la session ouverte pour suivre les connexions en cours.

; comma-separated list of users, who are allowed to change settings


admin_users = postgres,pooler

; comma-separated list of users who are just allowed to use SHOW command
stats_users = stats, postgres,pooler

sudo systemctl reload pgbouncer

$ psql -h 127.0.0.1 -p6432 -U pooler -d pgbouncer


Mot de passe pour l'utilisateur pooler :
psql (13.0, serveur 1.15.0/bouncer)
Saisissez « help » pour l'aide.

pgbouncer=# SHOW HELP ;


NOTICE: Console usage
DÉTAIL :
SHOW HELP|CONFIG|DATABASES|POOLS|CLIENTS|SERVERS|USERS|VERSION

Si une connexion via PgBouncer est ouverte par ailleurs, on la retrouve ici :
pgbouncer=# SHOW POOLS \gx
-[ RECORD 1 ]---------
database | pgbouncer
701
https://dalibo.com/formations
SQL pour PostgreSQL

user | pgbouncer
cl_active | 1
cl_waiting | 0
sv_active | 0
sv_idle | 0
sv_used | 0
sv_tested | 0
sv_login | 0
maxwait | 0
maxwait_us | 0
pool_mode | statement
-[ RECORD 2 ]---------
database | postgres
user | pooler
cl_active | 1
cl_waiting | 0
sv_active | 1
sv_idle | 0
sv_used | 0
sv_tested | 0
sv_login | 0
maxwait | 0
maxwait_us | 0
pool_mode | session

17.9.1 POOLING PAR SESSION

Ouvrir deux connexions sur le pooler. Combien de connexions


sont-elles ouvertes côté serveur ?

Le pooling par session est le mode par défaut de PgBouncer.

On se connecte dans 2 sessions différentes :


$ psql -h 127.0.0.1 -p6432 -U pooler -d postgres
psql (13.0)

postgres=>

$ psql -h 127.0.0.1 -p6432 -U pooler -d postgres



SELECT COUNT(*) FROM pg_stat_activity
WHERE backend_type='client backend' AND usename='pooler' ;
count
-------
2

702
17. POOLING

Ici, PgBouncer a donc bien ouvert autant de connexions côté serveur que côté pooler.

17.9.2 POOLING PAR TRANSACTION

Passer PgBouncer en pooling par transaction. Bien vérifier qu’il


n’y a plus de connexions ouvertes.

Il faut changer le pool_mode dans pgbouncer.ini, soit globalement :


; When server connection is released back to pool:
; session - after client disconnects
; transaction - after transaction finishes
; statement - after statement finishes
pool_mode = transaction

soit dans la définition des connexions :


* = host=127.0.0.1 port=5432 pool_mode=transaction

En toute rigueur, il n’y a besoin que de recharger la configuration de PgBouncer, mais


il y a le problème des connexions ouvertes. Dans notre cas, nous pouvons forcer une
déconnexion brutale :
sudo systemct restart pgbouncer

Rouvrir deux connexions via PgBouncer.


Cette fois, combien de connexions sont ouvertes côté serveur ?

Après reconnexion de 2 sessions, la pseudo-base indique 2 connexions clientes, 1


serveur :
pgbouncer=# SHOW POOLS \gx

-[ RECORD 2 ]-----------
database | postgres
user | pooler
cl_active | 2
cl_waiting | 0
sv_active | 0
sv_idle | 0
sv_used | 1
sv_tested | 0
sv_login | 0
maxwait | 0
maxwait_us | 0
pool_mode | transaction
703
https://dalibo.com/formations
SQL pour PostgreSQL

Ce que l’on retrouve en demandant directement au serveur :


postgres=> SELECT COUNT(*) FROM pg_stat_activity
WHERE backend_type='client backend' AND usename='pooler' ;
count
-------
1

Successivement et à chaque fois dans une transaction, créer une


table dans une des sessions ouvertes, puis dans l’autre insérer
des données. Suivre le nombre de connexions ouvertes.
Recommencer avec des transactions simultanées.

Dans la première connexion ouvertes :


BEGIN ;
CREATE TABLE log (i timestamptz) ;
COMMIT ;

Dans la deuxième :
BEGIN ;
INSERT INTO log SELECT now() ;
END ;

On a bien toujours une seule connexion :


pgbouncer=# SHOW POOLS \gx

-[ RECORD 2 ]-----------
database | postgres
user | pooler
cl_active | 2
cl_waiting | 0
sv_active | 0
sv_idle | 0
sv_used | 1
sv_tested | 0
sv_login | 0
maxwait | 0
maxwait_us | 0
pool_mode | transaction

Du point de vue du serveur PostgreSQL, tout s’est passé dans la même session (même
PID) :
… 10:01:45.448 UTC [2841] LOG: duration: 0.025 ms statement: BEGIN ;
… 10:01:45.450 UTC [2841] LOG: duration: 0.631 ms statement: CREATE TABLE log (i timestamptz) ;
… 10:01:45.454 UTC [2841] LOG: duration: 4.037 ms statement: COMMIT ;

704
17. POOLING

… 10:01:49.128 UTC [2841] LOG: duration: 0.053 ms statement: BEGIN ;


… 10:01:49.129 UTC [2841] LOG: duration: 0.338 ms statement: INSERT INTO log SELECT now() ;
… 10:01:49.763 UTC [2841] LOG: duration: 4.393 ms statement: END ;

À présent, commençons la seconde transaction avant la fin de la première.

Session 1 :

BEGIN ; INSERT INTO log SELECT now() ;

Session 2 :

BEGIN ; INSERT INTO log SELECT now() ;

De manière transparente, une deuxième connexion au serveur a été créée :

pgbouncer=# show pools \gx



-[ RECORD 2 ]-----------
database | postgres
user | pooler
cl_active | 2
cl_waiting | 0
sv_active | 2
sv_idle | 0
sv_used | 0
sv_tested | 0
sv_login | 0
maxwait | 0
maxwait_us | 0
pool_mode | transaction

Ce que l’on voit dans les traces de PostgreSQL :

… 10:05:49.695 UTC [2841] LOG: duration: 0.144 ms statement: select 1


… 10:05:49.695 UTC [2841] LOG: duration: 0.014 ms statement: BEGIN ;
… 10:05:49.695 UTC [2841] LOG: duration: 0.110 ms statement: INSERT INTO log SELECT now() ;
… 10:05:52.320 UTC [2943] LOG: connection received: host=127.0.0.1 port=50554
… 10:05:52.321 UTC [2943] LOG: connection authorized: user=pooler database=postgres
… 10:05:52.323 UTC [2943] LOG: duration: 0.171 ms statement: SET application_name='psql';
… 10:05:52.323 UTC [2943] LOG: duration: 0.015 ms statement: BEGIN ;
… 10:05:52.324 UTC [2943] LOG: duration: 0.829 ms statement: INSERT INTO log SELECT now() ;

Du point de l’application, cela a été transparent.

Cette deuxième connexion va rester ouverte, mais elle n’est pas forcément associée à la
deuxième session. Cela peut se voir simplement ainsi en demandant le PID du backend
sur le serveur, qui sera le même dans les deux sessions :

postgres=> SELECT pg_backend_pid() ;


705
https://dalibo.com/formations
SQL pour PostgreSQL

pg_backend_pid
----------------
2841

17.9.3 POOLING PAR REQUÊTE

Passer le pooler en mode pooling par requête et tenter d’ouvrir


une transaction.

De la même manière que ci-dessus, soit :


pool_mode = statement

soit :
* = host=127.0.0.1 port=5432 pool_mode=statement

Redémarrage du pooler :
 # systemctl restart pgbouncer

Si on essaie de démarrer une transaction :


BEGIN;
ERROR: transaction blocks not allowed in statement pooling mode
la connexion au serveur a été coupée de façon inattendue
Le serveur s’est peut-être arrêté anormalement avant ou durant le
traitement de la requête.
La connexion au serveur a été perdue. Tentative de réinitialisation : Succès.

Le pooling par requête empêche l’utilisation de transactions.

Repasser PgBouncer en pooling par session.

Cela revient à revenir au mode par défaut (pool_mode=session).

706
17. POOLING

17.9.4 PGBENCH

Créer une base nommée bench appartenant à pooler.


Avec pgbench, l’initialiser avec un scale factor de 100.

Le pooler n’est pas configuré pour que postgres puisse s’y connecter, il faut donc se con-
necter directement à l’instance pour créer la base :
postgres$ createdb -h /var/run/postgresql -p 5432 --owner pooler bench

La suite peut passer par le pooler :


$ /usr/pgsql-13/bin/pgbench -i -s 100 -U pooler -h 127.0.0.1 -p 6432 bench
Password:
dropping old tables...
NOTICE: table "pgbench_accounts" does not exist, skipping
NOTICE: table "pgbench_branches" does not exist, skipping
NOTICE: table "pgbench_history" does not exist, skipping
NOTICE: table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
10000000 of 10000000 tuples (100%) done (elapsed 25.08 s, remaining 0.00 s)
vacuuming...
creating primary keys...
done in 196.24 s (drop tables 0.00 s, create tables 0.06 s, client-side generate 28.00 s,
vacuum 154.35 s, primary keys 13.83 s).

Lancer des tests (lectures uniquement, avec --select) de 60 sec-


ondes avec 80 connexions :
une fois sur le pooler, et une fois directement sur le serveur. Com-
parer les performances.

NB : Pour des résultats rigoureux, pgbench doit être utilisé sur une plus longue durée.

Sur le pooler, on lance :


$ /usr/pgsql-13/bin/pgbench \
--select -T 60 -c 80 -p 6432 -U pooler -h 127.0.0.1 -d bench 2>/dev/null
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 100
query mode: simple
number of clients: 80
number of threads: 1
duration: 60 s
707
https://dalibo.com/formations
SQL pour PostgreSQL

number of transactions actually processed: 209465


latency average = 22.961 ms
tps = 3484.222638 (including connections establishing)
tps = 3484.278500 (excluding connections establishing)

(Ces chiffres ont été obtenus sur un portable avec SSD.)

On recommence directement sur l’instance. (Si l’ordre échoue par saturation des connex-
ions, il faudra attendre que PgBouncer relâche les 20 connexions qu’il a gardées ouvertes.)

$ /usr/pgsql-13/bin/pgbench \
--select -T 60 -c 80 -p 5432 -U pooler -h 127.0.0.1 -d bench 2>/dev/null
starting vacuum...end.
transaction type: <builtin: select only>
scaling factor: 100
query mode: simple
number of clients: 80
number of threads: 1
duration: 60 s
number of transactions actually processed: 241482
latency average = 19.884 ms
tps = 4023.255058 (including connections establishing)
tps = 4023.573501 (excluding connections establishing)

Le test n’est pas assez rigoureux (surtout sur une petite machine de test) pour dire plus
que : les résultats sont voisins.

Refaire ce test en demandant d’ouvrir et fermer les connexions


(-C), sur le serveur puis sur le pooler. Effectuer un SHOW POOLS
pendant ce dernier test.

Sur le serveur :

$ /usr/pgsql-13/bin/pgbench \
-C --select -T 60 -c 80 -p 5432 -U pooler -h 127.0.0.1 -d bench 2>/dev/null
Password:
transaction type: <builtin: select only>
scaling factor: 100
query mode: simple
number of clients: 80
number of threads: 1
duration: 60 s
number of transactions actually processed: 9067
latency average = 529.654 ms
tps = 151.041956 (including connections establishing)
tps = 152.922609 (excluding connections establishing)

708
17. POOLING

On constate une division par 26 du débit de transactions : le coût des connexions/décon-


nexions est énorme.

Si on passe par le pooler :


$ /usr/pgsql-13/bin/pgbench \
-C --select -T 60 -c 80 -p 6432 -U pooler -h 127.0.0.1 -d bench 2>/dev/null
Password:
transaction type: <builtin: select only>
scaling factor: 100
query mode: simple
number of clients: 80
number of threads: 1
duration: 60 s
number of transactions actually processed: 49926
latency average = 96.183 ms
tps = 831.745556 (including connections establishing)
tps = 841.461561 (excluding connections establishing)

On ne retrouve pas les performances originales, mais le gain est tout de même d’un facteur
5, puisque les connexions existantes sur le serveur PostgreSQL sont réutilisées et n’ont
pas à être recréées.

Pendant ce dernier test, on peut consulter les connexions ouvertes : il n’y en que 20, pas
80. Noter le grand nombre de celles en attente.
pgbouncer=# SHOW POOLS \gx
-[ RECORD 1 ]---------
database | bench
user | pooler
cl_active | 20
cl_waiting | 54
sv_active | 20
sv_idle | 0
sv_used | 0
sv_tested | 0
sv_login | 0
maxwait | 0
maxwait_us | 73982
pool_mode | session

Ces tests n’ont pas pour objectif d’être représentatif mais juste de mettre en évidence le
coût d’ouverture/fermeture de connexion. Dans ce cas, le pooler peut apporter un gain
très significatif sur les performances.

709
https://dalibo.com/formations
NOTES
NOTES
NOTES
NOTES
NOS AUTRES PUBLICATIONS

FORMATIONS

• DBA1 : Administra on PostgreSQL


https://dali.bo/dba1

• DBA2 : Administra on PostgreSQL avancé


https://dali.bo/dba2

• DBA3 : Sauvegardes et réplica on avec PostgreSQL


https://dali.bo/dba3

• DEVPG : Développer avec PostgreSQL


https://dali.bo/devpg

• DEVSQLPG : SQL pour PostgreSQL


https://dali.bo/devsqlpg

• PERF1 : PostgreSQL Performances


https://dali.bo/perf1

• PERF2 : Indexa on et SQL avancé


https://dali.bo/perf2

• MIGORPG : Migrer d’Oracle vers PostgreSQL


https://dali.bo/migorpg

LIVRES BLANCS

• Migrer d’Oracle à PostgreSQL

• Industrialiser PostgreSQL

• Bonnes pra ques de modélisa on avec PostgreSQL

• Bonnes pra ques de développement avec PostgreSQL

TÉLÉCHARGEMENT GRATUIT

Les versions électroniques de nos publica ons sont disponibles gratuitement sous licence
open-source ou sous licence Crea ve Commons. Contactez-nous à l’adresse contact@
dalibo.com pour plus d’informa on.
DALIBO, L’EXPERTISE POSTGRESQL

Depuis 2005, DALIBO met à la disposi on de ses clients son savoir-faire dans le domaine
des bases de données et propose des services de conseil, de forma on et de support aux
entreprises et aux ins tu onnels.

En parallèle de son ac vité commerciale, DALIBO contribue aux développements de la


communauté PostgreSQL et par cipe ac vement à l’anima on de la communauté fran-
cophone de PostgreSQL. La société est également à l’origine de nombreux ou ls libres de
supervision, de migra on, de sauvegarde et d’op misa on.

Le succès de PostgreSQL démontre que la transparence, l’ouverture et l’auto-ges on sont


à la fois une source d’innova on et un gage de pérennité. DALIBO a intégré ces principes
dans son ADN en optant pour le statut de SCOP : la société est contrôlée à 100 % par
ses salariés, les décisions sont prises collec vement et les bénéfices sont partagés à parts
égales.

Vous aimerez peut-être aussi