SQL Pratique
Self Join SQL : interroger une table sur elle-même
14 min de lecture

Self Join SQL : interroger une table sur elle-même

Maîtrisez le self join SQL : syntaxe, cas d'usage concrets, erreurs fréquentes et exercices pour briller en entretien technique.

Avatar de Thomas LeroyThomas Leroy

Le self join SQL consiste à joindre une table avec elle-même — une technique qui déroute beaucoup de candidats en entretien, alors qu'elle résout des problèmes très concrets : hiérarchies, comparaisons entre lignes, doublons, séquences temporelles.

Contrairement aux jointures classiques qui relient deux tables distinctes, le self join exploite deux alias d'une même table pour créer des paires de lignes comparables. Cette subtilité syntaxique cache une puissance analytique réelle : identifier le manager d'un employé, détecter des doublons, comparer des ventes consécutives ou construire des arbres d'organisation.

Dans cet article, vous allez apprendre la syntaxe exacte du self join, découvrir ses cas d'usage les plus fréquents en entretien data, comprendre quand le préférer aux fonctions fenêtre, et éviter les pièges classiques qui font perdre des points lors d'un test technique.

📌 Ce qu'il faut retenir

  • Un self join joint une table avec elle-même via 2 alias différents
  • Il est indispensable pour modéliser les hiérarchies (employé → manager)
  • Il permet aussi de comparer des lignes entre elles sans fonctions fenêtre
  • Toujours utiliser des alias clairs pour éviter les erreurs d'ambiguïté

Qu'est-ce qu'un self join et pourquoi l'utiliser ?

Un self join (ou auto-jointure) est une jointure ordinaire — INNER JOIN, LEFT JOIN, etc. — appliquée à une seule et même table, référencée deux fois sous des alias distincts. SQL traite chaque alias comme une table virtuelle indépendante, ce qui permet de comparer ou de relier n'importe quelles lignes de cette table entre elles.

La vraie question est : pourquoi aurait-on besoin de ça ? La réponse tient dans la structure de certaines données réelles. Dans une table employees, la colonne manager_id référence une autre ligne de la même table. Pour afficher le nom du manager à côté du nom de l'employé, vous avez besoin de lire la table deux fois simultanément — d'où le self join.

Sans cette technique, vous seriez obligé de faire deux requêtes séparées et de reconstituer le résultat en dehors de SQL, ce qui est inefficace et souvent impossible dans un contexte de rapport automatisé.

Syntaxe de base du self join

La syntaxe ne diffère en rien d'un JOIN classique, sauf que la même table apparaît deux fois avec des alias différents.

SELECT
    e.employee_id,
    e.first_name        AS employe,
    m.first_name        AS manager
FROM employees AS e
INNER JOIN employees AS m
    ON e.manager_id = m.employee_id;

Ici, e représente chaque employé et m représente la ligne correspondant à son manager. La clause ON relie la clé étrangère manager_id de l'employé à la clé primaire employee_id du manager.

Utiliser LEFT JOIN pour inclure les employés sans manager

