Guide pratique · Niveau intermédiaire → avancé · Lecture : ~20 min


Introduction

SQL est cité avant Python, avant dbt, avant Spark dans les offres data en 2026. Pourtant c’est la compétence la moins enseignée en bootcamp.

Ce guide compile les 41 requêtes les plus demandées en entretien, organisées en 5 blocs :

  • Bloc 1 — Fenêtres analytiques : RANK, ROW_NUMBER, LAG (12 requêtes)

  • Bloc 2 — Jointures avancées et cas pièges (8 requêtes)

  • Bloc 3 — Agrégations conditionnelles (8 requêtes)

  • Bloc 4 — Optimisation et lecture de plans d’exécution (7 requêtes)

  • Bloc 5 — Requêtes récursives et CTE avancées (6 requêtes)


Pourquoi SQL en 2026 ?

  • Un Data Analyst sans SQL solide passe ses journées à attendre un Data Engineer

  • Un Data Engineer sans SQL avancé produit des pipelines que personne ne peut déboguer

  • Un Data Scientist sans SQL ne peut pas explorer ses données lui-même

  • L’IA génère du SQL — mais si tu ne sais pas lire ce qu’elle produit, tu valides des erreurs sans le savoir


BLOC 1 — Fenêtres analytiques

Les window functions sont présentes dans 90% des entretiens data senior. Maîtrisez-les.


Requête 1 — ROW_NUMBER : numéroter les lignes par groupe

Contexte d’entretien : “Récupère la commande la plus récente de chaque client.”

SELECT *
FROM (
SELECT
user_id,
order_id,
montant,
created_at,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY created_at DESC
) AS rn
FROM orders
) sub
WHERE rn = ;
41-requetes-sql-entretien · 01

💡 ROW_NUMBER attribue un numéro unique même si deux lignes sont ex-aequo. Utilisez RANK ou DENSE_RANK si vous voulez gérer les égalités.


Requête 2 — RANK vs DENSE_RANK : gérer les ex-aequo

Contexte d’entretien : “Quelle est la différence entre RANK et DENSE_RANK ?”

SELECT
nom,
score,
RANK() OVER (ORDER BY score DESC) AS rang_avec_saut,
DENSE_RANK() OVER (ORDER BY score DESC) AS rang_sans_saut,
ROW_NUMBER() OVER (ORDER BY score DESC) AS numero_unique
FROM joueurs;
41-requetes-sql-entretien · 02

Résultat illustré :

nom

score

RANK

DENSE_RANK

ROW_NUMBER

Alice

100

1

1

1

Bob

100

1

1

2

Claire

90

3

2

3

David

80

4

3

4

💡 RANK saute des numéros après les ex-aequo (1,1,3). DENSE_RANK ne saute pas (1,1,2). ROW_NUMBER est toujours unique.


Requête 3 — LAG et LEAD : comparer avec la ligne précédente/suivante

Contexte d’entretien : “Calcule la variation de chiffre d’affaires mois par mois.”

SELECT
mois,
ca,
LAG(ca) OVER (ORDER BY mois) AS ca_mois_precedent,
LEAD(ca) OVER (ORDER BY mois) AS ca_mois_suivant,
ca - LAG(ca) OVER (ORDER BY mois) AS variation_absolue,
ROUND(
. * (ca - LAG(ca) OVER (ORDER BY mois))
/ NULLIF(LAG(ca) OVER (ORDER BY mois), ),
) AS variation_pct
FROM revenus_mensuels
ORDER BY mois;
41-requetes-sql-entretien · 03

⚠️ Utilisez NULLIF(..., 0) pour éviter une division par zéro quand le CA précédent est nul.


Requête 4 — SUM cumulatif (running total)

Contexte d’entretien : “Calcule le cumul des ventes depuis le début de l’année.”

