7.2. Expressions de table

Une expression de table calcule une table. L'expression de table contient une clause FROM qui peut être suivie des clauses WHERE, GROUP BY et HAVING. Les expressions de table triviales font simplement référence à une table sur le disque, appelée table de base, mais des expressions plus complexes peuvent être utilisées pour modifier ou combiner des tables de base de différentes façons.

Les clauses optionnelles WHERE, GROUP BY et HAVING dans l'expression de table spécifient un enchaînement de transformations successives réalisées sur la table dérivée de la clause FROM. Toutes ces transformations produisent une table virtuelle fournissant les lignes à passer à la liste de sélection qui choisira les lignes à afficher en sortie de la requête.

7.2.1. La clause FROM

La clause FROM dérive une table à partir d'une ou plusieurs tables, spécifiées dans une liste de référence dont le séparateur est une virgule.

FROM reference_table [,
reference_table [, ...]]

Une référence de table peut être un nom de table (optionnellement préfixé par le nom du schéma) ou une table dérivée. Une table dérivée peut-être une sous-requête, une jointure de tables ou toute combinaison complexe de celles-ci. Si plusieurs références de tables sont listées dans la clause FROM, elles sont jointes (voir plus bas) pour former la table virtuelle intermédiaire, sujet possible de transformations par les clauses WHERE, GROUP BY et HAVING. et qui est finalement le résultat de l'expression des tables globale.

Lorsqu'une référence de table fait référence à une table supertable d'une hiérarchie d'héritage, la référence de table produit les lignes non seulement de la table mais aussi des successeurs de cette table sauf si le mot clé ONLY précède le nom de la table. Toutefois, la référence produit seulement les colonnes qui apparaissent dans la table nommée --- toute colonne ajoutée dans une sous-table est ignorée.

7.2.1.1. Tables jointes

Une table jointe est une table dérivée de deux autres tables (réelles ou dérivées) suivant les règles du type de jointure particulier. Les jointures internes (inner), externes (outer) et croisées (cross) sont disponibles.

Types de jointures

Jointure croisée (cross join)
T1 CROSS JOIN T2

Pour chaque combinaison de lignes provenant de T1 et de T2, la table dérivée contiendra une ligne constituée de toutes les colonnes de T1 suivies de toutes les colonnes de T2. Si les tables ont respectivement N et M lignes, la table jointe en aura N * M.

FROM T1 CROSS JOIN T2 est équivalent à FROM T1, T2. C'est aussi équivalent à FROM T1 INNER JOIN T2 ON TRUE (voir ci-dessous).

