Introduction

Les jointures permettent de combiner des données provenant de plusieurs tables en fonction de relations logiques. C'est un concept fondamental pour travailler avec des bases de données relationnelles où les informations sont réparties dans différentes tables pour éviter la redondance et maintenir l'intégrité des données.

Une jointure lie les lignes de deux tables ou plus en utilisant une ou plusieurs colonnes communes, généralement une clé primaire d'une table et une clé étrangère d'une autre table. Sans les jointures, il serait impossible de récupérer des informations complètes dans une base de données normalisée.

Pourquoi les jointures sont importantes : Dans une base relationnelle bien conçue, les données sont séparées en tables distinctes. Par exemple, les informations clients sont dans une table Customers, les commandes dans Orders, et les produits dans Products. Les jointures permettent de récupérer toutes ces informations ensemble.

INNER JOIN

L'INNER JOIN retourne uniquement les lignes qui ont une correspondance dans les deux tables. Si une ligne d'une table n'a pas de correspondance dans l'autre table, elle n'apparaît pas dans le résultat. C'est le type de jointure le plus utilisé et le plus restrictif.

Exemple 1 : Clients avec leurs commandes

SQL
-- Récupérer tous les clients qui ont passé des commandes
SELECT 
    c.CustomerID,
    c.CustomerName,
    c.Email,
    o.OrderID,
    o.OrderDate,
    o.TotalAmount
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID
WHERE o.OrderDate >= '2025-01-01'
ORDER BY o.OrderDate DESC;

Exemple 2 : Détails de commande avec produits

SQL
-- Afficher les détails des commandes avec les noms de produits
SELECT 
    o.OrderID,
    o.OrderDate,
    od.Quantity,
    p.ProductName,
    p.Price,
    (od.Quantity * p.Price) AS LineTotal
FROM Orders o
INNER JOIN OrderDetails od ON o.OrderID = od.OrderID
INNER JOIN Products p ON od.ProductID = p.ProductID
WHERE o.OrderDate BETWEEN '2025-01-01' AND '2025-12-31'
ORDER BY o.OrderID, p.ProductName;

Exemple 3 : Employés de département actif

SQL
-- Liste des employés avec leur département (seulement départements actifs)
SELECT 
    e.EmployeeID,
    e.FirstName + ' ' + e.LastName AS EmployeeName,
    e.Email,
    d.DepartmentName,
    d.Location
FROM Employees e
INNER JOIN Departments d ON e.DepartmentID = d.DepartmentID
WHERE d.IsActive = 1
ORDER BY d.DepartmentName, e.LastName;
Astuce : Utilisez des alias courts (c, o, p) pour rendre vos requêtes plus lisibles et plus rapides à écrire. Préfixez toujours les noms de colonnes avec l'alias pour éviter les ambiguïtés.

LEFT JOIN (LEFT OUTER JOIN)

Le LEFT JOIN retourne toutes les lignes de la table de gauche (première table mentionnée), même si elles n'ont pas de correspondance dans la table de droite. Les valeurs manquantes sont remplacées par NULL. C'est très utile pour trouver des données manquantes ou optionnelles.

Exemple 1 : Tous les clients avec ou sans commandes

SQL
-- Afficher tous les clients et leurs commandes (même sans commande)
SELECT 
    c.CustomerID,
    c.CustomerName,
    c.Email,
    COUNT(o.OrderID) AS TotalOrders,
    COALESCE(SUM(o.TotalAmount), 0) AS TotalSpent
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID
GROUP BY c.CustomerID, c.CustomerName, c.Email
ORDER BY TotalSpent DESC;

Exemple 2 : Trouver les clients sans commandes

SQL
-- Identifier les clients qui n'ont jamais passé de commande
SELECT 
    c.CustomerID,
    c.CustomerName,
    c.Email,
    c.RegistrationDate,
    DATEDIFF(DAY, c.RegistrationDate, GETDATE()) AS DaysSinceRegistration
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID
WHERE o.OrderID IS NULL
ORDER BY c.RegistrationDate;

Cas d'usage typiques

  • Identifier les enregistrements sans relation : Clients sans commandes, produits jamais commandés, employés sans département
  • Données optionnelles : Adresses de livraison facultatives, numéros de téléphone secondaires
  • Statistiques avec valeurs par défaut : Compter les activités même si elles sont nulles
  • Rapports complets : Afficher toutes les entités principales avec leurs relations optionnelles
