Rendu 3D d'un cylindre empilé en dégradé de bleu, symbole d'une base de données, illustration d'un cours SQL débutant

SQL débutant : le cours pas à pas et 12 exercices corrigés

L’essentiel en une minute

Ce cours SQL débutant part d’une petite boutique en ligne : trois tables, une vingtaine de lignes, et un script à copier pour la créer chez vous. Chaque notion est illustrée par une requête et par le résultat exact qu’elle renvoie, puis 12 exercices corrigés vous font pratiquer. Toutes les requêtes ont été exécutées sur SQLite avant publication.

Il traite aussi la sécurité, les valeurs NULL et les écarts entre SQLite, PostgreSQL et MySQL.

Points clés

  • SQL est le deuxième langage de l’enquête Stack Overflow 2026, à 58,4 %, et PostgreSQL, SQLite et MySQL y sont les trois bases les plus utilisées.
  • Une requête de lecture s’écrit toujours dans cet ordre : SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT.
  • INNER JOIN ne garde que les lignes qui ont une correspondance, LEFT JOIN garde toutes les lignes de la table de gauche.
  • Un UPDATE ou un DELETE sans WHERE touche toute la table : travaillez dans une transaction.
  • Ne collez jamais une saisie utilisateur dans une requête : passez-la en paramètre.

Pourquoi apprendre SQL en 2026

SQL est le langage commun des bases de données relationnelles, et son usage reste stable. L’enquête annuelle de Stack Overflow a été menée du 23 juin au 5 août 2026 et retient 30 903 réponses valides (méthodologie de l’enquête 2026, consultée le 30 octobre 2026). SQL y est utilisé par 58,4 % des 14 099 répondants à la question sur les langages, au coude-à-coude avec HTML/CSS et Python, derrière JavaScript à 62,0 % (Stack Overflow Developer Survey 2026, consulté le 30 octobre 2026).

L’édition 2025 donnait 58,6 % pour SQL, sur un échantillon différent (Stack Overflow Developer Survey 2025, consulté le 30 octobre 2026) : un usage stable, pas une hausse.

Graphique en sucettes : SQL est le deuxième langage le plus utilisé dans l'enquête Stack Overflow 2026, à 58,4 %, au coude-à-coude avec HTML/CSS et Python et derrière JavaScript à 62,0 %, d'où l'intérêt d'un cours SQL débutant

SQL est déclaratif : vous décrivez le résultat voulu, et la base choisit comment l’obtenir. C’est ce qui le rend abordable pour un profil SQL débutant, même sans autre langage derrière.

Pour comparer les métiers, notre analyse des salaires des développeurs en France recoupe six études.


Préparer votre poste en cinq minutes

Le plus simple pour débuter est SQLite : la base tient dans un seul fichier, sans processus serveur, un choix assumé par sa documentation (SQLite, Quirks, consulté le 30 octobre 2026).

Deux options, au choix :

  • L’outil en ligne de commande sqlite3. Il se télécharge sur la page officielle de SQLite (paquet « sqlite-tools » pour Windows, macOS ou Linux). Lancez sqlite3 boutique.db, tapez PRAGMA foreign_keys = ON; pour activer les clés étrangères (à refaire à chaque ouverture), puis .read boutique.sql pour charger le script, et .mode box pour afficher des tableaux lisibles. Notre guide des commandes Linux de base aide à démarrer avec le terminal.
  • Python, déjà installé chez beaucoup. Le module sqlite3 fait partie de la bibliothèque standard. Le code ci-dessous crée la base à partir du script, clés étrangères activées.
import sqlite3

con = sqlite3.connect("boutique.db")
con.execute("PRAGMA foreign_keys = ON")
con.executescript(open("boutique.sql", encoding="utf-8").read())

Si vous découvrez Python, notre guide pour apprendre Python de zéro couvre l’installation.


La base d’exemple : clients, produits, commandes

La base contient trois tables : clients, produits et commandes, chaque commande reliant un client à un produit. Enregistrez ce script sous le nom boutique.sql. Il est écrit en SQL portable (les trois bases acceptent aussi DROP TABLE IF EXISTS, qui n’est pas dans la norme) et fonctionne tel quel sur SQLite ; les adaptations pour PostgreSQL et MySQL sont détaillées plus bas. Les trois DROP TABLE IF EXISTS du début effacent les tables existantes : relancer le script remet donc la base dans son état d’origine.

