Dalibo SCOP
Formation | Formation DEV0 |
Titre | Introduction à SQL |
Révision | 24.09 |
ISBN | N/A |
https://dali.bo/dev0_pdf | |
EPUB | https://dali.bo/dev0_epub |
HTML | https://dali.bo/dev0_html |
Slides | https://dali.bo/dev0_slides |
Vous trouverez en ligne les différentes versions complètes de ce document. La version imprimée ne contient pas les travaux pratiques. Ils sont présents dans la version numérique (PDF ou HTML).
Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :
Vous n’avez pas le droit d’utiliser cette création à des fins commerciales.
Si vous modifiez, transformez ou adaptez cette création, vous n’avez le droit de distribuer la création qui en résulte que sous un contrat identique à celui-ci.
Vous devez citer le nom de l’auteur original de la manière indiquée par l’auteur de l’œuvre ou le titulaire des droits qui vous confère cette autorisation (mais pas d’une manière qui suggérerait qu’ils vous soutiennent ou approuvent votre utilisation de l’œuvre). À chaque réutilisation ou distribution de cette création, vous devez faire apparaître clairement au public les conditions contractuelles de sa mise à disposition. La meilleure manière de les indiquer est un lien vers cette page web. Chacune de ces conditions peut être levée si vous obtenez l’autorisation du titulaire des droits sur cette œuvre. Rien dans ce contrat ne diminue ou ne restreint le droit moral de l’auteur ou des auteurs.
Le texte complet de la licence est disponible sur http://creativecommons.org/licenses/by-nc-sa/2.0/fr/legalcode
Cela inclut les diapositives, les manuels eux-mêmes et les travaux pratiques. Cette formation peut également contenir quelques images et schémas dont la redistribution est soumise à des licences différentes qui sont alors précisées.
PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.
Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 12 à 16.
Sur les versions précédentes susceptibles d’être encore rencontrées en production, seuls quelques points très importants sont évoqués, en plus éventuellement de quelques éléments historiques.
Sauf précision contraire, le système d’exploitation utilisé est Linux.
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.
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 System (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.
Au fil des années ont été développés plusieurs modèles de données, que nous allons décrire.
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 concevoir 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.
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épendant 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 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).
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. Certaines
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;
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 centralisé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 description 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.
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 enregistrements), 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).
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.
Les propriétés ACID sont le fondement même de toute bonne base de données. Il s’agit de l’acronyme des quatre règles que toute transaction (c’est-à-dire une suite d’ordres modifiant les données) doit respecter :
Les bases de données relationnelles les plus courantes depuis des décennies (PostgreSQL bien sûr, mais aussi Oracle, MySQL, SQL Server, SQLite…) se basent sur ces principes, même si elles font chacune des compromis différents suivant leurs cas d’usage, les compromis acceptés à chaque époque avec la performance et les versions.
Atomicité :
Une transaction doit être exécutée entièrement ou pas du tout, et surtout pas partiellement, même si elle est longue et complexe, même en cas d’incident majeur sur la base de données. L’exemple basique est une transaction bancaire : le montant d’un virement doit être sur un compte ou un autre, et en cas de problème ne pas disparaître ou apparaître en double. Ce principe garantit que les données modifiées par des transactions valides seront toujours visibles dans un état stable, et évite nombre de problèmes fonctionnels comme techniques.
Cohérence :
Un état cohérent respecte les règles de validité définies dans le modèle, c’est-à-dire les contraintes définies dans le modèle : types, plages de valeurs admissibles, unicité, liens entre tables (clés étrangères), etc. Le non-respect de ces règles par l’applicatif entraîne une erreur et un rejet de la transaction.
Isolation :
Des transactions simultanées doivent agir comme si elles étaient seules sur la base. Surtout, elles ne voient pas les données non validées des autres transactions. Ainsi une transaction peut travailler sur un état stable et fixe, et durer assez longtemps sans risque de gêner les autres transactions.
Il existe plusieurs « niveaux d’isolation » pour définir précisément le comportement en cas de lectures ou écritures simultanées sur les mêmes données et pour arbitrer avec les contraintes de performances ; le niveau le plus contraignant exige que tout se passe comme si toutes les transactions se déroulaient successivement.
Durabilité :
Une fois une transaction validée par le serveur (typiquement :
COMMIT
ne retourne pas d’erreur, ce qui valide la cohérence
et l’enregistrement physique), l’utilisateur doit avoir la garantie que
la donnée ne sera pas perdue ; du moins jusqu’à ce qu’il décide de la
modifier à nouveau. Cette garantie doit valoir même en cas d’événément
catastrophique : plantage de la base, perte d’un disque… C’est donc au
serveur de s’assurer autant que possible que les différents éléments
(disque, système d’exploitation…) ont bien rempli leur office. C’est à
l’humain d’arbitrer entre le niveau de criticité requis et les
contraintes de performances et de ressources adéquates (et fiables) à
fournir à la base de données.
NoSQL :
À l’inverse, les outils de la mouvance (« NoSQL », par exemple MongoDB ou Cassandra), ne fournissent pas les garanties ACID. C’est le cas de la plupart des bases non-relationnelles, qui reprennent le modèle BASE (Basically Available, Soft State, Eventually Consistent, soit succintement : disponibilité d’abord ; incohérence possible entre les réplicas ; cohérence… à terme, après un délai). Un intérêt est de débarasser le développeur de certaines lourdeurs apparentes liées à la modélisation assez stricte d’une base de données relationnelle. Cependant, la plupart des applications ont d’abord besoin des garanties de sécurité et cohérence qu’offrent un moteur transactionnel classique, et la décision d’utiliser un système ne les garantissant pas ne doit pas être prise à la légère ; sans parler d’autres critères comme la fragmentation du domaine par rapport au monde relationnel et son SQL (à peu près) standardisé. Avec le temps, les moteurs transactionnels ont acquis des fonctionnalités qui faisaient l’intérêt des bases NoSQL (en premier lieu la facilité de réplication et le stockage de JSON), et ces dernières ont tenté d’intégrer un peu plus de sécurité dans leur modèle.
Le langage SQL a été normalisé par l’ANSI en 1986 et est devenu une norme ISO internationale en 1987. Elle a subi plusieurs évolutions dans le but d’ajouter des fonctionnalités correspondantes 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.
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.
Quelle est la requête la plus lisible ?
celle-ci ?
select groupeid,datecreationitem from itemagenda where typeitemagenda = 5 and
in(12225,12376) and datecreationitem > now() order by groupeid,
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.fr.
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 l’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 commentaires
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.
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 y retrouve les
ordres suivants :
CREATE
: crée un objetALTER
: modifie la définition d’un objetDROP
: supprime un objetTRUNCATE
: vide un objetCOMMENT
: ajoute un commentaire sur un objetLes ordres DML (pour Data Manipulation Language
)
permettent l’accès et la modification des données. On y retrouve les
ordres suivants :
SELECT
: lit les données d’une ou plusieurs tablesINSERT
: ajoute des données dans une tableUPDATE
: modifie les données d’une tableDELETE
: supprime les données d’une tableLes ordres DCL (pour Data Control Language
) permettent
de contrôler l’accès aux donné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
objetREVOKE
: retire un droit d’accès d’un rôle sur un
objetEnfin, les ordres TCL (pour
Transaction Control Language
) permettent de contrôler les
transactions :
BEGIN
: ouvre une transactionCOMMIT
: valide les traitements d’une transactionROLLBACK
: annule les traitements d’une
transactionSAVEPOINT
: crée un point de reprise dans une
transactionSET TRANSACTION
: modifie les propriétés d’une
transaction en coursLes ordres BEGIN
et COMMIT
sont souvent
implicites dans le cas d’ordres isolés, si l’« autocommit » 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 (notamment 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
, TRUNCATE TABLE
… sont
transactionnels, n’effectuent aucun COMMIT
et peuvent être
annulés par ROLLBACK
.
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.
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
PostgreSQL.
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.
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 constantes 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 :
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;
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
).
Par défaut, SELECT
retourne tous les résultats d’une
requête. Parfois, des doublons peuvent 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 une déduplication
systématique des données juste avant de retourner les résultats de la
requête, ce qui va souvent 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
.
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 standards : +
, -
,
/
, *
. 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)
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(s.contenance * s.nombre) || ' litres'
FROM stock AS s
JOIN contenant 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.
Parmi les fonctions les plus couramment utilisées, la fonction
NOW()
permet d’obtenir la date et l’heure courante. Elle ne
prend aucun argument. Elle est souvent utilisée, notamment 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
Affichage du nombre de lignes de la table vin
:
SELECT COUNT(*) FROM vin;
COUNT
-------
6067
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.
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 :
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 - ou de création - 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);
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.
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é.
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 retourne 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
comparées soient de même type.
L’opérateur <>
signifie « pas égal à » et peut
aussi s’écrire !=
.
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;
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;
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--------
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
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
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érivant 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;
Enfin, la clause COLLATE
permet d’influencer sur l’ordre
de tri des chaînes de caractères.
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 conformer 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
...)
(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
6
2 rows) (
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 (
integer PRIMARY KEY,
id_fille integer REFERENCES mere(id),
id_mere
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
----+----------+----------+---------+-----------
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) (
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 extensible 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.
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, 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 comprises entre
-2,147,483,648 et 2,147,483,647. Si l’utilisateur tente d’insérer une
donné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 programmation fortement typés, cela permet de
détecter davantage d’erreurs, plus tôt : à la compilation 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 a une
représentation textuelle, 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.
Les types de données standards permettent de traiter la plupart des situations qui peuvent survenir. Dans certains cas, il peut être nécessaire de faire appel aux types spécifiques à 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.
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.
Le standard SQL propose également des types standards pour stocker des éléments temporels (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 données validées au format XML. Le format JSON est de plus en plus courant.
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.
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 langages tels que le C, par exemple \n
pour un retour de ligne, \t
pour une tabulation, etc. :
'chaîne avec un retour \nde ligne et une \ttabulation' E
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 permettent 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 à virgules 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 combien 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.
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 constantes 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'
Le type boolean
permet d’exprimer des valeurs
booléennes, c’est-à-dire une valeur exprimant 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;
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 horaire. 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 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
permettent 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
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'
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.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----------------------------------+--------+------------+--------
01:00:00 | t
GB | BST | 09:00:00 | f
ROK | KST | 00:00:00 | f
Greenwich | GMT | ...) (
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 :
SELECT '2017-08-29 06:00:00' AT TIME ZONE 'US/Pacific';
timezone---------------------
28/08/2017 21:00:00
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.
Le type xml
permet de stocker des documents XML. Par
rapport à une chaîne de caractè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 officielle).
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 manipulations
spécifiques (voir la
documentation officielle). 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.
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.
Le type text
est l’équivalent du type
varchar
mais sans limite de taille de la chaîne de
caractère.
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 permettant d’accélérer
les recherches.
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écessaire 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écanismes 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 utilisateur 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');
value for enum arc_en_ciel: "pink"
ERROR: invalid input 1: INSERT INTO test (id, couleur) VALUES (2, 'pink'); LINE
Création d’un type interval float8_range
:
CREATE TYPE float8_range AS RANGE (subtype = float8, subtype_diff = float8mi);
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
.
Bases de données - de la modélisation au SQL
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
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, 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
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
The Art of SQL
Ce livre s’adresse également à un public avancé. Il présente également les bonnes pratiques lorsque l’on utilise une base de données.
Ce TP utilise la base tpc. La base tpc (dump de 31 Mo, pour 267 Mo sur le disque au final) et ses utilisateurs peuvent être installés comme suit :
curl -kL https://dali.bo/tp_tpc -o /tmp/tpc.dump
curl -kL https://dali.bo/tp_tpc_roles -o /tmp/tpc_roles.sql
# Exécuter le script de création des rôles
psql < /tmp/tpc_roles.sql
# Création de la base
createdb --owner tpc_owner tpc
# L'erreur sur un schéma 'public' existant est normale
pg_restore -d tpc /tmp/tpc.dump
rm -- /tmp/tpc.dump /tmp/tpc_roles.sql
Les mots de passe sont dans le script
/tmp/tpc_roles.sql
. Pour vous connecter :
$ psql -U tpc_admin -h localhost -d tpc
Le schéma suivant montre les différentes tables de la base :
Afficher l’heure courante, au méridien de Greenwich.
Afficher la date et l’heure qu’il sera dans 1 mois et 1 jour.
Ajouter 1 au nombre de type réel ‘1.42’. Pourquoi ce résultat ? Quel type de données permet d’obtenir un résultat correct ?
Afficher le contenu de la table
pays
en classant les pays dans l’ordre alphabétique.
Afficher les pays contenant la lettre
a
, majuscule ou minuscule. Plusieurs solutions sont possibles.
Afficher le nombre lignes de commandes dont la quantité commandée est comprise entre 5 et 10.
Pour les questions suivantes, il faudra se connecter à la base
tpc
.
Avec psql
, pour changer de base, vous pouvez lancer la
méta-commande suivante:
\c tpc
Pour chaque pays, afficher son nom et la région du monde dont il fait partie.
nom_pays | nom_region
--------------------+-----------
ALGÉRIE | Afrique
(...)
Afficher le nombre total de clients français et allemands.
Sortie attendue :
count
-------
12418
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
(...)
Afficher les noms et codes des pays qui font partie de la région « Europe ».
Sortie attendue :
nom_pays | code_pays
----------------+-----------
ALLEMAGNE | DE
(...)
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.
Sortie attendue :
detail_pays
----------------------
ALGÉRIE (DZ), Afrique
(...)
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
Pour les dix premières commandes de l’année 2011, afficher le numéro de la commande, 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
(...)
Afficher l’heure courante, au méridien de Greenwich.
SELECT now() AT TIME ZONE 'GMT';
Afficher la date et l’heure qu’il sera dans 1 mois et 1 jour.
SELECT now() + INTERVAL '1 month 1 day';
Ajouter 1 au nombre de type réel ‘1.42’. Pourquoi ce résultat ? Quel type de données permet d’obtenir un résultat correct ?
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
.
Pour les questions suivantes, il faudra se connecter à la base
tpc
.
Avec psql
, pour changer de base, vous pouvez lancer la
méta-commande suivante:
\c tpc
Afficher le contenu de la table
pays
en classant les pays dans l’ordre alphabétique.
SELECT * FROM pays ORDER BY nom_pays;
Afficher les pays contenant la lettre
a
, majuscule ou minuscule. Plusieurs solutions sont possibles.
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.
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;
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;
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');
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
AND cl.contact_id = cn.contact_id
ORDER BY numero_commande
FIRST 20 ROWS ONLY
FETCH 20; OFFSET
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
20; OFFSET
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
20; OFFSET
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';
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
JOIN pays p ON (r.region_id = p.region_id);
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';
Pour les dix premières commandes de l’année 2011, afficher le numéro de la commande, 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;
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.
Les ordres DDL (acronyme de Data Definition Language
)
permettent de créer des objets dans la base de données et notamment la
structure de base du standard SQL : les tables.
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.
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.
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.
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.
Si un objet dépendant de l’objet à supprimer a lui aussi une dépendance, sa dépendance sera également supprimée. Ainsi de suite jusqu’à la dernière dépendance.
La notion de schéma dans PostgreSQL est à rapprocher de la notion
d’espace de nommage (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.
Jusque PostgreSQL 14, tout le monde a le droit d’y créer des objets. Cela posait des problèmes de sécurité, ce droit était souvent révoqué, ou le schéma supprimé. À partir de PostgreSQL 15, le droit d’écriture doit être donné explicitement.
Il existe des schémas système masqués, par exemple pour des tables
temporaires, ou les tables système (schéma pg_catalog
).
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 positionné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,char(5),
codepostal
departement text,integer
codeinsee );
La suppression directe du schéma ne fonctionne pas car il porte
encore la table communes
:
DROP SCHEMA reference;
drop schema reference because other objects depend on it
ERROR: cannot table reference.communes depends on schema reference
DETAIL: Use DROP ... CASCADE to drop the dependent objects too. HINT:
L’option CASCADE
permet de supprimer le schéma et ses
objets dépendants :
DROP SCHEMA reference CASCADE;
drop cascades to table reference.communes NOTICE:
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 positionnée dans le fichier
postgresql.conf
:
= '"$user",public' search_path
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;
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 objets mais à leurs identifiants. Du
coup, un search_path
changeant entre deux exécutions 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 s11 row)
(
SELECT * FROM s2.t1;
c1-----------
schéma s21 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 s11 row)
(
SET search_path TO s2;
EXECUTE req;
c1-----------
schéma s11 row)
(
-- malgré le changement de search_path, nous en sommes toujours
-- aux données de l'autre table
=# SELECT * FROM t1;
b1
c1-----------
schéma s21 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 supprimés (dans le cas de la fonction)
ou recréés (dans le cas des requêtes préparées).
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 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.
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');
nextval---------
3
Les propriétés de la séquence peuvent être modifiées 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
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).
La fonction nextval()
permet d’obtenir le numéro de
séquence suivant. Son comportement 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');
of sequence "testseq" is not yet defined in this session
ERROR: currval
SELECT * FROM testseq ;
- [ RECORD 1 ]-+--------
sequence_name | testseqlast_value | 15
15
start_value | 1
increment_by | 20
max_value | 10
min_value | 5
cache_value | 0
log_cnt |
is_cycled | f
is_called | f
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;
value (5) cannot be less than MINVALUE (10)
ERROR: RESTART
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 t2Table "s2.t2"
Column | Type | Modifiers
--------+---------+-------------------------------------------------
id | integer | not null default nextval('t2_id_seq'::regclass)
DROP SEQUENCE t2_id_seq;
drop sequence t2_id_seq because other objects depend on it
ERROR: cannot default for table t2 column id depends on sequence t2_id_seq
DETAIL: Use DROP ... CASCADE to drop the dependent objects too.
HINT:
DROP SEQUENCE t2_id_seq CASCADE;
drop cascades to default for table t2 column id
NOTICE:
\d t2Table "s2.t2"
Column | Type | Modifiers
--------+---------+-----------
id | integer | not null
Certaines bases de données offrent des colonnes auto-incrémentées
(autoincrement
de MySQL ou identity
de SQL
Server).
PostgreSQL possède identity
à partir de PostgreSQL 10.
Il était déjà possible d’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,
INTEGER NOT NULL
valeur );
On s’aperçoit que la 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
:
=# \d
postgresList of relations
Schema | Name | Type | Owner
--------+-----------------------+----------+--------
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 :
=# \d exemple_serial
postgresTable "public.exemple_serial"
Column | Type | Modifiers
--------+---------+-------------------------------------------------------------
id | integer | not null default nextval('exemple_serial_id_seq'::regclass)
integer | not null
valeur | 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.
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 employesTable « public.employes »
Type | NULL-able | Par défaut
Colonne | ---------+---------+------------+--------------------------------------
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);
value for domain salaire violates check constraint "salaire_check"
ERROR: -- 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;
column "paye" of table "employes" contains null values
ERROR: -- 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);
domain salaire does not allow null values
ERROR: -- la contrainte est bien vérifiée
-- supprimons maintenant la contrainte
DROP DOMAIN salaire;
drop type salaire because other objects depend on it
ERROR: cannot table employes column paye depends on type salaire
DETAIL: Use DROP ... CASCADE to drop the dependent objects too.
HINT: -- 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
=# DROP DOMAIN salaire CASCADE;
b1drop cascades to table employes column paye
NOTICE: DROP DOMAIN
-- le domaine a été supprimée ainsi que toutes les colonnes ayant ce type
\d employesTable « public.employes »
Type | NULL-able | Par défaut
Colonne | ---------+---------+-----------+--------------------------------------
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
CHECK(
VALUE ~ '^\d{5}$'
OR VALUE ~ '^\d{5}-\d{4}$'
);
CREATE TABLE courrier_us (
PRIMARY KEY,
id_adresse SERIAL NOT NULL,
rue1 TEXT
rue2 TEXT,
rue3 TEXT,NOT NULL,
ville TEXT NOT NULL
code_postal code_postal_us
);
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 »
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.
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.
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.
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. Quelle que soit la colonne, il est ensuite possible d’ajouter des contraintes.
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,
integer DEFAULT 0,
i integer DEFAULT 0
j
);
INSERT INTO valdefaut (id, i) VALUES (1, 10);
SELECT * FROM valdefaut ;
id | i | j
----+----+---
1 | 10 | 0
1 row) (
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 habituelle
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
);
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, suppression… 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 contiennent que des données convertibles vers le type
timestamp
. Dans les anciennes versions, 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 caractè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érification 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.
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.
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.
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 purement et simplement annulée.
Une clé primaire permet d’identifier une ligne de façon unique. Il n’en existe qu’une seule par table.
Une clé primaire exige que toutes les valeurs de la ou des colonnes qui composent cette clé soient uniques et non nulles. La clé 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. Cette clé peut être « naturelle » et visible de l’utilisateur, ou purement technique et non visible.
Exemple :
Par exemple, une table de factures peut avoir comme clé primaire « naturelle » le numéro de la facture.
Cela peut poser problème car il y a des contraintes légales sur l’incrémentation des numéros de facture qui obligent à le générer tard dans le processus. Enregistrer une facture incomplète sans son numéro devient compliqué. Un numéro provisoire est possible, mais le changer implique de modifier aussi toutes les tables qui y font référence. De manière générale, on évitera toujours de modifier une clé primaire.
Techniquement, la table des lignes de facture aurait alors comme clé primaire composée le numéro de la facture et le numéro de la ligne. Cela fonctionne, mais une clé composée, d’ailleurs souvent de type texte, est moins performante pour les jointures qu’un champ monocolonne numérique.
De nombreuses applications utilisent des clés naturelles composées, mais les bonnes pratiques conseillent depuis longtemps d’utiliser sur chaque table une clé technique monocolonne et numérique, de valeur arbitraire, générée par exemple avec une séquence ou un UUID, et invisible de l’utilisateur final.
Les jointures se font alors sur un seul champ. Les clés « naturelles » restent présentes sous forme de champ avec une contrainte d’unicité.
Dans notre exemple, la table des factures porterait une clé primaire
numérique facture_id
arbitraire, et un champ
numero_facture
, unique, qui pourrait être modifié. La table
des lignes de facture porterait une clé primaire
facture_ligne_id
sans lien, et une clé étrangère (non
composée) reprenant facture_id
, et un champ unique
numero_ligne
.
Index :
La création d’une clé primaire crée implicitement un index sur le champ.
Exemple avec clé technique manuelle :
CREATE TABLE region
(id int PRIMARY KEY,
NOT NULL UNIQUE
libelle text
);
INSERT INTO region VALUES (1, 'Alsace');
INSERT INTO region VALUES (2, 'Île-de-France');
-- La clé primaire est forcément NOT NULL
INSERT INTO region VALUES (NULL, 'Corse');
null value in column "id" of relation "region" violates not-null constraint
ERROR: row contains (null, Corse).
DÉTAIL : Failing
INSERT INTO region VALUES (1, 'Corse');
key value violates unique constraint "region_pkey"
ERROR: duplicate Key (id)=(1) already exists.
DÉTAIL :
TABLE region ;
id | libelle
----+---------------
1 | Alsace
2 | Île de France
Exemple avec séquence :
La séquence génère des identifiants que l’on peut utiliser :
CREATE SEQUENCE departement_seq AS int ;
CREATE TABLE departement
(id int PRIMARY KEY,
NOT NULL UNIQUE,
libelle text varchar(3) NOT NULL UNIQUE,
code int NOT NULL REFERENCES region (id)
region_id
);
INSERT INTO departement
SELECT nextval ('departement_seq'), 'Bas-Rhin', '67', 1 ;
INSERT INTO departement
VALUES ( nextval ('departement_seq'), 'Haut-Rhin', '68', 1 ),
'departement_seq'), 'Paris', '75', 2 ) ;
( nextval (
-- Cette erreur va « consommer » un numéro de séquence
-- ce qui n'a pas d'importance pour une clé technique
INSERT INTO departement
SELECT nextval ('departement_seq'), 'Yvelines', '78', null ;
null value in column "region_id" of relation "departement" violates not-null constraint
ERROR: row contains (4, Yvelines, 78, null).
DETAIL : Failing
WITH nouveaudept AS (
INSERT INTO departement
SELECT nextval ('departement_seq'), 'Yvelines', '78', 2
RETURNING *)
SELECT * FROM nouveaudept ;
id | libelle | code | region_id
----+----------+------+-----------
78 | 2
| Yvelines |
TABLE departement ;
id | libelle | code | region_id
----+-----------+------+-----------
1 | Bas-Rhin | 67 | 1
2 | Haut-Rhin | 68 | 1
3 | Paris | 75 | 2
5 | Yvelines | 78 | 2
Exemples avec serial :
Noter que serial
est considéré comme un type (il existe
aussi bigserial
). Il y a création d’une séquence en
arrière-plan, utilisée de manière transparente.
CREATE TABLE ville
(id serial PRIMARY KEY,
NOT NULL,
libelle text int NOT NULL REFERENCES departement
departement_id
);
INSERT INTO ville (libelle, departement_id) VALUES ('Strasbourg',1);
INSERT INTO ville (libelle, departement_id) VALUES ('Paris',3);
-- Interférer avec l'autoincrémentation est une mauvaise idée
INSERT INTO ville VALUES (3, 'Mulhouse', 2);
INSERT INTO ville (libelle, departement_id) VALUES ('Sélestat',1);
key value violates unique constraint "ville_pkey"
ERROR: duplicate Key (id)=(3) already exists. DETAIL :
Exemple avec IDENTITY :
CREATE TABLE ville2
(id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
NOT NULL,
libelle text int NOT NULL REFERENCES departement
departement_id );
Le maniement est identique au serial
. Il faut préférer
l’identité pour des raisons de respect de standard SQL et à cause de
quelques spécificités dans la gestion automatique des séquences
sous-jacentes.
Exemple avec une clé composée :
Nous verrons plus loin un exemple avec une clé primaire composée, qui doit se déclarer alors après les colonnes :
CREATE TABLE stock
(int not null,
vin_id int not null,
contenant_id not null,
annee int4 not null,
nombre int4
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
);
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 remarquer
au moment d’une reprise du schéma pour une évolution majeure de
l’application. Une colonne possédant juste l’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é.
Voici un exemple complet.
Sans contrainte d’unicité, on peut insérer plusieurs fois la même valeur :
=# CREATE TABLE utilisateurs(id integer);
postgresCREATE TABLE
=# INSERT INTO utilisateurs VALUES (10);
postgresINSERT 0 1
=# INSERT INTO utilisateurs VALUES (10);
postgresINSERT 0 1
Ce n’est plus le cas en déclarant une contrainte d’unicité :
=# TRUNCATE utilisateurs;
postgresTRUNCATE TABLE
=# ALTER TABLE utilisateurs ADD UNIQUE(id);
postgresALTER TABLE
=# INSERT INTO utilisateurs (id) VALUES (10);
postgresINSERT 0 1
=# INSERT INTO utilisateurs (id) VALUES (10);
postgreskey value violates unique constraint "utilisateurs_id_key"
ERROR: duplicate Key (id)=(10) already exists.
DETAIL: =# INSERT INTO utilisateurs (id) VALUES (11);
postgresINSERT 0 1
Par contre, on peut insérer plusieurs valeurs NULL :
=# INSERT INTO utilisateurs (id) VALUES (NULL);
postgresINSERT 0 1
=# INSERT INTO utilisateurs (id) VALUES (NULL);
postgresINSERT 0 1
=# INSERT INTO utilisateurs (id) VALUES (NULL);
postgresINSERT 0 1
Ce comportement est modifiable en version 15. Lors de la création de la contrainte, il faut préciser ce nouveau comportement :
=# TRUNCATE utilisateurs;
postgresTRUNCATE TABLE
=# ALTER TABLE utilisateurs DROP CONSTRAINT utilisateurs_id_key;
postgresALTER TABLE
=# ALTER TABLE utilisateurs ADD UNIQUE NULLS NOT DISTINCT(id);
postgresALTER TABLE
=# INSERT INTO utilisateurs (id) VALUES (10);
postgresINSERT 0 1
=# INSERT INTO utilisateurs (id) VALUES (10);
postgreskey value violates unique constraint "utilisateurs_id_key"
ERROR: duplicate Key (id)=(10) already exists.
DETAIL: =# INSERT INTO utilisateurs (id) VALUES (11);
postgresINSERT 0 1
=# INSERT INTO utilisateurs (id) VALUES (NULL);
postgresINSERT 0 1
=# INSERT INTO utilisateurs (id) VALUES (NULL);
postgreskey value violates unique constraint "utilisateurs_id_key"
ERROR: duplicate Key (id)=(null) already exists. DETAIL:
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.
À titre d’exemple nous allons utiliser la base cave
.
La base cave (dump de 2,6 Mo, pour 71 Mo sur le disque au final) peut être téléchargée et restaurée ainsi :
curl -kL https://dali.bo/tp_cave -o cave.dump
psql -c "CREATE ROLE caviste LOGIN PASSWORD 'caviste'"
psql -c "CREATE DATABASE cave OWNER caviste"
pg_restore -d cave cave.dump
# NB : une erreur sur un schéma 'public' existant est normale
Le schéma suivant montre les différentes tables de la base :
Ainsi, la base cave définit une table region
et une
table appellation
. Une appellation 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
.
Exemples
Définition de la table stock
:
CREATE TABLE stock
(int not null,
vin_id int not null,
contenant_id not null,
annee int4 not null,
nombre int4
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 :
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)
FULL
MATCH 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');
insert or update on table "fille" violates foreign key constraint
ERROR: "fk_mere_fille"
Key (mere_id)=(3) is not present in table "mere".
DETAIL:
=# SELECT * FROM fille;
b1id | 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)
=# UPDATE mere SET id=3 WHERE id=2;
b1update or delete on table "mere" violates foreign key constraint
ERROR: "fk_mere_fille" on table "fille"
Key (id)=(2) is still referenced from table "fille".
DETAIL:
-- 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
-- (ON DELETE CASCADE)
=# DELETE FROM mere WHERE id=2;
b1DELETE 1
=# SELECT * FROM fille;
b1id | mere_id | t
----+---------+------
1 | 1 | val1
1 row)
(
=# SELECT * FROM mere;
b1id | t
----+------
1 | val1
1 row) (
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 table référençant peuvent comporter des
valeurs NULL
. Dans le cas des clés étrangères
multicolonnes, 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
----+---------+------
1 | 1 | val1
2 | 2 | val2
4 | | test
2 rows) (
Cette contrainte permet d’avoir une colonne dont la valeur est
incrémentée automatiquement, 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, contrairement au pseudo-type
serial
.
De plus, GENERATED … AS IDENTITY
corrige certains
défauts du pseudo-type serial
. serial
n’est en
effet qu’un raccourci pour créer à la fois une séquence, un champ entier
et un DEFAULT nextval()
pour alimenter ce champ, et cela
peut poser quelques soucis. Notamment, le DDL saisi par l’utilisateur
diffère de celui stocké en base ou sorti par pg_dump
, ce
qui n’est pas idéal. Ou encore, l’ordre
CREATE TABLE … LIKE (INCLUDING ALL)
copie le
serial
sans en changer la séquence ni en créer une autre :
on a alors une séquence partagée par deux tables. Il n’est pas non plus
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
SELECT * FROM personnes ;
id | nom
----+--------
1 | Dupont
2 | Durand
2 lignes)
(
INSERT INTO personnes (id,nom) VALUES (3,'Martin') ;
insert into column "id"
ERROR: cannot Column "id" is an identity column defined as GENERATED ALWAYS.
DÉTAIL : Use OVERRIDING SYSTEM VALUE to override.
ASTUCE :
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
Si des valeurs d’une clé primaire sont mises à jour ou supprimées, cela peut entrainer des 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');
insert or update on table "fille" violates foreign key constraint
ERROR: "fk_mere_fille"
Key (mere_id)=(3) is not present in table "mere". DETAIL:
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;
update or delete on table "mere" violates foreign key constraint
ERROR: "fk_mere_fille" on table "fille"
Key (id)=(1) is still referenced from table "fille". DETAIL:
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
----+---------+------
2 | 2 | val2
1 row) (
La clause NOT NULL
permet de s’assurer que la valeur de
la colonne portant cette contrainte est renseignée. Dit 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 techniquement réalisable, mais
non supporté.
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 problè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.
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 :
mere
et
fille
=# BEGIN;
b1UPDATE mere SET id=3 where id=1;
update or delete on table "mere" violates foreign key constraint
ERROR: "fk_mere_fille" on table "fille"
Key (id)=(1) is still referenced from table "fille". DETAIL:
BEGIN;
SET CONSTRAINTS ALL DEFERRED;
UPDATE mere SET id=3 WHERE id=1;
update or delete on table "mere" violates foreign key constraint
ERROR: "fk_mere_fille" on table "fille"
Key (id)=(1) is still referenced from table "fille". DETAIL:
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)
FULL
MATCH 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
----+------
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;
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 fort impact 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.
Les champs DEFAULT
sont très utilisés, mais PostgreSQL
supporte les colonnes générées (ou calculées).
Les champs avec une clause DEFAULT
sont remplis
automatiquement à la création de la ligne, à moins qu’une valeur ne soit
fournie dans l’ordre INSERT
. Seules sont autorisées des
expressions simples, sans variable, ni utilisation de champs de la
ligne, ni sous-requête. Sont acceptées des constantes, certains calculs
ou fonctions simples, comme now()
, ou un appel à
nextval ('nom_séquence')
.
Ajouter une clause DEFAULT
sur une table calcule les
valeurs pour toutes les lignes pré-existantes de la table Une
optimisation évite de tout réécrire si les anciennes lignes se
retrouvent toutes avec la même valeur constante.
La valeur par défaut peut être écrasée, notamment par
INSERT
ou UPDATE
.
Changer l’expression d’une clause DEFAULT
est possible
:
ALTER TABLE paquet
ALTER COLUMN livraison SET DEFAULT now()+interval '4d';
mais cela n’a pas d’impact sur les lignes existantes, juste les nouvelles.
La syntaxe est :
<type> GENERATED ALWAYS AS ( <expression> ) STORED ; nomchamp
Les colonnes générées sont recalculées à chaque fois que les champs
sur lesquels elles sont basées changent, donc aussi lors d’un
UPDATE
(avant PostgreSQL 13, ils étaient systématiquement
recalculés, parfois inutilement). Ces champs calculés sont
impérativement marqués ALWAYS
, c’est-à-dire obligatoires et
non modifiables, et STORED
, c’est-à-dire stockés sur le
disque (et non recalculés à la volée comme dans une vue). Ils ne doivent
pas se baser sur d’autres champs calculés.
Un intérêt est que les champs calculés peuvent porter des
contraintes, par exemple la clause CHECK
ci-dessous, mais
encore des clés étrangères ou unique.
Exemple :
CREATE TABLE paquet (
PRIMARY KEY,
code text DEFAULT now(),
reception timestamptz DEFAULT now() + interval '3d',
livraison timestamptz int, longueur int, profondeur int,
largeur int
volume GENERATED ALWAYS AS ( largeur * longueur * profondeur )
CHECK (volume > 0.0)
STORED
) ;
INSERT INTO paquet (code, largeur, longueur, profondeur)
VALUES ('ZZ1', 3, 5, 10) ;
\x on
TABLE paquet ;
-[ RECORD 1 ]-----------------------------
code | ZZ1
reception | 2024-04-19 18:02:41.021444+02
livraison | 2024-04-22 18:02:41.021444+02
largeur | 3
longueur | 5
profondeur | 10 volume | 150
-- Les champs DEFAULT sont modifiables
-- Changer la largeur va modifier le volume
UPDATE paquet
SET largeur=4,
= '2024-07-14'::timestamptz,
livraison = '2024-04-20'::timestamptz
reception WHERE code='ZZ1' ;
TABLE paquet ;
-[ RECORD 1 ]----------------------
code | ZZ1
reception | 2024-04-20 00:00:00+02
livraison | 2024-07-14 00:00:00+02
largeur | 4
longueur | 5
profondeur | 10 volume | 200
-- Le volume ne peut être modifié
UPDATE paquet
SET volume = 250
WHERE code = 'ZZ1' ;
ERROR: column "volume" can only be updated to DEFAULT DETAIL : Column "volume" is a generated column.
Expression immutable :
Avec GENERATED
, l’expression du calcul doit être
« immutable », c’est-à-dire ne dépendre que des autres
champs de la même ligne, n’utiliser que des fonctions elles-mêmes
immutables, et rien d’autre. Il n’est donc pas possible d’utiliser des
fonctions comme now()
, ni des fonctions de conversion de
date dépendant du fuseau horaire, ou du paramètre de formatage de la
session en cours (toutes choses autorisées avec DEFAULT
),
ni des appels à d’autres lignes ou tables…
La colonne calculée peut être convertie en colonne « normale » :
ALTER TABLE paquet ALTER COLUMN volume DROP EXPRESSION ;
Mais modifier l’expression n’est pas (encore) possible, sauf à supprimer la colonne générée et en créer une nouvelle, ce qui implique de recalculer toutes les lignes et réécrire toute la table.
Il est possible de créer sa propre fonction pour l’expression, qui doit aussi être immutable :
CREATE OR REPLACE FUNCTION volume (l int, h int, p int)
int
RETURNS AS $$
SELECT l * h * p ;
$$
LANGUAGE sql-- cette fonction dépend uniquement des données de la ligne donc :
PARALLEL SAFE
IMMUTABLE ;
ALTER TABLE paquet DROP COLUMN volume ;
ALTER TABLE paquet ADD COLUMN volume int
GENERATED ALWAYS AS ( volume (largeur, longueur, profondeur) )
STORED;
TABLE paquet ;
-[ RECORD 1 ]----------------------
code | ZZ1
reception | 2024-04-20 00:00:00+02
livraison | 2024-07-14 00:00:00+02
largeur | 4
longueur | 5
profondeur | 10 volume | 200
Attention : modifier la fonction ne réécrit pas spontanément la table, il faut forcer la réécriture avec par exemple :
UPDATE paquet SET longueur = longueur ;
Ne pas réécrire les anciennes valeurs calculées n’est pas un moyen de les conserver. En effet, en cas de sauvegarde logique et restauration, tous les champs seront recalculés avec la dernière formule !
Un autre piège : il faut résister à la tentation de déclarer une fonction comme immutable sans la certitude qu’elle l’est bien (penser aux paramètres de session, aux fuseaux horaires…), sous peine d’incohérences dans les données.
Cas d’usage :
Les colonnes générées économisent la création de triggers, ou de vues de « présentation ». Elles facilitent la dénormalisation de données calculées dans une même table tout en garantissant l’intégrité.
Un cas d’usage courant est la dénormalisation d’attributs JSON pour les manipuler comme des champs de table classiques :
ALTER TABLE personnes
ADD COLUMN lastname text
GENERATED ALWAYS AS ((datas->>'lastName')) STORED ;
L’accès au champ est notablement plus rapide que l’analyse systématique du champ JSON.
Par contre, les colonnes GENERATED
ne sont
pas un bon moyen pour créer des champs portant la
dernière mise à jour. Certes, PostgreSQL ne vous empêchera pas de
déclarer une fonction (abusivement) immutable utilisant
now()
ou une variante. Mais ces informations seront perdues
en cas de restauration logique. Dans ce cas, les triggers restent une
option plus complexe mais plus propre.
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.
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éclaration, 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 ne correspondent
pas au type de données déclarées sont transtypées automatiquement, dans
la mesure du possible.
La clause VALUES
permet de définir une liste
d’expressions qui va constituer la ligne à insé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 fonctions 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);
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 requê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 correspondre.
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;
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);
for integer: "XXX"
ERROR: invalid input syntax 1: INSERT INTO insere VALUES (2, 'XXX', 10); LINE
L’ordre de mise à jour de lignes s’appelle UPDATE
.
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 colonnes 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
.
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,
= bp.commission_rate2
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)
);
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';
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
WHERE vin_id = 7 AND contenant_id = 1 AND annee = 1967 RETURNING nombre;
nombre--------
16
1 row) (
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 transaction 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 respectivement 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
.
PostgreSQL fonctionne en auto-commit. Autrement dit, sans
BEGIN
, une requête est considé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
.
Une transaction est toujours terminée par un COMMIT
ou
un END
quand on veut que les modifications soient
définitivement enregistrées, et par un ROLLBACK
dans le cas
contraire.
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;
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);
.setAutoCommit(false); conn
La transaction est confirmée (COMMIT
) avec la méthode
suivante :
.commit(); conn
À l’inverse, elle est annulée (ROLLBACK
) avec la méthode
suivante :
.rollback(); conn
Au sein d’une transaction, les points de sauvegarde (ordre
SAVEPOINT nom
) permettent d’encadrer un traitement sur
lequel on peut vouloir revenir sans quitter la transaction (par exemple,
selon le retour d’un test), ou au cas où un ordre tombe en erreur.
Revenir à l’état du point de sauvegarde se fait avec
ROLLBACK TO SAVEPOINT
pour annuler uniquement la partie du
traitement voulue, ou pour revenir à l’état d’avant l’erreur, sans
annuler le début de la transaction.
Les savepoints n’existent qu’au sein de la transaction ; on
ne peut bien sûr revenir sur une validation par COMMIT
.
L’ordre RELEASE SAVEPOINT
permet de « libérer »
(oublier) un savepoint précédent (ainsi que ceux posés
après). C’est surtout utile pour libérer quelques ressources
dans des transactions complexes.
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 :
Savepoint save1 = connection.setSavepoint();
En cas d’erreurs, la transaction peut être ramenée à l’état du point de sauvegarde avec :
.rollback(save1); connection
À l’inverse, un point de sauvegarde est relâché de la façon suivante :
.releaseSavepoint(save1); connection
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');
key value violates unique constraint "fille_pkey"
ERROR: duplicate Key (id_fille)=(1) already exists.
DETAIL:
ROLLBACK TO SAVEPOINT insert_fille;
COMMIT;
SELECT * FROM mere;
id | val_mere
----+----------
1 | mere 1
2 | mere 2
10 | essai
Le standard SQL permet de traiter des ensembles d’enregistrements,
que ce soit en lecture, en insertion, en modification et en suppression.
Les ensembles d’enregistrements sont généralement des tables qui, comme
tous les autres objets, sont créées (CREATE
), modifier
(ALTER
) et/ou supprimer (DROP
).
Cet exercice utilise la base tpc.
La base tpc (dump de 31 Mo, pour 267 Mo sur le disque au final) et ses utilisateurs peuvent être installés comme suit :
curl -kL https://dali.bo/tp_tpc -o /tmp/tpc.dump
curl -kL https://dali.bo/tp_tpc_roles -o /tmp/tpc_roles.sql
# Exécuter le script de création des rôles
psql < /tmp/tpc_roles.sql
# Création de la base
createdb --owner tpc_owner tpc
# L'erreur sur un schéma 'public' existant est normale
pg_restore -d tpc /tmp/tpc.dump
rm -- /tmp/tpc.dump /tmp/tpc_roles.sql
Les mots de passe sont dans le script
/tmp/tpc_roles.sql
. 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 suffisants.
Ajouter une colonne
text
à la tablecontacts
. Cette colonne va permettre de stocker l’adresse e-mail des clients et des fournisseurs. Ajouter également un commentaire décrivant cette colonne dans le catalogue de PostgreSQL (utiliser la commandeCOMMENT
).
Mettre à jour la table des contacts pour indiquer l’adresse e-mail de Client6657 qui est
client6657@dalibo.com
.
Ajouter une contrainte d’intégrité qui valide que la valeur de la colonne
@
).
Valider la contrainte dans une transaction de test.
Déterminer quels sont les contacts qui disposent d’une adresse e-mail et affichez leur nom ainsi que le code de leur pays.
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 garantir 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 pour obtenir un numéro de commande sans avoir de « trou » dans la séquence en cas d’échec d’une transaction.
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 tablepieces
.
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.
Ajouter une colonne
text
à la tablecontacts
. Cette colonne va permettre de stocker l’adresse e-mail des clients et des fournisseurs. Ajouter également un commentaire décrivant cette colonne dans le catalogue de PostgreSQL (utiliser la commandeCOMMENT
).
-- Ajouter une colonne email de type text
ALTER TABLE contacts
ADD COLUMN email text;
-- Ajouter un commentaire
COMMENT ON COLUMN contacts.email IS 'Adresse e-mail du contact';
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';
Ajouter une contrainte d’intégrité qui valide que la valeur de la colonne
@
).
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 simple, 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
CHECK (email ~ '.+@.+\..+');
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 fonctionnelle :
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 ;
Déterminer quels sont les contacts qui disposent d’une adresse e-mail et affichez leur nom ainsi que le code de leur pays.
SELECT nom, code_pays
FROM contacts
WHERE email IS NOT NULL;
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 garantir 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 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 (
NOT NULL PRIMARY KEY,
nom text 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éation 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.
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 tablepieces
.
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
);
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
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 ;
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 perfectionnements 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.
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.
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).
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,
character varying(60) NOT NULL,
nom character varying(60),
prenom date,
date_naissance 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
SELECT * FROM personnes ;
id | nom | prenom | date_naissance
----+----------+--------+----------------
1 | Lagaffe | Gaston | 1957-02-28
2 | Fantasio | (null) | 1938-01-01
3 | Prunelle | Léon | (null)
L’affichage (null)
dans psql
est obtenu
avec la méta-commande :
\pset null (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
SELECT 1 + 2 + NULL AS resultat ;
resultat
----------
(null)
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)
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
L’affichage (null)
est obtenu avec la méta-commande
\pset null (null)
du shell psql.
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 ternaire.
Exemples
Comparaison directe avec NULL
, qui est invalide :
SELECT * FROM personnes WHERE date_naissance = NULL ;
id | nom | prenom | date_naissance
----+-----+--------+----------------
(0 rows)
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)
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
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
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
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
Comme son nom l’indique, l’agrégation permet de regrouper des données, qu’elles viennent d’une ou de plusieurs colonnes. Le but est principalement de réaliser des calculs sur les données des lignes regroupées.
La clause GROUP BY
permet de réaliser des regroupements
de données. Les données regroupées sont alors représentées sur une seule
ligne. Le principal intérêt de ces regroupements est de permettre de
réaliser des calculs sur ces données.
Nous allons voir les différentes fonctions d’agrégats disponibles.
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 complè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.
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 disponibles.
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
Déterminer la date de naissance de la personne la plus jeune :
SELECT MAX(date_naissance) FROM personnes ;
max
------------
1957-02-28
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 donné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 disponibles.
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
Une fonction d’agrégat peut aussi prendre plusieurs variables.
Par exemple concernant la méthode des « moindres carrés » :
regr_slope(Y,X)
regr_intercept(Y,X)
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
SELECT regr_intercept(y,x) FROM test ;
regr_intercept
------------------
5.00000015894572
SELECT corr(y,x) FROM test ;
corr
-------------------
0.999962873745297
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
.
La clause HAVING
est toujours accompagnée de la clause
GROUP BY
. La clause GROUP BY
quant à elle
n’est pas toujours accompagnée de 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 connaître 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
Une sous-requête peut faire référence à des variables de la requête principale. Ces variables 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.
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 modification
(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 suivante 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
...
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
performant. 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') ;
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.
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
---
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 contenance 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 principale 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) ;
À 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.
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
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
.
La clause ANY
, ou son synonyme SOME
, permet
de comparer l’expression de gauche à chaque ligne du résultat de la
sous-requête en utilisant l’opérateur indiqué. Ainsi, la requête de
l’exemple avec la clause IN
aurait pu être écrite avec
= ANY
de la façon suivante :
SELECT *
FROM stock
WHERE contenant_id = ANY (SELECT id
FROM contenant
WHERE contenance
BETWEEN 0.3 AND 1.0) ;
La clause ALL
permet de comparer l’expression de gauche
à chaque ligne du résultat de la sous-requête en utilisant l’opérateur
de comparaison indiqué.
La ligne de la table de gauche sera retournée si toutes les
comparaisons sont vraies ou si la sous-requête retourne un ensemble
vide. En revanche, la ligne de la table de gauche sera exclue si au
moins une comparaison est fausse ou si au moins une comparaison est
NULL
.
La requête d’exemple de la clause NOT IN
aurait pu être
écrite avec <> ALL
de la façon suivante :
SELECT *
FROM stock
WHERE contenant_id <> ALL (SELECT id
FROM contenant
WHERE contenance < 2.0) ;
La sous-requête peut être utilisée dans la clause FROM
afin d’être utilisée comme une table 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 comportement 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 ;
EXISTS
présente peu d’intérêt sans corrélation entre la
sous-requête et la requête principale.
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) ;
Les jointures permettent d’écrire des requêtes qui impliquent plusieurs tables. Elles permettent de combiner les colonnes de plusieurs tables selon des critères particuliers, appelé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.
JOIN
ou dans WHERE
?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 simplement 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 beaucoup 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_commandesINNER 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
= 'FR'
pays.code AND addresses.ville = 'Strasbourg'
AND commandes.statut = 'LIVRÉ'
AND clients.type = 'PARTICULIER'
AND clients.actif IS TRUE
GROUP BY clients.numero ;
et :
SELECT
clients.numero,SUM(lignes_commandes.chiffre_affaire)
FROM
lignes_commandes,
commandes,
clients,
addresses,
paysWHERE
= 'FR'
pays.code 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 ;
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
----+----+----+----
0 | 0 | 2 | 2
0 | 0 | 3 | 3
1 | 1 | 2 | 2
1 | 1 | 3 | 3
Ou plus simplement, en listant les deux tables dans la clause
FROM
sans indiquer de condition 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 (
int,
id_sonde check
heure_releve timestamptz 'hour',heure_releve)=heure_releve),
(date_trunc(numeric);
valeur
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_relevesFROM sondes
CROSS JOIN generate_series('2013-01-01 12:00:00','2013-01-01 14:00:00',
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
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.
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.
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ée 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éterminer 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) ;
Il existe deux types de jointures externes : la jointure à gauche et la jointure à droite. Cela ne concerne que l’ordre de la jointure, le traitement en lui- même est identique.
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.
Les voici en exemple :
sql SELECT article.art_titre, auteur.aut_nom FROM article LEFT JOIN auteur ON (article.aut_id=auteur.aut_id) ;
sql SELECT article.art_titre, auteur.aut_nom FROM article LEFT JOIN auteur USING (aut_id) ;
Les jointures à droite sont moins fréquentes mais elles restent utilisées.
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.
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;
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 associer une expression
particulière :
CASE nom_region
WHEN 'Afrique' THEN 1
WHEN 'Amérique' THEN 2
WHEN 'Asie' THEN 3
WHEN 'Europe' THEN 4
ELSE 0
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
Il est possible de placer plusieurs clauses WHEN
. Elles
sont évaluées dans leur ordre d’apparition.
CASE nom_region
WHEN 'Afrique' THEN 1
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 ;
for integer: "inconnu"
ERROR: invalid input syntax 7 : ELSE 'inconnu' LIGNE
La clause ELSE
n’est pas obligatoire mais fortement
recommandé. En effet, si une expression 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
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 ;
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
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
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
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
PostgreSQL propose un nombre conséquent de fonctions permettant de manipuler les différents types de données disponibles. Les étudier de façon exhaustive n’est pas l’objet de ce module. Néanmoins, le manuel de PostgreSQL établit une liste complète des fonctions disponibles dans le SGBD.
Les opérateurs de transtypages permettent de convertir une donnée d’un type particulier vers un autre type. La conversion échoue si les types de données sont incompatibles.
Exemples
Transtypage incorrect d’une chaîne de caractères vers un entier :
SELECT 3 + '3.5'::integer ;
for type integer: "3.5"
ERROR: invalid input syntax 1 : select 3 + '3.5'::integer ;
LIGNE ^
PostgreSQL est volontairement peu flexible pour éviter certaines erreurs subtiles liées à une conversion trop hâtive.
L’opérateur de concaténation permet de concaténer deux chaînes de caractères :
SELECT 'Bonjour' || ', Monde!' ;
-----------------
Bonjour, Monde!
Il permet aussi de concaténer une chaîne de caractères avec d’autres types de données :
SELECT 'Texte ' || 1::integer ;
---------
Texte 1
La fonction char_length()
permet de connaître la
longueur d’une chaîne de caractères :
SELECT char_length('Texte' || 1::integer) ;
-------------
6
Les fonctions lower
et upper
permettent de
convertir une chaîne respectivement en minuscule et en majuscule :
SELECT lower('Bonjour, Monde!') ;
-----------------
bonjour, monde!
SELECT upper('Bonjour, Monde!') ;
-----------------
BONJOUR, MONDE!
La fonction substring
permet d’extraire une chaîne de
caractères à partir d’une chaîne en entrée. Il faut lui indiquer, en
plus de la chaîne source, la position de départ, et la longeur de la
sous-chaîne. Par exemple :
SELECT substring('Bonjour, Monde' from 5 for 4) ;
substring
-----------
our,
Notez que vous pouvez aussi utiliser un appel de fonction plus standard :
SELECT substring('Bonjour, Monde', 5, 4) ;
substring
-----------
our,
La fonction position
indique la position d’une chaîne de
caractères dans la chaîne indiquée. Par exemple :
SELECT position (',' in 'Bonjour, Monde') ;
position
----------
8
La combinaison des deux est intéressante :
SELECT version() ;
version
-------------------------------------------------------------------
PostgreSQL 14.2 (Ubuntu 14.2-1.pgdg20.04+1) on x86_64-pc-linux-…
SELECT substring(version() from 1 for position(' on' in version()));
substring
-----------------
PostgreSQL 14.2
Ces opérateurs sont classiques. L’ensemble des opérations arithmétiques disponibles sont documentées dans le manuel.
La principale surprise vient de /
qui est par défaut une
division entière si des entiers sont seuls impliqués :
SELECT 100 / 101 AS div_entiere,
# 100 * 1.0 / 101 AS div_non_entiere ;
div_entiere | div_non_entiere
-------------+------------------------
0 | 0.99009900990099009901
Ces fonctions sont décrites dans le manuel.
La fonction generate_series(n, m)
est spécifique à
PostgreSQL et permet de générer une suite d’entiers compris entre une
borne de départ et une borne de fin, en suivant un certain
intervalle :
SELECT generate_series(1, 4) ;
generate_series
-----------------
1
2
3
4
(4 rows)
La déclinaison generate_series(n, m, interval)
permet de
spécifier un incrément pour chaque itération :
SELECT generate_series(1, 10, 4) ;
generate_series
-----------------
1
5
9
(3 rows)
Quant à la fonction random()
, elle génère un nombre
aléatoire, de type numeric
, compris entre 0 et 1.
SELECT random() ;
random
-------------------
0.381810061167926
Pour générer un entier compris entre 0 et 100, il suffit de réaliser la requête suivante :
SELECT round(100*random())::integer ;
round
-------
74
Il est possible de contrôler la graine du générateur de nombres
aléatoires en positionnant le paramètre de session
SEED
:
SET SEED = 0.123 ;
ou à l’aide de la fonction setseed()
:
SELECT setseed(0.123) ;
La graine est un flottant compris entre -1 et 1.
Ces fonctions sont décrites dans le manuel de PostgreSQL.
Les fonctions current_date
et current_time
permettent d’obtenir respectivement la date courante et l’heure
courante. La première fonction retourne le résultat sous la forme d’un
type date
et la seconde sous la forme d’un type
time with time zone
.
Préférer current_timestamp
et son synonyme
now()
pour obtenir la date et l’heure courante, le résultat
étant de type timestamp with time zone
.
Exceptionnellement, les fonctions current_date
,
current_time
et current_timestamp
n’ont pas
besoin d’être invoquée avec les parenthèses ouvrantes et fermantes
typiques de l’appel d’une fonction. En revanche, l’appel de la fonction
now()
requiert ces parenthèses.
SELECT current_date ;
current_date
--------------
2017-10-04
SELECT current_time ;
current_time
--------------------
16:32:47.386689+02
SELECT current_timestamp ;
current_timestamp
-------------------------------
2017-10-04 16:32:50.314897+02
SELECT now();
now-------------------------------
2017-10-04 16:32:53.684813+02
Il est possible d’utiliser ces variables comme valeur par défaut d’une colonne :
CREATE TABLE test (
id int GENERATED ALWAYS AS IDENTITY,
timestamp with time zone DEFAULT current_timestamp,
dateheure varchar
valeur
) ;
INSERT INTO test (valeur) VALUES ('Bonjour, monde!');
SELECT * FROM test ;
id | dateheure | valeur
----+-------------------------------+-----------------
1 | 2020-01-30 18:34:34.067738+01 | Bonjour, monde!
La fonction age(timestamp)
permet de déterminer l’âge de
la date donnée en paramètre par rapport à la date courante. L’âge sera
donné sous la forme d’un type interval
.
La forme age(timestamp, timestamp)
permet d’obtenir
l’âge d’une date par rapport à une autre date, par exemple pour
connaître l’âge de Gaston Lagaffe au 5 janvier 1997 :
SELECT age(date '1997-01-05', date '1957-02-28') ;
age
-------------------------
39 years 10 mons 5 days
La fonction date_trunc(text, timestamp)
permet de
tronquer la date à une précision donnée. La précision est exprimée en
anglais, et autorise les valeurs suivantes :
microseconds
milliseconds
second
minute
hour
day
week
month
quarter
year
decade
century
millennium
La fonction date_trunc()
peut agir sur une donnée de
type timestamp
, date
ou interval
.
Par exemple, pour arrondir l’âge de Gaston Lagaffe de manière à
ne représenter que le nombre d’années :
SELECT date_trunc('year',
date '1997-01-05', date '1957-02-28')) AS age_lagaffe ; age(
age_lagaffe
-------------
39 years
La fonction extract(text from timestamp)
permet
d’extraire uniquement une composante donnée d’une date, par exemple
l’année. Elle retourne un type de données flottant
double precision
.
SELECT extract('year' from
date '1997-01-05', date '1957-02-28')) AS age_lagaffe ; age(
age_lagaffe
-------------
39
La soustraction de deux types de données représentant des dates permet d’obtenir un intervalle qui représente le délai écoulé entre ces deux dates :
SELECT timestamp '2012-01-01 10:23:10' - date '0001-01-01' AS soustraction ;
soustraction
----------------------
734502 days 10:23:10
L’addition entre deux types de données est plus restreinte. En effet,
l’expression de gauche est obligatoirement de type
timestamp
ou date
et l’expression de droite
doit être obligatoirement de type time
. Le résultat de
l’addition permet d’obtenir une donnée de type timestamp
,
avec ou sans information sur le fuseau horaire selon que cette
information soit présente ou non sur l’expression de gauche.
SELECT timestamp '2001-01-01 10:34:12' + time '23:56:13' AS addition ;
addition
---------------------
2001-01-02 10:30:25
SELECT date '2001-01-01' + time '23:56:13' AS addition ;
addition
---------------------
2001-01-01 23:56:13
L’addition d’une donnée datée avec une donnée de type
interval
permet d’obtenir un résultat du même type que
l’expression de gauche :
SELECT timestamp with time zone '2001-01-01 10:34:12' +
interval '1 day 1 hour' AS addition ;
addition
------------------------
2001-01-02 11:34:12+01
SELECT date '2001-01-01' + interval '1 day 1 hour' AS addition ;
addition
---------------------
2001-01-02 01:00:00
SELECT time '10:34:24' + interval '1 day 1 hour' AS addition ;
addition
----------
11:34:24
Une donnée de type interval
peut subir des opérations
arithmétiques. Le résultat sera de type interval
:
SELECT interval '1 day 1 hour' * 2 AS multiplication ;
multiplication
-----------------
2 days 02:00:00
SELECT interval '1 day 1 hour' / 2 AS division ;
division
----------
12:30:00
SELECT interval '1 day 1 hour' + interval '2 hour' AS addition ;
addition
----------------
1 day 03:00:00
SELECT interval '1 day 1 hour' - interval '2 hour' AS soustraction ;
soustraction-----------------
1 day -01:00:00
La fonction to_char()
permet de restituer une date selon
un format donné :
SELECT current_timestamp ;
current_timestamp
-------------------------------
2017-10-04 16:35:39.321341+02
SELECT to_char(current_timestamp, 'DD/MM/YYYY HH24:MI:SS');
to_char
---------------------
04/10/2017 16:35:43
Quant à la fonction to_date()
, elle permet de convertir
une chaîne de caractères dans une donnée de type date
. La
fonction to_timestamp()
permet de réaliser la même mais en
donnée de type timestamp
.
SELECT to_timestamp('04/12/2000 12:00:00', 'DD/MM/YYYY HH24:MI:SS') ;
to_timestamp
------------------------
2000-12-04 12:00:00+01
Ces fonctions sont détaillées dans la section concernant les fonctions de formatage de données du manuel.
Le paramètre DateStyle
contrôle le format de saisie et
de restitution des dates et heures. 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
permettent de rendre l’application indépendante de la configuration du
SGBD.
La norme ISO 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 le paramètre DateStyle
est égal à
'SQL, DMY'
.
SET datestyle = 'ISO, DMY';
SELECT current_timestamp ;
now
-------------------------------
2017-10-04 16:36:38.189973+02
SET datestyle = 'SQL, DMY' ;
SELECT current_timestamp ;
now
---------------------------------
04/10/2017 16:37:04.307034 CEST
La fonction
generate_series(date_debut, date_fin, interval)
permet de
générer des séries de dates :
SELECT generate_series(date '2012-01-01',date '2012-12-31',interval '1 month') ;
generate_series
------------------------
2012-01-01 00:00:00+01
2012-02-01 00:00:00+01
2012-03-01 00:00:00+01
2012-04-01 00:00:00+02
2012-05-01 00:00:00+02
2012-06-01 00:00:00+02
2012-07-01 00:00:00+02
2012-08-01 00:00:00+02
2012-09-01 00:00:00+02
2012-10-01 00:00:00+02
2012-11-01 00:00:00+01
2012-12-01 00:00:00+01
(12 rows)
Les vues sont des « tables virtuelles » qui permettent d’obtenir le
résultat d’une requête SELECT
. Sa définition est stockée
dans le catalogue système de la base de données. Le SELECT
est exécuté quand la vue est appelée.
De cette façon, il est possible de créer une vue à destination de certains utilisateurs pour combler différents besoins :
En plus de cela, les vues permettent d’obtenir facilement des valeurs dérivées d’autres colonnes. Ces valeurs dérivées pourront alors être utilisées simplement en appelant la vue plutôt qu’en réécrivant systématiquement le calcul de dérivation à chaque requête qui le nécessite.
Les vues classiques équivalent à exécuter un SELECT
. Il
existe des « vues matérialisées », qui ne seront pas développées ici,
qui sont des vraies tables créées à partir d’une requête (et rafraîchies
uniquement sur demande explicitement).
Bien qu’une vue n’ait pas de représentation physique directe, elle
est accédée au même titre qu’une table avec SELECT
et dans
certains cas avec INSERT
, UPDATE
et
DELETE
. La vue logique ne distingue pas les accès à une vue
des accès à une table. De cette façon, une vue doit utiliser les mêmes
conventions de nommage qu’une table.
Une vue est créée avec l’ordre SQL CREATE VIEW
:
CREATE [ OR REPLACE ] [ TEMP | TEMPORARY ] [ RECURSIVE ] VIEW nom
...] ) ]
[ ( nom_colonne [, WITH ( nom_option_vue [= valeur_option_vue] [, ... ] ) ]
[ AS requete
Le mot clé CREATE VIEW
permet de créer une vue. Si elle
existe déjà, il est possible d’utiliser
CREATE OR REPLACE VIEW
qui aura pour effet de créer la vue
si elle n’existe pas ou de remplacer la définition de la vue si elle
existe déjà. Attention, dans ce dernier cas, les colonnes et les types
de données retournés par la vue ne doivent pas changer d’ordre par
rapport à l’ordre CREATE
original.
La clause nom
permet de nommer la vue. La clause
nom_colonne, …
’ permet lister explicitement les colonnes
retournées par une vue, cette clause est optionnelle mais recommandée
pour mieux documenter la vue.
La clause requete
correspond simplement à la requête
SELECT
exécutée lorsqu’on accède à la vue.
Exemples
CREATE TABLE phone_data (person text, phone text, private boolean) ;
CREATE VIEW phone_number (person, phone) AS
SELECT person, CASE WHEN NOT private THEN phone END AS phone
FROM phone_data ;
Une vue est lue de la même façon qu’une table. On utilisera donc
l’ordre SELECT
pour le faire. L’optimiseur de PostgreSQL
remplacera l’appel à la vue par la définition de la vue pendant la phase
de réécriture de la requête. Le plan d’exécution prendra alors compte
des particularités de la vue pour optimiser les accès aux données.
Exemples
CREATE TABLE phone_data (person text, phone text, private boolean);
CREATE VIEW phone_number (person, phone) AS
SELECT person, CASE WHEN NOT private THEN phone END AS phone
FROM phone_data ;
INSERT INTO phone_data (person, phone, private)
VALUES ('Titi', '0123456789', true) ;
INSERT INTO phone_data (person, phone, private)
VALUES ('Rominet', '0123456788', false) ;
SELECT person, phone FROM phone_number ;
person | phone
---------+------------
Titi |
Rominet | 0123456788
Il est possible d’accorder (ou de révoquer) à un utilisateur les mêmes droits sur une vue que sur une table :
GRANT { { SELECT | INSERT | UPDATE | DELETE | TRUNCATE | REFERENCES | TRIGGER }
...] | ALL [ PRIVILEGES ] }
[, ON { [ TABLE ] nom_table [, ...]
ALL TABLES IN SCHEMA nom_schéma [, ...] }
| TO { [ GROUP ] nom_rôle | PUBLIC } [, ...] [ WITH GRANT OPTION ]
Le droit SELECT
autorise un utilisateur à lire une
table. Les droits INSERT
, UPDATE
et
DELETE
permettent de contrôler les accès en mise à jour à
une vue.
Les droits TRUNCATE
et REFERENCES
n’ont pas
d’utilité avec une vue. Ils ne sont tout simplement pas supportés car
TRUNCATE
n’agit que sur une table et une clé étrangère ne
peut être liée d’une table qu’à une autre table.
Les vues sont parfois utilisées pour filtrer les lignes pouvant être
lues par l’utilisateur. Cette protection peut être contournée si
l’utilisateur a la possibilité de créer une fonction. L’option
security_barrier
permet d’éviter ce problème.
Exemples
=# CREATE TABLE elements (id serial, contenu text, prive boolean) ;
postgresCREATE TABLE
=# INSERT INTO elements (contenu, prive)
postgresVALUES ('a', false), ('b', false), ('c super prive', true),
'd', false), ('e prive aussi', true) ;
(INSERT 0 5
=# SELECT * FROM elements ; postgres
id | contenu | prive
----+---------------+-------
1 | a | f
2 | b | f
3 | c super prive | t
4 | d | f
5 | e prive aussi | t
La table elements
contient cinq lignes, dont trois
considérés comme privés. Nous allons donc créer une vue ne permettant de
voir que les lignes publiques.
=# CREATE OR REPLACE VIEW elements_public AS
postgresSELECT * FROM elements
WHERE CASE WHEN current_user='postgres' THEN TRUE ELSE NOT prive END ;
CREATE VIEW
=# SELECT * FROM elements_public ; postgres
id | contenu | prive
----+---------------+-------
1 | a | f
2 | b | f
3 | c super prive | t
4 | d | f
5 | e prive aussi | t
=# CREATE USER u1;
postgresCREATE ROLE
=# GRANT SELECT ON elements_public TO u1;
postgresGRANT
=# \c - u1
postgresto database "postgres" as user "u1".
You are now connected
=> SELECT * FROM elements ;
postgresfor relation elements
ERROR: permission denied
=> SELECT * FROM elements_public ; postgres
id | contenu | prive
----+---------+-------
1 | a | f
2 | b | f
4 | d | f
L’utilisateur u1
n’a pas le droit de lire directement la
table elements
mais a le droit d’y accéder via la vue
elements_public
, uniquement pour les lignes dont le champ
prive
est à false
.
Cependant, il est possible de révéler le contenu des données à travers une fonction spécifique. Cette fonction peut divulguer les informations des lignes de données qu’elle traite avant même qu’elles ne soient filtrées par la vue :
=> CREATE OR REPLACE FUNCTION abracadabra(integer, text, boolean)
postgresAS $$
RETURNS bool BEGIN
-- afficher chaque ligne rencontrée
'% - % - %', $ 1, $ 2, $ 3 ;
RAISE NOTICE RETURN true ;
END$$
LANGUAGE plpgsql-- désigner un coût bas pour exécuter cette fonction
-- avant le filtre dans la vue
COST 0.0000000000000000000001 ;
CREATE FUNCTION
=> SELECT * FROM elements_public WHERE abracadabra(id, contenu, prive) ; postgres
NOTICE: 1 - a - f
NOTICE: 2 - b - f
NOTICE: 3 - c super prive - t
NOTICE: 4 - d - f
NOTICE: 5 - e prive aussi - t
id | contenu | prive
----+---------+-------
1 | a | f
2 | b | f
4 | d | f
Que s’est-il passé ? pour comprendre, il suffit de regarder
l’EXPLAIN
de cette requête :
=> EXPLAIN SELECT * FROM elements_public
postgresWHERE abracadabra(id, contenu, prive) ;
QUERY PLAN
------------------------------------------------------------------------------
Seq Scan on elements (cost=0.00..28.15 rows=202 width=37)
Filter: (abracadabra(id, contenu, prive) AND
CASE WHEN ("current_user"() = 'u1'::name)
THEN (NOT prive) ELSE true END)
La requête contient deux filtres : celui dans la vue, celui dans la
fonction abracadra
. On a déclaré un coût si faible pour
cette dernière que PostgreSQL, pour optimiser, l’exécute avant le filtre
de la vue. Du coup, la fonction voit toutes les lignes de la table et
peut trahir leur contenu.
Seul moyen d’échapper à cette optimisation du planificateur, utiliser
l’option security_barrier
:
=> \c - postgres
postgresto database "postgres" as user "postgres". You are now connected
=# CREATE OR REPLACE VIEW elements_public
postgresWITH (security_barrier)
AS
SELECT * FROM elements
WHERE CASE WHEN current_user='postgres' THEN true ELSE NOT prive END ;
CREATE VIEW
=# \c - u1
postgresto database "postgres" as user "u1".
You are now connected
=> SELECT * FROM elements_public WHERE abracadabra(id, contenu, prive); postgres
NOTICE: 1 - a - f
NOTICE: 2 - b - f
NOTICE: 4 - d - f
id | contenu | prive
----+---------+-------
1 | a | f
2 | b | f
4 | d | f
=> EXPLAIN SELECT * FROM elements_public WHERE
postgresid, contenu, prive) ; abracadabra(
QUERY PLAN
-------------------------------------------------------------------------------
Subquery Scan on elements_public (cost=0.00..34.20 rows=202 width=37)
Filter: abracadabra(elements_public.id, elements_public.contenu,
elements_public.prive)
-> Seq Scan on elements (cost=0.00..28.15 rows=605 width=37)
Filter: CASE WHEN ("current_user"() = 'u1'::name)
THEN (NOT prive) ELSE true END
Il peut y avoir un impact en performance : le filtre de la vue s’applique d’abord, et peut donc forcer l’optimiseur à s’écarter du chemin optimal.
Un très bon article de blog expliquant plus en détail le
SECURITY BARRIER
est disponible à cette adresse.
Depuis PostgreSQL 9.3, le moteur permet de mettre à jour des vues
simples, ou plus exactement de mettre à jour les tables sous-jacentes au
travers de la vue. Les critères déterminant si une vue peut être mise à
jour ou non sont assez simples à résumer : la vue doit reprendre la
définition de la table avec éventuellement une clause WHERE
pour restreindre les résultats.
La clause WITH CHECK OPTION
(à partir de PostgreSQL 9.4)
empêche l’utilisateur d’insérer des données qui ne satisfont pas les
critères de filtrage de la vue. Sans elle, il est possible d’insérer un
numéro de téléphone privé alors que la vue ne permet pas d’afficher les
numéros privés. Cette option doit être demandée explicitement.
Pour gérer les cas plus complexes, PostgreSQL permet de créer des
triggers INSTEAD OF
sur des vues. Une alternative est
d’utiliser le système de règles (RULES
) mais cette pratique
est peu recommandée en raison de la difficulté de débogage et de
maintenance.
Un trigger INSTEAD OF
permet de déclencher une fonction
utilisateur lorsqu’une opération de mise à jour est déclenchée sur une
vue. Le code de la fonction sera exécuté en lieu et place de la mise à
jour.
Exemples
CREATE TABLE phone_data (person text, phone text, private boolean);
CREATE VIEW maj_phone_number (person, phone, private) AS
SELECT person, phone, private
FROM phone_data
WHERE private = false ;
-- On peut insérer des données car les colonnes de la vue correspondent aux
-- colonnes de la table
INSERT INTO maj_phone_number VALUES ('Titi', '0123456789', false) ;
-- On parvient même à insérer des données qui ne pourront pas être affichées
-- par la vue. Ça peut être gênant.
INSERT INTO maj_phone_number VALUES ('Loulou', '0123456789', true) ;
SELECT * FROM maj_phone_number ;
person | phone | private
--------+------------+---------
Titi | 0123456789 | f
-- L'option WITH CHECK OPTION rajoute une sécurité
CREATE OR REPLACE VIEW maj_phone_number (person, phone, private) AS
SELECT person, phone, private
FROM phone_data
WHERE private = false
WITH CHECK OPTION ;
INSERT INTO maj_phone_number VALUES ('Lili', '9993456789', true);
new row violates check option for view "maj_phone_number"
ERROR: row contains (Lili, 9993456789, t). DETAIL : Failing
-- Cas d'une vue avec un champ calculé
CREATE VIEW phone_number (person, phone) AS
SELECT person, CASE WHEN NOT private THEN phone END AS phone
FROM phone_data ;
-- On ne peut pas insérer de données car les colonnes de la vue ne
-- correspondent pas à celles de la table
INSERT INTO phone_number VALUES ('Fifi', '0123456789');
insert into column "phone" of view "phone_number"
ERROR: cannot View columns that are not columns of their base relation are not updatable. DETAIL:
CREATE OR REPLACE FUNCTION phone_number_insert_row()
TRIGGER
RETURNS
LANGUAGE plpgsqlAS $function$
BEGIN
INSERT INTO phone_data (person, phone, private)
VALUES (NEW.person, NEW.phone, false);
RETURN NEW ;
END ;
$function$;
CREATE TRIGGER view_insert
INSTEAD OF INSERT ON phone_number
FOR EACH ROW
EXECUTE PROCEDURE phone_number_insert_row();
-- Avec le trigger, c'est maintenant possible.
INSERT INTO phone_number VALUES ('Rominet', '0123456788');
SELECT * FROM phone_number ;
person | phone---------+------------
0123456789
Titi |
Loulou |0123456788 Rominet |
La création d’une vue doit être pensée préalablement et doit se justifier du point de vue de l’application ou d’une règle métier. Toute vue créée doit être documentée, au moins en plaçant un commentaire sur la vue.
Bien qu’une vue n’ait pas de représentation physique, elle occupe malgré tout un peu d’espace disque. En effet, le catalogue système comporte une entrée pour chaque vue créée, autant d’entrées qu’il y a de colonnes à la vue, etc. Trop de vues entraîne donc malgré tout l’augmentation de la taille du catalogue système, donc une empreinte mémoire plus importante car ce catalogue reste en général systématiquement présent en cache.
Un problème fréquent est celui de vues complexes calculant beaucoup de choses pour le confort de l’usager… au prix des performances quand l’utilisateur n’a pas besoin de ces informations. L’optimiseur ne peut pas forcément tout élaguer.
Pour cette raison, et pour des raisons de facilité de maintenance, il faut aussi éviter d’empiler les vues.
Les requêtes préparées, aussi appelées requêtes paramétrées, permettent de séparer la phase de préparation du plan d’exécution de la phase d’exécution. Le plan d’exécution qui est alors généré est générique car les paramètres de la requêtes sont inconnus à ce moment là.
L’exécution est ensuite commandée par l’application, en passant l’ensemble des valeurs des paramètres de la requête. De plus, ces paramètres sont passés de façon à éviter les injections SQL.
L’exécution peut être ensuite commandée plusieurs fois, sans avoir à préparer le plan d’exécution. Cela permet un gain important en terme de performances car l’étape d’analyse syntaxique et de recherche du plan d’exécution optimal n’est plus à faire.
L’utilisation de requêtes préparées peut toutefois être contre-performant si les sessions ne sont pas maintenues et les requêtes exécutées qu’une seule fois. En effet, l’étape de préparation oblige à un premier aller-retour entre l’application et la base de données et l’exécution oblige à un second aller- retour, ajoutant ainsi une surcharge qui peut devenir significative.
L’ordre PREPARE
permet de préparer le plan d’exécution
d’une requête. Le plan d’exécution prendra en compte le contexte courant
de l’utilisateur au moment où la requête est préparée, et notamment le
search_path
. Tout changement ultérieur de ces variables ne
sera pas pris en compte à l’exécution.
L’ordre EXECUTE
permet de passer les paramètres de la
requête et de l’exécuter.
La plupart des langages de programmation mettent à disposition des méthodes qui permettent d’employer les mécanismes de préparation de plans d’exécution directement. Les paramètres des requêtes seront alors transmis un à un à l’aide d’une méthode particulière.
Voici comment on prépare une requête :
PREPARE req1 (text) AS
SELECT person, phone FROM phone_number WHERE person = $1;
Le test suivant montre le gain en performance qu’on peut attendre d’une requête préparée :
CREATE TABLE t1 (c1 integer primary key, c2 text);
INSERT INTO t1 select i, md5(random()::text)
FROM generate_series(1, 1000000) AS i ;
$ for i in $(seq 1 100000); do
echo "SELECT * FROM t1 WHERE c1=$i;";
done > requetes_std.sql
echo "PREPARE req AS SELECT * FROM t1 WHERE c1=\$1;" > requetes_prep.sql
for i in $(seq 1 100000); do echo "EXECUTE req($i);"; done >> requetes_prep.sql
$ time psql -f requetes_std.sql postgres >/dev/null
real 0m12.742s
user 0m2.633s
sys 0m0.771s
$ time psql -f requetes_std.sql postgres >/dev/null
real 0m12.781s
user 0m2.573s
sys 0m0.852s
$ time psql -f requetes_prep.sql postgres >/dev/null
real 0m10.186s
user 0m2.500s
sys 0m0.814s
$ time psql -f requetes_prep.sql postgres >/dev/null
real 0m10.131s
user 0m2.521s
sys 0m0.808s
Le gain est de 16 % dans cet exemple. Il peut être bien plus important. En lisant 500 000 lignes (et non pas 100 000), on arrive à 25 % de gain.
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.
Ce TP utilise la base tpc. La base tpc (dump de 31 Mo, pour 267 Mo sur le disque au final) et ses utilisateurs peuvent être installés comme suit :
curl -kL https://dali.bo/tp_tpc -o /tmp/tpc.dump
curl -kL https://dali.bo/tp_tpc_roles -o /tmp/tpc_roles.sql
# Exécuter le script de création des rôles
psql < /tmp/tpc_roles.sql
# Création de la base
createdb --owner tpc_owner tpc
# L'erreur sur un schéma 'public' existant est normale
pg_restore -d tpc /tmp/tpc.dump
rm -- /tmp/tpc.dump /tmp/tpc_roles.sql
Les mots de passe sont dans le script
/tmp/tpc_roles.sql
. Pour vous connecter :
$ psql -U tpc_admin -h localhost -d tpc
Le schéma suivant montre les différentes tables de la base :
Affichez, par pays, le nombre de fournisseurs.
Sortie attendue :
nom_pays | nombre
-------------------------------+--------
ARABIE SAOUDITE | 425
ARGENTINE | 416
(...)
Affichez, par continent (
regions
), le nombre de fournisseurs.
Sortie attendue :
nom_region | nombre
---------------------------+--------
Afrique | 1906
Moyen-Orient | 2113
Europe | 2094
Asie | 2002
Amérique | 1885
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
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
(...)
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
(...)
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)
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 (fonction
ceil()
).
Sortie attendue :
temps_moyen_livraison
-----------------------
38 jour(s)
Déterminez le taux de retour des marchandises (l’état à
R
indiquant qu’une marchandise est retournée).
Sortie attendue :
taux_retour
-------------
24.29
Déterminez le mode d’expédition qui est le plus rapide, en moyenne.
Sortie attendue :
mode_expedition | delai
-----------------+--------------------
AIR | 7.4711070230494535
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.
Sortie attendue :
count
-------
2
Écrivez une requête qui corrige les données erronés en positionnant la date de commande à 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.
É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
É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
(...)
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
Écrivez une requête pour supprimer ces commandes. Vérifiez le travail avant de valider.
É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
(...)
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
(...)
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
Affichez pour les commandes passées en 2011, la liste des continents (
régions
) 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
(...)
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'
(...)
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
(...)
Ce TP s’exécute sur la base tpc :
Ajouter une adresse mail à chaque contact avec la concaténation du nom avec le texte « @dalibo.com ».
Concaténer nom et adresse mail des contacts français sous la forme « nom <mail> ».
Même demande mais avec le nom en majuscule et l’adresse mail en minuscule.
Ajouter la colonne
prix_total
de typenumeric(10,2)
à la tablecommandes
.
Ecrivez une requête qui calcul la somme d’une commande en fonction de la quantité, le prix unitaire ainsi que la remise d’un produit. Vous trouverez ces informations dans la table
lignes_commandes
.
Mettre à jour la colonne
prix_total
de la tablecommandes
avec la somme récupérée de la tablelignes_commandes
.
Récupérer le montant total des commandes par mois pour l’année 2010. Les montants seront arrondis à deux décimales.
Supprimer les commandes de mai 2010.
Ré-exécuter la requête trouvée à la septième question.
Qu’observez-vous ?
Corriger le problème rencontré.
Créer une vue calculant le prix total de chaque commande.
Réécrire la requête de la question 7 pour utiliser la vue créée au point 10.
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 ;
Affichez, par continent (
regions
), 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 ;
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 ;
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
ORDER BY date_commande
LIMIT 30
) cJOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
GROUP BY c.numero_commande
ORDER BY sum(quantite * prix_unitaire - remise) DESC ;
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) ;
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 ;
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 (fonction
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
) cJOIN 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 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
Déterminez le taux de retour des marchandises (l’état à
R
indiquant qu’une marchandise 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 ;
La clause FILTER
des fonctions d’agré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 ;
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 ;
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
JOIN lignes_commandes lc ON c.numero_commande = lc.numero_commande
AND c.date_commande > lc.date_expedition ;
Écrivez une requête qui corrige les données erronés en positionnant la date de commande à 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 ;
É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
:
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 ;
É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 ;
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 donné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
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
) ;
É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 :
COMMIT ;
É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 ;
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 ;
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
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 ;
Affichez pour les commandes passées en 2011, la liste des continents (
régions
) et la marge brute d’exploitation réalisée par continents, 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 cJOIN 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 ;
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_paysFROM
fournisseurs fJOIN contacts c ON f.contact_id = c.contact_id
JOIN pays p ON c.code_pays = p.code_pays
WHERE f.commentaire LIKE '%Complaints%' ;
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 lcJOIN 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 ;
Ajouter une adresse mail à chaque contact avec la concaténation du nom avec le texte « @dalibo.com ».
BEGIN ;
UPDATE contacts
SET email = nom||'@dalibo.com' ;
COMMIT ;
Note : pour éviter de mettre à jour les contacts
ayant déjà une adresse mail, il suffit d’ajouter une clause
WHERE
:
UPDATE contacts
SET email = nom||'@dalibo.com'
WHERE email IS NULL ;
Concaténer nom et adresse mail des contacts français sous la forme « nom <mail> ».
SELECT nom||' <'||email||'>'
FROM contacts;
Même demande mais avec le nom en majuscule et l’adresse mail en minuscule.
SELECT upper(nom)||' <'||lower(email)||'>'
FROM contacts;
Ajouter la colonne
prix_total
de typenumeric(10,2)
à la tablecommandes
.
ALTER TABLE commandes ADD COLUMN prix_total numeric(10,2) ;
Ecrivez une requête qui calcul la somme d’une commande en fonction de la quantité, le prix unitaire ainsi que la remise d’un produit. Vous trouverez ces informations dans la table
lignes_commandes
.
SELECT SUM(quantite * prix_unitaire - remise)
FROM lignes_commandes lc
WHERE lc.numero_commande=32;
Mettre à jour la colonne
prix_total
de la tablecommandes
avec la somme récupérée de la tablelignes_commandes
.
BEGIN ;
UPDATE commandes c
SET prix_total= (
/* cette sous-requête fait une jointure entre lignes_commandes et la
table commandes à mettre à jour pour calculer le prix par commande */
SELECT SUM(quantite * prix_unitaire - remise)
FROM lignes_commandes lc
WHERE lc.numero_commande=c.numero_commande
)-- on peut récupérer le détail de la mise à jour directement dans la
-- requête :
-- RETURNING numero_commande, prix_total
;COMMIT ;
Une autre variante de cette requête serait :
UPDATE commandes c SET prix_total=prix_calc
FROM (
SELECT numero_commande, SUM(quantite * prix_unitaire - remise) AS prix_calc
FROM lignes_commandes
GROUP BY numero_commande
as prix_detail
) WHERE prix_detail.numero_commande = c.numero_commande ;
Bien que cette dernière variante soit moins lisible, elle est bien plus rapide sur un gros volume de données.
Récupérer le montant total des commandes par mois pour l’année 2010. Les montants seront arrondis à deux décimales.
SELECT extract('month' from date_commande) AS numero_mois,
round(sum(prix_total),2) AS montant_total
FROM commandes
WHERE date_commande >= to_date('01/01/2010', 'DD/MM/YYYY')
AND date_commande < to_date('01/01/2011', 'DD/MM/YYYY')
GROUP BY 1
ORDER BY 1 ;
Attention : il n’y a pas de contrainte NOT NULL
sur le
champ date_commande
, donc s’il existe des commandes sans
date de commande, celles-ci seront agrégées à part des autres, puisque
extract()
renverra NULL
pour ces lignes.
Supprimer les commandes de mai 2010.
BEGIN;
/* en raison de la présence de clés étrangères, il faut en premier leur
supprimer les lignes de la table lignes_commandes correspondant aux
commandes à supprimer */
DELETE
FROM lignes_commandes
WHERE numero_commande IN (
SELECT numero_commande
FROM commandes
WHERE date_commande >= to_date('01/05/2010', 'DD/MM/YYYY')
AND date_commande < to_date('01/06/2010', 'DD/MM/YYYY')
);
-- ensuite seulement on peut supprimer les commandes
DELETE
FROM commandes
WHERE date_commande >= to_date('01/05/2010', 'DD/MM/YYYY')
AND date_commande < to_date('01/06/2010', 'DD/MM/YYYY') ;
COMMIT ;
Le problème de l’approche précédente est d’effectuer l’opération en
deux temps. Il est possible de réaliser la totalité des suppressions
dans les deux tables lignes_commandes
et
commandes
en une seule requête en utilisant une
CTE :
WITH del_lc AS (
/* ici on déclare la CTE qui va se charger de supprimer les lignes
dans la table lignes_commandes et retourner les numeros de
commande supprimés */
DELETE
FROM lignes_commandes
WHERE numero_commande IN (
SELECT numero_commande
FROM commandes
WHERE date_commande >= to_date('01/05/2010', 'DD/MM/YYYY')
AND date_commande < to_date('01/06/2010', 'DD/MM/YYYY')
)RETURNING numero_commande
)/* requête principale, qui supprime les commandes dont les numéros
correspondent aux numéros de commandes remontés par la CTE */
DELETE
FROM commandes c
WHERE EXISTS (
SELECT 1
FROM del_lc
WHERE del_lc.numero_commande = c.numero_commande
) ;
Ré-exécuter la requête trouvée à la septième question.
SELECT extract('month' from date_commande) AS numero_mois,
round(sum(prix_total),2) AS montant_total
FROM commandes
GROUP BY 1
ORDER BY 1 ;
Qu’observez-vous ?
La ligne correspondant au mois de mai a disparu.
Corriger le problème rencontré.
SELECT numero_mois, round(coalesce(sum(prix_total), 0.0),2) AS montant_total
/* la fonction generate_series permet de générer une pseudo-table
d'une colonne contenant les chiffres de 1 à 12 */
FROM generate_series(1, 12) AS numero_mois
/* le LEFT JOIN entre cette pseudo-table et la table commandes permet
de s'assurer que même si aucune commande n'a eu lieu sur un mois, la
ligne correspondante sera tout de même présente */
LEFT JOIN commandes ON extract('month' from date_commande) = numero_mois
AND date_commande >= to_date('01/01/2010', 'DD/MM/YYYY')
AND date_commande < to_date('01/01/2011', 'DD/MM/YYYY')
GROUP BY 1
ORDER BY 1 ;
Notez l’utilisation de la fonction coalesce()
dans le
SELECT
, afin d’affecter la valeur 0.0
aux
lignes « ajoutées » par le LEFT JOIN
qui n’ont par défaut
aucune valeur (NULL
).
Créer une vue calculant le prix total de chaque commande.
CREATE VIEW commande_montant AS
SELECT
numero_commande,sum(quantite * prix_unitaire - remise) AS total_commande
FROM lignes_commandes
GROUP BY numero_commande ;
Réécrire la requête de la question 7 pour utiliser la vue créée au point 10.
SELECT extract('month' from date_commande) AS numero_mois,
round(sum(total_commande),2) AS montant_total
FROM commandes c
JOIN commande_montant cm ON cm.numero_commande = c.numero_commande
WHERE date_commande >= to_date('01/01/2010', 'DD/MM/YYYY')
AND date_commande < to_date('01/01/2011', 'DD/MM/YYYY')
GROUP BY 1
ORDER BY 1 ;
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.
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.
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 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
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
00000040 | Lebrac | Publication | 3000.00
5 lignes) (
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écution suivant :
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
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
900 ; OFFSET
Le plan 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
article.
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
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.
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)
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 atomique,
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 depesz. 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 PostgreSQL1. 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) |
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
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.
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
00000020 | Lagaffe | Courrier | 3000.00
00000040 | Lebrac | Publication | 3000.00
00000001 | Dupuis | Direction | 10000.00
(5 rows)
La transaction est toujours valide.
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, silencieusement. 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 (
PRIMARY KEY,
i serial UNIQUE,
v text integer CHECK (x > 0)
x
);
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 :
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).
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
00000001 | M. Pirate | Direction | 10000.00
(5 rows)
La clause ON CONFLICT
permet également de définir une
contrainte d’intégrité sur laquelle 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
UPDATE SET salaire = excluded.salaire; DO
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)
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
table employes
, 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é.
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.
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 là ou les colonnes qui portent une
contrainte d’unicité. Cette contrainte d’unicité permettra de détecter
la duplication 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 (...)
UPDATE
DO SET colonne = excluded.colonne,
= excluded.autre_colonne,
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é :
INSERT ....
ON CONFLICT ON CONSTRAINT nom_contrainte
UPDATE
DO SET colonne_a_modifier = excluded.colonne,
= excluded.autre_colonne,
autre_colonne_a_modifier ...;
De plus amples informations quant à la syntaxe sont disponibles dans la documentation.
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.
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.
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 performante 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éterminer 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.
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);
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.
L’exemple ci-dessous montre qu’il est possible d’utiliser une fonction retournant un ensemble (SRF pour Set Returning Functions).
La fonction get_top_5_messages
est la suivante :
CREATE OR REPLACE FUNCTION get_top_5_messages (p_sujet_id integer)
TABLE (date_publication timestamp, extrait text)
RETURNS 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;
Les Common Table Expressions ou CTE permettent de factoriser la définition d’une sous-requête qui pourrait être appelé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 requê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
.
On utilise principalement une CTE pour factoriser la définition d’une sous-requête commune, 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
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
-------------------------------------------------------------------------------
cost=244618.50..323855.66 rows=12 width=67)
Append (
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)
-> 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.
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>
AS (
), nom_vue2 <requête pour générer la vue 2, pouvant utiliser la vue 1>
)<requête principale utilisant vue 1 et/ou vue2>;
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 remonté 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
------------------------------------------------------------------------------
cost=1.21..1.22 rows=1 width=4)
Aggregate (-> 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é appliqué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
-----------------------------------------------------------------
cost=1.29..1.30 rows=1 width=4)
Aggregate (
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)
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)
time=7.757..5526.148 rows=7 loops=1)
(actual
CTE nos_commandes-> GroupAggregate (cost=3.51..154567.39 rows=1000000 width=48)
time=0.043..5076.121 rows=1000000 loops=1)
(actual Group Key: c.numero_commande
-> Merge Join (cost=3.51..110641.89 rows=3142550 width=26)
time=0.017..2511.385 rows=3142632 loops=1)
(actual 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)
(time=0.008..317.547 rows=1000000 loops=1)
(actual -> Index Scan using lignes_commandes_pkey on lignes_commandes l
cost=0.43..52570.08 rows=3142550 width=18)
(time=0.006..1030.420 rows=3142632 loops=1)
(actual -> Index Scan using clients_pkey on clients
cost=0.29..0.51 rows=1 width=10)
(time=0.009..0.009 rows=1 loops=1)
(actual Index Cond: (client_id = 6845)
-> CTE Scan on nos_commandes (cost=0.00..22500.00 rows=5000 width=48)
time=7.746..5526.128 rows=7 loops=1)
(actual 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 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 millisecondes.
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)
time=0.113..0.117 rows=7 loops=1)
(actual -> Index Scan using clients_pkey on clients (cost=0.29..0.51 rows=1 width=10)
time=0.007..0.007 rows=1 loops=1)
(actual Index Cond: (client_id = 6845)
-> HashAggregate (cost=12.54..12.67 rows=11 width=48)
time=0.106..0.108 rows=7 loops=1)
(actual Group Key: c.numero_commande
-> Nested Loop (cost=0.85..12.19 rows=35 width=26)
time=0.028..0.087 rows=23 loops=1)
(actual -> Index Scan using commandes_clients_fkey on commandes c
cost=0.42..1.82 rows=11 width=16)
(time=0.022..0.028 rows=7 loops=1)
(actual Index Cond: (client_id = 6845)
-> Index Scan using lignes_commandes_pkey on lignes_commandes l
cost=0.43..0.89 rows=5 width=18)
(time=0.006..0.007 rows=3 loops=7)
(actual 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.
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
.
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 enregistrements 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ébugger une requête complexe.
WITH sous-requete1 AS (
),-requete1 AS (
debug_sousINSERT INTO debug_sousrequete1
SELECT * FROM sous-requete1
-requete2 AS (
), sousSELECT ...
FROM sous-requete1
JOIN ....
WHERE ....
GROUP BY ...
),-requete2 AS (
debug_sousINSERT 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),
AS (SELECT * FROM fonction_en_ecriture())
fonction_en_ecriture SELECT 1;
supprimera l’intégralité des données de la table
nom_table
, mais n’appellera pas la fonction
fonction_en_ecriture()
, même si celle-ci effectue des
écritures.
Le langage SQL permet de réaliser des récursions avec des CTE récursives. Son principal 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 (parcours 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 données orientées graphes, tel que Neo4J. À noter que l’extension pgRouting implémente des algorithmes de parcours de graphes plus efficace. Cela permet de rester dans PostgreSQL mais nécessite un certain formalisme et il faut avoir conscience 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.
Voici le résultat de cette requête :
valeur
--------
1
2
3
4
5
6
7
8
9
10
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.
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,
integer); parent_id
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
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
table 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,
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)
Plusieurs problèmes de concurrences d’accès peuvent se poser quand plusieurs transactions 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';
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 :
UPDATE
utilisant la valeur lue par
l’ordre UPDATE
,SELECT FOR UPDATE
,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
.
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.
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;
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 transactions. 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.
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 traitements au risque d’obtenir le message
suivant :
ERROR: current transaction is aborted, commands ignored until
end of transaction block
Une dernière fonctionnalité intéressante de
SELECT FOR UPDATE
, apparue avec PostgreSQL 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;
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
aurait 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
WHERE id IN (...);
COMMIT;
De même pour la seconde transaction, qui aura traité d’autres lignes en parallèle de la transaction #1.
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 ‘rouge’. Deux utilisateurs essayent simultanément de convertir tous les enregistrements vers une couleur unique, mais chacun dans une direction opposée. Un utilisateur veut passer tous les blancs en rouge, et l’autre tous les rouges en blanc.
L’exemple peut être mis en place avec ces ordres :
create table points
(id int not null primary key,
not null
couleur text
);insert into points
with x(id) as (select generate_series(1,10))
select id, case when id % 2 = 1 then 'rouge'
else 'blanc' end from x;
Session 1 :
set default_transaction_isolation = 'serializable';
begin;
update points set couleur = 'rouge'
where couleur = 'blanc';
Session 2 :
set default_transaction_isolation = 'serializable';
begin;
update points set couleur = 'blanc'
where couleur = 'rouge';
À 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.
Une erreur de sérialisation. On annule et on réessaye.
rollback;
begin;
update points set couleur = 'rouge'
where couleur = 'blanc';
commit;
Il n’y a pas de transaction concurrente pour gêner.
select * from points order by id;
id | couleur
----+-------
1 | rouge
2 | rouge
3 | rouge
4 | rouge
5 | rouge
6 | rouge
7 | rouge
8 | rouge
9 | rouge
10 | rouge
(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 « apparaî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 officielle.
Jointure latérale
Cette série de question utilise la base de TP magasin. La base magasin (dump de 96 Mo, pour 667 Mo sur le disque au final) peut être téléchargée et restaurée comme suit dans une nouvelle base magasin :
createdb magasin
curl -kL https://dali.bo/tp_magasin -o /tmp/magasin.dump
pg_restore -d magasin /tmp/magasin.dump
# le message sur public préexistant est normal
rm -- /tmp/magasin.dump
Toutes les données sont dans deux schémas nommés magasin et facturation.
Afficher les 10 derniers articles commandés.
Pour chacune des 10 dernières commandes passées, afficher le premier article commandé.
CTE récursive
La table genealogie
peut être téléchargée depuis https://dali.bo/tp_genealogie et restaurée à l’aide de
pg_restore
:
curl -kL https://dali.bo/tp_genealogie -o genealogie.dump
createdb genealogie
pg_restore -O -d genealogie genealogie.dump
# le message d'erreur sur le schéma public est normal
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 descendants de Fernand DEVAUX.
À l’inverse, déterminer qui sont les ancètres de Adèle TAILLANDIER
Réseau social
La table socialnet
peut être téléchargée et restaurée
ainsi :
curl -kL https://dali.bo/tp_socialnet -o /tmp/socialnet.dump
createdb socialnet
pg_restore -O -d socialnet /tmp/socialnet.dump
# le message d'erreur sur le schéma public est normal
Cet exercice est assez similaire au précédent et propose de manipuler des arborescences.
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 :
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 objetspg_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 fonctions retournant des ensemblesLa 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 :
L’objectif de se TP consiste à récupérer l’ordre de suppression et de
recréation des vues de la base brno2015
en fonction du
niveau de dépendances entre chacune des vues. Brno est une ville de
Tchéquie, dans la région de Moravie-du-Sud. Le circuit Brno-Masaryk est
situé au nord-ouest de la ville. Le Grand Prix moto de Tchéquie s’y
déroule chaque année.
La table brno2015
peut être téléchargée et restaurée
ainsi :
curl -kL https://dali.bo/tp_brno2015 -o /tmp/brno2015.dump
createdb brno2015
pg_restore -O -d brno2015 /tmp/brno2015.dump
# une erreur sur l'existence du schéma public est normale
Retrouver les dépendances de la vue
pilotes_brno
. Déduisez également l’ordre de suppression et de recréation des vues.
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 magasin :
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,
SELECT p.produit_id, p.nom
LATERAL (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_commandeORDER BY c.numero_commande DESC
LIMIT 10;
CTE récursive
À partir de la table
genealogie
, déterminer qui sont les descendants 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;
À l’inverse, déterminer qui sont les ancètres de Adèle TAILLANDIER
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.mereFROM 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
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)
AS (
), plus_courte_connexion 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,
WITH ORDINALITY AS list(id, level)
unnest(personnes_connectees) 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 relationnelles sont rapidement atteintes sur de telles requêtes.
Une solution consisterait à implémenter un algorithme de parcours de graphe avec pgRouting, 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.
Dépendance de vues
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;
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
),AS (
resolved SELECT n.nspname AS dependent_schema, d.relname as dependent,
AS dependee_schema, d2.relname as dependee,
n2.nspname 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;
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
),AS (
resolved SELECT n.nspname AS dependent_schema, d.relname as dependent,
AS dependee_schema, d2.relname as dependee,
n2.nspname oid as dependent_oid,
d.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));
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 contrô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 :
=# CREATE TABLE test_date (dt date);
postgresCREATE TABLE
=# INSERT INTO test_date VALUES ('2015-0717');
postgresfor type date: "2015-0717"
ERROR: invalid input syntax 1: INSERT INTO test_date VALUES ('2015-0717');
LINE
^=# INSERT INTO test_date VALUES ('2015-02-30');
postgresdate/time field value out of range: "2015-02-30"
ERROR: 1: INSERT INTO test_date VALUES ('2015-02-30');
LINE
=# INSERT INTO test_date VALUES ('2015-07-17');
postgresINSERT 0 1
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 :
SELECT 2 | 4;
?column?
----------
6
(1 ligne)
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ésultat, mais l’une sera capable
d’utiliser un éventuel index sur id
, l’autre non :
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)
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 la conversion est réalisée
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
qu’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.
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
flottants. 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 implications 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
:
CREATE TABLE t_float AS (
SELECT 0.04::float AS cf,
0.04::numeric AS cn
FROM generate_series(1, 25000)
);
SELECT sum(cn), sum(cf) FROM t_float ;
sum | sum
---------+-----------------
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 :
SELECT sum(cn) FROM t_float ;
sum
---------
1000.00
(1 ligne)
Temps : 10,611 ms
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 flottants selon le standard IEEE.
SELECT now()::date ;
now
------------
2019-11-13
SELECT now()::time ;
now
-----------------
15:19:39.947677
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
.
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
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.
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 à proprement parler, mais il est important de considérer les
types range
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 contre 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 maintenance 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 :
=# SELECT * FROM generate_series(
postgres'2015-01-01',
'2015-01-31',
'1 day'::interval
);
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
En général on choisira une chaîne de longueur variable. Nous ne
parlerons pas ici du type char
(à taille fixe),
d’utilisation très restreinte.
Le type bytea
permet de stocker des données binaires
dans une base de données PostgreSQL.
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.
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 UTF8
(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 :
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 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)
SELECT * FROM mots WHERE t > 'z' COLLATE "da_DK";
t
---
aa
ä
å
Å
aa
æ
(6 lignes)
Des collations comme en_US.UTF-8
ou
fr_FR.UTF-8
sont dépendantes des locales installé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 locale 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
-----------
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 Eisentraut et la documentation officielle.
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
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 | …
json
xml
range
Les intervalles de valeurs (range
) représentent un
ensemble de valeurs continues comprises entre deux bornes. Ces dernières
sont entourées par des crochets [
et ]
lorsqu’elles sont incluses, et par des parenthèses (
et )
lorsqu’elles sont exclues. L’absence de borne est
admise et correspond à l’infini.
[0,10]
: toutes les valeurs comprises entre 0 à
10 ;(100,200]
: toutes les valeurs comprises entre 100 et
200, 100 exclu ;[2021-01-01,)
: toutes les dates supérieures au 1er
janvier 2021 inclus ;empty
: aucune valeur ou intervalle vide.Le type abstrait anyrange
se décline en
int4range
(int
), int8range
(bigint
), numrange
(numeric
),
daterange
(date
), tsrange
(timestamp without timezone
), tstzrange
(timestamp with timezone
).
Les opérateurs d’inclusion <@
et @>
déterminent si une valeur ou un autre intervalle sont contenus dans
l’intervalle de gauche ou de droite.
SELECT produit, date_validite FROM produits
WHERE date_validite @> '2020-01-01'::date;
produit | date_validite
--------------------------------------+-------------------------
a0fd7a5a-6deb-4454-b7a7-9cd38eef53a4 | [2012-07-12,)
79eb3a63-eb76-43b9-b1d6-f9f82dd77460 | [2019-07-31,2021-04-01)
e4edaac4-33f1-426d-b2b0-4ea3b1c6caec | (,2020-01-02)
L’opérateur de chevauchement &&
détermine si
deux intervalles du même type disposent d’au moins une valeur
commune.
SELECT produit, date_validite FROM produits
WHERE date_validite && '[2021-01-01,2021-12-31]'::daterange
produit | date_validite
--------------------------------------+-------------------------
8791d13f-bdfe-46f8-afc6-8be33acdbfc7 | [2012-07-12,)
000a72d5-a90f-4030-aa15-f0a05e54b701 | [2019-07-31,2021-04-01)
L’opérateur d’intersection *
reconstruit l’intervalle
des valeurs continues et communes entre deux intervalles.
SELECT '[2021-01-01,2021-12-31]'::daterange
+ '[2019-07-31,2021-04-01)'::daterange AS intersection;
intersection
-------------------------
[2021-01-01,2021-04-01)
Pour garantir des temps de réponse acceptables sur les recherches avancées avec les opérateurs ci-dessus, il est nécessaire d’utiliser les index GiST ou SP-GiST. La syntaxe est la suivante :
CREATE INDEX ON produits USING gist (date_validite);
Enfin, il est possible de créer ses propres types range
personnalisés à l’aide d’une fonction de différence. L’exemple
ci-dessous permet de manipuler l’intervalle de données pour le type
time
. La fonction time_subtype_diff()
est
tirée de la documentation
RANGETYPES-DEFINING.
-- fonction utilitaire pour le type personnalisé "timerange"
CREATE FUNCTION time_subtype_diff(x time, y time)
AS
RETURNS float8 'SELECT EXTRACT(EPOCH FROM (x - y))'
LANGUAGE sql STRICT IMMUTABLE;
-- définition du type "timerange", basé sur le type "time"
CREATE TYPE timerange AS RANGE (
= time,
subtype = time_subtype_diff
subtype_diff
);
-- Exemple
SELECT '[11:10, 23:00]'::timerange;
timerange
---------------------
[11:10:00,23:00:00]
Une contrainte d’exclusion s’apparente à une contrainte d’unicité, mais pour des intervalles de valeurs. Le principe consiste à identifier les chevauchements entre deux lignes pour prévenir l’insertion d’un doublon sur un intervalle commun.
Dans l’exemple suivant, nous utilisons le type personnalisé
timerange
, présenté ci-dessus. La table
vendeurs
reprend les agents de vente d’un magasin et leurs
plages horaires de travail, valables pour tous les jours ouvrés de la
semaine.
CREATE TABLE vendeurs (
varchar NOT NULL,
nickname NOT NULL,
plage_horaire timerange USING GIST (plage_horaire WITH &&)
EXCLUDE
);
INSERT INTO vendeurs (nickname, plage_horaire)
VALUES
'john', '[09:00:00,11:00:00)'::timerange),
('bobby', '[11:00:00,14:00:00)'::timerange),
('jessy', '[14:00:00,17:00:00)'::timerange),
('thomas', '[17:00:00,20:00:00]'::timerange); (
Un index GiST est créé automatiquement pour la colonne
plage_horaire
.
on
\x + \di
List of relations
-[ RECORD 1 ]-+----------------------------
Schema | public
Name | vendeurs_plage_horaire_excl
Type | index
Owner | postgres
Table | vendeurs
Persistence | permanent
Access method | gist
Size | 8192 bytes
Description |
L’ajout d’un nouveau vendeur pour une plage déjà couverte par l’un de
ces collègues est impossible, avec une violation de contrainte
d’exclusion, gérée par l’opérateur de chevauchement
&&
.
INSERT INTO vendeurs (nickname, plage_horaire)
VALUES ('georges', '[10:00:00,12:00:00)'::timerange);
ERROR: conflicting key value violates exclusion constraint
"vendeurs_plage_horaire_excl"
DETAIL: Key (plage_horaire)=([10:00:00,12:00:00)) conflicts
with existing key (plage_horaire)=([09:00:00,11:00:00)).
Il est aussi possible de mixer les contraintes d’unicité et
d’exclusion grâce à l’extension btree_gist
. Dans l’exemple
précédent, nous imaginons qu’un nouveau magasin ouvre et recrute de
nouveaux vendeurs. La contrainte d’exclusion doit évoluer pour prendre
en compte une nouvelle colonne, magasin_id
.
CREATE EXTENSION btree_gist;
ALTER TABLE vendeurs
DROP CONSTRAINT IF EXISTS vendeurs_plage_horaire_excl,
ADD COLUMN magasin_id int NOT NULL DEFAULT 1,
ADD EXCLUDE USING GIST (magasin_id WITH =, plage_horaire WITH &&);
INSERT INTO vendeurs (magasin_id, nickname, plage_horaire)
VALUES (2, 'georges', '[10:00:00,12:00:00)'::timerange);
En cas de recrutement pour une plage horaire déjà couverte par le nouveau magasin, la contrainte d’exclusion lèvera toujours une erreur, comme attendu.
INSERT INTO vendeurs (magasin_id, nickname, plage_horaire)
VALUES (2, 'laura', '[09:00:00,11:00:00)'::timerange);
ERROR: conflicting key value violates exclusion constraint
"vendeurs_magasin_id_plage_horaire_excl"
DETAIL: Key (magasin_id, plage_horaire)=(2, [09:00:00,11:00:00)) conflicts
with existing key (magasin_id, plage_horaire)=(2, [10:00:00,12:00:00)).
Les types composites sont assez difficiles à utiliser, car ils nécessitent d’adapter la syntaxe 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.
En revanche, il peut être intéressant pour stocker un tableau de données composites dans une table.
Référence :
À 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 :
CREATE TABLE employes (
char(8) primary key,
matricule not null,
nom text
service text,numeric(7,2)
salaire
);
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 :
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 regroupement avec
GROUP BY
:
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;
^
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.
Des calculs pourront être réalisés sur les données agrégées selon le critère de regroupement donné. Le résultat sera alors représenté en n’affichant que les colonnes de regroupement puis les valeurs calculées par les fonctions d’agrégation :
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.
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 requê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 ensembles 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,
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
.
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.
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,
', ' ORDER BY nom) AS liste_employes
string_agg(nom, 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,
ORDER BY nom) AS liste_employes
array_agg(nom FROM employes
GROUP BY service;
service | liste_employes
-------------+--------------------
Courrier | {Fantasio,Lagaffe}
Direction | {Dupuis}
Publication | {Lebrac,Prunelle}
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.
CASE
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é.
L’exemple suivant montre l’utilisation de la clause
FILTER
et son équivalent écrit avec une expression
CASE
:
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)
PostgreSQL supporte les fonctions de fenêtrage. Elles apportent des fonctionnalités analytiques à PostgreSQL, et permettent d’écrire beaucoup plus simplement 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
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
:
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)
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 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.
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.
Le terme PARTITION BY
permet d’indiquer les critères de
regroupement de la fenêtre sur laquelle on souhaite travailler.
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 :
SELECT row_number() OVER (ORDER BY matricule),
matricule, nomFROM 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.
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.
Le terme ORDER BY
permet d’indiquer les critères de tri
de la fenêtre sur laquelle on souhaite travailler.
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 appliquant 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.
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
(…)
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.
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 ;
NULL
sera renvoyé.lead()
et lag()
La construction lead(colonne)
est équivalente à
lead(colonne, 1)
. De même, la construction
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.
lead()
et lag()
:
exempleLa 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.
lead()
et
lag()
: principeNULL
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.
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)
first
/last
/nth_value
:
exempleLorsque 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 fonction 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.
WINDOW
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
AS (ORDER BY salaire); WINDOW w
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
AS (PARTITION BY service),
WINDOW w AS (w ORDER BY salaire),
w_asc AS (w ORDER BY salaire DESC); w_desc
WINDOW
: syntaxeGROUPS
Ceci n’est disponible que depuis la version 11.
EXCLUDE
Ceci n’est disponible que depuis la version 11.
La clause WITHIN GROUP
est une 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.
WITHIN GROUP
: exempleCet 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 :
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
(…)
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
.
| | | |
CREATE TABLE stock AS SELECT * FROM (
VALUES ('ecrous', 'est', 50),
'ecrous', 'ouest', 0),
('ecrous', 'sud', 40),
('clous', 'est', 70),
('clous', 'nord', 0),
('vis', 'ouest', 50),
('vis', 'sud', 50),
('vis', 'nord', 60)
(AS VALUES(piece, region, quantite); )
Le comportement de la clause GROUPING SETS
peut être
émulée avec deux requêtes utilisant 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
-> 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)
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
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,
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
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
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
.
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édia.
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
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 correspondante. 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 regroupement
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)
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
.
La table brno2015
peut être téléchargée et restaurée
ainsi :
curl -kL https://dali.bo/tp_brno2015 -o /tmp/brno2015.dump
createdb brno2015
pg_restore -O -d brno2015 /tmp/brno2015.dump
# une erreur sur l'existence du schéma public est normale
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
Quel est le pilote qui a le moins gros écart entre son meilleur tour et son moins bon tour ?
Déterminer quel est le pilote le plus régulier (écart-type).
Window Functions
Afficher la place sur le podium pour chaque coureur.
À 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.
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 chaque coureur, quel est son meilleur tour et quelle place avait-il sur ce tour ?
Déterminer quels sont les coureurs ayant terminé la course qui ont gardé la même position tout au long de la course.
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
.
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.
Ajouter également le montant total des commandes depuis le début de l’activité.
Ajouter également le montant total des commandes par pays.
La table brno2015
peut être téléchargée et restaurée
ainsi :
curl -kL https://dali.bo/tp_brno2015 -o /tmp/brno2015.dump
createdb brno2015
pg_restore -O -d brno2015 /tmp/brno2015.dump
# une erreur sur l'existence du schéma public est normale
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;
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
ORDER BY 2
LIMIT 1;
La requête donne le résultat suivant :
nom | ecart
-----------------+--------------
Jorge LORENZO | 00:00:04.661
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
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 :
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;
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_timeFROM (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
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)
À 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,- first_value(total_time)
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
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)
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
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,- first_value(temps_tour_glissant)
temps_tour_glissant OVER (PARTITION BY no_tour
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,- first_value(temps_tour_glissant)
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
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)
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
),
AS (
classement_tour 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
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)
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
),AS (
temps_glissant 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
),AS (
classement_tour SELECT no_tour, no_pilote, lap_time, total_tour,
rank() OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
as place_course
) 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
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
),AS (
classement_tour SELECT no_tour, no_pilote, lap_time,
rank() OVER (
PARTITION BY no_tour
ORDER BY temps_tour_glissant
as place_course,
)
temps_tour_glissantFROM temps_glissant
),AS (
depassement 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
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;
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
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
(...)
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);
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);
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ésentation des résultats.
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);
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.↩︎