Astuce : Pour filtrer sur les correspondances manquantes, utilisez WHERE table_droite.colonne IS NULL après le LEFT JOIN. C'est plus efficace que NOT EXISTS dans la plupart des cas.

RIGHT JOIN (RIGHT OUTER JOIN)

Le RIGHT JOIN est l'inverse du LEFT JOIN : il retourne toutes les lignes de la table de droite, même sans correspondance à gauche. En pratique, RIGHT JOIN est rarement utilisé car on préfère inverser l'ordre des tables et utiliser LEFT JOIN, ce qui est plus intuitif à lire.

Exemple : Tous les produits avec ou sans ventes

SQL
-- Afficher tous les produits, même ceux jamais vendus
SELECT 
    p.ProductID,
    p.ProductName,
    p.Price,
    COUNT(od.OrderDetailID) AS TimesSold
FROM OrderDetails od
RIGHT JOIN Products p ON od.ProductID = p.ProductID
GROUP BY p.ProductID, p.ProductName, p.Price
ORDER BY TimesSold DESC, p.ProductName;

-- Version préférée avec LEFT JOIN (plus lisible)
SELECT 
    p.ProductID,
    p.ProductName,
    p.Price,
    COUNT(od.OrderDetailID) AS TimesSold
FROM Products p
LEFT JOIN OrderDetails od ON p.ProductID = od.ProductID
GROUP BY p.ProductID, p.ProductName, p.Price
ORDER BY TimesSold DESC, p.ProductName;
Bonne pratique : RIGHT JOIN peut prêter à confusion lors de la lecture du code. Il est recommandé d'utiliser systématiquement LEFT JOIN en inversant l'ordre des tables si nécessaire. Cela améliore la lisibilité et la maintenabilité du code.

FULL OUTER JOIN

Le FULL OUTER JOIN retourne toutes les lignes des deux tables, avec NULL pour les correspondances manquantes de chaque côté. C'est utile pour comparer deux ensembles de données et identifier ce qui est présent dans l'un, l'autre, ou les deux.

Exemple 1 : Comparaison de deux tables de données

SQL
-- Comparer les produits de deux systèmes
SELECT 
    COALESCE(sys1.ProductCode, sys2.ProductCode) AS ProductCode,
    sys1.ProductName AS System1Name,
    sys1.Price AS System1Price,
    sys2.ProductName AS System2Name,
    sys2.Price AS System2Price,
    CASE 
        WHEN sys1.ProductCode IS NULL THEN 'Seulement Système 2'
        WHEN sys2.ProductCode IS NULL THEN 'Seulement Système 1'
        WHEN sys1.Price != sys2.Price THEN 'Prix différent'
        ELSE 'Identique'
    END AS Status
FROM System1Products sys1
FULL OUTER JOIN System2Products sys2 
    ON sys1.ProductCode = sys2.ProductCode
ORDER BY Status, ProductCode;

Exemple 2 : Audit de données (réconciliation)

SQL
-- Réconciliation entre budget et dépenses réelles
SELECT 
    COALESCE(b.Department, a.Department) AS Department,
    b.BudgetAmount,
    a.ActualAmount,
    CASE 
        WHEN b.BudgetAmount IS NULL THEN 'Pas de budget prévu'
        WHEN a.ActualAmount IS NULL THEN 'Aucune dépense'
        WHEN a.ActualAmount > b.BudgetAmount THEN 'Dépassement'
        ELSE 'Conforme'
    END AS Status,
    COALESCE(a.ActualAmount, 0) - COALESCE(b.BudgetAmount, 0) AS Difference
FROM Budget2025 b
FULL OUTER JOIN Actuals2025 a ON b.Department = a.Department
ORDER BY Difference DESC;

Cas d'usage

  • Audit et réconciliation : Comparer deux systèmes ou périodes
  • Migration de données : Vérifier que toutes les données ont été migrées
  • Synchronisation : Identifier les différences entre deux sources
  • Rapports complets : Afficher tout ce qui existe dans n'importe quelle source

CROSS JOIN

Le CROSS JOIN produit un produit cartésien : chaque ligne de la première table est combinée avec chaque ligne de la seconde table. Si Table1 a 100 lignes et Table2 a 50 lignes, le résultat aura 5000 lignes. Attention aux performances !

Exemple 1 : Génération de toutes les combinaisons de produits

SQL
-- Générer toutes les variations de produits (tailles x couleurs)
SELECT 
    p.ProductID,
    p.ProductName,
    s.SizeName,
    c.ColorName,
    CONCAT(p.ProductName, ' - ', s.SizeName, ' - ', c.ColorName) AS FullVariation,
    p.BasePrice + s.PriceAdjustment + c.PriceAdjustment AS FinalPrice
