Aller au contenu
Développement medium

Jointures SQL et sous-requêtes : croiser les données de plusieurs tables

15 min de lecture

Votre base infra.db contient des serveurs, des services qui tournent dessus, des déploiements faits par des utilisateurs, et des alertes. Vous devez lister les services avec le hostname du serveur sur lequel ils tournent. L’information est répartie sur deux tables — un simple SELECT sur une seule table ne suffit plus. C’est exactement le problème que les jointures résolvent.

En SQL, les données sont structurées en tables normalisées : chaque entité a sa propre table, reliée aux autres par des clés. Les jointures (JOIN) reconstituent l’information complète en combinant ces tables. Les sous-requêtes offrent une alternative pour filtrer ou calculer à partir de données d’une autre table.

Ces deux mécanismes sont fondamentaux — impossible d’administrer une base relationnelle sans les maîtriser.

  • Écrire un INNER JOIN pour combiner deux tables sur une clé commune
  • Distinguer LEFT JOIN, RIGHT JOIN et FULL OUTER JOIN
  • Utiliser CROSS JOIN et les self joins
  • Écrire des sous-requêtes scalaires, avec IN et avec EXISTS
  • Choisir entre jointure et sous-requête selon le contexte
  • Joindre plus de deux tables dans une même requête

Les jointures interviennent dans presque toute requête réelle d’administration :

  • Lister les services avec le hostname du serveur hôte
  • Trouver les serveurs sans alertes récentes (LEFT JOIN … IS NULL)
  • Croiser les tables de déploiements avec les utilisateurs pour un audit
  • Alimenter un tableau de bord qui agrège des données de plusieurs tables
  • Écrire des scripts de migration qui vérifient la cohérence entre tables liées
  • Savoir écrire un SELECT … WHERE (guide Syntaxe de base)
  • La base fil rouge infra.db chargée dans ~/Projets/cours-sql
  • Comprendre la notion de clé primaire (colonne qui identifie chaque ligne de façon unique)

Une base relationnelle sépare les données en tables distinctes pour éviter la redondance. Dans votre base fil rouge :

  • servers : id, hostname, ip_address, …
  • services : id, name, server_id, port, status, …

La colonne server_id dans services est une clé étrangère : elle pointe vers id dans servers. Cette relation permet de retrouver sur quel serveur tourne chaque service — à condition de joindre les deux tables.

ConceptRôleExemple dans infra.db
Clé primaire (PK)Identifiant unique de chaque ligneservers.id
Clé étrangère (FK)Référence vers la PK d’une autre tableservices.server_idservers.id

La jointure utilise cette relation pour associer les lignes des deux tables.

INNER JOIN renvoie uniquement les lignes qui ont une correspondance dans les deux tables.

SELECT s.name AS service, sv.hostname, s.port, s.status
FROM services s
INNER JOIN servers sv ON sv.id = s.server_id;
  • s et sv sont des alias pour éviter de répéter les noms complets
  • ON définit la condition de jointure
  • Seuls les services qui ont un serveur existant apparaissent

Quand la relation repose sur plusieurs colonnes (moins courant dans notre base) :

SELECT *
FROM deliveries d
INNER JOIN warehouses w
ON w.region = d.region
AND w.city = d.city;

L’ancienne syntaxe SQL-89 place la jointure dans le WHERE :

-- Syntaxe implicite (déconseillée)
SELECT s.name, sv.hostname
FROM services s, servers sv
WHERE sv.id = s.server_id;

LEFT JOIN (ou LEFT OUTER JOIN) renvoie toutes les lignes de la table de gauche, même celles sans correspondance à droite. Les colonnes de droite sont remplies de NULL quand il n’y a pas de match.

SELECT sv.hostname, a.severity, a.message
FROM servers sv
LEFT JOIN alerts a ON a.server_id = sv.id;