DROP TABLE IF EXISTS commandes;
DROP TABLE IF EXISTS produits;
DROP TABLE IF EXISTS clients;

CREATE TABLE clients (
  id               INTEGER PRIMARY KEY,
  nom              VARCHAR(100) NOT NULL,
  ville            VARCHAR(50),
  email            VARCHAR(255) UNIQUE,
  date_inscription DATE NOT NULL
);

CREATE TABLE produits (
  id        INTEGER PRIMARY KEY,
  nom       VARCHAR(100) NOT NULL,
  categorie VARCHAR(50) NOT NULL,
  prix      DECIMAL(10, 2) NOT NULL CHECK (prix >= 0)
);

CREATE TABLE commandes (
  id            INTEGER PRIMARY KEY,
  client_id     INTEGER NOT NULL,
  produit_id    INTEGER NOT NULL,
  quantite      INTEGER NOT NULL CHECK (quantite > 0),
  date_commande DATE NOT NULL,
  FOREIGN KEY (client_id)  REFERENCES clients (id),
  FOREIGN KEY (produit_id) REFERENCES produits (id)
);

INSERT INTO clients (id, nom, ville, email, date_inscription) VALUES
  (1, 'Alice Martin',  'Paris',     '[email protected]',  '2026-01-15'),
  (2, 'Karim Benali',  'Lyon',      '[email protected]',  '2026-02-03'),
  (3, 'Sophie Durand', 'Paris',     '[email protected]', '2026-03-20'),
  (4, 'Lucas Petit',   'Marseille', NULL,                '2026-04-11'),
  (5, 'Emma Leroy',    NULL,        '[email protected]',   '2026-05-02'),
  (6, 'Yanis Moreau',  'Lyon',      '[email protected]',  '2026-06-18');

INSERT INTO produits (id, nom, categorie, prix) VALUES
  (1, 'Clavier mécanique',  'Informatique', 89.00),
  (2, 'Souris sans fil',    'Informatique', 25.50),
  (3, 'Écran 27 pouces',    'Informatique', 249.00),
  (4, 'Cahier A5',          'Papeterie',    4.50),
  (5, 'Stylos gel (x10)',   'Papeterie',    12.75),
  (6, 'Lampe de bureau',    'Mobilier',     39.00),
  (7, 'Chaise ergonomique', 'Mobilier',     199.00);

INSERT INTO commandes (id, client_id, produit_id, quantite, date_commande) VALUES
  (1,  1, 1, 1,  '2026-07-02'),
  (2,  1, 2, 2,  '2026-07-02'),
  (3,  2, 3, 1,  '2026-07-15'),
  (4,  3, 4, 10, '2026-08-01'),
  (5,  3, 5, 2,  '2026-08-01'),
  (6,  2, 2, 1,  '2026-08-20'),
  (7,  4, 6, 1,  '2026-09-05'),
  (8,  1, 4, 5,  '2026-09-12'),
  (9,  5, 1, 1,  '2026-09-30'),
  (10, 3, 3, 1,  '2026-10-03');

Deux pièges sont posés exprès : Lucas n’a pas d’e-mail et Emma pas de ville. Ils serviront dans la partie sur NULL. Yanis n’a rien commandé et la chaise n’a jamais été vendue : les jointures les feront apparaître.


Lire des données : SELECT, WHERE, ORDER BY, LIMIT

Une requête de lecture dit quelles colonnes afficher (SELECT), depuis quelle table (FROM), pour quelles lignes (WHERE), dans quel ordre (ORDER BY) et combien (LIMIT). L’astérisque sélectionne toutes les colonnes ; en pratique, nommez celles dont vous avez besoin.

SELECT nom, email FROM clients WHERE ville = 'Paris' ORDER BY nom;
nom email
Alice Martin [email protected]
Sophie Durand [email protected]

