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.
| Type | Exemples | Usage |
|---|---|---|
| Relationnelle | MariaDB, MySQL, PostgreSQL | données structurées, en tables reliées |
| NoSQL | MongoDB, 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.
02Le modèle relationnel
Les données sont organisées en tables : des lignes (enregistrements) et des colonnes (attributs).
| Terme | Définition | Dans le projet Présences |
|---|---|---|
| Entité | un objet du monde réel que l'on veut stocker | un étudiant, un administrateur |
| Attribut | une caractéristique de l'entité | nom, prenom, classe |
| Association | un lien entre deux entités | un étudiant a des présences |
| Clé primaire (PK) | identifie chaque ligne de façon unique | id |
| Clé étrangère (FK) | pointe vers la clé primaire d'une autre table | id_etudiant |
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.
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
| Relation | Exemple | Traduction en tables |
|---|---|---|
| 1 — 1 | un utilisateur a un compte bancaire | une clé étrangère dans l'une des deux tables |
| 1 — N | un client passe plusieurs commandes | la clé étrangère va du côté N |
| N — N | des étudiants suivent des cours | table intermédiaire avec les deux clés étrangères |
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
- chaque entité devient une table ;
- chaque attribut devient une colonne ;
- on définit la clé primaire de chaque table ;
- 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 ;
- 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 lire | Exemple |
|---|---|---|
0,1 | au plus une fois, éventuellement jamais | un employé peut avoir une place de parking |
1,1 | exactement une fois | un PC appartient à un seul utilisateur |
0,N | autant de fois que l'on veut, zéro compris | un logiciel peut n'être installé nulle part |
1,N | au moins une fois, sans maximum | un utilisateur possède au moins un PC |
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 :
- « 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 maximums | Type | Traduction en tables |
|---|---|---|
1 et N | 1–N | une clé étrangère, placée dans la table du côté
1,1 |
N et N | N–N | une table intermédiaire portant les deux clés — et les éventuels attributs de l'association |
1 et 1 | 1–1 | rare : soit on fusionne les deux entités, soit une clé étrangère unique d'un côté |
1,1, pas du côté 1,NC'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.
USER —(1,N)— Posséder —(1,1)— PC
PC —(1,N)— Installer —(1,N)— LOGICIELPremiè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.
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èresC'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.
Trois entités, une relation 1–N (un étudiant a plusieurs présences) :
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
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
| Type | Pour | Exemple |
|---|---|---|
INT | un nombre entier | un identifiant, une quantité |
VARCHAR(n) | du texte court, longueur variable | VARCHAR(50) pour un nom |
TEXT | du texte long | un commentaire |
DATE | une date | 2026-09-13 |
DATETIME | date et heure | un horodatage |
DECIMAL(p,s) | un nombre décimal exact | un prix |
BOOLEAN | vrai / faux | actif ou non |
Les contraintes
PRIMARY KEY | identifie la ligne — unique et non nulle |
AUTO_INCREMENT | le SGBD numérote tout seul |
NOT NULL | la colonne doit être renseignée |
UNIQUE | pas deux fois la même valeur |
DEFAULT | valeur par défaut si rien n'est fourni |
FOREIGN KEY … REFERENCES | lie à une autre table |
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)
);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_etudiantqui 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 disqueVARCHAR 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
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 supprimerDROP ne demande aucune confirmationDROP 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 ? ».
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
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');UPDATE etudiants
SET classe = 'BTS CIEL 2'
WHERE id = 1;DELETE FROM etudiants
WHERE id = 1;WHERE oublié — la catastrophe classiqueUPDATE etudiants SET classe = 'BTS CIEL 2'; -- ✘ TOUS les étudiants changent de classe
DELETE FROM etudiants; -- ✘ la table est VIDÉESQL 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é.
SELECT * FROM etudiants WHERE id = 1; -- 1. je vérifie : une seule ligne ?
DELETE FROM etudiants WHERE id = 1; -- 2. alors je supprime08SELECT et filtres
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 premiersLes opérateurs du WHERE
= <> > <
>= <= | comparaisons |
AND · OR · NOT | combinaisons |
LIKE '%dup%' | contient — % remplace n'importe quoi |
IN ('BTS1', 'BTS2') | fait partie de la liste |
BETWEEN 1 AND 10 | dans l'intervalle, bornes comprises |
IS NULL · IS NOT NULL | valeur absente ou non |
= NULL ne fonctionne jamaisSELECT * 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.
LIKE de la barre de rechercheLe projet demande de rechercher un étudiant par nom, prénom ou classe :
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.
SELECT * FROM presences;
-- id | id_etudiant | date_presence | statut
-- 1 | 3 | 2026-09-13 | present
-- ↑ « 3 », d'accord… mais QUI est 3 ?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| Jointure | Renvoie |
|---|---|
INNER JOIN | uniquement les lignes présentes dans les deux tables |
LEFT JOIN | toutes celles de la table de gauche,
+ celles de droite quand elles existent (sinon NULL) |
RIGHT JOIN | l'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.
SELECT id FROM presences INNER JOIN etudiants ON …
-- ✘ Error: Column 'id' in field list is ambiguous
SELECT presences.id FROM … -- ✔ on précise la tableLes 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 |
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-- 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èsWHERE | filtre les lignes, avant le regroupement |
HAVING | filtre 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.
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) ?COUNT(*) -- compte TOUTES les lignes
COUNT(telephone) -- compte celles où telephone n'est PAS NULLLa 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
$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
require 'db.php';
$resultat = $pdo->query("SELECT * FROM etudiants");
while ($ligne = $resultat->fetch()) {
echo $ligne['nom'] . " " . $ligne['prenom'] . "<br>";
}
?>charset=utf8mb4 | sans lui, les accents et les emoji s'abîment |
ERRMODE_EXCEPTION | PDO 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.
Si db.php crée $pdo mais que les pages écrivent
$connexion->query(…), le résultat est immédiat :
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
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 » :
$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 :
' OR '1'='1La requête réellement envoyée à la base devient :
SELECT * FROM administrateurs
WHERE login = '' OR '1'='1' AND mot_de_passe = ''
└──────┘
toujours vraiLa 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.
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.
$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.
// 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();prepare() + execute().query() ne sert que pour une requête entièrement fixe.htmlspecialchars() ne protège PAS des injections SQLLe 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 :
| Fonction | Protège de | Quand |
|---|---|---|
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'Brien — et ressortira ainsi dans tous les exports.
On échappe pour afficher dans la page.
Le script de création de la base insère le compte administrateur ainsi :
INSERT INTO administrateurs (login, mot_de_passe)
VALUES ('admin', 'admin'); -- ✘ en clair dans la tableQuiconque 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 :
// à 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.
Cette page supprime un étudiant. Quelles sont les deux failles ?
<?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
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.
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.
# 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 --versionL'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
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éelbrew 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.
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- 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 :
SELECT User, Host, JSON_VALUE(Priv, '$.plugin') AS methode
FROM mysql.global_priv ORDER BY User;User Host methode
------------ ---------- ---------------------
etudiant localhost mysql_native_password ← le compte de la session
mariadb.sys localhost mysql_native_password
root localhost mysql_native_passwordTrois 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 :
plugin : mysql_native_password
authentication_string : invalid
auth_or : [ {}, { "plugin": "unix_socket" } ]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.
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
À 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.
# 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 veut | Commande | Où |
|---|---|---|
| Démarrer / arrêter / relancer | brew services start|stop|restart mariadb | terminal |
| Voir si le serveur tourne | brew services list | terminal |
| Se connecter | mariadb | terminal |
| Lister les bases | SHOW DATABASES; | client |
| Choisir une base | USE ma_base; | client |
| Lister les tables | SHOW TABLES; | client |
| Voir la structure d'une table | DESCRIBE ma_table; | client |
| Sortir | exit; ou Ctrl+D | client |
->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.
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.