Résultat : tous les serveurs, y compris ceux sans aucune alerte (avec severity = NULL).

Le pattern LEFT JOIN … WHERE … IS NULL est très courant pour trouver les lignes orphelines :

-- Serveurs qui n'ont jamais eu d'alerte
SELECT sv.hostname
FROM servers sv
LEFT JOIN alerts a ON a.server_id = sv.id
WHERE a.id IS NULL;

RIGHT JOIN est le miroir de LEFT JOIN : toutes les lignes de la table de droite, même sans correspondance à gauche.

SELECT u.username, d.version, d.deployed_at
FROM users u
RIGHT JOIN deployments d ON d.deployed_by = u.id;

En pratique, RIGHT JOIN est rarement utilisé — il est plus lisible d’inverser les tables et d’utiliser un LEFT JOIN.

FULL OUTER JOIN renvoie toutes les lignes des deux tables, avec NULL quand il n’y a pas de correspondance d’un côté.

-- PostgreSQL
SELECT u.username, d.version
FROM users u
FULL OUTER JOIN deployments d ON d.deployed_by = u.id;

CROSS JOIN combine chaque ligne de la première table avec chaque ligne de la seconde — sans condition ON.

-- Générer toutes les combinaisons serveur × environnement
SELECT sv.hostname, env.name
FROM servers sv
CROSS JOIN (
SELECT 'production' AS name
UNION SELECT 'staging'
UNION SELECT 'development'
) env;

Un self join relie une table à elle-même, typiquement pour explorer une hiérarchie. La table categories de votre base fil rouge a une colonne parent_id qui pointe vers id de la même table :

SELECT c.name AS categorie, p.name AS parent
FROM categories c
LEFT JOIN categories p ON p.id = c.parent_id;

Les alias sont obligatoires pour distinguer les deux rôles de la même table.

Résultat :

categorieparent
InfrastructureNULL
ComputeInfrastructure
RéseauInfrastructure
Machines virtuellesCompute
ConteneursCompute

Il suffit de chaîner les JOIN. Ici, on récupère l’historique complet de déploiement avec le service, le serveur et l’utilisateur :

SELECT sv.hostname, s.name AS service, d.version, d.status, u.full_name AS deploye_par, d.deployed_at
FROM deployments d
INNER JOIN services s ON s.id = d.service_id
INNER JOIN servers sv ON sv.id = s.server_id
INNER JOIN users u ON u.id = d.deployed_by
WHERE d.deployed_at > '2026-04-01'
ORDER BY d.deployed_at DESC;

Chaque JOIN ajoute une table. L’optimiseur de la base de données choisit l’ordre d’exécution le plus efficace — vous n’avez pas besoin de vous en soucier dans la plupart des cas.

Une sous-requête est un SELECT imbriqué dans une autre requête. Elle s’exécute en premier et son résultat alimente la requête principale.

-- Dernier serveur ajouté à l'inventaire
SELECT hostname, ip_address
FROM servers
WHERE created_at = (SELECT MAX(created_at) FROM servers);

La sous-requête renvoie une seule valeur (la date la plus récente), utilisée dans le WHERE.

-- Serveurs qui ont au moins une alerte critique
SELECT hostname
FROM servers
WHERE id IN (SELECT DISTINCT server_id FROM alerts WHERE severity = 'critical');

IN vérifie si la valeur fait partie de la liste retournée.

-- Utilisateurs qui ont effectué au moins un déploiement (souvent plus performant)
SELECT u.full_name
FROM users u
WHERE EXISTS (
SELECT 1 FROM deployments d WHERE d.deployed_by = u.id
);

EXISTS renvoie vrai dès qu’au moins une ligne correspond — il ne parcourt pas toute la sous-requête.