Jointures qualifiées (qualified joins)
T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 ON boolean_expression
T1 { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2 USING ( join column list )
T1 NATURAL { [INNER] | { LEFT | RIGHT | FULL } [OUTER] } JOIN T2

Les mots INNER et OUTER sont optionnels dans toutes les formes. INNER est la valeur par défaut ; LEFT, RIGHT et FULL impliquent une jointure externe.

La condition de jointure est spécifiée dans la clause ON ou USING, ou implicitement par le mot NATURAL. La condition de jointure détermine les lignes des deux tables sources << correspondantes >>, comme l'explique le paragraphe ci-dessous.

La clause ON est le type le plus général de condition de jointure : elle prend une expression booléenne du même genre que celle utilisée dans une clause WHERE. Une paire de lignes de T1 et T2 correspondent si l'expression ON est évaluée à vraie (true) pour ces deux lignes.

USING est une notation raccourcie : elle prend une liste de noms de colonnes, séparés par des virgules, que les tables jointes doivent avoir en commun, et forme une condition de jointure spécifiant l'égalité de chacune de ces paires de colonnes. De plus, la sortie de JOIN USING a une colonne pour chaque paire égalée de colonnes en entrée. Ces colonnes sont suivies par toutes les autres colonnes de chaque table. De ce fait, USING (a, b, c) est équivalent à ON (t1.a = t2.a AND t1.b = t2.b AND t1.c = t2.c) à la différence que si ON est utilisé, il y a deux colonnes a, b, puis c dans le résultat, alors qu'avec USING, il n'y en aura qu'une de chaque.

Enfin, NATURAL est un format raccourci de USING : il forme une liste USING constituée exactement des noms des colonnes apparaissant à la fois dans les deux tables en entrée. Comme avec USING, ces colonnes n'apparaissent qu'une fois dans la table de sortie.

Les types possibles de jointures qualifiées sont :

INNER JOIN

Pour chaque ligne L1 de T1, la table jointe a une ligne pour chaque ligne de T2 satisfaisant la condition de jointure avec L1.

LEFT OUTER JOIN

En premier lieu, une jointure interne est réalisée. Puis, pour chaque ligne de T1 qui ne satisfait à la condition de jointure avec aucune des lignes de T2, une ligne jointe est ajoutée avec des valeurs nulles dans les colonnes de T2. Du coup, la table jointe a au moins une ligne pour chaque ligne de T1 quelques soient les conditions.

RIGHT OUTER JOIN

En premier lieu, une jointure interne est réalisée. Puis, pour chaque ligne de T2 qui ne satisfait à la condition de jointure avec aucune des lignes de T1, une ligne jointe est ajoutée avec des valeurs nulles dans les colonnes de T1. C'est l'inverse d'une jointure gauche : la table résultante aura une ligne pour chaque ligne de T2 quelques soient les conditions.

FULL OUTER JOIN

En premier lieu, une jointure interne est réalisée. Puis, pour chaque ligne de T1 qui ne satisfait à la condition de jointure avec aucune des lignes de T2, une ligne jointe est ajoutée avec des valeurs nulles dans les colonnes de T2. Enfin, pour chaque ligne de T2 qui ne satisfait à la condition de jointure avec aucune des lignes de T1, une ligne jointe est ajoutée avec des valeurs nulles dans les colonnes de T1.

Des jointures de tout type peuvent être chaînées ou imbriquées : T1 et T2 peuvent être des tables jointes (l'une, l'autre ou les deux). Des parenthèses peuvent être utilisées autour des clauses JOIN pour contrôler l'ordre de jointure. En l'absence de parenthèses, les clauses JOIN s'imbriquent de gauche à droite.

Pour résumer, supposons que nous ayons une table t1

 num | nom
-----+------
   1 | a
   2 | b
   3 | c

et une table t2

 num | valeur
-----+-------
   1 | xxx
   3 | yyy
   5 | zzz

nous obtenons les résultats suivants pour les différentes jointures :

=> SELECT * FROM t1 CROSS JOIN t2;
 num | nom | num | valeur
-----+-----+-----+-------
   1 | a   |   1 | xxx
   1 | a   |   3 | yyy
   1 | a   |   5 | zzz
   2 | b   |   1 | xxx
   2 | b   |   3 | yyy
   2 | b   |   5 | zzz
   3 | c   |   1 | xxx
   3 | c   |   3 | yyy
   3 | c   |   5 | zzz
(9 rows)

=> SELECT * FROM t1 INNER JOIN t2 ON t1.num = t2.num;
 num | nom | num | valeur
-----+-----+-----+-------
   1 | a   |   1 | xxx
   3 | c   |   3 | yyy
(2 rows)

=> SELECT * FROM t1 INNER JOIN t2 USING (num);
 num | nom | valeur
-----+-----+-------
   1 | a   | xxx
   3 | c   | yyy
(2 rows)

=> SELECT * FROM t1 NATURAL INNER JOIN t2;
 num | nom | valeur
-----+-----+-------
   1 | a   | xxx
   3 | c   | yyy
(2 rows)

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num;
 num | nom | num | valeur
-----+-----+-----+-------
   1 | a   |   1 | xxx
   2 | b   |     |
   3 | c   |   3 | yyy
(3 rows)

=> SELECT * FROM t1 LEFT JOIN t2 USING (num);
 num | nom | valeur
-----+-----+-------
   1 | a   | xxx
   2 | b   |
   3 | c   | yyy
(3 rows)

=> SELECT * FROM t1 RIGHT JOIN t2 ON t1.num = t2.num;
 num | nom | num | valeur
-----+-----+-----+-------
   1 | a   |   1 | xxx
   3 | c   |   3 | yyy
     |     |   5 | zzz
(3 rows)

=> SELECT * FROM t1 FULL JOIN t2 ON t1.num = t2.num;
 num | nom | num | valeur
-----+-----+-----+-------
   1 | a   |   1 | xxx
   2 | b   |     |
   3 | c   |   3 | yyy
     |     |   5 | zzz
(4 rows)

La condition de jointure spécifiée avec ON peut aussi contenir des conditions sans relation directe avec la jointure. Par exemple :

=> SELECT * FROM t1 LEFT JOIN t2 ON t1.num = t2.num AND t2.value = 'xxx';
 num | nom | num | valeur
-----+-----+-----+-------
   1 | a   |   1 | xxx
   2 | b   |     |
   3 | c   |     |
(3 rows)

7.2.1.2. Alias de table et de colonne

Un nom temporaire peut être donné aux tables et aux références de tables complexes. Celui-ci sera ensuite utilisé pour faire référence à la table dérivée dans la suite de l'exécution. On appelle cela un alias de table.

Pour créer un alias de table, il suffit d'écrire

FROM reference_table AS alias

ou

FROM reference_table alias

Le mot clé AS n'est pas nécessaire. alias peut être n'importe quel identifiant.

Une application typique des alias de table est l'affectation d'identifiants courts pour les noms de table longs, ce qui permet de garder des clauses de jointures lisibles. Par exemple :

SELECT * FROM nom_de_table_tres_long n JOIN un_autre_nom_assez_long u ON n.id = u.num;

L'alias devient le nouveau nom de la référence de la table pour la requête courante -- il n'est plus possible de référencer la table avec son nom d'origine. Du coup,

SELECT * FROM ma_table AS m WHERE ma_table.a > 5;

n'est pas une syntaxe SQL valide. En réalité, (c'est une extension du standard proposée par PostgreSQL) une référence de table implicite est ajoutée à la clause FROM, de façon à ce que la requête soit exécutée comme si elle était écrite ainsi