FROM Products p
CROSS JOIN Sizes s
CROSS JOIN Colors c
WHERE p.CategoryID = 1  -- Seulement vêtements
ORDER BY p.ProductName, s.SizeOrder, c.ColorName;

Exemple 2 : Calendrier de disponibilités

SQL
-- Créer un calendrier de créneaux horaires pour tous les médecins
SELECT 
    d.DoctorID,
    d.DoctorName,
    d.Specialty,
    ts.TimeSlot,
    ts.Duration
FROM Doctors d
CROSS JOIN TimeSlots ts
WHERE d.IsActive = 1
  AND ts.DayOfWeek IN ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday')
ORDER BY d.DoctorName, ts.TimeSlot;

Exemple 3 : Matrice de comparaison

SQL
-- Comparer tous les produits entre eux (paires)
SELECT 
    p1.ProductName AS Product1,
    p2.ProductName AS Product2,
    ABS(p1.Price - p2.Price) AS PriceDifference
FROM Products p1
CROSS JOIN Products p2
WHERE p1.ProductID < p2.ProductID  -- Éviter les doublons (A-B = B-A)
  AND ABS(p1.Price - p2.Price) < 10  -- Seulement produits de prix similaire
ORDER BY PriceDifference;
Attention aux performances ! Un CROSS JOIN entre deux tables de 1000 lignes produit 1'000'000 de lignes ! Utilisez toujours des clauses WHERE pour limiter le résultat. Vérifiez le nombre de lignes avec COUNT(*) avant d'exécuter sur de grandes tables.

Jointures Multiples

Dans une application réelle, vous devez souvent joindre plus de deux tables pour obtenir toutes les informations nécessaires. Les jointures multiples se lisent de haut en bas et s'enchaînent logiquement.

Exemple complet : Système de commandes e-commerce

SQL
-- Rapport complet de commande avec toutes les informations
SELECT 
    -- Informations client
    c.CustomerID,
    c.CustomerName,
    c.Email,
    c.Phone,
    
    -- Informations commande
    o.OrderID,
    o.OrderDate,
    o.OrderStatus,
    
    -- Détails produits
    p.ProductName,
    p.SKU,
    cat.CategoryName,
    
    -- Quantités et prix
    od.Quantity,
    od.UnitPrice,
    (od.Quantity * od.UnitPrice) AS LineTotal,
    
    -- Informations livraison
    sa.AddressLine1,
    sa.City,
    sa.PostalCode,
    sa.Country,
    
    -- Informations paiement
    pm.PaymentMethod,
    pm.TransactionID
    
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID
INNER JOIN OrderDetails od ON o.OrderID = od.OrderID
INNER JOIN Products p ON od.ProductID = p.ProductID
LEFT JOIN Categories cat ON p.CategoryID = cat.CategoryID
LEFT JOIN ShippingAddresses sa ON o.ShippingAddressID = sa.AddressID
LEFT JOIN PaymentMethods pm ON o.PaymentMethodID = pm.PaymentMethodID

WHERE o.OrderDate >= DATEADD(MONTH, -1, GETDATE())  -- Dernier mois
ORDER BY o.OrderDate DESC, o.OrderID, od.OrderDetailID;
Organisation des jointures :
  • Commencez par la table principale (FROM)
  • Ajoutez les tables obligatoires avec INNER JOIN
  • Ajoutez les tables optionnelles avec LEFT JOIN
  • Mettez les filtres dans WHERE après toutes les jointures

SELF JOIN

Un SELF JOIN est une jointure d'une table avec elle-même. C'est particulièrement utile pour les structures hiérarchiques (employés-managers, catégories-sous-catégories) ou pour comparer des lignes au sein de la même table.

Exemple 1 : Hiérarchie employés-managers

SQL
-- Afficher chaque employé avec son manager
SELECT 
    e.EmployeeID,
    e.FirstName + ' ' + e.LastName AS EmployeeName,
    e.Title AS EmployeeTitle,
    e.Salary AS EmployeeSalary,
    m.FirstName + ' ' + m.LastName AS ManagerName,
    m.Title AS ManagerTitle,
    CASE 
        WHEN m.EmployeeID IS NULL THEN 'Top Management'
        ELSE 'Reports to ' + m.FirstName + ' ' + m.LastName
    END AS ReportingStructure
FROM Employees e
LEFT JOIN Employees m ON e.ManagerID = m.EmployeeID
ORDER BY m.LastName, e.LastName;