Les chaînes s’écrivent entre apostrophes simples. Les guillemets doubles servent aux noms de colonnes dans la norme ; SQLite les accepte pour des chaînes, mais sa documentation le classe parmi ses bizarreries, car une faute de frappe dans un nom de colonne devient alors une chaîne au lieu d’une erreur (SQLite, Quirks).

WHERE combine les conditions avec AND, OR et NOT, et dispose de raccourcis utiles :

Opérateur Exemple Ce qu’il garde
=, <>, <, >= prix < 100 Comparaison simple
IN ville IN ('Lyon', 'Marseille') Une valeur parmi une liste
BETWEEN date_commande BETWEEN '2026-08-01' AND '2026-08-31' Un intervalle, bornes comprises
LIKE nom LIKE 'S%' Un motif : % pour n’importe quelle suite, _ pour un caractère
IS NULL email IS NULL Une valeur absente

Pour trier et limiter, ajoutez ORDER BY puis LIMIT. Voici les trois produits les plus chers :

SELECT nom, prix FROM produits
ORDER BY prix DESC
LIMIT 3;
nom prix
Écran 27 pouces 249
Chaise ergonomique 199
Clavier mécanique 89

SQLite affiche 249 et non 249.00 : il ne stocke pas de type décimal fixe, nous y revenons plus bas. Sans ORDER BY, l’ordre des lignes n’est pas garanti. La documentation de PostgreSQL insiste : avec LIMIT, un tri qui donne un ordre unique est nécessaire, sinon vous obtenez un sous-ensemble imprévisible des lignes (PostgreSQL 18, LIMIT et OFFSET, consulté le 30 octobre 2026).

Enfin, SELECT DISTINCT ville FROM clients renvoie chaque ville une seule fois. Sur notre base, il renvoie quatre lignes : Lyon, Marseille, Paris, plus une ligne NULL pour Emma, car DISTINCT traite NULL comme une valeur à part entière.


Compter et regrouper : GROUP BY et HAVING

Les fonctions d’agrégat résument plusieurs lignes en une valeur : COUNT, SUM, AVG, MIN et MAX. Sans GROUP BY, elles portent sur toute la table.

SELECT COUNT(*) AS nb_produits,
       MIN(prix)  AS moins_cher,
       MAX(prix)  AS plus_cher,
       ROUND(AVG(prix), 2) AS prix_moyen
FROM produits;
nb_produits moins_cher plus_cher prix_moyen
7 4.5 249 88.39

GROUP BY calcule un agrégat par groupe. Ici, le nombre de produits et le prix moyen par catégorie :

SELECT categorie, COUNT(*) AS nb, ROUND(AVG(prix), 2) AS prix_moyen
FROM produits
GROUP BY categorie
ORDER BY nb DESC, categorie;
categorie nb prix_moyen
Informatique 3 121.17
Mobilier 2 119.0
Papeterie 2 8.63

La règle à retenir : toute colonne du SELECT qui n’est pas dans un agrégat doit figurer dans le GROUP BY. SQLite tolère qu’on l’oublie et prend alors une valeur d’une des lignes du groupe (SQLite, Quirks). PostgreSQL refuse ce genre de requête (PostgreSQL 18, SELECT), tout comme MySQL 8.4, dont le mode SQL par défaut inclut ONLY_FULL_GROUP_BY (MySQL 8.4, modes SQL). Prenez la bonne habitude dès le départ.

WHERE filtre les lignes avant le regroupement, HAVING filtre les groupes après. Les clients qui ont passé au moins trois commandes :

SELECT client_id, COUNT(*) AS nb_commandes
FROM commandes
GROUP BY client_id
HAVING COUNT(*) >= 3
ORDER BY client_id;
client_id nb_commandes
1 3
3 3

Croiser les tables : INNER JOIN et LEFT JOIN

Une jointure assemble les lignes de deux tables qui partagent une valeur, ici l’identifiant du client ou du produit. Sans elle, la table commandes ne montre que des numéros.

SELECT c.nom AS client, p.nom AS produit, co.quantite,
       co.quantite * p.prix AS montant
