Module L08 · Partie F · Coder comme un professionnel
Données persistantes et réseau : SQL, HTTP, API.
Un robot produit des milliers de mesures par minute ; un projet d’IA en manipule des millions. Les stocker dans des listes ne tient pas. Ce module présente le modèle relationnel et SQL (exécuté dans la page avec SQLite), puis le web : HTTP, JSON, une API REST, et comment un tableau de bord parle à un robot.
Durée : 2 séances · Prérequis : L01, dictionnaires, fichiers. Objectifs : concevoir un schéma relationnel, écrire des requêtes SQL (SELECT, JOIN, GROUP BY, sous-requêtes, index), comprendre transactions et injections, HTTP et JSON, écrire un serveur et un client minimaux.
Ce que vous saurez faire à la fin
- Modéliser des données en tables avec clés primaires et étrangères, en évitant la redondance.
- Écrire et optimiser des requêtes SQL sur des centaines de milliers de lignes.
- Exposer les données d’un robot via une API HTTP/JSON et les consommer depuis Python ou un navigateur.
Références : programme NSI terminale (bases de données), freeCodeCamp « Relational Database », Designing Data-Intensive Applications (Kleppmann) chapitres 1-3, MDN Web Docs.
Fiche de cours · Définitions
Bases de données et web : définitions
Fiche de cours · Formules
Formules et équivalences à connaître
| Algèbre | SQL | Coût naïf |
|---|---|---|
| σc(R) | SELECT * FROM R WHERE c | O(|R|), O(log |R|) avec index |
| πa,b(R) | SELECT DISTINCT a, b FROM R | O(|R|) (+ tri pour DISTINCT) |
| R ⋈R.k = S.k S | FROM R JOIN S ON R.k = S.k | O(|R|·|S|) boucles imbriquées ; O(|R| + |S|) par hachage |
| γg, f(a)(R) | SELECT g, f(a) FROM R GROUP BY g | O(|R|) par hachage |
| R − S | … WHERE k NOT IN (SELECT k FROM S) | O(|R| + |S|) par hachage |
Codes HTTP à connaître : 200 OK, 201 Created, 204 No Content, 301/302 redirection, 304 Not Modified, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests, 500 Internal Server Error, 503 Service Unavailable.
Fiche de cours · Théorèmes et démonstrations
Démonstrations à savoir refaire
"SELECT * FROM users WHERE nom = '" + nom + "'". Avec nom = ' OR '1'='1, elle devient SELECT * FROM users WHERE nom = '' OR '1'='1', vraie pour toute ligne. Avec nom = '; DROP TABLE users; --, deux requêtes sont exécutées. Le texte de l’utilisateur est interprété comme du code parce que rien ne sépare données et code. Les requêtes paramétrées (cur.execute("… WHERE nom = ?", (nom,))) transmettent la donnée séparément : elle n’est jamais analysée comme SQL, quel que soit son contenu. C’est la seule protection correcte (l’échappement manuel est fragile).Fiche de cours · Méthodes
Méthodes et pièges
EXPLAIN QUERY PLAN pour voir si un index est utilisé./robots, /robots/42/missions) ; GET sans effet de bord ; codes de statut corrects ; erreurs en JSON avec message ; pagination (?page=2&size=50) ; validation des entrées ; authentification par jeton ; journaliser chaque requête.Pièges : SELECT * dans du code (fragile aux changements de schéma) ; oublier la condition de jointure (produit cartésien) ; NULL n’est égal à rien (= NULL est toujours faux : IS NULL) ; agrégat sans GROUP BY sur des colonnes non agrégées ; stocker des mots de passe en clair (hacher avec sel, L22) ; GET qui modifie des données.
Fiche de cours · Exercices corrigés
Exercices corrigés
-- (a) différence : robots sans participation
SELECT r.nom FROM Robot r LEFT JOIN Participation p ON p.robot_id = r.id WHERE p.robot_id IS NULL;
-- (b) agrégat par mission (LEFT JOIN pour garder les missions à 0 robot)
SELECT m.titre, COUNT(p.robot_id) AS n FROM Mission m LEFT JOIN Participation p ON p.mission_id = m.id GROUP BY m.id;
-- (c) division relationnelle : missions dont le nombre de robots distincts = nombre total de robots
SELECT m.titre FROM Mission m JOIN Participation p ON p.mission_id = m.id
GROUP BY m.id HAVING COUNT(DISTINCT p.robot_id) = (SELECT COUNT(*) FROM Robot);(c) est la « division » : il n’y a pas d’opérateur SQL direct, on compte.CREATE INDEX ON Mesure(robot, capteur, t) permet d’aller directement à la plage (B-arbre : O(log n) + taille du résultat) : quelques millisecondes. L’ordre des colonnes dans l’index doit suivre les égalités (robot, capteur) puis l’intervalle (t). unite dépend de capteur, pas de la clé complète (robot, capteur, t) : dépendance partielle, violation de la 2FN, 10⁷ répétitions de « m/s ». Déplacer dans Capteur(nom, unite).GET /robots/{id}/position et POST /robots/{id}/commande avec un corps JSON {"v": 0.3, "w": 0.1}. Donner les réponses attendues (codes, corps) pour : robot inexistant, corps malformé, commande acceptée, robot en panne.{"erreur": "robot 7 inconnu"}. JSON invalide ou champ manquant/hors plage (v = 12) : 400 {"erreur": "v doit être dans [-1, 1]"} — valider avant d’agir. Commande acceptée : 202 Accepted (si exécution asynchrone) ou 200 avec l’état résultant {"v": 0.3, "w": 0.1, "t": …}. Robot en panne : 503 Service Unavailable avec Retry-After, ou 409 Conflict si l’état du robot interdit la commande. GET position : 200 {"x": …, "y": …, "theta": …, "t": …}, jamais de modification d’état. Chaque requête est journalisée avec son code pour les métriques.01 / Relationnel
Tables, clés, relations
Pourquoi trois tables et pas une ?
Une seule table « mesure » avec les colonnes robot_nom, robot_modele, capteur_type… répéterait « R2, explorateur, ultrason, cm » 200 fois : gaspillage, et si le robot est renommé, il faut modifier 200 lignes (risque d’incohérence). La normalisation : chaque fait est stocké une fois, les tables se référencent par des clés. La clé primaire identifie une ligne ; la clé étrangère pointe vers la clé primaire d’une autre table. SQLite est une vraie base SQL dans un fichier, présente dans chaque téléphone et navigateur ; PostgreSQL et MySQL ajoutent le réseau et la concurrence.
01 / Relationnel
SELECT : filtrer, trier, agréger
L’ordre logique d’un SELECT : FROM (tables) → WHERE (filtrer les lignes) → GROUP BY (regrouper) → HAVING (filtrer les groupes) → SELECT (colonnes calculées) → ORDER BY → LIMIT. Écrire dans cet ordre mental évite 90 % des erreurs.
01 / Relationnel
JOIN : recoller les tables
Les jointures en une image
JOIN (interne) : seulement les lignes qui ont une correspondance des deux côtés. LEFT JOIN : toutes les lignes de gauche, avec NULL à droite si rien ne correspond. Une jointure est mathématiquement un produit cartésien filtré ; le moteur SQL choisit l’algorithme (boucles imbriquées, hachage, tri-fusion — les mêmes que dans L03-L04) selon les index et les statistiques. Ce planificateur de requêtes est l’un des logiciels les plus sophistiqués qui existent : vous dites quoi, il décide comment. C’est le paradigme déclaratif.
01 / Relationnel
Index, transactions, EXPLAIN : ce qui fait la différence à l’échelle
ACID et index
Un index est un arbre B (cousin de l’AVL du module L03) sur une ou plusieurs colonnes : la recherche passe de O(n) (« SCAN ») à O(log n) (« SEARCH USING INDEX »). Il coûte de l’espace et ralentit un peu les insertions : on indexe les colonnes des WHERE et JOIN fréquents, pas tout. Une transaction garantit ACID : Atomicité (tout ou rien), Cohérence (les contraintes tiennent), Isolation (les transactions concurrentes ne se voient pas à moitié), Durabilité (une fois validé, c’est sur le disque même si le courant coupe). Un journal de robot écrit par transactions de 100 mesures survit à un arrêt brutal.
02 / Web
HTTP : une requête, une réponse, du texte
GET /api/robots/1/mesures?depuis=120&limite=3 HTTP/1.1
Host: robot.local
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 142
{"robot": "R2", "mesures": [{"t": 120.0, "valeur": 61.2}, {"t": 120.1, "valeur": 60.8}, {"t": 120.2, "valeur": 59.9}]}Méthodes, codes, en-têtes
| Méthode | Sens | Exemple |
|---|---|---|
| GET | Lire (sans effet) | GET /api/mesures |
| POST | Créer / envoyer | POST /api/commandes avec un corps JSON |
| PUT / PATCH | Remplacer / modifier | PATCH /api/robots/1 |
| DELETE | Supprimer | DELETE /api/mesures/42 |
Codes : 200 OK, 201 créé, 400 requête invalide, 401/403 non autorisé, 404 introuvable, 500 erreur serveur. HTTPS = HTTP chiffré par TLS : sans lui, tout ce qui transite (mots de passe compris) est lisible par le réseau. Une API REST organise les URL par ressources (noms) et les actions par méthodes (verbes).
02 / Web
Un serveur d’API pour le robot (Flask), et son client
# serveur.py — sur le Raspberry Pi du robot : pip install flask
from flask import Flask, jsonify, request
import sqlite3
app = Flask(__name__)
def db(): return sqlite3.connect("robot.db")
@app.get("/api/mesures")
def mesures():
capteur = request.args.get("capteur", type=int)
limite = request.args.get("limite", 100, type=int)
with db() as c:
lignes = c.execute("SELECT t, valeur FROM mesure WHERE capteur_id = ? ORDER BY t DESC LIMIT ?",
(capteur, limite)).fetchall()
return jsonify([{"t": t, "valeur": v} for t, v in lignes])
@app.post("/api/commandes")
def commande():
corps = request.get_json()
if corps.get("action") not in ("avancer", "stop", "tourner"):
return jsonify({"erreur": "action inconnue"}), 400
file_commandes.put(corps) # traitée par le thread de contrôle (séance 20)
return jsonify({"ok": True}), 201
app.run(host="0.0.0.0", port=8080)# client.py — sur le PC : pip install requests
import requests
r = requests.get("http://robot.local:8080/api/mesures", params={"capteur": 1, "limite": 5})
print(r.status_code, r.json())
r = requests.post("http://robot.local:8080/api/commandes", json={"action": "avancer", "vitesse": 0.4})
print(r.status_code, r.json())<!-- tableau de bord : une page HTML qui interroge l'API toutes les secondes -->
<script>
setInterval(async () => {
const r = await fetch("/api/mesures?capteur=1&limite=1");
const [m] = await r.json();
document.querySelector("#distance").textContent = m.valeur.toFixed(1) + " cm";
}, 1000);
</script>La séance 26 faisait cela par port série ; ici le robot est un serveur web : n’importe quel appareil du réseau (téléphone, autre robot) peut lire ses données et lui envoyer des ordres. FastAPI est l’alternative moderne à Flask (types + documentation automatique).
02 / Web
Simuler client et serveur dans la page
Le décorateur @route est exactement ce que fait Flask : un registre (patron Fabrique, L02) qui associe (méthode, chemin) → fonction. Le vrai serveur ajoute la socket réseau, l’analyse du texte HTTP et la concurrence.
03 / Sécurité
Les erreurs qui coûtent cher
| Faille | Cause | Parade |
|---|---|---|
| Injection SQL | Concaténer l’entrée utilisateur dans la requête | Requêtes paramétrées (?) |
| XSS | Afficher du texte utilisateur comme HTML | Échapper (html.escape) ; les frameworks le font par défaut |
| Mots de passe en clair | Stocker le mot de passe | Stocker un hachage lent et salé (bcrypt, argon2) ; jamais SHA-1/MD5 |
| Secrets dans Git | Clé d’API dans le code | Variables d’environnement, fichier .env ignoré |
| Robot sur Internet sans authentification | « C’est juste un test » | Réseau local, jeton d’API, HTTPS |
| Débordement de tampon (C) | Lire une trame réseau dans un tableau fixe sans vérifier | Toujours borner (strncpy, vérifier les longueurs) — module L18 |
Cours
Cours 1 — Modéliser : entités, relations, cardinalités, formes normales
Méthode. 1) Lister les entités (noms de l’énoncé : robot, capteur, mesure, mission) et leurs attributs ; 2) les relations avec leur cardinalité : un robot a plusieurs capteurs (1–N : clé étrangère côté N), un robot participe à plusieurs missions et une mission implique plusieurs robots (N–N : table de liaison participation(robot_id, mission_id, …)) ; 3) choisir les clés primaires (entier auto-incrémenté, ou clé naturelle unique) ; 4) normaliser.
| Forme normale | Règle | Violation typique | Correction |
|---|---|---|---|
| 1NF | Chaque cellule contient une valeur atomique | Colonne capteurs = "ultrason,ir,imu" | Table capteur, une ligne par capteur |
| 2NF | Tout attribut dépend de toute la clé | Dans participation(robot_id, mission_id, nom_robot) | nom_robot va dans robot |
| 3NF | Aucun attribut ne dépend d’un attribut non-clé | mesure(capteur_id, unite) alors que l’unité dépend du type de capteur | unite dans capteur (ou type_capteur) |
Pourquoi normaliser : éviter les anomalies de mise à jour (changer un nom en 200 endroits), d’insertion (impossible d’ajouter un capteur sans mesure) et de suppression (supprimer la dernière mesure fait disparaître le capteur). Quand dénormaliser : pour la lecture massive (entrepôts de données, tableaux de bord) on recopie volontairement, en acceptant la redondance contrôlée.
Cours
Cours 2 — Algèbre relationnelle : les cinq opérations derrière SQL
| Opération | Notation | SQL | Résultat |
|---|---|---|---|
| Sélection | σcondition(R) | WHERE | Les lignes qui vérifient la condition |
| Projection | πcolonnes(R) | SELECT a, b (+ DISTINCT pour la vraie projection ensembliste) | Certaines colonnes |
| Produit cartésien | R × S | FROM R, S ou CROSS JOIN | Toutes les paires de lignes |
| Jointure | R ⋈cond S = σcond(R × S) | JOIN … ON | Paires compatibles |
| Union / différence / intersection | ∪, −, ∩ | UNION, EXCEPT, INTERSECT | Sur des tables de même schéma |
| Agrégation | γgroupe ; f(R) | GROUP BY + COUNT/SUM/AVG/MIN/MAX | Une ligne par groupe |
Le sens d’une requête, c’est sa traduction. « Les robots qui n’ont participé à aucune mission » = πnom(robot) − πnom(robot ⋈ participation). En SQL : SELECT nom FROM robot EXCEPT SELECT r.nom FROM robot r JOIN participation p ON p.robot_id = r.id, ou avec NOT EXISTS, ou LEFT JOIN … WHERE p.robot_id IS NULL : trois écritures, un seul sens. Savoir passer de l’une à l’autre est ce qu’on attend en NSI et en entretien.
Cours
Cours 3 — HTTP en détail : requête, réponse, état, cache, authentification
POST /api/commandes HTTP/1.1
Host: robot.local:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOi... ← jeton : prouve qui envoie
Content-Length: 34
{"action": "avancer", "vitesse": 0.4}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/commandes/57 ← où est la ressource créée
Cache-Control: no-store ← ne jamais mettre en cache une commande
{"id": 57, "ok": true}| Notion | Ce qu’il faut savoir |
|---|---|
| Sans état | Chaque requête est indépendante ; le serveur ne se souvient de rien. L’« état » (session) est porté par un cookie ou un jeton envoyé à chaque fois. |
| Idempotence | GET, PUT, DELETE peuvent être répétés sans effet supplémentaire ; POST non. Un client qui rejoue un POST après un timeout peut créer un doublon : d’où les clés d’idempotence. |
| Codes | 2xx succès, 3xx redirection, 4xx erreur du client (400 mal formé, 401 non authentifié, 403 interdit, 404 introuvable, 429 trop de requêtes), 5xx erreur du serveur. |
| Cache | Cache-Control: max-age=60 : le navigateur ne redemande pas pendant 60 s. C’est pourquoi ce site ajoute ?v=… aux fichiers modifiés. |
| CORS | Un navigateur refuse qu’une page de A appelle l’API de B sauf si B l’autorise (Access-Control-Allow-Origin). Ce n’est pas une protection du serveur, c’est une protection de l’utilisateur. |
| WebSocket | Connexion bidirectionnelle persistante pour le temps réel (télémétrie, chat) ; SSE pour du serveur → client seulement. |
TP guidé
TP — Base de données, API et tableau de bord d’une flotte (sur PC, 3 h)
- Schéma. Fichier
schema.sql: robot, capteur, mission, participation, mesure (avec index sur (capteur_id, t)).sqlite3 flotte.db < schema.sql. Chargez 100 000 mesures synthétiques avec un script Python (executemanydans une transaction) ; chronométrez avec et sans transaction. - Requêtes. Fichier
requetes.sqlavec 10 requêtes commentées : dernière mesure de chaque capteur (sous-requête corrélée puis fenêtreROW_NUMBER() OVER (PARTITION BY …)), moyenne horaire, robots sans mission, mission la plus longue, top 3 des capteurs les plus bruyants (écart-type). Pour chacune :EXPLAIN QUERY PLANavant/après index, temps mesuré. - API.
pip install fastapi uvicorn;api.pyavecGET /robots,GET /robots/{id}/mesures?capteur=&depuis=&limite=,POST /commandes(validation par un modèle Pydantic : action ∈ {avancer, stop, tourner}, vitesse ∈ [−1, 1]).uvicorn api:app --reload; documentation automatique sur/docs. Testez aveccurlet avecrequests. Requêtes paramétrées uniquement. - Tableau de bord.
index.htmlservi par l’API (StaticFiles) : un tableau des robots, un graphique (Chart.js via CDN) de la dernière heure d’un capteur, rafraîchi toutes les 2 s parfetch. Puis une version WebSocket (@app.websocket) qui pousse chaque nouvelle mesure. - Sécurité. Ajoutez un jeton d’API (en-tête
Authorization) obligatoire sur POST ; stockez-le haché dans la base ; tentez une injection sur un paramètre : elle doit échouer. Lancezpip install bandit && bandit -r .et corrigez ce qui est signalé. - Livrable. Dépôt (schema.sql, requetes.sql avec plans et temps, api.py, tests
pytestviaTestClient, index.html), capture du tableau de bord.
Exercices
Exercices auto-corrigés — SQL
Exercice 1 — Écrire les requêtes
Sur la base c2 (robot, mission, participation) ci-dessus, complétez les chaînes SQL. La cellule exécute et compare.
Correction
R1 = "SELECT r.nom FROM robot r JOIN participation p ON p.robot_id = r.id GROUP BY r.id HAVING COUNT(*) >= 2"
R2 = "SELECT m.nom, COUNT(p.robot_id) FROM mission m LEFT JOIN participation p ON p.mission_id = m.id GROUP BY m.id ORDER BY m.nom"
R3 = "SELECT r.nom, m.nom FROM participation p JOIN robot r ON r.id = p.robot_id JOIN mission m ON m.id = p.mission_id WHERE p.role IN ('explorateur', 'vedette') ORDER BY r.nom, m.nom"Dans R2, COUNT(p.robot_id) (et non COUNT(*)) compte 0 pour la mission sans participant : COUNT(colonne) ignore les NULL.
Exercice 2 — Transactions et contraintes
Écrivez transferer_role(cur2, robot_a, robot_b, mission_id) qui donne à B le rôle de A sur la mission et retire A, de façon atomique (tout ou rien) : si B n’existe pas ou si A n’est pas sur la mission, rien ne change et une exception est levée.
Correction
def transferer_role(cur2, a, b, m):
cur2.execute("SAVEPOINT t")
try:
role = cur2.execute("SELECT role FROM participation WHERE robot_id = ? AND mission_id = ?", (a, m)).fetchone()
if role is None or cur2.execute("SELECT 1 FROM robot WHERE id = ?", (b,)).fetchone() is None:
raise ValueError("transfert impossible")
cur2.execute("DELETE FROM participation WHERE robot_id = ? AND mission_id = ?", (a, m))
cur2.execute("INSERT INTO participation VALUES (?, ?, ?)", (b, m, role[0]))
cur2.execute("RELEASE t")
except Exception:
cur2.execute("ROLLBACK TO t"); cur2.execute("RELEASE t"); raiseExercices
Exercices auto-corrigés — HTTP et JSON
Exercice 3 — Analyser une requête HTTP brute
Écrivez analyser(texte) qui découpe une requête HTTP (ligne de requête, en-têtes, corps) en dictionnaire : methode, chemin, params (query string décodée), entetes (clés en minuscules), corps (JSON décodé si content-type est application/json, sinon texte).
Correction
def analyser(texte):
tete, _, corps = texte.partition("\r\n\r\n")
lignes = tete.split("\r\n"); methode, cible, _ = lignes[0].split(" ")
u = urlparse(cible); entetes = {}
for l in lignes[1:]:
k, _, v = l.partition(":"); entetes[k.strip().lower()] = v.strip()
if entetes.get("content-type", "").startswith("application/json") and corps: corps = json.loads(corps)
return {"methode": methode, "chemin": u.path, "params": parse_qs(u.query), "entetes": entetes, "corps": corps}Exercice 4 — Valider un JSON de commande
valider(commande) renvoie une liste d’erreurs (vide si valide) : action obligatoire ∈ {avancer, stop, tourner} ; vitesse nombre dans [−1, 1] obligatoire pour avancer ; angle nombre dans [−180, 180] obligatoire pour tourner ; toute clé inconnue est une erreur. Message : "champ: raison".
Correction
def valider(c):
err = []; connues = {"action", "vitesse", "angle"}
err += [f"{k}: inconnu" for k in c if k not in connues]
a = c.get("action")
if a not in ("avancer", "stop", "tourner"): err.append("action: obligatoire, parmi avancer/stop/tourner")
def nombre(champ, lo, hi):
if champ not in c: err.append(f"{champ}: obligatoire")
elif not isinstance(c[champ], (int, float)) or not lo <= c[champ] <= hi: err.append(f"{champ}: nombre entre {lo} et {hi} attendu")
if a == "avancer": nombre("vitesse", -1, 1)
if a == "tourner": nombre("angle", -180, 180)
return err04 / Défis
Défi ★ — Le journal de bord en SQL
Consigne
Sur la base du module : 1) la mesure maximale de chaque capteur avec son instant ; 2) pour chaque robot, le nombre de mesures d’ultrason inférieures à 20 cm (obstacles proches) ; 3) les 3 secondes (entières) où la température moyenne était la plus haute ; 4) un rapport « robot, type, nb, moyenne, écart-type » (l’écart-type se calcule avec AVG(valeur*valeur) − AVG(valeur)²).
Correction
q("""SELECT capteur_id, t, valeur FROM mesure m
WHERE valeur = (SELECT MAX(valeur) FROM mesure WHERE capteur_id = m.capteur_id)""")
q("""SELECT r.nom, COUNT(*) FROM mesure m JOIN capteur c ON m.capteur_id = c.id JOIN robot r ON c.robot_id = r.id
WHERE c.type = 'ultrason' AND m.valeur < 20 GROUP BY r.nom""")
q("""SELECT CAST(t AS INTEGER) s, ROUND(AVG(valeur), 2) FROM mesure m JOIN capteur c ON m.capteur_id = c.id
WHERE c.type = 'temperature' GROUP BY s ORDER BY 2 DESC LIMIT 3""")
q("""SELECT r.nom, c.type, COUNT(*), ROUND(AVG(valeur), 2),
ROUND(SQRT(AVG(valeur * valeur) - AVG(valeur) * AVG(valeur)), 2)
FROM mesure m JOIN capteur c ON m.capteur_id = c.id JOIN robot r ON c.robot_id = r.id
GROUP BY r.nom, c.type""")Si SQRT n’est pas disponible dans votre SQLite, calculez la variance en SQL et la racine en Python. La sous-requête corrélée de la question 1 est réévaluée par ligne : sur de gros volumes, préférez une jointure avec un GROUP BY.
04 / Défis
Défi ★★ — Concevoir le schéma d’une compétition de robots
Consigne
Équipes, robots (une équipe a plusieurs robots), épreuves (nom, date), participations (un robot dans une épreuve : temps, score, rang) et incidents (participation, instant, description). 1) Écrivez le schéma avec clés, contraintes NOT NULL/UNIQUE/CHECK (score ≥ 0). 2) Insérez des données de test (au moins 4 équipes, 6 robots, 3 épreuves). 3) Requêtes : classement général par équipe (somme des meilleurs scores par épreuve), robot avec le plus d’incidents, épreuve la plus disputée (écart-type des scores minimal). 4) Quel index accélère « les participations d’un robot » ? Vérifiez avec EXPLAIN.
Piste
CREATE TABLE participation (
id INTEGER PRIMARY KEY,
robot_id INTEGER NOT NULL REFERENCES robot(id),
epreuve_id INTEGER NOT NULL REFERENCES epreuve(id),
score REAL NOT NULL CHECK (score >= 0),
temps REAL,
UNIQUE (robot_id, epreuve_id) -- un robot participe une fois par épreuve
);
CREATE INDEX idx_part_robot ON participation (robot_id);Le classement « meilleur score par épreuve puis somme » demande une sous-requête groupée (meilleur score par (équipe, épreuve)) puis un GROUP BY équipe : deux niveaux d’agrégation, cas classique d’examen NSI.
04 / Défis
Défi ★★★ — Esprit prépa : un moteur de requêtes minimal
Consigne
Implémentez en Python pur une mini-algèbre relationnelle sur des listes de dictionnaires : selection(table, predicat), projection(table, colonnes), jointure(a, b, cle_a, cle_b) (version boucles imbriquées O(n·m) puis version par hachage O(n + m)), groupement(table, cle, agregats). Reproduisez la requête « robot, type, nb, moyenne » du module et vérifiez que le résultat est identique à SQLite. Mesurez les deux jointures sur 20 000 × 20 000 lignes. Question : pourquoi les bases de données choisissent-elles parfois quand même la boucle imbriquée ?
Correction (extrait)
def selection(t, p): return [l for l in t if p(l)]
def projection(t, cols): return [{c: l[c] for c in cols} for l in t]
def jointure_naive(a, b, ka, kb):
return [{**x, **{f"b.{k}": v for k, v in y.items()}} for x in a for y in b if x[ka] == y[kb]]
def jointure_hachage(a, b, ka, kb):
index = {}
for y in b: index.setdefault(y[kb], []).append(y)
return [{**x, **{f"b.{k}": v for k, v in y.items()}} for x in a for y in index.get(x[ka], [])]
def groupement(t, cle, agregats):
groupes = {}
for l in t: groupes.setdefault(l[cle], []).append(l)
return [{cle: k, **{nom: f(g) for nom, f in agregats.items()}} for k, g in groupes.items()]
import time, random
A = [dict(id=i, x=random.random()) for i in range(20_000)]
B = [dict(a_id=random.randrange(20_000), y=random.random()) for _ in range(20_000)]
t0 = time.perf_counter(); n1 = len(jointure_naive(A, B, "id", "a_id")); t1 = time.perf_counter()
n2 = len(jointure_hachage(A, B, "id", "a_id")); t2 = time.perf_counter()
print(n1 == n2, f"naïve {t1 - t0:.1f} s, hachage {t2 - t1:.2f} s")La jointure par hachage exige de tenir une table en mémoire ; si elle ne tient pas, ou si un index existe déjà sur la clé, ou si les tables sont minuscules, la boucle imbriquée (éventuellement avec index) gagne. Le planificateur choisit sur des estimations de taille — et se trompe parfois, d’où EXPLAIN.
05 / Vérification
Comment empêcher une injection SQL ?
Deux questions supplémentaires
1. Pourquoi une transaction accélère-t-elle 100× les insertions ? Sans elle, chaque INSERT est validé sur le disque (fsync) individuellement ; avec, une seule écriture durable à la fin.
2. Différence entre JOIN et LEFT JOIN ? Le LEFT JOIN garde les lignes de gauche sans correspondance (avec NULL), le JOIN les élimine.
Référence
Les mots à retenir
| Mot | Définition |
|---|---|
| Clé primaire / étrangère | Identifiant unique d’une ligne / référence vers une autre table. |
| Normalisation | Stocker chaque fait une seule fois. |
| JOIN | Combiner des tables par une condition. |
| GROUP BY / HAVING | Agréger par groupes / filtrer les groupes. |
| Index | Arbre B sur une colonne : recherche O(log n). |
| Transaction / ACID | Ensemble d’opérations tout-ou-rien, durable. |
| Injection | Entrée interprétée comme du code ; parade : paramètres. |
| HTTP | Protocole requête/réponse du web ; méthodes GET/POST… |
| JSON | Format texte d’échange de données structurées. |
| API REST | Interface HTTP organisée en ressources. |
| Hachage salé | Stockage sûr des mots de passe. |
Pour continuer
Fin de la partie F : vous êtes un développeur
Partie G : les mathématiques de l’informatique. Module suivant : logique et preuves de programmes — invariants, terminaison, correction : prouver qu’un algorithme est juste, pas seulement le tester.
À faire chez soi
- Sur PC : installer Flask, faire tourner le serveur d’API avec la base SQLite du robot, et le tableau de bord HTML.
- Refaire les exercices SQL du bac NSI (sujets 2021-2024, « bases de données »).
- Lire le chapitre 2 de Designing Data-Intensive Applications (modèles de données).