{
  "chapter": {
    "id": "bases-de-donnees",
    "level": "terminale",
    "theme": "Bases de données",
    "title": "Bases de données et SQL",
    "description": "Modèle relationnel (tables, attributs, clés primaires\net étrangères), contraintes d'intégrité, anomalies dans\nun schéma, langage SQL (SELECT, FROM, WHERE, JOIN,\nINSERT, UPDATE, DELETE), services rendus par un SGBD.",
    "prerequisites": [],
    "references": []
  },
  "questions": [
    {
      "id": "q01",
      "difficulty": 1,
      "skills": [
        "definition",
        "sgbd"
      ],
      "title": "Définition d'un SGBD",
      "statement": "Que désigne le sigle SGBD ?",
      "options": [
        {
          "text": "Stockage Garanti des Bases Distribuées",
          "correct": false,
          "feedback": "Erreur : sigle inventé.\n"
        },
        {
          "text": "Système de Génération Binaire de Données",
          "correct": false,
          "feedback": "Erreur : sigle inventé. SGBD est lié à la **gestion**\ndes bases de données.\n"
        },
        {
          "text": "Standard Général des Bibliothèques de Données",
          "correct": false,
          "feedback": "Erreur : sigle inventé.\n"
        },
        {
          "text": "Système de Gestion de Base de Données",
          "correct": true,
          "feedback": "Bonne réponse : un SGBD est un logiciel qui permet de\nstocker, organiser, interroger et sécuriser des\ndonnées. Exemples : MySQL, PostgreSQL, SQLite, Oracle.\n"
        }
      ],
      "explanation": "Un SGBD relationnel (SGBDR) ajoute la gestion du modèle\nrelationnel et du langage SQL. Tous les exemples cités\nci-dessus sont des SGBDR."
    },
    {
      "id": "q02",
      "difficulty": 1,
      "skills": [
        "modele-relationnel",
        "vocabulaire"
      ],
      "title": "Vocabulaire du modèle relationnel",
      "statement": "Dans le modèle relationnel, comment appelle-t-on **une ligne**\nd'une table ?",
      "options": [
        {
          "text": "Un attribut",
          "correct": false,
          "feedback": "Erreur : un attribut est une **colonne** (un champ),\npas une ligne.\n"
        },
        {
          "text": "Un n-uplet",
          "correct": true,
          "feedback": "Bonne réponse : une ligne d'une table relationnelle est\nun n-uplet (en français) ou *tuple* (en anglais), aussi\nappelé enregistrement. Elle représente une instance\nd'entité.\n"
        },
        {
          "text": "Une clé",
          "correct": false,
          "feedback": "Erreur : une clé est une combinaison d'attributs qui\nidentifie de manière unique chaque ligne.\n"
        },
        {
          "text": "Un domaine",
          "correct": false,
          "feedback": "Erreur : un domaine est l'ensemble des valeurs possibles\npour un attribut (par exemple, les entiers naturels\npour l'âge).\n"
        }
      ],
      "explanation": "Vocabulaire du modèle relationnel : **table** (relation),\n**ligne** (n-uplet, enregistrement), **colonne** (attribut),\n**domaine** (type des valeurs), **clé primaire** (identifiant\nunique)."
    },
    {
      "id": "q03",
      "difficulty": 1,
      "skills": [
        "cle-primaire"
      ],
      "title": "Clé primaire",
      "statement": "Quelle est la propriété fondamentale d'une **clé primaire** ?",
      "options": [
        {
          "text": "Elle identifie de façon unique chaque ligne",
          "correct": true,
          "feedback": "Bonne réponse : la clé primaire est par définition\nunique dans la table. Deux lignes ne peuvent jamais\navoir la même valeur de clé primaire.\n"
        },
        {
          "text": "Elle doit obligatoirement être un nombre entier positif",
          "correct": false,
          "feedback": "Erreur : une clé primaire peut être de n'importe quel\ntype (chaîne, code-barre, identifiant composite). Le\ntype entier est seulement le plus courant.\n"
        },
        {
          "text": "Elle peut prendre la valeur `NULL`",
          "correct": false,
          "feedback": "Erreur : une clé primaire ne peut **jamais** être\n`NULL`, sinon elle ne pourrait plus identifier une\nligne de manière unique.\n"
        },
        {
          "text": "Elle doit toujours être une seule colonne",
          "correct": false,
          "feedback": "Erreur : une clé primaire peut être **composite** (sur\nplusieurs colonnes). Par exemple, dans une table\nd'inscriptions, le couple `(eleve_id, cours_id)` peut\nêtre une clé primaire composite.\n"
        }
      ],
      "explanation": "Une clé primaire est : (1) unique, (2) non nulle, (3) stable\ndans le temps. Elle peut être composite (composée de\nplusieurs colonnes) si aucune colonne unique ne suffit."
    },
    {
      "id": "q04",
      "difficulty": 1,
      "skills": [
        "cle-etrangere"
      ],
      "title": "Clé étrangère",
      "statement": "Que représente une **clé étrangère** dans une table ?",
      "options": [
        {
          "text": "Un attribut référençant la clé primaire d'une autre table",
          "correct": true,
          "feedback": "Bonne réponse : la clé étrangère établit un lien\nrelationnel entre deux tables. Par exemple, dans une\ntable `Commandes`, l'attribut `client_id` référence\nla clé primaire de la table `Clients`.\n"
        },
        {
          "text": "Une clé dont la valeur ne peut jamais être modifiée ensuite",
          "correct": false,
          "feedback": "Erreur : la modification est possible. Le terme\n« étrangère » concerne la référence vers une autre\ntable.\n"
        },
        {
          "text": "Une clé importée d'un autre fichier",
          "correct": false,
          "feedback": "Erreur : « étrangère » ne signifie pas « importée\nd'un fichier ». Elle référence simplement une autre\n**table** dans la même base.\n"
        },
        {
          "text": "Un attribut crypté pour la sécurité",
          "correct": false,
          "feedback": "Erreur : aucune notion de chiffrement dans le concept\nde clé étrangère. C'est une notion structurelle, pas\nde sécurité.\n"
        }
      ],
      "explanation": "Les clés étrangères implémentent les **relations** entre\ntables et permettent au SGBD de garantir l'**intégrité référentielle** : on ne peut pas créer une commande pour\nun client inexistant."
    },
    {
      "id": "q05",
      "difficulty": 1,
      "skills": [
        "sql",
        "select"
      ],
      "title": "Syntaxe de base de SELECT",
      "statement": "Quelle est la syntaxe SQL pour récupérer **toutes** les\ncolonnes de la table `eleves` ?",
      "options": [
        {
          "text": "`SELECT eleves`",
          "correct": false,
          "feedback": "Erreur : la syntaxe `SELECT` requiert au minimum la\nliste des colonnes (ou `*`) puis `FROM` pour la table.\n"
        },
        {
          "text": "`GET ALL FROM eleves`",
          "correct": false,
          "feedback": "Erreur : `GET` n'existe pas en SQL. La commande de\nlecture est `SELECT`.\n"
        },
        {
          "text": "`SELECT * FROM eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `*` est le joker qui désigne toutes\nles colonnes. `FROM` indique la table source.\n"
        },
        {
          "text": "`READ eleves`",
          "correct": false,
          "feedback": "Erreur : `READ` n'est pas un mot-clé SQL. La lecture\nse fait avec `SELECT`.\n"
        }
      ],
      "explanation": "Bonne pratique : éviter `SELECT *` en production et lister\nexplicitement les colonnes nécessaires. Cela rend le code\nplus robuste aux évolutions de schéma et plus clair."
    },
    {
      "id": "q06",
      "difficulty": 1,
      "skills": [
        "sql",
        "where"
      ],
      "title": "Filtrage avec WHERE",
      "statement": "Pour ne sélectionner que les élèves majeurs de la table\n`eleves`, quelle requête utiliser ?",
      "options": [
        {
          "text": "`FILTER eleves WHERE age >= 18`",
          "correct": false,
          "feedback": "Erreur : `FILTER` n'est pas la syntaxe SQL standard.\nOn utilise `SELECT ... WHERE`.\n"
        },
        {
          "text": "`SELECT * FROM eleves CONDITION age >= 18`",
          "correct": false,
          "feedback": "Erreur : `CONDITION` n'existe pas en SQL. C'est `WHERE`\nqui introduit la condition.\n"
        },
        {
          "text": "`SELECT * FROM eleves WHERE age >= 18`",
          "correct": true,
          "feedback": "Bonne réponse : `WHERE` introduit la condition de\nfiltrage. La syntaxe est `WHERE condition`, où\ncondition utilise les opérateurs SQL standard\n(`=`, `<`, `>`, `<=`, `>=`, `<>`, `LIKE`, etc.).\n"
        },
        {
          "text": "`SELECT * FROM eleves IF age >= 18`",
          "correct": false,
          "feedback": "Erreur : `IF` n'est pas un mot-clé SQL pour le\nfiltrage. Le filtre se fait avec `WHERE`.\n"
        }
      ],
      "explanation": "Une requête SQL classique a la forme :\n`SELECT colonnes FROM table WHERE condition`. Les\nconditions multiples se combinent avec `AND`, `OR`, `NOT`."
    },
    {
      "id": "q07",
      "difficulty": 1,
      "skills": [
        "sql",
        "ordre"
      ],
      "title": "Tri avec ORDER BY",
      "statement": "Pour afficher les élèves par ordre alphabétique de leur\nnom, on utilise :",
      "options": [
        {
          "text": "`SELECT * FROM eleves ARRANGE BY nom`",
          "correct": false,
          "feedback": "Erreur : `ARRANGE BY` n'est pas du SQL valide.\n"
        },
        {
          "text": "`SELECT * FROM eleves ORDER BY nom`",
          "correct": true,
          "feedback": "Bonne réponse : `ORDER BY` trie le résultat selon une\nou plusieurs colonnes. Par défaut, l'ordre est\nascendant (`ASC`). Pour un ordre descendant, ajouter\n`DESC`.\n"
        },
        {
          "text": "`SELECT * FROM eleves SORTED nom`",
          "correct": false,
          "feedback": "Erreur : `SORTED` n'existe pas en SQL.\n"
        },
        {
          "text": "`SELECT * FROM eleves SORT BY nom`",
          "correct": false,
          "feedback": "Erreur : `SORT BY` n'est pas une syntaxe SQL. Le tri\nse fait avec `ORDER BY`.\n"
        }
      ],
      "explanation": "Pour trier par nom puis par prénom : `ORDER BY nom, prenom`.\nPour ordre décroissant : `ORDER BY nom DESC`. On peut\nmélanger : `ORDER BY classe ASC, moyenne DESC`."
    },
    {
      "id": "q08",
      "difficulty": 1,
      "skills": [
        "contrainte"
      ],
      "title": "Contraintes d'intégrité",
      "statement": "Qu'est-ce qu'une **contrainte d'intégrité** dans une base de\ndonnées relationnelle ?",
      "options": [
        {
          "text": "Un mot de passe pour accéder à une table",
          "correct": false,
          "feedback": "Erreur : aucun rapport avec l'authentification. Les\ncontraintes d'intégrité concernent la cohérence des\ndonnées, pas la sécurité d'accès.\n"
        },
        {
          "text": "Une règle restreignant les valeurs admissibles, vérifiée par le SGBD",
          "correct": true,
          "feedback": "Bonne réponse : les contraintes garantissent la\ncohérence des données. Exemples : `NOT NULL` (valeur\nobligatoire), `UNIQUE` (pas de doublon), clé primaire,\nclé étrangère, `CHECK` (condition arbitraire).\n"
        },
        {
          "text": "Une copie de sauvegarde automatique et régulière des données",
          "correct": false,
          "feedback": "Erreur : aucun rapport avec les sauvegardes.\n"
        },
        {
          "text": "Un index pour accélérer les requêtes",
          "correct": false,
          "feedback": "Erreur : les index sont des structures auxiliaires\npour la performance, pas des règles de cohérence.\n"
        }
      ],
      "explanation": "Les contraintes d'intégrité sont **vérifiées automatiquement** par le SGBD à chaque modification. Si\nune opération les violerait, elle est rejetée. C'est une\ngarantie forte de cohérence."
    },
    {
      "id": "q09",
      "difficulty": 1,
      "skills": [
        "sql",
        "insert"
      ],
      "title": "Insertion d'une ligne",
      "statement": "Quelle commande SQL ajoute une ligne dans une table ?",
      "options": [
        {
          "text": "`INSERT INTO eleves VALUES ('Alice', 17)`",
          "correct": true,
          "feedback": "Bonne réponse : `INSERT INTO table VALUES (...)` est la\nforme classique. On peut aussi préciser les colonnes :\n`INSERT INTO eleves (nom, age) VALUES ('Alice', 17)`.\n"
        },
        {
          "text": "`CREATE INTO eleves VALUES ('Alice', 17)`",
          "correct": false,
          "feedback": "Erreur : `CREATE` est utilisé pour créer une table\n(`CREATE TABLE`), pas pour insérer une ligne.\n"
        },
        {
          "text": "`PUT INTO eleves VALUES ('Alice', 17)`",
          "correct": false,
          "feedback": "Erreur : `PUT` n'existe pas en SQL.\n"
        },
        {
          "text": "`ADD INTO eleves VALUES ('Alice', 17)`",
          "correct": false,
          "feedback": "Erreur : `ADD` n'est pas la syntaxe SQL standard pour\najouter une ligne.\n"
        }
      ],
      "explanation": "Les quatre commandes principales du SQL DML (Data\nManipulation Language) sont : `SELECT` (lire),\n`INSERT` (créer), `UPDATE` (modifier), `DELETE` (supprimer)."
    },
    {
      "id": "q10",
      "difficulty": 1,
      "skills": [
        "anomalie",
        "atomique"
      ],
      "title": "Valeurs atomiques",
      "statement": "Dans une base relationnelle correctement conçue, on\nattend que chaque attribut contienne une valeur\n**atomique**. Que signifie cette propriété ?",
      "options": [
        {
          "text": "La valeur doit impérativement être un nombre entier",
          "correct": false,
          "feedback": "Erreur : aucune contrainte de type. Une valeur\natomique peut être une chaîne, un booléen, un\nflottant…\n"
        },
        {
          "text": "La valeur ne se décompose pas en plusieurs informations",
          "correct": true,
          "feedback": "Bonne réponse : un attribut comme\n`telephones = \"06 12 ; 06 34\"` mélange deux\ninformations dans une cellule, ce qui rend les\nrequêtes difficiles. Il faut alors créer une\nseconde table reliée par une clé étrangère.\n"
        },
        {
          "text": "La valeur ne peut jamais être modifiée",
          "correct": false,
          "feedback": "Erreur : « atomique » ne signifie pas\n« immuable ». Les valeurs peuvent évidemment\nêtre mises à jour avec `UPDATE`.\n"
        },
        {
          "text": "La valeur ne peut jamais être nulle (`NULL` interdit)",
          "correct": false,
          "feedback": "Erreur : aucun rapport. L'absence de valeur est\ngérée par `NULL` ou par la contrainte\n`NOT NULL`.\n"
        }
      ],
      "explanation": "Stocker des valeurs non atomiques (listes, couples,\ntableaux) dans une cellule est une anomalie\nfréquente. Cela empêche de filtrer ou de joindre\nfacilement la donnée et peut générer des\nredondances et des incohérences."
    },
    {
      "id": "q11",
      "difficulty": 2,
      "skills": [
        "sql",
        "jointure"
      ],
      "title": "Jointure entre deux tables",
      "statement": "Pour combiner les tables `commandes` et `clients` en\nreliant `commandes.client_id` à `clients.id`, quelle\nsyntaxe utiliser ?",
      "options": [
        {
          "text": "`SELECT * FROM commandes JOIN clients ON commandes.client_id = clients.id`",
          "correct": true,
          "feedback": "Bonne réponse : c'est une jointure **interne** explicite.\nSeules les commandes ayant un client correspondant\napparaissent. Syntaxe préférée à la virgule depuis\nSQL-92.\n"
        },
        {
          "text": "`SELECT * FROM commandes, clients`",
          "correct": false,
          "feedback": "Erreur : cette syntaxe produit le **produit cartésien**\n(toutes les paires possibles), sans condition de\njointure. Le résultat est rarement ce qu'on souhaite.\n"
        },
        {
          "text": "`SELECT * FROM commandes INNER JOIN clients ON commandes.id = clients.id`",
          "correct": false,
          "feedback": "Erreur : la condition relie la clé primaire des\ncommandes à celle des clients, alors que le lien est\nporté par la clé étrangère `commandes.client_id`.\nChaque commande serait associée au client dont\nl'identifiant coïncide par hasard avec son propre\nnuméro, ce qui n'a aucun sens.\n"
        },
        {
          "text": "`SELECT * FROM commandes UNION SELECT * FROM clients`",
          "correct": false,
          "feedback": "Erreur : `UNION` empile les lignes de deux requêtes\nl'une après l'autre, et exige de plus le même nombre\nde colonnes de part et d'autre. Pour placer les\ncolonnes des deux tables côte à côte, c'est `JOIN`.\n"
        }
      ],
      "explanation": "Différents types de jointures : `INNER JOIN` (ne garde\nque les lignes appariées), `LEFT JOIN` (garde toutes les\nlignes de la table de gauche), `RIGHT JOIN` (symétrique),\n`FULL JOIN` (les deux côtés)."
    },
    {
      "id": "q12",
      "difficulty": 2,
      "skills": [
        "sql",
        "agregation"
      ],
      "title": "Fonctions d'agrégation",
      "statement": "Pour calculer la **moyenne des âges** des élèves, quelle\nrequête utiliser ?",
      "options": [
        {
          "text": "`SELECT MOYENNE(age) FROM eleves`",
          "correct": false,
          "feedback": "Erreur : SQL utilise des mots-clés en anglais. La\nversion française n'existe pas.\n"
        },
        {
          "text": "`SELECT (age) FROM eleves AVERAGE`",
          "correct": false,
          "feedback": "Erreur : la syntaxe `AVERAGE` après le `FROM` n'existe\npas. Les fonctions d'agrégation s'appliquent à\nl'intérieur du `SELECT`.\n"
        },
        {
          "text": "`SELECT MEAN(age) FROM eleves`",
          "correct": false,
          "feedback": "Erreur : la fonction de moyenne en SQL standard\ns'appelle `AVG`, pas `MEAN`.\n"
        },
        {
          "text": "`SELECT AVG(age) FROM eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `AVG` (*average*) calcule la moyenne\narithmétique. Les fonctions d'agrégation principales\nsont `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`.\n"
        }
      ],
      "explanation": "Les fonctions d'agrégation principales en SQL sont\n`COUNT`, `SUM`, `AVG`, `MIN` et `MAX`. Elles\nsynthétisent une colonne entière en une seule\nvaleur dans le résultat de la requête."
    },
    {
      "id": "q13",
      "difficulty": 2,
      "skills": [
        "sql",
        "combinaison-clauses"
      ],
      "title": "Combiner WHERE et ORDER BY",
      "statement": "Quelle requête renvoie les noms des élèves majeurs,\nclassés par âge **décroissant** ?",
      "options": [
        {
          "text": "`SELECT nom FROM eleves ORDER BY age DESC WHERE age >= 18`",
          "correct": false,
          "feedback": "Erreur : l'ordre des clauses est imposé en SQL.\n`WHERE` se place avant `ORDER BY`.\n"
        },
        {
          "text": "`SELECT nom FROM eleves WHERE age >= 18 ORDER BY age DESC`",
          "correct": true,
          "feedback": "Bonne réponse : `WHERE` filtre les lignes\n(majeurs), puis `ORDER BY age DESC` les trie par\nâge décroissant.\n"
        },
        {
          "text": "`SELECT nom FROM eleves WHERE age >= 18 ORDER BY age`",
          "correct": false,
          "feedback": "Erreur : sans le mot-clé `DESC`, le tri se fait\npar défaut en ordre croissant.\n"
        },
        {
          "text": "`SELECT nom FROM eleves WHERE age >= 18 SORT age DESC`",
          "correct": false,
          "feedback": "Erreur : la clause `SORT` n'existe pas en SQL\nstandard. Il faut utiliser `ORDER BY`.\n"
        }
      ],
      "explanation": "L'ordre canonique des clauses dans une requête\nd'interrogation est : `SELECT` … `FROM` … `WHERE` …\n`ORDER BY` …. Le mot-clé `DESC` après `ORDER BY`\nindique un tri décroissant ; `ASC` (par défaut)\nindique un tri croissant."
    },
    {
      "id": "q14",
      "difficulty": 2,
      "skills": [
        "sql",
        "update"
      ],
      "title": "Modification d'une ligne",
      "statement": "Pour augmenter de $1$ point la note de l'élève d'identifiant\n$42$, quelle requête utiliser ?",
      "options": [
        {
          "text": "`MODIFY eleves SET note = note + 1 WHERE id = 42`",
          "correct": false,
          "feedback": "Erreur : `MODIFY` n'est pas un mot-clé SQL standard.\n"
        },
        {
          "text": "`UPDATE eleves SET note = note + 1 WHERE id = 42`",
          "correct": true,
          "feedback": "Bonne réponse : `UPDATE` met à jour des lignes\nexistantes. La clause `SET` indique les modifications\net `WHERE` filtre les lignes affectées.\n"
        },
        {
          "text": "`UPDATE eleves WHERE id = 42 SET note = note + 1`",
          "correct": false,
          "feedback": "Erreur : ordre incorrect des clauses. La syntaxe\nstandard est `UPDATE table SET ... WHERE ...`.\n"
        },
        {
          "text": "`CHANGE eleves SET note = note + 1 WHERE id = 42`",
          "correct": false,
          "feedback": "Erreur : `CHANGE` n'est pas un mot-clé SQL standard.\n"
        }
      ],
      "explanation": "Toujours penser à mettre une clause `WHERE` sur les\n`UPDATE` et `DELETE`. **Sans elle, la modification s'applique à toutes les lignes** de la table, ce qui est rarement ce\nqu'on veut et souvent catastrophique."
    },
    {
      "id": "q15",
      "difficulty": 2,
      "skills": [
        "sql",
        "distinct"
      ],
      "title": "Suppression de doublons",
      "statement": "Pour obtenir la liste des **classes différentes** présentes\ndans la table `eleves` (sans doublons), quelle requête\nutiliser ?",
      "options": [
        {
          "text": "`SELECT DISTINCT classe FROM eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `DISTINCT` élimine les doublons. Si\nla table contient $30$ élèves de la classe « 1NSI »\net $25$ de « TNSI », la requête renvoie deux lignes :\n« 1NSI » et « TNSI ».\n"
        },
        {
          "text": "`SELECT classe DISTINCT FROM eleves`",
          "correct": false,
          "feedback": "Erreur : `DISTINCT` se place **avant** la liste des\ncolonnes, juste après `SELECT`.\n"
        },
        {
          "text": "`SELECT UNIQUE classe FROM eleves`",
          "correct": false,
          "feedback": "Erreur : `UNIQUE` est utilisé pour les contraintes au\nmoment de la création de table, pas pour la sélection.\nLe mot-clé pour la sélection est `DISTINCT`.\n"
        },
        {
          "text": "`SELECT classe FROM eleves`",
          "correct": false,
          "feedback": "Erreur : sans `DISTINCT`, le résultat contiendra une\nentrée par élève, donc beaucoup de doublons.\n"
        }
      ],
      "explanation": "`DISTINCT` agit sur l'ensemble des colonnes\nsélectionnées. Par exemple,\n`SELECT DISTINCT nom, prenom FROM eleves` ne renverra\nqu'une ligne par couple (nom, prenom) distinct."
    },
    {
      "id": "q16",
      "difficulty": 2,
      "skills": [
        "association-multiple"
      ],
      "title": "Représenter une association multiple",
      "statement": "On souhaite représenter le fait qu'un élève peut\nsuivre **plusieurs cours** et qu'un cours peut\naccueillir **plusieurs élèves**. Quel schéma\nrelationnel adopter ?",
      "options": [
        {
          "text": "Deux tables avec une duplication des cours dans chaque ligne d'élève",
          "correct": false,
          "feedback": "Erreur : la duplication crée des redondances et\ndes anomalies de mise à jour si l'intitulé\nd'un cours change.\n"
        },
        {
          "text": "Une seule table `eleves` avec autant de colonnes `cours_1`, `cours_2`, `cours_3` qu'il y a de cours possibles",
          "correct": false,
          "feedback": "Erreur : ce schéma est rigide (il faut\nmodifier la structure si un nouveau cours\napparaît) et la plupart des cellules seront\nvides.\n"
        },
        {
          "text": "Une seule table contenant `eleve` et tous ses cours dans une même cellule",
          "correct": false,
          "feedback": "Erreur : on viole la propriété d'atomicité des\nattributs et on ne peut plus interroger\nfacilement les données.\n"
        },
        {
          "text": "Trois tables `eleves(id, nom)`, `cours(id, intitule)` et `inscriptions(eleve_id, cours_id)` reliées par des clés étrangères",
          "correct": true,
          "feedback": "Bonne réponse : on isole chaque entité dans sa\ntable et on crée une table **d'association**\ndont chaque ligne représente une inscription\nd'un élève à un cours.\n"
        }
      ],
      "explanation": "Une association « plusieurs à plusieurs » se\nreprésente par une table d'association qui contient\nles clés étrangères des deux entités liées. C'est un\npatron classique du modèle relationnel pour éviter\nles redondances."
    },
    {
      "id": "q18",
      "difficulty": 2,
      "skills": [
        "sql",
        "delete"
      ],
      "title": "Suppression d'une ligne",
      "statement": "Que fait la requête `DELETE FROM eleves;` ?",
      "options": [
        {
          "text": "Elle supprime toutes les lignes de la table `eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `DELETE` sans clause `WHERE` vide la\ntable (mais la structure reste). C'est l'erreur\nclassique en production. Pour supprimer la table\nentière (structure incluse), il faut `DROP TABLE eleves`.\n"
        },
        {
          "text": "Elle supprime la table entière (structure et contenu)",
          "correct": false,
          "feedback": "Erreur : c'est `DROP TABLE` qui supprime la structure.\n`DELETE FROM` ne touche que les lignes, pas le schéma.\n"
        },
        {
          "text": "Elle ne fait rien (manque de `WHERE`)",
          "correct": false,
          "feedback": "Erreur : sans `WHERE`, la requête s'applique à\n**toutes** les lignes. Elle est syntaxiquement valide.\n"
        },
        {
          "text": "Elle lève une erreur SQL",
          "correct": false,
          "feedback": "Erreur : la syntaxe est valide. Aucune erreur n'est\nlevée : c'est précisément le piège.\n"
        }
      ],
      "explanation": "Toujours utiliser `WHERE` avec `DELETE`, sauf intention\nexplicite de tout vider. Beaucoup de SGBD permettent\nd'imposer une clause `WHERE` obligatoire en mode\n« *safe updates* » pour éviter les catastrophes."
    },
    {
      "id": "q19",
      "difficulty": 2,
      "skills": [
        "sgbd",
        "services"
      ],
      "title": "Services rendus par un SGBD",
      "statement": "Lequel des éléments suivants n'est **pas** un\nservice rendu par un système de gestion de bases de\ndonnées ?",
      "options": [
        {
          "text": "La gestion des accès concurrents par plusieurs utilisateurs ou programmes",
          "correct": false,
          "feedback": "Erreur : c'est bien un service du SGBD, qui évite\nles incohérences quand plusieurs requêtes\nmodifient les mêmes données simultanément.\n"
        },
        {
          "text": "La persistance des données (les données survivent à l'arrêt du programme)",
          "correct": false,
          "feedback": "Erreur : c'est bien un service du SGBD, qui assure\nle stockage durable des données sur disque.\n"
        },
        {
          "text": "La compilation du code source d'un programme Python",
          "correct": true,
          "feedback": "Bonne réponse : la compilation de code Python\nn'a aucun rapport avec un SGBD. C'est le rôle de\nl'interpréteur Python ou d'un compilateur, pas\ndu gestionnaire de bases de données.\n"
        },
        {
          "text": "La sécurisation des accès (authentification, droits par utilisateur)",
          "correct": false,
          "feedback": "Erreur : c'est bien un service du SGBD, qui\ncontrôle qui peut lire ou modifier quoi.\n"
        }
      ],
      "explanation": "Les services principaux d'un SGBD sont la\npersistance, la gestion des accès concurrents,\nl'efficacité du traitement des requêtes et la\nsécurisation des accès. Le programme officiel\ndemande de les **identifier**, sans en détailler le\nfonctionnement interne."
    },
    {
      "id": "q20",
      "difficulty": 2,
      "skills": [
        "contrainte",
        "integrite"
      ],
      "title": "Intégrité référentielle",
      "statement": "Quelle situation viole l'**intégrité référentielle** ?",
      "options": [
        {
          "text": "Avoir deux clients avec le même nom",
          "correct": false,
          "feedback": "Erreur : ce n'est pas une violation tant que la clé\nprimaire (l'`id`) est unique. Le nom peut être en\ndouble.\n"
        },
        {
          "text": "Insérer une commande avec un `client_id` inexistant dans `clients`",
          "correct": true,
          "feedback": "Bonne réponse : la clé étrangère `client_id` doit\npointer vers une ligne existante de la table\n`clients`. Sinon, on a une référence orpheline,\nviolation typique de l'intégrité référentielle.\n"
        },
        {
          "text": "Insérer une commande sans préciser la quantité",
          "correct": false,
          "feedback": "Erreur : c'est une violation de contrainte `NOT NULL`,\npas d'intégrité référentielle. Cette dernière concerne\nles références entre tables.\n"
        },
        {
          "text": "Trier l'ensemble des commandes par leur date croissante",
          "correct": false,
          "feedback": "Erreur : un tri ne viole aucune contrainte.\n"
        }
      ],
      "explanation": "L'intégrité référentielle est garantie par les\ncontraintes `FOREIGN KEY`. À la suppression d'un client,\non peut configurer `ON DELETE CASCADE` (supprimer aussi\nles commandes), `ON DELETE RESTRICT` (interdire la\nsuppression s'il y a des commandes), etc."
    },
    {
      "id": "q22",
      "difficulty": 3,
      "skills": [
        "normalisation",
        "redondance"
      ],
      "title": "Détection d'une redondance",
      "statement": "Une table `commandes(id, client_id, nom_client, prenom_client, produit, prix)`\npose un problème classique de schéma. Lequel ?",
      "options": [
        {
          "text": "La clé primaire de la table est manifestement mal choisie",
          "correct": false,
          "feedback": "Erreur : `id` est un choix raisonnable de clé primaire\npour des commandes.\n"
        },
        {
          "text": "Les informations du client sont dupliquées à chaque commande",
          "correct": true,
          "feedback": "Bonne réponse : c'est une **redondance**\nclassique, source d'incohérences. Le nom et le prénom dépendent de\n`client_id`, pas de l'`id` de commande. Il\nfaudrait les mettre dans une table `clients`\nséparée et conserver uniquement la clé\nétrangère `client_id` dans `commandes`.\n"
        },
        {
          "text": "Le prix devrait être stocké dans une table `produits` séparée",
          "correct": false,
          "feedback": "Erreur : le prix peut varier d'une commande à l'autre\n(promotions, historiques) et a sa place dans `commandes`.\n"
        },
        {
          "text": "La table contient trop de colonnes pour être performante",
          "correct": false,
          "feedback": "Erreur : le nombre de colonnes n'est pas un problème\nen soi. Le problème est ailleurs.\n"
        }
      ],
      "explanation": "La normalisation vise à éliminer les redondances pour\ngarantir la **cohérence**. Si on stocke le nom du client\ndans chaque commande et que le client se marie, il faut\nmettre à jour toutes ses commandes, risqué et coûteux."
    },
    {
      "id": "q23",
      "difficulty": 3,
      "skills": [
        "sql",
        "jointure-externe",
        "approfondissement"
      ],
      "title": "Différence entre INNER et LEFT JOIN",
      "statement": "Quelle est la différence entre `INNER JOIN` et `LEFT JOIN` ?",
      "options": [
        {
          "text": "`INNER JOIN` ne garde que les paires appariées, `LEFT JOIN` toute la table de gauche",
          "correct": true,
          "feedback": "Bonne réponse : avec `LEFT JOIN`, les lignes de gauche\nsans correspondance sont complétées par `NULL`. Pour\nlister tous les clients avec leurs commandes (ou pas),\non utilise `LEFT JOIN` ; avec `INNER JOIN`, les clients\nsans commande disparaissent du résultat.\n"
        },
        {
          "text": "Aucune différence, ce sont en réalité deux synonymes stricts",
          "correct": false,
          "feedback": "Erreur : ils diffèrent sur la gestion des lignes non\nappariées de la table de gauche.\n"
        },
        {
          "text": "`LEFT JOIN` ne fonctionne que sur des tables triées",
          "correct": false,
          "feedback": "Erreur : aucune contrainte de tri n'est requise pour\nun `LEFT JOIN`.\n"
        },
        {
          "text": "`INNER JOIN` est simplement plus rapide que `LEFT JOIN`",
          "correct": false,
          "feedback": "Erreur : la performance dépend du SGBD et du contexte.\nLa différence sémantique est ce qui compte avant\ntout.\n"
        }
      ],
      "explanation": "Le `JOIN` simple (équivalent de `INNER JOIN`)\napparie les lignes selon la condition `ON`. Le\n`LEFT JOIN` étend ce comportement en conservant\naussi les lignes de gauche sans correspondance, en\nles complétant par des `NULL` côté droit."
    },
    {
      "id": "q24",
      "difficulty": 3,
      "skills": [
        "transaction",
        "isolation"
      ],
      "title": "Problème de concurrence",
      "statement": "Sans isolation des transactions, deux transactions\nsimultanées qui modifient le même compte bancaire peuvent\nprovoquer :",
      "options": [
        {
          "text": "Une perte de mise à jour : une transaction annule l'effet de l'autre",
          "correct": true,
          "feedback": "Bonne réponse : si T1 lit le solde ($1000$), puis T2\nlit aussi ($1000$), puis T1 ajoute $100$ et écrit\n$1100$, puis T2 ajoute $50$ et écrit $1050$, le dépôt\nde T1 est perdu. C'est la **perte de mise à jour**\nclassique.\n"
        },
        {
          "text": "Une erreur de syntaxe SQL au moment de l'exécution",
          "correct": false,
          "feedback": "Erreur : la syntaxe est correcte. Le problème est de\n**cohérence**, pas syntaxique.\n"
        },
        {
          "text": "Une duplication automatique des données",
          "correct": false,
          "feedback": "Erreur : aucune duplication automatique. C'est plutôt\nde la perte d'information.\n"
        },
        {
          "text": "Une accélération de l'exécution des deux transactions",
          "correct": false,
          "feedback": "Erreur : sans isolation, on a au contraire des\nincohérences. La gestion correcte est plus lente mais\nfiable.\n"
        }
      ],
      "explanation": "La gestion des accès concurrents est l'un des\nservices principaux d'un SGBD. Le système prend en\ncharge l'enchaînement des opérations pour garantir\nla cohérence des données, même quand plusieurs\nutilisateurs ou programmes les manipulent en même\ntemps."
    },
    {
      "id": "q26",
      "difficulty": 2,
      "skills": [
        "sql",
        "group-by",
        "approfondissement"
      ],
      "title": "Regroupement avec GROUP BY",
      "statement": "Pour obtenir le **nombre d'élèves par classe** dans la\ntable `eleves(id, nom, classe)`, quelle requête utiliser ?",
      "options": [
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves;\n```\n",
          "correct": false,
          "feedback": "Erreur : sans `GROUP BY`, mélanger une colonne et une\nfonction d'agrégation provoque une erreur SQL ou un\nrésultat incohérent (selon le SGBD). Il faut grouper\nexplicitement par `classe`.\n"
        },
        {
          "text": "```\nSELECT DISTINCT classe, COUNT(*)\nFROM eleves;\n```\n",
          "correct": false,
          "feedback": "Erreur : `DISTINCT` élimine les doublons après calcul,\nil ne sert pas à regrouper pour appliquer une\nagrégation. La syntaxe correcte est `GROUP BY`.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(eleves)\nFROM eleves\nGROUP BY classe;\n```\n",
          "correct": false,
          "feedback": "Erreur : `COUNT(eleves)` n'est pas valide :\n`eleves` est le nom de la table, pas une colonne.\nPour compter les lignes, on utilise `COUNT(*)` ou une\ncolonne précise (`COUNT(id)`).\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nGROUP BY classe;\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : `GROUP BY` regroupe les lignes par\nvaleur distincte de `classe`. La fonction d'agrégation\n`COUNT(*)` compte les lignes de chaque groupe. Le\nrésultat est une ligne par classe avec son effectif.\n"
        }
      ],
      "explanation": "Schéma général : `SELECT colonne, AGREGATION(...) FROM\ntable GROUP BY colonne`. Toutes les colonnes du `SELECT`\nqui ne sont pas dans une fonction d'agrégation doivent\napparaître dans le `GROUP BY`. C'est la règle dite\n« des colonnes visibles »."
    },
    {
      "id": "q27",
      "difficulty": 3,
      "skills": [
        "sql",
        "having",
        "approfondissement"
      ],
      "title": "HAVING vs WHERE",
      "statement": "Pour ne garder que les classes ayant **au moins $25$ élèves**, on doit filtrer un résultat agrégé. Quelle\nest la requête correcte ?",
      "options": [
        {
          "text": "```\nSELECT classe\nFROM eleves\nGROUP BY classe\nWHERE COUNT(*) >= 25;\n```\n",
          "correct": false,
          "feedback": "Erreur : la clause `WHERE` doit toujours apparaître\n**avant** `GROUP BY` dans la syntaxe SQL. De plus,\non ne peut pas y mettre une fonction d'agrégation.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nWHERE COUNT(*) >= 25\nGROUP BY classe;\n```\n",
          "correct": false,
          "feedback": "Erreur : `WHERE` ne peut pas porter sur une fonction\nd'agrégation, qui n'est calculée qu'**après** le\nregroupement. Cette requête échoue avec une erreur\nSQL. C'est précisément le rôle de `HAVING`.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nHAVING classe COUNT(*) >= 25;\n```\n",
          "correct": false,
          "feedback": "Erreur : la syntaxe est invalide. `HAVING` doit\ncontenir une condition logique, et il manque le\n`GROUP BY` qui définit les groupes sur lesquels\nporte le `HAVING`.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nGROUP BY classe\nHAVING COUNT(*) >= 25;\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : `HAVING` filtre les groupes après\nagrégation, contrairement à `WHERE` qui filtre les\nlignes avant agrégation. Pour porter sur le résultat\nde `COUNT(*)`, il faut utiliser `HAVING`.\n"
        }
      ],
      "explanation": "Ordre canonique des clauses :\n`SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY`.\nMnémonique : `WHERE` filtre les lignes individuelles avant\nregroupement ; `HAVING` filtre les groupes après agrégation."
    },
    {
      "id": "q30",
      "difficulty": 2,
      "skills": [
        "sql",
        "trace-select"
      ],
      "title": "Trace d'une requête SELECT avec WHERE",
      "statement": "On considère la table `eleves` suivante :\n\n```\n| id | nom    | classe | moyenne |\n|----|--------|--------|---------|\n| 1  | Alice  | 1A     | 14.5    |\n| 2  | Bob    | 1B     | 9.0     |\n| 3  | Chloé  | 1A     | 16.0    |\n| 4  | David  | 1B     | 13.0    |\n| 5  | Émma   | 1A     | 11.5    |\n```\n\nQue renvoie la requête suivante ?\n\n```sql\nSELECT nom, moyenne\nFROM eleves\nWHERE classe = '1A' AND moyenne >= 12\nORDER BY moyenne DESC;\n```",
      "options": [
        {
          "text": "Cinq lignes (toutes les lignes de la table)\n",
          "correct": false,
          "feedback": "Erreur : on a oublié le filtrage par `WHERE`.\nSans filtre, on aurait bien toutes les\nlignes, mais ici la requête restreint aux\nélèves de `1A` avec une moyenne d'au moins\n$12$.\n"
        },
        {
          "text": "Trois lignes : `('Alice', 14.5)`,\n`('Chloé', 16.0)`, `('Émma', 11.5)`\n",
          "correct": false,
          "feedback": "Erreur : Émma est en `1A` mais sa moyenne\n($11{,}5$) est inférieure à $12$, donc elle\nne satisfait pas la condition `moyenne >= 12`.\nDe plus, l'ordre n'est pas décroissant ici.\n"
        },
        {
          "text": "Deux lignes : `('Chloé', 16.0)` puis\n`('Alice', 14.5)`\n",
          "correct": true,
          "feedback": "Bonne réponse. Étape 1, le filtre\n`WHERE classe = '1A' AND moyenne >= 12`\nretient les élèves de la classe `1A` avec\nune moyenne d'au moins $12$ : Alice ($14{,}5$)\net Chloé ($16$). Émma est en `1A` mais sa\nmoyenne ($11{,}5$) ne satisfait pas la\ndeuxième condition. Étape 2, on ne garde\nque les colonnes `nom` et `moyenne`. Étape 3,\non trie par `moyenne DESC` : Chloé d'abord,\npuis Alice.\n"
        },
        {
          "text": "Une seule ligne : `('Chloé', 16.0)`\n",
          "correct": false,
          "feedback": "Erreur : Alice satisfait elle aussi les deux\nconditions ($14{,}5 \\geq 12$ et classe\n`1A`). Elle doit donc apparaître dans le\nrésultat, après Chloé du fait du tri\ndécroissant.\n"
        }
      ],
      "explanation": "Méthode systématique pour évaluer une requête\nSQL : (1) appliquer le filtre `WHERE` pour ne\nconserver que les lignes pertinentes ; (2)\nprojeter sur les colonnes du `SELECT` ; (3)\ntrier selon `ORDER BY` si présent. Cette\nséquence d'étapes est l'algèbre relationnelle en\naction."
    },
    {
      "id": "q31",
      "difficulty": 2,
      "skills": [
        "sql",
        "trace-join"
      ],
      "title": "Trace d'une jointure",
      "statement": "On dispose des deux tables suivantes :\n\nTable `clients` :\n\n```\n| id | nom    |\n|----|--------|\n| 1  | Alice  |\n| 2  | Bob    |\n| 3  | Chloé  |\n```\n\nTable `commandes` :\n\n```\n| id | client_id | produit  |\n|----|-----------|----------|\n| 10 | 1         | Livre    |\n| 11 | 2         | Cahier   |\n| 12 | 1         | Stylo    |\n```\n\nCombien de lignes renvoie la requête suivante ?\n\n```sql\nSELECT c.nom, cmd.produit\nFROM clients c\nJOIN commandes cmd ON c.id = cmd.client_id;\n```",
      "options": [
        {
          "text": "2 lignes (une seule par client présent dans\nles commandes)\n",
          "correct": false,
          "feedback": "Erreur : Alice a **deux** commandes, donc\nelle apparaît deux fois dans le résultat\n(avec `'Livre'` et avec `'Stylo'`). La\njointure ne fusionne pas les commandes\nd'un même client.\n"
        },
        {
          "text": "3 lignes",
          "correct": true,
          "feedback": "Bonne réponse : la jointure interne (`JOIN`,\nsynonyme de `INNER JOIN`) ne garde que les\npaires `(client, commande)` où la condition\n`c.id = cmd.client_id` est vraie. Alice\n(id $1$) a deux commandes ($10$ et $12$),\nBob (id $2$) en a une, Chloé n'en a aucune\net disparaît du résultat. Total : trois\nlignes. Le résultat est :\n`('Alice', 'Livre')`, `('Bob', 'Cahier')`,\n`('Alice', 'Stylo')`.\n"
        },
        {
          "text": "9 lignes (le produit cartésien $3 \\times 3$)",
          "correct": false,
          "feedback": "Erreur : ce serait le résultat sans la\ncondition `ON c.id = cmd.client_id`. Avec\nla condition, on ne garde que les paires\ncohérentes, soit trois lignes ici. Le\nproduit cartésien apparaît avec\n`FROM clients, commandes` (sans `JOIN`).\n"
        },
        {
          "text": "4 lignes",
          "correct": false,
          "feedback": "Erreur : seules trois commandes existent,\ndonc au plus trois lignes peuvent\napparaître. Une ligne supplémentaire\nn'apparaîtrait que si l'on utilisait un\n`LEFT JOIN` qui inclurait Chloé sans\ncommande (avec `NULL` côté commandes).\n"
        }
      ],
      "explanation": "Pour faire apparaître Chloé (qui n'a aucune\ncommande) dans le résultat, il faudrait\nutiliser un `LEFT JOIN`. La ligne\ncorrespondante aurait alors `NULL` dans la\ncolonne `cmd.produit`. Cela permet de\nrépondre à la question « tous les clients,\navec leurs commandes éventuelles »."
    },
    {
      "id": "q32",
      "difficulty": 3,
      "skills": [
        "sql",
        "group-by",
        "having-trace",
        "approfondissement"
      ],
      "title": "GROUP BY avec HAVING en pratique",
      "statement": "Avec la table `eleves(id, nom, classe, moyenne)`\ncontenant trente élèves répartis entre trois\nclasses (`1A`, `1B`, `1C`), que renvoie la\nrequête suivante ?\n\n```sql\nSELECT classe, AVG(moyenne) AS moy_classe\nFROM eleves\nGROUP BY classe\nHAVING AVG(moyenne) > 12\nORDER BY moy_classe DESC;\n```",
      "options": [
        {
          "text": "Les classes de moyenne strictement\nsupérieure à $12$, avec leur moyenne\n",
          "correct": true,
          "feedback": "Bonne réponse, le tout trié par moyenne\ndécroissante. Étape 1, `GROUP BY classe`\nregroupe les élèves par classe. Étape 2,\n`AVG(moyenne)` calcule la moyenne de chaque\ngroupe. Étape 3, `HAVING AVG(moyenne) > 12`\nfiltre les groupes (et non les lignes\nindividuelles) selon cette condition.\nÉtape 4, `ORDER BY moy_classe DESC` trie\nle résultat.\n"
        },
        {
          "text": "Une erreur SQL, car on ne peut pas mettre\nun alias dans `ORDER BY`\n",
          "correct": false,
          "feedback": "Erreur : SQL autorise tout à fait\nd'utiliser un alias défini dans le\n`SELECT` (ici `moy_classe`) au sein de\n`ORDER BY`. La requête est valide.\n"
        },
        {
          "text": "Toutes les classes, avec la somme des\nmoyennes des élèves\n",
          "correct": false,
          "feedback": "Erreur : la fonction utilisée est `AVG`\n(moyenne), pas `SUM` (somme). Les deux\nfonctions d'agrégation sont distinctes.\nDe plus, le `HAVING` filtrerait certaines\nclasses.\n"
        },
        {
          "text": "La liste des élèves dont la moyenne\nindividuelle est supérieure à $12$\n",
          "correct": false,
          "feedback": "Erreur : c'est ce que ferait un simple\n`WHERE moyenne > 12` sans `GROUP BY`. Ici,\nla fonction d'agrégation `AVG(moyenne)`\ncombinée à `GROUP BY classe` calcule la\nmoyenne **par classe**, pas par élève.\n"
        }
      ],
      "explanation": "Distinction essentielle : `WHERE` filtre les\nlignes individuelles **avant** l'agrégation ;\n`HAVING` filtre les groupes **après**\nl'agrégation. Si on voulait restreindre aux\nélèves majeurs avant de calculer les moyennes,\non aurait combiné les deux :\n`WHERE age >= 18 ... HAVING AVG(moyenne) > 12`."
    },
    {
      "id": "q34",
      "difficulty": 3,
      "skills": [
        "sql",
        "schema-relationnel"
      ],
      "title": "Lecture d'un schéma relationnel",
      "statement": "On considère le schéma relationnel suivant :\n\n```\neleves(*id_eleve, nom, prenom, id_classe#)\nclasses(*id_classe, niveau, salle)\nnotes(*id_eleve#, *id_devoir#, note)\ndevoirs(*id_devoir, matiere, date)\n```\n\nLe `*` marque les attributs qui composent la\nclé primaire de chaque table (souligné dans\nla notation usuelle) et le `#` désigne une\nclé étrangère. Quelle affirmation est\ncorrecte ?",
      "options": [
        {
          "text": "La table `eleves` n'a pas de lien avec\n`classes`\n",
          "correct": false,
          "feedback": "Erreur : la colonne `id_classe#` dans\n`eleves` est précisément une clé\nétrangère qui pointe vers `classes`, ce\nqui établit le lien « un élève appartient\nà une classe ».\n"
        },
        {
          "text": "On peut avoir plusieurs élèves avec le\nmême `id_eleve`\n",
          "correct": false,
          "feedback": "Erreur : `id_eleve` est la clé primaire de\nla table `eleves`, donc unique par\ndéfinition. Deux élèves différents ne\npeuvent pas partager la même valeur de\n`id_eleve`.\n"
        },
        {
          "text": "Pour ajouter une note dans `notes`, il\nn'est pas nécessaire que l'élève existe\ndans `eleves`\n",
          "correct": false,
          "feedback": "Erreur : la clé étrangère `id_eleve#` dans\n`notes` impose que la valeur référence un\nélève existant. Insérer une note pour un\nélève inexistant viole l'intégrité\nréférentielle et le SGBD rejette\nl'opération.\n"
        },
        {
          "text": "La clé primaire de `notes` est composite,\nformée de deux clés étrangères\n",
          "correct": true,
          "feedback": "Bonne réponse : la clé `(id_eleve,\nid_devoir)` référence respectivement\n`eleves` et `devoirs`. C'est la lecture\ncorrecte du schéma. La table `notes` représente une\nassociation binaire porteuse d'un attribut\n(elle relie un élève et un devoir, et\nporte la note). La clé primaire composite\ngarantit l'unicité (un élève ne peut avoir\nqu'une note par devoir), et les clés\nétrangères assurent l'intégrité\nréférentielle (on ne peut pas mettre une\nnote pour un élève ou un devoir\ninexistants).\n"
        }
      ],
      "explanation": "Méthode pour lire un schéma relationnel : (1)\nidentifier la clé primaire de chaque table\n(souvent soulignée ou notée en premier) ; (2)\nrepérer les clés étrangères (notées `#`,\nsoulignées en pointillés, ou en italique selon\nla convention) ; (3) interpréter les\ncardinalités. Ici, l'association `notes` est\nune **association plusieurs-à-plusieurs**\ntypique, qui ne peut pas se modéliser par une\nsimple clé étrangère ajoutée à une table\nexistante."
    },
    {
      "id": "q35",
      "difficulty": 3,
      "skills": [
        "securite",
        "injection-sql"
      ],
      "title": "Injection SQL",
      "statement": "Un programme construit la requête SQL suivante\nen concaténant directement la chaîne\n`nom_saisi` reçue de l'utilisateur :\n\n```python\nrequete = \"SELECT * FROM users WHERE nom = '\" + nom_saisi + \"';\"\n```\n\nSi l'utilisateur saisit\n`' OR '1'='1`, que se passe-t-il ?",
      "options": [
        {
          "text": "Le SGBD détecte l'injection et rejette la\nrequête\n",
          "correct": false,
          "feedback": "Erreur : la requête est syntaxiquement\nparfaitement valide. Le SGBD n'a aucun\nmoyen de détecter qu'elle vient d'une\nconcaténation dangereuse plutôt que d'une\nintention légitime. C'est précisément ce\nqui rend l'injection SQL si pernicieuse.\n"
        },
        {
          "text": "Python lève une exception au moment de la\nconcaténation\n",
          "correct": false,
          "feedback": "Erreur : Python concatène sans aucun\nproblème les chaînes, quel que soit leur\ncontenu. Aucune exception n'est levée.\nC'est précisément le drame : tout se passe\nsilencieusement.\n"
        },
        {
          "text": "Toutes les lignes de la table sont\nrenvoyées : c'est une injection SQL\n",
          "correct": true,
          "feedback": "Bonne réponse : la requête devient\n`SELECT * FROM users WHERE nom = '' OR '1'='1';`.\nLa concaténation naïve fait\nsortir la chaîne saisie de son rôle de\nsimple donnée et l'injecte dans la\nstructure SQL. La condition `'1'='1'`\nétant toujours vraie, l'opérateur `OR` la\nfait court-circuiter le filtre. C'est la\nforme la plus simple d'injection SQL,\ncélèbre attaque qui a touché de nombreux\nsites mal protégés. La parade standard :\nutiliser des **requêtes préparées** avec\ndes paramètres liés\n(`cursor.execute(req, (nom_saisi,))`).\n"
        },
        {
          "text": "La requête ne retourne aucune ligne\n",
          "correct": false,
          "feedback": "Erreur : c'est l'inverse. La condition\n`'1'='1'` étant toujours vraie, **toutes**\nles lignes de la table sont retournées,\ny compris des données potentiellement\nsensibles que l'utilisateur ne devrait\npas voir.\n"
        }
      ],
      "explanation": "L'injection SQL reste, en 2026, l'une des\nvulnérabilités les plus exploitées dans les\napplications web. La parade est connue depuis\nlongtemps : ne **jamais** construire une\nrequête SQL par concaténation de chaînes\nutilisateur, mais utiliser systématiquement\ndes **requêtes préparées** où les paramètres\nsont passés séparément\n(`cursor.execute(\"... WHERE nom = ?\", (nom_saisi,))`).\nLe SGBD échappe alors automatiquement les\ncaractères dangereux."
    },
    {
      "id": "q36",
      "difficulty": 2,
      "skills": [
        "sql",
        "update-trace"
      ],
      "title": "Trace d'un UPDATE ciblé",
      "statement": "On part de la table `comptes` suivante :\n\n```\n| id | titulaire | solde |\n|----|-----------|-------|\n| 1  | Alice     | 1000  |\n| 2  | Bob       | 500   |\n| 3  | Chloé     | 2000  |\n```\n\nOn exécute la requête suivante :\n\n```sql\nUPDATE comptes\nSET solde = solde * 1.05\nWHERE solde >= 1000;\n```\n\nQuel est le contenu de la table après\nexécution ?",
      "options": [
        {
          "text": "Bob est supprimé de la table\n",
          "correct": false,
          "feedback": "Erreur : `UPDATE` ne supprime jamais de\nligne, il ne fait que modifier des\nvaleurs. Pour supprimer une ligne, il\nfaudrait utiliser `DELETE FROM comptes\nWHERE solde < 1000;`.\n"
        },
        {
          "text": "Toutes les lignes voient leur solde\nmultiplié par $1{,}05$\n",
          "correct": false,
          "feedback": "Erreur : c'est ce qui se passerait si la\nclause `WHERE` était absente. Avec\n`WHERE solde >= 1000`, seules les lignes\nsatisfaisant la condition sont\nmodifiées. Bob ($500$) ne fait pas partie\ndu lot.\n"
        },
        {
          "text": "Alice : $1050$, Bob : $500$ (inchangé),\nChloé : $2100$\n",
          "correct": true,
          "feedback": "Bonne réponse : la clause `WHERE solde >= 1000`\nsélectionne les lignes où le solde est\nd'au moins $1000$ (Alice et Chloé). Pour\nchacune, on applique `solde = solde * 1.05`.\nAlice passe de $1000$ à $1050$, Chloé de\n$2000$ à $2100$. Bob, dont le solde est\ninférieur à $1000$, n'est pas affecté.\n"
        },
        {
          "text": "La table reste inchangée car la clause\n`WHERE` est trop restrictive\n",
          "correct": false,
          "feedback": "Erreur : Alice ($1000$) et Chloé ($2000$)\nsatisfont bien la condition `solde >= 1000`,\ndonc leurs lignes sont modifiées. La\ncondition n'est pas trop restrictive : elle\nest ciblée.\n"
        }
      ],
      "explanation": "Schéma général d'un `UPDATE` : (1) la clause\n`WHERE` sélectionne les lignes affectées ; (2)\nla clause `SET` indique comment modifier ces\nlignes ; (3) le SGBD applique la modification\natomiquement (toutes les lignes en une seule\nopération). Sans `WHERE`, **toutes** les lignes\nsont modifiées, ce qui est rarement souhaité\net souvent catastrophique."
    }
  ]
}