FROM commandes AS co
INNER JOIN clients  AS c ON c.id = co.client_id
INNER JOIN produits AS p ON p.id = co.produit_id
WHERE co.date_commande >= '2026-09-01'
ORDER BY co.date_commande;
client produit quantite montant
Lucas Petit Lampe de bureau 1 39
Alice Martin Cahier A5 5 22.5
Emma Leroy Clavier mécanique 1 89
Sophie Durand Écran 27 pouces 1 249

Les alias (co, c, p) lèvent l’ambiguïté entre les colonnes nom et id des trois tables. Les dates au format ISO AAAA-MM-JJ se comparent comme des chaînes.

INNER JOIN écarte les lignes sans correspondance : Yanis, qui n’a rien commandé, n’apparaît jamais. LEFT JOIN garde toutes les lignes de la table de gauche et remplit les colonnes manquantes avec NULL :

SELECT c.nom, COUNT(co.id) AS nb_commandes
FROM clients AS c
LEFT JOIN commandes AS co ON co.client_id = c.id
GROUP BY c.id, c.nom
ORDER BY nb_commandes DESC, c.nom;
nom nb_commandes
Alice Martin 3
Sophie Durand 3
Karim Benali 2
Emma Leroy 1
Lucas Petit 1
Yanis Moreau 0

Notez COUNT(co.id) : COUNT(*) compterait 1 pour Yanis, dont la ligne existe même sans commande. Pour ne garder que les clients sans commande, ajoutez WHERE co.id IS NULL : la requête renvoie alors Yanis Moreau seul.

RIGHT JOIN est le miroir de LEFT JOIN. La documentation de MySQL recommande LEFT JOIN pour que le code reste portable (MySQL 8.4, JOIN, consulté le 30 octobre 2026).


Modifier les données sans rien casser

INSERT ajoute des lignes, UPDATE les modifie et DELETE les supprime. Les deux derniers agissent sur toutes les lignes que sélectionne leur WHERE, et sur toute la table s’il n’y en a pas.

INSERT INTO clients (nom, ville, email, date_inscription)
VALUES ('Inès Garcia', 'Toulouse', '[email protected]', '2026-10-30');

UPDATE clients SET ville = 'Bordeaux' WHERE id = 7;

DELETE FROM clients WHERE id = 7;

Sans id fourni, SQLite a attribué le numéro 7 à la nouvelle cliente ; l’UPDATE puis le DELETE ont chacun touché une ligne.

Attention. Avant tout UPDATE ou DELETE, lancez un SELECT avec le même WHERE et vérifiez le nombre de lignes. Travaillez dans une transaction : si le résultat vous surprend, ROLLBACK annule tout. Faites une copie de la base avant une opération en production.

BEGIN;
UPDATE produits SET prix = 0;   -- oubli du WHERE : 7 lignes modifiées
ROLLBACK;                       -- les prix d'origine sont rétablis

Nous avons exécuté ce scénario : l’UPDATE a modifié les 7 produits, et le ROLLBACK a rendu les prix d’origine. COMMIT valide la transaction quand le résultat est correct.


Clés primaires, clés étrangères et index

La clé primaire identifie chaque ligne de façon unique : ici la colonne id. La clé étrangère garantit qu’une valeur pointe vers une ligne existante : commandes.client_id doit correspondre à un clients.id.

Sur notre base, avec les clés étrangères actives, deux opérations échouent avec l’erreur FOREIGN KEY constraint failed : supprimer Alice, qui a des commandes, et créer une commande pour un client 99 qui n’existe pas. De même, insérer un second client avec l’adresse d’Alice échoue sur la contrainte UNIQUE.

Le piège propre à SQLite : les clés étrangères sont désactivées par défaut, pour des raisons de compatibilité, et doivent être activées à chaque connexion avec PRAGMA foreign_keys = ON (SQLite, Foreign Key Support, consulté le 30 octobre 2026). Sans cette ligne, notre commande pour le client 99 a été acceptée sans erreur. Le module node:sqlite de Node.js fait l’inverse : son option enableForeignKeyConstraints vaut true par défaut (documentation de Node.js, consultée le 30 octobre 2026), ce dont profite notre tutoriel d’API REST avec Node.js.

