NSI · Terminale

Bases de données et SQL : relier, interroger, modifier sans erreur

Une base relationnelle organise des données en tables reliées par des clés. Le schéma fixe les règles ; les requêtes SQL permettent de lire et de modifier les lignes. Avec des ateliers fictifs, apprends à prévoir une jointure, contrôler une contrainte et annuler une modification incomplète.

Explications et exemples en accès libre. Ateliers, quiz et cartes avec l’abonnement.

Étude en accès libre
Environ 50 min à 1 h 35
Progression
6 étapes guidées
Vérification
12 questions
Rappel actif
12 cartes
Prévoir mon tempsExplications et exemples en accès libre

Une première étude des explications, schémas, exemples résolus et erreurs expliquées. Les activités, productions, quiz et cartes réservés à l’abonnement ne sont pas comptés ici.

Comprendre2267 mots d’explication et 5 schémas
15 à 25 min
Étudier les exemples et les erreurs18 cas, exemples et activités guidés
36 à 66 min

Étude du cours en accès libre, environ50 min à 1 h 35

Voir le calcul et le temps par étape

Le calcul suit automatiquement les explications et les tâches présentes dans le cours. Ses coefficients sont des repères de planification, pas des temps mesurés auprès d’élèves.

  • Lecture active : 2267 mots, à raison de 160 à 220 mots par minute.
  • Schémas : 5, avec 1 à 2 min pour lire chacun.
  • Exemple guidé : 6 × 2 à 4 min.
  • Suivre le code résolu et ses tests : 6 × 3 à 5 min.
  • Comprendre une erreur expliquée : 6 × 1 à 2 min.
  1. Du tableau au modèle : quelles valeurs sont autorisées ?8 à 16 min
  2. Des clés pour relier, des contraintes pour rester cohérent8 à 16 min
  3. Prévoir SELECT : filtrer les lignes, choisir les colonnes, ordonner8 à 16 min
  4. Lire une jointure comme une rencontre de clés8 à 16 min
  5. Modifier une base : la bonne portée et le tout ou rien8 à 16 min
  6. Le SGBD protège des règles, l’application protège un accès7 à 14 min

Les sous-totaux sont arrondis à la minute, puis additionnés. La fourchette totale est élargie aux cinq minutes voisines. Les étapes ci-dessus comprennent leurs explications et exemples ; elles ne s’ajoutent pas une seconde fois au total.

Adapte ce repère à tes acquis et au soin apporté aux exercices. Les pauses, les reprises, la consultation des sources externes et les révisions suivantes s’ajoutent selon tes besoins.

Avec les ateliers, le projet, le quiz et les cartes : environ 2 h 50 à 4 h 50, à répartir sur plusieurs séances.

Objectifs du cours

Ce que tu vas savoir faire

  • Distinguer relation, attribut, domaine, schéma et instance sur une même base.
  • Choisir des clés et prévenir les anomalies d’insertion, de modification et de suppression.
  • Écrire et vérifier SELECT, WHERE, JOIN, DISTINCT, ORDER BY et des agrégats simples.
  • Utiliser INSERT, UPDATE et DELETE avec une portée précise, puis une transaction tout ou rien.
  • Distinguer cohérence des données, paramètres SQL, droits d’accès et services du SGBD.
01

Étape du cours · 8 à 16 min

Du tableau au modèle : quelles valeurs sont autorisées ?

Un atelier a un identifiant, un titre, une matière et une durée. Dans le modèle relationnel, chaque atelier est un n-uplet ; chaque propriété nommée est un attribut. Une relation est un ensemble de n-uplets décrits par les mêmes attributs. On la représente par une table, mais la position d’une ligne ne constitue pas son identité. L’atelier 1 reste le même lorsque l’affichage est trié autrement.

Le schéma Atelier(id, titre, matiere, duree) décrit la structure et ses règles. L’instance est le contenu à un instant donné, par exemple Graphes et SQL. Ajouter Logique change cette instance, pas le schéma. Ajouter un attribut salle modifie le schéma. Pour chaque attribut, indique aussi un domaine : ici un identifiant entier de 1 à 9 999, un titre non vide, une matière parmi NSI, HLP et MATHS, une durée entière de 15 à 240 minutes.

Une déclaration de type n’exprime pas toutes ces règles. NOT NULL impose une valeur et CHECK impose une condition. Le script utilise INT NOT NULL PRIMARY KEY pour l’identifiant fourni par le programme. Avec SQLite, l’affinité entière peut convertir une entrée numérique ; typeof(duree) = 'integer' vérifie donc le type stocké, pas le type Python d’origine. La durée 15,5 doit être refusée, tandis que 15 et 240 sont autorisées.

NULL marque une valeur absente ou inconnue : ce n’est ni zéro ni le texte vide. Si une durée est facultative dans un autre schéma, la recherche des valeurs absentes utilise IS NULL, pas = NULL. Une comparaison ordinaire avec NULL ne donne pas vrai. Le transfert de cette étape rend cette différence observable, puis distingue le nombre de lignes du nombre de durées renseignées.

Les scripts se copient dans Python et créent une base SQLite en mémoire : tu peux recommencer avec les mêmes données fictives. La fonction connecter choisit explicitement le mode permettant BEGIN, COMMIT et ROLLBACK. Commence par lire les commandes SQL à l’intérieur des chaînes ; Python sert à installer les exemples et à comparer les résultats. Une absence d’erreur prouve seulement que l’instruction a été acceptée : lis aussi l’état obtenu.

Voir pour comprendre

Un schéma, quatre domaines

Les lignes de ce tableau décrivent les attributs ; elles ne sont pas des ateliers.

Un schéma, quatre domaines
AttributDomaine choisiExemple
idEntier de 1 à 9 999 ; clé primaire1
titreTexte non videGraphes
matiereNSI, HLP ou MATHSNSI
dureeEntier de 15 à 240 minutes90

Lis le schéma. Lis une ligne par attribut. La dernière colonne donne une valeur possible, pas une nouvelle règle.

Une nouvelle valeur conforme change l’instance ; un nouvel attribut change le schéma.

Schéma original Maxdecours. Données entièrement fictives. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Une durée valide ne prouve pas le domaine

  1. Lis (1, 'Graphes', 'NSI', 90) : quatre valeurs correspondent aux quatre attributs, dans l’ordre explicite de l’insertion.
  2. Remplace 90 par 15 : la borne basse est incluse. Avec 240, la borne haute l’est aussi.
  3. Essaie 14, 241, 15.5 et None : les bornes, le type stocké et NOT NULL ont chacun une raison distincte de refuser.
  4. Ajoute (3, 'Logique', 'NSI', 45) : la liste des attributs est inchangée. Trois lignes remplacent l’instance précédente de deux lignes.

Conclusion. Un domaine est une règle pour toutes les valeurs futures, pas un constat sur les données présentes.

Laboratoire de code

Languepython

ButConstruire le schéma, inspecter ses attributs et éprouver le domaine entier des durées dans SQLite.

Code solution
import sqlite3

def connecter():
    options = {"isolation_level": None}
    if hasattr(sqlite3, "LEGACY_TRANSACTION_CONTROL"):
        options["autocommit"] = sqlite3.LEGACY_TRANSACTION_CONTROL
    return sqlite3.connect(":memory:", **options)

def creer_base_schema():
    c = connecter()
    c.execute("""
        CREATE TABLE Atelier (
            id INT NOT NULL PRIMARY KEY
               CHECK (typeof(id) = 'integer' AND id BETWEEN 1 AND 9999),
            titre TEXT NOT NULL CHECK (length(titre) > 0),
            matiere TEXT NOT NULL CHECK (matiere IN ('NSI', 'HLP', 'MATHS')),
            duree INT NOT NULL
               CHECK (typeof(duree) = 'integer' AND duree BETWEEN 15 AND 240)
        )
    """)
    c.executemany('INSERT INTO Atelier VALUES (?, ?, ?, ?)', [
        (1, 'Graphes', 'NSI', 90),
        (2, 'SQL', 'NSI', 60),
    ])
    return c

def schema_atelier(c):
    return [colonne[1] for colonne in c.execute('PRAGMA table_info(Atelier)')]

def ateliers(c):
    return c.execute('SELECT id, titre, matiere, duree FROM Atelier ORDER BY id').fetchall()

def ajouter_atelier(c, ligne):
    c.execute('INSERT INTO Atelier (id, titre, matiere, duree) VALUES (?, ?, ?, ?)', ligne)

Tests

c = creer_base_schema()
assert schema_atelier(c) == ['id', 'titre', 'matiere', 'duree']
assert len(ateliers(c)) == 2
for i, duree in ((3, 15), (4, 240)):
    ajouter_atelier(c, (i, 'Test', 'NSI', duree))
assert [r[3] for r in ateliers(c)] == [90, 60, 15, 240]
avant = ateliers(c)
for duree in (14, 241, 15.5, None):
    try:
        ajouter_atelier(c, (5, 'Test', 'NSI', duree))
    except sqlite3.IntegrityError:
        pass
    else:
        raise AssertionError('durée hors domaine acceptée')
    assert ateliers(c) == avant
assert schema_atelier(c) == ['id', 'titre', 'matiere', 'duree']
c.close()

Trace

  • La création fixe quatre attributs et des contraintes ; l’instance contient deux ateliers.
  • 15 et 240 passent les bornes inclusives ; 15.5 échoue au contrôle du type stocké.
  • Un refus conserve les lignes précédentes ; une insertion valide ne change pas le schéma.

Clinique de bogue

Indice observéUn atelier de 15,5 minutes est accepté alors que la durée annoncée doit être entière.

CauseL’affinité INT de SQLite ne suffit pas à interdire une valeur réelle non entière. La condition de bornes laisse passer 15,5. Il faut vérifier le type stocké en plus des bornes, sans retirer NOT NULL.

02

Étape du cours · 8 à 16 min

Des clés pour relier, des contraintes pour rester cohérent

Le titre d’un atelier peut changer ou être partagé par deux ateliers. On choisit donc id pour identifier chaque atelier sans ambiguïté. Une clé primaire peut comporter plusieurs attributs : dans Inscription(atelier_id, groupe_code), le couple identifie une inscription. Chaque atelier peut accueillir plusieurs groupes et chaque groupe plusieurs ateliers ; rendre atelier_id seul unique interdirait à tort le deuxième groupe.

Une clé étrangère indique à quelle clé d’une autre table se réfère une valeur. Inscription.atelier_id référence Atelier.id ; Inscription.groupe_code référence Groupe.code. Une valeur peut donc apparaître plusieurs fois dans une colonne de clé étrangère. Pour qu’une inscription désigne réellement un atelier et un groupe, on impose aussi NOT NULL sur les deux colonnes. Une référence absente n’est pas un identifiant inconnu.

Le contrôle doit être effectivement activé. Chaque connexion SQLite du laboratoire exécute PRAGMA foreign_keys = ON, puis vérifie la réponse avant toute transaction. Le schéma ajoute explicitement NOT NULL à la clé composée : certaines tables SQLite ordinaires acceptent sinon des NULL dans une telle clé primaire. La théorie dit « identifiant unique et renseigné » ; le test doit vérifier que l’implantation respecte bien ce contrat.

Pourquoi séparer les tables ? Imagine une table aplatie contenant le titre et la durée de Graphes une fois pour G-A et une autre pour G-B. Modifier une seule copie crée deux durées contradictoires. Supprimer la dernière inscription peut faire perdre la description de l’atelier. Enregistrer un atelier avant sa première inscription devient difficile si le groupe est obligatoire. Ce sont des anomalies de modification, de suppression et d’insertion.

