Questions d’entretien SQL corrigées

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).

Table staff (10 lignes)
idnamedepartmentsalarymanager_idhired
1AliceFinance52000NULL2015-03-01
2BobIT4100012018-06-15
3ClaireFinance3800012019-01-10
4DavidHR2900012020-09-01
5EmmaIT5000022017-11-20
6FaridIT4700022021-02-14
7GaelleHR3300042022-05-30
8HugoFinance4400032016-08-08
9IrisIT4700052023-01-09
10JulesFinance4400032024-03-18
Table customers (4 lignes)
idnamecity
1AliceParis
2BrunoLyon
3ChloeParis
4DylanNantes
Table orders (6 lignes)
idcustomer_idamount
11120
2180
32200
4350
5375
6330
Table sales (12 lignes)
idsellerregionmonthamount
1AnaNorth2025-01300
2AnaNorth2025-02450
3AnaNorth2025-03400
4BenNorth2025-01500
5BenNorth2025-02350
6BenNorth2025-03600
7CleoSouth2025-01200
8CleoSouth2025-02700
9CleoSouth2025-03650
10DanSouth2025-01400
11DanSouth2025-02400
12DanSouth2025-03100

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);
Résultat
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;
Résultat
rows_countwith_manager
109

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;
Résultat
namesalaryrnrkdrk
Alice52000111
Emma50000222
Farid47000333
Iris47000433
Hugo44000554
Jules44000654
Bob41000775
Claire38000886
Gaelle33000997
David2900010108

À 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);
Résultat
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;
Résultat
departmentn
Finance4
HR2
IT4

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;
Résultat
namesalarymanagermanager_salary
Emma50000Bob41000
Farid47000Bob41000
Gaelle33000David29000
Hugo44000Claire38000
Jules44000Claire38000

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;
Résultat
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;
Résultat
departmentnamesalary
FinanceAlice52000
HRGaelle33000
ITEmma50000

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;
Résultat
monthtotalrunning_total
2025-0114001400
2025-0219003300
2025-0317505050

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;
Résultat
regiontotalpct
North260051.5
South245048.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