Cours MySQL Complet 🐬
Les 12 chapitres essentiels — SQL, tables, requêtes, jointures, index et administration
🏠 Hub Programmation
🐘 PostgreSQL
🍃 MongoDB
🔌 APIs REST
🟢 Node.js
🐍 Python
Introduction et installation
— 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.
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.
Bases de données et tables
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;
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);
Types de données
| 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.
INSERT, UPDATE, DELETE
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é.
SELECT et filtrage
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;
Jointures
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 |
Fonctions et 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 : FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. 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.
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;
Sous-requêtes et vues
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;
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;
Index et performance
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).
Transactions et ACID
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 |
Administration
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’@‘%’;
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.
Bonnes pratiques
| 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 |
✅ À 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
🏠 Hub Programmation
🐘 Cours PostgreSQL
🍃 Cours MongoDB
🔌 Cours APIs REST
🟢 Cours Node.js
🐍 Cours Python
🐘 Cours PHP
Cours MySQL Complet — SQL, tables, jointures, index, transactions et administration
Référence : MySQL Docs | MariaDB | phpMyAdmin