La décomposition conserve la description de l’atelier une fois dans Atelier, le groupe une fois dans Groupe et leurs associations dans Inscription. Dans le schéma proposé, supprimer un atelier encore référencé est refusé : aucune suppression en cascade n’est déclarée. Si l’on veut retirer définitivement l’atelier, on doit d’abord décider du sort des inscriptions et traiter l’ensemble dans une transaction. Le SGBD fait respecter une politique ; il ne choisit pas à ta place ce que signifie supprimer.

Voir pour comprendre

Trois tables, trois anomalies évitées

La description d’un atelier reste dans Atelier, indépendamment de ses inscriptions.

Trois tables, trois anomalies évitées
OpérationTout dans une tableTables séparées
Changer la duréeUne copie peut rester ancienneUne valeur dans Atelier
Retirer la dernière inscriptionLa description peut disparaîtreAtelier reste présent
Créer un atelier sans groupeLe groupe obligatoire manqueAtelier peut être créé seul

Lis le schéma. Compare l’effet d’une opération dans une table aplatie et dans les tables séparées.

Décomposer permet de conserver une seule description de l’atelier et de gérer ses associations séparément.

Schéma original Maxdecours. Données entièrement fictives. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Trois inscriptions, quatre essais de contrôle

  1. Pars de (1, 'G-A'), (1, 'G-B'), (2, 'G-A'). Le 1 et G-A sont répétés, mais aucun couple ne l’est.
  2. Réinsérer (1, 'G-A') doit échouer : le couple existe déjà. (2, 'G-B') doit au contraire être accepté.
  3. (99, 'G-A') échoue parce que l’atelier 99 n’existe pas ; (1, 'G-Z') échoue parce que le groupe G-Z n’existe pas.
  4. (None, 'G-A') est refusé par NOT NULL. Une clé étrangère seule n’obligerait pas à renseigner l’atelier.

Conclusion. Chaque essai vise une contrainte différente : identité du couple, existence des références et présence des valeurs.

Laboratoire de code

Languepython

ButInstaller trois tables cohérentes et distinguer doublon, référence inconnue et référence absente.

Code solution
import sqlite3

def connecter():
    options = {"isolation_level": None}
    if hasattr(sqlite3, "LEGACY_TRANSACTION_CONTROL"):
        options["autocommit"] = sqlite3.LEGACY_TRANSACTION_CONTROL
    return sqlite3.connect(":memory:", **options)

def creer_base_integrite():
    c = connecter()
    c.execute('PRAGMA foreign_keys = ON')
    if c.execute('PRAGMA foreign_keys').fetchone() != (1,):
        raise RuntimeError('clés étrangères inactives')
    c.execute("""
        CREATE TABLE Atelier (
            id INT NOT NULL PRIMARY KEY
               CHECK (typeof(id) = 'integer' AND id BETWEEN 1 AND 9999),
            titre TEXT NOT NULL CHECK (length(titre) > 0),
            matiere TEXT NOT NULL CHECK (matiere IN ('NSI', 'HLP', 'MATHS')),
            duree INT NOT NULL
               CHECK (typeof(duree) = 'integer' AND duree BETWEEN 15 AND 240)
        )
    """)
    c.execute("""
        CREATE TABLE Groupe (
            code TEXT NOT NULL PRIMARY KEY CHECK (length(code) > 0)
        )
    """)
    c.execute("""
        CREATE TABLE Inscription (
            atelier_id INT NOT NULL REFERENCES Atelier(id),
            groupe_code TEXT NOT NULL REFERENCES Groupe(code),
            PRIMARY KEY (atelier_id, groupe_code)
        )
    """)
    c.executemany('INSERT INTO Atelier VALUES (?, ?, ?, ?)', [
        (1, 'Graphes', 'NSI', 90),
        (2, 'SQL', 'NSI', 60),
    ])
    c.executemany('INSERT INTO Groupe VALUES (?)', [('G-A',), ('G-B',)])
    c.executemany('INSERT INTO Inscription VALUES (?, ?)',
                  [(1, 'G-A'), (1, 'G-B'), (2, 'G-A')])
    return c

def inscrire(c, atelier_id, groupe_code):
    c.execute('INSERT INTO Inscription VALUES (?, ?)', (atelier_id, groupe_code))

def inscriptions(c):
    return c.execute('SELECT atelier_id, groupe_code FROM Inscription ORDER BY atelier_id, groupe_code').fetchall()

def inscription_refusee(c, couple):
    try:
        inscrire(c, *couple)
    except sqlite3.IntegrityError:
        return True
    return False

Tests

c = creer_base_integrite()
assert c.execute('PRAGMA foreign_keys').fetchone() == (1,)
avant = inscriptions(c)
for couple in ((1, 'G-A'), (99, 'G-A'), (1, 'G-Z'), (None, 'G-A'), (1, None)):
    assert inscription_refusee(c, couple)
    assert inscriptions(c) == avant
assert not inscription_refusee(c, (2, 'G-B'))
assert len(inscriptions(c)) == 4
assert c.execute('PRAGMA foreign_key_check').fetchall() == []
c.close()

Trace

  • Deux ateliers et deux groupes existent ; trois couples les relient.
  • Une répétition de couple, une référence inconnue ou une référence NULL sont refusées.
  • Le nouveau couple (2, G-B) est valide même si 2 et G-B apparaissent déjà séparément.

Clinique de bogue

Indice observéUne inscription sans atelier ou avec un atelier inexistant entre dans la base.

CauseDéclarer REFERENCES ne suffit pas si le contrôle des clés étrangères est désactivé sur la connexion. Par ailleurs, une référence NULL peut contourner le test d’existence. L’activation et les contraintes NOT NULL résolvent deux défauts distincts.

03

Étape du cours · 8 à 16 min

Prévoir SELECT : filtrer les lignes, choisir les colonnes, ordonner