SELECT * FROM ma_table AS m, ma_table AS ma_table WHERE ma_table.a > 5;

ce qui aboutira à une jointure croisée. Et ce n'est habituellement pas ce qui est attendu.

Les alias de table sont essentiellement disponibles pour faciliter l'écriture de requêtes mais ils apparaissent nécessaires pour joindre une table avec elle-même, par exemple

SELECT * FROM ma_table AS a CROSS JOIN ma_table AS b ...

De plus, un alias est requis si la référence de table est une sous-requête (voir Section 7.2.1.3).

Les parenthèses sont utilisées pour lever les ambiguités. L'instruction suivante affectera le résultat de la jointure à l'alias b, contrairement à l'exemple précédent :

SELECT * FROM (ma_table AS a CROSS JOIN ma_table) AS b ...

Une autre forme d'alias de tables donne des noms temporaires aux colonnes des tables :

FROM reference_table [AS] alias ( colonne1 [, colonne2 [,...]] )

Si le nombre d'alias de colonnes spécifiés est plus petit que le nombre de colonnes dont dispose la table réelle, les colonnes résiduelles ne sont pas renommées. Cette syntaxe est particulièrement utile dans le cas d'auto-jointures ou dans le cas de sous-requêtes.

Quand un alias est appliqué à la sortie d'une clause JOIN, quelqu'en soit la forme, l'alias cache le nom original à l'intérieur du JOIN. Par exemple,

SELECT a.* FROM ma_table AS a JOIN ta_table AS b ON ...

est du SQL valide, mais

SELECT a.* FROM (ma_table AS a JOIN ta_table AS b ON ...) AS c

n'est pas valide : l'alias de table a n'est pas visible en dehors de l'alias c.

7.2.1.3. Sous-requêtes

Une sous-requête spécifiant une table dérivée doit être écrite entre parenthèses et doit se voir affecter un alias de table. (Voir Section 7.2.1.2.) Par exemple :

FROM (SELECT * FROM table1) AS nom_alias

Cet exemple est équivalent à FROM table1 AS nom_alias. Des cas plus intéressants, qui ne peuvent pas être réduits à une jointure pleine, surviennent quand la sous-requête implique un groupement ou un agrégat.

7.2.1.4. Fonctions de table

Les fonctions de table sont des fonctions produisant un ensemble de lignes composées de types de données basiques (types scalaires) ou de types de données composites (lignes de table). Elles sont utilisées de la même façon qu'une table, une vue ou une sous-requête de la clause FROM d'une requête. Les colonnes retournées par les fonctions de table peuvent être incluses dans une clause SELECT, JOIN ou WHERE de la même manière qu'une colonne de table, vue ou sous-requête.

Si une fonction de table renvoie un type de données basique, la colonne de résultat est nommée comme la fonction. Si la fonction renvoie un type composite, les colonnes résultantes ont le même nom que les attributs individuels du type.

Une fonction de table peut avoir un alias dans la clause FROM. Dans le cas contraire, le nom de la fonction est utilisé comme nom de table résultante.

Quelques exemples :

CREATE TABLE truc (trucid int, trucsousid int, trucnom text);

CREATE FUNCTION recuptruc(int) RETURNS SETOF truc AS '
    SELECT * FROM truc WHERE trucid = $1;
' LANGUAGE SQL;

SELECT * FROM recuptruc(1) AS t1;

SELECT * FROM truc
    WHERE trucsousid IN (select trucsousid from recuptruc(truc.trucid) z
                           where z.trucid = truc.trucid);

