{
  "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.",
    "parties": [
      {
        "id": "modele",
        "title": "Modèle relationnel et clés"
      },
      {
        "id": "sql",
        "title": "Requêtes SQL"
      },
      {
        "id": "jointures",
        "title": "Jointures et agrégation"
      },
      {
        "id": "usage",
        "title": "SGBD, sécurité et accès"
      }
    ],
    "prerequisites": [],
    "references": []
  },
  "questions": [
    {
      "id": "q01",
      "difficulty": 1,
      "skills": [
        "definition",
        "sgbd"
      ],
      "title": "Définition d'un SGBD",
      "partie": "usage",
      "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",
      "partie": "modele",
      "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",
      "partie": "modele",
      "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",
      "partie": "modele",
      "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",
      "partie": "sql",
      "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",
      "partie": "sql",
      "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ù la\ncondition compare des valeurs avec les opérateurs\n`=`, `<`, `>`, `<=`, `>=` ou `<>`.\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",
      "partie": "sql",
      "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é",
      "partie": "modele",
      "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",
      "partie": "sql",
      "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 pour manipuler les données\nd'une table sont : `SELECT` (lire), `INSERT` (ajouter),\n`UPDATE` (modifier), `DELETE` (supprimer)."
    },
    {
      "id": "q10",
      "difficulty": 1,
      "skills": [
        "anomalie",
        "atomique"
      ],
      "title": "Valeurs atomiques",
      "partie": "modele",
      "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",
      "partie": "jointures",
      "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 : la condition `ON` relie la clé étrangère\n`commandes.client_id` à la clé primaire `clients.id`. Seules\nles commandes ayant un client correspondant apparaissent.\n"
        },
        {
          "text": "`SELECT * FROM commandes JOIN clients`",
          "correct": false,
          "feedback": "Erreur : sans condition `ON`, rien n'indique quelle commande\nva avec quel client. Chaque commande est associée à chaque\nclient, ce qui n'a aucun sens.\n"
        },
        {
          "text": "`SELECT * FROM commandes 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 WHERE commandes.client_id = clients.id`",
          "correct": false,
          "feedback": "Erreur : la table `clients` n'est citée ni dans `FROM` ni dans\nun `JOIN`. Le SGBD ne connaît donc pas `clients.id` et signale\nune colonne inconnue.\n"
        }
      ],
      "explanation": "Une jointure s'écrit `FROM table1 JOIN table2 ON condition`.\nLa condition relie en général la clé étrangère d'une table à\nla clé primaire de l'autre ; les lignes sans correspondance\nn'apparaissent pas dans le résultat."
    },
    {
      "id": "q12",
      "difficulty": 2,
      "skills": [
        "sql",
        "agregation"
      ],
      "title": "Fonctions d'agrégation",
      "partie": "jointures",
      "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",
      "partie": "sql",
      "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",
      "partie": "sql",
      "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",
      "partie": "sql",
      "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",
      "partie": "modele",
      "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",
      "partie": "sql",
      "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",
      "partie": "usage",
      "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",
      "partie": "modele",
      "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 : une valeur manquante concerne l'attribut lui-même,\npas l'intégrité référentielle. Cette dernière porte sur les\nréférences d'une table vers une autre.\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 la clé étrangère :\nle SGBD refuse toute insertion ou modification qui ferait\npointer `client_id` vers un client inexistant, et refuse aussi\nde supprimer un client encore cité par des commandes."
    },
    {
      "id": "q22",
      "difficulty": 3,
      "skills": [
        "normalisation",
        "redondance"
      ],
      "title": "Détection d'une redondance",
      "partie": "modele",
      "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": [
        "schema",
        "anomalie-suppression"
      ],
      "title": "Anomalie de suppression",
      "partie": "modele",
      "statement": "Le lycée enregistre les inscriptions aux clubs dans une seule\ntable :\n\n```\n| id_inscription | nom_eleve | club      | salle |\n|----------------|-----------|-----------|-------|\n| 1              | Amina     | Échecs    | B12   |\n| 2              | Basile    | Théâtre   | A03   |\n| 3              | Chloé     | Théâtre   | A03   |\n| 4              | Dorian    | Robotique | C07   |\n```\n\nDorian quitte le club de robotique, et l'on exécute\n`DELETE FROM inscriptions WHERE id_inscription = 4;`.\nQuel défaut du schéma cette suppression révèle-t-elle ?",
      "options": [
        {
          "text": "La base ne sait plus que la robotique a lieu en salle C07",
          "correct": true,
          "feedback": "Bonne réponse : la salle d'un club n'est enregistrée que dans\nles lignes d'inscription. En supprimant la seule inscription au\nclub de robotique, on perd aussi cette information : c'est une\nanomalie de suppression.\n"
        },
        {
          "text": "Le SGBD refuse la suppression, la salle C07 étant encore utilisée",
          "correct": false,
          "feedback": "Erreur : aucune clé étrangère ne relie cette table à une table\nde salles. Rien n'empêche la suppression, qui est acceptée, et\nc'est justement ce qui fait perdre l'information.\n"
        },
        {
          "text": "La requête supprime aussi les autres inscriptions de Dorian",
          "correct": false,
          "feedback": "Erreur : la condition `id_inscription = 4` désigne une seule\nligne, puisque `id_inscription` est la clé primaire. Aucune autre\nligne n'est touchée.\n"
        },
        {
          "text": "Aucun défaut : chaque club garde sa salle dans une autre ligne",
          "correct": false,
          "feedback": "Erreur : c'est vrai pour le théâtre, présent dans deux lignes,\nmais pas pour la robotique, qui n'apparaissait que dans la ligne\nsupprimée.\n"
        }
      ],
      "explanation": "La table mélange deux informations : les inscriptions et les\nsalles des clubs. On sépare le schéma en\n`clubs(*id_club, nom, salle)` et\n`inscriptions(*id_inscription, nom_eleve, id_club#)` : supprimer\nune inscription ne fait alors plus rien perdre sur le club."
    },
    {
      "id": "q24",
      "difficulty": 3,
      "skills": [
        "sgbd",
        "acces-concurrents"
      ],
      "title": "Problème de concurrence",
      "partie": "usage",
      "statement": "Si le SGBD ne gérait pas les accès concurrents, deux programmes\nqui modifient en même temps le même compte bancaire pourraient\nprovoquer :",
      "options": [
        {
          "text": "Une perte de mise à jour : une modification efface l'effet de l'autre",
          "correct": true,
          "feedback": "Bonne réponse : si le programme A lit le solde ($1000$), puis\nle programme B le lit aussi ($1000$), puis A ajoute $100$ et\nécrit $1100$, puis B ajoute $50$ et écrit $1050$, le dépôt\nde A 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 programmes",
          "correct": false,
          "feedback": "Erreur : sans gestion des accès concurrents, on obtient au\ncontraire des incohérences. La coordination des écritures\ndemande un peu de temps, mais rend les données fiables.\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": [
        "structure",
        "contenu"
      ],
      "title": "Structure ou contenu d'une base",
      "partie": "modele",
      "statement": "Dans la base de données d'un lycée, laquelle de ces opérations\nmodifie la **structure** de la base, et non son contenu ?",
      "options": [
        {
          "text": "Inscrire un nouvel élève dans la relation `eleves`",
          "correct": false,
          "feedback": "Erreur : un `INSERT` ajoute une ligne, c'est-à-dire un nouvel\nélément du contenu. Le schéma de la relation `eleves` reste le\nmême.\n"
        },
        {
          "text": "Changer la classe d'un élève qui a été réorienté",
          "correct": false,
          "feedback": "Erreur : un `UPDATE` modifie la valeur d'un attribut dans une\nligne existante. Les attributs de la relation et leurs domaines\nne changent pas.\n"
        },
        {
          "text": "Ajouter un attribut `email` à la relation `eleves`",
          "correct": true,
          "feedback": "Bonne réponse : ajouter un attribut change le schéma relationnel\nde `eleves`, donc la structure de la base. Toutes les lignes,\nprésentes et futures, sont concernées.\n"
        },
        {
          "text": "Supprimer tous les élèves qui ont quitté le lycée",
          "correct": false,
          "feedback": "Erreur : même s'il retire beaucoup de lignes d'un coup, un\n`DELETE` ne touche que le contenu. La relation garde les mêmes\nattributs.\n"
        }
      ],
      "explanation": "La structure d'une base est l'ensemble de ses schémas\nrelationnels : relations, attributs, domaines, clés primaires et\nétrangères. Le contenu, ce sont les lignes, que les requêtes\n`INSERT`, `UPDATE` et `DELETE` font évoluer sans toucher à la\nstructure."
    },
    {
      "id": "q27",
      "difficulty": 3,
      "skills": [
        "sql",
        "jointure",
        "agregation"
      ],
      "title": "Compter après une jointure filtrée",
      "partie": "jointures",
      "statement": "On dispose des deux tables suivantes.\nTable `eleves` :\n```\n| id | nom    | classe |\n|----|--------|--------|\n| 1  | Amina  | T1     |\n| 2  | Basile | T2     |\n| 3  | Chloé  | T1     |\n| 4  | Dorian | T1     |\n```\nTable `notes` :\n```\n| id | id_eleve | valeur |\n|----|----------|--------|\n| 10 | 1        | 14     |\n| 11 | 1        | 10     |\n| 12 | 2        | 16     |\n| 13 | 3        | 9      |\n| 14 | 1        | 15     |\n| 15 | 3        | 12     |\n| 16 | 4        | 7      |\n```\nQue renvoie la requête suivante ?\n```sql\nSELECT COUNT(*)\nFROM notes\nJOIN eleves ON notes.id_eleve = eleves.id\nWHERE eleves.classe = 'T1' AND notes.valeur >= 10;\n```",
      "options": [
        {
          "text": "$2$",
          "correct": false,
          "feedback": "Erreur : $2$ est le nombre d’élèves concernés, Amina et Chloé.\nLa requête compte les lignes de la jointure, une par note : les\nnotes retenues d'un même élève comptent chacune.\n"
        },
        {
          "text": "$3$",
          "correct": false,
          "feedback": "Erreur : ce serait le résultat avec `notes.valeur > 10`. La\ncondition `>=` retient aussi la note $10$.\n"
        },
        {
          "text": "$4$",
          "correct": true,
          "feedback": "Bonne réponse : la jointure associe chaque note à son élève.\nParmi les six notes des élèves de T1, quatre valent au moins\n$10$ : $14, 10, 15$ pour Amina, 12 pour Chloé.\n"
        },
        {
          "text": "$6$",
          "correct": false,
          "feedback": "Erreur : $6$ est le nombre de notes des élèves de T1. La seconde\ncondition du `WHERE` écarte encore les notes $9, 7$.\n"
        }
      ],
      "explanation": "La requête s'évalue en trois temps : la jointure apparie chaque\nnote à son élève, le `WHERE` ne garde que les lignes qui vérifient\nles deux conditions, puis `COUNT(*)` compte les lignes restantes.",
      "variants": [
        {
          "statement": "On dispose des deux tables suivantes.\nTable `eleves` :\n```\n| id | nom    | classe |\n|----|--------|--------|\n| 1  | Amina  | T1     |\n| 2  | Basile | T2     |\n| 3  | Chloé  | T1     |\n| 4  | Dorian | T1     |\n```\nTable `notes` :\n```\n| id | id_eleve | valeur |\n|----|----------|--------|\n| 10 | 1        | 14     |\n| 11 | 1        | 10     |\n| 12 | 2        | 16     |\n| 13 | 3        | 9      |\n| 14 | 1        | 15     |\n| 15 | 3        | 12     |\n| 16 | 4        | 7      |\n```\nQue renvoie la requête suivante ?\n```sql\nSELECT COUNT(*)\nFROM notes\nJOIN eleves ON notes.id_eleve = eleves.id\nWHERE eleves.classe = 'T1' AND notes.valeur >= 10;\n```",
          "options": [
            {
              "text": "$2$",
              "correct": false,
              "feedback": "Erreur : $2$ est le nombre d’élèves concernés, Amina et Chloé.\nLa requête compte les lignes de la jointure, une par note : les\nnotes retenues d'un même élève comptent chacune.\n"
            },
            {
              "text": "$3$",
              "correct": false,
              "feedback": "Erreur : ce serait le résultat avec `notes.valeur > 10`. La\ncondition `>=` retient aussi la note $10$.\n"
            },
            {
              "text": "$4$",
              "correct": true,
              "feedback": "Bonne réponse : la jointure associe chaque note à son élève.\nParmi les six notes des élèves de T1, quatre valent au moins\n$10$ : $14, 10, 15$ pour Amina, 12 pour Chloé.\n"
            },
            {
              "text": "$6$",
              "correct": false,
              "feedback": "Erreur : $6$ est le nombre de notes des élèves de T1. La seconde\ncondition du `WHERE` écarte encore les notes $9, 7$.\n"
            }
          ],
          "explanation": "La requête s'évalue en trois temps : la jointure apparie chaque\nnote à son élève, le `WHERE` ne garde que les lignes qui vérifient\nles deux conditions, puis `COUNT(*)` compte les lignes restantes."
        },
        {
          "statement": "On dispose des deux tables suivantes.\nTable `eleves` :\n```\n| id | nom    | classe |\n|----|--------|--------|\n| 1  | Amina  | T1     |\n| 2  | Basile | T2     |\n| 3  | Chloé  | T1     |\n| 4  | Dorian | T1     |\n```\nTable `notes` :\n```\n| id | id_eleve | valeur |\n|----|----------|--------|\n| 10 | 1        | 14     |\n| 11 | 1        | 10     |\n| 12 | 2        | 16     |\n| 13 | 3        | 11      |\n| 14 | 1        | 15     |\n| 15 | 3        | 12     |\n| 16 | 4        | 7      |\n```\nQue renvoie la requête suivante ?\n```sql\nSELECT COUNT(*)\nFROM notes\nJOIN eleves ON notes.id_eleve = eleves.id\nWHERE eleves.classe = 'T1' AND notes.valeur >= 10;\n```",
          "options": [
            {
              "text": "$2$",
              "correct": false,
              "feedback": "Erreur : $2$ est le nombre d’élèves concernés, Amina et Chloé.\nLa requête compte les lignes de la jointure, une par note : les\nnotes retenues d'un même élève comptent chacune.\n"
            },
            {
              "text": "$4$",
              "correct": false,
              "feedback": "Erreur : ce serait le résultat avec `notes.valeur > 10`. La\ncondition `>=` retient aussi la note $10$.\n"
            },
            {
              "text": "$5$",
              "correct": true,
              "feedback": "Bonne réponse : la jointure associe chaque note à son élève.\nParmi les six notes des élèves de T1, cinq valent au moins\n$10$ : $14, 10, 15$ pour Amina, 11 et 12 pour Chloé.\n"
            },
            {
              "text": "$6$",
              "correct": false,
              "feedback": "Erreur : $6$ est le nombre de notes des élèves de T1. La seconde\ncondition du `WHERE` écarte encore les notes $7$.\n"
            }
          ],
          "explanation": "La requête s'évalue en trois temps : la jointure apparie chaque\nnote à son élève, le `WHERE` ne garde que les lignes qui vérifient\nles deux conditions, puis `COUNT(*)` compte les lignes restantes."
        },
        {
          "statement": "On dispose des deux tables suivantes.\nTable `eleves` :\n```\n| id | nom    | classe |\n|----|--------|--------|\n| 1  | Amina  | T1     |\n| 2  | Basile | T2     |\n| 3  | Chloé  | T1     |\n| 4  | Dorian | T1     |\n```\nTable `notes` :\n```\n| id | id_eleve | valeur |\n|----|----------|--------|\n| 10 | 1        | 14     |\n| 11 | 1        | 10     |\n| 12 | 2        | 16     |\n| 13 | 3        | 9      |\n| 14 | 1        | 15     |\n| 15 | 3        | 8     |\n| 16 | 4        | 7      |\n```\nQue renvoie la requête suivante ?\n```sql\nSELECT COUNT(*)\nFROM notes\nJOIN eleves ON notes.id_eleve = eleves.id\nWHERE eleves.classe = 'T1' AND notes.valeur >= 10;\n```",
          "options": [
            {
              "text": "$1$",
              "correct": false,
              "feedback": "Erreur : $1$ est un élève concerné, Amina.\nLa requête compte les lignes de la jointure, une par note : les\nnotes retenues d'un même élève comptent chacune.\n"
            },
            {
              "text": "$2$",
              "correct": false,
              "feedback": "Erreur : ce serait le résultat avec `notes.valeur > 10`. La\ncondition `>=` retient aussi la note $10$.\n"
            },
            {
              "text": "$3$",
              "correct": true,
              "feedback": "Bonne réponse : la jointure associe chaque note à son élève.\nParmi les six notes des élèves de T1, trois valent au moins\n$10$ : $14, 10, 15$ pour Amina, rien pour Chloé.\n"
            },
            {
              "text": "$6$",
              "correct": false,
              "feedback": "Erreur : $6$ est le nombre de notes des élèves de T1. La seconde\ncondition du `WHERE` écarte encore les notes $9, 8, 7$.\n"
            }
          ],
          "explanation": "La requête s'évalue en trois temps : la jointure apparie chaque\nnote à son élève, le `WHERE` ne garde que les lignes qui vérifient\nles deux conditions, puis `COUNT(*)` compte les lignes restantes."
        }
      ]
    },
    {
      "id": "q30",
      "difficulty": 2,
      "skills": [
        "sql",
        "trace-select"
      ],
      "title": "Trace d'une requête SELECT avec WHERE",
      "partie": "sql",
      "statement": "On considère la table `eleves` suivante :\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```\nQue renvoie la requête suivante ?\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, dans l'ordre de la table : `('Alice', 14.5)`, `('Chloé', 16.0)`, `('Émma', 11.5)`\n",
          "correct": false,
          "feedback": "Erreur : l'ordre n'est pas celui de la table\nmais celui de `ORDER BY moyenne DESC`, et le\nfiltre `moyenne >= 12` doit être vérifié\nligne par ligne.\n"
        },
        {
          "text": "Deux lignes : `('Chloé', 16.0)` puis `('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$) et Chloé ($16$).\nÉmma est en `1A` mais sa moyenne ($11{,}5$) ne satisfait pas la deuxième condition. Étape 2, on ne garde\nque les colonnes `nom` et `moyenne`. Étape 3,\non trie par `moyenne DESC` : Chloé d'abord, puis Alice.\n"
        },
        {
          "text": "Une seule ligne : `('Chloé', 16.0)`\n",
          "correct": false,
          "feedback": "Erreur : Alice satisfait aussi les deux\nconditions (classe `1A` et moyenne d'au moins\n$12$). Cette ligne doit donc apparaître dans le\nrésultat, après la première 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.",
      "variants": [
        {
          "statement": "On considère la table `eleves` suivante :\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```\nQue renvoie la requête suivante ?\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, dans l'ordre de la table : `('Alice', 14.5)`, `('Chloé', 16.0)`, `('Émma', 11.5)`\n",
              "correct": false,
              "feedback": "Erreur : l'ordre n'est pas celui de la table\nmais celui de `ORDER BY moyenne DESC`, et le\nfiltre `moyenne >= 12` doit être vérifié\nligne par ligne.\n"
            },
            {
              "text": "Deux lignes : `('Chloé', 16.0)` puis `('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$) et Chloé ($16$).\nÉmma est en `1A` mais sa moyenne ($11{,}5$) ne satisfait pas la deuxième condition. Étape 2, on ne garde\nque les colonnes `nom` et `moyenne`. Étape 3,\non trie par `moyenne DESC` : Chloé d'abord, puis Alice.\n"
            },
            {
              "text": "Une seule ligne : `('Chloé', 16.0)`\n",
              "correct": false,
              "feedback": "Erreur : Alice satisfait aussi les deux\nconditions (classe `1A` et moyenne d'au moins\n$12$). Cette ligne doit donc apparaître dans le\nrésultat, après la première 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."
        },
        {
          "statement": "On considère la table `eleves` suivante :\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```\nQue renvoie la requête suivante ?\n```sql\nSELECT nom, moyenne\nFROM eleves\nWHERE classe = '1A' AND moyenne >= 11\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$11$.\n"
            },
            {
              "text": "Trois lignes, dans l'ordre de la table : `('Alice', 14.5)`, `('Chloé', 16.0)`, `('Émma', 11.5)`\n",
              "correct": false,
              "feedback": "Erreur : l'ordre n'est pas celui de la table\nmais celui de `ORDER BY moyenne DESC`, et le\nfiltre `moyenne >= 11` doit être vérifié\nligne par ligne.\n"
            },
            {
              "text": "Trois lignes : `('Chloé', 16.0)`, `('Alice', 14.5)` puis `('Émma', 11.5)`\n",
              "correct": true,
              "feedback": "Bonne réponse. Étape 1, le filtre\n`WHERE classe = '1A' AND moyenne >= 11`\nretient les élèves de la classe `1A` avec\nune moyenne d'au moins $11$ : Alice ($14{,}5$), Chloé ($16$) et Émma ($11{,}5$).\nles trois élèves de `1A` ont une moyenne d'au moins $11$. Étape 2, on ne garde\nque les colonnes `nom` et `moyenne`. Étape 3,\non trie par `moyenne DESC` : Chloé d'abord, puis Alice, puis Émma.\n"
            },
            {
              "text": "Une seule ligne : `('Chloé', 16.0)`\n",
              "correct": false,
              "feedback": "Erreur : Alice satisfait aussi les deux\nconditions (classe `1A` et moyenne d'au moins\n$11$). Cette ligne doit donc apparaître dans le\nrésultat, après la première 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."
        },
        {
          "statement": "On considère la table `eleves` suivante :\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```\nQue renvoie la requête suivante ?\n```sql\nSELECT nom, moyenne\nFROM eleves\nWHERE classe = '1B' AND moyenne >= 9\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 `1B` avec une moyenne d'au moins\n$9$.\n"
            },
            {
              "text": "Deux lignes, dans l'ordre de la table : `('Bob', 9.0)`, `('David', 13.0)`\n",
              "correct": false,
              "feedback": "Erreur : l'ordre n'est pas celui de la table\nmais celui de `ORDER BY moyenne DESC`, et le\nfiltre `moyenne >= 9` doit être vérifié\nligne par ligne.\n"
            },
            {
              "text": "Deux lignes : `('David', 13.0)` puis `('Bob', 9.0)`\n",
              "correct": true,
              "feedback": "Bonne réponse. Étape 1, le filtre\n`WHERE classe = '1B' AND moyenne >= 9`\nretient les élèves de la classe `1B` avec\nune moyenne d'au moins $9$ : Bob ($9$) et David ($13$).\nles deux élèves de `1B` ont une moyenne d'au moins $9$, Bob tout juste. Étape 2, on ne garde\nque les colonnes `nom` et `moyenne`. Étape 3,\non trie par `moyenne DESC` : David d'abord, puis Bob.\n"
            },
            {
              "text": "Une seule ligne : `('David', 13.0)`\n",
              "correct": false,
              "feedback": "Erreur : Bob satisfait aussi les deux\nconditions (classe `1B` et moyenne d'au moins\n$9$). Cette ligne doit donc apparaître dans le\nrésultat, après la première 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",
      "partie": "jointures",
      "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 (chaque client apparié à chaque commande)",
          "correct": false,
          "feedback": "Erreur : ce serait le résultat sans\ncondition de jointure. La condition\n`ON c.id = cmd.client_id` ne garde que\nles paires cohérentes, soit trois\nlignes ici.\n"
        },
        {
          "text": "4 lignes",
          "correct": false,
          "feedback": "Erreur : seules trois commandes existent,\net chacune est associée à un seul\nclient. Chloé, qui n'a aucune commande,\nn'apporte aucune ligne au résultat.\n"
        }
      ],
      "explanation": "Une jointure `JOIN ... ON` produit une\nligne par paire qui vérifie la\ncondition. Un client sans commande,\ncomme Chloé, disparaît du résultat ;\nun client à plusieurs commandes, comme\nAlice, y figure plusieurs fois."
    },
    {
      "id": "q32",
      "difficulty": 3,
      "skills": [
        "sql",
        "python-sqlite3"
      ],
      "title": "Lire un résultat de requête depuis Python",
      "partie": "sql",
      "statement": "La table `eleves(id, nom, moyenne)` de la base `lycee.db` contient\nquatre lignes : Basile ($13.5$), Amina ($15.0$), Chloé ($11.0$) et\nDorian ($12.0$). On exécute :\n\n```python\nimport sqlite3\n\nconnexion = sqlite3.connect(\"lycee.db\")\ncurseur = connexion.cursor()\ncurseur.execute(\"SELECT nom, moyenne FROM eleves WHERE moyenne > 12 ORDER BY nom\")\nresultats = curseur.fetchall()\nprint(resultats[0])\nconnexion.close()\n```\n\nQu'affiche ce programme ?",
      "options": [
        {
          "text": "`Amina`",
          "correct": false,
          "feedback": "Erreur : chaque ligne du résultat est un p-uplet qui contient\ntoutes les colonnes demandées dans le `SELECT`, ici `nom` et\n`moyenne`. `resultats[0]` est ce p-uplet entier.\n"
        },
        {
          "text": "`('Basile', 13.5)`",
          "correct": false,
          "feedback": "Erreur : Basile est la première ligne de la table, mais\n`ORDER BY nom` range le résultat par ordre alphabétique. Amina\npasse devant.\n"
        },
        {
          "text": "`('Amina', 15.0)`",
          "correct": true,
          "feedback": "Bonne réponse : `fetchall` renvoie la liste des lignes du\nrésultat, chacune sous forme de p-uplet. Amina et Basile ont une\nmoyenne supérieure à $12$, et Amina vient en premier dans\nl'ordre alphabétique.\n"
        },
        {
          "text": "`[('Amina', 15.0), ('Basile', 13.5)]`",
          "correct": false,
          "feedback": "Erreur : c'est le contenu de `resultats` tout entier.\nL'indexation `resultats[0]` n'en extrait que le premier\nélément.\n"
        }
      ],
      "explanation": "Depuis Python, `execute` envoie la requête SQL au SGBD, et\n`fetchall` récupère le résultat sous forme de liste de p-uplets,\nun par ligne. On traite ensuite ces données avec les outils\nhabituels : indexation, boucles, tests."
    },
    {
      "id": "q34",
      "difficulty": 3,
      "skills": [
        "sql",
        "schema-relationnel"
      ],
      "title": "Lecture d'un schéma relationnel",
      "partie": "sql",
      "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",
      "partie": "usage",
      "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é",
      "partie": "sql",
      "statement": "On part de la table `comptes` suivante :\n```\n| id | titulaire | solde |\n|----|-----------|-------|\n| 1  | Alice     | 1000  |\n| 2  | Bob       | 500   |\n| 3  | Chloé     | 2000  |\n```\nOn exécute la requête suivante :\n```sql\nUPDATE comptes\nSET solde = solde * 1.05\nWHERE solde >= 1000;\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é), Chloé : $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 $2000$ à $2100$. Bob, dont le solde est infé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$)\nsatisfait bien la condition `solde >= 1000`,\ndonc la ligne est modifiée. 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.",
      "variants": [
        {
          "statement": "On part de la table `comptes` suivante :\n```\n| id | titulaire | solde |\n|----|-----------|-------|\n| 1  | Alice     | 1000  |\n| 2  | Bob       | 500   |\n| 3  | Chloé     | 2000  |\n```\nOn exécute la requête suivante :\n```sql\nUPDATE comptes\nSET solde = solde * 1.05\nWHERE solde >= 1000;\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é), Chloé : $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 $2000$ à $2100$. Bob, dont le solde est infé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$)\nsatisfait bien la condition `solde >= 1000`,\ndonc la ligne est modifiée. 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."
        },
        {
          "statement": "On part de la table `comptes` suivante :\n```\n| id | titulaire | solde |\n|----|-----------|-------|\n| 1  | Alice     | 1000  |\n| 2  | Bob       | 500   |\n| 3  | Chloé     | 2000  |\n```\nOn exécute la requête suivante :\n```sql\nUPDATE comptes\nSET solde = solde * 1.1\nWHERE solde >= 600;\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 < 600;`.\n"
            },
            {
              "text": "Toutes les lignes voient leur solde\nmultiplié par $1{,}1$\n",
              "correct": false,
              "feedback": "Erreur : c'est ce qui se passerait si la\nclause `WHERE` était absente. Avec\n`WHERE solde >= 600`, seules les lignes\nsatisfaisant la condition sont\nmodifiées. Bob ($500$) ne fait pas partie\ndu lot.\n"
            },
            {
              "text": "Alice : $1100$, Bob : $500$ (inchangé), Chloé : $2200$\n",
              "correct": true,
              "feedback": "Bonne réponse : la clause `WHERE solde >= 600`\nsélectionne les lignes où le solde est\nd'au moins $600$ (Alice et Chloé). Pour\nchacune, on applique `solde = solde * 1.1`.\nAlice passe de $1000$ à $1100$, Chloé de $2000$ à $2200$. Bob, dont le solde est inférieur à $600$, 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$)\nsatisfait bien la condition `solde >= 600`,\ndonc la ligne est modifiée. 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."
        },
        {
          "statement": "On part de la table `comptes` suivante :\n```\n| id | titulaire | solde |\n|----|-----------|-------|\n| 1  | Alice     | 1000  |\n| 2  | Bob       | 500   |\n| 3  | Chloé     | 2000  |\n```\nOn exécute la requête suivante :\n```sql\nUPDATE comptes\nSET solde = solde * 1.02\nWHERE solde >= 2000;\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 < 2000;`.\n"
            },
            {
              "text": "Toutes les lignes voient leur solde\nmultiplié par $1{,}02$\n",
              "correct": false,
              "feedback": "Erreur : c'est ce qui se passerait si la\nclause `WHERE` était absente. Avec\n`WHERE solde >= 2000`, seules les lignes\nsatisfaisant la condition sont\nmodifiées. Alice ($1000$) et Bob ($500$) ne fait pas partie\ndu lot.\n"
            },
            {
              "text": "Alice : $1000$ (inchangé), Bob : $500$ (inchangé), Chloé : $2040$\n",
              "correct": true,
              "feedback": "Bonne réponse : la clause `WHERE solde >= 2000`\nsélectionne les lignes où le solde est\nd'au moins $2000$ (Chloé seule). Pour\nchacune, on applique `solde = solde * 1.02`.\nChloé passe de $2000$ à $2040$. Alice et Bob, dont les soldes sont inférieurs à $2000$, ne sont pas affectés.\n"
            },
            {
              "text": "La table reste inchangée car la clause\n`WHERE` est trop restrictive\n",
              "correct": false,
              "feedback": "Erreur : Chloé ($2000$)\nsatisfait bien la condition `solde >= 2000`,\ndonc la ligne est modifiée. 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."
        }
      ]
    },
    {
      "id": "q37",
      "difficulty": 1,
      "skills": [
        "sql",
        "jointure",
        "clause-on"
      ],
      "title": "Rôle de la clause ON",
      "partie": "jointures",
      "statement": "Dans la requête\n`SELECT * FROM livres JOIN auteurs ON livres.id_auteur = auteurs.id`,\nà quoi sert la clause `ON` ?",
      "options": [
        {
          "text": "À indiquer la condition qui apparie une ligne de `livres` à une ligne d'`auteurs`",
          "correct": true,
          "feedback": "Chaque livre est associé à l'auteur dont\nl'identifiant est égal à sa clé étrangère\n`id_auteur`.\n"
        },
        {
          "text": "À trier les lignes du résultat selon les valeurs de la colonne `auteurs.id`",
          "correct": false,
          "feedback": "Le tri s'obtient avec `ORDER BY` ; `ON` ne\nchange pas l'ordre des lignes.\n"
        },
        {
          "text": "À choisir les colonnes des deux tables qui apparaissent dans le résultat",
          "correct": false,
          "feedback": "Ce choix revient à `SELECT` ; ici l'étoile\naffiche toutes les colonnes des deux tables.\n"
        },
        {
          "text": "À renommer la table `auteurs` en `livres` le temps de la requête",
          "correct": false,
          "feedback": "Renommer une table se fait avec `AS` ; `ON`\ncompare des colonnes, il ne renomme rien.\n"
        }
      ],
      "explanation": "Une jointure combine deux tables ligne à ligne. La\nclause `ON` donne la règle d'appariement, presque\ntoujours l'égalité entre la clé étrangère d'une\ntable et la clé primaire de l'autre. Sans elle, le\nSGBD ne saurait pas quelle ligne d'`auteurs`\naccompagne chaque livre."
    },
    {
      "id": "q38",
      "difficulty": 1,
      "skills": [
        "sql",
        "jointure",
        "ecriture-requete"
      ],
      "title": "Écrire une jointure villes et pays",
      "partie": "jointures",
      "statement": "La base contient `villes(id, nom, id_pays)` et\n`pays(id, nom)`, où `id_pays` est une clé étrangère\nvers `pays`. Quelle requête affiche le nom de chaque\nville suivi du nom de son pays ?",
      "options": [
        {
          "text": "`SELECT villes.nom, pays.nom FROM villes JOIN pays ON villes.id_pays = pays.id`",
          "correct": true,
          "feedback": "La clé étrangère `id_pays` est comparée à la clé\nprimaire `id` de la table `pays`.\n"
        },
        {
          "text": "`SELECT villes.nom, pays.nom FROM villes JOIN pays ON villes.id = pays.id`",
          "correct": false,
          "feedback": "Cette condition apparie une ville et un pays qui\nont par hasard le même numéro d'identifiant, ce\nqui n'a aucun sens.\n"
        },
        {
          "text": "`SELECT villes.nom, pays.nom FROM villes WHERE villes.id_pays = pays.id`",
          "correct": false,
          "feedback": "La table `pays` n'est pas dans la clause `FROM` :\nle SGBD signale une colonne inconnue.\n"
        },
        {
          "text": "`SELECT villes.nom, pays.nom FROM villes JOIN pays ON villes.nom = pays.nom`",
          "correct": false,
          "feedback": "Comparer le nom d'une ville au nom d'un pays\nn'apparie rien, sauf homonymie accidentelle.\n"
        }
      ],
      "explanation": "Pour relier deux tables, on cherche la clé étrangère\nde l'une et la clé primaire de l'autre :\n`villes.id_pays` désigne `pays.id`. La condition de\njointure compare exactement ces deux colonnes."
    },
    {
      "id": "q39",
      "difficulty": 2,
      "skills": [
        "sql",
        "jointure",
        "lignes-sans-correspondance"
      ],
      "title": "Un auteur absent du résultat",
      "partie": "jointures",
      "statement": "Table `auteurs` :\n\n```\n| id | nom     |\n|----|---------|\n| 1  | Colette |\n| 2  | Camus   |\n| 3  | Duras   |\n```\n\nTable `livres` :\n\n```\n| id | titre      | id_auteur |\n|----|------------|-----------|\n| 10 | Chéri      | 1         |\n| 11 | La Peste   | 2         |\n| 12 | Sido       | 1         |\n| 13 | L'Étranger | 2         |\n| 14 | Gigi       | 1         |\n```\n\nOn exécute :\n\n```sql\nSELECT auteurs.nom, livres.titre\nFROM auteurs JOIN livres ON auteurs.id = livres.id_auteur;\n```\n\nPourquoi le nom « Duras » n'apparaît-il pas dans le\nrésultat ?",
      "options": [
        {
          "text": "Parce qu'aucun livre n'a `id_auteur` égal à $3$ : la jointure ne garde que les paires appariées",
          "correct": true,
          "feedback": "Une jointure produit une ligne par couple\n(auteur, livre) vérifiant la condition ; Duras\nn'entre dans aucun couple.\n"
        },
        {
          "text": "Parce que la table `auteurs` est citée en premier dans `FROM`, ce qui l'exclut du résultat",
          "correct": false,
          "feedback": "L'ordre des tables dans la jointure ne retire\naucune ligne ; Colette et Camus, de la même\ntable, apparaissent bien.\n"
        },
        {
          "text": "Parce que le SGBD n'affiche que les deux premiers auteurs de la table",
          "correct": false,
          "feedback": "Rien ne limite le nombre d'auteurs ; Colette et\nCamus figurent parce qu'ils ont des livres, pas\nparce qu'ils sont en tête.\n"
        },
        {
          "text": "Parce que la requête contient une erreur et ne renvoie que des lignes partielles",
          "correct": false,
          "feedback": "La requête est correcte et renvoie cinq lignes\ncomplètes, une par livre.\n"
        }
      ],
      "explanation": "Le résultat compte cinq lignes, une par livre,\nchacune avec le nom de son auteur. Un auteur sans\nlivre ne forme aucune paire et disparaît : c'est le\ncomportement de la jointure interne, la seule au\nprogramme."
    },
    {
      "id": "q40",
      "difficulty": 2,
      "skills": [
        "sql",
        "jointure",
        "alias"
      ],
      "title": "Noms courts dans une jointure",
      "partie": "jointures",
      "statement": "Que désignent `e` et `c` dans la requête suivante ?\n\n```sql\nSELECT e.nom, c.libelle\nFROM eleves AS e\nJOIN clubs AS c ON e.id_club = c.id;\n```",
      "options": [
        {
          "text": "Des alias : des noms courts donnés aux tables `eleves` et `clubs` le temps de la requête",
          "correct": true,
          "feedback": "Ils allègent l'écriture des colonnes et lèvent\ntoute ambiguïté quand deux tables ont une colonne\nde même nom.\n"
        },
        {
          "text": "Deux colonnes de la table `eleves`, qui contiennent respectivement le nom et le libellé",
          "correct": false,
          "feedback": "`nom` et `libelle` sont les colonnes ; `e` et `c`\nprécèdent le point et désignent des tables.\n"
        },
        {
          "text": "Deux nouvelles tables, créées dans la base au moment où la requête est exécutée",
          "correct": false,
          "feedback": "`AS` n'écrit rien dans la base ; les noms courts\ndisparaissent dès la fin de la requête.\n"
        },
        {
          "text": "L'ordre de tri du résultat : d'abord selon les élèves (`e`), puis selon les clubs (`c`)",
          "correct": false,
          "feedback": "Aucun `ORDER BY` n'est présent ; `e` et `c` ne\ndisent rien de l'ordre des lignes.\n"
        }
      ],
      "explanation": "`eleves AS e` déclare un alias. Partout ailleurs\ndans la requête, `e.nom` signifie `eleves.nom`. Le\nmot `AS` est facultatif : `FROM eleves e` a le même\neffet."
    },
    {
      "id": "q41",
      "difficulty": 2,
      "skills": [
        "sql",
        "jointure",
        "where"
      ],
      "title": "Jointure puis filtre",
      "partie": "jointures",
      "statement": "Avec `livres(id, titre, annee, id_auteur)` et\n`auteurs(id, nom)`, quelle requête renvoie les\ntitres des livres écrits par Colette ?",
      "options": [
        {
          "text": "`SELECT livres.titre FROM livres JOIN auteurs ON livres.id_auteur = auteurs.id WHERE auteurs.nom = 'Colette'`",
          "correct": true,
          "feedback": "La jointure attache l'auteur à chaque livre, puis\n`WHERE` ne garde que les lignes dont l'auteur\ns'appelle Colette.\n"
        },
        {
          "text": "`SELECT livres.titre FROM livres JOIN auteurs ON livres.id = auteurs.id WHERE auteurs.nom = 'Colette'`",
          "correct": false,
          "feedback": "La condition de jointure compare deux clés\nprimaires sans rapport : aucun livre n'est relié\nà son auteur, et le résultat est faux sans\nmessage d'erreur.\n"
        },
        {
          "text": "`SELECT livres.titre FROM livres JOIN auteurs ON livres.id_auteur = auteurs.id WHERE livres.id_auteur = 'Colette'`",
          "correct": false,
          "feedback": "La jointure est bonne, mais `id_auteur` contient un\nnuméro, jamais un nom : le filtre ne garde aucune\nligne.\n"
        },
        {
          "text": "`SELECT livres.titre FROM livres JOIN auteurs ON auteurs.nom = 'Colette'`",
          "correct": false,
          "feedback": "La condition de jointure ne relie pas les deux\ntables : chaque livre est apparié à Colette,\nqu'il soit d'elle ou non, et tous les titres\nsortent.\n"
        }
      ],
      "explanation": "Deux étapes : relier chaque livre à son auteur par\nla clé étrangère, puis filtrer sur le nom. Le filtre\nva dans `WHERE`, la règle d'appariement dans `ON`."
    },
    {
      "id": "q42",
      "difficulty": 2,
      "skills": [
        "sql",
        "jointure",
        "order-by",
        "trace"
      ],
      "title": "Première ligne d'une jointure triée",
      "partie": "jointures",
      "statement": "Table `equipes` :\n\n```\n| id | nom     |\n|----|---------|\n| 1  | Lions   |\n| 2  | Aigles  |\n| 3  | Requins |\n```\n\nTable `joueurs` :\n\n```\n| id | nom  | id_equipe | buts |\n|----|------|-----------|------|\n| 1  | Sami | 2         | 4    |\n| 2  | Lou  | 1         | 7    |\n| 3  | Inès | 3         | 2    |\n| 4  | Teo  | 2         | 9    |\n| 5  | Maya | 1         | 5    |\n```\n\n```sql\nSELECT joueurs.nom, equipes.nom\nFROM joueurs JOIN equipes ON joueurs.id_equipe = equipes.id\nORDER BY joueurs.buts DESC;\n```\n\nQuelle est la première ligne du résultat ?",
      "options": [
        {
          "text": "`Teo | Aigles`",
          "correct": true,
          "feedback": "Teo a le plus de buts ($9$) et son équipe,\nd'identifiant $2$, est Aigles.\n"
        },
        {
          "text": "`Lou | Lions`",
          "correct": false,
          "feedback": "Lou vient en deuxième position avec $7$ buts.\n"
        },
        {
          "text": "`Teo | Lions`",
          "correct": false,
          "feedback": "Teo joue dans l'équipe $2$, Aigles ; Lions porte\nl'identifiant $1$.\n"
        },
        {
          "text": "`Inès | Requins`",
          "correct": false,
          "feedback": "Inès, avec $2$ buts, termine dernière du\nclassement décroissant.\n"
        }
      ],
      "explanation": "La jointure remplace chaque `id_equipe` par le nom\nde l'équipe correspondante ; le tri décroissant sur\n`buts` place Teo ($9$) devant Lou ($7$), Maya ($5$),\nSami ($4$) et Inès ($2$)."
    },
    {
      "id": "q43",
      "difficulty": 3,
      "skills": [
        "sql",
        "jointure",
        "trois-tables"
      ],
      "title": "Jointure sur trois tables",
      "partie": "jointures",
      "statement": "Schéma : `eleves(id, nom)`, `activites(id, libelle)`\net `inscriptions(id_eleve, id_activite)`. Quelle\nrequête affiche le nom de chaque élève avec le\nlibellé de chaque activité où il est inscrit ?",
      "options": [
        {
          "text": "```sql\nSELECT eleves.nom, activites.libelle\nFROM eleves\nJOIN inscriptions ON eleves.id = inscriptions.id_eleve\nJOIN activites ON inscriptions.id_activite = activites.id;\n```\n",
          "correct": true,
          "feedback": "Deux jointures en chaîne : l'élève est relié à\nses inscriptions, chaque inscription à son\nactivité.\n"
        },
        {
          "text": "```sql\nSELECT eleves.nom, activites.libelle\nFROM eleves\nJOIN inscriptions ON eleves.id = inscriptions.id_activite\nJOIN activites ON inscriptions.id_eleve = activites.id;\n```\n",
          "correct": false,
          "feedback": "Les clés sont croisées : un élève est apparié\naux inscriptions dont le numéro d'activité vaut\nson propre numéro. Le résultat est faux sans\naucun message d'erreur.\n"
        },
        {
          "text": "```sql\nSELECT eleves.nom, activites.libelle\nFROM eleves\nJOIN activites ON eleves.id = activites.id;\n```\n",
          "correct": false,
          "feedback": "La table `inscriptions` est oubliée : on apparie\nun élève à l'activité qui porte le même numéro,\nsans rapport avec ses inscriptions.\n"
        },
        {
          "text": "```sql\nSELECT eleves.nom, activites.libelle\nFROM eleves, inscriptions, activites;\n```\n",
          "correct": false,
          "feedback": "Sans condition d'appariement, chaque élève est\nassocié à chaque inscription et à chaque\nactivité : le résultat contient toutes les\ncombinaisons.\n"
        }
      ],
      "explanation": "`inscriptions` est une table de liaison : elle ne\ncontient que des clés étrangères. Il faut deux\njointures en chaîne, chacune reliant une clé\nétrangère à la clé primaire visée."
    },
    {
      "id": "q44",
      "difficulty": 3,
      "skills": [
        "sql",
        "jointure",
        "besoin-jointure"
      ],
      "title": "Quand une jointure est nécessaire",
      "partie": "jointures",
      "statement": "Base : `livres(id, titre, annee, id_auteur)` et\n`auteurs(id, nom, pays)`. Quelle demande exige une\njointure ?",
      "options": [
        {
          "text": "Les titres des livres parus après l'année $2000$",
          "correct": false,
          "feedback": "Titre et année sont dans la seule table\n`livres` : un `WHERE annee > 2000` suffit.\n"
        },
        {
          "text": "Les noms des auteurs dont le pays est la France",
          "correct": false,
          "feedback": "La table `auteurs` contient le nom et le pays :\naucune autre table n'est nécessaire.\n"
        },
        {
          "text": "Les titres des livres d'un auteur irlandais",
          "correct": true,
          "feedback": "Le titre est dans `livres` et le pays dans\n`auteurs` : il faut relier les deux par\n`id_auteur`.\n"
        },
        {
          "text": "Le nombre de livres parus pendant l'année $1947$",
          "correct": false,
          "feedback": "`COUNT(*)` sur `livres` avec `WHERE annee = 1947`\nrépond sans toucher à `auteurs`.\n"
        }
      ],
      "explanation": "Une jointure sert quand les informations demandées,\nou les critères de filtrage, sont répartis sur\nplusieurs tables. Si tout se trouve dans une seule\ntable, un `SELECT` simple suffit."
    },
    {
      "id": "q45",
      "difficulty": 1,
      "skills": [
        "sgbd",
        "persistance"
      ],
      "title": "Persistance des données",
      "partie": "usage",
      "statement": "On dit qu'un SGBD assure la **persistance** des données. Que\nsignifie cette propriété ?",
      "options": [
        {
          "text": "Les données survivent à l'arrêt du programme et de la machine",
          "correct": true,
          "feedback": "Bonne réponse : les données sont écrites sur un support durable\net retrouvées intactes au redémarrage, contrairement aux\nvariables d'un programme, qui disparaissent avec lui.\n"
        },
        {
          "text": "Les données sont gardées en mémoire vive pour être lues plus rapidement",
          "correct": false,
          "feedback": "Erreur : la mémoire vive s'efface à l'extinction. La persistance\nrepose justement sur l'écriture sur un support durable, le cache\nen mémoire n'étant qu'une accélération.\n"
        },
        {
          "text": "Les données ne peuvent plus être modifiées une fois insérées dans la base",
          "correct": false,
          "feedback": "Erreur : une base vit, on y insère, modifie et supprime des\nlignes. La persistance garantit que l'état courant survit aux\narrêts, pas qu'il est figé.\n"
        },
        {
          "text": "Les données sont recopiées automatiquement sur un second site distant",
          "correct": false,
          "feedback": "Erreur : la réplication est une mesure de sécurité\nsupplémentaire, pas la définition de la persistance, qui\nconcerne d'abord la conservation locale sur disque.\n"
        }
      ],
      "explanation": "La persistance est l'un des services fondamentaux d'un SGBD : ce\nqui a été validé est conservé durablement, quels que soient les\narrêts ou les pannes de courant ultérieurs."
    },
    {
      "id": "q46",
      "difficulty": 2,
      "skills": [
        "sgbd",
        "securite",
        "droits"
      ],
      "title": "Droits d'accès à une table",
      "partie": "usage",
      "statement": "Dans la base d'un lycée, le compte utilisé par l'application\nd'affichage des emplois du temps doit pouvoir lire la table `cours`\nmais jamais la modifier. Quel mécanisme du SGBD répond à ce\nbesoin ?",
      "options": [
        {
          "text": "Des droits d'accès limités à `SELECT` pour ce compte",
          "correct": true,
          "feedback": "Bonne réponse : le SGBD associe à chaque compte des droits par\ntable et par opération. Une requête de modification émise par\nce compte est refusée avant même d'être exécutée.\n"
        },
        {
          "text": "Le chiffrement de la table `cours`, illisible sans la clé de déchiffrement",
          "correct": false,
          "feedback": "Erreur : chiffrer la table empêcherait aussi la lecture, qui est\nprécisément ce que l'application doit pouvoir faire. Le besoin\nporte sur les opérations autorisées, pas sur la confidentialité.\n"
        },
        {
          "text": "La clé primaire de la table `cours`, qui interdit toute modification des lignes",
          "correct": false,
          "feedback": "Erreur : une clé primaire garantit l'unicité des lignes, elle ne\nlimite en rien les opérations permises à un compte.\n"
        },
        {
          "text": "Une contrainte d'intégrité qui rejette toute requête `UPDATE` sur la table",
          "correct": false,
          "feedback": "Erreur : les contraintes d'intégrité portent sur la validité des\ndonnées, pas sur l'identité de celui qui les modifie. C'est la\ngestion des droits qui distingue les comptes.\n"
        }
      ],
      "explanation": "La sécurisation des accès est un service du SGBD : chaque compte\nreçoit des droits précis, lecture seule pour une application\nd'affichage, écriture pour la gestion, administration pour\nquelques personnes."
    },
    {
      "id": "q47",
      "difficulty": 2,
      "skills": [
        "sgbd",
        "acces-concurrents"
      ],
      "title": "Deux réservations simultanées",
      "partie": "usage",
      "statement": "Deux clients tentent au même instant de réserver la dernière place\nd'un spectacle. Comment le SGBD évite-t-il de vendre deux fois la\nmême place ?",
      "options": [
        {
          "text": "Il les traite l'une après l'autre, et la seconde trouve la place déjà prise",
          "correct": true,
          "feedback": "Bonne réponse : la gestion des accès concurrents ordonne les\nopérations comme si elles avaient été faites l'une après\nl'autre. Le second client reçoit un refus cohérent.\n"
        },
        {
          "text": "Il accepte les deux réservations et signale le conflit à l'administrateur du site",
          "correct": false,
          "feedback": "Erreur : accepter les deux, c'est précisément l'incohérence à\néviter. Le SGBD l'empêche avant qu'elle ne soit écrite, il ne se\ncontente pas de la signaler.\n"
        },
        {
          "text": "Il refuse toute réservation tant que plusieurs clients sont connectés en même temps",
          "correct": false,
          "feedback": "Erreur : bloquer tout le monde rendrait le service inutilisable.\nLe SGBD n'arbitre que les opérations qui touchent les mêmes\ndonnées.\n"
        },
        {
          "text": "Il attribue la place au client dont la connexion au serveur est la plus rapide",
          "correct": false,
          "feedback": "Erreur : la vitesse de connexion n'intervient pas. Le SGBD\ngarantit seulement qu'une des deux opérations est entièrement\ntraitée avant que l'autre ne lise l'état de la place.\n"
        }
      ],
      "explanation": "Sans gestion des accès concurrents, deux lectures simultanées de\n« place libre » conduiraient à deux ventes. Le SGBD sérialise les\nopérations conflictuelles pour que la base reste cohérente."
    },
    {
      "id": "q48",
      "difficulty": 3,
      "skills": [
        "securite",
        "injection-sql",
        "requete-parametree"
      ],
      "title": "Parade à l'injection SQL",
      "partie": "usage",
      "statement": "Un programme construit ses requêtes à partir de valeurs saisies\npar l'utilisateur. Quelle pratique le protège contre l'injection\nSQL ?",
      "options": [
        {
          "text": "Des requêtes paramétrées, les valeurs étant transmises à part",
          "correct": true,
          "feedback": "Bonne réponse : avec une requête paramétrée, la structure de la\nrequête est fixée avant que les valeurs n'y soient insérées.\nUne saisie contenant `' OR 1=1` reste une simple chaîne de\ncaractères comparée telle quelle.\n"
        },
        {
          "text": "Le masquage des messages d'erreur renvoyés par le SGBD à l'utilisateur",
          "correct": false,
          "feedback": "Erreur : cacher les erreurs complique le travail de l'attaquant\nmais n'empêche pas l'injection. Une requête détournée s'exécute\ntoujours, en silence.\n"
        },
        {
          "text": "La limitation de la longueur des champs du formulaire à vingt caractères",
          "correct": false,
          "feedback": "Erreur : `' OR 1=1 --` tient en moins de vingt caractères. La\nlimitation gêne à peine l'attaque et ne traite pas sa cause, le\nmélange entre code SQL et données.\n"
        },
        {
          "text": "Le chiffrement de l'ensemble du contenu de la base de données",
          "correct": false,
          "feedback": "Erreur : le chiffrement protège les données volées ou lues sur\nle disque. Une requête injectée est exécutée par le SGBD\nlui-même, qui déchiffre normalement ce qu'il renvoie.\n"
        }
      ],
      "explanation": "L'injection SQL vient de la concaténation de texte saisi dans une\nrequête. La parade consiste à ne jamais mélanger les deux : la\nrequête est écrite avec des emplacements, et les valeurs sont\nfournies à part."
    },
    {
      "id": "q49",
      "difficulty": 1,
      "skills": [
        "usage",
        "sauvegarde",
        "sgbd"
      ],
      "title": "Les sauvegardes d'une base de données",
      "partie": "usage",
      "statement": "Pourquoi un système de gestion de bases de données sauvegarde-t-il\nrégulièrement les données qu'il héberge ?",
      "options": [
        {
          "text": "Pour restaurer les données après une panne ou une erreur",
          "correct": true,
          "feedback": "Bonne réponse : un disque qui lâche, une requête de suppression\nmalheureuse ou un piratage détruisent des données ; seule une\ncopie récente permet de les retrouver.\n"
        },
        {
          "text": "Pour accélérer les requêtes les plus fréquentes du site",
          "correct": false,
          "feedback": "Erreur : la vitesse relève des index et de la mémoire cache, pas\ndes sauvegardes.\n"
        },
        {
          "text": "Pour supprimer automatiquement les lignes en double",
          "correct": false,
          "feedback": "Erreur : les doublons se traitent par les contraintes\nd'intégrité.\n"
        },
        {
          "text": "Pour remplacer la gestion des droits d'accès des utilisateurs",
          "correct": false,
          "feedback": "Erreur : sauvegarde et droits d'accès sont deux garanties\ndistinctes.\n"
        }
      ],
      "explanation": "Parmi les services d'un SGBD figure la sécurité des données au\nsens de leur conservation : la sauvegarde en est l'outil."
    },
    {
      "id": "q50",
      "difficulty": 2,
      "skills": [
        "usage",
        "droits",
        "moindre-privilege"
      ],
      "title": "Le compte utilisé par une application de consultation",
      "partie": "usage",
      "statement": "Un site affiche les horaires d'une bibliothèque depuis une base de\ndonnées, sans jamais les modifier. Avec quel compte doit-il se\nconnecter à la base ?",
      "options": [
        {
          "text": "Un compte limité à la lecture, pour qu'une faille ne modifie rien",
          "correct": true,
          "feedback": "Bonne réponse : donner le moins de droits possible limite les\ndégâts d'une attaque ; un compte en lecture seule ne peut ni\neffacer ni altérer les horaires.\n"
        },
        {
          "text": "Le compte administrateur, pour n'essuyer aucun refus d'accès",
          "correct": false,
          "feedback": "Erreur : une injection réussie disposerait alors de tous les\ndroits, suppression comprise.\n"
        },
        {
          "text": "Aucun compte, les horaires étant des données publiques de toute façon",
          "correct": false,
          "feedback": "Erreur : le SGBD exige une identification pour tout accès, même\nen lecture.\n"
        },
        {
          "text": "Un compte distinct pour chaque visiteur, créé à la volée",
          "correct": false,
          "feedback": "Erreur : les visiteurs ne se connectent pas à la base, c'est le\nsite qui le fait.\n"
        }
      ],
      "explanation": "Les droits d'accès s'attribuent selon le besoin réel : lire quand\nil suffit de lire."
    }
  ]
}