La situation que tu vas rencontrer
Imagine une petite application de gestion de tâches. Il y a une table users et une table tasks. Rien d'exotique. Pourtant, dès la première fonctionnalité, les questions s'enchaînent. Afficher les tâches d'un utilisateur. Compter combien il en a terminé ce mois-ci. Trier par date d'échéance. Marquer une tâche comme faite. Supprimer un compte et tout ce qui va avec.
Chacune de ces actions correspond à une requête SQL. Et si tu regardes la liste, tu remarques qu'elle se répète d'un projet à l'autre. Une boutique en ligne remplace tasks par orders, un blog par articles, mais les opérations restent les mêmes. Lire une ligne précise, en filtrer plusieurs, joindre deux tables, agréger des chiffres, écrire et modifier. Ce sont ces cinq familles qui reviennent, quel que soit le domaine.
Apprendre le SQL de façon utile revient donc à s'entraîner sur ce petit ensemble jusqu'à ce que les patterns deviennent automatiques. Le reste, les fonctions de fenêtrage, les procédures stockées, l'optimisation fine des index, viendra plus tard, quand un besoin réel le justifiera.
Le noyau de requêtes qui suffit à 90% des cas
Avant le code, voici la carte. Les requêtes que tu écris tous les jours se rangent dans cinq familles. Garde cette liste sous les yeux, elle sert de repère quand une nouvelle fonctionnalité arrive.
- Lire avec un filtre. Le fameux
SELECT ... WHERE, complété parORDER BY,LIMITet la pagination. C'est ce que tu écris le plus souvent. - Relier deux tables. Le
JOIN, pour croiser des données réparties dans plusieurs tables. - Compter et regrouper. Le duo
GROUP BYavec les fonctions d'agrégation commeCOUNT,SUMouAVG. - Écrire et modifier. Le trio
INSERT,UPDATE,DELETE, pour créer, changer et retirer des lignes. - Poser une question dans une question. La sous-requête, utile quand un filtre dépend du résultat d'une autre requête.
Cinq familles, et tu tiens la charpente. La formation SQL et bases de données relationnelles avec MySQL de LaPolaris est construite autour de ce noyau, avec des exercices sur une base réelle plutôt que des exemples abstraits.
Les requêtes, une par une, avec du code
1. Lire avec un filtre
La requête de base récupère des lignes selon une condition. Prenons les tâches non terminées d'un utilisateur, les plus urgentes en premier.
SELECT id, titre, date_echeance
FROM tasks
WHERE user_id = 42
AND statut = 'en_cours'
ORDER BY date_echeance ASC
LIMIT 20;
Trois clauses portent l'essentiel. WHERE filtre les lignes, ORDER BY définit l'ordre, LIMIT plafonne le nombre de résultats. Ce dernier point compte plus qu'il n'en a l'air. Une table de production peut contenir des millions de lignes, et une requête sans limite les rapatrie toutes vers ton application. Prendre l'habitude de borner les résultats évite de saturer la mémoire dès que la table grossit.
Le WHERE accepte plus que l'égalité. Quelques opérateurs reviennent sans arrêt et méritent d'être connus tôt. IN teste l'appartenance à une liste, par exemple statut IN ('en_cours', 'a_valider'). BETWEEN cible un intervalle, pratique pour des dates ou des montants. LIKE cherche un motif dans du texte, avec le caractère % comme joker. Une recherche du type titre LIKE 'rapport%' renvoie tous les titres qui commencent par ce mot. Avec ces quelques opérateurs, tu couvres la plupart des filtres d'un écran de liste ou d'une barre de recherche.
Deux compléments accompagnent presque toujours cette lecture. Le premier est la pagination. Un écran n'affiche pas dix mille lignes d'un coup, il les découpe en pages, et OFFSET indique combien de lignes sauter avant de commencer.
SELECT id, titre
FROM tasks
WHERE user_id = 42
ORDER BY date_echeance ASC
LIMIT 20 OFFSET 40;
Cette requête ramène la troisième page de vingt résultats. Un point à garder en tête : LIMIT et OFFSET n'ont de sens qu'avec un ORDER BY, sinon l'ordre des pages n'est pas garanti d'une requête à l'autre. Le second complément est DISTINCT, qui retire les doublons du résultat. SELECT DISTINCT statut FROM tasks renvoie la liste des statuts existants, chacun une seule fois, ce qui sert souvent à remplir un menu déroulant.
2. Relier deux tables
Les données utiles sont rarement dans une seule table. Le nom de l'utilisateur est dans users, ses tâches dans tasks. Le JOIN les rapproche via une clé commune.
SELECT u.nom, t.titre, t.date_echeance
FROM tasks t
INNER JOIN users u ON u.id = t.user_id
WHERE t.statut = 'en_cours';
Le INNER JOIN ne garde que les lignes ayant une correspondance des deux côtés. Si tu veux aussi les utilisateurs qui n'ont aucune tâche, tu passes au LEFT JOIN, qui conserve toutes les lignes de la table de gauche et remplit avec NULL quand rien ne correspond. La différence entre ces deux formes est la source la plus fréquente de résultats surprenants. Un rapport qui affiche trop peu de lignes vient souvent d'un INNER JOIN là où un LEFT JOIN était attendu.
Les lettres t et u collées aux noms de tables sont des alias, des raccourcis déclarés juste après le nom de la table. Ils évitent de réécrire tasks et users en entier à chaque colonne, et deviennent indispensables dès qu'une requête relie trois ou quatre tables. Quand deux tables ont une colonne du même nom, l'alias lève l'ambiguïté en précisant de laquelle tu parles.
3. Compter et regrouper
Vient le moment où on ne veut plus les lignes, mais des chiffres. Combien de tâches par utilisateur, quel chiffre d'affaires par mois, combien d'articles par catégorie. C'est le rôle de GROUP BY.
SELECT user_id, COUNT(*) AS nb_taches
FROM tasks
WHERE statut = 'terminee'
GROUP BY user_id
HAVING COUNT(*) > 5
ORDER BY nb_taches DESC;
La requête regroupe les lignes par user_id, puis compte les tâches terminées de chaque groupe. Le HAVING filtre après le regroupement, là où WHERE filtre avant. La règle à retenir tient en une phrase : WHERE travaille sur les lignes individuelles, HAVING travaille sur les groupes. Confondre les deux mène à des erreurs de syntaxe ou à des résultats faux, et c'est un point qui revient dans presque tous les débuts.
Le même mécanisme sert à produire des statistiques métier. Sur une table de commandes, un chiffre d'affaires par mois s'obtient en regroupant par mois et en sommant les montants.
SELECT DATE_FORMAT(created_at, '%Y-%m') AS mois,
SUM(montant) AS ca
FROM orders
WHERE statut = 'payee'
GROUP BY mois
ORDER BY mois;
Cette seule requête alimente un graphique de tableau de bord. La plupart des rapports que tu croiseras en entreprise reposent sur cette combinaison de regroupement et d'agrégation, avec parfois un JOIN en amont pour ramener des libellés lisibles.
4. Écrire et modifier
Lire ne suffit pas, une application écrit aussi. Trois verbes couvrent ce besoin.
INSERT INTO tasks (user_id, titre, statut)
VALUES (42, 'Rédiger le rapport', 'en_cours');
UPDATE tasks
SET statut = 'terminee'
WHERE id = 108;
DELETE FROM tasks
WHERE id = 108;
Un détail mérite ton attention immédiate sur UPDATE et DELETE : sans clause WHERE, l'opération s'applique à toute la table. Un DELETE FROM tasks lancé sans condition vide la table entière. Beaucoup de bases de production ont été abîmées par un WHERE oublié. Prends le réflexe d'écrire d'abord un SELECT avec la même condition, de vérifier les lignes visées, puis de transformer ce SELECT en UPDATE ou DELETE.
Une écriture touche parfois plusieurs tables d'un coup. Créer une commande, puis diminuer le stock du produit correspondant, forme une seule opération métier qui doit réussir entièrement ou pas du tout. La transaction encadre ce genre de cas. Tu ouvres avec BEGIN, tu enchaînes tes requêtes, tu valides avec COMMIT. Si une étape échoue, un ROLLBACK annule tout ce qui a été fait depuis le début, et la base revient à son état d'avant. Sans ce mécanisme, une commande pourrait s'enregistrer sans que le stock ne bouge, et laisser les données dans un état incohérent.
5. Poser une question dans une question
La sous-requête sert quand un filtre dépend d'un calcul intermédiaire. Par exemple, récupérer les utilisateurs qui ont au moins une tâche en retard.
SELECT nom, email
FROM users
WHERE id IN (
SELECT user_id
FROM tasks
WHERE statut = 'en_cours'
AND date_echeance < CURRENT_DATE
);
La requête interne dresse la liste des identifiants concernés, la requête externe s'en sert pour filtrer. Beaucoup de sous-requêtes peuvent se réécrire en JOIN, souvent plus rapide sur de gros volumes, mais la version en sous-requête reste plus lisible quand on découvre. Commence par ce qui se lit facilement, tu optimiseras une fois que la logique sera claire dans ta tête.
L'ordre qui explique beaucoup d'erreurs
On écrit une requête dans un certain ordre, mais la base l'exécute dans un autre. C'est cette différence qui rend certaines erreurs difficiles à comprendre. La base commence par FROM et les JOIN pour rassembler les données, applique ensuite WHERE, regroupe avec GROUP BY, filtre les groupes avec HAVING, choisit les colonnes du SELECT, trie avec ORDER BY, et coupe enfin avec LIMIT.
Cet ordre logique explique un message d'erreur classique. Un alias défini dans le SELECT ne peut pas toujours servir dans le WHERE, puisque le WHERE est traité avant que le SELECT n'existe. Une fois cette séquence en tête, une bonne partie des messages du moteur cessent d'être mystérieux et deviennent des indications précises sur l'étape qui bloque.
Les pièges qui coûtent des heures
Concaténer les valeurs dans la requête
Construire une requête en collant directement une saisie utilisateur dans le texte SQL ouvre la porte à l'injection SQL, l'une des failles les plus exploitées du web. La parade s'appelle requête préparée : tu écris la requête avec des marqueurs, la valeur est transmise à part. Ce sujet est détaillé dans l'article sur les failles de sécurité qu'on retrouve dans presque tous les projets juniors.
Le SELECT * partout
Récupérer toutes les colonnes avec SELECT * paraît pratique, mais transporte des données inutiles et casse dès qu'une colonne change. Nommer les colonnes attendues rend la requête plus rapide et plus stable dans le temps.
Comparer avec NULL en utilisant l'égalité
Une colonne vide vaut NULL, et WHERE colonne = NULL ne renvoie jamais rien. La comparaison correcte utilise IS NULL ou IS NOT NULL. Ce comportement déroute au premier contact, il se retient vite une fois qu'on est tombé dessus.
Donner à l'application un compte tout-puissant
Un compte de base utilisé par ton application n'a pas besoin de pouvoir supprimer des tables. Restreindre ses droits aux seules opérations nécessaires limite les dégâts d'un bug ou d'une intrusion, un principe développé dans l'article sur le principe de moindre privilège.
Où s'entraîner ensuite
Ces requêtes se retiennent en les tapant, pas en les lisant. Monte une petite base locale, crée deux ou trois tables, et rejoue chaque famille sur tes propres données. Le SQL sert autant côté backend Java avec la roadmap développeur Spring Boot que dans un projet fullstack, comme le montre la formation Fullstack avec Next.js : API Routes et base de données.
Un bon premier réflexe : ajouter une petite fonctionnalité qui lit et écrit en base à un projet existant, plutôt que de partir d'une base vide. C'est aussi une manière de nourrir la section projets d'un CV, un point traité dans l'article premier projet pour un CV de développeur junior.
Questions fréquentes
Faut-il apprendre MySQL ou PostgreSQL en premier ?
Les requêtes de base présentées ici fonctionnent quasi à l'identique sur les deux. Choisis celui qui correspond à ton projet ou à l'offre d'emploi que tu vises, apprends le noyau, et le passage à l'autre te prendra quelques jours. MySQL reste très présent dans l'écosystème PHP et WordPress, PostgreSQL a la faveur de beaucoup de projets récents et de la data.
Combien de temps pour être à l'aise avec ces requêtes ?
Avec une pratique régulière sur une vraie base, quelques semaines suffisent pour écrire ces cinq familles sans hésiter. Le déclic vient quand tu arrêtes de recopier des exemples et que tu formules toi-même la requête à partir du besoin. Un exercice par jour sur tes propres tables accélère nettement ce passage.
Un ORM ne rend-il pas le SQL inutile ?
Un ORM comme Doctrine, Prisma ou Hibernate écrit le SQL à ta place la plupart du temps. Le jour où une requête devient lente ou produit un résultat inattendu, il faut lire le SQL généré pour comprendre. Sans les bases, ce moment devient un mur. L'ORM te fait gagner du temps, il ne te dispense pas de savoir lire ce qu'il produit.
Quelle différence entre WHERE et HAVING ?
WHERE filtre les lignes avant tout regroupement, HAVING filtre les groupes après un GROUP BY. Si tu veux garder les commandes de plus de 100 euros, tu utilises WHERE. Si tu veux garder les clients dont le total de commandes dépasse 100 euros, tu utilises HAVING, car ce total se calcule sur un groupe.
Comment éviter d'effacer des données par erreur ?
Écris toujours la condition en SELECT d'abord pour voir les lignes visées, puis transforme la requête en UPDATE ou DELETE une fois sûr du résultat. Travaille sur une copie de test avant la production, et donne à ton compte applicatif le moins de droits possible. Ces trois habitudes évitent la grande majorité des accidents.