SELECT titre, duree FROM Atelier WHERE duree >= 90 ORDER BY duree DESC, titre ASC, id ASC décrit le résultat voulu. Lis d’abord la source après FROM, puis les lignes conservées par WHERE, les colonnes demandées par SELECT et enfin l’ordre. C’est une méthode de raisonnement, pas la promesse que le moteur exécutera matériellement ces étapes dans cet ordre.

Sur les quatre ateliers du laboratoire, le filtre conserve Graphes et Argumenter, tous deux de 90 minutes. Le tri décroissant des durées ne les départage pas ; le titre croissant place Argumenter avant Graphes. Le dernier critère id rend l’ordre déterministe si deux ateliers ont aussi le même titre. Sans ORDER BY, ne déduis aucun ordre contractuel de celui observé dans un essai.

WHERE matiere = 'NSI' AND duree >= 60 exige les deux conditions : Graphes et SQL passent, Logique ne passe pas. Avec OR, une ligne satisfaisant une seule condition suffit. Les parenthèses rendent les combinaisons explicites. SELECT matiere peut renvoyer plusieurs fois NSI ; SELECT DISTINCT matiere retire les lignes de résultat répétées. Une projection SQL peut donc contenir des doublons alors qu’une relation du modèle mathématique est un ensemble.

Un agrégat résume les lignes sélectionnées. Ici COUNT(*) vaut 4, MIN(duree) vaut 45, MAX(duree) vaut 90, SUM(duree) vaut 285 et AVG(duree) vaut 71,25. Avec un filtre qui ne garde aucune ligne, COUNT(*) vaut zéro ; ces quatre autres agrégats valent NULL. Si des durées sont absentes, COUNT(duree) et AVG(duree) ne les comptent pas comme des zéros.

Le seuil variable est lié au paramètre ? par execute(requete, (seuil,)). La virgule fabrique un tuple d’une valeur. La fonction refuse un seuil qui n’est pas un entier Python compris entre 0 et 240, y compris un booléen. Pour tester le résultat, filtre et trie séparément une liste Python : ce calcul indépendant est plus utile que recopier dans le test la même requête erronée.

Voir pour comprendre

Du filtre au résultat attendu

Exemple : titre et durée des ateliers d’au moins 90 minutes, triés par durée décroissante puis titre.

  1. FROM : 4 ateliersGraphes 90 ; SQL 60 ; Argumenter 90 ; Logique 45.
  2. WHERE : 2 lignesduree >= 90 retient Graphes et Argumenter.
  3. SELECT : 2 colonnesChaque résultat contient seulement titre et duree.
  4. ORDER BY : 2 critères utilesÀ durée égale, Argumenter précède Graphes par le titre.

Lis le schéma. Suis les étapes de raisonnement ; elles ne décrivent pas le plan matériel choisi par le moteur.

Les lignes retenues et leur ordre doivent tous deux correspondre à la demande.

Schéma original Maxdecours. Données entièrement fictives. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Deux lignes conservées, un ordre à justifier

  1. Pars de Graphes/NSI/90, SQL/NSI/60, Argumenter/HLP/90 et Logique/NSI/45.
  2. Applique duree >= 90 : SQL et Logique sont éliminés ; Graphes et Argumenter restent.
  3. Projette titre et duree : tu attends deux couples, pas les quatre colonnes de la table.
  4. Trie par durée décroissante puis titre croissant : [('Argumenter', 90), ('Graphes', 90)]. Le test au seuil 91 doit donner une liste vide.

Conclusion. Un résultat peut contenir les bonnes lignes dans le mauvais ordre. Vérifie la sélection et le tri séparément.

Laboratoire de code

Languepython

ButComparer une requête filtrée et triée à un calcul Python indépendant, puis vérifier DISTINCT et les agrégats.

Code solution
import sqlite3

def connecter():
    options = {"isolation_level": None}
    if hasattr(sqlite3, "LEGACY_TRANSACTION_CONTROL"):
        options["autocommit"] = sqlite3.LEGACY_TRANSACTION_CONTROL
    return sqlite3.connect(":memory:", **options)

def creer_base_requetes():
    c = connecter()
    c.execute("""
        CREATE TABLE Atelier (
            id INT NOT NULL PRIMARY KEY
               CHECK (typeof(id) = 'integer' AND id BETWEEN 1 AND 9999),
            titre TEXT NOT NULL CHECK (length(titre) > 0),
            matiere TEXT NOT NULL CHECK (matiere IN ('NSI', 'HLP', 'MATHS')),
            duree INT NOT NULL
               CHECK (typeof(duree) = 'integer' AND duree BETWEEN 15 AND 240)
        )
    """)
    c.executemany('INSERT INTO Atelier VALUES (?, ?, ?, ?)', [
        (1, 'Graphes', 'NSI', 90),
        (2, 'SQL', 'NSI', 60),
        (3, 'Argumenter', 'HLP', 90),
        (4, 'Logique', 'NSI', 45),
    ])
    return c

def verifier_seuil(seuil):
    if type(seuil) is not int or not 0 <= seuil <= 240:
        raise ValueError('seuil entier entre 0 et 240 attendu')

def ateliers_longue_duree(c, seuil):
    verifier_seuil(seuil)
    return c.execute(
        'SELECT titre, duree FROM Atelier WHERE duree >= ? '
        'ORDER BY duree DESC, titre ASC, id ASC', (seuil,)
    ).fetchall()

def matieres(c):
    return c.execute('SELECT DISTINCT matiere FROM Atelier ORDER BY matiere').fetchall()

def resume(c, seuil):
    verifier_seuil(seuil)
    return c.execute(
        'SELECT COUNT(*), MIN(duree), MAX(duree), SUM(duree), AVG(duree) '
        'FROM Atelier WHERE duree >= ?', (seuil,)
    ).fetchone()

Tests

