🔗 Les Jointures SQL (JOIN)

Maîtrisez l'art de combiner des données provenant de plusieurs tables

⏱️ 2-3 heures de lecture 📚 Niveau intermédiaire 💾 SQL & SQLite

Pourquoi les jointures ?

💡

Le problème

Dans une base de données relationnelle, les informations sont souvent réparties dans plusieurs tables pour éviter la redondance et améliorer l'organisation. Les jointures (JOIN) permettent de récupérer ces données en combinant plusieurs tables dans une seule requête.

Exemple concret

Imaginez une bibliothèque avec deux tables :

Table : auteur

id nom pays
1 Victor Hugo France
2 J.K. Rowling Royaume-Uni
3 Haruki Murakami Japon

Table : livre

id titre auteur_id annee
1 Les Misérables 1 1862
2 Harry Potter 2 1997
3 1Q84 3 2009

Question : Comment récupérer la liste des livres avec le nom de l'auteur en une seule requête ?
Réponse : Avec une jointure ! 🎯

Clés primaires et clés étrangères

auteur id (PK) nom pays 1 Victor Hugo FR 2 J.K Rowling UK 3 Murakami JP livre id (PK) titre auteur_id (FK) 1 Les Mis 1 2 HP 2 3 1Q84 3 Clé Primaire (PK) Clé Étrangère (FK)PK 🔑 🔗
🔑

Vocabulaire clé

  • Clé primaire (PK) : Identifiant unique d'une ligne dans une table
  • Clé étrangère (FK) : Référence à la clé primaire d'une autre table
  • Relation : Lien logique entre deux tables via une clé étrangère

Types de relations entre tables

1️⃣→1️⃣

Un à Un (1:1)

Une ligne de la table A correspond à une seule ligne de la table B.

Exemple :
utilisateurprofil
Un utilisateur a un seul profil
utilisateur [1] ←→ [1] profil
1️⃣→🔢

Un à Plusieurs (1:N)

Une ligne de la table A peut correspondre à plusieurs lignes de la table B.

Exemple :
auteurlivre
Un auteur écrit plusieurs livres
auteur [1] ←→ [N] livres
🔢→🔢

Plusieurs à Plusieurs (N:M)

Plusieurs lignes de A correspondent à plusieurs lignes de B. Nécessite une table de jonction.

Exemple :
etudiantcours
Un étudiant suit plusieurs cours, un cours a plusieurs étudiants
etudiant [N] ←→ [M] cours
via table: inscription

Table de jonction (pour relations N:M)

etudiant

id nom
1 Alice
2 Bob

inscription

etudiant_id cours_id
1 101
1 102
2 101

cours

id nom
101 Math
102 Anglais

INNER JOIN - Jointure interne

INNER JOIN

Retourne uniquement les lignes qui ont une correspondance dans les deux tables.

Syntaxe

Syntaxe de base
SELECT table1.colonne1, table2.colonne2
FROM table1
INNER JOIN table2 ON table1.cle = table2.cle;

Exemple pratique

📝 Livres avec leurs auteurs
SELECT 
    livre.titre,
    livre.annee,
    auteur.nom AS nom_auteur,
    auteur.pays
FROM livre
INNER JOIN auteur ON livre.auteur_id = auteur.id;

📊 Résultat

titre annee nom_auteur pays
Les Misérables 1862 Victor Hugo France
Harry Potter 1997 J.K. Rowling Royaume-Uni
1Q84 2009 Haruki Murakami Japon
⚠️

Important

Si un livre n'a pas d'auteur assigné (auteur_id = NULL), il ne sera pas inclus dans le résultat d'un INNER JOIN.

Alias de tables

Pour simplifier la lecture, utilisez des alias :

Avec alias
SELECT 
    l.titre,
    a.nom AS auteur
FROM livre AS l
INNER JOIN auteur AS a ON l.auteur_id = a.id;

LEFT JOIN - Jointure gauche

LEFT JOIN

Retourne toutes les lignes de la table de gauche, même si elles n'ont pas de correspondance à droite.

Syntaxe

Syntaxe de base
SELECT table1.colonne1, table2.colonne2
FROM table1
LEFT JOIN table2 ON table1.cle = table2.cle;

Exemple

Supposons qu'un livre n'a pas d'auteur assigné :

Table : livre

id titre auteur_id
1 Les Misérables 1
2 Livre Orphelin NULL
📝 Tous les livres, avec ou sans auteur
SELECT 
    l.titre,
    a.nom AS auteur
FROM livre AS l
LEFT JOIN auteur AS a ON l.auteur_id = a.id;

📊 Résultat

titre auteur
Les Misérables Victor Hugo
Livre Orphelin NULL

⚠️ Le livre sans auteur apparaît quand même !