PostgreSQL applique les clés étrangères d’office (PostgreSQL 18, contraintes). MySQL aussi avec InnoDB, son moteur par défaut, puisque la variable foreign_key_checks y est activée par défaut, à une condition : selon la documentation de MySQL 8.4, il analyse puis ignore les REFERENCES écrits dans la définition d’une colonne, et n’accepte que des clauses FOREIGN KEY (...) REFERENCES ... séparées (MySQL 8.4, CREATE TABLE et clés étrangères). Notre script utilise donc cette forme, valable sur les trois bases.

Un index accélère la recherche sur une colonne, comme l’index d’un livre. La commande EXPLAIN QUERY PLAN de SQLite montre la différence :

EXPLAIN QUERY PLAN SELECT * FROM commandes WHERE client_id = 3;
-- SCAN commandes          (lecture de toute la table)

CREATE INDEX idx_commandes_client ON commandes (client_id);

EXPLAIN QUERY PLAN SELECT * FROM commandes WHERE client_id = 3;
-- SEARCH commandes USING INDEX idx_commandes_client (client_id=?)

Un index a un coût : selon la documentation de PostgreSQL, la base doit le tenir à jour, ce qui alourdit les écritures (PostgreSQL 18, index, consulté le 30 octobre 2026). Indexez les colonnes souvent filtrées ou jointes, pas toutes.


Les pièges de NULL

NULL ne signifie pas zéro ni chaîne vide, mais « valeur inconnue ». Toute comparaison avec NULL donne NULL, donc ni vrai ni faux. La documentation de MySQL le dit en une phrase : on ne peut pas tester NULL avec =, < ou <> (MySQL 8.4, Working with NULL Values, consulté le 30 octobre 2026).

Cinq pièges, tous vérifiés sur notre base :

Requête Résultat obtenu Explication
WHERE ville = NULL 0 ligne La comparaison donne NULL ; il faut IS NULL
WHERE ville IS NULL Emma Leroy La bonne écriture
WHERE ville <> 'Paris' 3 lignes, sans Emma Une ville inconnue n’est pas « différente de Paris »
COUNT(*), COUNT(email) 6 et 5 COUNT(colonne) ignore les NULL
WHERE id NOT IN (SELECT NULL UNION SELECT 1) 0 ligne Un seul NULL dans la liste vide un NOT IN

Pour remplacer une valeur absente à l’affichage, utilisez COALESCE(ville, 'inconnue'), qui renvoie le premier argument non NULL. Pour inclure Emma dans la recherche, écrivez WHERE ville IS NULL OR ville <> 'Paris'.

Le tri des NULL varie selon la base. SQLite les considère plus petits que toute autre valeur, donc en tête d’un tri croissant (SQLite, SELECT) ; MySQL aussi (MySQL 8.4, Working with NULL Values). PostgreSQL les traite comme plus grands, donc en fin de tri croissant (PostgreSQL 18, tri). NULLS FIRST ou NULLS LAST fixent le comportement là où ils existent.


L’injection SQL et les requêtes paramétrées

L’injection SQL survient quand une saisie utilisateur est collée dans le texte d’une requête : l’attaquant écrit du SQL au lieu d’une valeur.

Ce programme Python a été exécuté sur notre base :

import sqlite3

con = sqlite3.connect("boutique.db")
saisie = "x' OR '1'='1"   # ce qu'un attaquant tape dans le formulaire

# À NE JAMAIS FAIRE : la saisie devient du code SQL
requete = "SELECT nom, email FROM clients WHERE email = '" + saisie + "'"
print(len(con.execute(requete).fetchall()))   # 6 : toute la table

# La bonne méthode : un paramètre lié par le pilote
lignes = con.execute(
    "SELECT nom, email FROM clients WHERE email = ?", (saisie,)
).fetchall()
print(len(lignes))                             # 0

La première version produit WHERE email = 'x' OR '1'='1', une condition toujours vraie : les six clients et leurs e-mails fuient. La seconde envoie la saisie comme une simple valeur, qui ne correspond à aucun e-mail.

L’OWASP place les requêtes préparées avec paramètres en tête des défenses, et classe l’échappement manuel des saisies comme « fortement déconseillé » (notre traduction) (OWASP, SQL Injection Prevention Cheat Sheet, consulté le 30 octobre 2026). La documentation de Python demande elle aussi d’utiliser toujours les marqueurs plutôt que des chaînes formatées (module sqlite3).

