{
  "chapter": {
    "id": "bases-de-donnees",
    "level": "terminale",
    "theme": "Bases de données",
    "title": "Bases de données et SQL",
    "description": "Modèle relationnel (tables, attributs, clés primaires\net étrangères), contraintes d'intégrité, anomalies dans\nun schéma, langage SQL (SELECT, FROM, WHERE, JOIN,\nINSERT, UPDATE, DELETE), services rendus par un SGBD.",
    "prerequisites": [],
    "references": []
  },
  "questions": [
    {
      "id": "q01",
      "difficulty": 1,
      "skills": [
        "definition",
        "sgbd"
      ],
      "title": "Définition d'un SGBD",
      "statement": "Que désigne le sigle SGBD ?",
      "options": [
        {
          "text": "Stockage Garanti des Bases Distribuées",
          "correct": false,
          "feedback": "Erreur : sigle inventé.\n"
        },
        {
          "text": "Système de Génération Binaire de Données",
          "correct": false,
          "feedback": "Erreur : sigle inventé. SGBD est lié à la **gestion**\ndes bases de données.\n"
        },
        {
          "text": "Standard Général des Bibliothèques de Données",
          "correct": false,
          "feedback": "Erreur : sigle inventé.\n"
        },
        {
          "text": "Système de Gestion de Base de Données",
          "correct": true,
          "feedback": "Bonne réponse : un SGBD est un logiciel qui permet de\nstocker, organiser, interroger et sécuriser des\ndonnées. Exemples : MySQL, PostgreSQL, SQLite, Oracle.\n"
        }
      ],
      "explanation": "Un SGBD relationnel (SGBDR) ajoute la gestion du modèle\nrelationnel et du langage SQL. Tous les exemples cités\nci-dessus sont des SGBDR."
    },
    {
      "id": "q02",
      "difficulty": 1,
      "skills": [
        "modele-relationnel",
        "vocabulaire"
      ],
      "title": "Vocabulaire du modèle relationnel",
      "statement": "Dans le modèle relationnel, comment appelle-t-on **une ligne**\nd'une table ?",
      "options": [
        {
          "text": "Un attribut",
          "correct": false,
          "feedback": "Erreur : un attribut est une **colonne** (un champ),\npas une ligne.\n"
        },
        {
          "text": "Un n-uplet (ou enregistrement)",
          "correct": true,
          "feedback": "Bonne réponse : une ligne d'une table relationnelle est\nun n-uplet (en français) ou *tuple* (en anglais), aussi\nappelé enregistrement. Elle représente une instance\nd'entité.\n"
        },
        {
          "text": "Une clé",
          "correct": false,
          "feedback": "Erreur : une clé est une combinaison d'attributs qui\nidentifie de manière unique chaque ligne.\n"
        },
        {
          "text": "Un domaine",
          "correct": false,
          "feedback": "Erreur : un domaine est l'ensemble des valeurs possibles\npour un attribut (par exemple, les entiers naturels\npour l'âge).\n"
        }
      ],
      "explanation": "Vocabulaire du modèle relationnel : **table** (relation),\n**ligne** (n-uplet, enregistrement), **colonne** (attribut),\n**domaine** (type des valeurs), **clé primaire** (identifiant\nunique)."
    },
    {
      "id": "q03",
      "difficulty": 1,
      "skills": [
        "cle-primaire"
      ],
      "title": "Clé primaire",
      "statement": "Quelle est la propriété fondamentale d'une **clé primaire** ?",
      "options": [
        {
          "text": "Elle identifie de manière unique chaque ligne de la table",
          "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 être un nombre entier",
          "correct": false,
          "feedback": "Erreur : une clé primaire peut être de n'importe quel\ntype (chaîne, code-barre, identifiant composite). Le\ntype entier est seulement le plus courant.\n"
        },
        {
          "text": "Elle peut prendre la valeur `NULL`",
          "correct": false,
          "feedback": "Erreur : une clé primaire ne peut **jamais** être\n`NULL`, sinon elle ne pourrait plus identifier une\nligne de manière unique.\n"
        },
        {
          "text": "Elle doit toujours être une seule colonne",
          "correct": false,
          "feedback": "Erreur : une clé primaire peut être **composite** (sur\nplusieurs colonnes). Par exemple, dans une table\nd'inscriptions, le couple `(eleve_id, cours_id)` peut\nêtre une clé primaire composite.\n"
        }
      ],
      "explanation": "Une clé primaire est : (1) unique, (2) non nulle, (3) stable\ndans le temps. Elle peut être composite (composée de\nplusieurs colonnes) si aucune colonne unique ne suffit."
    },
    {
      "id": "q04",
      "difficulty": 1,
      "skills": [
        "cle-etrangere"
      ],
      "title": "Clé étrangère",
      "statement": "Que représente une **clé étrangère** dans une table ?",
      "options": [
        {
          "text": "Un attribut qui référence 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é qui ne peut pas être modifiée",
          "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é\nréférentielle** : on ne peut pas créer une commande pour\nun client inexistant."
    },
    {
      "id": "q05",
      "difficulty": 1,
      "skills": [
        "sql",
        "select"
      ],
      "title": "Syntaxe de base de SELECT",
      "statement": "Quelle est la syntaxe SQL pour récupérer **toutes** les\ncolonnes de la table `eleves` ?",
      "options": [
        {
          "text": "`SELECT eleves`",
          "correct": false,
          "feedback": "Erreur : la syntaxe `SELECT` requiert au minimum la\nliste des colonnes (ou `*`) puis `FROM` pour la table.\n"
        },
        {
          "text": "`GET ALL FROM eleves`",
          "correct": false,
          "feedback": "Erreur : `GET` n'existe pas en SQL. La commande de\nlecture est `SELECT`.\n"
        },
        {
          "text": "`SELECT * FROM eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `*` est le joker qui désigne toutes\nles colonnes. `FROM` indique la table source.\n"
        },
        {
          "text": "`READ eleves`",
          "correct": false,
          "feedback": "Erreur : `READ` n'est pas un mot-clé SQL. La lecture\nse fait avec `SELECT`.\n"
        }
      ],
      "explanation": "Bonne pratique : éviter `SELECT *` en production et lister\nexplicitement les colonnes nécessaires. Cela rend le code\nplus robuste aux évolutions de schéma et plus clair."
    },
    {
      "id": "q06",
      "difficulty": 1,
      "skills": [
        "sql",
        "where"
      ],
      "title": "Filtrage avec WHERE",
      "statement": "Pour ne sélectionner que les élèves majeurs de la table\n`eleves`, quelle requête utiliser ?",
      "options": [
        {
          "text": "`FILTER eleves WHERE age >= 18`",
          "correct": false,
          "feedback": "Erreur : `FILTER` n'est pas la syntaxe SQL standard.\nOn utilise `SELECT ... WHERE`.\n"
        },
        {
          "text": "`SELECT * FROM eleves CONDITION age >= 18`",
          "correct": false,
          "feedback": "Erreur : `CONDITION` n'existe pas en SQL. C'est `WHERE`\nqui introduit la condition.\n"
        },
        {
          "text": "`SELECT * FROM eleves WHERE age >= 18`",
          "correct": true,
          "feedback": "Bonne réponse : `WHERE` introduit la condition de\nfiltrage. La syntaxe est `WHERE condition`, où\ncondition utilise les opérateurs SQL standard\n(`=`, `<`, `>`, `<=`, `>=`, `<>`, `LIKE`, etc.).\n"
        },
        {
          "text": "`SELECT * FROM eleves IF age >= 18`",
          "correct": false,
          "feedback": "Erreur : `IF` n'est pas un mot-clé SQL pour le\nfiltrage. Le filtre se fait avec `WHERE`.\n"
        }
      ],
      "explanation": "Une requête SQL classique a la forme :\n`SELECT colonnes FROM table WHERE condition`. Les\nconditions multiples se combinent avec `AND`, `OR`, `NOT`."
    },
    {
      "id": "q07",
      "difficulty": 1,
      "skills": [
        "sql",
        "ordre"
      ],
      "title": "Tri avec ORDER BY",
      "statement": "Pour afficher les élèves par ordre alphabétique de leur\nnom, on utilise :",
      "options": [
        {
          "text": "`SELECT * FROM eleves ARRANGE BY nom`",
          "correct": false,
          "feedback": "Erreur : `ARRANGE BY` n'est pas du SQL valide.\n"
        },
        {
          "text": "`SELECT * FROM eleves ORDER BY nom`",
          "correct": true,
          "feedback": "Bonne réponse : `ORDER BY` trie le résultat selon une\nou plusieurs colonnes. Par défaut, l'ordre est\nascendant (`ASC`). Pour un ordre descendant, ajouter\n`DESC`.\n"
        },
        {
          "text": "`SELECT * FROM eleves SORTED nom`",
          "correct": false,
          "feedback": "Erreur : `SORTED` n'existe pas en SQL.\n"
        },
        {
          "text": "`SELECT * FROM eleves SORT BY nom`",
          "correct": false,
          "feedback": "Erreur : `SORT BY` n'est pas une syntaxe SQL. Le tri\nse fait avec `ORDER BY`.\n"
        }
      ],
      "explanation": "Pour trier par nom puis par prénom : `ORDER BY nom, prenom`.\nPour ordre décroissant : `ORDER BY nom DESC`. On peut\nmélanger : `ORDER BY classe ASC, moyenne DESC`."
    },
    {
      "id": "q08",
      "difficulty": 1,
      "skills": [
        "contrainte"
      ],
      "title": "Contraintes d'intégrité",
      "statement": "Qu'est-ce qu'une **contrainte d'intégrité** dans une base de\ndonnées relationnelle ?",
      "options": [
        {
          "text": "Un mot de passe pour accéder à une table",
          "correct": false,
          "feedback": "Erreur : aucun rapport avec l'authentification. Les\ncontraintes d'intégrité concernent la cohérence des\ndonnées, pas la sécurité d'accès.\n"
        },
        {
          "text": "Une règle qui restreint les valeurs admissibles dans la base, 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",
          "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\nautomatiquement** par le SGBD à chaque modification. Si\nune opération les violerait, elle est rejetée. C'est une\ngarantie forte de cohérence."
    },
    {
      "id": "q09",
      "difficulty": 1,
      "skills": [
        "sql",
        "insert"
      ],
      "title": "Insertion d'une ligne",
      "statement": "Quelle commande SQL ajoute une ligne dans une table ?",
      "options": [
        {
          "text": "`INSERT INTO eleves VALUES ('Alice', 17)`",
          "correct": true,
          "feedback": "Bonne réponse : `INSERT INTO table VALUES (...)` est la\nforme classique. On peut aussi préciser les colonnes :\n`INSERT INTO eleves (nom, age) VALUES ('Alice', 17)`.\n"
        },
        {
          "text": "`CREATE INTO eleves VALUES ('Alice', 17)`",
          "correct": false,
          "feedback": "Erreur : `CREATE` est utilisé pour créer une table\n(`CREATE TABLE`), pas pour insérer une ligne.\n"
        },
        {
          "text": "`PUT INTO eleves VALUES ('Alice', 17)`",
          "correct": false,
          "feedback": "Erreur : `PUT` n'existe pas en SQL.\n"
        },
        {
          "text": "`ADD INTO eleves VALUES ('Alice', 17)`",
          "correct": false,
          "feedback": "Erreur : `ADD` n'est pas la syntaxe SQL standard pour\najouter une ligne.\n"
        }
      ],
      "explanation": "Les quatre commandes principales du SQL DML (Data\nManipulation Language) sont : `SELECT` (lire),\n`INSERT` (créer), `UPDATE` (modifier), `DELETE` (supprimer)."
    },
    {
      "id": "q10",
      "difficulty": 1,
      "skills": [
        "anomalie",
        "atomique"
      ],
      "title": "Valeurs atomiques",
      "statement": "Dans une base relationnelle correctement conçue, on\nattend que chaque attribut contienne une valeur\n**atomique**. Que signifie cette propriété ?",
      "options": [
        {
          "text": "La valeur doit être un 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 n'est pas décomposable en plusieurs informations distinctes (par exemple, on ne stocke pas plusieurs numéros de téléphone dans une même cellule)",
          "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 pas être nulle",
          "correct": false,
          "feedback": "Erreur : aucun rapport. L'absence de valeur est\ngérée par `NULL` ou par la contrainte\n`NOT NULL`.\n"
        }
      ],
      "explanation": "Stocker des valeurs non atomiques (listes, couples,\ntableaux) dans une cellule est une anomalie\nfréquente. Cela empêche de filtrer ou de joindre\nfacilement la donnée et peut générer des\nredondances et des incohérences."
    },
    {
      "id": "q11",
      "difficulty": 2,
      "skills": [
        "sql",
        "jointure"
      ],
      "title": "Jointure entre deux tables",
      "statement": "Pour combiner les tables `commandes` et `clients` en\nreliant `commandes.client_id` à `clients.id`, quelle\nsyntaxe utiliser ?",
      "options": [
        {
          "text": "`SELECT * FROM commandes JOIN clients ON commandes.client_id = clients.id`",
          "correct": true,
          "feedback": "Bonne réponse : c'est une jointure **interne** explicite.\nSeules les commandes ayant un client correspondant\napparaissent. Syntaxe préférée à la virgule depuis\nSQL-92.\n"
        },
        {
          "text": "`SELECT * FROM commandes, clients`",
          "correct": false,
          "feedback": "Erreur : cette syntaxe produit le **produit cartésien**\n(toutes les paires possibles), sans condition de\njointure. Le résultat est rarement ce qu'on souhaite.\n"
        },
        {
          "text": "`SELECT * FROM commandes WITH clients`",
          "correct": false,
          "feedback": "Erreur : `WITH` introduit des sous-requêtes nommées\n(CTE), pas des jointures.\n"
        },
        {
          "text": "`SELECT * FROM commandes UNION clients`",
          "correct": false,
          "feedback": "Erreur : `UNION` combine deux requêtes en empilant\nleurs résultats (les lignes de l'une après l'autre).\nPour combiner colonnes côte à côte, c'est `JOIN`.\n"
        }
      ],
      "explanation": "Différents types de jointures : `INNER JOIN` (ne garde\nque les lignes appariées), `LEFT JOIN` (garde toutes les\nlignes de la table de gauche), `RIGHT JOIN` (symétrique),\n`FULL JOIN` (les deux côtés)."
    },
    {
      "id": "q12",
      "difficulty": 2,
      "skills": [
        "sql",
        "agregation"
      ],
      "title": "Fonctions d'agrégation",
      "statement": "Pour calculer la **moyenne des âges** des élèves, quelle\nrequête utiliser ?",
      "options": [
        {
          "text": "`SELECT MOYENNE(age) FROM eleves`",
          "correct": false,
          "feedback": "Erreur : SQL utilise des mots-clés en anglais. La\nversion française n'existe pas.\n"
        },
        {
          "text": "`SELECT (age) FROM eleves AVERAGE`",
          "correct": false,
          "feedback": "Erreur : la syntaxe `AVERAGE` après le `FROM` n'existe\npas. Les fonctions d'agrégation s'appliquent à\nl'intérieur du `SELECT`.\n"
        },
        {
          "text": "`SELECT MEAN(age) FROM eleves`",
          "correct": false,
          "feedback": "Erreur : la fonction de moyenne en SQL standard\ns'appelle `AVG`, pas `MEAN`.\n"
        },
        {
          "text": "`SELECT AVG(age) FROM eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `AVG` (*average*) calcule la moyenne\narithmétique. Les fonctions d'agrégation principales\nsont `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`.\n"
        }
      ],
      "explanation": "Les fonctions d'agrégation principales en SQL sont\n`COUNT`, `SUM`, `AVG`, `MIN` et `MAX`. Elles\nsynthétisent une colonne entière en une seule\nvaleur dans le résultat de la requête."
    },
    {
      "id": "q13",
      "difficulty": 2,
      "skills": [
        "sql",
        "combinaison-clauses"
      ],
      "title": "Combiner WHERE et ORDER BY",
      "statement": "Quelle requête renvoie les noms des élèves majeurs,\nclassés par âge **décroissant** ?",
      "options": [
        {
          "text": "`SELECT nom FROM eleves ORDER BY age DESC WHERE age >= 18`",
          "correct": false,
          "feedback": "Erreur : l'ordre des clauses est imposé en SQL.\n`WHERE` se place avant `ORDER BY`.\n"
        },
        {
          "text": "`SELECT nom FROM eleves WHERE age >= 18 ORDER BY age DESC`",
          "correct": true,
          "feedback": "Bonne réponse : `WHERE` filtre les lignes\n(majeurs), puis `ORDER BY age DESC` les trie par\nâge décroissant.\n"
        },
        {
          "text": "`SELECT nom FROM eleves WHERE age >= 18 ORDER BY age`",
          "correct": false,
          "feedback": "Erreur : sans le mot-clé `DESC`, le tri se fait\npar défaut en ordre croissant.\n"
        },
        {
          "text": "`SELECT nom FROM eleves WHERE age >= 18 SORT age DESC`",
          "correct": false,
          "feedback": "Erreur : la clause `SORT` n'existe pas en SQL\nstandard. Il faut utiliser `ORDER BY`.\n"
        }
      ],
      "explanation": "L'ordre canonique des clauses dans une requête\nd'interrogation est : `SELECT` … `FROM` … `WHERE` …\n`ORDER BY` …. Le mot-clé `DESC` après `ORDER BY`\nindique un tri décroissant ; `ASC` (par défaut)\nindique un tri croissant."
    },
    {
      "id": "q14",
      "difficulty": 2,
      "skills": [
        "sql",
        "update"
      ],
      "title": "Modification d'une ligne",
      "statement": "Pour augmenter de $1$ point la note de l'élève d'identifiant\n$42$, quelle requête utiliser ?",
      "options": [
        {
          "text": "`MODIFY eleves SET note = note + 1 WHERE id = 42`",
          "correct": false,
          "feedback": "Erreur : `MODIFY` n'est pas un mot-clé SQL standard.\n"
        },
        {
          "text": "`UPDATE eleves SET note = note + 1 WHERE id = 42`",
          "correct": true,
          "feedback": "Bonne réponse : `UPDATE` met à jour des lignes\nexistantes. La clause `SET` indique les modifications\net `WHERE` filtre les lignes affectées.\n"
        },
        {
          "text": "`UPDATE eleves WHERE id = 42 SET note = note + 1`",
          "correct": false,
          "feedback": "Erreur : ordre incorrect des clauses. La syntaxe\nstandard est `UPDATE table SET ... WHERE ...`.\n"
        },
        {
          "text": "`CHANGE eleves SET note = note + 1 WHERE id = 42`",
          "correct": false,
          "feedback": "Erreur : `CHANGE` n'est pas un mot-clé SQL standard.\n"
        }
      ],
      "explanation": "Toujours penser à mettre une clause `WHERE` sur les\n`UPDATE` et `DELETE`. **Sans elle, la modification s'applique\nà toutes les lignes** de la table, ce qui est rarement ce\nqu'on veut et souvent catastrophique."
    },
    {
      "id": "q15",
      "difficulty": 2,
      "skills": [
        "sql",
        "distinct"
      ],
      "title": "Suppression de doublons",
      "statement": "Pour obtenir la liste des **classes différentes** présentes\ndans la table `eleves` (sans doublons), quelle requête\nutiliser ?",
      "options": [
        {
          "text": "`SELECT DISTINCT classe FROM eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `DISTINCT` élimine les doublons. Si\nla table contient $30$ élèves de la classe « 1NSI »\net $25$ de « TNSI », la requête renvoie deux lignes :\n« 1NSI » et « TNSI ».\n"
        },
        {
          "text": "`SELECT classe DISTINCT FROM eleves`",
          "correct": false,
          "feedback": "Erreur : `DISTINCT` se place **avant** la liste des\ncolonnes, juste après `SELECT`.\n"
        },
        {
          "text": "`SELECT UNIQUE classe FROM eleves`",
          "correct": false,
          "feedback": "Erreur : `UNIQUE` est utilisé pour les contraintes au\nmoment de la création de table, pas pour la sélection.\nLe mot-clé pour la sélection est `DISTINCT`.\n"
        },
        {
          "text": "`SELECT classe FROM eleves`",
          "correct": false,
          "feedback": "Erreur : sans `DISTINCT`, le résultat contiendra une\nentrée par élève, donc beaucoup de doublons.\n"
        }
      ],
      "explanation": "`DISTINCT` agit sur l'ensemble des colonnes\nsélectionnées. Par exemple,\n`SELECT DISTINCT nom, prenom FROM eleves` ne renverra\nqu'une ligne par couple (nom, prenom) distinct."
    },
    {
      "id": "q16",
      "difficulty": 2,
      "skills": [
        "association-multiple"
      ],
      "title": "Représenter une association multiple",
      "statement": "On souhaite représenter le fait qu'un élève peut\nsuivre **plusieurs cours** et qu'un cours peut\naccueillir **plusieurs élèves**. Quel schéma\nrelationnel adopter ?",
      "options": [
        {
          "text": "Deux tables avec une duplication des cours dans chaque ligne d'élève",
          "correct": false,
          "feedback": "Erreur : la duplication crée des redondances et\ndes anomalies de mise à jour si l'intitulé\nd'un cours change.\n"
        },
        {
          "text": "Une seule table `eleves` avec autant de colonnes `cours_1`, `cours_2`, `cours_3` qu'il y a de cours possibles",
          "correct": false,
          "feedback": "Erreur : ce schéma est rigide (il faut\nmodifier la structure si un nouveau cours\napparaît) et la plupart des cellules seront\nvides.\n"
        },
        {
          "text": "Une seule table contenant `eleve` et tous ses cours dans une même cellule",
          "correct": false,
          "feedback": "Erreur : on viole la propriété d'atomicité des\nattributs et on ne peut plus interroger\nfacilement les données.\n"
        },
        {
          "text": "Trois tables `eleves(id, nom)`, `cours(id, intitule)` et `inscriptions(eleve_id, cours_id)` reliées par des clés étrangères",
          "correct": true,
          "feedback": "Bonne réponse : on isole chaque entité dans sa\ntable et on crée une table **d'association**\ndont chaque ligne représente une inscription\nd'un élève à un cours.\n"
        }
      ],
      "explanation": "Une association « plusieurs à plusieurs » se\nreprésente par une table d'association qui contient\nles clés étrangères des deux entités liées. C'est un\npatron classique du modèle relationnel pour éviter\nles redondances."
    },
    {
      "id": "q17",
      "difficulty": 2,
      "skills": [
        "sql",
        "like"
      ],
      "title": "Recherche avec LIKE",
      "statement": "Pour trouver tous les élèves dont le nom commence par \"Du\",\nquelle requête utiliser ?",
      "options": [
        {
          "text": "`WHERE nom STARTS 'Du'`",
          "correct": false,
          "feedback": "Erreur : `STARTS` n'est pas un mot-clé SQL standard.\nPour cette recherche, on utilise `LIKE` avec `%`.\n"
        },
        {
          "text": "`WHERE nom LIKE 'Du%'`",
          "correct": true,
          "feedback": "Bonne réponse : `LIKE` permet la correspondance par\nmotif. Le caractère `%` représente n'importe quelle\nséquence de caractères (zéro ou plus). Donc `'Du%'`\ncorrespond à \"Du\", \"Dupont\", \"Dubois\", etc.\n"
        },
        {
          "text": "`WHERE nom = 'Du'`",
          "correct": false,
          "feedback": "Erreur : `=` teste l'égalité exacte. Cela ne\nrenverrait que les élèves dont le nom est **exactement**\n\"Du\", pas ceux qui commencent par \"Du\".\n"
        },
        {
          "text": "`WHERE nom CONTAINS 'Du'`",
          "correct": false,
          "feedback": "Erreur : `CONTAINS` est non standard (existant dans\ncertains SGBD pour la recherche plein texte). Le SQL\nstandard utilise `LIKE` avec des jokers.\n"
        }
      ],
      "explanation": "Les jokers de `LIKE` : `%` (zéro ou plus de caractères) et\n`_` (un caractère exactement). Pour une recherche\ninsensible à la casse, certains SGBD acceptent `ILIKE`\n(PostgreSQL) ou `LIKE` avec `LOWER()` (portable)."
    },
    {
      "id": "q18",
      "difficulty": 2,
      "skills": [
        "sql",
        "delete"
      ],
      "title": "Suppression d'une ligne",
      "statement": "Que fait la requête `DELETE FROM eleves;` ?",
      "options": [
        {
          "text": "Elle supprime toutes les lignes de la table `eleves`",
          "correct": true,
          "feedback": "Bonne réponse : `DELETE` sans clause `WHERE` vide la\ntable (mais la structure reste). C'est l'erreur\nclassique en production. Pour supprimer la table\nentière (structure incluse), il faut `DROP TABLE eleves`.\n"
        },
        {
          "text": "Elle supprime la table entière (structure et contenu)",
          "correct": false,
          "feedback": "Erreur : c'est `DROP TABLE` qui supprime la structure.\n`DELETE FROM` ne touche que les lignes, pas le schéma.\n"
        },
        {
          "text": "Elle ne fait rien (manque de `WHERE`)",
          "correct": false,
          "feedback": "Erreur : sans `WHERE`, la requête s'applique à\n**toutes** les lignes. Elle est syntaxiquement valide.\n"
        },
        {
          "text": "Elle lève une erreur SQL",
          "correct": false,
          "feedback": "Erreur : la syntaxe est valide. Aucune erreur n'est\nlevée : c'est précisément le piège.\n"
        }
      ],
      "explanation": "Toujours utiliser `WHERE` avec `DELETE`, sauf intention\nexplicite de tout vider. Beaucoup de SGBD permettent\nd'imposer une clause `WHERE` obligatoire en mode\n« *safe updates* » pour éviter les catastrophes."
    },
    {
      "id": "q19",
      "difficulty": 2,
      "skills": [
        "sgbd",
        "services"
      ],
      "title": "Services rendus par un SGBD",
      "statement": "Lequel des éléments suivants n'est **pas** un\nservice rendu par un système de gestion de bases de\ndonnées ?",
      "options": [
        {
          "text": "La gestion des accès concurrents par plusieurs utilisateurs ou programmes",
          "correct": false,
          "feedback": "Erreur : c'est bien un service du SGBD, qui évite\nles incohérences quand plusieurs requêtes\nmodifient les mêmes données simultanément.\n"
        },
        {
          "text": "La persistance des données (les données survivent à l'arrêt du programme)",
          "correct": false,
          "feedback": "Erreur : c'est bien un service du SGBD, qui assure\nle stockage durable des données sur disque.\n"
        },
        {
          "text": "La compilation du code source d'un programme Python",
          "correct": true,
          "feedback": "Bonne réponse : la compilation de code Python\nn'a aucun rapport avec un SGBD. C'est le rôle de\nl'interpréteur Python ou d'un compilateur, pas\ndu gestionnaire de bases de données.\n"
        },
        {
          "text": "La sécurisation des accès (authentification, droits par utilisateur)",
          "correct": false,
          "feedback": "Erreur : c'est bien un service du SGBD, qui\ncontrôle qui peut lire ou modifier quoi.\n"
        }
      ],
      "explanation": "Les services principaux d'un SGBD sont la\npersistance, la gestion des accès concurrents,\nl'efficacité du traitement des requêtes et la\nsécurisation des accès. Le programme officiel\ndemande de les **identifier**, sans en détailler le\nfonctionnement interne."
    },
    {
      "id": "q20",
      "difficulty": 2,
      "skills": [
        "contrainte",
        "integrite"
      ],
      "title": "Intégrité référentielle",
      "statement": "Quelle situation viole l'**intégrité référentielle** ?",
      "options": [
        {
          "text": "Avoir deux clients avec le même nom",
          "correct": false,
          "feedback": "Erreur : ce n'est pas une violation tant que la clé\nprimaire (l'`id`) est unique. Le nom peut être en\ndouble.\n"
        },
        {
          "text": "Insérer une commande avec un `client_id` qui n'existe pas dans la table `clients`",
          "correct": true,
          "feedback": "Bonne réponse : la clé étrangère `client_id` doit\npointer vers une ligne existante de la table\n`clients`. Sinon, on a une référence orpheline,\nviolation typique de l'intégrité référentielle.\n"
        },
        {
          "text": "Insérer une commande sans préciser la quantité",
          "correct": false,
          "feedback": "Erreur : c'est une violation de contrainte `NOT NULL`,\npas d'intégrité référentielle. Cette dernière concerne\nles références entre tables.\n"
        },
        {
          "text": "Trier les commandes par date",
          "correct": false,
          "feedback": "Erreur : un tri ne viole aucune contrainte.\n"
        }
      ],
      "explanation": "L'intégrité référentielle est garantie par les\ncontraintes `FOREIGN KEY`. À la suppression d'un client,\non peut configurer `ON DELETE CASCADE` (supprimer aussi\nles commandes), `ON DELETE RESTRICT` (interdire la\nsuppression s'il y a des commandes), etc."
    },
    {
      "id": "q21",
      "difficulty": 3,
      "skills": [
        "sql",
        "sous-requete"
      ],
      "title": "Sous-requête",
      "statement": "Que renvoie cette requête ?\n\n```\nSELECT nom FROM eleves\nWHERE moyenne > (SELECT AVG(moyenne) FROM eleves);\n```",
      "options": [
        {
          "text": "Les moyennes des élèves au-dessus de zéro",
          "correct": false,
          "feedback": "Erreur : la condition `>` ne porte pas sur zéro mais\nsur la valeur de la sous-requête.\n"
        },
        {
          "text": "Une erreur (sous-requête non autorisée)",
          "correct": false,
          "feedback": "Erreur : les sous-requêtes sont parfaitement\nautorisées en SQL. C'est une fonctionnalité\nessentielle.\n"
        },
        {
          "text": "La moyenne des moyennes des élèves",
          "correct": false,
          "feedback": "Erreur : `AVG(moyenne)` calcule effectivement la\nmoyenne des moyennes, mais la requête principale\nrenvoie les **noms** des élèves dépassant cette\nmoyenne, pas le calcul lui-même.\n"
        },
        {
          "text": "Les noms des élèves dont la moyenne est strictement supérieure à la moyenne de la classe",
          "correct": true,
          "feedback": "Bonne réponse : la sous-requête calcule la moyenne\nglobale, et la requête principale filtre les élèves\nau-dessus. C'est un usage très courant des\nsous-requêtes pour comparer à un agrégat.\n"
        }
      ],
      "explanation": "On peut utiliser `>=`, `<`, `=`, `IN` avec une sous-requête.\nLa sous-requête est exécutée **une fois** ici (elle ne\ndépend pas de la ligne courante), donc le SGBD optimise\nautomatiquement."
    },
    {
      "id": "q22",
      "difficulty": 3,
      "skills": [
        "normalisation",
        "redondance"
      ],
      "title": "Détection d'une redondance",
      "statement": "Une table `commandes(id, client_id, nom_client, prenom_client, produit, prix)`\npose un problème classique de schéma. Lequel ?",
      "options": [
        {
          "text": "La clé primaire est mal choisie",
          "correct": false,
          "feedback": "Erreur : `id` est un choix raisonnable de clé primaire\npour des commandes.\n"
        },
        {
          "text": "Les informations du client (nom, prénom) sont dupliquées à chaque commande, ce qui crée des incohérences potentielles si les noms sont mal mis à jour",
          "correct": true,
          "feedback": "Bonne réponse : c'est une **redondance**\nclassique. 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é séparément",
          "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",
          "correct": false,
          "feedback": "Erreur : le nombre de colonnes n'est pas un problème\nen soi. Le problème est ailleurs.\n"
        }
      ],
      "explanation": "La normalisation vise à éliminer les redondances pour\ngarantir la **cohérence**. Si on stocke le nom du client\ndans chaque commande et que le client se marie, il faut\nmettre à jour toutes ses commandes, risqué et coûteux."
    },
    {
      "id": "q23",
      "difficulty": 3,
      "skills": [
        "sql",
        "jointure-externe"
      ],
      "title": "Différence entre INNER et LEFT JOIN",
      "statement": "Quelle est la différence entre `INNER JOIN` et `LEFT JOIN` ?",
      "options": [
        {
          "text": "`INNER JOIN` ne garde que les paires appariées ; `LEFT JOIN` garde toutes les lignes de la table de gauche, complétées par `NULL` à droite si aucune correspondance",
          "correct": true,
          "feedback": "Bonne réponse : pour lister tous les clients avec\nleurs commandes (ou pas), on utilise `LEFT JOIN`. Avec\n`INNER JOIN`, les clients sans commande disparaissent\ndu résultat.\n"
        },
        {
          "text": "Aucune, ce sont des synonymes",
          "correct": false,
          "feedback": "Erreur : ils diffèrent sur la gestion des lignes non\nappariées de la table de gauche.\n"
        },
        {
          "text": "`LEFT JOIN` ne fonctionne que sur des tables triées",
          "correct": false,
          "feedback": "Erreur : aucune contrainte de tri n'est requise pour\nun `LEFT JOIN`.\n"
        },
        {
          "text": "`INNER JOIN` est plus rapide que `LEFT JOIN`",
          "correct": false,
          "feedback": "Erreur : la performance dépend du SGBD et du contexte.\nLa différence sémantique est ce qui compte avant\ntout.\n"
        }
      ],
      "explanation": "Le `JOIN` simple (équivalent de `INNER JOIN`)\napparie les lignes selon la condition `ON`. Le\n`LEFT JOIN` étend ce comportement en conservant\naussi les lignes de gauche sans correspondance, en\nles complétant par des `NULL` côté droit."
    },
    {
      "id": "q24",
      "difficulty": 3,
      "skills": [
        "transaction",
        "isolation"
      ],
      "title": "Problème de concurrence",
      "statement": "Sans isolation des transactions, deux transactions\nsimultanées qui modifient le même compte bancaire peuvent\nprovoquer :",
      "options": [
        {
          "text": "Une perte de mise à jour : l'effet d'une transaction est annulé par l'autre, faisant disparaître un dépôt",
          "correct": true,
          "feedback": "Bonne réponse : si T1 lit le solde ($1000$), puis T2\nlit aussi ($1000$), puis T1 ajoute $100$ et écrit\n$1100$, puis T2 ajoute $50$ et écrit $1050$, le dépôt\nde T1 est perdu. C'est la **perte de mise à jour**\nclassique.\n"
        },
        {
          "text": "Une erreur de syntaxe SQL",
          "correct": false,
          "feedback": "Erreur : la syntaxe est correcte. Le problème est de\n**cohérence**, pas syntaxique.\n"
        },
        {
          "text": "Une duplication automatique des données",
          "correct": false,
          "feedback": "Erreur : aucune duplication automatique. C'est plutôt\nde la perte d'information.\n"
        },
        {
          "text": "Une accélération de l'exécution des deux transactions",
          "correct": false,
          "feedback": "Erreur : sans isolation, on a au contraire des\nincohérences. La gestion correcte est plus lente mais\nfiable.\n"
        }
      ],
      "explanation": "La gestion des accès concurrents est l'un des\nservices principaux d'un SGBD. Le système prend en\ncharge l'enchaînement des opérations pour garantir\nla cohérence des données, même quand plusieurs\nutilisateurs ou programmes les manipulent en même\ntemps."
    },
    {
      "id": "q25",
      "difficulty": 3,
      "skills": [
        "sql",
        "optimisation"
      ],
      "title": "Index",
      "statement": "Pourquoi crée-t-on des **index** sur les colonnes\nfréquemment utilisées dans des `WHERE` ou des jointures ?",
      "options": [
        {
          "text": "Pour accélérer la recherche en évitant un parcours complet de la table",
          "correct": true,
          "feedback": "Bonne réponse : un index est une structure\nauxiliaire qui permet de retrouver rapidement\nles lignes correspondant à une valeur donnée,\nsans avoir à parcourir toute la table.\n"
        },
        {
          "text": "Pour économiser de l'espace de stockage",
          "correct": false,
          "feedback": "Erreur : c'est l'inverse, un index **consomme** de\nl'espace supplémentaire. Le bénéfice est ailleurs.\n"
        },
        {
          "text": "Pour rendre les données automatiquement triées",
          "correct": false,
          "feedback": "Erreur : un index trie en interne, mais les données\nde la table elle-même restent dans leur ordre\noriginal.\n"
        },
        {
          "text": "Pour chiffrer les données sensibles",
          "correct": false,
          "feedback": "Erreur : les index sont des structures de performance,\npas de sécurité.\n"
        }
      ],
      "explanation": "Compromis classique : un index accélère **les lectures**\nmais ralentit **les écritures** (chaque insertion ou mise\nà jour doit aussi mettre à jour l'index). On crée donc des\nindex uniquement sur les colonnes vraiment souvent\ninterrogées."
    },
    {
      "id": "q26",
      "difficulty": 2,
      "skills": [
        "sql",
        "group-by"
      ],
      "title": "Regroupement avec GROUP BY",
      "statement": "Pour obtenir le **nombre d'élèves par classe** dans la\ntable `eleves(id, nom, classe)`, quelle requête utiliser ?",
      "options": [
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves;\n```\n",
          "correct": false,
          "feedback": "Erreur : sans `GROUP BY`, mélanger une colonne et une\nfonction d'agrégation provoque une erreur SQL ou un\nrésultat incohérent (selon le SGBD). Il faut grouper\nexplicitement par `classe`.\n"
        },
        {
          "text": "```\nSELECT DISTINCT classe, COUNT(*)\nFROM eleves;\n```\n",
          "correct": false,
          "feedback": "Erreur : `DISTINCT` élimine les doublons après calcul,\nil ne sert pas à regrouper pour appliquer une\nagrégation. La syntaxe correcte est `GROUP BY`.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(eleves)\nFROM eleves\nGROUP BY classe;\n```\n",
          "correct": false,
          "feedback": "Erreur : `COUNT(eleves)` n'est pas valide :\n`eleves` est le nom de la table, pas une colonne.\nPour compter les lignes, on utilise `COUNT(*)` ou une\ncolonne précise (`COUNT(id)`).\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nGROUP BY classe;\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : `GROUP BY` regroupe les lignes par\nvaleur distincte de `classe`. La fonction d'agrégation\n`COUNT(*)` compte les lignes de chaque groupe. Le\nrésultat est une ligne par classe avec son effectif.\n"
        }
      ],
      "explanation": "Schéma général : `SELECT colonne, AGREGATION(...) FROM\ntable GROUP BY colonne`. Toutes les colonnes du `SELECT`\nqui ne sont pas dans une fonction d'agrégation doivent\napparaître dans le `GROUP BY`. C'est la règle dite\n« des colonnes visibles »."
    },
    {
      "id": "q27",
      "difficulty": 3,
      "skills": [
        "sql",
        "having"
      ],
      "title": "HAVING vs WHERE",
      "statement": "Pour ne garder que les classes ayant **au moins\n$25$ élèves**, on doit filtrer un résultat agrégé. Quelle\nest la requête correcte ?",
      "options": [
        {
          "text": "```\nSELECT classe\nFROM eleves\nGROUP BY classe\nWHERE COUNT(*) >= 25;\n```\n",
          "correct": false,
          "feedback": "Erreur : la clause `WHERE` doit toujours apparaître\n**avant** `GROUP BY` dans la syntaxe SQL. De plus,\non ne peut pas y mettre une fonction d'agrégation.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nWHERE COUNT(*) >= 25\nGROUP BY classe;\n```\n",
          "correct": false,
          "feedback": "Erreur : `WHERE` ne peut pas porter sur une fonction\nd'agrégation, qui n'est calculée qu'**après** le\nregroupement. Cette requête échoue avec une erreur\nSQL. C'est précisément le rôle de `HAVING`.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nHAVING classe COUNT(*) >= 25;\n```\n",
          "correct": false,
          "feedback": "Erreur : la syntaxe est invalide. `HAVING` doit\ncontenir une condition logique, et il manque le\n`GROUP BY` qui définit les groupes sur lesquels\nporte le `HAVING`.\n"
        },
        {
          "text": "```\nSELECT classe, COUNT(*)\nFROM eleves\nGROUP BY classe\nHAVING COUNT(*) >= 25;\n```\n",
          "correct": true,
          "feedback": "Bonne réponse : `HAVING` filtre les groupes après\nagrégation, contrairement à `WHERE` qui filtre les\nlignes avant agrégation. Pour porter sur le résultat\nde `COUNT(*)`, il faut utiliser `HAVING`.\n"
        }
      ],
      "explanation": "Ordre canonique des clauses :\n`SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY`.\nMnémonique : `WHERE` filtre les lignes individuelles avant\nregroupement ; `HAVING` filtre les groupes après agrégation."
    },
    {
      "id": "q28",
      "difficulty": 2,
      "skills": [
        "transaction",
        "acid"
      ],
      "title": "Propriétés ACID",
      "statement": "Les propriétés ACID des transactions garantissent leur\nbon comportement dans un SGBD. Que désigne le « C » de\nACID ?",
      "options": [
        {
          "text": "Compression",
          "correct": false,
          "feedback": "Erreur : la compression est une optimisation de\nstockage, sans rapport avec les transactions.\n"
        },
        {
          "text": "Confidentialité",
          "correct": false,
          "feedback": "Erreur : la confidentialité relève de la sécurité\n(chiffrement, contrôle d'accès), pas des propriétés\nACID des transactions. Ce sont deux dimensions\ndifférentes du SGBD.\n"
        },
        {
          "text": "Connexion",
          "correct": false,
          "feedback": "Erreur : la connexion à la base est un mécanisme\nd'accès, pas une propriété des transactions. Le\n« C » désigne une propriété logique des transactions.\n"
        },
        {
          "text": "Cohérence",
          "correct": true,
          "feedback": "Bonne réponse : la **cohérence** garantit qu'une\ntransaction fait passer la base d'un état cohérent\n(toutes les contraintes respectées) à un autre état\ncohérent. Si une transaction violerait une\ncontrainte d'intégrité, elle est rejetée\nintégralement.\n"
        }
      ],
      "explanation": "Les quatre propriétés ACID sont : **Atomicité** (tout\nou rien), **Cohérence** (contraintes respectées),\n**Isolation** (transactions concurrentes invisibles\nles unes aux autres), **Durabilité** (modifications\npersistées même en cas de panne). Indispensable pour\nles bases métier (banque, santé, stock)."
    },
    {
      "id": "q29",
      "difficulty": 2,
      "skills": [
        "sql",
        "ddl"
      ],
      "title": "DROP, DELETE, TRUNCATE",
      "statement": "Quelle est la différence entre `DROP TABLE eleves`,\n`DELETE FROM eleves` et `TRUNCATE TABLE eleves` ?",
      "options": [
        {
          "text": "Aucune n'est valide en SQL standard\n",
          "correct": false,
          "feedback": "Erreur : les trois sont des commandes SQL valides.\n`TRUNCATE` est légèrement moins répandu que les\ndeux autres, mais reste standard dans la plupart\ndes SGBD relationnels.\n"
        },
        {
          "text": "`DROP` et `DELETE` sont identiques ; seul\n`TRUNCATE` est différent (il vide aussi les\nfichiers de log)\n",
          "correct": false,
          "feedback": "Erreur : `DROP TABLE` et `DELETE FROM` ne sont\n**pas** équivalents. Le premier supprime la\nstructure de la table ; le second ne supprime\nque les lignes.\n"
        },
        {
          "text": "Les trois commandes sont équivalentes\n",
          "correct": false,
          "feedback": "Erreur : elles ont des effets très différents,\ncomme le détaille la bonne réponse. Confondre\n`DROP` et `DELETE` peut être catastrophique en\nproduction : `DROP` supprime tout, structure\ncomprise.\n"
        },
        {
          "text": "`DROP TABLE` supprime la table entière (structure et\ncontenu) ; `DELETE FROM` (sans `WHERE`) supprime\ntoutes les lignes mais conserve la structure (les\ncontraintes et les types restent) ; `TRUNCATE TABLE`\nvide rapidement le contenu en conservant la\nstructure (souvent plus rapide que `DELETE`)\n",
          "correct": true,
          "feedback": "Bonne réponse : trois commandes très différentes.\n`DROP` est destructif sur le schéma : la table\nn'existe plus du tout après. `DELETE` est une\nopération transactionnelle au cas par cas (logge\nchaque suppression). `TRUNCATE` est une opération\nen bloc, sans logging détaillé, généralement plus\nrapide pour vider une grosse table.\n"
        }
      ],
      "explanation": "Bonne pratique : utiliser `DELETE FROM ... WHERE` pour\ndes suppressions ciblées et sécurisées ; `TRUNCATE` pour\nvider rapidement une table en conservant son schéma ;\n`DROP TABLE` uniquement quand on veut effectivement se\ndébarrasser de la table elle-même. Toujours sauvegarder\navant `DROP`."
    },
    {
      "id": "q30",
      "difficulty": 2,
      "skills": [
        "sql",
        "trace-select"
      ],
      "title": "Trace d'une requête SELECT avec WHERE",
      "statement": "On considère la table `eleves` suivante :\n\n| 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\n```sql\nSELECT nom, moyenne\nFROM eleves\nWHERE classe = '1A' AND moyenne >= 12\nORDER BY moyenne DESC;\n```",
      "options": [
        {
          "text": "Cinq lignes (toutes les lignes de la table)\n",
          "correct": false,
          "feedback": "Erreur : on a oublié le filtrage par `WHERE`.\nSans filtre, on aurait bien toutes les\nlignes, mais ici la requête restreint aux\nélèves de `1A` avec une moyenne d'au moins\n$12$.\n"
        },
        {
          "text": "Trois lignes : `('Alice', 14.5)`,\n`('Chloé', 16.0)`, `('Émma', 11.5)`\n",
          "correct": false,
          "feedback": "Erreur : Émma est en `1A` mais sa moyenne\n($11{,}5$) est inférieure à $12$, donc elle\nne satisfait pas la condition `moyenne >= 12`.\nDe plus, l'ordre n'est pas décroissant ici.\n"
        },
        {
          "text": "Deux lignes : `('Chloé', 16.0)` puis\n`('Alice', 14.5)`\n",
          "correct": true,
          "feedback": "Bonne réponse. Étape 1, le filtre\n`WHERE classe = '1A' AND moyenne >= 12`\nretient les élèves de la classe `1A` avec\nune moyenne d'au moins $12$ : Alice ($14{,}5$)\net Chloé ($16$). Émma est en `1A` mais sa\nmoyenne ($11{,}5$) ne satisfait pas la\ndeuxième condition. Étape 2, on ne garde\nque les colonnes `nom` et `moyenne`. Étape 3,\non trie par `moyenne DESC` : Chloé d'abord,\npuis Alice.\n"
        },
        {
          "text": "Une seule ligne : `('Chloé', 16.0)`\n",
          "correct": false,
          "feedback": "Erreur : Alice satisfait elle aussi les deux\nconditions ($14{,}5 \\geq 12$ et classe\n`1A`). Elle doit donc apparaître dans le\nrésultat, après Chloé du fait du tri\ndécroissant.\n"
        }
      ],
      "explanation": "Méthode systématique pour évaluer une requête\nSQL : (1) appliquer le filtre `WHERE` pour ne\nconserver que les lignes pertinentes ; (2)\nprojeter sur les colonnes du `SELECT` ; (3)\ntrier selon `ORDER BY` si présent. Cette\nséquence d'étapes est l'algèbre relationnelle en\naction."
    },
    {
      "id": "q31",
      "difficulty": 2,
      "skills": [
        "sql",
        "trace-join"
      ],
      "title": "Trace d'une jointure",
      "statement": "On dispose des deux tables suivantes :\n\nTable `clients` :\n\n| id | nom    |\n|----|--------|\n| 1  | Alice  |\n| 2  | Bob    |\n| 3  | Chloé  |\n\nTable `commandes` :\n\n| id | client_id | produit  |\n|----|-----------|----------|\n| 10 | 1         | Livre    |\n| 11 | 2         | Cahier   |\n| 12 | 1         | Stylo    |\n\nCombien de lignes renvoie la requête suivante ?\n\n```sql\nSELECT c.nom, cmd.produit\nFROM clients c\nJOIN commandes cmd ON c.id = cmd.client_id;\n```",
      "options": [
        {
          "text": "2 lignes (une seule par client présent dans\nles commandes)\n",
          "correct": false,
          "feedback": "Erreur : Alice a **deux** commandes, donc\nelle apparaît deux fois dans le résultat\n(avec `'Livre'` et avec `'Stylo'`). La\njointure ne fusionne pas les commandes\nd'un même client.\n"
        },
        {
          "text": "3 lignes",
          "correct": true,
          "feedback": "Bonne réponse : la jointure interne (`JOIN`,\nsynonyme de `INNER JOIN`) ne garde que les\npaires `(client, commande)` où la condition\n`c.id = cmd.client_id` est vraie. Alice\n(id $1$) a deux commandes ($10$ et $12$),\nBob (id $2$) en a une, Chloé n'en a aucune\net disparaît du résultat. Total : trois\nlignes. Le résultat est :\n`('Alice', 'Livre')`, `('Bob', 'Cahier')`,\n`('Alice', 'Stylo')`.\n"
        },
        {
          "text": "9 lignes (le produit cartésien $3 \\times 3$)",
          "correct": false,
          "feedback": "Erreur : ce serait le résultat sans la\ncondition `ON c.id = cmd.client_id`. Avec\nla condition, on ne garde que les paires\ncohérentes, soit trois lignes ici. Le\nproduit cartésien apparaît avec\n`FROM clients, commandes` (sans `JOIN`).\n"
        },
        {
          "text": "4 lignes",
          "correct": false,
          "feedback": "Erreur : seules trois commandes existent,\ndonc au plus trois lignes peuvent\napparaître. Une ligne supplémentaire\nn'apparaîtrait que si l'on utilisait un\n`LEFT JOIN` qui inclurait Chloé sans\ncommande (avec `NULL` côté commandes).\n"
        }
      ],
      "explanation": "Pour faire apparaître Chloé (qui n'a aucune\ncommande) dans le résultat, il faudrait\nutiliser un `LEFT JOIN`. La ligne\ncorrespondante aurait alors `NULL` dans la\ncolonne `cmd.produit`. Cela permet de\nrépondre à la question « tous les clients,\navec leurs commandes éventuelles »."
    },
    {
      "id": "q32",
      "difficulty": 3,
      "skills": [
        "sql",
        "group-by",
        "having-trace"
      ],
      "title": "GROUP BY avec HAVING en pratique",
      "statement": "Avec la table `eleves(id, nom, classe, moyenne)`\ncontenant trente élèves répartis entre trois\nclasses (`1A`, `1B`, `1C`), que renvoie la\nrequête suivante ?\n\n```sql\nSELECT classe, AVG(moyenne) AS moy_classe\nFROM eleves\nGROUP BY classe\nHAVING AVG(moyenne) > 12\nORDER BY moy_classe DESC;\n```",
      "options": [
        {
          "text": "La liste des classes dont la moyenne de\nla classe est strictement supérieure à\n$12$, avec leur moyenne, triée par\nmoyenne décroissante\n",
          "correct": true,
          "feedback": "Bonne réponse. Étape 1, `GROUP BY classe`\nregroupe les élèves par classe. Étape 2,\n`AVG(moyenne)` calcule la moyenne de chaque\ngroupe. Étape 3, `HAVING AVG(moyenne) > 12`\nfiltre les groupes (et non les lignes\nindividuelles) selon cette condition.\nÉtape 4, `ORDER BY moy_classe DESC` trie\nle résultat.\n"
        },
        {
          "text": "Une erreur SQL, car on ne peut pas mettre\nun alias dans `ORDER BY`\n",
          "correct": false,
          "feedback": "Erreur : SQL autorise tout à fait\nd'utiliser un alias défini dans le\n`SELECT` (ici `moy_classe`) au sein de\n`ORDER BY`. La requête est valide.\n"
        },
        {
          "text": "Toutes les classes, avec la somme des\nmoyennes des élèves\n",
          "correct": false,
          "feedback": "Erreur : la fonction utilisée est `AVG`\n(moyenne), pas `SUM` (somme). Les deux\nfonctions d'agrégation sont distinctes.\nDe plus, le `HAVING` filtrerait certaines\nclasses.\n"
        },
        {
          "text": "La liste des élèves dont la moyenne\nindividuelle est supérieure à $12$\n",
          "correct": false,
          "feedback": "Erreur : c'est ce que ferait un simple\n`WHERE moyenne > 12` sans `GROUP BY`. Ici,\nla fonction d'agrégation `AVG(moyenne)`\ncombinée à `GROUP BY classe` calcule la\nmoyenne **par classe**, pas par élève.\n"
        }
      ],
      "explanation": "Distinction essentielle : `WHERE` filtre les\nlignes individuelles **avant** l'agrégation ;\n`HAVING` filtre les groupes **après**\nl'agrégation. Si on voulait restreindre aux\nélèves majeurs avant de calculer les moyennes,\non aurait combiné les deux :\n`WHERE age >= 18 ... HAVING AVG(moyenne) > 12`."
    },
    {
      "id": "q33",
      "difficulty": 2,
      "skills": [
        "sql",
        "vues"
      ],
      "title": "Notion de vue",
      "statement": "Une **vue** (`VIEW`) en SQL est :",
      "options": [
        {
          "text": "Une requête nommée dont le résultat se\ncomporte comme une table virtuelle :\nchaque interrogation de la vue\nréexécute la requête sous-jacente sur les\ndonnées actuelles\n",
          "correct": true,
          "feedback": "Bonne réponse : on définit une vue avec\n`CREATE VIEW majeurs AS SELECT ... FROM\neleves WHERE age >= 18;`. Ensuite,\n`SELECT * FROM majeurs` renvoie le résultat\nactualisé. Avantages : factoriser des\nrequêtes complexes, masquer certaines\ncolonnes (sécurité), simplifier l'écriture\ndes requêtes. La vue ne stocke pas les\ndonnées, elle stocke la requête.\n"
        },
        {
          "text": "Une représentation graphique des relations\nentre les tables\n",
          "correct": false,
          "feedback": "Erreur : une représentation graphique\ns'appelle un **schéma** ou un diagramme\nentité-association, pas une vue. La vue est\nune notion logique (une requête nommée),\npas une représentation visuelle.\n"
        },
        {
          "text": "Un type particulier d'index pour accélérer\nles recherches\n",
          "correct": false,
          "feedback": "Erreur : un index est une structure\nauxiliaire de performance ; une vue est une\nrequête nommée. Ces deux notions sont\ntotalement distinctes, même si certains\nSGBD permettent des « vues matérialisées »\nqui s'apparentent à un index.\n"
        },
        {
          "text": "Une copie figée des données à un instant\ndonné\n",
          "correct": false,
          "feedback": "Erreur : ce serait une **table** (par\nexemple créée via `CREATE TABLE ... AS\nSELECT ...`). La vue, elle, ne contient pas\nde données : elle contient une requête,\nréévaluée à chaque interrogation pour\nrefléter l'état courant des tables source.\n"
        }
      ],
      "explanation": "Les vues facilitent aussi la **sécurité** : on\npeut accorder l'accès à une vue qui masque\ncertaines colonnes sensibles, sans donner\naccès aux tables sous-jacentes. Variante\navancée : la **vue matérialisée** stocke\neffectivement le résultat, et est rafraîchie\npériodiquement ; elle combine vue et cache."
    },
    {
      "id": "q34",
      "difficulty": 3,
      "skills": [
        "sql",
        "schema-relationnel"
      ],
      "title": "Lecture d'un schéma relationnel",
      "statement": "On considère le schéma relationnel suivant :\n\n```\neleves(id_eleve, nom, prenom, id_classe#)\nclasses(id_classe, niveau, salle)\nnotes(id_eleve#, id_devoir#, note)\ndevoirs(id_devoir, matiere, date)\n```\n\nLe `#` désigne une clé étrangère. Quelle\naffirmation est correcte ?",
      "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 table `notes` a une clé primaire\ncomposite `(id_eleve, id_devoir)`, et ses\ndeux composantes sont des clés étrangères\nréférençant respectivement `eleves` et\n`devoirs`\n",
          "correct": true,
          "feedback": "Bonne réponse : c'est la lecture correcte\ndu schéma. La table `notes` représente une\nassociation ternaire (un élève, un devoir,\nune 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 n-aire** typique, qui ne\npeut pas se modéliser par une simple clé\nétrangère ajoutée à une table existante."
    },
    {
      "id": "q35",
      "difficulty": 3,
      "skills": [
        "securite",
        "injection-sql"
      ],
      "title": "Injection SQL",
      "statement": "Un programme construit la requête SQL suivante\nen concaténant directement la chaîne\n`nom_saisi` reçue de l'utilisateur :\n\n```python\nrequete = \"SELECT * FROM users WHERE nom = '\" + nom_saisi + \"';\"\n```\n\nSi l'utilisateur saisit\n`' OR '1'='1`, que se passe-t-il ?",
      "options": [
        {
          "text": "Le SGBD détecte l'injection et rejette la\nrequête\n",
          "correct": false,
          "feedback": "Erreur : la requête est syntaxiquement\nparfaitement valide. Le SGBD n'a aucun\nmoyen de détecter qu'elle vient d'une\nconcaténation dangereuse plutôt que d'une\nintention légitime. C'est précisément ce\nqui rend l'injection SQL si pernicieuse.\n"
        },
        {
          "text": "Python lève une exception au moment de la\nconcaténation\n",
          "correct": false,
          "feedback": "Erreur : Python concatène sans aucun\nproblème les chaînes, quel que soit leur\ncontenu. Aucune exception n'est levée.\nC'est précisément le drame : tout se passe\nsilencieusement.\n"
        },
        {
          "text": "La requête devient\n`SELECT * FROM users WHERE nom = '' OR '1'='1';`,\nce qui sélectionne toutes les lignes\nde la table : c'est une injection SQL\n",
          "correct": true,
          "feedback": "Bonne réponse : la concaténation naïve fait\nsortir la chaîne saisie de son rôle de\nsimple donnée et l'injecte dans la\nstructure SQL. La condition `'1'='1'`\nétant toujours vraie, l'opérateur `OR` la\nfait court-circuiter le filtre. C'est la\nforme la plus simple d'injection SQL,\ncélèbre attaque qui a touché de nombreux\nsites mal protégés. La parade standard :\nutiliser des **requêtes préparées** avec\ndes paramètres liés\n(`cursor.execute(req, (nom_saisi,))`).\n"
        },
        {
          "text": "La requête ne retourne aucune ligne\n",
          "correct": false,
          "feedback": "Erreur : c'est l'inverse. La condition\n`'1'='1'` étant toujours vraie, **toutes**\nles lignes de la table sont retournées,\ny compris des données potentiellement\nsensibles que l'utilisateur ne devrait\npas voir.\n"
        }
      ],
      "explanation": "L'injection SQL reste, en 2026, l'une des\nvulnérabilités les plus exploitées dans les\napplications web. La parade est connue depuis\nlongtemps : ne **jamais** construire une\nrequête SQL par concaténation de chaînes\nutilisateur, mais utiliser systématiquement\ndes **requêtes préparées** où les paramètres\nsont passés séparément\n(`cursor.execute(\"... WHERE nom = ?\", (nom_saisi,))`).\nLe SGBD échappe alors automatiquement les\ncaractères dangereux."
    },
    {
      "id": "q36",
      "difficulty": 2,
      "skills": [
        "sql",
        "update-trace"
      ],
      "title": "Trace d'un UPDATE ciblé",
      "statement": "On part de la table `comptes` suivante :\n\n| 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\n```sql\nUPDATE comptes\nSET solde = solde * 1.05\nWHERE solde >= 1000;\n```\n\nQuel est le contenu de la table après\nexécution ?",
      "options": [
        {
          "text": "Bob est supprimé de la table\n",
          "correct": false,
          "feedback": "Erreur : `UPDATE` ne supprime jamais de\nligne, il ne fait que modifier des\nvaleurs. Pour supprimer une ligne, il\nfaudrait utiliser `DELETE FROM comptes\nWHERE solde < 1000;`.\n"
        },
        {
          "text": "Toutes les lignes voient leur solde\nmultiplié par $1{,}05$\n",
          "correct": false,
          "feedback": "Erreur : c'est ce qui se passerait si la\nclause `WHERE` était absente. Avec\n`WHERE solde >= 1000`, seules les lignes\nsatisfaisant la condition sont\nmodifiées. Bob ($500$) ne fait pas partie\ndu lot.\n"
        },
        {
          "text": "Alice : $1050$, Bob : $500$ (inchangé),\nChloé : $2100$\n",
          "correct": true,
          "feedback": "Bonne réponse : la clause `WHERE solde >= 1000`\nsélectionne les lignes où le solde est\nd'au moins $1000$ (Alice et Chloé). Pour\nchacune, on applique `solde = solde * 1.05`.\nAlice passe de $1000$ à $1050$, Chloé de\n$2000$ à $2100$. Bob, dont le solde est\ninférieur à $1000$, n'est pas affecté.\n"
        },
        {
          "text": "La table reste inchangée car la clause\n`WHERE` est trop restrictive\n",
          "correct": false,
          "feedback": "Erreur : Alice ($1000$) et Chloé ($2000$)\nsatisfont bien la condition `solde >= 1000`,\ndonc leurs lignes sont modifiées. La\ncondition n'est pas trop restrictive : elle\nest ciblée.\n"
        }
      ],
      "explanation": "Schéma général d'un `UPDATE` : (1) la clause\n`WHERE` sélectionne les lignes affectées ; (2)\nla clause `SET` indique comment modifier ces\nlignes ; (3) le SGBD applique la modification\natomiquement (toutes les lignes en une seule\nopération). Sans `WHERE`, **toutes** les lignes\nsont modifiées, ce qui est rarement souhaité\net souvent catastrophique."
    }
  ]
}