c = creer_base_requetes()
donnees = [(1, 'Graphes', 'NSI', 90), (2, 'SQL', 'NSI', 60),
           (3, 'Argumenter', 'HLP', 90), (4, 'Logique', 'NSI', 45)]
for seuil in (0, 45, 60, 90, 91, 240):
    filtre = [r for r in donnees if r[3] >= seuil]
    attendu = [(r[1], r[3]) for r in sorted(filtre, key=lambda r: (-r[3], r[1], r[0]))]
    assert ateliers_longue_duree(c, seuil) == attendu
assert ateliers_longue_duree(c, 90) == [('Argumenter', 90), ('Graphes', 90)]
assert matieres(c) == [('HLP',), ('NSI',)]
assert resume(c, 0) == (4, 45, 90, 285, 71.25)
assert resume(c, 91) == (0, None, None, None, None)
c.close()

Trace

  • FROM lit quatre ateliers ; WHERE duree >= 90 en retient deux.
  • Les durées ex æquo imposent le deuxième critère : Argumenter précède Graphes.
  • COUNT(*) compte les lignes filtrées ; sur un ensemble vide les autres agrégats renvoient None côté Python.

Clinique de bogue

Indice observéAu seuil 90, aucun atelier n’apparaît ; avec un seuil plus bas, les durées arrivent dans l’ordre croissant.

CauseLa requête cumule deux écarts au contrat : > exclut la borne demandée, et ASC inverse la priorité des durées. Tester seulement un seuil sans égalité ou des durées identiques peut laisser une de ces erreurs invisible.

04

Étape du cours · 8 à 16 min

Lire une jointure comme une rencontre de clés

Une jointure combine des lignes de plusieurs tables selon une condition. FROM Inscription AS i JOIN Atelier AS a ON i.atelier_id = a.id associe chaque inscription à l’atelier portant l’identifiant référencé. Les alias i et a raccourcissent l’écriture ; ils ne créent pas de nouvelles tables permanentes. Qualifier les attributs évite de confondre des colonnes qui portent le même nom.

Le schéma visuel part de deux ateliers et de trois inscriptions. Pour l’inscription (1, 'G-B'), seule la ligne d’Atelier dont id vaut 1 convient : elle apporte le titre Graphes. Chaque inscription possède ici exactement une référence non nulle vers une clé primaire existante. Cette jointure produit donc exactement trois lignes. Cette conclusion dépend des contraintes : toute jointure ne conserve pas systématiquement le nombre de lignes.

Sans condition de rapprochement, les deux ateliers peuvent être associés à chacune des trois inscriptions : le produit cartésien comporte six couples. SQL peut accepter cette instruction et fournir un résultat faux pour la question posée. Inversement, joindre sur le titre est fragile : deux ateliers distincts peuvent s’appeler Graphes. Une égalité de textes n’est pas une preuve d’identité.

SELECT i.groupe_code, a.titre choisit les colonnes après rapprochement. G-A apparaît deux fois puisqu’il est inscrit à deux ateliers. Ajouter DISTINCT peut masquer une multiplication indésirable, mais ne répare pas une mauvaise condition ON ; cela peut aussi supprimer des lignes légitimement identiques après projection. Prévois d’abord le nombre de couples, puis les valeurs et l’ordre demandés.

Pour interroger trois tables, ajoute une condition par lien utile : Inscription vers Atelier et vers Groupe. Le transfert affiche la matière avec le titre pour un groupe donné, puis applique un filtre de durée. À chaque étape, relie les attributs du schéma avant d’écrire la requête. Les petites tables servent à tout calculer à la main ; le projet reprend la méthode sur deux cents ateliers fictifs.

Voir pour comprendre

Trois références, trois rencontres

Les flèches relient atelier_id à id. La matière et la durée ne sont pas affichées dans cette vue du schéma.

Clés d’atelier et inscriptions Inscription (1, G-A) vers atelier 1 : Graphes. Inscription (1, G-B) vers atelier 1 : Graphes. Inscription (2, G-A) vers atelier 2 : SQL. La clé primaire de l’association comporte les deux attributs. Atelier · clé primaire : id Inscription · clé composée des deux attributs idtitre atelier_idgroupe_code 1Graphes 2SQL 1G-A 1G-B 2G-A
Résultat trié par groupe, titre, id
GroupeTitre
G-AGraphes
G-ASQL
G-BGraphes

6 couples possibles · 3 correspondent aux clés.

Condition : i.atelier_id = a.id. La clé d’atelier est unique ; chaque référence est renseignée et existe.

Lis le schéma. Pars d’une inscription en bas et remonte sa flèche jusqu’à l’atelier identifié. Le résultat est calculé à partir des mêmes lignes.

Une même clé d’atelier peut recevoir plusieurs flèches ; la clé primaire de l’association est le couple (atelier_id, groupe_code).

Schéma original Maxdecours. Données entièrement fictives. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Retrouver le planning de G-A

  1. Le groupe G-A est présent dans les inscriptions (1, 'G-A') et (2, 'G-A') ; G-B est aussi inscrit à l’atelier 1.
  2. Le lien atelier_id = id apporte Graphes à la première inscription et SQL à la seconde.
  3. La projection groupe_code, titre donne trois couples pour tous les groupes : G-A/Graphes, G-B/Graphes et G-A/SQL.
  4. Avec ORDER BY groupe_code, titre, id, le résultat devient G-A/Graphes, G-A/SQL, G-B/Graphes. Le filtre sur G-A n’en conserve que deux.

Conclusion. La clé reconstitue le lien, WHERE limite les inscriptions retenues et ORDER BY organise leur affichage.

Laboratoire de code

Languepython

ButVérifier une jointure par un dictionnaire Python et mesurer l’erreur d’un produit cartésien.

Code solution
import sqlite3

def connecter():
    options = {"isolation_level": None}
    if hasattr(sqlite3, "LEGACY_TRANSACTION_CONTROL"):
        options["autocommit"] = sqlite3.LEGACY_TRANSACTION_CONTROL
    return sqlite3.connect(":memory:", **options)

