Bases de données & SQL · L2 · Section 3/11
SELECT
Progression
#SELECT : projeter, filtrer, trier
SELECT sert à poser une question au moteur : quelles colonnes (projection), quelles lignes (filtre), dans quel ordre (tri) et combien (limite). Au-delà de la syntaxe, comprendre l'ordre logique d'évaluation et le traitement des NULL permet d'écrire des requêtes correctes du premier coup.
#Prérequis et objectifs
- Prérequis : la section Modélisation (notions de table, clé primaire, clé étrangère).
- Écrire des requêtes avec projection, filtre, tri et limite.
- Prévoir le résultat d'une requête avant de l'exécuter, NULL compris.
- Reconnaître les prédicats qui exploitent un index (SARGability) et paginer sans surcoût.
#L'ordre logique d'évaluation
Le SQL s'écrit dans un ordre, le moteur en évalue un autre :
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Deux conséquences à retenir : WHERE ne voit pas les alias du SELECT (évalué plus tôt), alors que ORDER BY les voit. Écrire where total > 50 quand total est un alias échoue ; order by total fonctionne.
#L'ordre complet, sous-requêtes comprises
L'ordre ci-dessus décrit une requête sans sous-requête. Dès qu'il y en a une, l'ordre de traitement devient :
FROM → WHERE → GROUP BY → HAVING → sous-requête → SELECT → ORDER BY → LIMIT
La logique est cohérente : chaque clause ne peut utiliser que ce que les précédentes ont produit.
FROMlit les tables : c'est la source.WHEREapplique une ou des conditions sur les lignes de ces tables.GROUP BYregroupe les lignes selon un attribut — les lignes regroupées sont celles qui ont survécu auWHERE.HAVINGne garde que les groupes satisfaisant une condition portant sur un calcul.- La sous-requête récupère un jeu de données, qui peut alors servir au
SELECT. SELECTchoisit les attributs à afficher, et peut donc calculer à partir de la sous-requête.ORDER BYordonne l'affichage,LIMITen borne le nombre de lignes.
Ce que cette liste interdit : utiliser un agrégat dans WHERE (il n'existe pas encore), ou utiliser dans WHERE le résultat d'une sous-requête non corrélée évaluée après le HAVING. Ce qu'elle autorise : aliaser dans le SELECT et trier dessus.
#Les cinq familles de conditions de recherche
Tout prédicat de WHERE appartient à l'une de ces cinq familles. Les reconnaître, c'est savoir immédiatement comment l'écrire et s'il est indexable.
| Famille | Écriture | Exemple |
|---|---|---|
| Comparaison | =, <>, <, <=, >, >= | salaire > 10000, ville = 'Paris' |
| Étendue (intervalle) | BETWEEN ... AND ... | salaire BETWEEN 20000 AND 30000 |
| Appartenance à un ensemble | IN (...) | couleur IN ('rouge', 'vert') |
| Correspondance à un masque | LIKE | adresse LIKE '%Montréal%' |
| Nul | IS NULL, IS NOT NULL | adresse IS NULL |
Deux remarques qui valent des points à l'examen :
BETWEEN a AND binclut les bornes. Pour exclure, il faut écrire> a AND < b, ou décaler les bornes comme on le fait sur les dates :>= '2026-01-05' AND < '2026-01-06'.INest une disjonction d'égalités :x IN (a, b, c)équivaut àx = a OR x = b OR x = c. C'est aussi pour cela quex IN (select ...)est une sous-requête, pas un filtre sur une colonne.
Le masque LIKE utilise deux caractères spéciaux : % remplace une suite quelconque de caractères (y compris vide) et _ remplace exactement un caractère. Attention aux dialectes d'interface graphique : certains générateurs de requêtes (et le langage Access) affichent * et ? à la place de % et _. En SQL standard et en MySQL, le caractère à employer est % : nom LIKE 'Nom%' sélectionne tous les noms commençant par « Nom ».
#Schéma et données de travail
Toutes les requêtes de cette section utilisent le même jeu fil rouge : users(id, name) et orders(id, user_id, amount, order_date). Alice (1) et Bob (2) ont des commandes ; Charlie (3) n'en a aucune.
1create table users(id integer primary key, name text not null);2create table orders(3 id integer primary key,4 user_id integer not null references users(id),5 amount real not null,6 order_date text not null7);8 9insert into users values (1, 'Alice'), (2, 'Bob'), (3, 'Charlie');10insert into orders values11 (1, 1, 40.0, '2026-01-05'),12 (2, 1, 60.0, '2026-01-12'),13 (3, 2, 15.5, '2026-01-08'),14 (4, 2, 90.0, '2026-02-01'),Première requête, avec résultat attendu :
1-- Attendu : 3 lignes, ordre Alice, Bob, Charlie2select id, name from users order by name;#NULL : l'inconnu, pas le vide
NULL signifie « valeur inconnue ou absente ». Toute comparaison avec NULL renvoie inconnu, donc n'est jamais vraie : amount = NULL ne sélectionne rien, pas même les lignes où amount vaut NULL. Les tests dédiés sont IS NULL et IS NOT NULL.
Côté agrégats, la règle change : COUNT(*) compte toutes les lignes, COUNT(col) ignore les NULL de col ; SUM et AVG les ignorent aussi.
1select2 (select count(*) from orders) as toutes,3 (select count(distinct user_id) from orders) as clients_distincts;4-- Attendu : une ligne : toutes = 5, clients_distincts = 2#Colonnes calculées, alias, expressions
Le SELECT calcule : fonctions de texte (upper, length, substr), arithmétique, concaténation avec ||, conditions avec CASE.
1select2 name,3 upper(name) as nom_maj,4 length(name) as nb_caracteres,5 case when id <= 2 then 'ancien' else 'nouveau' end as statut6from users7order by id;8-- Attendu : 3 lignes ; Alice → ALICE, 5, ancien ; Bob → BOB, 3, ancien ; Charlie → CHARLIE, 7, nouveau#SARGability : écrire des prédicats indexables
Un prédicat est SARGable quand le moteur peut le résoudre en parcourant un index. La règle : ne pas envelopper la colonne indexée dans une fonction du côté comparé.
1-- Non SARGable : la fonction sur la colonne empêche l'index2-- where date(order_date) = '2026-01-05'3 4-- SARGable : bornes directes sur la colonne5select id, amount from orders6where order_date >= '2026-01-05' and order_date < '2026-01-06';7-- Attendu : 1 ligne, la commande 1 (40.0)Harmonisez aussi les types : comparer un entier à une chaîne provoque des conversions implicites qui neutralisent l'index.
#Pagination : OFFSET ou curseur ?
LIMIT/OFFSET est simple, mais un offset élevé oblige le moteur à lire puis jeter toutes les lignes précédentes. La pagination par curseur (keyset) page par comparaison sur la dernière valeur vue, à coût constant :
1-- Page 1 : les deux premières lignes selon l'ordre de tri2select id, order_date, amount3from orders4order by order_date, id5limit 2;6-- Attendu : commandes 1 puis 3 ; (2026-01-05, 40.0) et (2026-01-08, 15.5)7-- Curseur à retenir : (order_date, id) = ('2026-01-08', 3)8 9-- Page 2 : on repart strictement après le curseur10select id, order_date, amount11from orders12where (order_date, id) > ('2026-01-08', 3)13order by order_date, id14limit 2;Deux propriétés rendent ce mécanisme correct. La comparaison est stricte (>), sinon la dernière ligne de la page 1 réapparaîtrait en tête de la page 2. Et la clé de tri (order_date, id) est totale : id départage les égalités de date, sans quoi deux lignes de même date pourraient être sautées ou répétées selon l'ordre d'examen.
Le curseur doit être la dernière ligne réellement renvoyée par la page précédente dans l'ordre de tri, pas la dernière ligne insérée : ici, la page 1 se termine sur la commande 3 (2026-01-08) et non sur la commande 2 (2026-01-12). Utiliser le mauvais curseur ferait silencieusement disparaître des lignes — le type de bug que seule une comparaison du nombre total de lignes permet de détecter.
#Playground
L'éditeur exécute du SQLite dans votre navigateur. Exécuter lance le script, Réinitialiser restaure le code, le résultat s'affiche en JSON. Modifiez les prédicats et prévoyez le résultat avant chaque exécution : c'est l'exercice le plus formateur.
#Le jeu de données officiel du cours
Le fil rouge ci-dessus est pratique, mais les énoncés d'examen s'appuient sur le schéma du cours. Le voici, avec ses tuples réels — le reconnaître fait gagner de précieuses minutes le jour de l'épreuve.
1create table Adresse(numero integer primary key, numRue integer, bis text, nomRue text, codePostal text, ville text);2create table Etudiant(numero integer primary key, nom text, prenom text, adresse integer references Adresse(numero));3create table Enseignant(numero integer primary key, nom text, prenom text, age integer, nbHeures integer, ville text);4create table UE(code text primary key, libelle text, nbHeures integer, responsable integer references Enseignant(numero));5create table EtudiantUE(etudiant integer references Etudiant(numero), uniteValeur text references UE(code),6 noteCC integer, noteExam integer, primary key (etudiant, uniteValeur));7 8insert into Adresse values9 (1, 3, 'b', 'Jean médecin', 'O6000', 'Nice'),10 (2, 10, ' ', 'Barla', 'O6000', 'Nice'),11 (3, 10, ' ', 'Jean Jaures', 'O6200', 'Cagnes');12insert into Etudiant values (1001, 'Nom1', 'prenom1', 1), (1002, 'Nom2', 'prenom2', 2), (1003, 'Nom3', 'prenom3', 3);13insert into Enseignant values14 (1, 'Menez', 'Gilles', 25, 35, 'Antibes'),Notez que EtudiantUE n'a pas de numéro propre : sa clé primaire est composite (etudiant, uniteValeur), ce qui interdit d'inscrire deux fois le même étudiant à la même UE. C'est le choix recommandé par l'énoncé.
Quatre requêtes de consultation simples, avec leur résultat attendu :
1-- 1. Code postal et ville, pour toutes les adresses (avec suppression des doublons)2select distinct codePostal, ville from Adresse;3-- Attendu : 2 lignes ; O6000/Nice, O6200/Cagnes4 5-- 2. Numéros des étudiants qui suivent l'UE « SL2IBD »6select etudiant from EtudiantUE where uniteValeur = 'SL2IBD';7-- Attendu : 3 lignes ; 1001, 1002, 10038 9-- 3. Enseignants dont le prénom contient « ll » ou « pp »10select * from Enseignant where prenom like '%ll%' or prenom like '%pp%';11-- Attendu : 3 lignes ; Menez (Gilles), Lahire (Philippe), Renevier (Philippe)12 13-- 4. Noms de rues de la ville « Nice »14select nomRue from Adresse where ville = 'Nice';La requête 1 est l'illustration exacte de la différence ensemble/sac : sans distinct, SQLite renvoie trois lignes, dont deux identiques. La requête 3 est le piège du LIKE : '%ll%' cherche « ll » n'importe où dans la chaîne, pas seulement au début — et c'est pour cela que Gilles Menez sort du filtre, alors qu'on pensait ne chercher que des « Philippe ». Le masque ne connaît pas l'intention du rédacteur.
#Exercice : sous-requête corrélée dans SELECT
Affichez, pour chaque utilisateur, son nom et son nombre de commandes, avec une sous-requête dans la clause SELECT. La sous-requête doit compter les commandes de l'utilisateur courant (corrélation sur users.id) ; Charlie doit apparaître avec 0.