TypeComportementPerformance
Non corréléeExécutée une seule fois, résultat réutiliséGénéralement rapide
CorréléeExécutée pour chaque ligne de la requête externePeut être lente sur de grosses tables
-- Non corrélée : le SELECT interne ne dépend pas de la requête externe
WHERE id IN (SELECT server_id FROM alerts WHERE severity = 'critical')
-- Corrélée : le SELECT interne référence sv.id de la requête externe
WHERE EXISTS (SELECT 1 FROM alerts a WHERE a.server_id = sv.id)
CritèreJointureSous-requête
Afficher des colonnes des deux tablesOui (naturel)Nécessite un JOIN quand même
Tester l’existenceLEFT JOIN … IS NULLEXISTS / NOT EXISTS (plus lisible)
Filtrer par une listeINNER JOININ (SELECT …)
PerformanceSouvent équivalent grâce à l’optimiseurLes corrélées peuvent être lentes
LisibilitéMeilleure pour 2-3 tables liéesMeilleure pour des conditions ponctuelles

Règle pratique : si vous avez besoin de colonnes des deux tables dans le résultat, utilisez un JOIN. Si vous filtrez juste sur une condition externe, une sous-requête est souvent plus lisible.

Ouvrez sqlite3 infra.db et pratiquez :

  1. Listez chaque serveur avec ses services (incluez les serveurs sans service).

    SELECT sv.hostname, s.name AS service, s.port, s.status
    FROM servers sv
    LEFT JOIN services s ON s.server_id = sv.id
    ORDER BY sv.hostname;
  2. Trouvez les utilisateurs qui n’ont jamais effectué de déploiement (anti-join).

    SELECT u.full_name, u.role
    FROM users u
    LEFT JOIN deployments d ON d.deployed_by = u.id
    WHERE d.id IS NULL;
  3. Affichez l’historique de déploiement complet (jointure sur 4 tables) : hostname, service, version, statut, déployé par, date.

    SELECT sv.hostname, s.name AS service, d.version, d.status, u.full_name, d.deployed_at
    FROM deployments d
    INNER JOIN services s ON s.id = d.service_id
    INNER JOIN servers sv ON sv.id = s.server_id
    INNER JOIN users u ON u.id = d.deployed_by
    ORDER BY d.deployed_at DESC;
  4. Trouvez les serveurs de production qui ont eu des alertes critiques (sous-requête).

    SELECT hostname
    FROM servers
    WHERE environment = 'production'
    AND id IN (
    SELECT server_id FROM alerts WHERE severity = 'critical'
    );
SymptômeCause probableSolution
Résultat avec des lignes dupliquéesLa jointure produit un produit cartésien partiel (relation many-to-many)Vérifiez les conditions ON, ajoutez DISTINCT si approprié
JOIN renvoie trop de lignesCondition ON manquante ou incorrecte → produit cartésienVérifiez que chaque JOIN a un ON correct
Sous-requête corrélée très lenteExécutée pour chaque ligne de la table principaleRéécrivez en JOIN ou en CTE (guide SQL avancé)
NULL inattendu dans le résultatLEFT JOIN : les lignes sans correspondance ont des NULL à droiteC’est le comportement normal — filtrez avec WHERE … IS NOT NULL si nécessaire
FULL OUTER JOIN non supportéNi SQLite ni MySQL ne le supportentUtilisez LEFT JOIN UNION LEFT JOIN inversé (voir section ci-dessus)
  • INNER JOIN renvoie uniquement les lignes qui ont une correspondance dans les deux tables
  • LEFT JOIN conserve toutes les lignes de gauche, même sans correspondance
  • Le pattern LEFT JOIN … WHERE … IS NULL est un anti-join puissant pour détecter les orphelins
  • CROSS JOIN produit un produit cartésien — à n’utiliser que sur de petites tables
  • Les sous-requêtes offrent une alternative lisible pour filtrer (IN, EXISTS)
  • EXISTS est souvent plus performant que IN sur de grosses tables
  • Si vous avez besoin de colonnes des deux tables, préférez un JOIN

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn