Bases de données & SQL

Stocker des données reliées entre elles, les interroger — et ne pas laisser un visiteur les lire toutes.

📌

Résumé flash

Une base relationnelle range les données dans des tables reliées par des clés : une clé primaire identifie chaque ligne, une clé étrangère pointe vers une autre table. Le langage SQL crée (CREATE), modifie (INSERT, UPDATE, DELETE) et interroge (SELECT). Depuis PHP on s'y connecte en PDO — et l'on utilise des requêtes préparées, sous peine d'injection SQL.

🔑 #SGBD #MariaDB #Homebrew #table #PRIMARY-KEY #FOREIGN-KEY #MCD #MLD #CREATE-TABLE #SELECT #WHERE #JOIN #GROUP-BY #PDO #prepare #injection-SQL

01Base de données et SGBD

Une base de données stocke des informations de manière organisée, pour les retrouver, les modifier et les exploiter facilement. Un CFA y stockera ses élèves, ses professeurs, ses notes.

TypeExemplesUsage
RelationnelleMariaDB, MySQL, PostgreSQL données structurées, en tables reliées
NoSQLMongoDB, Redis données non structurées, gros volumes

Le SGBD (Système de Gestion de Base de Données) est le logiciel qui gère tout cela : création des tables, manipulation des données, gestion des droits, et intégrité référentielle — la garantie que les relations entre tables restent cohérentes.

CLIENT phpMyAdmin, PHP ou un terminal SQL SERVEUR MariaDB analyse, exécute, renvoie le résultat SELECT * FROM etudiants; ← les lignes trouvées
Le client envoie une requête, le serveur la traite et renvoie le résultat. C'est la même logique client/serveur qu'en PHP.

02Le modèle relationnel

Les données sont organisées en tables : des lignes (enregistrements) et des colonnes (attributs).

TermeDéfinitionDans le projet Présences
Entitéun objet du monde réel que l'on veut stocker un étudiant, un administrateur
Attributune caractéristique de l'entité nom, prenom, classe
Associationun lien entre deux entités un étudiant a des présences
Clé primaire (PK)identifie chaque ligne de façon uniqueid
Clé étrangère (FK)pointe vers la clé primaire d'une autre tableid_etudiant
💡 Le classeur et les intercalaires

Une table, c'est un tableau de type Excel : une ligne par étudiant, une colonne par information. La différence décisive avec Excel, c'est la clé étrangère : au lieu de recopier « Dupont Alice BTS1 » dans chaque ligne de présence, on écrit juste son numéro.

Résultat : si Alice change de classe, on modifie une seule ligne. Sans clé, il faudrait corriger les 200 lignes de présence — et en oublier une suffirait à rendre la base incohérente.

🧰 Reconnaître une clé primaire

Trois propriétés, toujours : unique (deux lignes ne peuvent pas partager la même), non nulle, et stable (elle ne change jamais).

C'est pourquoi on utilise presque toujours un id INT AUTO_INCREMENT plutôt que le nom ou l'email : deux étudiants peuvent être homonymes, et un email peut changer.

03Les types de relations

1 — 1 Utilisateur Compte un pour un 1 — N Client Commande Commande un client, plusieurs commandes N — N Etudiant Inscription Cours il FAUT une table intermédiaire qui porte les deux clés étrangères
Le cas N–N ne peut pas se représenter avec deux tables seulement.
RelationExempleTraduction en tables
1 — 1un utilisateur a un compte bancaire une clé étrangère dans l'une des deux tables
1 — Nun client passe plusieurs commandes la clé étrangère va du côté N
N — Ndes étudiants suivent des cours table intermédiaire avec les deux clés étrangères
⚠️ La clé étrangère va toujours du côté « N »

Dans « un client, plusieurs commandes », c'est la table commandes qui porte id_client — jamais l'inverse. Mettre id_commande dans clients forcerait à créer une ligne client par commande.

Le repère : quelle ligne a besoin de pointer vers une seule autre ? Une commande appartient à un client ; un client a plusieurs commandes. La flèche part donc de la commande.

04Du MCD au MLD

MCD
Modèle Conceptuel de Données
la vue « métier » : les entités, leurs attributs, leurs associations. Indépendant de tout SGBD
MLD
Modèle Logique de Données
la traduction en tables, prête à devenir du SQL

