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.
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.
-- 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;
-- 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;
-- 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;
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.
-- 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;
-- 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;
WHERE table_droite.colonne IS NULL après le LEFT JOIN. C'est plus efficace
que NOT EXISTS dans la plupart des cas.
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.
-- 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;
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.
-- 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;
-- 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;
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 !
-- 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;
-- 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;
-- 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;
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.
-- 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;
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.
-- 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;
-- 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;
-- 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;
Rend le code lisible et évite les ambiguïtés, surtout avec plusieurs jointures.
-- ❌ 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;
Évitez SELECT * dans les jointures pour de meilleures performances et clarté.
-- ❌ 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;
Créez des index sur les clés étrangères pour des performances optimales.
-- 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);
-- 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
Courts et significatifs pour lisibilité
Éviter SELECT * dans jointures
Index sur colonnes de jointure
Jointures dans ON, filtres dans WHERE
| 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 |
-- ❌ 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;
-- ❌ 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;
-- ⚠️ 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
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.
-- 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);
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)
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.
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)
LEFT JOIN Products avec OrderDetails, puis WHERE OrderDetailID IS NULL. Jointure avec Categories pour le nom. Utilisez DATEDIFF() pour les jours.
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)
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.