💡 Quand utiliser LEFT JOIN ?

  • Trouver les enregistrements sans correspondance
  • Afficher toutes les données de la table principale
  • Compter les relations (ex: livres par auteur, y compris 0)

Trouver les orphelins

Livres sans auteur
SELECT l.titre
FROM livre AS l
LEFT JOIN auteur AS a ON l.auteur_id = a.id
WHERE a.id IS NULL;

RIGHT JOIN - Jointure droite

RIGHT JOIN

Retourne toutes les lignes de la table de droite, même si elles n'ont pas de correspondance à gauche.

⚠️

SQLite et RIGHT JOIN

SQLite ne supporte pas RIGHT JOIN, mais vous pouvez obtenir le même résultat en inversant les tables avec LEFT JOIN :

RIGHT JOINLEFT JOIN inversé
Syntaxe (MySQL, PostgreSQL)
-- RIGHT JOIN (non supporté dans SQLite)
SELECT a.nom, l.titre
FROM livre AS l
RIGHT JOIN auteur AS a ON l.auteur_id = a.id;

-- Équivalent avec LEFT JOIN (fonctionne dans SQLite)
SELECT a.nom, l.titre
FROM auteur AS a
LEFT JOIN livre AS l ON l.auteur_id = a.id;

Exemple : Auteurs sans livres

Tous les auteurs, même sans livres
SELECT 
    a.nom AS auteur,
    COUNT(l.id) AS nombre_livres
FROM auteur AS a
LEFT JOIN livre AS l ON l.auteur_id = a.id
GROUP BY a.id, a.nom;

FULL OUTER JOIN - Jointure complète

FULL OUTER JOIN

Retourne toutes les lignes des deux tables, avec ou sans correspondance.

⚠️

SQLite et FULL OUTER JOIN

SQLite ne supporte pas FULL OUTER JOIN, mais vous pouvez le simuler avec UNION :

Simulation avec UNION
-- FULL OUTER JOIN simulé dans SQLite
SELECT l.titre, a.nom
FROM livre AS l
LEFT JOIN auteur AS a ON l.auteur_id = a.id

UNION

SELECT l.titre, a.nom
FROM auteur AS a
LEFT JOIN livre AS l ON l.auteur_id = a.id;

📊 Résultat

Cette requête retourne :

  • Tous les livres avec leurs auteurs
  • Les livres sans auteur (NULL)
  • Les auteurs sans livres (NULL)

CROSS JOIN - Produit cartésien

CROSS JOIN

Combine chaque ligne de la table A avec chaque ligne de la table B. Produit cartésien.

⚠️

Attention !

Si la table A a 10 lignes et la table B a 20 lignes, le résultat aura 10 × 20 = 200 lignes !

Syntaxe
SELECT *
FROM table1
CROSS JOIN table2;

-- Équivalent à :
SELECT *
FROM table1, table2;

Exemple : Combiner des couleurs et des tailles

couleurs

couleur
Rouge
Bleu

tailles

taille
S
M
Produit cartésien
SELECT c.couleur, t.taille
FROM couleurs AS c
CROSS JOIN tailles AS t;

📊 Résultat (2 × 2 = 4 lignes)

couleur taille
Rouge S
Rouge M
Bleu S
Bleu M

💡 Cas d'utilisation

  • Générer toutes les combinaisons possibles
  • Créer des variations de produits (e-commerce)
  • Tests combinatoires

Jointures multiples

Vous pouvez combiner plusieurs JOIN dans une seule requête pour relier 3 tables ou plus.

Exemple : Bibliothèque complète

auteur

id nom
1 Hugo

livres

id titre auteur_id categorie_id
1 Les Mis 1 10

categorie

id nom
10 Roman
📝 Livres avec auteurs ET catégories
SELECT 
    l.titre,
    a.nom AS auteur,
    c.nom AS categorie
FROM livre AS l
INNER JOIN auteur AS a ON l.auteur_id = a.id
INNER JOIN categorie AS c ON l.categorie_id = c.id;

Relations N:M avec table de jonction

Pour une relation plusieurs-à-plusieurs, vous devez joindre 3 tables :

Étudiants et leurs cours
SELECT 
    e.nom AS etudiant,
    c.nom AS cours,
    i.note
FROM etudiant AS e
INNER JOIN inscription AS i ON e.id = i.etudiant_id
INNER JOIN cours AS c ON i.cours_id = c.id
ORDER BY e.nom, c.nom;

📊 Résultat

etudiant cours note
Alice Mathématiques 85
Alice Anglais 92
Bob Mathématiques 78

Mélanger différents types de JOIN

INNER JOIN + LEFT JOIN
SELECT 
    l.titre,
    a.nom AS auteur,
    c.nom AS categorie
FROM livre AS l
INNER JOIN auteur AS a ON l.auteur_id = a.id
LEFT JOIN categorie AS c ON l.categorie_id = c.id;
-- Tous les livres avec auteur, même sans catégorie