CREATE VIEW vue_recuptruc AS SELECT * FROM recuptruc(1);
SELECT * FROM vue_recuptruc;

Dans certains cas, il est utile de définir des fonctions de table qui renvoient des ensembles de colonnes différents suivant la façon dont elles sont appelées. Pour ce faire, la fonction de table est déclarée comme renvoyant le pseudotype record. Quand une telle fonction est utilisée dans une requête, la structure de ligne attendue doit être spécifiée dans la requête elle-même. De cette façon, le système saura comment analyser et planifier la requête. Par exemple :

SELECT *
    FROM dblink('dbname=mabd', 'select proname, prosrc from pg_proc')
      AS t1(proname name, prosrc text)
    WHERE proname LIKE 'bytea%';

La fonction dblink exécute une requête distante (voir contrib/dblink). Elle a été déclarée avec un type de retour record, puisqu'elle peut être utilisée pour tout type de requête. L'ensemble de colonnes réelles doit être spécifié dans la requête appelante de sorte que l'analyseur sache, par exemple, comment étendre *.

7.2.2. La clause WHERE

La syntaxe de la clause WHERE est

WHERE condition_recherche

condition_recherche est une expression de valeur, telle que définie dans Section 4.2, qui retourne une valeur de type boolean.

Après le traitement de la clause FROM, chaque ligne de la table virtuelle dérivée est confrontée à 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 abandonnée. La condition de recherche fait, en général, référence à une colonne de la table engendrée dans la clause FROM ; ceci n'est pas impératif mais, dans le cas contraire, la clause WHERE s'avère inutile.

Note : Avant l'implémentation de la syntaxe JOIN, il était nécessaire de placer la condition de jointure d'une jointure interne dans la clause WHERE. Les expressions de tables suivantes sont équivalentes :

FROM a, b WHERE a.id = b.id AND b.val > 5

et

FROM a INNER JOIN b ON (a.id = b.id) WHERE b.val > 5

ou même

FROM a NATURAL JOIN b WHERE b.val > 5

L'utilisation de l'une par rapport à l'autre est affaire de style. La syntaxe JOIN dans la clause FROM n'est pas forcément très portable vers les autres systèmes de gestion de bases de données SQL. Pour les jointures externes, il n'y qu'une seule possibilité : elles doivent être faites dans la clause FROM. Une clause ON/USING d'une jointure externe n'est pas équivalente à une condition WHERE parce qu'elle détermine l'ajout de lignes (pour les lignes en entrée sans correspondance) ainsi que la suppression de lignes dans le résultat final.

Voici quelques exemples de clauses WHERE :

SELECT ... FROM fdt WHERE c1 > 5

SELECT ... FROM fdt WHERE c1 IN (1, 2, 3)

SELECT ... FROM fdt WHERE c1 IN (SELECT c1 FROM t2)

SELECT ... FROM fdt WHERE c1 IN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10)

SELECT ... FROM fdt WHERE c1 BETWEEN (SELECT c3 FROM t2 WHERE c2 = fdt.c1 + 10) AND 100

SELECT ... FROM fdt WHERE EXISTS (SELECT c1 FROM t2 WHERE c2 > fdt.c1)

fdt est la table dérivée dans la clause FROM. Les lignes qui ne correspondent pas à la condition de la recherche de la clause WHERE sont éliminées de la table fdt. On peut noter l'utilisation de sous-requêtes scalaires comme expressions de valeurs. A l'instar de n'importe quelle autre requête, les sous-requêtes peuvent employer des expressions de tables complexes. On peut également noter la façon dont fdt est référencée dans les sous-requêtes. Qualifier c1 sous la forme fdt.c1 n'est nécessaire que si c1 est aussi le nom d'une colonne dans la table dérivée en entrée de la sous-requête. Mais qualifier le nom de colonne, même lorsque cela n'est pas nécessaire, ajoute à la clarté. Cet exemple montre comment le nommage de colonne d'une requête externe est étendu dans les requêtes internes.

7.2.3. Les clauses GROUP BY et HAVING

Après avoir passé le filtre WHERE, la table d'entrée dérivée peut faire l'objet d'un regroupement par l'utilisation de la clause GROUP BY et d'une élimination de lignes groupées par la clause HAVING.

SELECT liste_selection
    FROM ...
    [WHERE ...]
    GROUP BY reference_colonne_regroupement [, reference_colonne_regroupement]...

