Questions d’entretien SQL corrigées
Mis à jour le
Voici les questions SQL qui reviennent le plus souvent en entretien d’embauche, pour un poste de data analyst, de développeur ou d’ingénieur data, avec une réponse courte et, quand il le faut, une requête corrigée. Les requêtes sont écrites en SQLite et ont été exécutées sur les tables ci-dessous ; elles fonctionnent aussi dans PostgreSQL et MySQL 8.
Les tables utilisées
Les questions pratiques utilisent trois tables de SpeedQL : des employés (staff, avec l’identifiant de leur manager), des clients et leurs commandes (customers, orders), et des ventes (sales).
| id | name | department | salary | manager_id | hired |
|---|---|---|---|---|---|
| 1 | Alice | Finance | 52000 | NULL | 2015-03-01 |
| 2 | Bob | IT | 41000 | 1 | 2018-06-15 |
| 3 | Claire | Finance | 38000 | 1 | 2019-01-10 |
| 4 | David | HR | 29000 | 1 | 2020-09-01 |
| 5 | Emma | IT | 50000 | 2 | 2017-11-20 |
| 6 | Farid | IT | 47000 | 2 | 2021-02-14 |
| 7 | Gaelle | HR | 33000 | 4 | 2022-05-30 |
| 8 | Hugo | Finance | 44000 | 3 | 2016-08-08 |
| 9 | Iris | IT | 47000 | 5 | 2023-01-09 |
| 10 | Jules | Finance | 44000 | 3 | 2024-03-18 |
| id | name | city |
|---|---|---|
| 1 | Alice | Paris |
| 2 | Bruno | Lyon |
| 3 | Chloe | Paris |
| 4 | Dylan | Nantes |
| id | customer_id | amount |
|---|---|---|
| 1 | 1 | 120 |
| 2 | 1 | 80 |
| 3 | 2 | 200 |
| 4 | 3 | 50 |
| 5 | 3 | 75 |
| 6 | 3 | 30 |
| id | seller | region | month | amount |
|---|---|---|---|---|
| 1 | Ana | North | 2025-01 | 300 |
| 2 | Ana | North | 2025-02 | 450 |
| 3 | Ana | North | 2025-03 | 400 |
| 4 | Ben | North | 2025-01 | 500 |
| 5 | Ben | North | 2025-02 | 350 |
| 6 | Ben | North | 2025-03 | 600 |
| 7 | Cleo | South | 2025-01 | 200 |
| 8 | Cleo | South | 2025-02 | 700 |
| 9 | Cleo | South | 2025-03 | 650 |
| 10 | Dan | South | 2025-01 | 400 |
| 11 | Dan | South | 2025-02 | 400 |
| 12 | Dan | South | 2025-03 | 100 |
WHERE ou HAVING : lequel utiliser pour filtrer ?
WHERE filtre les lignes avant le regroupement ; HAVING filtre les groupes après GROUP BY. Une condition sur un agrégat (COUNT, SUM, AVG…) va donc dans HAVING, une condition sur une colonne ordinaire dans WHERE, ce qui est aussi plus rapide.
À réviser : la fiche HAVING et les exercices corrigés sur HAVING.
Quelle différence entre INNER JOIN et LEFT JOIN ?
INNER JOIN ne garde que les lignes qui ont une correspondance dans les deux tables. LEFT JOIN garde toutes les lignes de la table de gauche et remplit de NULL les colonnes de droite quand il n’y a pas de correspondance. On utilise LEFT JOIN quand on ne veut perdre aucune ligne de la première table, par exemple pour lister tous les clients, y compris ceux sans commande.
À réviser : les exercices corrigés sur les jointures.
Quelle différence entre UNION et UNION ALL ?
UNION assemble les résultats de deux requêtes et supprime les doublons ; UNION ALL garde toutes les lignes, doublons compris, et il est plus rapide puisqu’il n’a pas à dédoublonner. Ici, les 10 employés appartiennent à 3 services : UNION renvoie 3 lignes, UNION ALL 20.
SELECT COUNT(*)
FROM (SELECT department FROM staff UNION SELECT department FROM staff)
UNION ALL SELECT COUNT(*)
FROM (SELECT department FROM staff UNION ALL SELECT department FROM staff);| COUNT(*) |
|---|
| 3 |
| 20 |
Quelle différence entre DELETE, TRUNCATE et DROP ?
DELETE supprime des lignes, toutes ou seulement celles d’un WHERE, une par une ; TRUNCATE vide toute la table d’un coup, bien plus vite, sans condition possible ; DROP supprime la table elle-même, structure comprise. SQLite n’a pas de TRUNCATE : un DELETE sans WHERE joue ce rôle.
Qu’est-ce qu’une clé primaire et une clé étrangère ?
La clé primaire identifie chaque ligne de façon unique et ne peut pas être NULL : staff.id par exemple. Une clé étrangère est une colonne qui fait référence à la clé primaire d’une autre table (ou de la même) : orders.customer_id pointe vers customers.id, et staff.manager_id vers staff.id. Elle garantit qu’on ne crée pas de commande pour un client inexistant.
À quoi sert un index, et quand en créer un ?
Un index est une structure triée, comme l’index d’un livre, qui permet de retrouver des lignes sans lire toute la table. Il accélère les WHERE, les jointures et les ORDER BY sur les colonnes indexées, mais ralentit un peu les INSERT et UPDATE et prend de la place. On indexe donc les colonnes souvent filtrées ou jointes, comme les clés étrangères.
Comment se comporte NULL, et que compte COUNT ?
NULL signifie « valeur inconnue » : toute comparaison avec NULL, même NULL = NULL, n’est ni vraie ni fausse. On teste donc IS NULL ou IS NOT NULL. COUNT(*) compte toutes les lignes, alors que COUNT(colonne) ignore les NULL : ici, 10 employés, dont 9 ont un manager.
SELECT COUNT(*) AS rows_count, COUNT(manager_id) AS with_manager
FROM staff;| rows_count | with_manager |
|---|---|
| 10 | 9 |
Dans quel ordre une requête SQL est-elle exécutée ?
L’ordre logique n’est pas celui de l’écriture : FROM et JOIN, puis WHERE, GROUP BY, HAVING, SELECT, DISTINCT, ORDER BY et enfin LIMIT. C’est pour cela qu’en SQL standard un alias défini dans SELECT n’est pas utilisable dans WHERE, mais l’est dans ORDER BY.
ROW_NUMBER, RANK ou DENSE_RANK : quelle différence ?
Les trois numérotent les lignes et ne diffèrent qu’en cas d’égalité. ROW_NUMBER donne toujours un numéro unique ; RANK donne le même rang aux ex æquo puis saute des rangs ; DENSE_RANK donne le même rang sans saut. Regarde Farid et Iris, ex æquo à 47 000.
SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn, RANK() OVER (ORDER BY salary DESC) AS rk, DENSE_RANK() OVER (ORDER BY salary DESC) AS drk
FROM staff;| name | salary | rn | rk | drk |
|---|---|---|---|---|
| Alice | 52000 | 1 | 1 | 1 |
| Emma | 50000 | 2 | 2 | 2 |
| Farid | 47000 | 3 | 3 | 3 |
| Iris | 47000 | 4 | 3 | 3 |
| Hugo | 44000 | 5 | 5 | 4 |
| Jules | 44000 | 6 | 5 | 4 |
| Bob | 41000 | 7 | 7 | 5 |
| Claire | 38000 | 8 | 8 | 6 |
| Gaelle | 33000 | 9 | 9 | 7 |
| David | 29000 | 10 | 10 | 8 |
À réviser : les exercices corrigés sur les fonctions de fenêtre.
Qu’est-ce que la normalisation d’une base de données ?
C’est l’organisation des tables pour éviter les répétitions et les incohérences. En pratique : une valeur par cellule (1re forme normale), chaque colonne dépend de toute la clé (2e), et pas d’une autre colonne non clé (3e). Au lieu de répéter le nom et la ville du client dans chaque commande, on les met une fois dans customers et on relie les commandes par customer_id.
Comment trouver le deuxième salaire le plus élevé ?
Le classique : prendre le plus grand salaire parmi ceux qui sont inférieurs au maximum. Cette version gère les ex æquo et renvoie NULL s’il n’y a qu’un seul salaire.
SELECT MAX(salary) AS second_salary
FROM staff
WHERE salary < (SELECT MAX(salary) FROM staff);| second_salary |
|---|
| 50000 |
Autre écriture, avec DISTINCT pour ne pas compter deux fois le même salaire :
SELECT DISTINCT salary
FROM staff
ORDER BY salary DESC
LIMIT 1 OFFSET 1;Comment trouver les valeurs en double dans une colonne ?
On regroupe sur la colonne et on garde les groupes qui comptent plus d’une ligne, avec HAVING COUNT(*) > 1.
SELECT department, COUNT(*) AS n
FROM staff
GROUP BY department
HAVING COUNT(*) > 1;| department | n |
|---|---|
| Finance | 4 |
| HR | 2 |
| IT | 4 |
Comment lister les employés mieux payés que leur manager ?
C’est une auto-jointure : on joint la table staff à elle-même, une fois pour l’employé (e) et une fois pour son manager (m), puis on compare les deux salaires.
SELECT e.name, e.salary, m.name AS manager, m.salary AS manager_salary
FROM staff e
JOIN staff m ON m.id = e.manager_id
WHERE e.salary > m.salary;| name | salary | manager | manager_salary |
|---|---|---|---|
| Emma | 50000 | Bob | 41000 |
| Farid | 47000 | Bob | 41000 |
| Gaelle | 33000 | David | 29000 |
| Hugo | 44000 | Claire | 38000 |
| Jules | 44000 | Claire | 38000 |
Comment trouver les clients qui n’ont jamais commandé ?
Avec un LEFT JOIN, les clients sans commande ont des colonnes de orders à NULL : on les garde avec IS NULL.
SELECT c.name
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE o.id IS NULL;| name |
|---|
| Dylan |
Même résultat avec NOT EXISTS, souvent plus lisible et plus sûr que NOT IN quand la sous-requête peut contenir des NULL :
SELECT name
FROM customers c
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);Comment obtenir le meilleur élément de chaque groupe ?
On numérote les lignes dans chaque groupe avec ROW_NUMBER() OVER (PARTITION BY …), dans une CTE, puis on garde le numéro 1. Pour les N premiers, il suffit d’écrire n <= N.
WITH r AS (
SELECT name, department, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS n
FROM staff)
SELECT department, name, salary
FROM r
WHERE n = 1;| department | name | salary |
|---|---|---|
| Finance | Alice | 52000 |
| HR | Gaelle | 33000 |
| IT | Emma | 50000 |
Comment calculer un cumul et un pourcentage du total ?
Une fonction de fenêtre calcule sur un ensemble de lignes sans les regrouper. SUM() OVER (ORDER BY …) donne un total cumulé :
SELECT month, SUM(amount) AS total, SUM(SUM(amount)) OVER (ORDER BY month) AS running_total
FROM sales
GROUP BY month
ORDER BY month;| month | total | running_total |
|---|---|---|
| 2025-01 | 1400 | 1400 |
| 2025-02 | 1900 | 3300 |
| 2025-03 | 1750 | 5050 |
Et SUM() OVER () (fenêtre vide) donne le total général, qui sert à calculer une part en pourcentage :
SELECT region, SUM(amount) AS total, ROUND(100.0 * SUM(amount) / SUM(SUM(amount)) OVER (), 1) AS pct
FROM sales
GROUP BY region;| region | total | pct |
|---|---|---|
| North | 2600 | 51.5 |
| South | 2450 | 48.5 |
S’entraîner pour l’entretien
Le plus efficace reste d’écrire des requêtes, vite et souvent. SpeedQL te fait écrire de vraies requêtes contre la montre, vérifiées tout de suite.
Lancer une partie chronométrée
Les 196 exercices SQL corrigés · Apprendre le SQL : par où commencer