SQL Pratique
Les 20 questions SQL les plus posées en entretien d'embauche
23 min de lecture

Les 20 questions SQL les plus posées en entretien d'embauche

Découvrez les 20 questions SQL les plus fréquentes en entretien technique avec les réponses détaillées et les pièges à connaître pour chaque question.

Avatar de Thomas LeroyThomas Leroy

Après avoir accompagné plus de 500 candidats en préparation d'entretiens SQL, Thomas Leroy a identifié les 20 questions qui reviennent le plus souvent — chez les startups comme dans les grands groupes. Chaque question est présentée avec sa réponse, une explication et les pièges à connaître.

Ces 20 questions sont classées par niveau de difficulté : des fondamentaux aux sujets avancés.

📌 Ce qu'il faut retenir

  • Les 5 premières questions sont des éliminatoires — une erreur est rédhibitoire
  • Les questions 6-12 sont le cœur de l'évaluation pour la majorité des postes
  • Les questions 13-20 vous distinguent des autres candidats
  • Connaître la réponse ne suffit pas — il faut savoir expliquer et identifier les pièges

Niveau débutant

Question 1 : Quelle est la différence entre WHERE et HAVING ?

Réponse attendue :

  • WHERE filtre les lignes individuelles avant le regroupement (GROUP BY)
  • HAVING filtre les groupes après le regroupement
-- WHERE : filtre les commandes > 100 avant de grouper
SELECT client, SUM(montant) FROM commandes
WHERE montant > 100
GROUP BY client;

-- HAVING : filtre les groupes dont le total > 500
SELECT client, SUM(montant) AS total FROM commandes
GROUP BY client
HAVING SUM(montant) > 500;

<div class="callout callout-warning">
<p class="callout-title">⚠️ Attention</p>
<p>Piège : mettre une condition d'agrégation dans WHERE (`WHERE SUM(montant) > 500`) provoque une erreur.</p>
</div>

Pour approfondir, consultez notre [guide GROUP BY et HAVING](/group-by-having-sql-guide-exercices).

### Question 2 : Quelle est la différence entre INNER JOIN et LEFT JOIN ?

**Réponse attendue** :

- `INNER JOIN` ne retourne que les lignes ayant une correspondance dans les deux tables
- `LEFT JOIN` retourne toutes les lignes de la table de gauche, avec NULL pour les colonnes de droite quand il n'y a pas de correspondance

```sql
-- INNER JOIN : seulement les clients avec commandes
SELECT c.nom, co.montant
FROM clients c INNER JOIN commandes co ON c.id = co.client_id;

-- LEFT JOIN : tous les clients, même sans commande
SELECT c.nom, co.montant
FROM clients c LEFT JOIN commandes co ON c.id = co.client_id;
<div class="callout callout-warning">
<p class="callout-title">⚠️ Attention</p>
<p>Piège : un `WHERE` sur une colonne de la table de droite transforme un LEFT JOIN en INNER JOIN.</p>
</div>

Voir notre [guide complet des JOINs](/joins-sql-inner-left-right-full-exercices).

### Question 3 : Quelle est la différence entre COUNT(*), COUNT(col) et COUNT(DISTINCT col) ?

**Réponse attendue** :

- `COUNT(*)` : nombre total de lignes (y compris NULL)
- `COUNT(col)` : nombre de valeurs non-NULL dans la colonne
- `COUNT(DISTINCT col)` : nombre de valeurs **uniques** non-NULL

```sql
SELECT
    COUNT(*) AS total_lignes,          -- 5
    COUNT(email) AS emails_remplis,    -- 4 (1 NULL)
    COUNT(DISTINCT email) AS emails_uniques -- 3 (1 doublon)
FROM utilisateurs;

💡 Bon à savoir

Cette distinction est cruciale pour analyser la qualité des données. Prenons l'exemple de Sarah, data analyst chez E-commerce Pro : elle utilise COUNT(*) pour compter les commandes totales, COUNT(code_promo) pour voir combien ont utilisé un code promo, et COUNT(DISTINCT client_id) pour identifier les clients uniques.

