Je suis Charlie

Autres trucs

Accueil

Seulement les RFC

Seulement les fiches de lecture

Ève

Jointures en SQL, quelques notes

Première rédaction de cet article le 6 mai 2009


Je ne vais pas faire le Nième tutoriel sur les jointures SQL (pour cela, je recommande l'excellent article du Wikipédia anglophone), j'essaie juste de documenter deux ou trois choses de base sur elles, pour m'en souvenir la prochaine fois. Si ça peut servir à d'autres...

Une jointure, c'est... joindre deux tables, en général avec un critère de sélection. L'idée de base est de générer des tuples qui combinent les tuples des deux bases. Ce n'est donc pas une opération ensembliste, contrairement à ce qu'à écrit Jeff Atwood.

Avant de commencer, je crée quelques tables pour illustrer la question, une table de personnes et une de livres (le code SQL complet, testé avec PostgreSQL est disponible en ligne) :

CREATE TABLE Authors (
   id SERIAL UNIQUE NOT NULL,
   name TEXT NOT NULL);

CREATE TABLE Books (
   id SERIAL UNIQUE NOT NULL,
   title TEXT NOT NULL,
   author INTEGER REFERENCES Authors(id)); 

Bien sûr, ce schéma de données est ultra-simplifié. Par exemple, il ne permet pas de représenter un livre qui a plusieurs auteurs. Mais peu importe, c'est pour faire des exemples. Le point important à noter est qu'un « auteur » n'est pas forcément référencé par un livre et qu'un livre peut ne pas avoir d'auteur connu (la colonne author peut être NULL).

Voici les données, avec des livres que j'ai apprécie (comme « La horde du contrevent » ou les livres de Fred Vargas) :

essais=> SELECT * FROM Authors;
 id |        name         
----+---------------------
  1 | Stéphane Bortzmeyer
  2 | Fred Vargas
  3 | Ève Demazière
  4 | Alain Damasio
(4 rows)

essais=> SELECT * FROM Books;  
 id |                  title                  | author 
----+-----------------------------------------+--------
  1 | Les cultures noires d'Amérique Centrale |      3
  2 | La horde du contrevent                  |      4
  3 | Pars vite et reviens tard               |      2
  4 | L'homme à l'envers                      |      2
  5 | Bible                                   |       
(5 rows)

(Cherchez le livre sans auteur connu et la personne qui n'a pas écrit de livre.)

Il existe deux grands types de jointures, la jointure interne (INNER JOIN) et l'externe (OUTER JOIN). S'il existe toujours un lien entre les deux tables (si tous les livres ont un auteur et si tous les auteurs sont référencés depuis au moins un livre), il n'existe pas de différence entre les deux types. Mais ce n'est pas le cas en général.

La jointure interne s'écrit avec le mot-clé INNER JOIN et la condition ON (l'ensemble des requêtes de jointure est disponible en ligne, tous les tests ont été faits avec PostgreSQL 8.3) :

essais=> SELECT name, title FROM Books INNER JOIN Authors 
                ON Books.author = Authors.id;
     name      |                  title                  
---------------+-----------------------------------------
 Ève Demazière | Les cultures noires d'Amérique Centrale
 Alain Damasio | La horde du contrevent
 Fred Vargas   | Pars vite et reviens tard
 Fred Vargas   | L'homme à l'envers
(4 rows)

(Le INNER est facultatif, la jointure interne est la jointure par défaut.) Si les deux colonnes sur lesquelles se fait la jointure ont le même nom, on peut utiliser USING au lieu de ON.

Les jointures internes s'écrivent plus fréquemment (mais c'est une question de goût) avec l'ancienne syntaxe :

essais=> SELECT name, title FROM Books, Authors 
                 WHERE Books.author = Authors.id;
     name      |                  title                  
---------------+-----------------------------------------
 Ève Demazière | Les cultures noires d'Amérique Centrale
 Alain Damasio | La horde du contrevent
 Fred Vargas   | Pars vite et reviens tard
 Fred Vargas   | L'homme à l'envers
(4 rows)