def creer_base_jointures():
    c = connecter()
    c.execute('PRAGMA foreign_keys = ON')
    if c.execute('PRAGMA foreign_keys').fetchone() != (1,):
        raise RuntimeError('clés étrangères inactives')
    c.execute("""
        CREATE TABLE Atelier (
            id INT NOT NULL PRIMARY KEY
               CHECK (typeof(id) = 'integer' AND id BETWEEN 1 AND 9999),
            titre TEXT NOT NULL CHECK (length(titre) > 0),
            matiere TEXT NOT NULL CHECK (matiere IN ('NSI', 'HLP', 'MATHS')),
            duree INT NOT NULL
               CHECK (typeof(duree) = 'integer' AND duree BETWEEN 15 AND 240)
        )
    """)
    c.execute("""
        CREATE TABLE Groupe (
            code TEXT NOT NULL PRIMARY KEY CHECK (length(code) > 0)
        )
    """)
    c.execute("""
        CREATE TABLE Inscription (
            atelier_id INT NOT NULL REFERENCES Atelier(id),
            groupe_code TEXT NOT NULL REFERENCES Groupe(code),
            PRIMARY KEY (atelier_id, groupe_code)
        )
    """)
    c.executemany('INSERT INTO Atelier VALUES (?, ?, ?, ?)', [
        (1, 'Graphes', 'NSI', 90),
        (2, 'SQL', 'NSI', 60),
    ])
    c.executemany('INSERT INTO Groupe VALUES (?)', [('G-A',), ('G-B',)])
    c.executemany('INSERT INTO Inscription VALUES (?, ?)',
                  [(1, 'G-A'), (1, 'G-B'), (2, 'G-A')])
    return c

def planning(c):
    return c.execute(
        'SELECT i.groupe_code, a.titre FROM Inscription AS i '
        'JOIN Atelier AS a ON i.atelier_id = a.id '
        'ORDER BY i.groupe_code, a.titre, a.id'
    ).fetchall()

def taille_produit(c):
    return c.execute('SELECT COUNT(*) FROM Inscription CROSS JOIN Atelier').fetchone()[0]

Tests

c = creer_base_jointures()
titres = {1: 'Graphes', 2: 'SQL'}
liens = [(1, 'G-A'), (1, 'G-B'), (2, 'G-A')]
attendu = sorted((groupe, titres[identifiant]) for identifiant, groupe in liens)
assert planning(c) == attendu
assert planning(c) == [('G-A', 'Graphes'), ('G-A', 'SQL'), ('G-B', 'Graphes')]
assert len(planning(c)) == 3
assert taille_produit(c) == 6
c.execute("UPDATE Atelier SET titre = 'Graphes' WHERE id = 2")
assert planning(c) == [('G-A', 'Graphes'), ('G-A', 'Graphes'), ('G-B', 'Graphes')]
assert len(planning(c)) == 3
c.close()

Trace

  • Deux ateliers × trois inscriptions donnent six couples possibles.
  • L’égalité de la référence et de la clé en conserve trois.
  • Renommer SQL en Graphes ne change pas les identifiants ni le nombre d’associations.

Clinique de bogue

Indice observéLe planning affiche six lignes au lieu de trois, tout en s’exécutant sans erreur SQL.

CauseLe produit cartésien associe chaque inscription à tous les ateliers. Ni une exécution réussie ni un tri correct ne prouvent la pertinence du rapprochement. Il faut relier atelier_id à la clé id, puis contrôler la cardinalité et les valeurs.

05

Étape du cours · 8 à 16 min

Modifier une base : la bonne portée et le tout ou rien

INSERT INTO ajoute des lignes, UPDATE ... SET change des valeurs et DELETE FROM retire des lignes. Pour modifier un seul atelier, cible sa clé dans WHERE : un titre n’est pas nécessairement unique. Sans WHERE, UPDATE ou DELETE vise toutes les lignes de la table. Avant la modification, formule la sélection attendue ; après, contrôle les lignes touchées et celles qui devaient rester intactes.

Une opération métier peut nécessiter plusieurs instructions. Imaginons deux réserves fictives de minutes pour des ateliers : A contient 100 minutes, B en contient 20. Transférer 30 minutes doit donner A = 70 et B = 50, sans changer le total. Une transaction délimite l’ensemble avec BEGIN ; COMMIT valide les modifications et ROLLBACK les annule. Le tout ou rien concerne toutes les instructions de cette transaction.

Un UPDATE sans ligne correspondante n’est pas forcément une erreur SQL. Si la source Z n’existe pas, débiter Z ne change rien : créditer B ensuite créerait pourtant des minutes. Le programme doit donc contrôler rowcount après chaque UPDATE. Le contrat de transferer exige des codes distincts, une quantité entière de 1 à 240 et aucune transaction déjà ouverte ; une demande mal formée lève ValueError sans modifier la base.

Une demande bien formée renvoie True si les deux modifications réussissent, False si un code manque, si A est insuffisant ou si B dépasserait sa capacité de 240 minutes. En cas de refus, l’état initial est restauré. Essaie surtout un échec de la seconde instruction : A = 100, B = 230 et un transfert de 20. A passe provisoirement à 80 ; B ne peut pas passer à 250 ; ROLLBACK remet A à 100 et B reste à 230.

Ne suppose pas qu’une erreur SQL annule toujours les instructions précédentes : ce comportement dépend de l’erreur et de la politique du moteur. La correction annule explicitement la transaction qu’elle a ouverte, puis propage les autres erreurs SQLite. Elle refuse d’intervenir dans une transaction déjà ouverte pour ne pas valider ou annuler le travail de l’appelant. Ces laboratoires isolés mettent en évidence l’atomicité ; la gestion de plusieurs connexions est une autre propriété du SGBD.

Voir pour comprendre

Un échec après le débit