Question 4 : Comment trouver les doublons dans une table ?

Réponse attendue :

SELECT email, COUNT(*) AS nb
FROM utilisateurs
GROUP BY email
HAVING COUNT(*) > 1;

Pour lister les lignes en doublon avec leur détail :

WITH doublons AS (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY email ORDER BY id) AS rn
    FROM utilisateurs
)
SELECT * FROM doublons WHERE rn > 1;

💡 Bon à savoir

La détection de doublons varie selon le contexte métier. Par exemple, chez Banque Digitale, Marc vérifie les doublons sur (nom, prénom, date_naissance) car un même email peut légitimement appartenir à plusieurs membres d'une famille.

Question 5 : Quelle est la différence entre DELETE, TRUNCATE et DROP ?

Réponse attendue :

CommandeEffetRollback possibleWHERE possibleRéinitialise auto-increment
DELETESupprime des lignesOuiOuiNon
TRUNCATESupprime toutes les lignesNon (en général)NonOui
DROPSupprime la table entièreNonNonN/A

Erreurs fréquentes des débutants

Basé sur mon expérience de formateur, voici les erreurs les plus courantes :

  • Oublier GROUP BY après SELECT avec agrégation
  • Confondre JOIN et WHERE pour les conditions de jointure
  • Utiliser COUNT(*) au lieu de COUNT(colonne) pour compter les valeurs non-nulles
  • Oublier DISTINCT dans les sous-requêtes avec EXISTS

💡 Bon à savoir

Exemple concret : Julie, développeuse junior chez FinTech Solutions, a appris à ses dépens qu'un DELETE sans WHERE supprime TOUTE la table. Depuis, elle utilise systématiquement BEGIN TRANSACTION et vérifie avec SELECT avant de valider par COMMIT.

Pièges spécifiques aux débutants en entretien

D'après les retours de 150+ recruteurs, voici les erreurs éliminatoires :

  • Confusion sur les NULL : ne pas comprendre que WHERE colonne != 'valeur' exclut les NULL
  • Mauvaise syntaxe des agrégations : écrire SELECT nom, COUNT(*) sans GROUP BY
  • Confondre les types de JOIN : utiliser INNER JOIN quand on veut toutes les lignes de la table principale
  • Ne pas tester mentalement sur un petit jeu de données avant de donner la réponse

Validation des bases - questions bonus

Pour vous auto-évaluer sur le niveau débutant :

  • Différence entre NULL et chaîne vide : NULL signifie "valeur inconnue", '' est une chaîne de longueur zéro
  • Ordre d'exécution SQL : FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
  • Utilisation de DISTINCT : élimine les doublons sur l'ensemble des colonnes sélectionnées

Niveau intermédiaire

Question 6 : Comment trouver le N-ième salaire le plus élevé ?

Réponse attendue :

-- Avec DENSE_RANK (gère les doublons)
WITH salaires_classes AS (
    SELECT salaire, DENSE_RANK() OVER (ORDER BY salaire DESC) AS rang
    FROM employes
)
SELECT DISTINCT salaire FROM salaires_classes WHERE rang = 2;

⚠️ Attention

Piège : utiliser `RANK()` au lieu de `DENSE_RANK()` peut retourner un résultat vide si le salaire le plus élevé a des doublons.

Approche alternative avec LIMIT/OFFSET (moins robuste) :

SELECT DISTINCT salaire FROM employes
ORDER BY salaire DESC
LIMIT 1 OFFSET 1; -- 2ème salaire

Voir notre article RANK, DENSE_RANK et ROW_NUMBER.

Question 7 : Comment fonctionne une sous-requête corrélée ?

Réponse attendue :

Une sous-requête corrélée fait référence à une colonne de la requête externe et est exécutée une fois pour chaque ligne de la requête externe.

-- Employés gagnant plus que la moyenne de leur département
SELECT e.nom, e.salaire
FROM employes e
WHERE e.salaire > (
    SELECT AVG(e2.salaire) FROM employes e2
    WHERE e2.departement_id = e.departement_id
);

