Aller au contenu principal

Bases de données & SQL · L2 · Section 3/11

SELECT

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#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 :

FROMWHEREGROUP BYHAVINGSELECTORDER BYLIMIT

FROM
Sources et jointures
WHERE
Filtrer les lignes (pas les alias)
GROUP BY
Former des groupes
HAVING
Filtrer après agrégat
SELECT
Projeter, aliaser
ORDER BY
Trier (voit les alias)
LIMIT
Limiter le résultat

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 :

FROMWHEREGROUP BYHAVINGsous-requêteSELECTORDER BYLIMIT

La logique est cohérente : chaque clause ne peut utiliser que ce que les précédentes ont produit.

  • FROM lit les tables : c'est la source.
  • WHERE applique une ou des conditions sur les lignes de ces tables.
  • GROUP BY regroupe les lignes selon un attribut — les lignes regroupées sont celles qui ont survécu au WHERE.
  • HAVING ne 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.
  • SELECT choisit les attributs à afficher, et peut donc calculer à partir de la sous-requête.
  • ORDER BY ordonne l'affichage, LIMIT en 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ÉcritureExemple
Comparaison=, <>, <, <=, >, >=salaire > 10000, ville = 'Paris'
Étendue (intervalle)BETWEEN ... AND ...salaire BETWEEN 20000 AND 30000
Appartenance à un ensembleIN (...)couleur IN ('rouge', 'vert')
Correspondance à un masqueLIKEadresse LIKE '%Montréal%'
NulIS NULL, IS NOT NULLadresse IS NULL

Deux remarques qui valent des points à l'examen :

  • BETWEEN a AND b inclut 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'.
  • IN est une disjonction d'égalités : x IN (a, b, c) équivaut à x = a OR x = b OR x = c. C'est aussi pour cela que x 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.

sqlsql

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 :

sqlsql

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.

sqlsql

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.

sqlsql

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é.

sqlsql

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 :

sqlsql

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.

Chargement de l’éditeur...

#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.

sqlsql

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 :

sqlsql

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.

#Quiz

Que renvoie SELECT COUNT(name) FROM users WHERE id > 0 si tous les users ont un name rempli ?
Que renvoie SELECT COUNT(name) FROM users WHERE id > 0 si tous les users ont un name rempli ?