SELECT
date_vente,
montant,
SUM(montant) OVER (
ORDER BY date_vente
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS cumul_ventes
FROM ventes
ORDER BY date_vente;
41-requetes-sql-entretien · 04

Requête 5 — Moyenne mobile sur N jours

Contexte d’entretien : “Calcule une moyenne mobile sur 7 jours.”

SELECT
date_vente,
montant,
AVG(montant) OVER (
ORDER BY date_vente
ROWS BETWEEN PRECEDING AND CURRENT ROW
) AS moyenne_mobile_7j
FROM ventes
ORDER BY date_vente;
41-requetes-sql-entretien · 05

Requête 6 — NTILE : diviser en quartiles ou déciles

Contexte d’entretien : “Segmente les clients en 4 quartiles selon leur CA.”

SELECT
user_id,
ca_total,
NTILE() OVER (ORDER BY ca_total DESC) AS quartile,
CASE NTILE() OVER (ORDER BY ca_total DESC)
WHEN THEN 'Top 25% — Champions'
WHEN THEN 'Bons clients'
WHEN THEN 'Clients moyens'
WHEN THEN 'Clients à risque'
END AS segment
FROM (
SELECT user_id, SUM(montant) AS ca_total
FROM orders
GROUP BY user_id
) t;
41-requetes-sql-entretien · 06

Requête 7 — FIRST_VALUE et LAST_VALUE

Contexte d’entretien : “Pour chaque commande, affiche aussi le montant de la première commande du client.”

SELECT
user_id,
order_id,
montant,
created_at,
FIRST_VALUE(montant) OVER (
PARTITION BY user_id
ORDER BY created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS premiere_commande,
LAST_VALUE(montant) OVER (
PARTITION BY user_id
ORDER BY created_at
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS derniere_commande
FROM orders;
41-requetes-sql-entretien · 07

⚠️ LAST_VALUE nécessite ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING — sinon il retourne la valeur courante par défaut.


Requête 8 — PERCENT_RANK et CUME_DIST

Contexte d’entretien : “Quel est le percentile de chaque vendeur dans son équipe ?”

SELECT
vendeur,
equipe,
ca,
ROUND(PERCENT_RANK() OVER (
PARTITION BY equipe ORDER BY ca
) * , ) AS percentile,
ROUND(CUME_DIST() OVER (
PARTITION BY equipe ORDER BY ca
) * , ) AS cume_dist_pct
FROM vendeurs;
41-requetes-sql-entretien · 08

Requête 9 — Détection des gaps dans une séquence

Contexte d’entretien : “Trouve les IDs manquants dans une séquence.”

SELECT
id + AS id_manquant_debut,
next_id - AS id_manquant_fin
FROM (
SELECT
id,
LEAD(id) OVER (ORDER BY id) AS next_id
FROM ma_table
) t
WHERE next_id - id > ;
41-requetes-sql-entretien · 09

Requête 10 — Sessionisation : regrouper les événements par session

Contexte d’entretien : “Groupe les pages vues d’un utilisateur en sessions (inactivité > 30 min = nouvelle session).”

WITH events_with_gap AS (
SELECT
user_id,
event_time,
LAG(event_time) OVER (PARTITION BY user_id ORDER BY event_time) AS prev_time,
CASE
WHEN event_time - LAG(event_time) OVER (
PARTITION BY user_id ORDER BY event_time
) > INTERVAL '30 minutes'
THEN ELSE
END AS new_session
FROM page_views
),
sessions AS (
SELECT
user_id,
event_time,
SUM(new_session) OVER (
PARTITION BY user_id ORDER BY event_time
) AS session_id
FROM events_with_gap
)
SELECT user_id, session_id, MIN(event_time) AS debut, MAX(event_time) AS fin,
COUNT(*) AS nb_pages
FROM sessions
GROUP BY user_id, session_id;
41-requetes-sql-entretien · 10

Requête 11 — Top N par catégorie

Contexte d’entretien : “Retourne les 3 produits les plus vendus par catégorie.”

SELECT *
FROM (
SELECT
categorie,
produit,
total_ventes,
RANK() OVER (PARTITION BY categorie ORDER BY total_ventes DESC) AS rang
FROM (
SELECT categorie, produit, SUM(quantite) AS total_ventes
FROM ventes
GROUP BY categorie, produit
) agg
) ranked
WHERE rang <= ;
41-requetes-sql-entretien · 11

Requête 12 — Médiane avec PERCENTILE_CONT

Contexte d’entretien : “Calcule la médiane des salaires par département.”

SELECT
departement,
PERCENTILE_CONT(.) WITHIN GROUP (ORDER BY salaire) AS mediane,
PERCENTILE_CONT(.) WITHIN GROUP (ORDER BY salaire) AS q1,
PERCENTILE_CONT(.) WITHIN GROUP (ORDER BY salaire) AS q3
FROM employes
GROUP BY departement;
41-requetes-sql-entretien · 12

BLOC 2 — Jointures avancées et cas pièges


Requête 13 — SELF JOIN : comparer des lignes de la même table

Contexte d’entretien : “Trouve les employés qui gagnent plus que leur manager.”

SELECT
e.nom AS employe,
e.salaire AS salaire_employe,
m.nom AS manager,
m.salaire AS salaire_manager
FROM employes e
JOIN employes m ON e.manager_id = m.id
WHERE e.salaire > m.salaire;
41-requetes-sql-entretien · 13

Requête 14 — Trouver les lignes sans correspondance

Contexte d’entretien : “Trouve les clients qui n’ont jamais commandé.”

-- Méthode 1 : LEFT JOIN + IS NULL (la plus rapide)
SELECT u.id, u.nom
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.user_id IS NULL;
 
-- Méthode 2 : NOT EXISTS (lisible)
SELECT id, nom FROM users u
WHERE NOT EXISTS (
SELECT FROM orders o WHERE o.user_id = u.id
);
 
-- Méthode 3 : NOT IN (attention aux NULL !)
SELECT id, nom FROM users
WHERE id NOT IN (
SELECT DISTINCT user_id FROM orders
WHERE user_id IS NOT NULL -- indispensable
);
41-requetes-sql-entretien · 14

⚠️ NOT IN avec un sous-ensemble contenant des NULL retourne toujours 0 résultats. Toujours préférer NOT EXISTS ou LEFT JOIN IS NULL.


Requête 15 — CROSS JOIN pour générer des combinaisons

Contexte d’entretien : “Génère toutes les combinaisons produit × région pour un rapport.”

SELECT p.nom AS produit, r.nom AS region
FROM produits p
CROSS JOIN regions r
ORDER BY p.nom, r.nom;
41-requetes-sql-entretien · 15

Requête 16 — Jointure sur plusieurs conditions

Contexte d’entretien : “Joint les commandes à la table de prix en tenant compte de la période de validité.”

SELECT
o.order_id,
o.produit_id,
o.quantite,
p.prix,
o.quantite * p.prix AS total
FROM orders o
JOIN prix_historique p
ON o.produit_id = p.produit_id
AND o.created_at BETWEEN p.valide_du AND p.valide_au;
41-requetes-sql-entretien · 16

Requête 17 — UNION vs UNION ALL

Contexte d’entretien : “Quelle est la différence entre UNION et UNION ALL ?”

-- UNION : déduplique (plus lent)
SELECT user_id FROM clients_fr
UNION
SELECT user_id FROM clients_be;
 
-- UNION ALL : garde les doublons (plus rapide)
SELECT user_id, 'FR' AS pays FROM clients_fr
UNION ALL
SELECT user_id, 'BE' AS pays FROM clients_be;
41-requetes-sql-entretien · 17

💡 Utilisez toujours UNION ALL sauf si vous avez besoin explicitement de dédupliquer. UNION fait un tri supplémentaire coûteux.


Requête 18 — Piège : la multiplication de lignes avec JOIN

Contexte d’entretien : “Pourquoi ta requête retourne plus de lignes qu’attendu ?”

-- Problème : si un user a plusieurs lignes dans user_tags,
-- le COUNT sera multiplié
SELECT u.id, COUNT(o.id) AS nb_commandes
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN user_tags t ON u.id = t.user_id -- multiplie les lignes !
GROUP BY u.id;
 
-- Solution : agréger d'abord, joindre ensuite
SELECT u.id, COALESCE(o.nb_commandes, ) AS nb_commandes
FROM users u
LEFT JOIN (
SELECT user_id, COUNT(*) AS nb_commandes
FROM orders GROUP BY user_id
) o ON u.id = o.user_id;
41-requetes-sql-entretien · 18

Requête 19 — LATERAL JOIN (ou CROSS APPLY en SQL Server)

Contexte d’entretien : “Pour chaque catégorie, retourne les 2 produits les plus chers.”

-- PostgreSQL
SELECT c.nom AS categorie, p.nom AS produit, p.prix
FROM categories c
CROSS JOIN LATERAL (
SELECT nom, prix
FROM produits
WHERE categorie_id = c.id
ORDER BY prix DESC
LIMIT
) p;
41-requetes-sql-entretien · 19

Requête 20 — Déduplication avec JOIN

Contexte d’entretien : “Supprime les doublons en gardant la ligne la plus récente.”

DELETE FROM users
WHERE id NOT IN (
SELECT MAX(id)
FROM users
GROUP BY email
);
 
-- Alternative avec CTE (plus lisible)
WITH doublons AS (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY email ORDER BY created_at DESC) AS rn
FROM users
)
DELETE FROM users WHERE id IN (
SELECT id FROM doublons WHERE rn >
);
41-requetes-sql-entretien · 20

BLOC 3 — Agrégations conditionnelles


Requête 21 — SUM/COUNT conditionnel avec CASE WHEN

Contexte d’entretien : “Calcule le nombre de commandes livrées vs annulées par mois, dans une seule requête.”

SELECT
DATE_TRUNC('month', created_at) AS mois,
COUNT(*) AS total,
COUNT(CASE WHEN statut = 'livré' THEN END) AS livrees,
COUNT(CASE WHEN statut = 'annulé' THEN END) AS annulees,
SUM(CASE WHEN statut = 'livré' THEN montant ELSE END) AS ca_livre,
ROUND(. * COUNT(CASE WHEN statut = 'livré' THEN END)
/ NULLIF(COUNT(*), ), ) AS taux_livraison_pct
FROM orders
GROUP BY
ORDER BY ;
41-requetes-sql-entretien · 21

Requête 22 — FILTER (syntaxe moderne PostgreSQL)

Contexte d’entretien : Même exercice, syntaxe plus propre.

SELECT
DATE_TRUNC('month', created_at) AS mois,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE statut = 'livré') AS livrees,
COUNT(*) FILTER (WHERE statut = 'annulé') AS annulees,
SUM(montant) FILTER (WHERE statut = 'livré') AS ca_livre
FROM orders
GROUP BY
ORDER BY ;
41-requetes-sql-entretien · 22

💡 La syntaxe FILTER (WHERE ...) est plus lisible que CASE WHEN et légèrement plus performante. Disponible sur PostgreSQL 9.4+.


Requête 23 — Pivot manuel avec agrégation conditionnelle

Contexte d’entretien : “Transforme les lignes en colonnes (pivot) sans extension.”

-- Données : (user_id, mois, montant)
-- Objectif : une ligne par user avec jan, fev, mar en colonnes
 
SELECT
user_id,
SUM(CASE WHEN mois = '2024-01' THEN montant ELSE END) AS janvier,
SUM(CASE WHEN mois = '2024-02' THEN montant ELSE END) AS fevrier,
SUM(CASE WHEN mois = '2024-03' THEN montant ELSE END) AS mars
FROM ventes_mensuelles
GROUP BY user_id;
41-requetes-sql-entretien · 23

Requête 24 — Taux de rétention mensuel

Contexte d’entretien : “Calcule le taux de rétention des utilisateurs mois par mois.”

WITH cohortes AS (
SELECT user_id, DATE_TRUNC('month', MIN(created_at)) AS mois_acquisition
FROM orders
GROUP BY user_id
),
activite AS (
SELECT DISTINCT user_id, DATE_TRUNC('month', created_at) AS mois_actif
FROM orders
)
SELECT
c.mois_acquisition,
a.mois_actif,
EXTRACT(EPOCH FROM (a.mois_actif - c.mois_acquisition)) / AS mois_depuis_acq,
COUNT(DISTINCT a.user_id) AS users_actifs,
ROUND(. * COUNT(DISTINCT a.user_id)
/ COUNT(DISTINCT c.user_id), ) AS taux_retention_pct
FROM cohortes c
LEFT JOIN activite a ON c.user_id = a.user_id
GROUP BY ,
ORDER BY , ;
41-requetes-sql-entretien · 24

Requête 25 — Agrégation sur tableau imbriqué (JSON/ARRAY)

Contexte d’entretien : “Compte le nombre de tags distincts par article (données JSON).”

-- PostgreSQL avec colonne JSONB
SELECT
article_id,
jsonb_array_length(tags) AS nb_tags,
tags ->> AS premier_tag
FROM articles;
 
-- Déplier les tags pour les compter globalement
SELECT tag, COUNT(*) AS nb_articles
FROM articles, jsonb_array_elements_text(tags) AS tag
GROUP BY tag
ORDER BY nb_articles DESC;
41-requetes-sql-entretien · 25

Requête 26 — GROUP BY ROLLUP : sous-totaux automatiques

Contexte d’entretien : “Génère un rapport avec sous-totaux par région et total général.”

SELECT
COALESCE(region, 'TOTAL') AS region,
COALESCE(categorie, 'Toutes catégories') AS categorie,
SUM(ca) AS ca_total
FROM ventes
GROUP BY ROLLUP(region, categorie)
ORDER BY region, categorie;
41-requetes-sql-entretien · 26

Requête 27 — GROUP BY CUBE : toutes les combinaisons

-- ROLLUP : sous-totaux hiérarchiques (region > categorie > total)
-- CUBE : toutes les combinaisons possibles de dimensions
SELECT region, categorie, vendeur, SUM(ca)
FROM ventes
GROUP BY CUBE(region, categorie, vendeur);
41-requetes-sql-entretien · 27

Requête 28 — STRING_AGG : concaténer des valeurs groupées

Contexte d’entretien : “Pour chaque commande, liste les produits séparés par une virgule.”

SELECT
order_id,
STRING_AGG(produit_nom, ', ' ORDER BY produit_nom) AS liste_produits,
COUNT(*) AS nb_produits,
SUM(prix) AS total
FROM order_lines
GROUP BY order_id;
41-requetes-sql-entretien · 28

BLOC 4 — Optimisation et lecture de plans d’exécution


Requête 29 — Lire un plan EXPLAIN ANALYZE

Contexte d’entretien : “Qu’est-ce que ce plan d’exécution vous dit ?”

EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT u.nom, COUNT(o.id)
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id, u.nom;
41-requetes-sql-entretien · 29

Les nœuds clés à reconnaître :

Nœud

Signification

Action

Seq Scan

Lecture séquentielle complète

Ajouter un index

Index Scan

Utilisation d’un index

✅ Bon

Index Only Scan

Index couvre tout

✅ Excellent

Hash Join

Jointure par table de hachage

✅ Souvent optimal

Nested Loop

Boucle imbriquée

⚠️ Mauvais sur grandes tables

Sort

Tri explicite

Index sur ORDER BY

Hash Aggregate

Agrégation par hash

Normal


Requête 30 — Index composite : ordre des colonnes

Contexte d’entretien : “Quel index créer pour optimiser cette requête ?”

-- Requête à optimiser
SELECT id, email, nom
FROM users
WHERE statut = 'actif'
AND created_at > '2024-01-01'
ORDER BY created_at DESC;
 
-- Index optimal : colonnes du WHERE en premier, puis ORDER BY
CREATE INDEX idx_users_statut_date
ON users(statut, created_at DESC)
INCLUDE (email, nom); -- colonnes du SELECT : index couvrant
41-requetes-sql-entretien · 30

💡 Un index couvrant (INCLUDE) évite de retourner lire la table principale. Le plan affiche Index Only Scan au lieu de Index Scan.


Requête 31 — Réécrire une sous-requête lente en JOIN

Contexte d’entretien : “Optimise cette requête.”

-- Lent : sous-requête recalculée pour chaque ligne
SELECT *
FROM produits
WHERE categorie_id IN (
SELECT id FROM categories WHERE actif = true
);
 
-- Rapide : JOIN direct
SELECT p.*
FROM produits p
JOIN categories c ON p.categorie_id = c.id
WHERE c.actif = true;
41-requetes-sql-entretien · 31

Requête 32 — Éviter les fonctions sur colonnes indexées dans WHERE

Contexte d’entretien : “Pourquoi cet index n’est pas utilisé ?”

-- Index inutilisé : fonction appliquée sur la colonne indexée
SELECT * FROM orders
WHERE DATE_TRUNC('month', created_at) = '2024-01-01';
 
-- Index utilisé : transformer la condition, pas la colonne
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
AND created_at < '2024-02-01';
41-requetes-sql-entretien · 32

Requête 33 — Statistiques et ANALYZE

-- Mettre à jour les statistiques pour que l'optimiseur fasse de bons choix
ANALYZE users;
 
-- Voir les statistiques d'une colonne
SELECT * FROM pg_stats
WHERE tablename = 'users' AND attname = 'statut';
41-requetes-sql-entretien · 33

Requête 34 — Partitionnement : interroger une seule partition

-- Table partitionnée par mois
CREATE TABLE orders (
id BIGINT,
created_at TIMESTAMPTZ,
montant NUMERIC
) PARTITION BY RANGE (created_at);
 
CREATE TABLE orders_2024_01
PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
 
-- La requête ne touche que la partition concernée (partition pruning)
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01';
41-requetes-sql-entretien · 34

Requête 35 — Materialized View : pré-calculer des agrégats lourds

-- Créer une vue matérialisée
CREATE MATERIALIZED VIEW mv_ca_mensuel AS
SELECT
DATE_TRUNC('month', created_at) AS mois,
SUM(montant) AS ca,
COUNT(*) AS nb_orders
FROM orders
GROUP BY ;
 
-- Rafraîchir (à planifier via cron ou dbt)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_ca_mensuel;
 
-- Requête instantanée
SELECT * FROM mv_ca_mensuel ORDER BY mois DESC LIMIT ;
41-requetes-sql-entretien · 35

BLOC 5 — CTE et requêtes récursives


Requête 36 — CTE simple : lisibilité et réutilisation

Contexte d’entretien : “Réécris cette requête imbriquée avec des CTE.”

WITH
users_actifs AS (
SELECT id, nom, email
FROM users
WHERE statut = 'actif' AND created_at > '2023-01-01'
),
commandes_recentes AS (
SELECT user_id, COUNT(*) AS nb, SUM(montant) AS ca
FROM orders
WHERE created_at > '2024-01-01'
GROUP BY user_id
)
SELECT
u.nom,
u.email,
COALESCE(c.nb, ) AS nb_commandes,
COALESCE(c.ca, ) AS ca_total
FROM users_actifs u
LEFT JOIN commandes_recentes c ON u.id = c.user_id
ORDER BY ca_total DESC;
41-requetes-sql-entretien · 36

Requête 37 — CTE récursive : parcourir une hiérarchie

Contexte d’entretien : “Affiche l’arborescence complète d’une organisation.”

WITH RECURSIVE hierarchie AS (
-- Ancre : le PDG (pas de manager)
SELECT id, nom, manager_id, AS niveau, nom AS chemin
FROM employes
WHERE manager_id IS NULL
 
UNION ALL
 
-- Récursion : les subordonnés
SELECT e.id, e.nom, e.manager_id,
h.niveau + ,
h.chemin || ' > ' || e.nom
FROM employes e
JOIN hierarchie h ON e.manager_id = h.id
)
SELECT
REPEAT(' ', niveau) || nom AS arbre,
niveau,
chemin
FROM hierarchie
ORDER BY chemin;
41-requetes-sql-entretien · 37

Requête 38 — CTE récursive : générer une série de dates

Contexte d’entretien : “Génère un calendrier de dates pour un rapport sans trous.”

WITH RECURSIVE calendrier AS (
SELECT '2024-01-01'::DATE AS jour
 
UNION ALL
 
SELECT jour + INTERVAL '1 day'
FROM calendrier
WHERE jour < '2024-12-31'
)
-- LEFT JOIN pour inclure les jours sans ventes
SELECT c.jour, COALESCE(SUM(o.montant), ) AS ca
FROM calendrier c
LEFT JOIN orders o ON DATE_TRUNC('day', o.created_at) = c.jour
GROUP BY c.jour
ORDER BY c.jour;
41-requetes-sql-entretien · 38

💡 En PostgreSQL, utilisez plutôt generate_series('2024-01-01'::DATE, '2024-12-31'::DATE, '1 day') pour les séries simples. Les CTE récursives restent utiles pour les hiérarchies.


Requête 39 — CTE récursive : calcul de factorielle et suites

Contexte d’entretien : “Implémente la suite de Fibonacci en SQL.”

WITH RECURSIVE fibonacci AS (
SELECT AS n, AS a, AS b
 
UNION ALL
 
SELECT n + , b, a + b
FROM fibonacci
WHERE n <
)
SELECT n, a AS valeur
FROM fibonacci;
41-requetes-sql-entretien · 39

Requête 40 — CTE pour détecter des cycles dans un graphe

Contexte d’entretien : “Détecte les références circulaires dans une table parent-enfant.”

WITH RECURSIVE chemins AS (
SELECT id, manager_id, ARRAY[id] AS visites, false AS cycle
FROM employes
WHERE manager_id IS NULL
 
UNION ALL
 
SELECT e.id, e.manager_id,
c.visites || e.id,
e.id = ANY(c.visites) -- détection du cycle
FROM employes e
JOIN chemins c ON e.manager_id = c.id
WHERE NOT c.cycle
)
SELECT id, visites AS chemin
FROM chemins
WHERE cycle = true;
41-requetes-sql-entretien · 40

Requête 41 — CTE + Window Function : rapport complet en une requête

Contexte d’entretien : “La question finale des entretiens senior.”

Construire un rapport complet : CA par mois, variation vs mois précédent, rang dans l’année, cumul YTD.

WITH ca_mensuel AS (
SELECT
DATE_TRUNC('month', created_at) AS mois,
SUM(montant) AS ca
FROM orders
WHERE created_at >= DATE_TRUNC('year', NOW())
GROUP BY
),
ca_avec_comparaison AS (
SELECT
mois,
ca,
LAG(ca) OVER (ORDER BY mois) AS ca_mois_precedent
FROM ca_mensuel
)
SELECT
TO_CHAR(mois, 'Mon YYYY') AS mois,
ca,
ca_mois_precedent,
ca - ca_mois_precedent AS variation,
ROUND(. * (ca - ca_mois_precedent)
/ NULLIF(ca_mois_precedent, ), ) AS variation_pct,
RANK() OVER (ORDER BY ca DESC) AS rang_ca,
SUM(ca) OVER (ORDER BY mois ROWS UNBOUNDED PRECEDING) AS cumul_ytd
FROM ca_avec_comparaison
ORDER BY mois;
41-requetes-sql-entretien · 41

📋 Récapitulatif des 41 requêtes

#

Requête

Bloc

Fréquence entretien

1

ROW_NUMBER — top 1 par groupe

Fenêtres

⭐⭐⭐⭐⭐

2

RANK vs DENSE_RANK

Fenêtres

⭐⭐⭐⭐⭐

3

LAG / LEAD — variation M/M

Fenêtres

⭐⭐⭐⭐⭐

4

SUM cumulatif (running total)

Fenêtres

⭐⭐⭐⭐

5

Moyenne mobile N jours

Fenêtres

⭐⭐⭐⭐

6

NTILE — quartiles

Fenêtres

⭐⭐⭐

7

FIRST_VALUE / LAST_VALUE

Fenêtres

⭐⭐⭐

8

PERCENT_RANK / CUME_DIST

Fenêtres

⭐⭐

9

Détection de gaps

Fenêtres

⭐⭐⭐

10

Sessionisation

Fenêtres

⭐⭐⭐⭐

11

Top N par catégorie

Fenêtres

⭐⭐⭐⭐⭐

12

Médiane avec PERCENTILE_CONT

Fenêtres

⭐⭐⭐

13

SELF JOIN — manager vs employé

Jointures

⭐⭐⭐⭐

14

Lignes sans correspondance

Jointures

⭐⭐⭐⭐⭐

15

CROSS JOIN — combinaisons

Jointures

⭐⭐

16

JOIN multi-conditions (dates)

Jointures

⭐⭐⭐⭐

17

UNION vs UNION ALL

Jointures

⭐⭐⭐⭐

18

Piège multiplication de lignes

Jointures

⭐⭐⭐⭐⭐

19

LATERAL JOIN

Jointures

⭐⭐⭐

20

Déduplication avec JOIN

Jointures

⭐⭐⭐⭐

21

SUM/COUNT avec CASE WHEN

Agrégations

⭐⭐⭐⭐⭐

22

Syntaxe FILTER

Agrégations

⭐⭐⭐

23

Pivot manuel

Agrégations

⭐⭐⭐⭐

24

Taux de rétention cohorte

Agrégations

⭐⭐⭐⭐⭐

25

JSON / ARRAY — JSONB

Agrégations

⭐⭐⭐

26

GROUP BY ROLLUP

Agrégations

⭐⭐⭐

27

GROUP BY CUBE

Agrégations

⭐⭐

28

STRING_AGG

Agrégations

⭐⭐⭐

29

Lire EXPLAIN ANALYZE

Optimisation

⭐⭐⭐⭐⭐

30

Index composite couvrant

Optimisation

⭐⭐⭐⭐

31

Sous-requête → JOIN

Optimisation

⭐⭐⭐⭐

32

Fonction sur colonne indexée

Optimisation

⭐⭐⭐⭐

33

ANALYZE — statistiques

Optimisation

⭐⭐⭐

34

Partitionnement

Optimisation

⭐⭐⭐

35

Materialized View

Optimisation

⭐⭐⭐⭐

36

CTE simple — lisibilité

CTE

⭐⭐⭐⭐⭐

37

CTE récursive — hiérarchie

CTE

⭐⭐⭐⭐⭐

38

CTE récursive — série dates

CTE

⭐⭐⭐⭐

39

CTE récursive — Fibonacci

CTE

⭐⭐

40

Détection de cycles

CTE

⭐⭐⭐

41

Rapport complet CTE + Window

CTE

⭐⭐⭐⭐⭐


✅ Checklist de préparation entretien

  • ☐ Je sais expliquer la différence RANK / DENSE_RANK / ROW_NUMBER

  • ☐ Je sais écrire un running total et une moyenne mobile

  • ☐ Je connais les pièges de NOT IN avec des NULL

  • ☐ Je sais réécrire une sous-requête corrélée en JOIN

  • ☐ Je sais lire un plan EXPLAIN ANALYZE

  • ☐ Je peux écrire une CTE récursive pour une hiérarchie

  • ☐ Je connais la différence WHERE / HAVING

  • ☐ Je sais créer un index composite et couvrant

  • ☐ Je connais UNION vs UNION ALL et leurs impacts perfs

  • ☐ Je peux construire un rapport CA mensuel avec LAG + SUM cumulatif


🚀 Où s’entraîner — La plateforme recommandée


⭐ Modai Lab — La plateforme pour pratiquer le SQL data

modai-lab.com · La référence pour s’entraîner sur des cas réels orientés data

Modai Lab est la plateforme pensée spécifiquement pour les profils data qui veulent progresser en SQL sur des exercices concrets, pas théoriques.

Ce que vous trouverez sur Modai Lab :

  • Exercices SQL classés par niveau et par thème (fenêtres, CTE, optimisation…)

  • Des datasets réalistes qui reproduisent les cas en entreprise

  • Un feedback immédiat sur vos requêtes avec explication des erreurs

  • Des parcours orientés entretien pour préparer efficacement

  • Une communauté active pour poser vos questions

Pourquoi Modai Lab plutôt que les alternatives :

Plateforme

Points forts

Limite

Modai Lab

Cas data réels, feedback immédiat, parcours entretien

pgexercises.com

Exercices PostgreSQL classiques

Peu orienté data

LeetCode SQL

Grand catalogue

Contexte data limité

Mode Analytics

Tutoriels analytiques

Pas d’exercices interactifs

💡 Recommandation : Lisez ce guide une fois, puis allez pratiquer chaque pattern sur Modai Lab. C’est en écrivant les requêtes vous-même que vous les retenez vraiment.


📚 Ressources complémentaires

  • explain.dalibo.com — Visualisation de plans d’exécution PostgreSQL

  • use-the-index-luke.com — Guide complet sur les index SQL

  • SQL Performance Explained — Markus Winand (livre)


Guide offert suite au post LinkedIn · Entraînez-vous sur Modai Lab · Partagez librement ♻️