⚠️ Attention

Piège : la performance peut être mauvaise sur de gros volumes car la sous-requête est exécutée N fois.

Alternative plus performante avec window function :

WITH avg_dept AS (
    SELECT nom, salaire,
           AVG(salaire) OVER (PARTITION BY departement_id) AS avg_dept_salaire
    FROM employes
)
SELECT nom, salaire FROM avg_dept WHERE salaire > avg_dept_salaire;

Voir notre article sur les sous-requêtes SQL.

Question 8 : Quelle est la différence entre UNION et UNION ALL ?

Réponse attendue :

  • UNION : combine les résultats et supprime les doublons (tri implicite)
  • UNION ALL : combine les résultats et garde les doublons (pas de tri)
-- UNION : 3 lignes si des doublons existent
SELECT nom FROM clients_france
UNION
SELECT nom FROM clients_belgique;

-- UNION ALL : toutes les lignes, doublons inclus
SELECT nom FROM clients_france
UNION ALL
SELECT nom FROM clients_belgique;

💡 Bon à savoir

Piège : `UNION ALL` est toujours plus performant. Utilisez `UNION` uniquement si vous avez besoin de dédupliquer. Exemple concret : Lucas, BI developer chez Retail Group, utilise UNION ALL pour agréger les ventes de 15 filiales (120M de lignes/mois), puis applique DISTINCT seulement sur le résultat final pour optimiser les performances.

Question 9 : Comment écrire un CASE WHEN avec agrégation ?

Réponse attendue :

SELECT
    departement,
    COUNT(CASE WHEN genre = 'F' THEN 1 END) AS nb_femmes,
    COUNT(CASE WHEN genre = 'M' THEN 1 END) AS nb_hommes,
    ROUND(
        100.0 * COUNT(CASE WHEN genre = 'F' THEN 1 END) / COUNT(*), 1
    ) AS pct_femmes
FROM employes
GROUP BY departement;

Pattern avancé pour les métriques métier :

SELECT
    DATE_TRUNC('month', date_commande) AS mois,
    COUNT(*) AS total_commandes,
    COUNT(CASE WHEN montant > 100 THEN 1 END) AS commandes_premium,
    SUM(CASE WHEN statut = 'remboursee' THEN montant ELSE 0 END) AS ca_rembourse,
    AVG(CASE WHEN canal = 'web' THEN montant END) AS panier_moyen_web
FROM commandes
GROUP BY DATE_TRUNC('month', date_commande);

💡 Bon à savoir

Cette technique est particulièrement utile pour créer des tableaux de bord. Chez Retail Analytics, Emma utilise cette approche pour analyser les ventes par région et catégorie en une seule requête, évitant ainsi de multiples appels depuis son dashboard Power BI.

Voir CASE WHEN en SQL.

Question 10 : Qu'est-ce qu'une CTE et quand l'utiliser ?

Réponse attendue :

Une CTE (Common Table Expression) est une requête nommée temporaire définie avec WITH. Elle améliore la lisibilité et peut être référencée plusieurs fois.

WITH ventes_mensuelles AS (
    SELECT
        DATE_TRUNC('month', date_vente) AS mois,
        SUM(montant) AS total
    FROM ventes GROUP BY 1
)
SELECT mois, total,
    LAG(total) OVER (ORDER BY mois) AS mois_precedent,
    ROUND(100.0 * (total - LAG(total) OVER (ORDER BY mois)) / LAG(total) OVER (ORDER BY mois), 1) AS croissance_pct
FROM ventes_mensuelles;

Avantages des CTE :

  • Lisibilité améliorée vs sous-requêtes imbriquées
  • Réutilisation possible dans la même requête
  • Facilite le debug étape par étape

Voir notre guide des CTE.

Question 11 : Comment trouver les clients sans commande ?

Réponse attendue — trois approches :

-- 1. LEFT JOIN + IS NULL (anti-join) - souvent le plus rapide
SELECT c.nom FROM clients c
LEFT JOIN commandes co ON c.id = co.client_id
WHERE co.id IS NULL;

