Cours MySQL Complet 🐬

Les 12 chapitres essentiels — SQL, tables, requêtes, jointures, index et administration

12
Chapitres
120+
Requêtes SQL
MySQL 8+
Version
A1→C2
Niveaux

CHAPITRE 01

Introduction et installation

🐬 Premiers pas
— Installer MySQL
— macOS : brew install mysql && brew services start mysql
— Ubuntu : sudo apt install mysql-server
— Windows : https://dev.mysql.com/downloads/installer/
— Docker : docker run -d -p 3306:3306 -e MYSQL_ROOT_PASSWORD=secret mysql:8

— Se connecter
mysql -u root -p

— Vérifier la version
SELECT VERSION();

— Clients graphiques recommandés :
— • DBeaver (gratuit, multi-BDD)
— • MySQL Workbench (officiel)
— • TablePlus (macOS/Windows)
— • DataGrip (JetBrains, payant)

MySQL est le SGBD relationnel open-source le plus populaire au monde. Créé par Michael Widenius en 1995, racheté par Oracle en 2010. Utilisé par Facebook, Twitter/X, YouTube, Netflix, Uber, Airbnb et WordPress (90% du web). MariaDB est un fork communautaire de MySQL, 100% compatible.

🗄️ Base de données relationnelle

Principe : les données sont organisées en tables (lignes + colonnes). Les tables sont liées entre elles par des clés étrangères (foreign keys). On interroge les données avec le langage SQL (Structured Query Language). SQL est universel : MySQL, PostgreSQL, SQLite, SQL Server utilisent tous SQL avec des variantes mineures.

CHAPITRE 02

Bases de données et tables

🏗️ Créer et gérer
— Bases de données
CREATE DATABASE mon_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SHOW DATABASES;
USE mon_app;
DROP DATABASE mon_app; — ⚠️ Irréversible

— Créer une table
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
role ENUM(‘user’, ‘admin’, ‘editor’) DEFAULT ‘user’,
bio TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

— Table avec clé étrangère
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
user_id INT NOT NULL,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

— Voir la structure
DESCRIBE users;
SHOW CREATE TABLE users;
SHOW TABLES;

🔧 Modifier une table (ALTER)
— Ajouter une colonne
ALTER TABLE users ADD COLUMN phone VARCHAR(20) AFTER email;

— Modifier une colonne
ALTER TABLE users MODIFY COLUMN name VARCHAR(200) NOT NULL;

— Renommer une colonne
ALTER TABLE users RENAME COLUMN phone TO phone_number;

— Supprimer une colonne
ALTER TABLE users DROP COLUMN phone_number;

— Ajouter un index
ALTER TABLE users ADD INDEX idx_email (email);

CHAPITRE 03

Types de données

📋 Types MySQL essentiels
Type Description Taille Usage
Entiers
TINYINT -128 à 127 1 octet Booléens, petits compteurs
INT -2.1 milliards à 2.1 milliards 4 octets IDs, compteurs (le plus courant)
BIGINT Très grands nombres 8 octets Grands IDs, timestamps Unix
Décimaux
DECIMAL(10,2) Précision exacte Variable Argent (jamais FLOAT pour l’argent)
FLOAT / DOUBLE Approximation 4 / 8 octets Calculs scientifiques
Texte
VARCHAR(n) Texte variable (max n) 1 à n octets Noms, emails, titres
TEXT Texte long (64 Ko) Variable Articles, descriptions
ENUM(‘a’,’b’) Valeur parmi une liste 1-2 octets Rôles, statuts
Date / Heure
DATE YYYY-MM-DD 3 octets Anniversaires
DATETIME YYYY-MM-DD HH:MM:SS 8 octets Événements (pas de timezone)
TIMESTAMP Comme DATETIME + timezone 4 octets created_at, updated_at
Autres
BOOLEAN TRUE/FALSE (alias TINYINT(1)) 1 octet Flags on/off
JSON Données JSON (MySQL 5.7+) Variable Données flexibles, métadonnées

Ne JAMAIS utiliser FLOAT ou DOUBLE pour l’argent. Les flottants ont des erreurs d’arrondi : 0.1 + 0.2 = 0.30000000000000004. Utilisez DECIMAL(10,2) pour une précision exacte des prix, salaires, montants.

CHAPITRE 04

INSERT, UPDATE, DELETE

✏️ Écrire des données
— INSERT — Insérer des lignes
INSERT INTO users (name, email, role) VALUES
(‘Alice’, ‘alice@mail.com’, ‘admin’);

— Insertion multiple (beaucoup plus rapide)
INSERT INTO users (name, email) VALUES
(‘Bob’, ‘bob@mail.com’),
(‘Claire’, ‘claire@mail.com’),
(‘David’, ‘david@mail.com’);

