Chapitre 718 minDébutant
Les ensembles en informatique : SQL, codage et tri
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- traduire une réunion, une intersection, une différence en SQL ;
- expliquer une jointure comme une partie d'un produit cartésien ;
- manipuler des ensembles avec des bits, en Python ou en JavaScript ;
- dire pourquoi un tri a besoin d'une relation d'ordre total.
4 exercices corrigés
La théorie des ensembles sert en informatique partout où l'on regroupe, compare, combine ou code des données : les bases de données relationnelles reposent sur elle, les masques de bits codent des ensembles dans un entier, les sous-réseaux sont des classes d'équivalence, et tout algorithme de tri suppose une relation d'ordre. Ce chapitre rassemble ces usages, avec des exemples que tu peux exécuter. Il sert aussi de révision : chaque notion des chapitres 1 à 6 y réapparaît dans un contexte professionnel.
Le modèle relationnel, une théorie des ensembles appliquée
En 1970, Edgar F. Codd propose de ranger les données dans des relations au sens mathématique, c'est-à-dire des parties d'un produit cartésien de domaines (chapitre 3). Il définit sur ces relations des opérations, l'algèbre relationnelle, dont SQL est la traduction.
| Opération de l'algèbre relationnelle | Notion d'ensemble | En SQL |
|---|---|---|
| Sélection σ | Partie définie en compréhension | WHERE |
| Projection π | Image par l'application « ligne ↦ colonnes choisies » | SELECT col1, col2 (avec DISTINCT pour un vrai ensemble) |
| Produit cartésien × | Produit cartésien | CROSS JOIN |
| Jointure ⋈ | Partie du produit cartésien | JOIN … ON |
| Union ∪ | Réunion | UNION |
| Intersection ∩ | Intersection | INTERSECT |
| Différence − | Différence | EXCEPT |
Une nuance sépare la théorie de la pratique : une table SQL peut contenir des lignes en double, et un SELECT sans DISTINCT les garde. On parle de multiensemble (un ensemble où un élément peut figurer plusieurs fois). C'est pourquoi SQL a des variantes UNION ALL, INTERSECT ALL et EXCEPT ALL, qui conservent les doublons.
UNION, INTERSECT et EXCEPT
Prenons deux tables d'une seule colonne : acces_vpn(login) contient ana, bilal, chloe, david et emma ; groupe_admin(login) contient bilal, emma et farid. Ce sont deux parties de l'ensemble des comptes.
-- Réunion : les comptes du VPN ou du groupe admin (doublons supprimés)
SELECT login FROM acces_vpn
UNION
SELECT login FROM groupe_admin
ORDER BY login;
-- ana, bilal, chloe, david, emma, farid : 6 lignes
-- Intersection : les administrateurs qui ont accès au VPN
SELECT login FROM acces_vpn
INTERSECT
SELECT login FROM groupe_admin
ORDER BY login;
-- bilal, emma
-- Différence : les comptes VPN qui ne sont pas administrateurs
SELECT login FROM acces_vpn
EXCEPT
SELECT login FROM groupe_admin
ORDER BY login;
-- ana, chloe, david
La formule du cardinal d'une réunion se vérifie : 5 + 3 − 2 = 6. Et UNION ALL, qui garde les doublons, renvoie 5 + 3 = 8 lignes. L'ordre des deux requêtes compte pour EXCEPT comme pour la différence : groupe_admin EXCEPT acces_vpn ne renvoie que farid.
Avant ces opérateurs, on écrivait l'intersection et la différence avec l'appartenance, IN et NOT IN, ou avec EXISTS :
-- Intersection avec IN : x ∈ A et x ∈ B
SELECT login FROM acces_vpn
WHERE login IN (SELECT login FROM groupe_admin);
-- Différence avec NOT EXISTS : x ∈ A et x ∉ B
SELECT v.login FROM acces_vpn v
WHERE NOT EXISTS (SELECT 1 FROM groupe_admin g WHERE g.login = v.login);
Le piège de NOT IN et de NULL
Dans une table employe(id_employe, nom, id_service, id_responsable), la directrice n'a pas de responsable : sa colonne id_responsable vaut NULL. On cherche les employés qui ne sont le responsable de personne, c'est-à-dire la différence « tous les employés privés des responsables » :
SELECT nom FROM employe
WHERE id_employe NOT IN (SELECT id_responsable FROM employe);
-- aucune ligne !
La sous-requête contient un NULL. Pour SQL, 3 NOT IN (1, 2, NULL) signifie « 3 ≠ 1 et 3 ≠ 2 et 3 ≠ NULL », et la comparaison avec NULL ne vaut ni vrai ni faux, mais inconnu : toute la condition devient inconnue, et la ligne est écartée. SQL ne suit pas la logique à deux valeurs des ensembles, mais une logique à trois valeurs. Deux corrections :
-- Retirer NULL de l'ensemble avant la différence
SELECT nom FROM employe
WHERE id_employe NOT IN (SELECT id_responsable FROM employe WHERE id_responsable IS NOT NULL);
-- Ou utiliser NOT EXISTS, qui ne souffre pas du problème
SELECT nom FROM employe e
WHERE NOT EXISTS (SELECT 1 FROM employe x WHERE x.id_responsable = e.id_employe);
Une jointure est une partie du produit cartésien
Avec une table service de 3 lignes et une table employe de 4 lignes, le produit cartésien service CROSS JOIN employe compte 3 × 4 = 12 lignes : chaque employé associé à chaque service. La jointure n'en garde que les couples où l'employé appartient vraiment au service :
SELECT e.nom, s.libelle
FROM employe e
JOIN service s ON s.id_service = e.id_service;
Mathématiquement, c'est la partie {(e ; s) ∈ Employés × Services | e.id_service = s.id_service} : 4 couples sur 12. Une jointure, c'est un produit cartésien suivi d'une sélection. Le SGBD ne calcule pas réellement les 12 lignes (il utilise des index pour aller droit aux bons couples), mais le résultat est le même.
Deux conséquences pratiques :
- une condition de jointure oubliée renvoie tout le produit cartésien : un nombre de lignes égal au produit des tailles des tables est le symptôme ;
- la clé étrangère
id_servicedéfinit une application des employés vers les services (chapitre 6), et la jointure interne ne garde que les services qui ont un antécédent. LeLEFT JOINgarde aussi ceux qui n'en ont pas, avec desNULL: c'est l'outil pour tester la surjectivité.
Regrouper et trier : équivalence et ordre en SQL
| Clause SQL | Notion | Ce qu'elle fait |
|---|---|---|
DISTINCT | Ensemble (sans doublon) | Transforme un multiensemble en ensemble |
GROUP BY col | Relation d'équivalence « même valeur de col » | Calcule les classes, une ligne par classe |
COUNT(*) après GROUP BY | Cardinal | Le cardinal de chaque classe |
ORDER BY col1, col2 | Ordre lexicographique | Range les lignes selon un ordre total |
IN (…) | Appartenance | Teste x ∈ A |
WHERE | Définition en compréhension | Garde les lignes qui vérifient la propriété |
Les masques de bits : un ensemble dans un entier
Au chapitre 1, une partie d'un ensemble de n éléments s'écrivait comme un mot de n bits. Les programmes stockent ainsi des ensembles dans un simple entier, et les opérations ensemblistes deviennent des opérations bit à bit, exécutées en une instruction du processeur.
| Ensembles | Opérateur bit à bit | En C, PHP, JavaScript, Python |
|---|---|---|
| Réunion A ∪ B | OU bit à bit | a | b |
| Intersection A ∩ B | ET bit à bit | a & b |
| Complémentaire Ā | NON bit à bit | ~a |
| Différence A \ B | A ET (NON B) | a & ~b |
| Différence symétrique A Δ B | OU exclusif | a ^ b |
| Appartenance de l'élément i | Tester le bit i | (a & (1 << i)) != 0 |
Exemple : les droits Unix. Avec r = 4 (100), w = 2 (010) et x = 1 (001), les droits {r ; w} valent 6 (110).
- Ajouter x :
6 | 1= 7, soit {r ; w ; x}. - Retirer w :
6 & ~2= 4, soit {r} (sur 3 bits). - Tester r :
6 & 4= 4, non nul : r ∈ {r ; w}.
Exemple : les sous-réseaux. L'adresse réseau d'une machine s'obtient par un ET bit à bit entre son adresse et le masque. Avec le masque 255.255.255.192 (/26) :
| Adresse | Adresse ET masque | Réseau (classe d'équivalence) |
|---|---|---|
| 192.168.10.10 | 192.168.10.0 | 192.168.10.0/26 |
| 192.168.10.77 | 192.168.10.64 | 192.168.10.64/26 |
| 192.168.10.100 | 192.168.10.64 | 192.168.10.64/26 |
| 192.168.10.130 | 192.168.10.128 | 192.168.10.128/26 |
192.168.10.77 et 192.168.10.100 ont la même image par l'application « adresse ↦ adresse ET masque » : elles sont dans la même classe, le même sous-réseau, et peuvent communiquer sans routeur. Le calcul détaillé des masques est dans le cours Réseaux.
Les ensembles dans les langages de programmation
Python et JavaScript ont un type ensemble natif, qui garantit l'absence de doublons et teste l'appartenance très vite, quelle que soit sa taille (il repose sur une table de hachage, donc sur des classes d'équivalence).
vpn = {"ana", "bilal", "chloe", "david", "emma"}
admin = {"bilal", "emma", "farid"}
print(sorted(vpn | admin)) # réunion : ['ana', 'bilal', 'chloe', 'david', 'emma', 'farid']
print(sorted(vpn & admin)) # intersection : ['bilal', 'emma']
print(sorted(vpn - admin)) # différence : ['ana', 'chloe', 'david']
print(sorted(vpn ^ admin)) # différence symétrique : ['ana', 'chloe', 'david', 'farid']
print(admin <= vpn) # inclusion : False (farid n'est pas dans vpn)
print("ana" in vpn) # appartenance : True
En JavaScript, les objets Set ont reçu en 2024 les méthodes ensemblistes, disponibles dans les navigateurs actuels et dans Node.js depuis la version 22 :
const vpn = new Set(["ana", "bilal", "chloe", "david", "emma"]);
const admin = new Set(["bilal", "emma", "farid"]);
vpn.union(admin); // Set { ana, bilal, chloe, david, emma, farid }
vpn.intersection(admin); // Set { bilal, emma }
vpn.difference(admin); // Set { ana, chloe, david }
vpn.symmetricDifference(admin); // Set { ana, chloe, david, farid }
admin.isSubsetOf(vpn); // false
vpn.has("ana"); // true
// Supprimer les doublons d'un tableau : passer par un ensemble
const ports = [80, 443, 80, 22, 443];
const uniques = [...new Set(ports)]; // [80, 443, 22]
Trier suppose une relation d'ordre total
Un algorithme de tri range des éléments du plus petit au plus grand : il lui faut une relation qui permette de comparer n'importe quels deux éléments, donc un ordre total (chapitre 5). Avec un ordre partiel (« divise », l'inclusion), il n'y a pas de « plus petit » entre deux éléments incomparables, et un tri n'a pas de sens.
En pratique, la fonction de comparaison qu'on passe à un tri (usort en PHP, sort avec un comparateur en JavaScript, key= en Python) définit cet ordre. Elle doit être cohérente : si a passe avant b et b avant c, a doit passer avant c (transitivité). Un comparateur incohérent donne un tri faux, sans erreur.
Quand on trie selon un seul critère qui ne départage pas tout (trier des employés par service), deux employés du même service sont « à égalité » : la relation « passe avant ou au même rang » est réflexive et transitive, mais pas antisymétrique. Un tri stable garde alors l'ordre d'origine des ex æquo. Les tris de Python, de JavaScript (depuis ES2019) et de PHP (depuis la version 8.0) sont stables. C'est ce qui permet de trier d'abord par prénom, puis par nom, et d'obtenir un résultat rangé par nom puis prénom : l'ordre lexicographique, construit en deux passes.
L'ordre lexicographique est aussi celui des chaînes : c'est pourquoi un tri de noms de fichiers place fichier10.txt avant fichier2.txt. Les explorateurs de fichiers utilisent un « tri naturel », qui compare les suites de chiffres comme des nombres.
Codage, chiffrement, compression : des applications
Le programme du BTS SIO cite le codage, le tri et la compression comme usages de ces notions. Chacun pose la même question : l'application est-elle injective ? bijective ?
| Traitement | Application | Propriété exigée | Pourquoi |
|---|---|---|---|
| Conversion décimal → binaire | {0 ; … ; 255} → {0 ; 1}⁸ | Bijection | Chaque nombre a une écriture et une seule |
| Codage de caractères (UTF-8) | Textes → suites d'octets | Injection | Deux textes différents ne doivent pas donner les mêmes octets |
| Compression sans perte (ZIP, PNG) | Fichiers → fichiers compressés | Injection | Sinon on ne peut pas décompresser |
| Chiffrement (César, affine, AES) | Messages → messages chiffrés | Bijection (pour une clé donnée) | Le déchiffrement est la réciproque |
| Hachage (SHA-256) | Fichiers → empreintes de 256 bits | Ni injection ni surjection exigée, collisions difficiles à trouver | Vérifier l'intégrité ou stocker un mot de passe, sans pouvoir revenir en arrière |
| Clé primaire | Lignes → valeurs de la clé | Injection | Identifier chaque ligne |
| Table de hachage | Clés → numéros de case | Aucune ; les collisions sont gérées | Ranger et retrouver vite |
Exercices
Exercice 1 sur 4 Traduire en SQL Réussi À revoir
Une base contient les tables inscrit_python(email), inscrit_reseau(email) et etudiant(email, nom). Écris une requête pour chaque ensemble, puis donne l'expression ensembliste correspondante (P, R et E désignent les trois ensembles d'adresses).
- Les étudiants inscrits aux deux ateliers.
- Les étudiants inscrits à au moins un atelier, sans doublon.
- Les étudiants inscrits à l'atelier Python mais pas à l'atelier réseau.
- Les étudiants inscrits à aucun atelier.
Voir la réponseMasquer la réponse de l'exercice 1
- P ∩ R :
SELECT email FROM inscrit_python INTERSECT SELECT email FROM inscrit_reseau; - P ∪ R :
SELECT email FROM inscrit_python UNION SELECT email FROM inscrit_reseau; - P \ R :
SELECT email FROM inscrit_python EXCEPT SELECT email FROM inscrit_reseau; - E \ (P ∪ R), c'est-à-dire le complémentaire de P ∪ R dans E. Comme E \ (P ∪ R) = (E \ P) \ R, deux
EXCEPTà la suite suffisent (ils s'appliquent de gauche à droite) :
SELECT email FROM etudiant
EXCEPT
SELECT email FROM inscrit_python
EXCEPT
SELECT email FROM inscrit_reseau;
Avec une version de MySQL antérieure à la 8.0.31, on écrirait les trois dernières avec NOT EXISTS, par exemple pour la 4 : SELECT email FROM etudiant e WHERE NOT EXISTS (SELECT 1 FROM inscrit_python p WHERE p.email = e.email) AND NOT EXISTS (SELECT 1 FROM inscrit_reseau r WHERE r.email = e.email); (les deux NOT EXISTS traduisent Ā ∩ B̄, comme De Morgan).
Tu avais trouvé ?
Exercice 2 sur 4 Combien de lignes ? Réussi À revoir
La table poste contient 40 lignes, la table logiciel 12 lignes.
- Combien de lignes renvoie
SELECT * FROM poste CROSS JOIN logiciel? - La table
installation(id_poste, id_logiciel), de clé primaire (id_poste, id_logiciel), contient 150 lignes. Pourquoi ne peut-elle pas en contenir plus de 480 ? - Que représente, en termes d'ensembles, la requête qui liste les couples (poste ; logiciel) non installés ? Combien de lignes renvoie-t-elle ?
Voir la réponseMasquer la réponse de l'exercice 2
- 40 × 12 = 480 lignes : le produit cartésien.
- La clé primaire interdit deux fois le même couple : chaque ligne est un couple distinct de Postes × Logiciels. La table est une partie de ce produit cartésien, donc a au plus 480 lignes.
- C'est la différence (Postes × Logiciels) \ Installations, le complémentaire de la relation « est installé sur » dans le produit cartésien. Elle contient 480 − 150 = 330 lignes.
Tu avais trouvé ?
Exercice 3 sur 4 Masques de bits Réussi À revoir
Un programme code les options d'un compte sur 4 bits : 1 = messagerie, 2 = VPN, 4 = télétravail, 8 = administrateur. Le compte de Julie vaut 7.
- Quelles options Julie a-t-elle ? Écris l'ensemble correspondant.
- On lui retire le VPN. Quelle opération bit à bit faut-il faire, et quel nombre obtient-on ?
- Le compte de Karim vaut 10. Quelles options les deux comptes ont-ils en commun, au départ ? Fais le calcul avec un opérateur bit à bit.
Voir la réponseMasquer la réponse de l'exercice 3
- 7 = 0111 = 1 + 2 + 4 : {messagerie ; VPN ; télétravail}.
- Une différence :
7 & ~2. 0111 ET 1101 = 0101 = 5, soit {messagerie ; télétravail}. - 10 = 1010 = 2 + 8 : {VPN ; administrateur}. L'intersection se calcule par
7 & 10: 0111 ET 1010 = 0010 = 2, soit {VPN}.
Tu avais trouvé ?
Exercice 4 sur 4 Tri et relations Réussi À revoir
- Un développeur trie une liste de tâches avec la comparaison « la tâche a doit être terminée avant la tâche b ». Pourquoi un algorithme de tri classique ne convient-il pas ? Quel type de relation est-ce ?
- Une liste d'employés est d'abord triée par prénom avec un tri stable, puis par service, toujours avec un tri stable. Dans quel ordre les employés d'un même service apparaissent-ils ?
Voir la réponseMasquer la réponse de l'exercice 4
- « Doit être terminée avant » (complétée par l'égalité) est une relation d'ordre partiel : deux tâches indépendantes ne sont pas comparables. Un tri classique exige de pouvoir comparer toutes les paires. Il faut un tri topologique, qui produit un ordre total compatible avec l'ordre partiel, celui qu'utilise l'ordonnancement (niveaux d'un graphe).
- Par ordre de prénom. Le second tri range par service ; comme il est stable, il conserve, à l'intérieur de chaque service, l'ordre du tri précédent, c'est-à-dire l'ordre des prénoms. Le résultat est l'ordre lexicographique sur (service ; prénom).
Tu avais trouvé ?