Avec un INNER JOIN, les lignes sans manager (c'est-à-dire celles dont manager_id est NULL — souvent le PDG) disparaissent du résultat. Un LEFT JOIN corrige ça :

SELECT
    e.employee_id,
    e.first_name        AS employe,
    m.first_name        AS manager
FROM employees AS e
LEFT JOIN employees AS m
    ON e.manager_id = m.employee_id;

Le PDG apparaît maintenant avec NULL dans la colonne manager. C'est le comportement attendu dans la quasi-totalité des exercices d'entretien sur ce thème. Pour aller plus loin sur les différences entre INNER et LEFT JOIN, consultez notre guide INNER JOIN vs LEFT JOIN : différences et guide complet SQL.

💡 Bon à savoir

Choisissez des alias métier parlants : e pour "employé" et m pour "manager" plutôt que a et b. Un recruteur lit votre requête en 30 secondes — la lisibilité compte autant que la justesse.

Les 5 cas d'usage les plus fréquents en entretien

1. Hiérarchie employé-manager

C'est le cas canonique, celui que tout recruteur data connaît. Le schéma type est une table employees avec une colonne manager_id qui pointe vers employee_id dans la même table (relation auto-référentielle).

-- Afficher tous les employés avec le nom de leur manager
SELECT
    e.employee_id,
    CONCAT(e.first_name, ' ', e.last_name)  AS employe,
    CONCAT(m.first_name, ' ', m.last_name)  AS manager,
    e.department
FROM employees AS e
LEFT JOIN employees AS m
    ON e.manager_id = m.employee_id
ORDER BY m.last_name, e.last_name;

Ce résultat permet de produire un organigramme complet directement depuis SQL.

2. Comparer des lignes successives dans le temps

Supposons une table daily_sales avec les colonnes sale_date et revenue. Vous voulez calculer la croissance jour après jour :

SELECT
    d1.sale_date                                        AS jour,
    d1.revenue                                          AS ca_jour,
    d2.revenue                                          AS ca_veille,
    ROUND((d1.revenue - d2.revenue) / d2.revenue * 100, 2) AS variation_pct
FROM daily_sales AS d1
LEFT JOIN daily_sales AS d2
    ON d2.sale_date = d1.sale_date - INTERVAL '1 day'
ORDER BY d1.sale_date;

Cette approche est particulièrement utile quand les fonctions fenêtre ne sont pas disponibles ou quand la logique de décalage est complexe. Pour une approche alternative avec les fonctions fenêtre, voir notre article sur l'exercice SQL : moyenne mobile avec les fonctions fenêtre.

3. Détecter des doublons ou des paires similaires

Un self join permet de trouver des paires de lignes qui partagent une caractéristique commune, par exemple des clients avec le même email mais des IDs différents :

SELECT
    a.customer_id   AS id_1,
    b.customer_id   AS id_2,
    a.email
FROM customers AS a
INNER JOIN customers AS b
    ON  a.email = b.email
    AND a.customer_id < b.customer_id;

La condition a.customer_id < b.customer_id est cruciale : elle évite d'obtenir les doublons en miroir (paire A-B et paire B-A) ainsi que les auto-correspondances d'une ligne avec elle-même.

4. Trouver les produits dans la même catégorie

Imaginez une table products avec product_id, product_name et category_id. Pour lister toutes les paires de produits appartenant à la même catégorie :

SELECT
    p1.product_name AS produit_1,
    p2.product_name AS produit_2,
    p1.category_id
FROM products AS p1
INNER JOIN products AS p2
    ON  p1.category_id = p2.category_id
    AND p1.product_id  < p2.product_id
ORDER BY p1.category_id, p1.product_name;

Ce type de requête est utilisé pour générer des recommandations "vous aimerez aussi" ou pour valider la cohérence d'un catalogue.

5. Construire une matrice de distances ou de comparaisons

Dans une table cities avec les colonnes city_id, city_name, latitude et longitude, un self join permet de calculer la distance entre chaque paire de villes :

SELECT
    c1.city_name    AS ville_depart,
    c2.city_name    AS ville_arrivee,
    ROUND(
        6371 * ACOS(
            SIN(RADIANS(c1.latitude)) * SIN(RADIANS(c2.latitude)) +
            COS(RADIANS(c1.latitude)) * COS(RADIANS(c2.latitude)) *
            COS(RADIANS(c2.longitude - c1.longitude))
        )
    , 0) AS distance_km
FROM cities AS c1
CROSS JOIN cities AS c2
WHERE c1.city_id < c2.city_id
ORDER BY distance_km;

Ici, on utilise un CROSS JOIN (qui est aussi une forme de self join) combiné à un filtre pour éviter les doublons.

Self join vs fonctions fenêtre : quand choisir quoi ?

Cette question revient régulièrement en entretien. Les deux approches peuvent résoudre des problèmes similaires, mais leurs performances et leur lisibilité diffèrent.

Critère Self Join Fonctions fenêtre (LAG/LEAD)
Comparaison ligne N avec ligne N-1 ✅ Possible (JOIN sur date-1) ✅ Natif avec LAG()
Comparaison sur critère non ordonné ✅ Très flexible ❌ Nécessite un ORDER BY
Performance sur grandes tables ⚠️ Peut être lente (produit cartésien) ✅ Généralement plus rapide
Lisibilité du code ⚠️ Alias à gérer ✅ Intentions explicites
Hiérarchies (parent-enfant) ✅ Cas d'usage naturel ❌ Non adapté
Compatibilité SQL ancienne ✅ Universel ⚠️ MySQL < 8.0 limité
Paires de lignes (N × N) ✅ Naturel ❌ Impossible directement

Règle pratique : utilisez les fonctions fenêtre pour les décalages temporels simples (LAG/LEAD), et le self join pour les relations hiérarchiques ou les comparaisons entre paires arbitraires de lignes.

⚠️ Attention

Un self join sur une table de 10 millions de lignes peut générer un produit cartésien de 100 milliards de paires avant filtrage. Ajoutez toujours des conditions restrictives dans la clause ON et créez des index sur les colonnes de jointure pour éviter un scan complet.

Self join vs CTE récursive : pour les hiérarchies profondes

Le self join gère parfaitement les hiérarchies à 2 niveaux (employé → manager direct). Mais si vous avez besoin de remonter toute une chaîne (employé → manager → manager du manager → …), il devient vite ingérable.

-- Self join limité à 2 niveaux
SELECT
    e.first_name    AS employe,
    m.first_name    AS manager_direct,
    d.first_name    AS manager_n2
FROM employees AS e
LEFT JOIN employees AS m ON e.manager_id = m.employee_id
LEFT JOIN employees AS d ON m.manager_id = d.employee_id;

Chaque niveau supplémentaire exige un nouveau LEFT JOIN. Pour les arbres de profondeur variable, une CTE récursive (WITH RECURSIVE) est la bonne réponse.

Situation Approche recommandée Raison
Hiérarchie à 1 ou 2 niveaux fixes Self join Simple, lisible, performant
Hiérarchie à profondeur variable CTE récursive Gère n niveaux automatiquement
Comparaison de paires de lignes Self join Pas d'alternative directe
Décalage temporel (J-1, J-7) LAG() ou self join LAG() plus lisible et rapide
Déduplication de paires Self join + condition < Élimine les doublons symétriques

Erreurs classiques à éviter absolument

Oublier la condition de filtre sur les paires

Sans la condition a.id < b.id dans un self join de comparaison, chaque paire apparaît deux fois (A-B et B-A), et chaque ligne s'associe avec elle-même. Le résultat explose en taille et devient inexploitable.

-- ❌ FAUX : produit cartésien non filtré
SELECT a.name, b.name
FROM products a
INNER JOIN products b ON a.category_id = b.category_id;

-- ✅ CORRECT : chaque paire une seule fois, sans auto-jointure
SELECT a.name, b.name
FROM products a
INNER JOIN products b
    ON  a.category_id = b.category_id
    AND a.product_id  < b.product_id;

Confondre les alias dans le SELECT

Avec deux alias qui exposent les mêmes colonnes, il est facile d'écrire SELECT name sans préciser l'alias — SQL vous retournera une erreur d'ambiguïté ou, pire, une colonne au hasard selon le moteur.

-- ❌ FAUX : ambiguïté sur "name"
SELECT name FROM employees e INNER JOIN employees m ON e.manager_id = m.employee_id;

-- ✅ CORRECT : alias explicites
SELECT e.first_name AS employe, m.first_name AS manager
FROM employees e
INNER JOIN employees m ON e.manager_id = m.employee_id;

Utiliser INNER JOIN quand les valeurs NULL sont importantes

Si le PDG (ou tout autre nœud racine) a un manager_id NULL, un INNER JOIN l'exclut silencieusement. Vérifiez toujours si vous voulez inclure ou exclure les lignes sans correspondance.

Exercice pratique : niveau entretien

Voici un exercice typique de niveau intermédiaire-avancé qu'on retrouve dans les tests techniques data analyst et data engineer.

Données : table orders avec les colonnes order_id, customer_id, order_date, amount.

Question : pour chaque commande, afficher la commande précédente du même client (par date), le montant de cette commande précédente, et la variation de montant en pourcentage.

SELECT
    o1.order_id,
    o1.customer_id,
    o1.order_date,
    o1.amount                                               AS montant_actuel,
    o2.order_id                                             AS commande_precedente,
    o2.amount                                               AS montant_precedent,
    ROUND(
        (o1.amount - o2.amount) / NULLIF(o2.amount, 0) * 100
    , 2)                                                    AS variation_pct
FROM orders AS o1
LEFT JOIN orders AS o2
    ON  o2.customer_id = o1.customer_id
    AND o2.order_date  = (
        SELECT MAX(o3.order_date)
        FROM orders AS o3
        WHERE o3.customer_id = o1.customer_id
          AND o3.order_date  < o1.order_date
    )
ORDER BY o1.customer_id, o1.order_date;

Notez l'utilisation de NULLIF(o2.amount, 0) pour éviter une division par zéro — un réflexe que les recruteurs apprécient. La sous-requête corrélée dans la clause ON identifie précisément la date de la commande précédente pour chaque client.

💡 Bon à savoir

Dans un entretien, proposez d'abord la version self join, puis mentionnez spontanément que LAG(amount) OVER (PARTITION BY customer_id ORDER BY order_date) produirait le même résultat de façon plus concise. Montrer que vous connaissez plusieurs approches valorise votre profil.

Performance et indexation

Un self join peut devenir un goulot d'étranglement si les colonnes de jointure ne sont pas indexées. Voici les bonnes pratiques :

-- Index composite recommandé pour l'exercice précédent
CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);