Dans les deux cas (c'était juste une différence de syntaxe), le résultat est le même et n'inclus pas les livres sans auteur connu, ni les « auteurs » qui n'ont pas écrit de livre. Une jointure interne ne sélectionne que les tuples qui répondent à la condition de jointure (le ON, dans la nouvelle syntaxe).

Et les jointures externes ? Au contraire des internes, elles produisent également des tuples où la condition de jointure n'est pas remplie. Un mot clé LEFT ou RIGHT est obligatoire. Une jointure gauche garde les tuples de la table indiquée à gauche et vous vous doutez de ce que fera une jointure droite. On peut facilement transformer l'une en l'autre, A LEFT OUTER JOIN B est la même chose que B RIGHT OUTER JOIN A. Je ne montrerai donc que les jointures externes à gauche :

essais=> SELECT name, title FROM Books LEFT OUTER JOIN Authors 
             ON Books.author = Authors.id;
     name      |                  title                  
---------------+-----------------------------------------
 Ève Demazière | Les cultures noires d'Amérique Centrale
 Alain Damasio | La horde du contrevent
 Fred Vargas   | Pars vite et reviens tard
 Fred Vargas   | L'homme à l'envers
               | Bible
(5 rows)

On voit désormais le livre sans auteur. Si la colonne name vide gène :

essais=> SELECT CASE WHEN name IS NULL THEN 'Unknown author' ELSE name END, 
                title FROM Books LEFT OUTER JOIN Authors 
            ON Books.author = Authors.id;
      name      |                  title                  
----------------+-----------------------------------------
 Ève Demazière  | Les cultures noires d'Amérique Centrale
 Alain Damasio  | La horde du contrevent
 Fred Vargas    | Pars vite et reviens tard
 Fred Vargas    | L'homme à l'envers
 Unknown author | Bible
(5 rows)

Et si on veut garder, non pas les livres sans auteur mais les personnes sans livres, on utilise une jointure externe à droite ou bien, tout simplement, on inverse les tables :

essais=> SELECT name, title FROM Authors LEFT OUTER JOIN Books 
                ON Books.author = Authors.id;
        name         |                  title                  
---------------------+-----------------------------------------
 Stéphane Bortzmeyer | 
 Fred Vargas         | Pars vite et reviens tard
 Fred Vargas         | L'homme à l'envers
 Ève Demazière       | Les cultures noires d'Amérique Centrale
 Alain Damasio       | La horde du contrevent
(5 rows)

Donc, contrairement à ce que présente l'article d'Atwood déjà cité, INNER JOIN n'est pas une intersection et OUTER JOIN n'est pas une union (SQL a des opérateurs pour ces opérations ensemblistes mais je ne les utilise pas ici). Les jointures créent des nouveaux tuples, elles ne sélectionnent pas des tuples existants.

Et si on veut aussi bien les livres sans auteurs que les gens qui n'ont pas écrit de livre ? C'est le rôle de la jointure externe complète, FULL OUTER JOIN :

essais=> SELECT name, title FROM Books FULL OUTER JOIN Authors ON Books.author = Authors.id;
        name         |                  title                  
---------------------+-----------------------------------------
 Stéphane Bortzmeyer | 
 Fred Vargas         | Pars vite et reviens tard
 Fred Vargas         | L'homme à l'envers
 Ève Demazière       | Les cultures noires d'Amérique Centrale
 Alain Damasio       | La horde du contrevent
                     | Bible
(6 rows)

Et enfin, pour un exemple réel, emprunté à DNSmezzo, une jointure externe un peu plus compliquée. La table DNS_packets contient des paquets DNS, la table DNS_types, la correspondance entre le numéro de type d'enregistrement, contenu dans la requête, et des chaînes de caractères mnémoniques comme NAPTR ou SRV. On fait une sous-requête SQL pour ne garder que les paquets utiles, puis une jointure externe (car certains paquets contiennent des requêtes pour des types qui ne sont pas enregistrés dans la base IANA et sont donc absents de la table DNS_types) :

dnsmezzo=> SELECT (CASE WHEN type IS NULL THEN qtype::TEXT ELSE type END), 
                   meaning, 
                   count(Results.id) AS requests 
               FROM (SELECT id, qtype FROM dns_packets WHERE 
                                (file=5 or file=13) AND query) AS
				Results 
                 LEFT OUTER JOIN DNS_types ON qtype = value
              GROUP BY qtype, type, meaning ORDER BY requests desc;
  type  |                meaning                 | requests 
--------+----------------------------------------+----------
 MX     | mail exchange                          |   983180
 A      | a host address                         |   847228
 AAAA   | IP6 Address                            |   129656
 NS     | an authoritative name server           |    13583
 SOA    | marks the start of a zone of authority |    10562
 TXT    | text strings                           |    10348
 255    |                                        |     9125
 38     |                                        |     8440
 SRV    | Server Selection                       |     3300
 SPF    |                                        |      677
 PTR    | a domain name pointer                  |      384
 CNAME  | the canonical name for an alias        |      351
 DNSKEY | DNSKEY                                 |      323
 0      |                                        |       39
 26226  |                                        |       11
 NAPTR  | Naming Authority Pointer               |       11
 HINFO  | host information                       |        7
 NSEC   | NSEC                                   |        7
 8808   |                                        |        1
 14184  |                                        |        1
 3840   |                                        |        1
 54312  |                                        |        1
 13203  |                                        |        1
(23 rows)

Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)

Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)