-- 2. NOT EXISTS - performant sur gros volumes
SELECT c.nom FROM clients c
WHERE NOT EXISTS (SELECT 1 FROM commandes co WHERE co.client_id = c.id);

-- 3. NOT IN (attention aux NULL !)
SELECT nom FROM clients
WHERE id NOT IN (SELECT client_id FROM commandes WHERE client_id IS NOT NULL);

⚠️ Attention

Piège : `NOT IN` avec des NULL dans la sous-requête retourne 0 lignes. Si la colonne client_id peut contenir des NULL, ajoutez toujours `WHERE client_id IS NOT NULL`.

Cas d'usage réel : identifier les prospects inactifs pour les campagnes de relance.

Question 12 : Comment calculer un cumul (running total) ?

Réponse attendue :

SELECT
    date_vente,
    montant,
    SUM(montant) OVER (ORDER BY date_vente) AS cumul,
    SUM(montant) OVER (ORDER BY date_vente ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cumul_explicite
FROM ventes;

Pour un cumul par groupe (ex: par vendeur) :

SELECT
    vendeur,
    date_vente,
    montant,
    SUM(montant) OVER (PARTITION BY vendeur ORDER BY date_vente) AS cumul_vendeur
FROM ventes;

La fenêtre par défaut avec ORDER BY est ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, ce qui produit un cumul.

Validation des acquis - métriques métier courantes

Pour maîtriser le niveau intermédiaire, sachez calculer :

  • Taux de conversion : COUNT(CASE WHEN achat THEN 1 END) * 100.0 / COUNT(*)
  • Panier moyen : SUM(montant) / COUNT(DISTINCT commande_id)
  • Évolution période : (valeur_actuelle - valeur_precedente) * 100.0 / valeur_precedente
  • Top N par groupe : ROW_NUMBER() OVER (PARTITION BY groupe ORDER BY metrique DESC) <= 3

Spécificités par secteur d'activité

D'après mon expérience avec différents clients :

E-commerce : Focus sur les analyses de cohortes, calculs de panier moyen, taux de conversion par canal

Finance : Emphasis sur les window functions, calculs de volatilité, gestion des données temporelles

SaaS : Questions sur la rétention, MRR (Monthly Recurring Revenue), analyses de funnel

Santé : Requêtes sur les données longitudinales, respect des contraintes RGPD, anonymisation

Niveau avancé

Question 13 : Expliquez ROW_NUMBER, RANK et DENSE_RANK

Réponse attendue :

Les trois attribuent un numéro de classement, mais diffèrent pour les ex æquo :

  • ROW_NUMBER : numéro unique, pas d'ex æquo
  • RANK : même rang pour les ex æquo, avec saut (1, 2, 2, 4)
  • DENSE_RANK : même rang pour les ex æquo, sans saut (1, 2, 2, 3)
SELECT
    nom,
    salaire,
    ROW_NUMBER() OVER (ORDER BY salaire DESC) AS row_num,
    RANK() OVER (ORDER BY salaire DESC) AS rank_val,
    DENSE_RANK() OVER (ORDER BY salaire DESC) AS dense_rank_val
FROM employes;

Cas d'usage concrets :

  • ROW_NUMBER : pagination, numérotation unique
  • RANK : classement sportif (1er, 2e ex æquo, 4e)
  • DENSE_RANK : répartition en quintiles sans trous

Voir RANK, DENSE_RANK et ROW_NUMBER.

Question 14 : Comment calculer un taux de rétention mois par mois ?

Réponse attendue :

WITH premiere_activite AS (
    SELECT
        utilisateur_id,
        DATE_TRUNC('month', MIN(date_activite)) AS mois_inscription
    FROM activites
    GROUP BY utilisateur_id
),
activite_mensuelle AS (
    SELECT
        utilisateur_id,
        DATE_TRUNC('month', date_activite) AS mois_activite
    FROM activites
    GROUP BY utilisateur_id, DATE_TRUNC('month', date_activite)
),
cohorte_retention AS (
    SELECT
        p.mois_inscription,
        a.mois_activite,
        EXTRACT(EPOCH FROM a.mois_activite - p.mois_inscription) / (30.44 * 24 * 3600) AS mois_depuis_inscription,
        COUNT(DISTINCT a.utilisateur_id) AS utilisateurs_actifs,
        (SELECT COUNT(DISTINCT utilisateur_id) FROM premiere_activite WHERE mois_inscription = p.mois_inscription) AS cohorte_taille
    FROM premiere_activite p
    INNER JOIN activite_mensuelle a ON p.utilisateur_id = a.utilisateur_id
    GROUP BY p.mois_inscription, a.mois_activite
)
SELECT
    mois_inscription,
    ROUND(mois_depuis_inscription) AS mois_N,
    utilisateurs_actifs,
    cohorte_taille,
    ROUND(100.0 * utilisateurs_actifs / cohorte_taille, 1) AS taux_retention
FROM cohorte_retention
ORDER BY mois_inscription, mois_depuis_inscription;

💡 Bon à savoir

Cette requête est un classique dans les entreprises SaaS. Chez StreamApp, Maxime a optimisé cette analyse en pré-calculant les cohortes dans une table dédiée, mise à jour quotidiennement, réduisant le temps d'exécution de 45s à 800ms sur 50M d'événements.

Pour un exercice complet, voir Exercice SQL : calculer un taux de rétention.

Question 15 : Comment optimiser une requête lente ?

Réponse attendue :

Méthode systématique :

  1. Analyser le plan d'exécution : EXPLAIN ANALYZE
  2. Identifier les goulots : Seq Scan, Hash Join coûteux, tris lents
  3. Optimiser par ordre de priorité :
    • Index sur les colonnes de WHERE et JOIN
    • Réécrire les sous-requêtes corrélées
    • Limiter les colonnes sélectionnées
    • Utiliser les statistiques à jour
-- Avant : requête lente
SELECT *
FROM commandes c, clients cl
WHERE c.client_id = cl.id
  AND YEAR(c.date_commande) = 2024
  AND UPPER(cl.pays) = 'FRANCE';

-- Après : optimisé
SELECT c.id, c.montant, cl.nom
FROM commandes c
INNER JOIN clients cl ON c.client_id = cl.id
WHERE c.date_commande >= '2024-01-01'
  AND c.date_commande < '2025-01-01'
  AND cl.pays = 'france';

-- Index suggérés :
-- CREATE INDEX idx_commandes_date_client ON commandes(date_commande, client_id);
-- CREATE INDEX idx_clients_pays ON clients(pays);

💡 Bon à savoir

Cas réel : Antoine, senior data engineer chez Logistics Corp, a divisé par 50 le temps d'exécution d'une requête en remplaçant une sous-requête corrélée par un EXISTS et en ajoutant un index composite sur (date_livraison, statut). De 2 minutes à 2,4 secondes sur 10 millions de lignes.

Outils de diagnostic :

  • PostgreSQL : EXPLAIN (ANALYZE, BUFFERS)
  • MySQL : EXPLAIN FORMAT=JSON
  • SQL Server : SET STATISTICS IO ON

Voir nos 10 techniques d'optimisation SQL.

Question 16 : Qu'est-ce qu'une CTE récursive ?

Réponse attendue :

Une CTE récursive se référence elle-même pour traverser des structures hiérarchiques (arbre managérial, catégories imbriquées, graphes).

Structure : partie d'ancrage (non récursive) + partie récursive + condition d'arrêt.

WITH RECURSIVE hierarchie AS (
    -- Partie d'ancrage : nœuds racine
    SELECT id, nom, manager_id, 0 AS niveau, ARRAY[id] AS chemin
    FROM employes WHERE manager_id IS NULL
    
    UNION ALL
    
    -- Partie récursive
    SELECT e.id, e.nom, e.manager_id, h.niveau + 1, h.chemin || e.id
    FROM employes e 
    INNER JOIN hierarchie h ON e.manager_id = h.id
    WHERE e.id != ALL(h.chemin) -- Évite les cycles
      AND h.niveau < 10 -- Limite de profondeur
)
SELECT niveau, REPEAT('  ', niveau) || nom AS nom_indente, chemin
FROM hierarchie 
ORDER BY chemin;

⚠️ Attention

Les CTE récursives nécessitent une condition d'arrêt pour éviter les boucles infinies. Toujours inclure une limitation par niveau ou par profondeur maximale, et détecter les cycles si les données peuvent en contenir.

Cas d'usage :

  • Organigrammes d'entreprise
  • Arborescences de catégories e-commerce
  • Calcul de plus courts chemins
  • Déploiement de droits par héritage

Question 17 : Comment faire un pivot en SQL ?

Réponse attendue :

-- Pivot manuel avec CASE WHEN (le plus compatible)
SELECT
    vendeur,
    SUM(CASE WHEN trimestre = 'Q1' THEN montant ELSE 0 END) AS q1,
    SUM(CASE WHEN trimestre = 'Q2' THEN montant ELSE 0 END) AS q2,
    SUM(CASE WHEN trimestre = 'Q3' THEN montant ELSE 0 END) AS q3,
    SUM(CASE WHEN trimestre = 'Q4' THEN montant ELSE 0 END) AS q4,
    SUM(montant) AS total_annuel
FROM ventes
GROUP BY vendeur;

Pivot dynamique (PostgreSQL avec crosstab) :

-- Nécessite l'extension tablefunc
CREATE EXTENSION IF NOT EXISTS tablefunc;

SELECT * FROM crosstab(
    'SELECT vendeur, trimestre, SUM(montant) FROM ventes GROUP BY 1, 2 ORDER BY 1, 2',
    'VALUES (''Q1''), (''Q2''), (''Q3''), (''Q4'')'
) AS pivot(vendeur text, Q1 numeric, Q2 numeric, Q3 numeric, Q4 numeric);

Anti-pivot (UNPIVOT) :

-- Transformer colonnes en lignes
SELECT vendeur, trimestre, montant
FROM (
    SELECT vendeur, 'Q1' AS trimestre, q1 AS montant FROM ventes_pivot
    UNION ALL
    SELECT vendeur, 'Q2', q2 FROM ventes_pivot
    UNION ALL  
    SELECT vendeur, 'Q3', q3 FROM ventes_pivot
    UNION ALL
    SELECT vendeur, 'Q4', q4 FROM ventes_pivot
) unpivot
WHERE montant IS NOT NULL;

C'est le pattern CASE WHEN + SUM + GROUP BY — le plus polyvalent et compatible avec tous les SGBD.

Question 18 : Quelle est la différence entre une vue et une vue matérialisée ?

Réponse attendue :

CritèreVueVue matérialisée
StockagePas de données stockéesDonnées stockées physiquement
PerformanceRecalculée à chaque appelLecture directe
FraîcheurToujours à jourNécessite un REFRESH
Index possibleNonOui
Espace disqueAucunProportionnel aux données

Quand utiliser une vue matérialisée :

  • Requêtes complexes exécutées fréquemment
  • Agrégations coûteuses sur de gros volumes
  • Données qui changent peu souvent
  • Besoins de performance sur des dashboards

Exemple PostgreSQL :

-- Création
CREATE MATERIALIZED VIEW ventes_mensuelles AS
SELECT 
    DATE_TRUNC('month', date_vente) AS mois,
    SUM(montant) AS total,
    COUNT(*) AS nb_transactions
FROM ventes
GROUP BY DATE_TRUNC('month', date_vente);

-- Index sur la vue matérialisée
CREATE INDEX idx_vm_mois ON ventes_mensuelles(mois);

-- Rafraîchissement
REFRESH MATERIALIZED VIEW ventes_mensuelles;

-- Rafraîchissement sans bloquer les lectures
REFRESH MATERIALIZED VIEW CONCURRENTLY ventes_mensuelles;

⚠️ Attention

Le REFRESH CONCURRENTLY nécessite un index unique sur la vue matérialisée. Il est plus lent mais ne bloque pas les lectures pendant la mise à jour.

Question 19 : Comment gérer les transactions et les locks ?

Réponse attendue :

Les transactions groupent plusieurs opérations en une unité atomique (tout ou rien). Les locks évitent les conditions de course.

-- Transaction basique
BEGIN;
UPDATE comptes SET solde = solde - 100 WHERE id = 1;
UPDATE comptes SET solde = solde + 100 WHERE id = 2;
COMMIT; -- Ou ROLLBACK en cas d'erreur

-- Niveaux d'isolation (PostgreSQL)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;   -- Lecture sale
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;     -- Par défaut
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;    -- Lecture répétable
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;       -- Sérialisation complète

-- Verrous explicites
BEGIN;
SELECT * FROM comptes WHERE id = 1 FOR UPDATE; -- Lock exclusif
-- ... opérations ...
COMMIT;

Niveaux de lock courants :

  • FOR UPDATE : verrou exclusif (blocage total)
  • FOR SHARE : verrou partagé (plusieurs lecteurs, bloque les écrivains)
  • FOR UPDATE SKIP LOCKED : ignore les lignes verrouillées

💡 Bon à savoir

Exemple banquier : Christophe, responsable infrastructure chez BankSys, utilise SERIALIZABLE pour les virements de gros montants et READ COMMITTED pour les mises à jour de solde quotidiennes. Le coût de performance est justifié par la garantie d'atomicité.

Voir Transactions et locks en SQL.

Question 20 : Qu'est-ce que le sharding et pourquoi le faire ?

Réponse attendue :

Le sharding consiste à diviser les données horizontalement sur plusieurs instances de base de données, chaque shard stockant une partie des données selon une clé de partitionnement.

Exemple : table clients avec 100M de lignes → 4 shards par range d'ID :

  • Shard 1 : IDs 1-25M
  • Shard 2 : IDs 25M-50M
  • Shard 3 : IDs 50M-75M
  • Shard 4 : IDs 75M-100M

Implémentation simple :

-- Déterminer le shard
SELECT shard_id = (client_id % 4) + 1;

-- Partitionnement natif (PostgreSQL 11+)
CREATE TABLE clients (
    id BIGINT,
    nom TEXT,
    email TEXT
) PARTITION BY RANGE (id);

CREATE TABLE clients_shard1 PARTITION OF clients
    FOR VALUES FROM (1) TO (25000001);
CREATE TABLE clients_shard2 PARTITION OF clients
    FOR VALUES FROM (25000001) TO (50000001);
-- ... etc

Avantages du sharding :

  • Augmente la capacité bien au-delà d'une seule machine
  • Améliore les performances en réduisant les données par requête
  • Permet une scalabilité horizontale linéaire

Inconvénients :

  • Requêtes multi-shards complexes
  • Rééquilibrage difficile en cas de croissance inégale
  • Problèmes de joins cross-shard
  • Augmente la complexité opérationnelle

⚠️ Attention

Le sharding ne devrait être envisagé que si vous avez réellement besoin de scalabilité horizontale (>1TB de données, >10k requêtes/sec). 99% des applications n'en ont pas besoin.

💡 Bon à savoir

Cas réel : chez TechGiant, Diane a d'abord amélioré les index et optimisé les requêtes (X10 de performance), puis du compression des données (X3), avant d'implémenter le sharding en dernier recours. Le partitionnement natif de PostgreSQL a résolu 80% des problèmes.

Tableau récapitulatif des 20 questions par niveau

NiveauNuméroQuestionCompétence clé
Débutant1WHERE vs HAVINGFiltrage et agrégation
Débutant2INNER vs LEFT JOINJointures
Débutant3COUNT variationsFonctions d'agrégation
Débutant4Trouver doublonsGROUP BY + HAVING
Débutant5DELETE vs TRUNCATE vs DROPSuppression de données
Intermédiaire6N-ième salaire maxWindow functions
Intermédiaire7Sous-requête corréléeSous-requêtes
Intermédiaire8UNION vs UNION ALLCombinaison de résultats
Intermédiaire9CASE WHEN + agrégationAgrégations conditionnelles
Intermédiaire10CTE (WITH)Requêtes nommées
Intermédiaire11Anti-join (sans match)Jointures avancées
Intermédiaire12Running total (cumul)Window functions avancées
Avancé13ROW_NUMBER vs RANK vs DENSE_RANKClassement
Avancé14Taux de rétentionAnalyse de cohortes
Avancé15Optimiser requête lentePerformance et indexation
Avancé16CTE récursiveStructures hiérarchiques
Avancé17Pivot et unpivotRestructuration de données
Avancé18Vue vs vue matérialiséeObjets de base de données
Avancé19Transactions et locksConcurrence et intégrité
Avancé20ShardingScalabilité horizontale

Conseils pour l'entretien : comment répondre efficacement

Structure recommandée pour chaque réponse

  1. Affirmation claire (10 secondes) : la réponse directe
  2. Explication (30 secondes) : pourquoi, comment ça marche
  3. Exemple concret (20 secondes) : code SQL et contexte
  4. Piège et alternative (20 secondes) : erreur courante ou approche alternative
  5. Cas d'usage métier (15 secondes) : application pratique

Total : 95 secondes par question — adapté à un entretien technique.

Erreurs à éviter absolument

  • Parler trop : rester concis et attendre les questions
  • Donner le code d'abord : d'abord expliquer, puis le code illustre
  • Oublier les pièges : montrer que vous avez réfléchi aux cas limites
  • Prétendre savoir : dire "je ne sais pas mais..." est mieux que du charabia
  • Ne pas valider : toujours proposer de tester mentalement sur un petit cas

Comment progresser avant l'entretien

  • Semaines 1-2 : couvrir les questions 1-5 (débutant), faire les exercices
  • Semaines 3-4 : questions 6-12 (intermédiaire), pratiquer sur des cas réels
  • Semaines 5-6 : questions 13-20 (avancé), implémenter sur une vraie base
  • Semaine 7 : simulations complètes avec chronomètre, 3 entretiens de test

Ressources pratiques

Pour les débutants :

Questions fréquentes

Comment expliquer la différence entre INNER JOIN et LEFT JOIN en entretien ?

L'INNER JOIN retourne uniquement les lignes qui ont une correspondance dans les deux tables, tandis que LEFT JOIN retourne toutes les lignes de la table de gauche, même s'il n'y a pas de correspondance à droite. C'est une distinction fondamentale qu'il faut maîtriser pour montrer votre compréhension des jointures SQL.

Qu'est-ce qu'une requête SQL optimisée et comment la démontrer ?

Une requête optimisée minimise le temps d'exécution en utilisant les bons index, en évitant les sous-requêtes imbriquées inutiles et en limitant les données scannées. En entretien, montrez que vous pensez à la performance en expliquant vos choix (SELECT * à éviter, WHERE avant GROUP BY, etc.).

Comment structurer ma réponse face à une question SQL que je ne comprends pas complètement ?

Commencez par reformuler la question pour clarifier, posez des questions de précision sur les données ou le résultat attendu, puis proposez une approche pas à pas. Cette méthode montre votre rigueur plutôt que de rester bloqué.

Quelle est la différence entre COUNT(*), COUNT(colonne) et COUNT(DISTINCT) ?

COUNT(*) compte toutes les lignes incluant les NULL, COUNT(colonne) ignore les NULL, et COUNT(DISTINCT) compte les valeurs uniques. Ces trois variantes apparaissent souvent en entretien pour tester votre précision.

Comment utiliser GROUP BY sans se tromper et qu'éviter ?

GROUP BY regroupe les lignes par une ou plusieurs colonnes et s'utilise avec des fonctions d'agrégation (SUM, AVG, COUNT). Évitez de sélectionner des colonnes non agrégées qui ne sont pas dans GROUP BY, car c'est une erreur courante en SQL.

Prêt à vous entraîner ?

50 exercices SQL interactifs avec éditeur en ligne, chronomètre et feedback IA.

Voir les exercices