Le principe vaut dans tous les langages. Notre tutoriel pour créer une API REST avec Node.js l’applique à une base SQLite : toutes ses requêtes passent par db.prepare() avec des paramètres ?.


SQLite, PostgreSQL et MySQL : ce qui change

Les trois bases parlent SQL, mais pas exactement le même. PostgreSQL arrive en tête de l’enquête Stack Overflow 2026 avec 57,9 % des 12 061 répondants à la question sur les bases, devant SQLite à 42,1 % et MySQL à 35,1 % (Stack Overflow Developer Survey 2026).

Graphique en barres : bases de données les plus utilisées dans l'enquête Stack Overflow 2026, PostgreSQL à 57,9 %, SQLite à 42,1 %, MySQL à 35,1 %, devant Redis et Microsoft SQL Server

Les écarts qui comptent pour un débutant, relevés dans la documentation officielle de chaque base :

Point SQLite PostgreSQL MySQL 8.4
Identifiant automatique INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY AUTO_INCREMENT
Clés étrangères Désactivées par défaut Actives Actives avec InnoDB
Types Souples, sauf tables STRICT Stricts Stricts avec le mode par défaut
NULL dans un tri croissant En tête En fin En tête
FULL OUTER JOIN Depuis la version 3.39.0 Oui Absent de la syntaxe
\|\| Concatène Concatène « OU » logique par défaut (obsolète)

Les sources de ce tableau : les pages Quirks, AUTOINCREMENT et notes de version 3.39.0 de SQLite ; les pages colonnes d’identité, contraintes, conversion de types et SELECT de PostgreSQL ; les pages AUTO_INCREMENT, modes SQL et opérateurs logiques de MySQL, toutes consultées le 30 octobre 2026. La page de conversion de types de PostgreSQL rappelle que SQL est un langage fortement typé ; le mode SQL par défaut de MySQL 8.4 inclut STRICT_TRANS_TABLES.

Pour rejouer notre script sur PostgreSQL ou MySQL, le point délicat est l’identifiant. Comme les id y sont écrits à la main, c’est surtout l’INSERT sans id du chapitre suivant qui demande une adaptation.

Sur PostgreSQL, une colonne GENERATED ALWAYS AS IDENTITY refuse les id écrits à la main, sauf avec OVERRIDING SYSTEM VALUE. Avec GENERATED BY DEFAULT, la valeur fournie l’emporte, mais elle ne passe pas par la séquence, et la documentation prévient qu’une colonne d’identité ne garantit alors plus l’unicité. Le plus simple : déclarez les id en identité et retirez-les des trois INSERT (clients, produits et commandes) ; sur une table neuve, les lignes reçoivent en pratique les mêmes numéros. Sur MySQL, ajoutez AUTO_INCREMENT aux id : il accepte les valeurs écrites à la main et reprend ensuite la numérotation après la plus grande.

La souplesse de SQLite surprend : il a accepté le texte 'gratuit' dans la colonne prix, déclarée DECIMAL, ce que sa documentation assume. Elle note aussi que upper() et lower() ne traitent que les caractères ASCII : sur notre base, lower('Écran') renvoie Écran, la majuscule accentuée reste.

Enfin, LIMIT n’est pas standard : la norme SQL:2008 prévoit FETCH FIRST n ROWS ONLY, selon la page SELECT de PostgreSQL. Pour lancer PostgreSQL sans l’installer, notre tutoriel Docker pour débutants le démarre dans Docker Compose.


12 exercices SQL débutant corrigés

Ces 12 exercices SQL débutant portent sur la base d’exemple. Relancez le script boutique.sql avant de commencer, pour repartir des données d’origine, cherchez chaque requête sans regarder, puis comparez votre résultat au tableau. Toutes les solutions ont été exécutées dans l’ordre, sur la base d’origine.

Exercice 1 : les produits de papeterie

Affichez le nom et le prix des produits de la catégorie Papeterie.

SELECT nom, prix FROM produits WHERE categorie = 'Papeterie'
ORDER BY prix;
nom prix
Cahier A5 4.5
Stylos gel (x10) 12.75

