Tu as compté ton score dans la vidéo ? Voici les 10 corrigés, en PostgreSQL, avec à chaque fois la notion utilisée, pourquoi ça marche, et le piège classique.
Moins de 7 ? Pas de panique : les 10 tiennent avec seulement 4 notions. Elles sont résumées en bas de la page.
Les tables d’exemple
Toutes les requêtes utilisent ces 5 tables. Copie-les dans ta base pour tester chaque corrigé toi-même.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL
);
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT REFERENCES customers(id),
order_date DATE NOT NULL,
amount NUMERIC(10,2) NOT NULL
);
CREATE TABLE employees (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
salary NUMERIC(10,2) NOT NULL,
department_id INT,
manager_id INT REFERENCES employees(id)
);
CREATE TABLE sales (
id SERIAL PRIMARY KEY,
product_id INT NOT NULL,
category TEXT NOT NULL,
qty INT NOT NULL,
amount NUMERIC(10,2) NOT NULL,
sale_date DATE NOT NULL
);
1. Les emails en double
L’énoncé : trouve les emails présents plus d’une fois dans la table users.
La notion : GROUP BY + HAVING
SELECT email, COUNT(*) AS nb
FROM users
GROUP BY email
HAVING COUNT(*) > 1;
Pourquoi ça marche : GROUP BY regroupe les lignes par email, COUNT(*) les compte, et HAVING garde seulement les groupes de plus d’une ligne.
Le piège : mettre la condition dans un WHERE. WHERE filtre les lignes avant le regroupement, HAVING filtre les groupes après.
2. Les clients sans commande
L’énoncé : liste les clients qui n’ont jamais passé de commande.
La notion : LEFT JOIN
SELECT c.*
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;
Pourquoi ça marche : le LEFT JOIN garde tous les clients, même sans commande. Pour ceux-là, les colonnes de orders valent NULL, et c’est ce qu’on filtre.
Le piège : utiliser un JOIN simple. Il ne garde que les clients qui ont au moins une commande, donc le résultat est toujours vide.
3. Les commandes par mois
L’énoncé : compte le nombre de commandes pour chaque mois.
La notion : GROUP BY sur une date
SELECT date_trunc('month', order_date)::date AS mois,
COUNT(*) AS nb_commandes
FROM orders
GROUP BY mois
ORDER BY mois;
Pourquoi ça marche : date_trunc('month', ...) ramène chaque date au 1er du mois. Toutes les commandes d’un même mois tombent donc dans le même groupe.
Le piège : regrouper sur EXTRACT(MONTH ...) seul. Janvier 2025 et janvier 2026 se retrouvent alors mélangés.
4. Le 2e salaire le plus élevé
L’énoncé : trouve le deuxième salaire le plus élevé de l’entreprise.
La notion : sous-requête
SELECT MAX(salary) AS deuxieme_salaire
FROM employees
WHERE salary < (SELECT MAX(salary) FROM employees);
Pourquoi ça marche : la sous-requête trouve le salaire maximum. On cherche ensuite le plus grand salaire strictement en dessous.
Le piège : ORDER BY salary DESC LIMIT 1 OFFSET 1 sans DISTINCT. Si deux personnes ont le salaire maximum, tu renvoies le premier salaire au lieu du deuxième.
Variante acceptée en entretien :
SELECT DISTINCT salary
FROM employees
ORDER BY salary DESC
OFFSET 1 LIMIT 1;
5. Les départements au-dessus de la moyenne
L’énoncé : liste les départements dont le salaire moyen dépasse le salaire moyen de toute l’entreprise.
La notion : sous-requête dans un HAVING
SELECT department_id, ROUND(AVG(salary), 2) AS moyenne
FROM employees
GROUP BY department_id
HAVING AVG(salary) > (SELECT AVG(salary) FROM employees);
Pourquoi ça marche : on calcule la moyenne de chaque département, puis on la compare à la moyenne globale, calculée par la sous-requête.
Le piège : comparer à la moyenne des moyennes des départements. Ce n’est pas la même chose si les départements n’ont pas le même nombre de personnes.
6. Les employés mieux payés que leur manager
L’énoncé : trouve les employés qui gagnent plus que leur manager.
La notion : jointure d’une table sur elle-même (self join)
SELECT e.name, e.salary, m.name AS manager, m.salary AS salaire_manager
FROM employees e
JOIN employees m ON e.manager_id = m.id
WHERE e.salary > m.salary;
Pourquoi ça marche : on lit la table employees deux fois, une fois comme « employé » (e) et une fois comme « manager » (m), puis on relie chaque employé à son manager.
Le piège : oublier les alias. Sans e et m, impossible de savoir de quel salary on parle.
7. Supprimer les doublons
L’énoncé : supprime les emails en double dans users, en gardant la ligne la plus ancienne (le plus petit id).
La notion : DELETE avec jointure, ou fonction de fenêtre
DELETE FROM users a
USING users b
WHERE a.email = b.email
AND a.id > b.id;
Pourquoi ça marche : pour chaque ligne, s’il existe une autre ligne avec le même email et un id plus petit, elle est supprimée. Seule la plus ancienne reste.
Le piège : lancer un DELETE sans tester. Écris d’abord la même condition dans un SELECT, ou travaille dans une transaction (BEGIN … ROLLBACK).
Variante avec une fonction de fenêtre :
DELETE FROM users
WHERE id IN (
SELECT id FROM (
SELECT id, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
FROM users
) t
WHERE rn > 1
);
8. Le top 3 par catégorie
L’énoncé : pour chaque catégorie, trouve les 3 produits les plus vendus (en quantité).
La notion : fonction de fenêtre ROW_NUMBER
SELECT category, product_id, total
FROM (
SELECT category, product_id, SUM(qty) AS total,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY SUM(qty) DESC) AS rang
FROM sales
GROUP BY category, product_id
) t
WHERE rang <= 3
ORDER BY category, total DESC;
Pourquoi ça marche : PARTITION BY category recommence le classement à 1 dans chaque catégorie. On garde ensuite les rangs 1 à 3.
Le piège : les ex æquo. ROW_NUMBER départage au hasard. Si l’entretien demande de garder les égalités, utilise RANK() ou DENSE_RANK().
9. Le chiffre d’affaires cumulé jour par jour
L’énoncé : affiche, pour chaque jour, le chiffre d’affaires du jour et le cumul depuis le début.
La notion : fonction de fenêtre SUM … OVER
SELECT sale_date,
SUM(amount) AS ca_jour,
SUM(SUM(amount)) OVER (ORDER BY sale_date) AS ca_cumule
FROM sales
GROUP BY sale_date
ORDER BY sale_date;
Pourquoi ça marche : SUM(amount) calcule le total de chaque jour. Le SUM(...) OVER (ORDER BY sale_date) additionne ensuite ces totaux, jour après jour.
Le piège : les jours sans vente n’apparaissent pas. Si on te demande une ligne pour chaque jour, il faut générer le calendrier avec generate_series.
10. La part de chaque catégorie en %
L’énoncé : pour chaque catégorie, affiche son chiffre d’affaires et sa part du total, en pourcentage.
La notion : fonction de fenêtre sur tout le résultat
SELECT category,
SUM(amount) AS ca,
ROUND(100.0 * SUM(amount) / SUM(SUM(amount)) OVER (), 1) AS pct
FROM sales
GROUP BY category;
Pourquoi ça marche : SUM(SUM(amount)) OVER (), avec des parenthèses vides, calcule le total de toutes les catégories sur chaque ligne. Il suffit de diviser.
Le piège : la division entière. 100 * 3 / 7 donne 42 en nombres entiers. Le 100.0 force un calcul décimal.
Les 4 notions qui débloquent tout
| Notion | Requêtes |
|---|---|
| GROUP BY (+ HAVING) | 1, 3 |
| Les jointures (LEFT JOIN, self join) | 2, 6 |
| Les sous-requêtes | 4, 5, 7 |
| Les fonctions de fenêtre (OVER, ROW_NUMBER) | 8, 9, 10 |
Maîtrise ces 4 notions et tu sais écrire les 10.
Pour t’entraîner
- SQLBolt : des exercices courts et interactifs, directement dans le navigateur, pour revoir les bases.
- SQL Murder Mystery : une enquête policière à résoudre uniquement en SQL.
- PostgreSQL Exercises (pgexercises) : des exercices sur PostgreSQL, des bases jusqu’aux fonctions de fenêtre.
Bonne chance pour ton entretien, futur codeur 🚀