La clause GROUP BY est utilisée pour regrouper les lignes d'une table qui partagent les mêmes valeurs pour toutes les colonnes précisées. L'ordre dans lequel ces colonnes sont indiquées importe peu. Le but est de réduire chaque groupe de lignes partageant des valeurs communes en une seule ligne de groupe représentative de toutes les lignes du groupe. Ceci est fait pour éliminer les redondances dans la sortie et/ou pour calculer les agrégats s'appliquant à ces groupes. Par exemple :

=> SELECT * FROM test1;
 x | y
---+---
 a | 3
 c | 2
 b | 5
 a | 1
(4 rows)

=> SELECT x FROM test1 GROUP BY x;
 x
---
 a
 b
 c
(3 rows)

Dans la seconde requête, il n'est pas possible d'écrire SELECT * FROM test1 GROUP BY x parce qu'il n'existe pas de valeur unique pour la colonne y pouvant être associée avec chaque autre groupe. Les colonnes de regroupement peuvent être référencées dans la liste de sélection car elles ont une valeur constante connue par groupe.

En général, si une table est groupée, il ne peut pas être fait référence aux colonnes qui ne sont pas utilisées dans le regroupement sinon dans les expressions d'agrégats. Voici un exemple d'expressions d'agrégat :

=> SELECT x, sum(y) FROM test1 GROUP BY x;
 x | sum
---+-----
 a |   4
 b |   5
 c |   2
(3 rows)

Ici, sum est une fonction d'agrégat qui calcule une valeur unique pour tout le groupe. On pourra touver plus d'informations sur les fonctions d'agrégats disponibles dans Section 9.15.

Astuce : Le regroupement sans expression d'agrégats calcule effectivement l'ensemble des valeurs distinctes d'une colonne. Le même résultat peut être obtenu à l'aide de la clause DISTINCT (voir Section 7.3.3).

L'exemple suivant calcule les ventes totales pour chaque produit (plutôt que le total des ventes sur tous les produits).

SELECT produit_id, p.nom, (sum(v.unite) * p.prix) AS ventes
    FROM produits p LEFT JOIN ventes v USING (produit_id)
    GROUP BY produit_id, p.nom, p.prix;

Dans cet exemple, les colonnes produit_id, p.nom et p.prix doivent se trouver dans la clause GROUP BY car elles sont référencées dans la liste de sélection de la requête. (Suivant la façon dont est conçue la table produits, le nom et le prix peuvent être totalement dépendants de l'ID du produit et de ce fait, les regroupements supplémentaires ne sont théoriquement pas nécessaires. Mais ceci n'est pas encore implémenté.) La colonne s.unite n'a pas besoin d'être dans la liste GROUP BY car elle est utilisée uniquement dans l'expression de l'agrégat (sum(...)) représentant les ventes d'un produit. Pour chaque produit, la requête renvoie une ligne de résumé sur les ventes de ce produit.

La norme SQL standard ne définit pour GROUP BY que le groupement par colonnes de la table source. PostgreSQL autorise également GROUP BY à grouper par colonnes de la liste de sélection. Grouper par expressions de valeurs plutôt que par simples noms de colonne est également autorisé.

Si une table a été groupée en utilisant la clause GROUP BY mais que seuls certains groupes ont un intérêt, la clause HAVING peut être utilisée, à l'instar d'une clause WHERE, pour éliminer les groupes d'une table groupée. La syntaxe est la suivante :

SELECT liste_selection FROM ... [WHERE ...] GROUP BY ... HAVING expression_booléenne

Les expressions de la clause HAVING peuvent faire référence autant à des expressions groupées qu'à des expressions non groupées (impliquant nécessairement une fonction d'agrégat).

Exemple :

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING sum(y) > 3;
 x | sum
---+-----
 a |   4
 b |   5
(2 rows)

=> SELECT x, sum(y) FROM test1 GROUP BY x HAVING x < 'c';
 x | sum
---+-----
 a |   4
 b |   5
(2 rows)

L'exemple suivant est plus réaliste :

SELECT produit_id, p.nom, (sum(v.unite) * (p.prix - p.cout)) AS profit
    FROM produits p LEFT JOIN ventes v USING (produit_id)
    WHERE v.date > CURRENT_DATE - INTERVAL '4 weeks'
    GROUP BY produit_id, p.nom, p.prix, p.cout
    HAVING sum(p.prix * v.unite) > 5000;

Dans l'exemple ci-dessus, la clause WHERE sélectionne les lignes à partir d'une colonne qui n'est pas groupée (l'expression est vraie seulement pour les ventes des quatre dernières semaines) alors que la clause HAVING restreint la sortie aux groupes dont le total des ventes dépasse 5000. Notez que les expressions d'agrégats n'ont pas besoin d'être les mêmes dans toutes les parties d'une requête.