— INSERT … ON DUPLICATE KEY UPDATE (upsert)
INSERT INTO users (email, name) VALUES (‘alice@mail.com’, ‘Alice M.’)
ON DUPLICATE KEY UPDATE name = VALUES(name);

— UPDATE — Modifier des lignes
UPDATE users SET name = ‘Alice Martin’ WHERE id = 1;
UPDATE users SET role = ‘editor’, is_active = TRUE WHERE email = ‘bob@mail.com’;

— DELETE — Supprimer des lignes
DELETE FROM users WHERE id = 3;
DELETE FROM users WHERE is_active = FALSE AND created_at < ‘2023-01-01’;

— TRUNCATE — vider la table (plus rapide que DELETE, reset AUTO_INCREMENT)
TRUNCATE TABLE logs;

Toujours un WHERE avec UPDATE et DELETE. Sans WHERE, TOUTES les lignes sont modifiées/supprimées. UPDATE users SET role = ‘admin’ → tout le monde devient admin. Astuce : commencez par un SELECT avec le même WHERE pour vérifier ce qui sera touché.

CHAPITRE 05

SELECT et filtrage

🔍 Interroger les données
— Sélection de base
SELECT * FROM users; — Toutes les colonnes
SELECT name, email FROM users; — Colonnes spécifiques
SELECT DISTINCT role FROM users; — Valeurs uniques

— WHERE — Filtrer
SELECT * FROM users WHERE role = ‘admin’;
SELECT * FROM users WHERE role IN (‘admin’, ‘editor’);
SELECT * FROM users WHERE name LIKE ‘%ali%’; — Contient « ali »
SELECT * FROM users WHERE bio IS NULL; — Bio vide
SELECT * FROM users WHERE bio IS NOT NULL; — Bio remplie
SELECT * FROM users WHERE created_at BETWEEN ‘2024-01-01’ AND ‘2024-12-31’;

— AND / OR / NOT
SELECT * FROM users WHERE role = ‘admin’ AND is_active = TRUE;
SELECT * FROM users WHERE role = ‘admin’ OR role = ‘editor’;

— ORDER BY — Trier
SELECT * FROM users ORDER BY created_at DESC; — Plus récents
SELECT * FROM users ORDER BY name ASC, created_at DESC;

— LIMIT / OFFSET — Pagination
SELECT * FROM users ORDER BY id LIMIT 20; — 20 premiers
SELECT * FROM users ORDER BY id LIMIT 20 OFFSET 40; — Page 3

— Alias
SELECT name AS nom, email AS courriel FROM users;

CHAPITRE 06

Jointures

🔗 JOIN — Combiner des tables
— INNER JOIN — seulement les correspondances
SELECT u.name, p.title, p.created_at
FROM posts p
INNER JOIN users u ON p.user_id = u.id
WHERE p.published = TRUE
ORDER BY p.created_at DESC;

— LEFT JOIN — tous les users, même sans posts
SELECT u.name, COUNT(p.id) AS nb_posts
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name;

— RIGHT JOIN — tous les posts, même si user supprimé
SELECT u.name, p.title
FROM users u
RIGHT JOIN posts p ON u.id = p.user_id;

— Jointure multiple (3 tables)
SELECT u.name, p.title, c.content AS comment
FROM comments c
INNER JOIN posts p ON c.post_id = p.id
INNER JOIN users u ON c.user_id = u.id
ORDER BY c.created_at DESC;

Type Retourne Usage courant
INNER JOIN Lignes avec correspondance des deux côtés Le plus fréquent (posts + auteurs)
LEFT JOIN Toutes les lignes de gauche + correspondances droite Users même sans posts
RIGHT JOIN Correspondances gauche + toutes les lignes de droite Rarement utilisé
CROSS JOIN Produit cartésien (toutes les combinaisons) Générer des paires

CHAPITRE 07

Fonctions et agrégation

📊 GROUP BY et fonctions d’agrégation
— Fonctions d’agrégation
SELECT COUNT(*) AS total FROM users; — Nombre total
SELECT COUNT(*) FROM users WHERE is_active = TRUE; — Compter avec filtre
SELECT AVG(price) AS prix_moyen FROM products; — Moyenne
SELECT SUM(amount) AS total_ventes FROM orders; — Somme
SELECT MIN(price), MAX(price) FROM products; — Min / Max

— GROUP BY — agréger par groupe
SELECT role, COUNT(*) AS nb
FROM users
GROUP BY role;

— GROUP BY + jointure
SELECT u.name, COUNT(p.id) AS nb_posts, MAX(p.created_at) AS dernier_post
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name
ORDER BY nb_posts DESC;