Exercice 2 : les deux derniers inscrits

Affichez le nom et la date d’inscription des deux clients inscrits le plus récemment.

SELECT nom, date_inscription FROM clients
ORDER BY date_inscription DESC
LIMIT 2;
nom date_inscription
Yanis Moreau 2026-06-18
Emma Leroy 2026-05-02

Exercice 3 : une fourchette de prix

Listez les produits entre 20 et 100 euros, du moins cher au plus cher.

SELECT nom, prix FROM produits
WHERE prix BETWEEN 20 AND 100
ORDER BY prix;
nom prix
Souris sans fil 25.5
Lampe de bureau 39
Clavier mécanique 89

Exercice 4 : les clients sans e-mail

Trouvez les clients dont l’e-mail n’est pas renseigné.

SELECT nom FROM clients WHERE email IS NULL;
nom
Lucas Petit

Écrire email = NULL aurait renvoyé zéro ligne.

Exercice 5 : le bilan de juillet

Comptez les commandes passées en juillet 2026 et le nombre d’articles commandés.

SELECT COUNT(*) AS nb, SUM(quantite) AS articles
FROM commandes
WHERE date_commande BETWEEN '2026-07-01' AND '2026-07-31';
nb articles
3 4

Exercice 6 : les clients par ville

Comptez les clients par ville, en ignorant les villes inconnues, du plus grand nombre au plus petit.

SELECT ville, COUNT(*) AS nb_clients
FROM clients
WHERE ville IS NOT NULL
GROUP BY ville
ORDER BY nb_clients DESC, ville;
ville nb_clients
Lyon 2
Paris 2
Marseille 1

Exercice 7 : le total dépensé par client

Calculez le montant total dépensé par chaque client, du plus gros au plus petit.

SELECT c.nom, SUM(co.quantite * p.prix) AS total
FROM commandes AS co
JOIN clients  AS c ON c.id = co.client_id
JOIN produits AS p ON p.id = co.produit_id
GROUP BY c.id, c.nom
ORDER BY total DESC;
nom total
Sophie Durand 319.5
Karim Benali 274.5
Alice Martin 162.5
Emma Leroy 89
Lucas Petit 39

JOIN seul équivaut à INNER JOIN. Yanis n’apparaît pas, puisqu’il n’a rien commandé.

Exercice 8 : le produit jamais vendu

Trouvez les produits qui n’apparaissent dans aucune commande.

SELECT p.nom
FROM produits AS p
LEFT JOIN commandes AS co ON co.produit_id = p.id
WHERE co.id IS NULL;
nom
Chaise ergonomique

Exercice 9 : le chiffre d’affaires par catégorie

Calculez le chiffre d’affaires de chaque catégorie et ne gardez que celles qui dépassent 50 euros.

SELECT p.categorie, SUM(co.quantite * p.prix) AS chiffre_affaires
FROM commandes AS co
JOIN produits AS p ON p.id = co.produit_id
GROUP BY p.categorie
HAVING SUM(co.quantite * p.prix) > 50
ORDER BY chiffre_affaires DESC;
categorie chiffre_affaires
Informatique 752.5
Papeterie 93.0

Le mobilier, à 39 euros, est filtré par HAVING. Un WHERE ne pourrait pas le faire, car le total n’existe qu’après le regroupement.

Exercice 10 : la dernière commande de chaque client

Affichez chaque client avec la date de sa dernière commande, y compris ceux qui n’ont jamais commandé.

SELECT c.nom, MAX(co.date_commande) AS derniere_commande
FROM clients AS c
LEFT JOIN commandes AS co ON co.client_id = c.id
GROUP BY c.id, c.nom
ORDER BY c.id;
nom derniere_commande
Alice Martin 2026-09-12
Karim Benali 2026-08-20
Sophie Durand 2026-10-03
Lucas Petit 2026-09-05
Emma Leroy 2026-09-30
Yanis Moreau NULL

Exercice 11 : plus cher que la moyenne

Listez les produits dont le prix dépasse le prix moyen de la table, sans écrire ce prix moyen à la main.