Exemple 2 : Produits fréquemment achetés ensemble

SQL
-- Trouver les paires de produits achetés dans la même commande
SELECT 
    p1.ProductName AS Product1,
    p2.ProductName AS Product2,
    COUNT(*) AS TimesOrderedTogether,
    COUNT(DISTINCT od1.OrderID) AS NumberOfOrders
FROM OrderDetails od1
INNER JOIN OrderDetails od2 ON od1.OrderID = od2.OrderID
INNER JOIN Products p1 ON od1.ProductID = p1.ProductID
INNER JOIN Products p2 ON od2.ProductID = p2.ProductID
WHERE od1.ProductID < od2.ProductID  -- Éviter doublons (A+B = B+A)
GROUP BY p1.ProductName, p2.ProductName
HAVING COUNT(*) >= 5  -- Au moins 5 fois ensemble
ORDER BY TimesOrderedTogether DESC;

Exemple 3 : Comparaison de salaires entre collègues

SQL
-- Comparer les salaires dans le même département
SELECT 
    e1.FirstName + ' ' + e1.LastName AS Employee,
    e1.Title,
    e1.Salary,
    d.DepartmentName,
    AVG(e2.Salary) AS AvgDeptSalary,
    e1.Salary - AVG(e2.Salary) AS DifferenceFromAverage,
    CASE 
        WHEN e1.Salary > AVG(e2.Salary) THEN 'Au-dessus de la moyenne'
        WHEN e1.Salary < AVG(e2.Salary) THEN 'En-dessous de la moyenne'
        ELSE 'Moyenne'
    END AS Position
FROM Employees e1
INNER JOIN Employees e2 ON e1.DepartmentID = e2.DepartmentID
INNER JOIN Departments d ON e1.DepartmentID = d.DepartmentID
GROUP BY e1.EmployeeID, e1.FirstName, e1.LastName, e1.Title, 
         e1.Salary, d.DepartmentName
ORDER BY d.DepartmentName, e1.Salary DESC;

Bonnes Pratiques

1. Toujours utiliser des alias

Rend le code lisible et évite les ambiguïtés, surtout avec plusieurs jointures.

SQL
-- ❌ Mauvais : sans alias, difficile à lire
SELECT Customers.CustomerName, Orders.OrderDate
FROM Customers
INNER JOIN Orders ON Customers.CustomerID = Orders.CustomerID;

-- ✅ Bon : avec alias courts et clairs
SELECT c.CustomerName, o.OrderDate
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID;

2. Spécifier les colonnes explicitement

Évitez SELECT * dans les jointures pour de meilleures performances et clarté.

SQL
-- ❌ Mauvais : récupère toutes les colonnes
SELECT * FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID;

-- ✅ Bon : colonnes spécifiques nécessaires
SELECT 
    c.CustomerName,
    c.Email,
    o.OrderID,
    o.OrderDate,
    o.TotalAmount
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID;

3. Indexer les colonnes de jointure

Créez des index sur les clés étrangères pour des performances optimales.

SQL
-- Créer des index sur les colonnes de jointure
CREATE INDEX idx_orders_customerid ON Orders(CustomerID);
CREATE INDEX idx_orderdetails_orderid ON OrderDetails(OrderID);
CREATE INDEX idx_orderdetails_productid ON OrderDetails(ProductID);

4. WHERE vs ON : où mettre les conditions ?

SQL
-- Règle : conditions de jointure dans ON, filtres dans WHERE

-- ✅ Correct
SELECT c.CustomerName, o.OrderID
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID
WHERE c.Country = 'Suisse';  -- Filtre sur table principale

-- ⚠️ Attention avec LEFT JOIN
SELECT c.CustomerName, o.OrderID
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID 
    AND o.OrderDate >= '2025-01-01'  -- Condition de jointure
WHERE c.Country = 'Suisse';  -- Filtre après jointure

Utiliser des alias

Courts et significatifs pour lisibilité

Spécifier colonnes

Éviter SELECT * dans jointures

Indexer clés

Index sur colonnes de jointure

WHERE vs ON

Jointures dans ON, filtres dans WHERE

Tableau Récapitulatif

Type de JOIN Description Performance Cas d'usage
INNER JOIN Correspondances uniquement 🟢 Rapide Données obligatoires des deux côtés (commandes validées, employés actifs)
LEFT JOIN Tout de gauche + correspondances 🟡 Moyen Données principales + optionnelles (clients avec/sans commandes)
RIGHT JOIN Tout de droite + correspondances 🟡 Moyen Rarement utilisé (préférer LEFT JOIN inversé)
FULL OUTER Tout des deux tables 🔴 Lent Comparaison, audit, réconciliation de données
CROSS JOIN Produit cartésien 🔴 Très lent Combinaisons, matrices (attention volume !)
SELF JOIN Table avec elle-même 🟡 Variable Hiérarchies, comparaisons internes

Erreurs Courantes

1. Oublier la condition de jointure

SQL
-- ❌ ERREUR : Pas de condition ON = CROSS JOIN accidentel !
SELECT c.CustomerName, o.OrderID
FROM Customers c, Orders o;  -- Ancienne syntaxe, évitez !

-- ✅ Correct : toujours spécifier la condition
SELECT c.CustomerName, o.OrderID
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID;

2. Colonnes ambiguës

SQL
-- ❌ ERREUR : Colonne "Status" existe dans les deux tables
SELECT Status, OrderDate
FROM Orders o
INNER JOIN Shipments s ON o.OrderID = s.OrderID;

-- ✅ Correct : préfixer avec l'alias
SELECT 
    o.Status AS OrderStatus,
    s.Status AS ShipmentStatus,
    o.OrderDate
FROM Orders o
INNER JOIN Shipments s ON o.OrderID = s.OrderID;

3. Confusion WHERE vs ON avec LEFT JOIN

SQL
-- ⚠️ Comportement différent selon WHERE ou ON

-- Exemple 1 : Filtre dans ON (garde tous les clients)
SELECT c.CustomerName, o.OrderID
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID 
    AND o.OrderDate >= '2025-01-01'
-- Résultat : Tous les clients, avec commandes 2025 si elles existent

-- Exemple 2 : Filtre dans WHERE (élimine clients sans commande 2025)
SELECT c.CustomerName, o.OrderID
FROM Customers c
LEFT JOIN Orders o ON c.CustomerID = o.CustomerID
WHERE o.OrderDate >= '2025-01-01'
-- Résultat : Seulement clients ayant commandé en 2025

4. Pas d'index sur les clés de jointure

Sans index sur les colonnes de jointure, la base de données doit scanner toute la table, ce qui est extrêmement lent sur de grandes tables.

SQL
-- Vérifier si une colonne a un index
EXEC sp_helpindex 'Orders';

-- Créer un index si nécessaire
CREATE INDEX idx_orders_customerid ON Orders(CustomerID);
Impact performance : Une jointure sans index peut prendre plusieurs minutes au lieu de quelques millisecondes. Vérifiez toujours les plans d'exécution et créez des index sur les colonnes utilisées dans les conditions de jointure.

Exercices Pratiques

Exercice 1 : Rapport de ventes par client

Énoncé

Créez une requête qui affiche pour chaque client : nom, email, nombre total de commandes, montant total dépensé, et date de dernière commande. Incluez aussi les clients qui n'ont jamais commandé (montant = 0).

Tables : Customers (CustomerID, CustomerName, Email), Orders (OrderID, CustomerID, OrderDate, TotalAmount)

💡 Indice

Utilisez LEFT JOIN pour inclure tous les clients. Utilisez COUNT(), SUM() et MAX() avec GROUP BY. N'oubliez pas COALESCE() pour les clients sans commande.

Exercice 2 : Produits jamais vendus par catégorie

Énoncé

Trouvez tous les produits qui n'ont jamais été vendus, en affichant le nom du produit, la catégorie, le prix et le nombre de jours depuis l'ajout au catalogue. Groupez par catégorie.

Tables : Products (ProductID, ProductName, CategoryID, Price, CreatedDate), Categories (CategoryID, CategoryName), OrderDetails (OrderDetailID, ProductID)

💡 Indice

LEFT JOIN Products avec OrderDetails, puis WHERE OrderDetailID IS NULL. Jointure avec Categories pour le nom. Utilisez DATEDIFF() pour les jours.

Exercice 3 : Hiérarchie complète de l'organisation

Énoncé

Créez un rapport montrant chaque employé avec son manager direct, le département du manager, et le nombre d'employés qui rapportent directement à cet employé. Incluez aussi les employés sans manager (top management).

Tables : Employees (EmployeeID, FirstName, LastName, ManagerID, DepartmentID), Departments (DepartmentID, DepartmentName)

💡 Indice

SELF JOIN sur Employees pour employé-manager. LEFT JOIN car certains n'ont pas de manager. Jointure avec Departments. Utilisez une sous-requête ou CTE pour compter les subordonnés.