Les cinq règles de passage

  1. chaque entité devient une table ;
  2. chaque attribut devient une colonne ;
  3. on définit la clé primaire de chaque table ;
  4. on traduit les relations : 1–N → une clé étrangère du côté N · N–N → une table intermédiaire portant les deux clés ;
  5. on choisit le type de données de chaque colonne.

Les cardinalités — ce qui décide de tout

Sur un MCD, chaque branche d'une association porte deux nombres : le minimum et le maximum de fois qu'une occurrence de l'entité participe à l'association. Ce sont eux, et eux seuls, qui déterminent la traduction en tables.

CardinalitéComment la lireExemple
0,1au plus une fois, éventuellement jamais un employé peut avoir une place de parking
1,1exactement une fois un PC appartient à un seul utilisateur
0,Nautant de fois que l'on veut, zéro compris un logiciel peut n'être installé nulle part
1,Nau moins une fois, sans maximum un utilisateur possède au moins un PC
🎯 Se lire à voix haute, dans les deux sens

Une association se lit deux fois, une fois par côté, et la cardinalité se lit sur le côté opposé à l'entité dont on parle :

USER —(1,N)— Posséder —(1,1)— PC
  • « un utilisateur possède 1 à N PC » ;
  • « un PC est possédé par exactement un utilisateur ».

Si une des deux phrases sonne faux, la cardinalité est mal placée. C'est le seul contrôle qui existe — et il suffit.

Les deux maximumsTypeTraduction en tables
1 et N1–N une clé étrangère, placée dans la table du côté 1,1
N et NN–N une table intermédiaire portant les deux clés — et les éventuels attributs de l'association
1 et 11–1 rare : soit on fusionne les deux entités, soit une clé étrangère unique d'un côté
⚠️ La clé étrangère va du côté 1,1, pas du côté 1,N

C'est l'erreur de MLD la plus fréquente, et elle se raisonne en une phrase : une colonne ne contient qu'une seule valeur.

Un PC n'a qu'un propriétaire → PC peut porter une colonne #id_user. Un utilisateur a plusieurs PC → mettre #id_pc dans USER obligerait à y ranger plusieurs valeurs. Impossible.

Formule à retenir : la clé étrangère descend du côté qui ne peut en avoir qu'un.

🧰 L'exemple du tableau — USER, PC, Logiciel
MCD
USER —(1,N)— Posséder —(1,1)— PC
PC   —(1,N)— Installer —(1,N)— LOGICIEL

Première association, 1–N : la clé étrangère part dans PC. Seconde association, N–N — un PC porte plusieurs logiciels, un logiciel est installé sur plusieurs PC : il faut une table intermédiaire.