— HAVING — filtrer APRÈS l’agrégation (WHERE = avant)
SELECT user_id, COUNT(*) AS nb_posts
FROM posts
GROUP BY user_id
HAVING nb_posts > 5; — Seulement les auteurs avec > 5 posts

Ordre d’exécution SQL : FROMJOINWHEREGROUP BYHAVINGSELECTORDER BYLIMIT. C’est pour ça que WHERE filtre avant le GROUP BY et HAVING filtre après. On ne peut pas utiliser un alias défini dans SELECT dans le WHERE.

🔧 Fonctions utiles
— Texte
SELECT CONCAT(first_name, ‘ ‘, last_name) AS full_name FROM users;
SELECT UPPER(name), LOWER(email) FROM users;
SELECT LENGTH(name), TRIM(name), SUBSTRING(name, 1, 3) FROM users;
SELECT REPLACE(email, ‘@gmail.com’, ‘@company.com’) FROM users;

— Date
SELECT NOW(), CURDATE(), CURTIME();
SELECT YEAR(created_at), MONTH(created_at), DAY(created_at) FROM users;
SELECT DATE_FORMAT(created_at, ‘%d/%m/%Y’) FROM users;
SELECT DATEDIFF(NOW(), created_at) AS jours_depuis FROM users;

— Conditionnel
SELECT name,
CASE role
WHEN ‘admin’ THEN ‘Administrateur’
WHEN ‘editor’ THEN ‘Éditeur’
ELSE ‘Utilisateur’
END AS role_label
FROM users;

— COALESCE — première valeur non NULL
SELECT name, COALESCE(bio, ‘Pas de bio’) AS bio FROM users;

— IF
SELECT name, IF(is_active, ‘Actif’, ‘Inactif’) AS statut FROM users;

CHAPITRE 08

Sous-requêtes et vues

🔄 Sous-requêtes
— Sous-requête dans WHERE
SELECT * FROM users
WHERE id IN (
SELECT DISTINCT user_id FROM posts WHERE published = TRUE
);

— Sous-requête scalaire (retourne 1 valeur)
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);

— EXISTS — plus performant que IN pour les grosses tables
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM posts p WHERE p.user_id = u.id AND p.published = TRUE
);

— CTE (Common Table Expression) — plus lisible
WITH active_authors AS (
SELECT user_id, COUNT(*) AS nb_posts
FROM posts WHERE published = TRUE
GROUP BY user_id
HAVING nb_posts > 3
)
SELECT u.name, a.nb_posts
FROM users u
INNER JOIN active_authors a ON u.id = a.user_id
ORDER BY a.nb_posts DESC;

👁️ Vues
— Créer une vue (requête réutilisable, comme une table virtuelle)
CREATE VIEW v_user_stats AS
SELECT
u.id, u.name, u.email,
COUNT(p.id) AS nb_posts,
MAX(p.created_at) AS last_post
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name, u.email;

— Utiliser la vue comme une table
SELECT * FROM v_user_stats WHERE nb_posts > 5;

— Supprimer
DROP VIEW v_user_stats;

CHAPITRE 09

Index et performance

⚡ Index — accélérer les requêtes
— Créer un index
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_published ON posts(user_id, published);
CREATE UNIQUE INDEX idx_users_email_unique ON users(email);

— Voir les index d’une table
SHOW INDEX FROM users;

— Supprimer un index
DROP INDEX idx_users_email ON users;

— EXPLAIN — analyser les performances d’une requête
EXPLAIN SELECT * FROM users WHERE email = ‘alice@mail.com’;
EXPLAIN ANALYZE SELECT * FROM posts WHERE user_id = 42 AND published = TRUE;

Quand indexer Quand NE PAS indexer
Colonnes dans WHERE Tables de < 1000 lignes
Colonnes dans JOIN ON Colonnes rarement lues
Colonnes dans ORDER BY Colonnes souvent modifiées
Clés étrangères Colonnes avec très peu de valeurs uniques

Un index = un « sommaire » de la table. Sans index, MySQL scanne TOUTE la table (full table scan). Avec index, il saute directement aux lignes pertinentes (comme un index de livre). Coût : chaque INSERT/UPDATE doit aussi mettre à jour l’index. Règle : indexez ce que vous filtrez (WHERE, JOIN, ORDER BY).

CHAPITRE 10

Transactions et ACID

🔄 Transactions
— Transaction = groupe d’opérations « tout ou rien »
START TRANSACTION;

— Transférer 100€ de Alice vers Bob
UPDATE accounts SET balance = balance 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;

— Si tout est OK :
COMMIT;

— Si erreur → annuler tout :
ROLLBACK;

