Guide pratique · De la question Ă la requĂȘte propre · Lecture : ~15 min Teste ton niveau sur modai-lab.com/test-sql/
Pourquoi ce guide existe La plupart des gens ouvrent un Ă©diteur SQL et commencent Ă taper des SELECT. RĂ©sultat : une requĂȘte qui tourne peut-ĂȘtre, mais que personne ne comprend, parfois pas mĂȘme celui qui l'a Ă©crite.
Ăcrire une bonne requĂȘte SQL, ça ne commence pas avec le clavier. Ăa commence avec la tĂȘte.
Ce guide te donne la méthode complÚte : par quoi commencer, dans quel ordre réfléchir, et comment valider que le résultat est juste avant de le livrer.
đ§Ș Calibre ton niveau avant de commencer Sais-tu dĂ©jĂ dans quel ordre SQL exĂ©cute les clauses que tu Ă©cris ? Fais le test en 5 minutes.
đ Faire le test sur modai-lab.com/test-sql/
Résultat en direct · Niveau calibré · Parcours personnalisé
ĂTAPE 0 : Comprendre la question avant de toucher au clavier C'est l'Ă©tape que presque tout le monde saute. Et c'est la source de 80% des requĂȘtes fausses.
Les 4 questions à se poser en premier 1. Quel est concrÚtement le résultat attendu ?
Pas "les ventes par région". Mais :
Une ligne par région, ou une ligne par région par mois ?
Avec les régions sans ventes (CA = 0) ou seulement celles qui ont vendu ?
Sur quelle période ?
Triées comment ?
Décris le résultat attendu comme si tu expliquais à quelqu'un ce qui devrait apparaßtre dans le tableau.
Exemple de question floue :
"Donne-moi les ventes par région"
Â
Exemple de question précise :
"Une ligne par région ayant vendu, le CA total des commandes livrées en 2024,
plus une ligne « Non renseignée » pour les clients sans région,
triées du CA le plus élevé au plus bas"
methode-ecrire-requete-sql · 01 2. Quelles tables contiennent les données nécessaires ?
Avant d'Ă©crire une jointure, identifie d'oĂč vient chaque information :
CA total â orders.montant
RĂ©gion â regions.nom via users.region_id (ou l'adresse de livraison ?)
Statut â orders.statut
PĂ©riode â orders.created_at
methode-ecrire-requete-sql · 02 Si une information est dans deux tables différentes, quelle est la clé qui les relie ?
3. Quelle est la granularité attendue ?
C'est la question la plus souvent oubliée. La granularité = le niveau de détail d'une ligne dans le résultat.
Une ligne par... â commande ? user ? jour ? rĂ©gion ? produit ?
methode-ecrire-requete-sql · 03 La granularité détermine :
Le GROUP BY que tu vas utiliser
Si tu as besoin d'une window function ou d'une agrégation
Si un fan-out de JOIN est possible
4. Y a-t-il des cas limites à gérer ?
Des NULL dans les colonnes clés ?
Des utilisateurs sans commande (LEFT JOIN vs INNER JOIN) ?
Des doublons possibles dans une des tables ?
Des timezones différentes dans les timestamps ?
methode-ecrire-requete-sql · 04 · 4. Y a-t-il des cas limites Ă gĂ©rer ? ĂTAPE 1 : Identifier les tables et les relations Avant d'Ă©crire quoi que ce soit, dessine mentalement (ou sur papier) le chemin entre les tables.
Le schéma d'exploration en 3 colonnes Table source Colonne de jointure Table cible
orders â orders.user_id â users.id
orders â orders.product_id â products.id
users â users.region_id â regions.id
methode-ecrire-requete-sql · 05 Questions à se poser pour chaque jointure :
1. Quelle est la cardinalité ? (1:1, 1:N, N:M)
â Un user peut avoir N commandes (1:N)
â Une commande peut avoir N lignes (1:N)
Â
2. Est-ce que chaque ligne de la table de gauche a forcément une correspondance à droite ?
â OUI â INNER JOIN (risque de perdre des lignes si la correspondance manque)
â NON â LEFT JOIN (garder les lignes sans correspondance)
Â
3. Est-ce que la jointure peut créer des doublons (fan-out) ?
â Si oui : agrĂ©ger avant de joindre
methode-ecrire-requete-sql · 06 · Questions Ă se poser pour chaque jointure : Le test de granularitĂ© avant d'Ă©crire les JOINs -- Avant d'Ă©crire la requĂȘte complĂšte, vĂ©rifier que les clĂ©s de jointure
-- ne créent pas de doublons
Â
-- Est-ce que id est unique dans products ?
SELECT id, COUNT (*) FROM products GROUP BY id HAVING COUNT (*) > ;
-- Si rĂ©sultat vide â une commande matche au plus un produit â pas de fan-out
Â
-- Est-ce qu'un user peut avoir plusieurs lignes dans user_tags ?
SELECT user_id, COUNT (*) FROM user_tags GROUP BY user_id ORDER BY COUNT (*) DESC LIMIT ;
-- Si un COUNT(*) dĂ©passe 1 â joindre user_tags sur user_id multiplie les lignes (fan-out)
methode-ecrire-requete-sql · 07 ĂTAPE 2 : Ăcrire dans le bon ordre L'ordre dans lequel tu Ă©cris une requĂȘte SQL n'est pas l'ordre dans lequel tu penses.
L'ordre de rĂ©flexion recommandĂ© 1. FROM (+ JOINs) â Quelles tables ? Quelles relations ?
2. WHERE â Quels filtres sur les donnĂ©es brutes ?
3. GROUP BY â Quelle granularitĂ© dans le rĂ©sultat ?
4. HAVING â Quels filtres sur les agrĂ©gats ?
5. SELECT â Quelles colonnes et calculs dans le rĂ©sultat ?
6. ORDER BY â Dans quel ordre afficher ?
7. LIMIT â Combien de lignes ?
methode-ecrire-requete-sql · 08 Pourquoi cet ordre ?
Parce que c'est l'ordre dans lequel SQL exécute les clauses. Si tu commences par le SELECT, tu réfléchis à l'envers du moteur, et tu crées des bugs que tu ne comprends pas.
Ordre d'exécution SQL (pas l'ordre d'écriture) :
FROM â JOIN â WHERE â GROUP BY â HAVING â SELECT â ORDER BY â LIMIT
methode-ecrire-requete-sql · 09 La méthode de rédaction étape par étape Commencer par FROM : définir le périmÚtre
-- Ătape 1 : juste les tables et les jointures
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN regions r ON u.region_id = r.id
-- Pas encore de SELECT, pas encore de WHERE
methode-ecrire-requete-sql · 10 · Commencer par FROM : définir le périmÚtre Ajouter WHERE : filtrer les données brutes
-- Ătape 2 : filtres sur colonnes brutes uniquement
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN regions r ON u.region_id = r.id
WHERE o.created_at >= '2024-01-01'
AND o.created_at < '2025-01-01'
AND o.statut = 'livré'
-- Attention : si WHERE est sur une colonne de la table de droite d'un LEFT JOIN
-- â ça transforme le LEFT JOIN en INNER JOIN â dĂ©placer dans le ON si nĂ©cessaire
methode-ecrire-requete-sql · 11 · Ajouter WHERE : filtrer les données brutes Ajouter GROUP BY : définir la granularité
-- Ătape 3 : dĂ©finir ce qui constitue "une ligne" dans le rĂ©sultat
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN regions r ON u.region_id = r.id
WHERE o.created_at >= '2024-01-01'
AND o.created_at < '2025-01-01'
AND o.statut = 'livré'
GROUP BY r.nom
-- Une ligne par région
methode-ecrire-requete-sql · 12 · Ajouter GROUP BY : définir la granularité Ajouter HAVING si nécessaire
-- Ătape 4 : filtres sur les agrĂ©gats (jamais sur les colonnes brutes)
HAVING SUM (o.montant) >
-- ou COUNT(DISTINCT o.id) > 100
methode-ecrire-requete-sql · 13 · Ajouter HAVING si nĂ©cessaire Ăcrire le SELECT, maintenant seulement
-- Ătape 5 : maintenant qu'on sait ce qu'est une ligne, on peut choisir les colonnes
SELECT
COALESCE (r.nom, 'Non renseignée' ) AS region,
COUNT (DISTINCT o.id) AS nb_commandes,
COUNT (DISTINCT o.user_id) AS nb_clients,
SUM (o.montant) AS ca_total,
ROUND(AVG (o.montant), ) AS panier_moyen
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN regions r ON u.region_id = r.id
WHERE o.created_at >= '2024-01-01'
AND o.created_at < '2025-01-01'
AND o.statut = 'livré'
GROUP BY r.nom
methode-ecrire-requete-sql · 14 · Ăcrire le SELECT, maintenant seulement Finir avec ORDER BY et LIMIT
-- Ătape 6 : tri et limite
ORDER BY ca_total DESC
LIMIT ;
methode-ecrire-requete-sql · 15 · Finir avec ORDER BY et LIMIT đ§Ș Pratique la mĂ©thode Ă©tape par Ă©tape sur Modai Lab Chaque exercice Modai Lab te force Ă dĂ©composer la requĂȘte avant de l'Ă©crire. Le feedback identifie exactement quelle Ă©tape a Ă©tĂ© brĂ»lĂ©e.
đ Pratiquer sur modai-lab.com
Exercices progressifs · Feedback sur chaque clause · Parcours personnalisé
ĂTAPE 3 : Les vĂ©rifications avant de valider Une requĂȘte qui "tourne" n'est pas forcĂ©ment une requĂȘte correcte. Voici les 5 vĂ©rifications systĂ©matiques.
Vérification 1 : La granularité est-elle correcte ? -- Avant d'agréger : une commande doit rester une seule ligne aprÚs les JOINs
SELECT
COUNT (*) AS lignes_apres_joins,
COUNT (DISTINCT o.id) AS commandes_distinctes
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
LEFT JOIN regions r ON u.region_id = r.id
WHERE o.created_at >= '2024-01-01'
AND o.created_at < '2025-01-01'
AND o.statut = 'livré' ;
-- Si lignes_apres_joins > commandes_distinctes â fan-out â les SUM seront gonflĂ©es
methode-ecrire-requete-sql · 16 Vérification 2 : Les NULL sont-ils gérés ? -- Identifier les colonnes avec NULL dans le résultat
SELECT *
FROM mon_rapport_par_region
WHERE region IS NULL
OR ca_total IS NULL ;
-- Est-ce attendu ou est-ce un problĂšme de jointure ?
methode-ecrire-requete-sql · 17 Vérification 3 : Le LEFT JOIN n'est-il pas devenu un INNER JOIN ? -- Vérifier combien de lignes de la table principale n'ont pas de correspondance
SELECT COUNT (*) AS total FROM orders;
Â
SELECT COUNT (*) AS apres_left_join
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE u.region_id IS NOT NULL ; -- â ce filtre Ă©limine les lignes sans correspondance
Â
-- Si total > apres_left_join â des commandes sont perdues
-- â dĂ©placer le filtre dans le ON ou utiliser INNER JOIN intentionnellement
methode-ecrire-requete-sql · 18 Vérification 4 : Le résultat est-il cohérent avec un cas connu ? -- Prendre un cas simple qu'on peut calculer à la main
-- Ex : vérifier le CA pour une région spécifique
SELECT SUM (montant)
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN regions r ON u.region_id = r.id
WHERE r.nom = 'Ăle-de-France'
AND o.statut = 'livré'
AND o.created_at >= '2024-01-01'
AND o.created_at < '2025-01-01' ;
Â
-- Comparer avec la ligne "Ăle-de-France" dans le rapport
-- Si les deux chiffres ne sont pas identiques â une condition filtre des donnĂ©es inattendues
methode-ecrire-requete-sql · 19 VĂ©rification 5 : Les totaux sont-ils cohĂ©rents ? -- Le total de la requĂȘte agrĂ©gĂ©e doit correspondre au total brut
-- Total brut de rĂ©fĂ©rence, avec exactement les mĂȘmes filtres que le rapport
SELECT SUM (montant)
FROM orders
WHERE statut = 'livré'
AND created_at >= '2024-01-01'
AND created_at < '2025-01-01' ;
Â
-- Total du rapport
SELECT SUM (ca_total) FROM mon_rapport_par_region;
Â
-- Si les deux ne sont pas Ă©gaux â chercher oĂč des lignes sont perdues ou comptĂ©es deux fois
methode-ecrire-requete-sql · 20 ĂTAPE 4 : Structurer pour la lisibilitĂ© Une requĂȘte correcte mais illisible sera réécrite par quelqu'un d'autre dans 6 mois. Autant la rendre maintenable.
RÚgles de lisibilité Nommer les alias de façon explicite
-- â Alias cryptiques
SELECT u.n, o.m, COUNT (*)
FROM users u JOIN orders o ON u.i = o.ui;
Â
-- â
Alias qui disent ce qu'ils font
SELECT
u.nom AS client,
o.montant AS montant_commande,
COUNT (*) AS nb_lignes
FROM users u
JOIN orders o ON u.id = o.user_id;
methode-ecrire-requete-sql · 21 · Nommer les alias de façon explicite Documenter l'intention métier
-- Rapport CA par région : commandes livrées 2024
-- Les clients sans région sont regroupés sous 'Non renseignée'
-- Granularité : une ligne par région
SELECT
COALESCE (r.nom, 'Non renseignée' ) AS region,
SUM (o.montant) AS ca_total
FROM orders o
-- LEFT JOIN : une commande reste comptĂ©e mĂȘme si son client est introuvable
LEFT JOIN users u ON o.user_id = u.id
-- LEFT JOIN : un client sans rĂ©gion reste comptĂ© (â 'Non renseignĂ©e')
LEFT JOIN regions r ON u.region_id = r.id
WHERE o.created_at >= '2024-01-01' -- 2024 uniquement
AND o.created_at < '2025-01-01'
AND o.statut = 'livré' -- commandes livrées uniquement
GROUP BY r.nom
ORDER BY ca_total DESC ;
methode-ecrire-requete-sql · 22 · Documenter l'intention mĂ©tier DĂ©composer avec des CTEs pour les requĂȘtes complexes
-- â
Chaque CTE a une seule responsabilité
WITH
-- 1. Filtrer les commandes du périmÚtre
commandes_2024 AS (
SELECT id AS order_id, user_id, montant
FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01'
AND statut = 'livré'
),
-- 2. Enrichir avec la région du client
commandes_avec_region AS (
SELECT c.order_id, c.montant, COALESCE (r.nom, 'Non renseignée' ) AS region
FROM commandes_2024 c
LEFT JOIN users u ON c.user_id = u.id
LEFT JOIN regions r ON u.region_id = r.id
)
-- 3. Agréger par région
SELECT
region,
COUNT (*) AS nb_commandes,
SUM (montant) AS ca_total
FROM commandes_avec_region
GROUP BY region
ORDER BY ca_total DESC ;
methode-ecrire-requete-sql · 23 · DĂ©composer avec des CTEs pour les requĂȘtes complexes La rĂšgle des 30 secondes Une requĂȘte est bien Ă©crite si quelqu'un qui ne l'a pas Ă©crite peut comprendre ce qu'elle fait en moins de 30 secondes.
Si ce n'est pas le cas : ajouter des commentaires, décomposer en CTEs, renommer les alias.
ĂTAPE 5 : La checklist complĂšte avant de livrer AVANT D'ĂCRIRE :
⥠Je sais exactement à quoi ressemble le résultat attendu
⥠J'ai identifié toutes les tables nécessaires et leurs relations
⥠J'ai défini la granularité attendue (une ligne par quoi ?)
⥠J'ai identifié les cas limites (NULL, doublons, lignes sans correspondance)
Â
EN ĂCRIVANT :
⥠J'ai commencé par FROM et les JOINs, pas par le SELECT
⥠Mes filtres sur données brutes sont dans WHERE, pas dans HAVING
⥠Mes filtres sur agrégats sont dans HAVING, pas dans WHERE
⥠Si LEFT JOIN : les filtres sur la table de droite sont dans le ON
⥠Je n'ai pas utilisé SELECT * (colonnes explicites uniquement)
Â
EN VALIDANT :
⥠Le nombre de lignes correspond à la granularité attendue
⥠COUNT(*) = COUNT(DISTINCT id) (pas de fan-out)
⥠Les NULL sont traités intentionnellement
⥠Le total du rapport = le total de la table brute
⥠J'ai testé sur un sous-ensemble connu pour vérifier un cas concret
Â
EN LIVRANT :
⥠Les alias sont explicites et lisibles
⥠Les commentaires expliquent l'intention métier, pas la syntaxe
⥠Les requĂȘtes complexes sont dĂ©composĂ©es en CTEs nommĂ©es
⥠Quelqu'un d'autre peut lire et modifier cette requĂȘte sans moi
methode-ecrire-requete-sql · 28 RĂ©capitulatif : Les 6 Ă©tapes en une page Ătape
Ce qu'on fait
Questions clés
0 · Comprendre
Décrire le résultat attendu
Quelle granularité ? Quels cas limites ?
1 · Cartographier
Identifier tables et relations
Quelle clé relie quelles tables ? LEFT ou INNER ? Fan-out ?
2 · Ăcrire
FROM â WHERE â GROUP BY â HAVING â SELECT â ORDER BY
Dans cet ordre, pas l'inverse
3 · Vérifier
Granularité, NULL, totaux, cohérence
Le total du rapport = le total brut ?
4 · Structurer
Alias, commentaires, CTEs
La rĂšgle des 30 secondes
5 · Livrer
Dérouler la checklist complÚte
Quelqu'un d'autre peut-il reprendre la requĂȘte sans moi ?
Prompt IA pour auditer tes requĂȘtes Audite cette requĂȘte SQL :
Â
[coller la requĂȘte]
Â
Vérifie :
1. La granularité est-elle clairement définie ? Y a-t-il un risque de fan-out ?
2. Les filtres sont-ils correctement placés dans WHERE (données brutes) ou HAVING (agrégats) ?
3. Les LEFT JOINs sont-ils transformés en INNER JOIN par un WHERE sur la table de droite ?
4. Y a-t-il des cas NULL non gérés ?
5. Le SELECT liste-t-il explicitement les colonnes (pas SELECT *) ?
6. La requĂȘte passerait-elle le test des 30 secondes ? Sinon, comment la restructurer en CTEs ?
Â
Pour chaque problÚme trouvé : montrer la version corrigée avec une explication d'une phrase.
methode-ecrire-requete-sql · 29 PARTIE BONUS : Les erreurs de dĂ©butants les plus frĂ©quentes Erreur 1 : Filtrer dans HAVING ce qui devrait ĂȘtre dans WHERE -- â Filtre sur une donnĂ©e brute dans HAVING
SELECT user_id, SUM (montant) AS ca
FROM orders
GROUP BY user_id
HAVING statut = 'livré' ;
-- ERROR: column "orders.statut" must appear in the GROUP BY clause
-- or be used in an aggregate function
-- HAVING travaille sur des groupes : statut n'existe plus Ă ce stade.
-- Ajouter statut au GROUP BY fait passer la requĂȘte mais masque l'intention :
-- c'est un filtre de lignes, sa place est dans WHERE.
Â
-- â
Filtre dans WHERE â les lignes sont filtrĂ©es avant l'agrĂ©gation
SELECT user_id, SUM (montant) AS ca
FROM orders
WHERE statut = 'livré'
GROUP BY user_id;
methode-ecrire-requete-sql · 24 Erreur 2 : SELECT * avec des JOINs -- â SELECT * avec plusieurs tables â colonnes en double, ambiguĂŻtĂ©, donnĂ©es inutiles
SELECT *
FROM orders o
JOIN users u ON o.user_id = u.id;
-- RĂ©sultat : colonnes "id" de orders ET de users â laquelle utiliser ?
Â
-- â
Nommer explicitement les colonnes nécessaires
SELECT
o.id AS order_id,
o.montant,
o.created_at,
u.nom AS client,
u.email
FROM orders o
JOIN users u ON o.user_id = u.id;
methode-ecrire-requete-sql · 25 Erreur 3 : Oublier que COUNT(*) et COUNT(colonne) ne sont pas Ă©quivalents -- COUNT(*) â compte toutes les lignes, y compris celles avec NULL
-- COUNT(col) â compte uniquement les lignes oĂč col n'est pas NULL
Â
SELECT
COUNT (*) AS total_commandes, -- toutes les lignes
COUNT (promo_code) AS avec_promo, -- uniquement les commandes avec un code promo
COUNT (*) - COUNT (promo_code) AS sans_promo -- commandes sans code promo
FROM orders;
methode-ecrire-requete-sql · 26 Erreur 4 : Joindre sur une colonne non indexĂ©e -- â Jointure sur une colonne libre â scan complet de la table Ă chaque ligne
SELECT * FROM orders o
JOIN users u ON o.email_client = u.email; -- email non indexé
Â
-- â
Toujours joindre sur des clés primaires ou des colonnes indexées
SELECT * FROM orders o
JOIN users u ON o.user_id = u.id; -- id est la clĂ© primaire â index automatique
Â
-- Si la jointure sur email est indispensable : CREATE INDEX idx_users_email ON users(email);
methode-ecrire-requete-sql · 27 đ Pratiquer la mĂ©thode sur Modai Lab Cette mĂ©thode ne s'ancre pas en la lisant une fois. Elle s'ancre en l'appliquant sur des cas rĂ©els, avec du feedback immĂ©diat Ă chaque Ă©tape.
Modai Lab est construit autour de cette approche : chaque exercice te force à réfléchir avant d'écrire, et le feedback indique exactement quelle étape a été brûlée.
đ modai-lab.com/test-sql/ · Exercices calibrĂ©s · Cas business rĂ©els · Feedback senior