Optimisation des jointures

🔑

1. Utilisez des index

Créez des index sur les colonnes de jointure (clés étrangères) pour accélérer les requêtes.

CREATE INDEX idx_livre_auteur 
ON livre(auteur_id);
🎯

2. Sélectionnez uniquement ce dont vous avez besoin

Évitez SELECT *, spécifiez les colonnes exactes.

-- ❌ Mauvais
SELECT * FROM livre JOIN auteur...

-- ✅ Bon
SELECT l.titre, a.nom FROM livre...
🔢

3. Utilisez LIMIT

Limitez le nombre de résultats si vous n'avez pas besoin de tout.

SELECT l.titre, a.nom
FROM livre l
JOIN auteur a ON l.auteur_id = a.id
LIMIT 10;
📊

4. Analysez vos requêtes

Utilisez EXPLAIN QUERY PLAN pour voir comment SQLite exécute votre requête.

EXPLAIN QUERY PLAN
SELECT l.titre, a.nom
FROM livre l
JOIN auteur a ON l.auteur_id = a.id;
💡

Bonnes pratiques

  • Préférez INNER JOIN à LEFT JOIN quand c'est possible (plus rapide)
  • Filtrez avec WHERE avant de joindre quand possible
  • Utilisez des alias courts et lisibles
  • Testez vos requêtes avec de vraies données volumineuses

Comparer les performances

Mesurer le temps d'exécution (SQLite)
.timer ON

SELECT l.titre, a.nom
FROM livre l
INNER JOIN auteur a ON l.auteur_id = a.id;

-- Affiche : Run Time: real 0.001 user 0.000000 sys 0.000000

Jointures dans une API PHP

Voyons comment utiliser les jointures SQL dans une API REST avec PHP et PDO.

Exemple : API de bibliothèque

📁 api/livre.php
<?php
header('Content-Type: application/json; charset=utf-8');

// Connexion SQLite
try {
    $pdo = new PDO('sqlite:bibliotheque.db');
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    http_response_code(500);
    echo json_encode(['error' => 'Erreur de connexion']);
    exit;
}

// Récupérer tous les livres avec leurs auteurs et catégories
$sql = "
    SELECT 
        l.id,
        l.titre,
        l.annee,
        l.prix,
        a.nom AS auteur_nom,
        a.pays AS auteur_pays,
        c.nom AS categorie
    FROM livre AS l
    INNER JOIN auteur AS a ON l.auteur_id = a.id
    LEFT JOIN categorie AS c ON l.categorie_id = c.id
    ORDER BY l.titre
";

$stmt = $pdo->query($sql);
$livres = $stmt->fetchAll(PDO::FETCH_ASSOC);

// Retourner JSON
echo json_encode([
    'success' => true,
    'data' => $livres,
    'count' => count($livres)
], JSON_PRETTY_PRINT);
?>

📊 Réponse JSON

{
  "success": true,
  "data": [
    {
      "id": 1,
      "titre": "Les Misérables",
      "annee": 1862,
      "prix": 15.99,
      "auteur_nom": "Victor Hugo",
      "auteur_pays": "France",
      "categorie": "Roman"
    },
    {
      "id": 2,
      "titre": "Harry Potter",
      "annee": 1997,
      "prix": 12.99,
      "auteur_nom": "J.K. Rowling",
      "auteur_pays": "Royaume-Uni",
      "categorie": "Fantasy"
    }
  ],
  "count": 2
}

Endpoint avec paramètres

Livres par auteur
<?php
// GET /api/auteur/{id}/livre

$auteurId = isset($_GET['id']) ? (int)$_GET['id'] : 0;

$sql = "
    SELECT 
        l.id,
        l.titre,
        l.annee,
        a.nom AS auteur
    FROM livre AS l
    INNER JOIN auteur AS a ON l.auteur_id = a.id
    WHERE a.id = :auteur_id
    ORDER BY l.annee DESC
";

$stmt = $pdo->prepare($sql);
$stmt->execute([':auteur_id' => $auteurId]);
$livres = $stmt->fetchAll(PDO::FETCH_ASSOC);

if (empty($livres)) {
    http_response_code(404);
    echo json_encode(['error' => 'Aucun livre trouvé pour cet auteur']);
    exit;
}

echo json_encode([
    'success' => true,
    'data' => $livres
], JSON_PRETTY_PRINT);
?>

Statistiques avec GROUP BY

Nombre de livres par auteur
<?php
$sql = "
    SELECT 
        a.id,
        a.nom,
        a.pays,
        COUNT(l.id) AS nombre_livres
    FROM auteur AS a
    LEFT JOIN livre AS l ON l.auteur_id = a.id
    GROUP BY a.id, a.nom, a.pays
    ORDER BY nombre_livres DESC
";

$stmt = $pdo->query($sql);
$stats = $stmt->fetchAll(PDO::FETCH_ASSOC);

echo json_encode($stats, JSON_PRETTY_PRINT);
?>
💡

Conseil

Dans une API REST, structurez vos réponses pour inclure les relations imbriquées quand c'est logique. Par exemple, un auteur avec sa liste de livres.

Exercices pratiques

Exercice 1 Facile

Première jointure simple

Base de données : Créez deux tables (produit et categorie)

CREATE TABLE categorie (
    id INTEGER PRIMARY KEY,
    nom TEXT NOT NULL
);

CREATE TABLE produit (
    id INTEGER PRIMARY KEY,
    nom TEXT NOT NULL,
    prix REAL,
    categorie_id INTEGER,
    FOREIGN KEY (categorie_id) REFERENCES categorie(id)
);

Tâche : Écrivez une requête INNER JOIN pour afficher tous les produits avec leur catégorie.

💡 Utilisez INNER JOIN categorie ON ...
Exercice 2 Facile

Compter avec GROUP BY

Tâche : Comptez le nombre de produits par catégorie.

💡 Utilisez COUNT(p.id) et GROUP BY c.id
Exercice 3 Moyen

Produits orphelins

Tâche : Trouvez tous les produits qui n'ont pas de catégorie assignée.

💡 Utilisez LEFT JOIN et WHERE categorie_id IS NULL
Exercice 4 Moyen

Relation N:M - Commandes

Contexte : Créez un système de commandes :

  • Table clients : id, nom
  • Table produit : id, nom, prix
  • Table commandes : id, client_id, date
  • Table commande_produit : commande_id, produit_id, quantite

Tâche : Écrivez une requête qui affiche toutes les commandes avec :

  • Le nom du client
  • Les produits commandés
  • Les quantités
  • Le total de chaque commande
Exercice 5 Difficile

API avec jointures

Tâche : Créez une API REST en PHP avec les endpoints suivants :

  • GET /api/categorie : Liste des catégories avec le nombre de produits
  • GET /api/categorie/{id}/produit : Produits d'une catégorie
  • GET /api/produit : Produits avec leur catégorie (pagination)
  • GET /api/stat : Statistiques (total produits, moyenne prix par catégorie, etc.)
🌟 Bonus : Ajoutez un paramètre de recherche et de tri
Projet final Difficile

Système de blog complet

Objectif : Créez une base de données et une API pour un blog

Structure :

  • utilisateur : id, nom, email
  • article : id, titre, contenu, auteur_id, categorie_id, date
  • categorie : id, nom
  • commentaire : id, article_id, auteur_id, contenu, date
  • tag : id, nom
  • article_tag : article_id, tag_id

Requêtes à implémenter :

  • Articles avec auteurs, catégories et tags
  • Commentaires d'un article avec infos auteur
  • Articles par tag
  • Top 10 auteurs (plus d'articles)
  • Statistiques complètes

Glossaire

JOIN (Jointure)

Opération SQL qui combine les lignes de deux ou plusieurs tables basées sur une condition de relation.

Clé primaire (Primary Key - PK)

Colonne(s) qui identifie de manière unique chaque ligne d'une table. Ne peut pas être NULL et doit être unique.

Clé étrangère (Foreign Key - FK)

Colonne qui établit un lien entre deux tables en référençant la clé primaire d'une autre table.

INNER JOIN

Retourne uniquement les lignes qui ont une correspondance dans les deux tables.

LEFT JOIN (LEFT OUTER JOIN)

Retourne toutes les lignes de la table de gauche, avec les correspondances de la table de droite (NULL si aucune correspondance).

RIGHT JOIN (RIGHT OUTER JOIN)

Retourne toutes les lignes de la table de droite, avec les correspondances de la table de gauche. Non supporté dans SQLite.

FULL OUTER JOIN

Retourne toutes les lignes des deux tables, avec ou sans correspondance. Non supporté dans SQLite (utilisez UNION).

CROSS JOIN

Produit cartésien : chaque ligne de la table A est combinée avec chaque ligne de la table B.

Table de jonction

Table intermédiaire utilisée pour implémenter une relation plusieurs-à-plusieurs (N:M).

Relation 1:1 (Un à Un)

Une ligne de la table A correspond à exactement une ligne de la table B.

Relation 1:N (Un à Plusieurs)

Une ligne de la table A peut correspondre à plusieurs lignes de la table B.

Relation N:M (Plusieurs à Plusieurs)

Plusieurs lignes de A correspondent à plusieurs lignes de B. Nécessite une table de jonction.

Alias de table

Nom court donné à une table pour simplifier les requêtes (ex: FROM livre AS l).

Index

Structure de données qui accélère la recherche et les jointures sur une colonne.