SELECT nom, prix FROM produits
WHERE prix > (SELECT AVG(prix) FROM produits)
ORDER BY prix DESC;
nom prix
Écran 27 pouces 249
Chaise ergonomique 199
Clavier mécanique 89

La sous-requête calcule la moyenne, 88,39 euros, puis la requête principale compare chaque prix.

Exercice 12 : une hausse de prix en sécurité

Augmentez de 10 % les prix du mobilier, vérifiez le résultat, puis annulez.

BEGIN;
UPDATE produits
SET prix = ROUND(prix * 1.10, 2)
WHERE categorie = 'Mobilier';
SELECT nom, prix FROM produits WHERE categorie = 'Mobilier';
ROLLBACK;
nom prix
Lampe de bureau 42.9
Chaise ergonomique 218.9

L’UPDATE a touché deux lignes. Remplacez ROLLBACK par COMMIT pour garder la hausse.

Pour continuer, passez à nos 19 exercices Python débutant corrigés, puis reliez Python et SQL avec le module sqlite3.


Ce que nous n’avons pas pu vérifier

Les points suivants restent ouverts :

  • Les résultats sur PostgreSQL et MySQL. Les requêtes ont été exécutées sur SQLite 3.45.1 seulement. Les écarts avec PostgreSQL et MySQL viennent de leur documentation, pas d’une exécution. L’affichage des nombres y sera différent (249.00 au lieu de 249).
  • La dernière version de SQLite. La page de téléchargement proposait la version 3.54.1 le 30 octobre 2026. Nos requêtes n’utilisent aucune fonction récente, mais nous ne les avons pas rejouées sur cette version.
  • La portée de l’enquête Stack Overflow. Ses répondants sont des volontaires venus de la communauté du site, pas un échantillon représentatif de tous les développeurs.

Questions fréquentes

Combien de temps faut-il pour apprendre SQL quand on est débutant ?

Selon notre estimation, pas une mesure, les notions de ce cours s’apprennent en quelques séances si vous pratiquez chaque requête. La maîtrise vient ensuite avec de vraies bases, plus grandes que notre exemple.

Quelle base choisir pour un cours SQL débutant ?

SQLite pour démarrer, car il n’y a rien à configurer et toute la base tient dans un fichier. Passez ensuite à PostgreSQL, la base la plus utilisée de l’enquête Stack Overflow 2026, pour vous habituer à des types stricts et à un serveur.

Existe-t-il une formation SQL débutant gratuite ?

Oui. Ce cours et ses 12 exercices sont gratuits, tout comme les documentations officielles de SQLite et de PostgreSQL, dont le tutoriel couvre les mêmes bases. La Wild Code School propose aussi un cours SQL en libre accès d’environ 5 h 30, avec quiz.

Faut-il connaître un langage de programmation avant SQL ?

Non. SQL se pratique seul, dans l’outil sqlite3. Un langage comme Python devient utile pour intégrer les requêtes dans une application, et c’est là que les requêtes paramétrées s’imposent.


À propos de l’auteur

Abdelilah Ourti est ingénieur IA et architecte logiciel. Il conçoit et déploie des applications web et des systèmes d’IA, et publie sur NeurauTech des guides et des comparatifs destinés aux développeurs francophones. Ses autres articles sont réunis sur sa page auteur.

Méthode. Le cours et les exercices ont été écrits pour cet article. Le script de la base, chaque requête du cours et chaque solution ont été exécutés sur SQLite 3.45.1, via le module sqlite3 de Python 3.11.9, et les résultats recopiés tels quels. Les différences avec PostgreSQL et MySQL viennent de leur documentation officielle (PostgreSQL 18, MySQL 8.4) ; les recommandations de sécurité, de l’OWASP ; les chiffres d’usage, de l’enquête Stack Overflow 2026. Aucun lien n’est affilié et aucune relation commerciale ne lie l’auteur aux éditeurs mentionnés.

Une erreur dans une solution ? Écrivez à [email protected] : l’article sera corrigé et la date de mise à jour modifiée.

Dernière mise à jour : 30 octobre 2026.

Image : Sunder Muthukumaran (Unsplash License). Graphiques : NeurauTech, d’après le Stack Overflow Developer Survey 2026.

Publications similaires