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