MLD
users      (id_user, nom, email, tel)
pc         (id_pc, sn, marque, garantie, #id_user)
logiciels  (id_app, nom, version, licence)
installer  (#id_pc, #id_app, date_installation)
            └────── clé primaire composée des deux clés étrangères

C'est ici que la table intermédiaire prend tout son sens : elle peut porter ses propres attributs — la date d'installation n'appartient ni au PC ni au logiciel, mais bien au couple des deux.

🧰 Le MLD du projet « Gestion des présences »

Trois entités, une relation 1–N (un étudiant a plusieurs présences) :

MLD
administrateurs (id, login, mot_de_passe)
etudiants       (id, nom, prenom, classe)
presences       (id, #id_etudiant, date_presence, statut)
                     ↑
                 le # note la clé étrangère → etudiants(id)

Le soulignement marque la clé primaire, le # la clé étrangère. C'est la notation attendue sur une copie.

05Créer la base et les tables

sql
CREATE DATABASE IF NOT EXISTS gestion_presence;
USE gestion_presence;

CREATE TABLE etudiants (
    id     INT AUTO_INCREMENT PRIMARY KEY,
    nom    VARCHAR(50) NOT NULL,
    prenom VARCHAR(50) NOT NULL,
    classe VARCHAR(50) NOT NULL
);

Les types de données

TypePourExemple
INTun nombre entierun identifiant, une quantité
VARCHAR(n)du texte court, longueur variable VARCHAR(50) pour un nom
TEXTdu texte longun commentaire
DATEune date2026-09-13
DATETIMEdate et heureun horodatage
DECIMAL(p,s)un nombre décimal exactun prix
BOOLEANvrai / fauxactif ou non

Les contraintes

PRIMARY KEYidentifie la ligne — unique et non nulle
AUTO_INCREMENTle SGBD numérote tout seul
NOT NULLla colonne doit être renseignée
UNIQUEpas deux fois la même valeur
DEFAULTvaleur par défaut si rien n'est fourni
FOREIGN KEY … REFERENCESlie à une autre table
sql — la clé étrangère
CREATE TABLE presences (
    id            INT AUTO_INCREMENT PRIMARY KEY,
    id_etudiant   INT NOT NULL,
    date_presence DATE NOT NULL,
    statut        VARCHAR(20) NOT NULL,
    FOREIGN KEY (id_etudiant) REFERENCES etudiants(id)
);
🧰 Ce que la clé étrangère vous garantit

C'est l'intégrité référentielle. Une fois la contrainte posée, le SGBD refuse :

  • d'insérer une présence pour un id_etudiant qui n'existe pas ;
  • de supprimer un étudiant qui a encore des présences enregistrées.

Ce n'est pas une gêne, c'est une protection : sans elle, la base accumulerait des présences « orphelines », rattachées à personne.

⚠️ VARCHAR(50) : 50 caractères, pas 50 octets de disque

VARCHAR stocke une longueur variable : « Ali » n'occupe pas 50 caractères. Le nombre est un maximum, pas une réservation.

Le piège est ailleurs : un nom de 60 caractères sera tronqué (ou rejeté). Prévoyez large sur les noms et les emails — VARCHAR(100) ne coûte rien de plus si les valeurs sont courtes.

06Modifier la structure

sql
ALTER TABLE etudiants ADD telephone VARCHAR(15);          -- ajouter une colonne
ALTER TABLE etudiants MODIFY nom VARCHAR(100);            -- changer un type
ALTER TABLE etudiants DROP COLUMN telephone;              -- supprimer une colonne
ALTER TABLE etudiants RENAME TO eleves;                   -- renommer la table

DROP TABLE presences;              -- supprimer la table ET ses données
DROP DATABASE gestion_presence;    -- tout supprimer
⚠️ DROP ne demande aucune confirmation

DROP TABLE supprime la structure et toutes les données, instantanément et sans retour possible. Il n'y a ni corbeille, ni « êtes-vous sûr ? ».

Avant tout DROP : une sauvegarde (export phpMyAdmin).

À ne pas confondre : DROP TABLE supprime la table ; DELETE FROM table vide son contenu mais garde la structure.

07INSERT, UPDATE, DELETE

sql — ajouter
INSERT INTO etudiants (nom, prenom, classe)
VALUES ('Dupont', 'Alice', 'BTS CIEL 1');

-- plusieurs lignes d'un coup
INSERT INTO etudiants (nom, prenom, classe) VALUES
    ('Martin', 'Bob', 'BTS CIEL 1'),
    ('Durand', 'Chloe', 'BTS CIEL 2');
sql — modifier
UPDATE etudiants
SET classe = 'BTS CIEL 2'
WHERE id = 1;
sql — supprimer
DELETE FROM etudiants
WHERE id = 1;
⚠️ Le WHERE oublié — la catastrophe classique
sql
UPDATE etudiants SET classe = 'BTS CIEL 2';    -- ✘ TOUS les étudiants changent de classe
DELETE FROM etudiants;                          -- ✘ la table est VIDÉE

SQL exécute sans broncher : il n'y a ni avertissement ni annulation. C'est l'accident le plus fréquent en base de données, et il se produit en une seconde.

Le réflexe qui sauve : écrire d'abord la requête en SELECT pour voir ce qu'elle touche, puis remplacer par UPDATE ou DELETE une fois le résultat vérifié.

sql — la bonne méthode
SELECT * FROM etudiants WHERE id = 1;      -- 1. je vérifie : une seule ligne ?
DELETE  FROM etudiants WHERE id = 1;       -- 2. alors je supprime

08SELECT et filtres

sql
SELECT * FROM etudiants;                      -- toutes les colonnes
SELECT nom, prenom FROM etudiants;            -- seulement certaines

SELECT * FROM etudiants
WHERE classe = 'BTS CIEL 1';                  -- filtrer

SELECT * FROM presences
WHERE date_presence > '2026-01-01';

SELECT * FROM etudiants
ORDER BY nom ASC;                             -- trier (DESC pour l'inverse)

SELECT * FROM etudiants
LIMIT 10;                                     -- les 10 premiers

Les opérateurs du WHERE

= <> > < >= <=comparaisons
AND · OR · NOTcombinaisons
LIKE '%dup%'contient — % remplace n'importe quoi
IN ('BTS1', 'BTS2')fait partie de la liste
BETWEEN 1 AND 10dans l'intervalle, bornes comprises
IS NULL · IS NOT NULLvaleur absente ou non
⚠️ = NULL ne fonctionne jamais
sql
SELECT * FROM etudiants WHERE classe = NULL;      -- ✘ renvoie TOUJOURS 0 ligne
SELECT * FROM etudiants WHERE classe IS NULL;     -- ✔

NULL signifie « valeur inconnue ». Comparer deux inconnues ne donne ni vrai ni faux, mais… inconnu — donc jamais « vrai ». D'où IS NULL, un opérateur à part.

🧰 Le LIKE de la barre de recherche

Le projet demande de rechercher un étudiant par nom, prénom ou classe :

sql
SELECT * FROM etudiants
WHERE nom    LIKE '%dup%'
   OR prenom LIKE '%dup%'
   OR classe LIKE '%dup%';

'%dup%' = « contient dup » · 'dup%' = « commence par » · '%dup' = « finit par ».

09Les jointures

Une jointure rassemble les données de plusieurs tables en suivant les clés. C'est ce qui rend le modèle relationnel utile.

sql — le problème
SELECT * FROM presences;

-- id | id_etudiant | date_presence | statut
--  1 |      3      |  2026-09-13   | present
-- ↑ « 3 », d'accord… mais QUI est 3 ?
sql — la jointure
SELECT etudiants.nom, etudiants.prenom, presences.date_presence, presences.statut
FROM presences
INNER JOIN etudiants ON presences.id_etudiant = etudiants.id;

-- nom    | prenom | date_presence | statut
-- Dupont | Alice  |  2026-09-13   | present
JointureRenvoie
INNER JOINuniquement les lignes présentes dans les deux tables
LEFT JOINtoutes celles de la table de gauche, + celles de droite quand elles existent (sinon NULL)
RIGHT JOINl'inverse : toutes celles de droite
🧰 INNER ou LEFT : la question à se poser

« Veux-je voir aussi ceux qui n'ont rien ? »

« La liste des présences avec le nom de l'étudiant » INNER JOIN — une présence a forcément un étudiant
« Tous les étudiants, avec leur nombre d'absences » LEFT JOIN — sinon les étudiants jamais absents disparaîtraient du tableau

C'est l'erreur d'analyse la plus fréquente : un INNER JOIN fait disparaître silencieusement les lignes sans correspondance. Le rapport paraît correct… mais il est incomplet.

⚠️ Préfixez les colonnes ambiguës
sql
SELECT id FROM presences INNER JOIN etudiants ON …
-- ✘ Error: Column 'id' in field list is ambiguous

SELECT presences.id FROM …      -- ✔ on précise la table

Les deux tables ont une colonne id : il faut dire laquelle. On peut aussi raccourcir avec des alias : FROM presences p INNER JOIN etudiants e ON p.id_etudiant = e.id.

10Agrégats et GROUP BY

COUNT()compter les lignes
SUM()additionner
AVG()moyenne
MIN() · MAX()plus petit · plus grand
sql — le tableau de bord du projet
SELECT COUNT(*) FROM etudiants;                                -- nb d'étudiants
SELECT COUNT(*) FROM presences WHERE statut = 'present';       -- nb de présences
SELECT COUNT(*) FROM presences WHERE statut = 'absent';        -- nb d'absences
sql — GROUP BY
-- combien d'étudiants par classe ?
SELECT classe, COUNT(*) AS nombre
FROM etudiants
GROUP BY classe;

-- classe        | nombre
-- BTS CIEL 1    |   18
-- BTS CIEL 2    |   15

-- seulement les classes de plus de 16 étudiants
SELECT classe, COUNT(*) AS nombre
FROM etudiants
GROUP BY classe
HAVING COUNT(*) > 16;
⚠️ WHERE avant, HAVING après
WHEREfiltre les lignes, avant le regroupement
HAVINGfiltre les groupes, après

On ne peut pas écrire WHERE COUNT(*) > 16 : au moment du WHERE, les groupes n'existent pas encore, donc le compte non plus.

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

Cet ordre d'exécution explique tout le reste — y compris pourquoi un alias défini dans le SELECT n'est pas utilisable dans le WHERE.

🧰 COUNT(*) ou COUNT(colonne) ?
sql
COUNT(*)          -- compte TOUTES les lignes
COUNT(telephone)  -- compte celles où telephone n'est PAS NULL

La différence n'apparaît que s'il y a des NULL — mais alors elle est importante. Pour « combien d'étudiants ? », c'est COUNT(*).

11PHP et PDO

PDO (PHP Data Objects) est l'interface standard de PHP pour parler à une base. On écrit la connexion une seule fois, dans un fichier réutilisé partout.

php — db.php
<?php
$hote = "localhost";
$base = "gestion_presence";
$user = "root";
$mdp  = "root";

try {
    $pdo = new PDO(
        "mysql:host=$hote;dbname=$base;charset=utf8mb4",
        $user,
        $mdp
    );
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    die("Connexion échouée : " . $e->getMessage());
}
?>
php — lire des données
<?php
require 'db.php';

$resultat = $pdo->query("SELECT * FROM etudiants");

while ($ligne = $resultat->fetch()) {
    echo $ligne['nom'] . " " . $ligne['prenom'] . "<br>";
}
?>
🧰 Deux réglages qui changent tout
charset=utf8mb4sans lui, les accents et les emoji s'abîment
ERRMODE_EXCEPTIONPDO lève une exception en cas d'erreur SQL, au lieu d'échouer en silence

Sans le second, une requête fautive renvoie simplement false — et le programme continue comme si de rien n'était, jusqu'à planter beaucoup plus loin, sur une cause introuvable.

⚠️ Gardez le même nom de variable partout

Si db.php crée $pdo mais que les pages écrivent $connexion->query(…), le résultat est immédiat :

erreur
Fatal error: Uncaught Error: Call to a member function query() on null

« on null » est l'indice : la variable n'existe pas. Choisissez un nom — $pdo ou $connexion — et tenez-vous-y dans tout le projet.

12Les injections SQL

🔧 Corrigé — le cours annonce le sujet mais ne donne pas la parade

Le polycopié affiche parmi ses objectifs : « assurer la sécurité basique des données et éviter les erreurs courantes (redondances, injections SQL) ». Pourtant il n'enseigne que $connexion->query($sql) avec des variables concaténées dans la chaîne — c'est-à-dire précisément ce qui rend l'injection possible. Les requêtes préparées, seule vraie parade, n'apparaissent nulle part.

Cette section comble ce manque. C'est le point le plus important du chapitre.

Le problème, sur du vrai code

Voici l'authentification telle qu'elle est écrite dans le projet « Gestion des présences » :

php — vulnérable
$login = $_POST['login'];
$mdp   = $_POST['mot_de_passe'];

$sql = "SELECT * FROM administrateurs
        WHERE login = '$login' AND mot_de_passe = '$mdp'";

$resultat = $connexion->query($sql);
if ($resultat->fetch()) {
    $_SESSION['admin_connecte'] = true;   // connecté !
}

Maintenant, un visiteur saisit ceci dans le champ identifiant :

ce qu'il tape
' OR '1'='1

La requête réellement envoyée à la base devient :

sql — ce que voit MariaDB
SELECT * FROM administrateurs
WHERE login = '' OR '1'='1' AND mot_de_passe = ''
                  └──────┘
             toujours vrai

La condition est satisfaite, une ligne est renvoyée, et l'attaquant est connecté en administrateur sans mot de passe. Il n'a eu besoin d'aucun outil : un clavier a suffi.

🌍 Pourquoi ça marche

Le serveur SQL ne reçoit pas « une requête et des données » : il reçoit une seule chaîne de caractères qu'il analyse entièrement comme du code. Une apostrophe saisie par l'utilisateur referme la chaîne SQL, et tout ce qui suit devient de la logique.

C'est la même famille de failles que le XSS vu en PHP : du contenu utilisateur interprété comme du code. Ici la victime n'est pas le navigateur, mais la base de données.

La parade : les requêtes préparées

Le principe : envoyer séparément la requête et les valeurs. Le SQL est analysé d'abord, les données arrivent ensuite — elles ne peuvent donc plus en modifier le sens.

php — sécurisé
$requete = $pdo->prepare(
    "SELECT * FROM administrateurs WHERE login = :login"
);
$requete->execute([':login' => $_POST['login']]);
$admin = $requete->fetch();

if ($admin && password_verify($_POST['mot_de_passe'], $admin['mot_de_passe'])) {
    $_SESSION['admin_connecte'] = true;
}

Avec ' OR '1'='1 saisi dans le champ, la base cherche désormais un administrateur dont le login est littéralement la chaîne ' OR '1'='1. Elle n'en trouve aucun. L'attaque est neutralisée.

🧰 Les trois écritures à connaître
php
// 1. marqueurs nommés — les plus lisibles
$req = $pdo->prepare("INSERT INTO etudiants (nom, prenom, classe)
                      VALUES (:nom, :prenom, :classe)");
$req->execute([
    ':nom'    => $_POST['nom'],
    ':prenom' => $_POST['prenom'],
    ':classe' => $_POST['classe']
]);

// 2. marqueurs positionnels — plus courts
$req = $pdo->prepare("DELETE FROM etudiants WHERE id = ?");
$req->execute([$_GET['id']]);

// 3. lire plusieurs lignes
$req = $pdo->prepare("SELECT * FROM etudiants WHERE classe = :classe");
$req->execute([':classe' => $classe]);
$liste = $req->fetchAll();
Dès qu'une variable entre dans une requête : prepare() + execute().
query() ne sert que pour une requête entièrement fixe.
🔧 Corrigé — htmlspecialchars() ne protège PAS des injections SQL

Le projet contient ce commentaire : « on sécurise les entrées avec htmlspecialchars() comme vu en cours ». C'est une confusion entre deux protections qui n'ont rien à voir :

FonctionProtège deQuand
htmlspecialchars()XSS à l'affichage dans la page
prepare() / execute()injection SQLà l'envoi vers la base

Pire : htmlspecialchars() avant l'insertion abîme les données. Un étudiant nommé « O'Brien » sera stocké en base sous la forme O&#039;Brien — et ressortira ainsi dans tous les exports.

On prépare pour écrire en base.
On échappe pour afficher dans la page.
🔧 Corrigé — un mot de passe ne se stocke jamais en clair

Le script de création de la base insère le compte administrateur ainsi :

sql — dans bdd.sql
INSERT INTO administrateurs (login, mot_de_passe)
VALUES ('admin', 'admin');           -- ✘ en clair dans la table

Quiconque accède à la base — ou récupère une sauvegarde — lit tous les mots de passe. Comme vu au chapitre cybersécurité, on stocke une empreinte :

php
// à l'inscription : on hache
$hash = password_hash($motDePasse, PASSWORD_DEFAULT);
// → $2y$10$K7Lm...   (60 caractères, non réversible)

// à la connexion : on vérifie
if (password_verify($saisie, $admin['mot_de_passe'])) {
    // mot de passe correct
}

C'est aussi pour cela que la colonne est déclarée VARCHAR(255) et non VARCHAR(20) : une empreinte est longue. La structure de la base était déjà prête — il ne manquait que le hachage.

🧠 Auto-évaluation

Cette page supprime un étudiant. Quelles sont les deux failles ?

php — supprimer_etudiant.php
<?php
require 'db.php';
$id = $_GET['id'];
$connexion->query("DELETE FROM etudiants WHERE id = $id");
header("Location: etudiants.php");
?>

1. Injection SQL. $id vient de l'URL et entre directement dans la requête. Il suffit d'appeler supprimer_etudiant.php?id=1 OR 1=1 pour supprimer toute la table.

2. Aucun contrôle de session. La page ne vérifie pas que l'utilisateur est connecté : n'importe qui peut appeler cette URL, sans même passer par la page de connexion.

php — corrigé
<?php
session_start();
if (!isset($_SESSION['admin_connecte'])) {    // le garde
    header("Location: login.php");
    exit();
}

require 'db.php';

$req = $pdo->prepare("DELETE FROM etudiants WHERE id = ?");
$req->execute([$_GET['id']]);                 // requête préparée

header("Location: etudiants.php");
exit();
?>

Les deux protections sont indissociables : une requête préparée sur une page non gardée reste une page que tout le monde peut déclencher.

13Installer MariaDB sur son Mac

Tout ce qui précède suppose un serveur qui tourne. En salle il est déjà là ; sur son propre Mac, il faut l'installer — et c'est là que la plupart des gens perdent une soirée, sur une erreur d'accès qui n'a rien à voir avec le SQL.

🗄️ Où se range quoi

MariaDB est le classeur. Une base de données est un cahier rangé dedans, une table une page du cahier, une ligne une fiche remplie. Installer MariaDB, c'est poser le classeur — il arrive vide.

MariaDB est le frère libre de MySQL : même langage, mêmes commandes, et les deux noms s'emploient l'un pour l'autre dans la quasi-totalité du programme.

Les deux pré-requis

Homebrew est le gestionnaire de paquets de macOS — l'équivalent de apt sous Linux. Il a lui-même besoin des outils de développement d'Apple, gratuits.

bash
# 1. les outils Apple — répond « already installed » s'ils sont déjà là
xcode-select --install

# 2. Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# 3. vérifier
brew --version

L'installation de Homebrew demande le mot de passe de la session : il ne s'affiche pas pendant la frappe, c'est normal.

Installer et démarrer

bash
brew install mariadb          # télécharge MariaDB et ses dépendances
brew services start mariadb   # le démarre, et au démarrage du Mac ensuite
brew services list            # started / stopped : l'état réel
⚠️ Installer ne démarre pas

brew install pose les fichiers, rien de plus. Sans brew services start, le client répondra Can't connect to local server — un message qui fait croire à un problème de mot de passe alors que rien n'écoute.

Avec quel compte se connecter

C'est la question qui coûte la soirée. Sur une installation Homebrew, le compte à utiliser au quotidien n'est pas root : c'est celui qui porte le nom de la session macOS, créé pendant l'installation avec tous les droits. Il suffit donc de ne rien préciser.

bash
mariadb                 # se connecte comme l'utilisateur de la session
mariadb ma_base         # idem, en ouvrant directement une base
mariadb -u root         # ← c'est CELLE-LÀ qui est refusée
🔍 Les deux numéros d'erreur ne disent pas la même chose
  • ERROR 1045 — Access denied … (using password: YES/NO) : le mot de passe fourni ne convient pas.
  • ERROR 1698 — Access denied, sans mention de mot de passe : le compte existe mais n'accepte pas du tout ce mode d'authentification. Aucun mot de passe ne le fera céder.

Relevé sur une installation Homebrew de MariaDB 12.2.2 : mariadb -u root rend un 1698. Taper un mot de passe de plus en plus compliqué ne pouvait donc rien donner.

La raison se lit dans la table des comptes, et c'est la seule commande qui dit la vérité plutôt que de la supposer :

sql
SELECT User, Host, JSON_VALUE(Priv, '$.plugin') AS methode
FROM   mysql.global_priv ORDER BY User;
sortie réelle — MariaDB 12.2.2, installation Homebrew
User          Host        methode
------------  ----------  ---------------------
etudiant      localhost   mysql_native_password      ← le compte de la session
mariadb.sys   localhost   mysql_native_password
root          localhost   mysql_native_password

Trois lignes identiques en apparence — et pourtant root est le seul à être refusé. Le détail est ailleurs, dans deux champs que la colonne plugin ne montre pas :

sortie réelle — la configuration complète de root
plugin                 : mysql_native_password
authentication_string  : invalid
auth_or                : [ {}, { "plugin": "unix_socket" } ]
🧰 Ce que ces trois lignes veulent dire
  • authentication_string: invalid — l'empreinte stockée n'est pas une empreinte valide, donc aucun mot de passe ne peut y correspondre. Ce n'est pas un compte sans mot de passe : c'est un compte dont le mot de passe est volontairement impossible.
  • auth_or — une seconde façon d'entrer est acceptée : unix_socket, qui ne demande rien et vérifie simplement quel utilisateur du système lance la commande.

D'où le comportement complet : mariadb -u root échoue depuis une session ordinaire, et sudo mariadb -u root passe — parce que sudo fait de l'utilisateur système « root », ce que unix_socket reconnaît.

🔧 Corrigé — « après la remise à zéro, mariadb -u root fonctionne »

Le tutoriel d'origine l'annonce deux fois. C'est faux sur une installation Homebrew : la remise à zéro recrée précisément la configuration ci-dessus, invalid comprise, et mariadb -u root rend toujours un 1698. Vérifié sur l'installation de cette machine.

Ce qui fonctionne après la remise à zéro, c'est mariadb tout court. La procédure ci-dessous reste utile — mais elle ne « débloque pas root », elle répare une structure de données abîmée.

Remettre les données à zéro

⚠️ Cette procédure efface toutes les bases

À réserver à une installation fraîche, ou dont on sait qu'elle ne contient rien d'important. Il n'y a pas de retour en arrière, et aucune confirmation n'est demandée.

Le vrai symptôme qui la justifie : Table 'mysql.db' doesn't exist, ou tout message qui désigne une table de la base mysql elle-même. Les tables système sont abîmées — c'est la structure qu'on refait, pas les droits.

bash
# 1. arrêter le serveur
brew services stop mariadb

# 2. supprimer les données — IRRÉVERSIBLE
rm -rf /opt/homebrew/var/mysql

# 3. recréer les tables système
/opt/homebrew/opt/mariadb/bin/mariadb-install-db \
  --user=$(whoami) \
  --basedir=/opt/homebrew/opt/mariadb \
  --datadir=/opt/homebrew/var/mysql

# 4. redémarrer, puis vérifier
brew services start mariadb
mariadb -e "SELECT VERSION();"

--user=$(whoami) n'est pas une formule magique : c'est ce qui crée le compte au nom de la session, celui avec lequel on travaillera ensuite. Les chemins en /opt/homebrew sont ceux d'un Mac à puce Apple ; sur un Mac Intel, Homebrew s'installe dans /usr/local.

Au quotidien

Ce qu'on veutCommandeOù
Démarrer / arrêter / relancer brew services start|stop|restart mariadbterminal
Voir si le serveur tournebrew services listterminal
Se connectermariadbterminal
Lister les basesSHOW DATABASES;client
Choisir une baseUSE ma_base;client
Lister les tablesSHOW TABLES;client
Voir la structure d'une tableDESCRIBE ma_table;client
Sortirexit; ou Ctrl+Dclient
⚠️ L'invite qui passe à ->

Quand MariaDB [(none)]> devient ->, le client n'attend pas une nouvelle commande : il attend la fin de la précédente. Un point-virgule a été oublié. On le tape, on valide, et tout repart — inutile de recommencer la requête.

Entre crochets, le client affiche la base active : [(none)] signifie qu'aucun USE n'a été fait, et c'est la cause la plus fréquente d'un No database selected.

💡 Sécuriser, ou pas

mariadb-secure-installation supprime les comptes anonymes et la base test, et interdit la connexion de root à distance. Sur un serveur, c'est obligatoire. Sur un Mac de TP qui n'écoute que lui-même, c'est facultatif — mais lancez-la au moins une fois pour voir ce qu'elle propose : ses sept questions sont un résumé de ce qu'on laisse ouvert par défaut.

⚡ Récap 2 minutes

  • Table = lignes + colonnes. PK identifie, FK relie.
  • Relations : 1–1, 1–N (la FK va du côté N), N–N (table intermédiaire).
  • MCD → MLD : entités → tables, attributs → colonnes, relations → clés.
  • Créer : CREATE TABLE, types (INT, VARCHAR, DATE), contraintes (NOT NULL, UNIQUE, FOREIGN KEY).
  • Manipuler : INSERT / UPDATE / DELETE — jamais sans WHERE.
  • Interroger : SELECT … WHERE … ORDER BY … LIMIT. IS NULL, pas = NULL.
  • Jointures : INNER (l'intersection) · LEFT (tout à gauche, même sans correspondance).
  • Agrégats : COUNT, SUM, AVG + GROUP BY. WHERE avant, HAVING après.
  • PDO : utf8mb4 + ERRMODE_EXCEPTION.
  • Sur son Mac : brew services start mariadb, puis mariadb sans -u root. Un 1698 dit que le compte refuse ce mode d'authentification, pas que le mot de passe est faux.
  • Toujours prepare() + execute() dès qu'une variable entre dans une requête. Et password_hash() pour les mots de passe.

Sources exploitées

  • Cours « Bases de données » (le professeur, BTS CIEL 2025-2026) — séances du 10/04 au 29/05
  • TP7 et TP8 — requêtes SQL et conception
  • « MariaDB sur Mac », guide d'installation déposé avec le chapitre — refondu au format de ce cours, et ses affirmations revérifiées sur une installation réelle
  • Photos de tableau — MCD USER / PC / Logiciel avec cardinalités, et son passage au modèle relationnel
  • Projet E6 « Application web de gestion des présences » — sujet et réalisation d'élève (base gestion_presence, PDO, CRUD complet)