Propriété ACID Signification
Atomicité Tout réussit ou tout échoue (pas d’état intermédiaire)
Cohérence La BDD passe d’un état valide à un autre état valide
Isolation Les transactions concurrentes ne s’interfèrent pas
Durabilité Une fois commitée, la donnée survit même à un crash

CHAPITRE 11

Administration

👤 Utilisateurs et permissions
— Créer un utilisateur
CREATE USER ‘app_user’@‘%’ IDENTIFIED BY ‘motdepasse_fort’;

— Donner des permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON mon_app.* TO ‘app_user’@‘%’;
FLUSH PRIVILEGES;

— Permissions en lecture seule
GRANT SELECT ON mon_app.* TO ‘readonly’@‘%’;

— Voir les permissions
SHOW GRANTS FOR ‘app_user’@‘%’;

— Supprimer
DROP USER ‘app_user’@‘%’;

💾 Sauvegardes
— Sauvegarde complète (dump SQL)
mysqldump -u root -p mon_app > backup.sql

— Sauvegarde d’une table
mysqldump -u root -p mon_app users > users_backup.sql

— Restaurer
mysql -u root -p mon_app < backup.sql

— Sauvegarde compressée
mysqldump -u root -p mon_app | gzip > backup_$(date +%Y%m%d).sql.gz

— Vérifier l’état du serveur
SHOW PROCESSLIST; — Requêtes en cours
SHOW STATUS; — Statistiques serveur
SHOW VARIABLES LIKE ‘%buffer%’; — Configuration

Sauvegardez automatiquement. Un cron job + mysqldump chaque nuit est le minimum. Pour la production : réplication MySQL (source → replica), sauvegardes incrémentales, et test régulier des restaurations. Une sauvegarde qu’on n’a jamais testée n’est pas une sauvegarde.

CHAPITRE 12

Bonnes pratiques

⚖️ MySQL vs PostgreSQL vs SQLite
Aspect MySQL PostgreSQL SQLite
Type Client-serveur Client-serveur Embarqué (fichier)
Performance Excellente en lecture Excellente (lecture + écriture) Bonne (mono-thread)
Fonctionnalités Bonnes Très avancées (JSONB, arrays, full-text) Basiques
Popularité web #1 (WordPress, Laravel) #2 (Django, Rails, Node) Mobile, petites apps
Idéal pour Sites web, CMS, e-commerce Apps complexes, GIS, analytics Prototypes, apps mobiles
✅ Bonnes pratiques

✅ À FAIRE
utf8mb4 pour supporter les emojis
DECIMAL pour l’argent (jamais FLOAT)
• Index sur les colonnes filtrées (WHERE, JOIN)
EXPLAIN pour analyser les requêtes lentes
• Transactions pour les opérations critiques
NOT NULL par défaut (sauf si NULL a un sens)
• Clés étrangères avec ON DELETE CASCADE
• Sauvegardes automatiques + testées
• Utilisateur dédié par app (pas root)
• Requêtes préparées (anti-injection SQL)

❌ À ÉVITER
SELECT * en production (colonnes explicites)
• UPDATE/DELETE sans WHERE
• Stocker des fichiers en BLOB (utiliser le filesystem)
• FLOAT pour les prix et montants
• Pas d’index sur les clés étrangères
• Concaténer l’input utilisateur dans le SQL
• Trop d’index (ralentit les INSERT/UPDATE)
• Table fourre-tout avec 100 colonnes
utf8 (utiliser utf8mb4 — utf8 est tronqué)
• Root en production

🧠 Quiz
WHERE vs HAVING — quelle différence ?
WHERE filtre avant l’agrégation (sur les lignes individuelles). HAVING filtre après l’agrégation (sur les résultats du GROUP BY). WHERE role = ‘admin’ → ne garde que les admins puis agrège. HAVING COUNT(*) > 5 → agrège tout puis ne garde que les groupes avec > 5 lignes.
Pourquoi utiliser utf8mb4 au lieu de utf8 ?
Le utf8 de MySQL est en réalité utf8mb3 — il ne supporte que 3 octets par caractère, ce qui exclut les emojis (🎉), certains caractères chinois et des symboles. utf8mb4 est le vrai UTF-8 (4 octets). Utilisez toujours utf8mb4_unicode_ci.
Que fait ON DELETE CASCADE ?
Quand vous supprimez un user, ON DELETE CASCADE supprime automatiquement tous les posts liés à cet user. Sans CASCADE, MySQL refuse la suppression si des lignes enfant existent (erreur de contrainte). Alternatives : ON DELETE SET NULL (met la FK à NULL) ou ON DELETE RESTRICT (interdit la suppression).

Cours MySQL Complet — SQL, tables, jointures, index, transactions et administration

Référence : MySQL Docs | MariaDB | phpMyAdmin