Un index mal choisi peut transformer un self join en opération de plusieurs minutes sur une table de production. Testez toujours sur un échantillon représentatif avant de déployer.


Questions fréquentes

Quelle est la différence entre un self join et un CROSS JOIN ?

Un CROSS JOIN produit toutes les combinaisons possibles entre deux ensembles de lignes (produit cartésien complet), sans aucune condition de jointure. Un self join utilise la même table mais avec une condition ON restrictive qui ne retient que les paires pertinentes. En pratique, un self join avec une condition très permissive peut se comporter comme un CROSS JOIN, d'où le risque d'explosion du nombre de lignes.

Peut-on faire un self join avec plus de 2 instances de la même table ?

Oui, il suffit d'ajouter un 3e alias. On parle alors d'une jointure à 3 niveaux. C'est utile pour les hiérarchies à profondeur fixe de 3 niveaux (employé → manager → directeur), mais au-delà de 2 ou 3 niveaux, une CTE récursive devient bien plus maintenable.

Le self join fonctionne-t-il dans tous les SGBD ?

Oui. Le self join repose sur la syntaxe JOIN standard, compatible avec PostgreSQL, MySQL, SQL Server, Oracle, SQLite et BigQuery. Les alias de table (AS) sont universels. Seule la syntaxe de certaines fonctions de date utilisées dans les conditions peut varier selon le SGBD.

Pourquoi mon self join retourne-t-il des doublons ?

Le problème vient généralement de l'absence de condition asymétrique (a.id < b.id ou a.id <> b.id). Sans cette restriction, chaque paire (A, B) apparaît deux fois : une fois sous la forme (A, B) et une fois sous la forme (B, A). Ajoutez cette condition dans la clause ON pour éliminer les doublons symétriques.

Self join ou sous-requête corrélée : lequel est plus rapide ?

Cela dépend du SGBD et des index disponibles. En général, le self join permet à l'optimiseur d'utiliser des stratégies de jointure efficaces (hash join, merge join), tandis qu'une sous-requête corrélée peut être exécutée ligne par ligne. Sur PostgreSQL et SQL Server modernes, les plans d'exécution tendent à être équivalents pour des requêtes simples, mais le self join reste souvent plus rapide sur de grands volumes.

Comment éviter les auto-correspondances dans un self join ?

Ajoutez la condition a.id <> b.id dans la clause ON. Si vous voulez en plus éviter les paires en miroir, utilisez a.id < b.id (chaque paire n'apparaît qu'une seule fois, dans l'ordre croissant des IDs).

Un self join peut-il remplacer une fonction fenêtre en entretien ?

Oui, dans certains cas. Avant SQL:2003 (qui a standardisé les fonctions fenêtre), le self join était la seule façon de comparer des lignes entre elles. Aujourd'hui, pour un décalage simple (J-1), LAG() est plus lisible et souvent plus performant. Mais pour des comparaisons complexes entre paires non ordonnées, le self join reste irremplaçable. En entretien, mentionner les deux approches montre une vraie maîtrise du SQL.


Conclusion

Le self join est l'une de ces techniques SQL qui semblent anodines en apparence mais révèlent une compréhension profonde du langage. Maîtriser les alias, anticiper les doublons, choisir entre INNER et LEFT JOIN selon le contexte, et savoir quand basculer vers une CTE récursive ou une fonction fenêtre : voilà les signaux que les recruteurs data cherchent lors d'un test technique.

Pratiquez les 5 cas d'usage présentés dans cet article sur des données réelles, comparez les plans d'exécution, et vous serez prêt à répondre à n'importe quelle question sur le sujet. Pour aller plus loin dans votre préparation aux entretiens SQL, découvrez tous les exercices pratiques disponibles sur SQL Pratique.

Prêt à vous entraîner ?

50 exercices SQL interactifs avec éditeur en ligne, chronomètre et feedback IA.

Voir les exercices