Transfert de 20 minutes de A vers B ; chaque réserve est limitée à 240 minutes.

Un échec après le débit
MomentAB
Avant BEGIN100230
Après le débit80230
Crédit refusé : 250 > 24080230
Après ROLLBACK100230

Lis le schéma. Lis chaque ligne comme un état réel de la base. La tentative B = 250 est refusée, donc cet état n’est jamais enregistré.

ROLLBACK restaure l’état d’avant BEGIN, y compris l’écriture qui avait réussi.

Schéma original Maxdecours. Données entièrement fictives. · Source du repère · 06/09/2026

Exemples résolus et erreurs expliquées

Un débit réussi ne suffit pas

  1. État de départ : A = 100, B = 230 ; chaque réserve doit rester entre 0 et 240. Le transfert demandé vaut 20.
  2. BEGIN ouvre la transaction. Le débit d’A touche une ligne et produit l’état provisoire A = 80, B = 230.
  3. Le crédit de B tenterait 250 : CHECK refuse cette instruction. Il ne faut pas laisser le débit d’A seul.
  4. ROLLBACK annule le débit. L’état final est A = 100, B = 230, et transferer renvoie False.

Conclusion. Le résultat attendu d’un échec est un état complet, pas seulement un message d’erreur.

Laboratoire de code

Languepython

ButTransférer des minutes fictives sans en créer ni en perdre, y compris lorsqu’une source manque ou que la seconde écriture échoue.

Code solution
import sqlite3

def connecter():
    options = {"isolation_level": None}
    if hasattr(sqlite3, "LEGACY_TRANSACTION_CONTROL"):
        options["autocommit"] = sqlite3.LEGACY_TRANSACTION_CONTROL
    return sqlite3.connect(":memory:", **options)

def creer_base_credits():
    c = connecter()
    c.execute("""CREATE TABLE Credit (
        code TEXT NOT NULL PRIMARY KEY,
        valeur INT NOT NULL CHECK (
            typeof(valeur) = 'integer' AND valeur BETWEEN 0 AND 240)
    )""")
    c.executemany('INSERT INTO Credit VALUES (?, ?)', [('A', 100), ('B', 20)])
    return c

def etat(c):
    return c.execute('SELECT code, valeur FROM Credit ORDER BY code').fetchall()

def transferer(c, source, destination, quantite):
    if c.in_transaction:
        raise ValueError('une transaction est déjà ouverte')
    if (type(source) is not str or type(destination) is not str
            or not source or not destination or source == destination):
        raise ValueError('deux codes distincts non vides attendus')
    if type(quantite) is not int or not 1 <= quantite <= 240:
        raise ValueError('quantité entière entre 1 et 240 attendue')
    c.execute('BEGIN')
    try:
        debit = c.execute(
            'UPDATE Credit SET valeur = valeur - ? WHERE code = ? AND valeur >= ?',
            (quantite, source, quantite))
        if debit.rowcount != 1:
            raise ValueError('source absente ou quantité insuffisante')
        credit = c.execute(
            'UPDATE Credit SET valeur = valeur + ? WHERE code = ?',
            (quantite, destination))
        if credit.rowcount != 1:
            raise ValueError('destination absente')
        c.execute('COMMIT')
        return True
    except (sqlite3.IntegrityError, ValueError):
        if c.in_transaction:
            c.execute('ROLLBACK')
        return False
    except sqlite3.Error:
        if c.in_transaction:
            c.execute('ROLLBACK')
        raise

Tests

c = creer_base_credits()
assert transferer(c, 'A', 'B', 30) is True
assert etat(c) == [('A', 70), ('B', 50)]
avant = etat(c)
assert transferer(c, 'Z', 'B', 10) is False
assert etat(c) == avant
assert transferer(c, 'A', 'Z', 10) is False
assert etat(c) == avant
assert transferer(c, 'A', 'B', 100) is False
assert etat(c) == avant
c.execute("UPDATE Credit SET valeur = 100 WHERE code = 'A'")
c.execute("UPDATE Credit SET valeur = 230 WHERE code = 'B'")
assert transferer(c, 'A', 'B', 20) is False
assert etat(c) == [('A', 100), ('B', 230)]
assert not c.in_transaction
c.close()

Trace

  • Succès : A100/B20 devient A70/B50, avec le même total de 120 minutes.
  • Source Z absente : zéro débit, donc aucun crédit n’est validé.
  • Deuxième écriture refusée : le débit provisoire est annulé ; A100/B230 est restauré.

Clinique de bogue

Indice observéUne source absente donne quand même dix minutes à la destination.

CauseLa présence d’une transaction ne prouve pas la validité métier. Un UPDATE qui ne trouve aucune source peut réussir en touchant zéro ligne. Sans contrôle du débit, le crédit est validé seul et le total augmente malgré le COMMIT.

06

Étape du cours · 7 à 14 min

Le SGBD protège des règles, l’application protège un accès

Le système de gestion de base de données, ou SGBD, reçoit les requêtes et gère leur exécution. La persistance permet de retrouver des données enregistrées après l’arrêt de l’application. La gestion des accès concurrents coordonne plusieurs utilisateurs ; l’optimisation cherche un traitement efficace ; les contrôles d’accès limitent ce que chaque utilisateur peut lire ou modifier. Ces services complètent les contraintes qui maintiennent la cohérence des tables.

SQL décrit les données attendues sans imposer ici un algorithme de parcours. Le moteur peut choisir un plan d’exécution et utiliser des index. Une requête rapide sur quatre lignes ne démontre donc pas sa performance à grande échelle. De même, une seule connexion à une base en mémoire permet de travailler les requêtes et l’atomicité, tandis qu’un test de persistance doit fermer puis rouvrir un stockage conservé.

Une donnée saisie ne doit pas devenir un morceau de syntaxe SQL. Si un programme concatène un code dans WHERE code = '...', une apostrophe peut fermer la chaîne et changer la condition. Avec WHERE code = ?, le texte est lié comme une valeur. Les essais restent dans une base éphémère de ressources fictives ; ils montrent à la fois une recherche contenant une apostrophe et une tentative qui ne doit pas élargir le résultat.

Paramétrer ne décide pas du droit d’accès. Une fonction qui recherche seulement le code R2 peut retourner un brouillon si elle ne vérifie pas son statut. La fonction chercher_public ajoute donc une condition fixe statut = 'publie'. L’application qui gère des comptes doit en plus vérifier l’identité et les autorisations côté serveur ; un rôle fourni librement par le navigateur ne constitue pas une autorisation.

Les paramètres servent aux valeurs, pas aux noms de colonnes ni aux mots clés. Pour proposer un tri, le transfert choisit une clause complète dans un dictionnaire fermé défini par le programme. Il n’interpole jamais directement le choix fourni. Associe chaque protection à sa preuve : valeur liée littéralement, brouillon absent du résultat public, choix de tri inconnu refusé et données inchangées après une lecture.

Exemples résolus et erreurs expliquées

Deux vérifications, deux protections

  1. R1 est publié ; R2 est un brouillon. chercher_interne(c, 'R2') renvoie SQL, car cette fonction ne filtre pas le statut.
  2. chercher_public(c, 'R2') renvoie une liste vide : la condition fixe de publication est indépendante du texte saisi.
  3. Le code littéral R'3 renvoie Logique. La présence d’une apostrophe ne doit pas casser une recherche paramétrée.
  4. Le texte ' OR 1=1 -- ne correspond à aucun code de cette base : chercher_public renvoie une liste vide, sans modifier les trois lignes.

Conclusion. Sépare la question « quelle valeur a été demandée ? » de « cette ressource est-elle autorisée dans cette vue ? ».

Laboratoire de code

Languepython

ButDistinguer une recherche interne d’une lecture publique, sans confondre paramètres et autorisation.

Code solution
import sqlite3

def connecter():
    options = {"isolation_level": None}
    if hasattr(sqlite3, "LEGACY_TRANSACTION_CONTROL"):
        options["autocommit"] = sqlite3.LEGACY_TRANSACTION_CONTROL
    return sqlite3.connect(":memory:", **options)

def creer_base_ressources():
    c = connecter()
    c.execute("""CREATE TABLE Ressource (
        code TEXT NOT NULL PRIMARY KEY,
        titre TEXT NOT NULL,
        statut TEXT NOT NULL CHECK (statut IN ('publie', 'brouillon'))
    )""")
    c.executemany('INSERT INTO Ressource VALUES (?, ?, ?)', [
        ('R1', 'Graphes', 'publie'),
        ('R2', 'SQL', 'brouillon'),
        ("R'3", 'Logique', 'publie'),
    ])
    return c

def chercher_interne(c, code):
    return c.execute('SELECT titre FROM Ressource WHERE code = ?', (code,)).fetchall()

def chercher_public(c, code):
    return c.execute(
        "SELECT titre FROM Ressource WHERE code = ? AND statut = 'publie'", (code,)
    ).fetchall()

Tests

c = creer_base_ressources()
avant = c.execute('SELECT * FROM Ressource ORDER BY code').fetchall()
assert chercher_interne(c, 'R2') == [('SQL',)]
assert chercher_public(c, 'R2') == []
assert chercher_public(c, 'R1') == [('Graphes',)]
assert chercher_public(c, "R'3") == [('Logique',)]
assert chercher_public(c, "' OR 1=1 --") == []
assert chercher_public(c, 'absent') == []
assert c.execute('SELECT * FROM Ressource ORDER BY code').fetchall() == avant
c.close()

Trace

  • Le filtre de code seul trouve R2 ; le filtre public l’exclut parce que son statut est brouillon.
  • L’apostrophe de R'3 est une donnée du paramètre, pas la fin d’une chaîne SQL.
  • Le texte tentant de modifier la condition est recherché littéralement ; aucun code ne lui correspond.

Clinique de bogue

Indice observéUne saisie peut élargir la recherche à toutes les ressources, y compris les brouillons.

CauseLa concaténation mêle une valeur externe et la syntaxe de la requête. Ajouter des paramètres résout ce premier défaut, mais la vue publique doit aussi imposer le statut publié : ce deuxième contrôle ne découle pas de la paramétrisation.

Poursuivre avec l’abonnement

Passer de la lecture à la pratique

Lis les six explications, les cinq schémas, les exemples résolus et les requêtes vérifiées.

Vérifie tes choix avec six ateliers, douze questions corrigées, douze cartes et un projet Python à adapter.

12 questions · 12 cartes. Ta reprise et tes révisions sont enregistrées dans ce navigateur. Elles ne se synchronisent pas entre appareils.

Accéder à l’entraînement

Vérifier et prolonger

Sources du cours

Édition Maxdecours · Vérifié le .

Spécialité NSI, Terminale générale · programme du BO spécial du 25 juillet 2019

  1. Programme NSI de Terminale généraleMinistère de l’Éducation nationale · consulté le 2026-09-06
  2. Programmes et ressources NSI, bases de donnéesÉduscol · consulté le 2026-09-06
  3. Contraintes relationnellesPostgreSQL Global Development Group · consulté le 2026-09-06
  4. CREATE TABLESQLite · consulté le 2026-09-06
  5. Types et affinitésSQLite · consulté le 2026-09-06
  6. Clés étrangèresSQLite · consulté le 2026-09-06
  7. SELECTSQLite · consulté le 2026-09-06
  8. Ordre des résultats SELECTPostgreSQL Global Development Group · consulté le 2026-09-06
  9. Fonctions d’agrégationSQLite · consulté le 2026-09-06
  10. TransactionsSQLite · consulté le 2026-09-06
  11. Module sqlite3 de PythonPython Software Foundation · consulté le 2026-09-06