// Formation complète — MySQL & PostgreSQL

Apprendre SQL
de A à Z

50 leçons progressives pour maîtriser les bases de données relationnelles — explications approfondies, exemples concrets, pièges à éviter.

50Leçons
5Chapitres
200+Exemples
Possibilités
Défiler
Chapitre 1 — Fondamentaux (Leçons 01–10)
LEÇON 01

Introduction à SQL

SQL — Structured Query Language — est le langage standardisé pour communiquer avec les bases de données relationnelles. Conçu en 1974 par Edgar Codd et Donald Chamberlin chez IBM sous le nom SEQUEL, il est devenu l'ISO standard en 1987 et reste à ce jour le langage de données le plus utilisé au monde.

Ce qui rend SQL universel, c'est sa lisibilité : une requête SQL se lit presque comme une phrase en anglais — "Sélectionne les noms des clients qui habitent à Dakar, triés alphabétiquement". La syntaxe est déclarative : vous décrivez quoi vous voulez, pas comment le récupérer.

Les quatre familles de commandes

FamilleCommandes principalesRôle
DDLCREATE, ALTER, DROP, TRUNCATEData Definition Language — Définit et modifie la structure des objets (tables, vues, index).
DMLSELECT, INSERT, UPDATE, DELETEData Manipulation Language — Lit et manipule les données stockées.
DCLGRANT, REVOKEData Control Language — Gère les permissions et accès utilisateurs.
TCLCOMMIT, ROLLBACK, SAVEPOINTTransaction Control Language — Contrôle les unités de travail atomiques.

Quel moteur choisir ?

🐬 MySQL / MariaDB

Le plus répandu sur le web. Léger, rapide en lecture. Idéal pour les applications web (WordPress, PHP). MariaDB est son fork open-source maintenu par la communauté.

🐘 PostgreSQL

Le plus riche en fonctionnalités. Supporte les types JSON, les tableaux, les fonctions fenêtres avancées. Choix par défaut pour les nouvelles applications sérieuses.

📦 SQLite

Embarqué dans le fichier de l'application. Zéro serveur, zéro configuration. Utilisé dans iOS, Android, navigateurs web, Electron. Parfait pour le développement local.

🏢 SQL Server / Oracle

Moteurs d'entreprise avec outils d'administration avancés, haute disponibilité, et licences commerciales. Courants dans les grandes organisations.

💬
Analogie : Si une base de données est une bibliothèque, SQL est la façon dont vous posez vos questions à la bibliothécaire : "Donne-moi tous les romans publiés après 2010, triés par auteur." La bibliothécaire (le moteur SQL) sait comment aller les trouver — vous vous concentrez sur votre besoin.
// votre première requête SQLSELECT 'Bonjour SQL' AS message, NOW() AS date_actuelle;
Résultat
messagedate_actuelle
Bonjour SQL2025-06-15 14:30:00
LEÇON 02

Comprendre une Base de Données Relationnelle

Une base de données relationnelle organise les informations en tables liées entre elles. Ce modèle, formalisé par Edgar Codd en 1970, repose sur la théorie des ensembles et la logique des prédicats. Il est encore dominant parce qu'il répond à deux défis fondamentaux : éviter la redondance et garantir la cohérence.

Imaginez une librairie en ligne. Sans base relationnelle, vous stockeriez pour chaque commande le nom du client, son email, son adresse, le nom de l'auteur, l'éditeur... Résultat : si un client change d'adresse, vous devez modifier des centaines de lignes. Si un livre change de prix, toutes les anciennes commandes sont affectées. C'est le cauchemar de la redondance.

🏫
Analogie — Une école : Un dossier par élève (table eleves), un dossier par cours (table cours), un cahier des présences (table inscriptions). Chaque dossier = une table, chaque page = une ligne (enregistrement), chaque rubrique = une colonne (attribut).
Base de données : librairie_online
├── clients (id, nom, email, adresse_id)
├── adresses (id, rue, ville, pays)
├── livres (id, titre, prix, auteur_id, editeur_id)
├── auteurs (id, nom, nationalite)
├── commandes (id, client_id, date, statut)
└── lignes_commande (id, commande_id, livre_id, quantite, prix_unitaire)

Chaque table représente un concept métier unique. Le prix d'un livre est stocké une seule fois dans la table livres. L'adresse d'un client est dans adresses. Les relations entre ces concepts passent par des clés (identifiants numériques qui pointent d'une table vers l'autre).

💡
Règle d'or : "Chaque fait doit exister en un seul endroit." Si pour mettre à jour une information vous devez toucher plusieurs lignes ou plusieurs tables, votre modèle a un problème de redondance.
LEÇON 03

Les Tables — Structure et Création

Une table est une grille à deux dimensions : des colonnes (définies à la création — les attributs) et des lignes ou enregistrements (ajoutés dynamiquement — les données). Une table bien conçue représente exactement un seul concept ou entité du monde réel.

// création d'une table complèteCREATE TABLE IF NOT EXISTS produits (
  id          INT AUTO_INCREMENT PRIMARY KEY,
  nom         VARCHAR(200) NOT NULL,
  description TEXT,
  prix        DECIMAL(10,2) NOT NULL,
  stock       INT DEFAULT 0,
  statut      ENUM('disponible', 'rupture', 'archivé') DEFAULT 'disponible',
  created_at  DATETIME DEFAULT NOW(),
  updated_at  DATETIME DEFAULT NOW() ON UPDATE NOW()
);

Bonnes pratiques de nommage

Structure de la table produits (DESCRIBE produits)
ColonneTypeNullDefault
idINT AUTO_INCREMENTNO
nomVARCHAR(200)NO
prixDECIMAL(10,2)NO
stockINTYES0
statutENUM(...)YESdisponible
ℹ️
CREATE TABLE IF NOT EXISTS est une sécurité essentielle : la requête ne plante pas si la table existe déjà. À utiliser systématiquement dans vos scripts d'initialisation.
LEÇON 04

Colonnes et Types de Données

Choisir le bon type de données est une décision architecturale importante. Elle détermine l'espace disque utilisé, les opérations possibles, et l'intégrité des données. Un mauvais choix peut impacter les performances de façon significative sur des millions de lignes.

Types numériques

TypePlage / TailleUsage recommandé
TINYINT−128 à 127 (1 octet)Booléens, scores, petits compteurs. TINYINT UNSIGNED = 0–255.
INT−2,1 milliards à +2,1 milliards (4 octets)IDs, compteurs courants. INT UNSIGNED double la plage positive.
BIGINT±9,2 × 10¹⁸ (8 octets)IDs de très grandes tables (Twitter, Facebook utilisent BIGINT).
DECIMAL(p,s)Précision exacteObligatoire pour les prix et montants. DECIMAL(10,2) = jusqu'à 99999999.99.
FLOAT / DOUBLEApproximatifMesures scientifiques. Jamais pour l'argent (0.1 + 0.2 ≠ 0.3 exactement).

Types texte

TypeLongueur maxUsage recommandé
CHAR(n)Fixe — exactement n caractèresCodes fixes : codes pays (FR, SN), numéros SIRET. Légèrement plus rapide que VARCHAR.
VARCHAR(n)Variable — jusqu'à n caractèresNoms, emails, titres. Le plus courant. N'occupe que l'espace nécessaire.
TEXT65 535 caractèresDescriptions, articles courts, commentaires.
MEDIUMTEXT16 millions de caractèresArticles longs, contenu HTML, JSON.
LONGTEXT4 milliards de caractèresFichiers texte très volumineux, logs massifs.
⚠️
L'erreur classique du FLOAT :
SELECT 0.1 + 0.2; → retourne 0.30000000000000004 en FLOAT.
SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2)); → retourne 0.30 exactement.
Pour tout ce qui touche à l'argent, utilisez toujours DECIMAL.
🔬
Types temporels : DATE (2025-06-15), TIME (14:30:00), DATETIME (2025-06-15 14:30:00, pas de fuseau horaire), TIMESTAMP (stocké en UTC, converti automatiquement selon le fuseau du serveur — préférez-le pour les horodatages). YEAR pour stocker une année seule.
LEÇON 05

Créer et Gérer une Base de Données

Avant de créer des tables, vous travaillez dans un contexte : une base de données (aussi appelée schema dans certains moteurs). C'est le conteneur principal. Un serveur MySQL peut héberger des centaines de bases isolées — chacune avec ses propres tables, utilisateurs et configurations.

// gestion de la base de données-- 1. Créer avec encodage complet (utf8mb4 supporte tous les caractères Unicode + emojis)
CREATE DATABASE IF NOT EXISTS ecole
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

-- 2. Sélectionner comme contexte de travail
USE ecole;

-- 3. Lister toutes les bases du serveur
SHOW DATABASES;

-- 4. Voir les tables de la base active
SHOW TABLES;

-- 5. Décrire la structure d'une table
DESCRIBE utilisateurs;
-- Ou en plus détaillé :
SHOW CREATE TABLE utilisateurs;
ℹ️
utf8mb4 vs utf8 en MySQL : L'encodage utf8 de MySQL est une implémentation incomplète qui ne stocke que 3 octets par caractère maximum. Les emojis et certains caractères CJK (chinois/japonais/coréen) nécessitent 4 octets. Utilisez toujours utf8mb4. La collation utf8mb4_unicode_ci fait les comparaisons de façon insensible à la casse (ci = case insensitive).
✦ Exercice pratique

Créez une base boutique_online en utf8mb4, sélectionnez-la, puis créez dedans une table produits avec les colonnes : id (INT AUTO_INCREMENT PRIMARY KEY), nom (VARCHAR 200, NOT NULL), prix (DECIMAL 10,2), stock (INT DEFAULT 0), created_at (DATETIME DEFAULT NOW()).

Vérifiez la création avec DESCRIBE produits.

LEÇON 06

Le Modèle Relationnel — Entités et Relations

Avant d'écrire du SQL, il faut modéliser. Le modèle entité-relation (MER) identifie les entités (les "choses" à stocker), leurs attributs, et les relations entre elles. C'est la phase de conception qui détermine la qualité de toute la base.

Trois types de relations existent : 1 à 1 (un utilisateur = un profil), 1 à plusieurs (un client = plusieurs commandes), plusieurs à plusieurs (un étudiant suit plusieurs cours, un cours a plusieurs étudiants). Les relations M à N nécessitent une table de jonction.

Exemple : Système de gestion scolaire etudiants (id, nom, prenom, date_naissance, classe_id)
└── 1:N avec classes (un étudiant appartient à une classe)

cours (id, titre, credits, professeur_id)
└── 1:N avec professeurs (un cours a un professeur)

inscriptions (etudiant_id, cours_id, note, date_inscription)
└── Table de jonction M:N entre etudiants et cours
🔑 Relation 1:1

Un utilisateur a exactement un profil. Stockez une colonne utilisateur_id UNIQUE dans la table profils. Peu fréquent — souvent, on fusionne les deux tables.

📦 Relation 1:N

Un client peut avoir plusieurs commandes. Mettez client_id dans la table commandes. Le "N" (plusieurs) porte la clé étrangère vers le "1".

🕸️ Relation M:N

Étudiants ↔ Cours. Créez une table de jonction inscriptions avec etudiant_id et cours_id. La table peut porter des attributs propres à la relation (la note).

🔄 Relation réflexive

Un employé a un manager... qui est aussi un employé. employes(id, nom, manager_id)manager_id référence la même table.

LEÇON 07

Clés Primaires — PRIMARY KEY

La clé primaire est l'identifiant unique et invariable de chaque ligne. Elle garantit qu'on peut toujours retrouver, modifier ou supprimer un enregistrement précis sans ambiguïté. Toute table sérieuse doit en avoir une.

En pratique, trois types de clés primaires coexistent :

Clé entière auto-incrémentée

Simple, performante, universelle. Le moteur génère automatiquement 1, 2, 3... Idéale pour 99% des tables. INT pour < 2 milliards de lignes, BIGINT sinon.

UUID (Universally Unique ID)

Chaîne hexadécimale de 36 caractères. Avantage : pas de collision entre serveurs (sharding, réplication). Inconvénient : plus lourd, plus lent sur les index.

Clé naturelle

Un attribut métier unique : IBAN, numéro de passeport. Déconseillé : ces valeurs peuvent changer dans le monde réel, ce qui casse les relations.

Clé composite

Combinaison de deux colonnes : (etudiant_id, cours_id) pour les inscriptions. Garantit l'unicité de la paire. Utilisée dans les tables de jonction.

// différentes formes de clés primaires-- Option 1 : INT AUTO_INCREMENT (recommandé)
CREATE TABLE clients (
  id    INT AUTO_INCREMENT PRIMARY KEY,
  nom   VARCHAR(100) NOT NULL
);

-- Option 2 : BIGINT pour les très grandes tables
CREATE TABLE transactions (
  id    BIGINT AUTO_INCREMENT PRIMARY KEY,
  ...
);

-- Option 3 : UUID
CREATE TABLE sessions (
  id     CHAR(36) PRIMARY KEY DEFAULT (UUID()),
  ...
);

-- Option 4 : Clé primaire composite
CREATE TABLE inscriptions (
  etudiant_id INT,
  cours_id    INT,
  note        DECIMAL(4,2),
  PRIMARY KEY (etudiant_id, cours_id)
);
💡
Pourquoi pas une clé naturelle ? Un email peut changer. Un numéro de téléphone peut être réattribué. Un numéro de sécurité sociale peut contenir des erreurs de saisie. Préférez toujours un INT AUTO_INCREMENT et stockez l'attribut naturel dans une colonne séparée avec contrainte UNIQUE.
LEÇON 08

Clés Étrangères — FOREIGN KEY

Une clé étrangère est une colonne dont la valeur doit correspondre à une clé primaire existante dans une autre table. Elle garantit l'intégrité référentielle : impossible d'avoir une commande pour un client qui n'existe pas, ou de supprimer un client qui a des commandes actives.

// déclaration d'une clé étrangère complèteCREATE TABLE commandes (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  client_id  INT NOT NULL,
  montant    DECIMAL(10,2) NOT NULL,
  created_at DATETIME DEFAULT NOW(),

  -- Déclaration de la contrainte FK
  CONSTRAINT fk_commandes_client
    FOREIGN KEY (client_id)
    REFERENCES clients(id)
    ON DELETE RESTRICT   -- Ne peut pas supprimer un client avec des commandes
    ON UPDATE CASCADE    -- Si l'id client change, répercuter automatiquement
);
OptionON DELETEON UPDATE
RESTRICTBloque la suppression du parent s'il a des enfants. Comportement recommandé par défaut.Bloque la modification de la clé parent.
CASCADESupprime automatiquement tous les enregistrements enfants. Utile pour les logs, les sessions.Propage le changement à tous les enfants.
SET NULLMet la FK à NULL dans les enfants. La colonne doit être nullable.Met la FK à NULL si le parent change.
NO ACTIONComportement similaire à RESTRICT dans MySQL.Idem.
⚠️
Ordre de création : La table référencée (parent) doit exister avant la table qui référence (enfant). Si vous avez des dépendances circulaires, créez d'abord les tables sans FK, puis ajoutez-les avec ALTER TABLE. Pour désactiver temporairement les FK (import massif) : SET FOREIGN_KEY_CHECKS = 0;
LEÇON 09

Contraintes — NOT NULL, UNIQUE, CHECK, DEFAULT

Les contraintes sont des règles d'intégrité définies au niveau de la base de données. Elles constituent la dernière ligne de défense contre des données incohérentes — même si votre application a un bug ou qu'un développeur insère des données directement en console.

// contraintes exhaustivesCREATE TABLE utilisateurs (
  id          INT          AUTO_INCREMENT PRIMARY KEY,
  nom         VARCHAR(100) NOT NULL,                       -- Obligatoire
  email       VARCHAR(200) UNIQUE NOT NULL,                -- Obligatoire + unique
  age         INT          CHECK (age BETWEEN 0 AND 150),  -- Validation de plage
  role        ENUM('admin','user','guest') DEFAULT 'user',   -- Valeur par défaut
  telephone   VARCHAR(20),                                  -- Optionnel (NULL autorisé)
  created_at  DATETIME     DEFAULT NOW(),
  updated_at  DATETIME     DEFAULT NOW() ON UPDATE NOW(),

  -- Contrainte unique composite : combinaison unique
  UNIQUE KEY uq_nom_tel (nom, telephone)
);

-- Ajouter des contraintes sur une table existante
ALTER TABLE produits
  ADD CONSTRAINT chk_prix_positif CHECK (prix > 0),
  ADD CONSTRAINT chk_stock_non_negatif CHECK (stock >= 0);
ℹ️
UNIQUE et NULL : Une colonne UNIQUE peut contenir plusieurs NULL (car NULL ≠ NULL). Pour qu'une colonne soit à la fois unique et obligatoire : UNIQUE NOT NULL. La contrainte PRIMARY KEY est implicitement UNIQUE NOT NULL.
LEÇON 10

Insérer des Données — INSERT INTO

INSERT INTO ajoute de nouveaux enregistrements. Vous pouvez insérer une seule ligne ou plusieurs milliers en une seule requête — bien plus efficace car le moteur optimise le traitement par lot.

// toutes les formes d'INSERT-- Insertion simple avec colonnes explicites (TOUJOURS spécifier les colonnes)
INSERT INTO clients (nom, email, ville)
VALUES ('Aminata Diallo', 'aminata@gmail.com', 'Dakar');

-- Insertion multiple : bien plus rapide qu'une boucle d'INSERTs individuels
INSERT INTO clients (nom, email, ville) VALUES
  ('Ibrahima Sow',   'ibrahima@yahoo.fr',  'Thiès'),
  ('Fatou Ndiaye',   'fatou@outlook.com',  'Dakar'),
  ('Moussa Traoré', 'moussa@gmail.com',   'Saint-Louis'),
  ('Aïcha Koné',    'aicha@orange.sn',    'Ziguinchor');

-- INSERT ... SELECT : copier des données d'une table à l'autre
INSERT INTO clients_archive (nom, email, archived_at)
SELECT nom, email, NOW()
FROM clients
WHERE created_at < '2023-01-01'
  AND statut = 'inactif';

-- INSERT ... ON DUPLICATE KEY UPDATE : upsert (insérer ou mettre à jour)
INSERT INTO stats_quotidiennes (date, nb_visites)
VALUES (CURDATE(), 1)
ON DUPLICATE KEY UPDATE nb_visites = nb_visites + 1;

-- Récupérer l'id du dernier INSERT
SELECT LAST_INSERT_ID() AS nouvel_id;
💡
Toujours spécifier les colonnes : INSERT INTO t VALUES (...) est fragile — il suffit d'ajouter une colonne ou de les réordonner pour que la requête plante ou insère des données dans les mauvaises colonnes. Listez toujours explicitement les colonnes cibles.
Chapitre 2 — Lecture et Filtrage (Leçons 11–20)
LEÇON 11

Afficher les Données — SELECT

SELECT est la commande la plus puissante et la plus utilisée de SQL. Elle interroge une ou plusieurs tables et retourne un jeu de résultats (un tableau virtuel). La clé pour de bonnes performances : ne sélectionner que ce dont on a besoin.

// anatomie complète d'un SELECT-- Ordre OBLIGATOIRE des clauses :
SELECT   colonnes_ou_expressions     -- Quoi retourner
FROM     table_principale             -- Où chercher
JOIN     autre_table ON condition     -- Relier d'autres tables
WHERE    condition_ligne              -- Filtrer les lignes
GROUP BY colonnes_agrégation         -- Regrouper
HAVING   condition_groupe            -- Filtrer les groupes
ORDER BY colonnes_tri                -- Trier
LIMIT    n OFFSET m;                -- Paginer

-- Exemples pratiques progressifs
SELECT * FROM clients;                            -- Toutes les colonnes (déconseillé en production)
SELECT nom, email FROM clients;                  -- Colonnes spécifiques
SELECT nom AS "Nom du client" FROM clients;      -- Avec alias
SELECT nom, prix * 1.18 AS prix_ttc FROM produits; -- Calcul à la volée
SELECT CONCAT(prenom, ' ', nom) AS nom_complet FROM employes; -- Fonction
SELECT 2025 - YEAR(date_naissance) AS age FROM employes; -- Calcul sur date
⚠️
Évitez SELECT * en production : Il récupère toutes les colonnes, même celles inutiles, surcharge le réseau, et casse votre code dès qu'une colonne est ajoutée, renommée ou réordonnée. Le surcoût peut être considérable si la table contient des colonnes TEXT ou BLOB volumineuses.
ℹ️
Ordre d'exécution interne : Le moteur SQL n'exécute pas les clauses dans l'ordre où vous les écrivez. L'ordre réel est : FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT. C'est pourquoi on ne peut pas utiliser un alias défini dans SELECT directement dans WHERE.
LEÇON 12

Filtrer avec WHERE

WHERE est le filtre fondamental de SQL. Il évalue une condition pour chaque ligne et ne retourne que les lignes où la condition est vraie. Sans WHERE, toutes les lignes sont traitées — sur une table de 10 millions de lignes, cela peut prendre plusieurs minutes.

// filtrage avec WHERE — exemples complets-- Valeur exacte
SELECT * FROM clients WHERE id = 42;

-- Texte (sensible à la casse selon la collation)
SELECT * FROM clients WHERE ville = 'Dakar';

-- Plage de dates
SELECT * FROM commandes
WHERE created_at >= '2024-01-01'
  AND created_at  < '2025-01-01';  -- Préférez < au lieu de <= sur les dates

-- Calcul dans la condition
SELECT nom, salaire FROM employes
WHERE salaire * 12 > 6000000;  -- Salaire annuel > 6 millions FCFA

-- Combinaisons de conditions
SELECT * FROM produits
WHERE (prix BETWEEN 5000 AND 50000)
  AND  statut = 'disponible'
  AND  stock > 0;
🔍
Analogie : WHERE est comme un crible. Vous versez toutes les données dedans ; seules celles qui correspondent à vos critères passent à travers. Plus le filtre est sélectif, moins de données voyagent sur le réseau, moins de mémoire est utilisée, plus c'est rapide.
🔬
Sargability (Search ARGument ABILity) : Une condition WHERE est "sargable" si le moteur peut utiliser un index pour l'évaluer. WHERE email = 'x' est sargable. WHERE UPPER(email) = 'X' ne l'est pas (la fonction empêche l'utilisation de l'index). Évitez d'appliquer des fonctions à la colonne filtrée.
LEÇON 13

Opérateurs de Comparaison

Les opérateurs de comparaison construisent les conditions dans WHERE, HAVING, et CASE WHEN. Ils fonctionnent sur les nombres, les textes (comparaison lexicographique) et les dates (comparaison chronologique).

OpérateurSignificationExemple
=Égal àWHERE statut = 'actif'
!= ou <>Différent de (les deux sont équivalents)WHERE pays != 'FR'
>Strictement supérieurWHERE prix > 10000
<Strictement inférieurWHERE stock < 5
>=Supérieur ou égal (inclut la borne)WHERE note >= 10
<=Inférieur ou égal (inclut la borne)WHERE age <= 25
IS NULLLa valeur est absente/inconnueWHERE telephone IS NULL
IS NOT NULLLa valeur est présenteWHERE manager_id IS NOT NULL
// opérateurs sur les dates-- Commandes passées APRÈS le 1er janvier 2024
SELECT * FROM commandes WHERE date_commande > '2024-01-01';

-- Clients nés en 1990 ou avant
SELECT * FROM clients WHERE YEAR(date_naissance) <= 1990;

-- Comparaison de texte (ordre alphabétique)
SELECT * FROM clients WHERE nom > 'M'; -- Noms après 'M' alphabétiquement
⚠️
Ne jamais comparer à NULL avec = : WHERE age = NULL retourne toujours 0 ligne, même s'il y a des NULL ! En SQL, NULL = NULL ne vaut pas TRUE mais NULL. Utilisez obligatoirement IS NULL ou IS NOT NULL.
LEÇON 14

AND, OR, NOT — Combiner les Conditions

Les opérateurs logiques combinent plusieurs conditions. Priorité : NOT s'évalue en premier, puis AND, puis OR. Quand vos conditions mélangent AND et OR, les parenthèses sont essentielles pour clarifier l'intention.

// logique booléenne en SQL-- AND : toutes les conditions doivent être vraies
SELECT * FROM employes
WHERE salaire > 400000
  AND departement = 'Tech'
  AND anciennete_annees >= 2
  AND statut != 'inactif';

-- OR : au moins une condition suffit
SELECT * FROM clients
WHERE ville = 'Dakar'
   OR ville = 'Abidjan'
   OR ville = 'Bamako';
-- Plus élégant avec IN :
WHERE ville IN ('Dakar', 'Abidjan', 'Bamako')

-- ❌ AMBIGU — AND a priorité sur OR
WHERE statut = 'vip' OR solde > 1000000 AND anciennete > 2;
-- Interprété comme : statut = 'vip' OR (solde > 1000000 AND anciennete > 2)

-- ✅ EXPLICITE — parenthèses obligatoires
WHERE (statut = 'vip' OR solde > 1000000)
  AND anciennete > 2;

-- NOT : inverse une condition
WHERE NOT (ville = 'Dakar' AND statut = 'bloqué')
🧮
Table de vérité : AND se comporte comme une multiplication booléenne (VRAI × VRAI = VRAI, VRAI × FAUX = FAUX). OR se comporte comme une addition (au moins un VRAI = VRAI). NULL dans une expression logique propagé sa nature inconnue : TRUE AND NULL = NULL, FALSE OR NULL = NULL.
LEÇON 15

LIKE — Recherche par Motif dans le Texte

LIKE effectue une recherche par motif (pattern matching). Deux jokers : % représente zéro ou plusieurs caractères quelconques, _ représente exactement un caractère quelconque.

// tous les cas d'usage de LIKE-- Commence par 'A'
SELECT * FROM clients WHERE nom LIKE 'A%';

-- Se termine par 'diallo' (insensible à la casse selon collation)
SELECT * FROM clients WHERE nom LIKE '%diallo';

-- Contient 'orange' n'importe où
SELECT * FROM produits WHERE description LIKE '%orange%';

-- Emails Gmail
SELECT * FROM utilisateurs WHERE email LIKE '%@gmail.com';

-- Codes produit : 3 lettres + exactement 4 chiffres
SELECT * FROM produits WHERE code LIKE '___####';
-- Ou en SQL standard avec _ pour un caractère :
SELECT * FROM produits WHERE code LIKE '_______'; -- 7 underscores = 7 caractères

-- Deuxième lettre est 'a'
SELECT * FROM villes WHERE nom LIKE '_a%';

-- NOT LIKE : exclure un motif
SELECT * FROM clients WHERE email NOT LIKE '%@hotmail%';

-- Recherche insensible à la casse forcée :
SELECT * FROM clients WHERE LOWER(nom) LIKE '%diallo%';
⚠️
Performances : LIKE 'texte%' (commence par) peut utiliser un index. LIKE '%texte%' ou LIKE '%texte' (avec % au début) ne peut pas — il force un full scan. Pour des recherches full-text performantes sur de grandes tables, utilisez MATCH() AGAINST() avec un index FULLTEXT, ou un moteur externe (Elasticsearch, Meilisearch).
LEÇON 16

BETWEEN et IN — Plages et Listes

BETWEEN a AND b teste si une valeur est dans une plage avec les deux bornes incluses. IN (liste) teste l'appartenance à un ensemble — bien plus lisible et souvent plus rapide que de chaîner des OR.

// BETWEEN et IN-- BETWEEN sur nombres (bornes incluses)
SELECT * FROM produits WHERE prix BETWEEN 5000 AND 50000;
-- Strictement équivalent à :
WHERE prix >= 5000 AND prix <= 50000;

-- BETWEEN sur des dates
SELECT * FROM commandes
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31 23:59:59';
-- Attention : '2024-12-31' seul exclut les lignes après minuit ce jour-là !

-- NOT BETWEEN : hors de la plage
SELECT * FROM employes WHERE anciennete NOT BETWEEN 1 AND 3;

-- IN : liste de valeurs (beaucoup plus lisible que des OR)
SELECT * FROM clients
WHERE ville IN ('Dakar', 'Thiès', 'Saint-Louis', 'Ziguinchor', 'Kaolack');

-- IN avec des entiers
SELECT * FROM commandes WHERE statut_id IN (1, 3, 5);

-- NOT IN : exclure une liste
SELECT * FROM produits
WHERE categorie NOT IN ('archivé', 'supprimé', 'test');
⚠️
Piège de NOT IN avec NULL : WHERE id NOT IN (1, 2, NULL) retourne zéro résultat ! Car 5 != NULL est NULL (ni vrai ni faux), donc aucune ligne ne passe. Si la liste peut contenir des NULL (notamment si elle vient d'une sous-requête), utilisez NOT EXISTS à la place.
LEÇON 17

Les Valeurs NULL — L'Inconnu en SQL

NULL ne signifie pas zéro, ni une chaîne vide, ni false. NULL signifie "information absente ou inconnue". C'est une valeur spéciale qui a des règles de comportement propres, source de nombreux bugs chez les développeurs qui ne les connaissent pas.

// comportement de NULL-- Tester la présence/absence d'une valeur
SELECT * FROM utilisateurs WHERE telephone IS NULL;
SELECT * FROM employes     WHERE manager_id IS NOT NULL;

-- ❌ NE FONCTIONNE JAMAIS (retourne toujours 0 ligne)
SELECT * FROM utilisateurs WHERE telephone = NULL;
SELECT * FROM utilisateurs WHERE telephone != NULL;

-- NULL dans les calculs arithmétiques (contagieux !)
SELECT 100 + NULL;         -- → NULL (pas 100)
SELECT NULL * 500;          -- → NULL
SELECT CONCAT('Bonjour', NULL); -- → NULL en MySQL !

-- Remplacer NULL par une valeur par défaut
SELECT nom, IFNULL(telephone, 'Non renseigné') FROM clients;

-- NULL dans les fonctions d'agrégation
SELECT
  COUNT(*)         AS total_lignes,      -- Compte TOUT, y compris NULL
  COUNT(telephone) AS avec_telephone,     -- Ignore les NULL
  AVG(bonus)       AS bonus_moyen         -- Ignore les NULL dans le calcul
FROM employes;
Comportement de NULL — Référence rapide
ExpressionRésultatPourquoi ?
NULL = NULLNULLL'inconnu n'est pas égal à l'inconnu
NULL IS NULLTRUE ✓IS NULL est fait pour ça
5 + NULLNULLNULL est "contagieux"
TRUE AND NULLNULLOn ne sait pas si c'est vrai
FALSE AND NULLFALSEPeu importe l'inconnu, c'est faux
TRUE OR NULLTRUEAu moins un est vrai
LEÇON 18

Trier avec ORDER BY

Sans ORDER BY, l'ordre des lignes retournées par une requête est totalement indéterminé. Le moteur peut les retourner dans l'ordre d'insertion, l'ordre de l'index utilisé, ou n'importe quel autre ordre interne. Si l'ordre compte, utilisez toujours ORDER BY.

// tris simples et avancés-- Tri alphabétique croissant (ASC est le défaut)
SELECT * FROM clients ORDER BY nom;
SELECT * FROM clients ORDER BY nom ASC; -- Identique

-- Du plus récent au plus ancien
SELECT * FROM articles ORDER BY created_at DESC;

-- Tri multi-niveaux : par ville, puis par nom dans chaque ville
SELECT nom, ville FROM clients
ORDER BY ville ASC, nom ASC;

-- Trier par alias calculé
SELECT nom, prix * 1.18 AS prix_ttc
FROM produits
ORDER BY prix_ttc DESC;

-- Trier par position de colonne (déconseillé pour la lisibilité)
SELECT nom, ville, email FROM clients ORDER BY 2; -- Trie par 'ville'

-- Trier avec CASE : ordre personnalisé
SELECT * FROM commandes
ORDER BY
  CASE statut
    WHEN 'urgent'     THEN 1
    WHEN 'en_cours'   THEN 2
    WHEN 'livré'      THEN 3
    ELSE                   4
  END;
💡
NULL et ORDER BY : Les valeurs NULL apparaissent en dernier avec ORDER BY ASC et en premier avec DESC (comportement MySQL). Pour contrôler leur position en PostgreSQL : ORDER BY colonne ASC NULLS LAST. En MySQL, astuce : ORDER BY colonne IS NULL ASC, colonne ASC.
LEÇON 19

DISTINCT — Éliminer les Doublons

DISTINCT retourne uniquement les combinaisons de valeurs uniques dans les colonnes sélectionnées. Il compare la combinaison complète de toutes les colonnes listées dans le SELECT.

// DISTINCT et ses variations-- Lister les villes où vous avez des clients (sans doublons)
SELECT DISTINCT ville FROM clients;

-- Paires uniques ville + pays
SELECT DISTINCT ville, pays FROM clients;
-- DISTINCT s'applique à la combinaison (ville, pays), pas séparément

-- Compter les valeurs uniques
SELECT
  COUNT(*)                AS total_commandes,
  COUNT(DISTINCT client_id) AS clients_uniques,
  COUNT(DISTINCT produit_id) AS produits_commandés
FROM lignes_commande;

-- Equivalent avec GROUP BY (souvent plus performant sur grandes tables)
SELECT ville FROM clients GROUP BY ville;
-- GROUP BY permet aussi de filtrer avec HAVING, ce que DISTINCT ne peut pas
Comparaison — Sans DISTINCT vs Avec DISTINCT
SELECT ville FROM clients (14 lignes)SELECT DISTINCT ville (4 lignes)
DakarDakar
DakarThiès
ThièsSaint-Louis
DakarZiguinchor
Saint-Louis
Dakar
⚠️
Performance : DISTINCT force le moteur à comparer et trier toutes les valeurs pour détecter les doublons. Sur des millions de lignes, cela peut être coûteux. Si vous avez fréquemment besoin de valeurs uniques, envisagez une table de référence ou un index couvrant.
LEÇON 20

LIMIT et OFFSET — Pagination

LIMIT est essentiel en production. Sans lui, une requête sur une table de plusieurs millions de lignes peut saturer la mémoire du serveur, bloquer la connexion, et faire planter votre application. Il est aussi la base de la pagination.

// pagination et cas d'usage-- Les 10 premiers articles (page 1)
SELECT * FROM articles ORDER BY created_at DESC LIMIT 10;

-- Pagination classique
-- Formule : OFFSET = (numéro_page - 1) × nb_par_page
SELECT * FROM articles ORDER BY created_at DESC LIMIT 10 OFFSET  0; -- Page 1
SELECT * FROM articles ORDER BY created_at DESC LIMIT 10 OFFSET 10; -- Page 2
SELECT * FROM articles ORDER BY created_at DESC LIMIT 10 OFFSET 20; -- Page 3

-- Compter le total pour calculer le nombre de pages
SELECT COUNT(*) FROM articles WHERE statut = 'publié';

-- Trouver le N-ième élément (5ème produit le plus cher)
SELECT nom, prix FROM produits
ORDER BY prix DESC LIMIT 1 OFFSET 4; -- OFFSET 4 = saute les 4 premiers

-- ✅ Keyset pagination (bien plus performant avec de grandes tables)
-- Page suivante après l'article d'id 1250 :
SELECT * FROM articles
WHERE id < 1250  -- Au lieu de OFFSET 50000
ORDER BY id DESC LIMIT 10;
ℹ️
Problème de l'OFFSET sur les grandes tables : LIMIT 10 OFFSET 100000 force MySQL à lire et ignorer 100 000 lignes avant de retourner les 10 suivantes. Avec la keyset pagination (WHERE id > dernierIdVu), le moteur saute directement au bon endroit via l'index. Indispensable pour les tables de millions de lignes.
✦ Exercice pratique

Écrivez les requêtes pour une API qui pagine les produits par 20 :

1. Récupérer la page 4 des produits disponibles, triés par prix croissant.

2. Compter le nombre total de produits disponibles pour calculer le nombre de pages.

3. Implémenter la même page 4 avec la keyset pagination (en supposant que le dernier id de la page 3 est 847).

Chapitre 3 — Modification et Agrégation (Leçons 21–30)
LEÇON 21

Mettre à Jour — UPDATE

UPDATE modifie des enregistrements existants. Vous pouvez modifier une ou plusieurs colonnes simultanément, sur une ou plusieurs lignes. C'est une opération permanente — sans transaction, elle est immédiatement persistée sur disque.

// toutes les formes d'UPDATE-- Mise à jour d'un seul enregistrement (toujours par la clé primaire)
UPDATE utilisateurs
SET   email = 'newemail@gmail.com'
WHERE id = 42;

-- Plusieurs colonnes simultanément
UPDATE employes
SET
  salaire     = salaire * 1.10,       -- Augmentation de 10%
  grade       = 'Senior',
  updated_at  = NOW()
WHERE departement = 'Ingénierie'
  AND anciennete_annees >= 3;

-- Mise à jour conditionnelle avec CASE
UPDATE produits
SET statut =
  CASE
    WHEN stock = 0      THEN 'rupture'
    WHEN stock < 5      THEN 'faible_stock'
    WHEN stock < 20     THEN 'stock_limité'
    ELSE                     'disponible'
  END;

-- Soft delete : ne pas vraiment supprimer, juste marquer
UPDATE utilisateurs
SET   deleted_at = NOW()
WHERE id = 15;
⚠️
Règle d'or avant tout UPDATE : Exécutez d'abord le SELECT correspondant pour vérifier exactement quelles lignes seront touchées. SELECT * FROM employes WHERE departement = 'Ingénierie' AND anciennete_annees >= 3. Vérifiez le nombre et la liste. Ensuite seulement, lancez l'UPDATE. Un UPDATE sans WHERE sur une grande table peut être catastrophique.
LEÇON 22

Supprimer — DELETE

DELETE supprime définitivement des enregistrements. Contrairement à TRUNCATE, il : déclenche les triggers, respecte les contraintes de clés étrangères, s'inscrit dans les logs de transactions (annulable avec ROLLBACK), et peut être filtré avec WHERE.

// DELETE sécurisé-- Étape 1 : TOUJOURS vérifier d'abord ce qui sera supprimé
SELECT COUNT(*), MIN(created_at), MAX(created_at)
FROM logs WHERE niveau = 'debug' AND created_at < DATE_SUB(NOW(), INTERVAL 90 DAY);

-- Étape 2 : Si le résultat est cohérent, procéder
DELETE FROM logs
WHERE niveau = 'debug'
  AND created_at < DATE_SUB(NOW(), INTERVAL 90 DAY);

-- Suppression en masse avec LIMIT (plus sûr, évite les locks longs)
-- Supprimer par lots de 1000 dans une boucle applicative :
DELETE FROM logs
WHERE niveau = 'debug'
LIMIT 1000;
-- Répéter jusqu'à ce que 0 lignes soient affectées

-- Supprimer avec JOIN (MySQL)
DELETE c FROM commandes c
JOIN clients cl ON c.client_id = cl.id
WHERE cl.statut = 'test';
💡
Pattern Soft Delete : En production, les suppressions "physiques" sont rares. On préfère ajouter une colonne deleted_at DATETIME DEFAULT NULL. "Supprimer" = UPDATE ... SET deleted_at = NOW(). Toutes les requêtes ajoutent WHERE deleted_at IS NULL. Avantage : récupération en cas d'erreur, auditabilité, conformité légale (RGPD peut nécessiter de garder des traces).
LEÇON 23

Fonctions d'Agrégation — COUNT, SUM, AVG, MIN, MAX

Les fonctions d'agrégation résument un ensemble de lignes en une seule valeur. Elles sont la base de toute analyse de données. Règle importante : elles ignorent les NULL (sauf COUNT(*) qui compte toutes les lignes, y compris celles avec des NULL).

// fonctions d'agrégation complètesSELECT
  COUNT(*)                      AS total_commandes,     -- Toutes les lignes
  COUNT(coupon_code)            AS avec_coupon,          -- Ignore NULL
  COUNT(DISTINCT client_id)    AS clients_uniques,
  SUM(montant)                  AS chiffre_affaires,
  AVG(montant)                  AS panier_moyen,
  ROUND(AVG(montant), 2)       AS panier_moyen_arrondi,
  MIN(montant)                  AS plus_petite_commande,
  MAX(montant)                  AS plus_grande_commande,
  SUM(montant) / COUNT(*)      AS moyenne_manuelle,
  MAX(created_at)              AS derniere_commande,
  STDDEV(montant)              AS ecart_type             -- Dispersion statistique
FROM commandes
WHERE statut = 'livré'
  AND YEAR(created_at) = 2024;
Résultat exemple — Analyse des ventes 2024
totalclients_uniquesca_totalpanier_moyenminmax
1 2473896 543 2005 247.80500245 000
ℹ️
SUM vs COUNT : COUNT(*) compte le nombre de lignes. SUM(colonne) additionne les valeurs. COUNT(DISTINCT client_id) compte les clients ayant passé au moins une commande. AVG ignore les NULL — ce qui peut fausser les moyennes si de nombreuses valeurs sont absentes.
LEÇON 24

GROUP BY — Regrouper les Données

GROUP BY divise les lignes en groupes partageant la même valeur dans une ou plusieurs colonnes, puis applique une fonction d'agrégation à chaque groupe. C'est l'outil fondamental de l'analyse de données en SQL.

// GROUP BY — exemples réels-- Ventes par catégorie de produit
SELECT
  c.nom            AS categorie,
  COUNT(*)         AS nb_produits,
  SUM(ventes)      AS total_ventes,
  AVG(prix)        AS prix_moyen
FROM       produits  p
JOIN       categories c ON p.categorie_id = c.id
GROUP BY   c.id, c.nom
ORDER BY   total_ventes DESC;

-- Chiffre d'affaires mensuel
SELECT
  YEAR(created_at)  AS annee,
  MONTH(created_at) AS mois,
  DATE_FORMAT(created_at, '%Y-%m') AS periode,
  COUNT(*)          AS nb_commandes,
  SUM(montant)      AS ca_mensuel,
  AVG(montant)      AS panier_moyen
FROM  commandes
WHERE statut = 'livré'
GROUP BY annee, mois
ORDER BY annee, mois;
📊
Analogie : GROUP BY est comme trier des pièces de monnaie par valeur. Chaque pile = un groupe. COUNT(*) = compter les pièces de chaque pile. SUM = additionner les valeurs de chaque pile. Les fonctions d'agrégation travaillent sur chaque pile séparément.
ℹ️
Règle fondamentale de GROUP BY : Chaque colonne présente dans SELECT qui n'est pas à l'intérieur d'une fonction d'agrégation DOIT être dans GROUP BY. Si SELECT nom, departement, AVG(salaire), alors GROUP BY nom, departement. MySQL est parfois permissif sur cette règle (mode non-strict), mais PostgreSQL et SQL Server l'appliquent strictement.
LEÇON 25

HAVING — Filtrer les Groupes

HAVING filtre les groupes après l'agrégation, là où WHERE filtre les lignes avant. C'est la seule façon de filtrer sur un résultat d'agrégation comme COUNT(*) ou SUM().

// HAVING vs WHERE — différences clés-- WHERE filtre les LIGNES avant agrégation
-- HAVING filtre les GROUPES après agrégation
SELECT
  client_id,
  COUNT(*)     AS nb_commandes,
  SUM(montant) AS total_depense
FROM   commandes
WHERE  statut = 'livré'         -- ① Filtre d'abord les lignes (statut livré)
GROUP BY client_id
HAVING COUNT(*) > 5              -- ② Puis garde seulement les gros clients (> 5 commandes)
   AND SUM(montant) > 100000    -- ③ Et dont le total dépasse 100 000
ORDER BY total_depense DESC;

-- Trouver les villes avec plus de 10 clients actifs
SELECT
  ville,
  COUNT(*) AS nb_clients,
  AVG(solde) AS solde_moyen
FROM   clients
WHERE  statut = 'actif'
GROUP BY ville
HAVING COUNT(*) >= 10
ORDER BY nb_clients DESC;
Ordre d'exécution SQL — À mémoriser
ÉtapeClauseCe qui se passe
1FROM / JOINCharge les tables et les assemble
2WHEREFiltre les lignes individuelles
3GROUP BYCrée les groupes
4HAVINGFiltre les groupes sur agrégats
5SELECTCalcule les expressions retournées
6ORDER BYTrie le résultat final
7LIMITCoupe le nombre de lignes
LEÇON 26

Alias — AS

Les alias donnent des noms temporaires aux colonnes et aux tables. Essentiels pour la lisibilité des requêtes complexes, indispensables pour nommer des expressions calculées, et obligatoires pour les tables dans les sous-requêtes.

// alias de colonnes et de tables-- Alias de colonnes
SELECT
  nom                          AS "Nom du client",
  email                        AS contact,
  UPPER(nom)                   AS nom_majuscule,
  DATEDIFF(NOW(), created_at)  AS jours_anciennete,
  prix * 1.18                  AS prix_ttc,
  CONCAT(prenom, ' ', nom)    AS nom_complet
FROM utilisateurs;

-- AS est facultatif syntaxiquement, mais recommandé pour la clarté
SELECT nom nom_client FROM clients; -- Fonctionne, mais moins lisible

-- Alias de tables — indispensable pour les jointures
SELECT
  c.nom    AS client,
  cmd.id   AS commande_id,
  cmd.montant,
  p.nom    AS produit
FROM        clients         c
JOIN        commandes       cmd ON c.id = cmd.client_id
JOIN        lignes_commande lc  ON cmd.id = lc.commande_id
JOIN        produits        p   ON lc.produit_id = p.id;

-- Attention : un alias défini dans SELECT n'est PAS disponible dans WHERE
-- ❌ Ceci ne fonctionne pas :
SELECT prix * 1.18 AS prix_ttc FROM produits WHERE prix_ttc > 5000;
-- ✅ Solution : répéter l'expression ou utiliser une sous-requête
SELECT prix * 1.18 AS prix_ttc FROM produits WHERE prix * 1.18 > 5000;
LEÇON 27

Fonctions de Chaîne de Caractères

SQL offre un riche ensemble de fonctions texte pour nettoyer, transformer et extraire des informations des chaînes — directement dans la base, sans passer par le code applicatif.

// fonctions texte essentiellesSELECT
  UPPER(nom)                          -- 'AMINATA DIALLO'
  LOWER(email)                        -- 'aminata@gmail.com'
  LENGTH(nom)                         -- Nombre de caractères
  CHAR_LENGTH(nom)                    -- Idem mais compte les chars unicode
  TRIM(nom)                           -- Supprime espaces début et fin
  LTRIM('  hello')                    -- Supprime espaces à gauche → 'hello'
  RTRIM('hello  ')                    -- Supprime espaces à droite → 'hello'
  CONCAT(prenom, ' ', nom)           -- 'Aminata Diallo'
  CONCAT_WS(', ', ville, pays)       -- 'Dakar, Sénégal' (séparateur sûr avec NULL)
  SUBSTRING(email, 1, 5)             -- 5 premiers caractères
  SUBSTRING_INDEX(email, '@', 1)    -- Partie avant le @ → 'aminata'
  SUBSTRING_INDEX(email, '@', -1)   -- Partie après le @ → 'gmail.com'
  REPLACE(telephone, ' ', '')        -- Supprime espaces du numéro
  REGEXP_REPLACE(texte, '[0-9]', 'X') -- Regex replace (MySQL 8+)
  LPAD(id, 6, '0')                   -- '000042' — padder un id sur 6 chiffres
  FORMAT(prix, 2, 'fr_FR')           -- '1 234,56'
FROM utilisateurs;
✦ Cas concret — Nettoyage de données importées

Après un import CSV désorganisé, nettoyer en une requête :

UPDATE clients SET nom = TRIM(UPPER(nom)), email = TRIM(LOWER(email)), telephone = REPLACE(REPLACE(telephone, ' ', ''), '-', '')

Cette requête normalise les noms en majuscules, les emails en minuscules, et supprime espaces et tirets des numéros de téléphone — pour toute la table en une seule opération.

LEÇON 28

Fonctions de Date et d'Heure

Les dates sont omniprésentes en bases de données. SQL dispose de fonctions puissantes pour les manipuler — extraire des composantes, calculer des durées, formater l'affichage, faire de l'arithmétique temporelle.

// fonctions de date complètesSELECT
  NOW()                                          -- 2025-06-15 14:30:22
  CURDATE()                                      -- 2025-06-15
  CURTIME()                                      -- 14:30:22
  UNIX_TIMESTAMP()                               -- Timestamp Unix

  -- Extraire des composantes
  YEAR(created_at)                               -- 2025
  MONTH(created_at)                              -- 6
  DAY(created_at)                                -- 15
  HOUR(created_at)                               -- 14
  DAYNAME(NOW())                                 -- 'Sunday'
  MONTHNAME(NOW())                               -- 'June'
  WEEK(NOW())                                    -- Numéro de semaine (1-53)
  QUARTER(NOW())                                 -- Trimestre (1-4)

  -- Calculer des durées
  DATEDIFF(NOW(), date_embauche)                 -- Différence en jours
  TIMESTAMPDIFF(YEAR, date_naissance, NOW())   -- Âge exact en années
  TIMESTAMPDIFF(MONTH, date_debut, NOW())      -- Durée en mois

  -- Ajouter / soustraire du temps
  DATE_ADD(NOW(), INTERVAL 30 DAY)           -- Dans 30 jours
  DATE_SUB(NOW(), INTERVAL 1 YEAR)           -- Il y a 1 an
  NOW() + INTERVAL 2 HOUR                    -- Dans 2 heures

  -- Formater
  DATE_FORMAT(created_at, '%d/%m/%Y')          -- '15/06/2025'
  DATE_FORMAT(created_at, '%d %M %Y à %H:%i')  -- '15 June 2025 à 14:30'
FROM employes;
💡
Performances sur les dates : WHERE YEAR(created_at) = 2024 empêche l'utilisation d'un index sur created_at. Préférez toujours : WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'. La requête devient "sargable" et utilise l'index.
LEÇON 29

CASE WHEN — Logique Conditionnelle

CASE WHEN est l'équivalent SQL du if/else if/else. Utilisable dans SELECT, WHERE, ORDER BY, UPDATE — c'est l'une des fonctionnalités les plus polyvalentes. Il existe en deux formes.

// deux formes de CASE WHEN-- Forme simple (teste l'égalité d'une expression)
CASE statut
  WHEN 'livré'    THEN '✓ Livré'
  WHEN 'en_cours'  THEN '⟳ En cours'
  WHEN 'annulé'   THEN '✗ Annulé'
  ELSE               '? Inconnu'
END AS statut_affichage

-- Forme recherchée (conditions booléennes, plus flexible)
CASE
  WHEN salaire >= 800000 THEN 'Expert'
  WHEN salaire >= 500000 THEN 'Senior'
  WHEN salaire >= 300000 THEN 'Confirmé'
  WHEN salaire >= 150000 THEN 'Junior'
  ELSE                        'Stagiaire'
END AS niveau

-- Technique avancée : Pivot avec CASE (transformer lignes en colonnes)
SELECT
  DATE_FORMAT(created_at, '%Y-%m') AS mois,
  SUM(CASE WHEN statut = 'livré'    THEN montant ELSE 0 END) AS ca_livre,
  SUM(CASE WHEN statut = 'annulé'   THEN montant ELSE 0 END) AS ca_annule,
  COUNT(CASE WHEN statut = 'livré'   THEN 1 END)            AS nb_livrees,
  COUNT(CASE WHEN statut = 'annulé'  THEN 1 END)            AS nb_annulees
FROM commandes
GROUP BY mois
ORDER BY mois;
LEÇON 30

COALESCE, IFNULL, NULLIF — Gérer les NULL

Trois fonctions essentielles pour remplacer, intercepter ou générer des NULL. COALESCE est la plus puissante et la plus portable (standard SQL).

// gestion des NULL-- COALESCE : retourne le premier argument non-NULL
SELECT
  nom,
  COALESCE(mobile, fixe, email, 'Aucun contact disponible') AS contact_principal
FROM clients;

-- IFNULL : version simplifiée de COALESCE à 2 arguments
SELECT
  nom,
  IFNULL(bonus, 0) + salaire        AS remuneration_totale,
  IFNULL(DATE_FORMAT(derniere_cnx, '%d/%m/%Y'), 'Jamais connecté') AS cnx
FROM employes;

-- NULLIF : retourne NULL si les deux valeurs sont égales
-- Utile pour éviter la division par zéro
SELECT
  commandes / NULLIF(visites, 0)          AS taux_conversion,
  -- Si visites = 0, retourne NULL au lieu d'une erreur division par zéro
  NULLIF(TRIM(nom), '')                   AS nom_propre
  -- Si nom est vide après TRIM, retourne NULL plutôt qu'une chaîne vide
FROM stats;

-- Pattern courant : valeur ou fallback
UPDATE factures
SET adresse_livraison = COALESCE(adresse_livraison, adresse_facturation)
WHERE adresse_livraison IS NULL;
💡
Portabilité : COALESCE est SQL standard et fonctionne sur tous les moteurs (MySQL, PostgreSQL, SQLite, SQL Server, Oracle). IFNULL est propre à MySQL/MariaDB. NVL est l'équivalent Oracle. Pour du code portable : utilisez toujours COALESCE.
Chapitre 4 — Jointures et Sous-requêtes (Leçons 31–40)
LEÇON 31

Introduction aux Jointures — Pourquoi et Comment

La normalisation répartit les données dans plusieurs tables spécialisées. Les jointures (JOIN) permettent de les recombiner à la volée lors des requêtes, en reliant les tables via leurs colonnes communes (les clés). C'est le mécanisme central du modèle relationnel.

Exemple — E-commerce normalisé clients (id, nom, email) ← table parent
commandes (id, client_id, date, montant) ← client_id pointe vers clients.id
lignes_commande (id, commande_id, produit_id, qte, prix)
produits (id, nom, categorie_id) ← categorie_id pointe vers categories.id
categories (id, nom)
INNER JOIN

Lignes avec correspondance dans les DEUX tables. La plus courante. Si un client n'a pas de commande, il n'apparaît pas.

LEFT JOIN

TOUTES les lignes de gauche + correspondances de droite. NULL si pas de match. Très utilisé pour "trouver ce qui n'a pas de correspondance".

RIGHT JOIN

Inverse du LEFT JOIN. Rarement nécessaire — un RIGHT JOIN peut toujours être réécrit en LEFT JOIN en inversant l'ordre des tables.

CROSS JOIN

Produit cartésien — chaque ligne de gauche avec chaque ligne de droite. Rarement utile, mais parfois nécessaire pour générer des combinaisons.

💡
Clé pour comprendre JOIN : La clause ON définit la condition de correspondance. Elle peut être n'importe quelle condition booléenne — pas seulement une égalité de clés. JOIN promotions p ON p.date_debut <= o.date AND p.date_fin >= o.date est un JOIN valide.
LEÇON 32

INNER JOIN — L'Intersection

INNER JOIN retourne uniquement les lignes ayant une correspondance dans les deux tables. C'est le JOIN par défaut (le mot INNER est optionnel). Il produit l'intersection des deux ensembles.

// INNER JOIN — de simple à complexe-- Jointure simple : clients avec leurs commandes
SELECT
  c.nom           AS client,
  c.ville,
  cmd.id          AS commande,
  cmd.montant,
  cmd.created_at
FROM       clients   c
INNER JOIN commandes cmd ON c.id = cmd.client_id
ORDER BY  cmd.created_at DESC;

-- Jointure sur 4 tables : du client jusqu'au produit
SELECT
  c.nom    AS client,
  cmd.id   AS commande,
  p.nom    AS produit,
  cat.nom  AS categorie,
  lc.qte,
  lc.prix_unitaire,
  lc.qte * lc.prix_unitaire AS sous_total
FROM       clients         c
JOIN       commandes       cmd ON c.id     = cmd.client_id
JOIN       lignes_commande lc  ON cmd.id   = lc.commande_id
JOIN       produits        p   ON lc.produit_id = p.id
JOIN       categories      cat ON p.categorie_id = cat.id
WHERE      cmd.statut = 'livré'
ORDER BY  cmd.id, p.nom;
ℹ️
INNER vs JOIN : JOIN seul est un INNER JOIN implicite — les deux syntaxes sont équivalentes. La clarté est préférable : être explicite avec INNER JOIN, LEFT JOIN, etc. aide à comprendre l'intention d'une requête complexe au premier regard.
LEÇON 33

LEFT JOIN — Conserver Toutes les Lignes

LEFT JOIN retourne toutes les lignes de la table gauche, qu'elles aient une correspondance ou non dans la table droite. Les colonnes de droite affichent NULL quand il n'y a pas de correspondance. C'est le JOIN le plus utilisé après INNER JOIN.

// LEFT JOIN — cas d'usage essentiels-- Tous les clients avec le total de leurs commandes (y compris ceux sans commande)
SELECT
  c.nom,
  c.email,
  COUNT(cmd.id)          AS nb_commandes,       -- 0 si aucune commande
  COALESCE(SUM(cmd.montant), 0) AS total_depense   -- 0 si aucune commande
FROM       clients   c
LEFT JOIN  commandes cmd ON c.id = cmd.client_id
GROUP BY  c.id, c.nom, c.email
ORDER BY  total_depense DESC;

-- Pattern anti-jointure : trouver les lignes SANS correspondance
-- (clients n'ayant JAMAIS passé commande)
SELECT c.nom, c.email, c.created_at
FROM      clients   c
LEFT JOIN commandes cmd ON c.id = cmd.client_id
WHERE     cmd.id IS NULL;  -- NULL = pas de correspondance trouvée

-- Articles sans commentaires (autre usage de l'anti-jointure)
SELECT a.titre, a.created_at
FROM      articles    a
LEFT JOIN commentaires co ON a.id = co.article_id
WHERE     co.id IS NULL
ORDER BY  a.created_at DESC;
🎯
Diagramme de Venn : INNER JOIN = l'intersection seule. LEFT JOIN = tout le cercle gauche (avec intersection). RIGHT JOIN = tout le cercle droit. FULL JOIN = les deux cercles entiers. Anti-jointure (LEFT JOIN ... WHERE droite IS NULL) = partie gauche SANS l'intersection.
LEÇON 34

RIGHT JOIN, FULL JOIN et CROSS JOIN

Les autres types de jointures, moins courants mais utiles dans des cas spécifiques.

// autres types de jointures-- RIGHT JOIN : toutes les lignes de la table droite
-- (Peu utilisé — un RIGHT JOIN peut toujours être réécrit en LEFT JOIN)
SELECT c.nom, cmd.id, cmd.montant
FROM       commandes cmd
RIGHT JOIN clients   c ON cmd.client_id = c.id;
-- Équivalent en LEFT JOIN :
SELECT c.nom, cmd.id, cmd.montant
FROM      clients   c
LEFT JOIN commandes cmd ON c.id = cmd.client_id;

-- FULL OUTER JOIN : toutes les lignes des deux tables
-- (MySQL ne le supporte pas nativement — simulation avec UNION)
SELECT c.nom, cmd.montant FROM clients c LEFT JOIN  commandes cmd ON c.id = cmd.client_id
UNION
SELECT c.nom, cmd.montant FROM clients c RIGHT JOIN commandes cmd ON c.id = cmd.client_id;

-- CROSS JOIN : produit cartésien (chaque ligne × chaque ligne)
-- Exemple : générer toutes les combinaisons taille × couleur pour un produit
SELECT
  t.valeur AS taille,
  c.valeur AS couleur
FROM       tailles  t
CROSS JOIN couleurs c;
-- 4 tailles × 6 couleurs = 24 combinaisons
ℹ️
Attention au CROSS JOIN accidentel : Oublier la clause ON d'un JOIN produit implicitement un CROSS JOIN. Sur deux tables de 1000 lignes chacune, cela donne 1 000 000 lignes — ce qui peut saturer la mémoire. MySQL affiche parfois un warning mais exécute quand même la requête.
LEÇON 35

UNION et UNION ALL — Combiner des Résultats

UNION combine verticalement les résultats de deux requêtes SELECT compatibles. UNION ALL fait la même chose mais conserve les doublons — plus rapide car il n'a pas à les éliminer.

// UNION et UNION ALL-- UNION : liste unique de tous les emails (clients + fournisseurs)
SELECT email, nom, 'client'     AS type_contact FROM clients
UNION
SELECT email, nom, 'fournisseur' AS type_contact FROM fournisseurs
ORDER BY nom;  -- ORDER BY s'applique au résultat final

-- UNION ALL : cumul des ventes 2023 + 2024 (avec doublons potentiels)
SELECT * FROM ventes_2023
UNION ALL
SELECT * FROM ventes_2024;

-- UNION avec conditions différentes par partie
-- Rapport : clients vip ET clients récents
SELECT id, nom, 'VIP' AS raison
FROM   clients WHERE statut = 'vip'
UNION
SELECT id, nom, 'Nouveau' AS raison
FROM   clients WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);
-- Un client VIP récent apparaît deux fois — utilisez UNION ALL si c'est voulu
ℹ️
Règles de UNION : 1) Même nombre de colonnes dans chaque SELECT. 2) Types compatibles (INT et VARCHAR peuvent coexister, MySQL convertit). 3) Les noms de colonnes du résultat viennent du premier SELECT. 4) ORDER BY et LIMIT s'appliquent au résultat global, pas à chaque partie.
LEÇON 36

Les Sous-Requêtes

Une sous-requête est une requête imbriquée dans une autre. Elle peut être scalaire (retourne 1 valeur), en ligne (retourne 1 ligne), ou en table (retourne plusieurs lignes). Puissante, mais parfois remplaçable par un JOIN plus performant.

// types de sous-requêtes-- Sous-requête scalaire dans WHERE : produits au-dessus du prix moyen
SELECT nom, prix
FROM   produits
WHERE  prix > (SELECT AVG(prix) FROM produits); -- Retourne 1 valeur : la moyenne

-- Sous-requête dans FROM (table dérivée) : doit avoir un alias
SELECT departement, moy_salaire
FROM (
  SELECT departement, ROUND(AVG(salaire), 0) AS moy_salaire
  FROM   employes
  GROUP BY departement
) AS stats_dept
WHERE  moy_salaire > 400000
ORDER BY moy_salaire DESC;

-- Sous-requête corrélée : référence la requête externe (réévaluée pour chaque ligne)
SELECT nom, salaire, departement
FROM   employes e1
WHERE  salaire > (
  SELECT AVG(salaire)
  FROM   employes e2
  WHERE  e2.departement = e1.departement  -- Corrélée : utilise e1
);
-- Retourne : chaque employé qui gagne plus que la moyenne de son département
💡
Sous-requête vs JOIN : Les sous-requêtes corrélées sont souvent plus lentes que leurs équivalents en JOIN ou CTE, car elles sont réévaluées pour chaque ligne. Utilisez EXPLAIN pour comparer les plans d'exécution. Les optimiseurs modernes (MySQL 8+, PostgreSQL) convertissent souvent automatiquement les sous-requêtes en JOIN.
LEÇON 37

EXISTS et NOT EXISTS

EXISTS teste si une sous-requête retourne au moins une ligne. Il est souvent plus performant qu'IN car il s'arrête dès qu'il trouve une correspondance. NOT EXISTS est la façon la plus robuste de trouver les lignes sans correspondance (alternative à l'anti-jointure).

// EXISTS vs IN — performance et usage-- EXISTS : clients ayant au moins une commande > 50 000
SELECT nom, email
FROM   clients c
WHERE EXISTS (
  SELECT 1  -- Peu importe ce qu'on sélectionne (1 ou * ou n'importe quoi)
  FROM   commandes cmd
  WHERE  cmd.client_id = c.id    -- Corrélée : lie avec la requête externe
    AND  cmd.montant > 50000
);

-- NOT EXISTS : clients n'ayant JAMAIS commandé
-- (plus robuste que LEFT JOIN IS NULL ou NOT IN)
SELECT nom, email, created_at
FROM   clients c
WHERE NOT EXISTS (
  SELECT 1 FROM commandes cmd WHERE cmd.client_id = c.id
);

-- Comparaison : 3 façons équivalentes de "clients sans commande"
-- 1. Anti-jointure (généralement la plus rapide)
WHERE c.id NOT IN (SELECT client_id FROM commandes WHERE client_id IS NOT NULL)
-- 2. LEFT JOIN IS NULL
LEFT JOIN commandes cmd ON c.id = cmd.client_id WHERE cmd.id IS NULL
-- 3. NOT EXISTS (le plus sûr avec les NULL)
WHERE NOT EXISTS (SELECT 1 FROM commandes WHERE client_id = c.id)
LEÇON 38

ANY et ALL

ANY est vrai si la condition est vraie pour au moins une valeur de la sous-requête. ALL exige que la condition soit vraie pour toutes les valeurs. En pratique, souvent remplacés par MIN/MAX pour la lisibilité.

// ANY et ALL-- ANY : au moins un employé de la liste gagne moins
-- Trouver les managers qui gagnent plus qu'au moins un junior
SELECT nom, salaire FROM employes
WHERE  type = 'manager'
  AND  salaire > ANY (
    SELECT salaire FROM employes WHERE type = 'junior'
  );
-- Équivalent (plus lisible) :
  AND  salaire > (SELECT MIN(salaire) FROM employes WHERE type = 'junior')

-- ALL : plus cher que TOUS les produits en promotion
SELECT nom, prix FROM produits_premium
WHERE  prix > ALL (
    SELECT prix_promo FROM promotions_actives
);
-- Équivalent :
WHERE  prix > (SELECT MAX(prix_promo) FROM promotions_actives)
ℹ️
ANY vs IN : = ANY (sous-requête) est strictement équivalent à IN (sous-requête). <> ALL (sous-requête) est équivalent à NOT IN (sous-requête) (avec le même piège des NULL). En pratique, préférez IN/NOT IN ou les sous-requêtes avec MIN/MAX pour la lisibilité.
LEÇON 39

Self JOIN — Jointure d'une Table sur Elle-Même

Un self JOIN relie une table à elle-même. Indispensable pour les structures hiérarchiques (organigrammes, catégories imbriquées, commentaires avec réponses) où les relations existent au sein de la même table.

// self JOIN — hiérarchies et relations réflexives-- Table : employes(id, nom, poste, manager_id)
-- manager_id référence id de la même table

-- Chaque employé avec le nom de son manager
SELECT
  emp.nom        AS employe,
  emp.poste,
  mgr.nom        AS manager,
  mgr.poste      AS poste_manager
FROM      employes emp
LEFT JOIN employes mgr ON emp.manager_id = mgr.id;
-- LEFT JOIN car le PDG n'a pas de manager (manager_id IS NULL)

-- Trouver les collègues du même département
SELECT
  e1.nom AS employe,
  e2.nom AS collegue
FROM  employes e1
JOIN  employes e2 ON e1.departement = e2.departement
                AND e1.id != e2.id  -- Exclure les paires avec soi-même
ORDER BY e1.nom, e2.nom;

-- Catégories avec leurs sous-catégories (1 niveau)
SELECT
  parent.nom AS categorie,
  enfant.nom AS sous_categorie
FROM      categories parent
LEFT JOIN categories enfant ON enfant.parent_id = parent.id
WHERE     parent.parent_id IS NULL  -- Seulement les catégories racine
ORDER BY  parent.nom, enfant.nom;
LEÇON 40

Requêtes Multi-Jointures Avancées

En production, les requêtes complexes combinent souvent 5 à 10 tables. La clé : structurer lisiblement avec des alias cohérents, ajouter les jointures progressivement, et vérifier le résultat à chaque étape.

// requête analytique complète — rapport de ventesSELECT
  c.nom                               AS client,
  c.ville,
  cat.nom                             AS categorie,
  p.nom                               AS produit,
  SUM(lc.qte)                        AS quantite_totale,
  SUM(lc.qte * lc.prix_unitaire)     AS ca_total,
  COUNT(DISTINCT cmd.id)             AS nb_commandes,
  MIN(cmd.created_at)                 AS premiere_commande,
  MAX(cmd.created_at)                 AS derniere_commande,
  AVG(lc.qte * lc.prix_unitaire)     AS panier_moyen_par_article

FROM       clients         c
JOIN       commandes       cmd ON c.id             = cmd.client_id
JOIN       lignes_commande lc  ON cmd.id            = lc.commande_id
JOIN       produits        p   ON lc.produit_id     = p.id
JOIN       categories      cat ON p.categorie_id    = cat.id
LEFT JOIN  remises         r   ON cmd.remise_id     = r.id  -- Optionnel

WHERE      cmd.statut = 'livré'
  AND      cmd.created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
  AND      cat.active = 1

GROUP BY   c.id, c.nom, c.ville, cat.id, cat.nom, p.id, p.nom
HAVING     SUM(lc.qte * lc.prix_unitaire) > 50000

ORDER BY   ca_total DESC
LIMIT      100;
✦ Méthode de débogage des JOINs

Commencez par la table principale seule. Ajoutez un JOIN à la fois. Vérifiez le nombre de lignes après chaque ajout. Si le nombre explose, votre condition ON est incorrecte (produit cartésien). Si des lignes disparaissent, passez à LEFT JOIN pour diagnostiquer.

Chapitre 5 — Avancé, Sécurité et Optimisation (Leçons 41–50)
LEÇON 41

Les Index — Accélérer les Requêtes

Un index est une structure de données auxiliaire (généralement un B-Tree) qui maintient une copie triée des valeurs d'une ou plusieurs colonnes. Il permet au moteur de localiser rapidement les lignes sans lire toute la table — comme un index de livre évite de le lire entièrement.

// gestion complète des index-- Index simple : accélère les WHERE sur email
CREATE INDEX        idx_email          ON utilisateurs(email);

-- Index unique : garantit l'unicité + accélère les recherches
CREATE UNIQUE INDEX idx_email_unique   ON utilisateurs(email);

-- Index composite : pour WHERE qui filtre sur plusieurs colonnes
-- L'ordre des colonnes dans l'index est crucial !
CREATE INDEX idx_ville_statut ON clients(ville, statut);
-- Efficace pour : WHERE ville = ? AND statut = ?
-- Efficace pour : WHERE ville = ? (préfixe gauche)
-- Inefficace pour : WHERE statut = ? (pas de préfixe gauche)

-- Index sur expression (MySQL 8+ / PostgreSQL)
CREATE INDEX idx_email_lower ON utilisateurs((LOWER(email)));

-- Index couvrant (covering index) : inclut toutes les colonnes du SELECT
-- Le moteur ne lit jamais la table — tout vient de l'index
CREATE INDEX idx_couvrant ON commandes(client_id, created_at, montant, statut);

-- Index fulltext : pour les recherches dans du texte
CREATE FULLTEXT INDEX idx_ft_contenu ON articles(titre, contenu);
SELECT * FROM articles WHERE MATCH(titre, contenu) AGAINST('SQL performance');

-- Voir et supprimer les index
SHOW INDEX FROM utilisateurs;
DROP INDEX idx_email ON utilisateurs;
Impact des index sur les performances (table 1M lignes)
RequêteSans indexAvec indexGain
WHERE email = ?~200ms~0.3ms×600
WHERE ville + statut~450ms~1ms×450
ORDER BY created_at~800ms~5ms×160
⚠️
Coût des index : Chaque index ralentit INSERT, UPDATE, DELETE car il doit être maintenu à jour. Ne créez pas d'index sur chaque colonne. Ciblez précisément les colonnes utilisées dans WHERE, JOIN ON, ORDER BY. Surveillez les index non utilisés avec SELECT * FROM sys.schema_unused_indexes;
LEÇON 42

Les Transactions — ACID

Une transaction est un groupe d'opérations traité comme une unité indivisible. Soit tout réussit et est persisté (COMMIT), soit tout est annulé comme si rien ne s'était passé (ROLLBACK). Les transactions respectent les propriétés ACID.

A — Atomicité

Tout ou rien. Si une opération de la transaction échoue, toutes les autres sont annulées.

C — Cohérence

Avant et après la transaction, la base est dans un état valide. Les contraintes d'intégrité sont respectées.

I — Isolation

Les transactions concurrentes ne se voient pas mutuellement pendant leur exécution.

D — Durabilité

Un COMMIT persiste définitivement, même en cas de panne système ou de redémarrage.

// transaction complète avec gestion d'erreurSTART TRANSACTION;

-- Virement bancaire : 50 000 FCFA du compte 1 vers le compte 2
UPDATE comptes SET solde = solde - 50000 WHERE id = 1;

-- Vérifier que le solde n'est pas négatif
-- Si solde < 0 dans votre application → ROLLBACK
SELECT solde FROM comptes WHERE id = 1 FOR UPDATE; -- Verrou pendant la transaction

UPDATE comptes SET solde = solde + 50000 WHERE id = 2;

-- Enregistrer l'opération
INSERT INTO historique_virements (de_compte, vers_compte, montant, date)
VALUES (1, 2, 50000, NOW());

-- Tout s'est bien passé → valider définitivement
COMMIT;

-- En cas d'erreur → annuler tout
ROLLBACK;

-- Sauvegarde intermédiaire
SAVEPOINT apres_debit;
ROLLBACK TO SAVEPOINT apres_debit; -- Retour partiel possible
ℹ️
Autocommit : Par défaut, MySQL est en mode autocommit — chaque requête est une transaction automatique. START TRANSACTION désactive l'autocommit jusqu'au prochain COMMIT ou ROLLBACK. Pour désactiver l'autocommit durablement : SET autocommit = 0;
LEÇON 43

Les Vues — VIEW

Une vue est une requête nommée et sauvegardée qui se comporte comme une table virtuelle. Elle ne stocke pas les données — à chaque appel, elle exécute la requête sous-jacente. Puissant outil d'abstraction, de sécurité et de simplification.

// vues — création et utilisation avancée-- Créer une vue : résumé complet des clients
CREATE OR REPLACE VIEW v_tableau_bord_clients AS
SELECT
  c.id,
  c.nom,
  c.email,
  c.ville,
  c.created_at                   AS date_inscription,
  COUNT(cmd.id)                  AS nb_commandes,
  COALESCE(SUM(cmd.montant), 0)  AS total_depense,
  MAX(cmd.created_at)            AS derniere_commande,
  CASE
    WHEN SUM(cmd.montant) >= 1000000 THEN 'Platine'
    WHEN SUM(cmd.montant) >= 500000  THEN 'Or'
    WHEN SUM(cmd.montant) >= 100000  THEN 'Argent'
    ELSE                                  'Standard'
  END AS segment
FROM       clients   c
LEFT JOIN  commandes cmd ON c.id = cmd.client_id AND cmd.statut = 'livré'
WHERE      c.deleted_at IS NULL
GROUP BY   c.id, c.nom, c.email, c.ville, c.created_at;

-- Utiliser comme une table normale
SELECT * FROM v_tableau_bord_clients WHERE segment = 'Platine' ORDER BY total_depense DESC;
SELECT segment, COUNT(*) FROM v_tableau_bord_clients GROUP BY segment;

-- Vue pour restreindre l'accès aux données sensibles
CREATE VIEW v_employes_public AS
SELECT id, nom, prenom, departement, poste FROM employes;
-- Les colonnes salaire et données personnelles sont masquées
🪟
Analogie : Une vue est une fenêtre sur les données. Elle vous montre une perspective particulière sans déplacer les meubles. Trois usages principaux : simplifier des requêtes complexes (façade d'abstraction), masquer des colonnes sensibles (sécurité), présenter des données transformées (reporting).
LEÇON 44

ALTER TABLE — Modifier la Structure

ALTER TABLE modifie la structure d'une table existante sans perdre les données. Attention : sur de grandes tables en production, certaines opérations ALTER TABLE peuvent verrouiller la table pendant plusieurs minutes ou heures.

// toutes les opérations ALTER TABLE-- Ajouter une colonne
ALTER TABLE utilisateurs
  ADD COLUMN telephone VARCHAR(20) DEFAULT NULL,
  ADD COLUMN avatar_url VARCHAR(500) AFTER email;  -- Position précise

-- Modifier le type d'une colonne
ALTER TABLE produits
  MODIFY COLUMN description MEDIUMTEXT;

-- Renommer une colonne (MySQL 8+)
ALTER TABLE clients
  RENAME COLUMN tel TO telephone;

-- Supprimer une colonne
ALTER TABLE utilisateurs
  DROP COLUMN colonne_obsolete;

-- Ajouter/supprimer un index
ALTER TABLE commandes
  ADD INDEX idx_client_date (client_id, created_at),
  DROP INDEX idx_ancien;

-- Ajouter une clé étrangère
ALTER TABLE commandes
  ADD CONSTRAINT fk_cmd_client
    FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE RESTRICT;

-- Changer le moteur de stockage
ALTER TABLE ma_table ENGINE = InnoDB;

-- Renommer la table
RENAME TABLE anciens_clients TO clients_archive;
⚠️
En production : Sur des tables de plusieurs millions de lignes, utilisez des outils comme pt-online-schema-change (Percona) ou gh-ost (GitHub). Ils créent une copie de la table, appliquent les changements, puis effectuent le swap atomique — sans verrouillage prolongé.
LEÇON 45

Fonctions de Fenêtre — OVER et PARTITION BY

Les window functions (fonctions fenêtre) calculent des valeurs sur un ensemble de lignes lié à la ligne courante, sans agréger — chaque ligne garde son identité. Disponibles depuis SQL:2003, elles sont parmi les outils analytiques les plus puissants.

// window functions complètesSELECT
  nom,
  departement,
  salaire,

  -- Rang et numérotation
  ROW_NUMBER() OVER (ORDER BY salaire DESC)                    AS rang_absolu,
  RANK()       OVER (ORDER BY salaire DESC)                    AS rang_ex_aequo,     -- Trous
  DENSE_RANK() OVER (ORDER BY salaire DESC)                    AS rang_dense,         -- Sans trous

  -- Rang au sein de chaque département
  RANK()       OVER (PARTITION BY departement ORDER BY salaire DESC) AS rang_dept,

  -- Agrégats sans GROUP BY
  AVG(salaire) OVER ()                                         AS moy_globale,
  AVG(salaire) OVER (PARTITION BY departement)                 AS moy_departement,
  salaire - AVG(salaire) OVER (PARTITION BY departement)       AS ecart_moy_dept,

  -- Cumul
  SUM(salaire) OVER (ORDER BY date_embauche)                   AS cumul_masse_salariale,

  -- Décalage temporel
  LAG(salaire,  1, 0) OVER (ORDER BY date_embauche)            AS salaire_precedent,
  LEAD(salaire, 1, 0) OVER (ORDER BY date_embauche)            AS salaire_suivant,

  -- Percentile
  PERCENT_RANK() OVER (ORDER BY salaire)                       AS percentile,
  NTILE(4)       OVER (ORDER BY salaire)                       AS quartile
FROM employes;
💡
Cas d'usage concret : "Trouver le meilleur vendeur de chaque région" = WHERE rang_region = 1 après avoir calculé RANK() OVER (PARTITION BY region ORDER BY ventes DESC) AS rang_region dans une CTE ou sous-requête. Impossible à exprimer simplement avec GROUP BY seul.
LEÇON 46

CTE — Common Table Expressions

Les CTE (WITH ... AS) créent des tables temporaires nommées pour la durée d'une requête. Elles rendent les requêtes complexes lisibles en les décomposant en étapes nommées. Les CTE récursives permettent de traverser des hiérarchies.

// CTE simples et récursives-- CTE multiple : décomposer une requête complexe en étapes
WITH
clients_actifs AS (
  SELECT id, nom, email, ville
  FROM   clients
  WHERE  statut = 'actif' AND deleted_at IS NULL
),
stats_commandes AS (
  SELECT
    client_id,
    COUNT(*)              AS nb,
    SUM(montant)          AS total,
    MAX(created_at)       AS derniere
  FROM   commandes
  WHERE  statut = 'livré'
  GROUP BY client_id
),
segments AS (
  SELECT
    ca.*,
    COALESCE(sc.nb, 0)      AS nb_commandes,
    COALESCE(sc.total, 0)   AS total_depense,
    sc.derniere
  FROM      clients_actifs  ca
  LEFT JOIN stats_commandes sc ON ca.id = sc.client_id
)
SELECT ville, COUNT(*) AS nb_clients, AVG(total_depense) AS depense_moyenne
FROM   segments
GROUP BY ville ORDER BY depense_moyenne DESC;

-- CTE récursive : traverser l'organigramme complet
WITH RECURSIVE organigramme AS (
  -- Cas de base : le PDG (sans manager)
  SELECT id, nom, manager_id, 0 AS niveau, CAST(nom AS CHAR(500)) AS chemin
  FROM   employes WHERE manager_id IS NULL

  UNION ALL

  -- Cas récursif : sous-ordres directs
  SELECT e.id, e.nom, e.manager_id, o.niveau + 1,
         CONCAT(o.chemin, ' > ', e.nom)
  FROM   employes e
  JOIN   organigramme o ON e.manager_id = o.id
)
SELECT
  REPEAT('  ', niveau) AS indentation,
  nom,
  niveau,
  chemin
FROM organigramme ORDER BY chemin;
LEÇON 47

EXPLAIN — Analyser les Requêtes

EXPLAIN révèle le plan d'exécution choisi par le moteur : quelles tables sont lues, dans quel ordre, si des index sont utilisés, combien de lignes sont estimées. C'est l'outil de diagnostic indispensable pour toute requête lente.

// diagnostic de performance-- Analyser une requête
EXPLAIN SELECT * FROM commandes WHERE client_id = 42;

-- Format JSON : beaucoup plus de détails (MySQL 8+)
EXPLAIN FORMAT=JSON SELECT *
FROM  commandes c
JOIN  clients cl ON c.client_id = cl.id
WHERE c.statut = 'livré'
  AND  cl.ville = 'Dakar';

-- ANALYZE : exécute réellement la requête et mesure les vraies métriques
EXPLAIN ANALYZE SELECT ... -- PostgreSQL / MySQL 8.0.18+
Guide de lecture EXPLAIN — Colonnes importantes
ColonneValeurInterprétation
typeALL⚠️ Full scan — lit chaque ligne. Catastrophique sur grandes tables.
typerange✓ Lit une plage de l'index (BETWEEN, <, >). Acceptable.
typeref✓✓ Utilise un index non-unique. Bon.
typeconst✓✓✓ Clé primaire ou unique = 1 ligne. Parfait.
keyNULL⚠️ Aucun index utilisé. Créez un index sur les colonnes WHERE/JOIN.
rows1 000 000⚠️ Estime devoir lire 1M lignes. Optimisez avec un index.
ExtraUsing index✓✓ Index couvrant — la table n'est pas lue du tout.
ExtraUsing filesort⚠️ Tri en mémoire. Indexez la colonne ORDER BY.
ExtraUsing temporary⚠️ Table temporaire créée. Souvent dû à GROUP BY non indexé.
✦ Workflow d'optimisation

1. Identifiez les requêtes lentes avec le slow query log : SET GLOBAL slow_query_log = ON;

2. Analysez avec EXPLAIN. Cherchez type = ALL et rows élevé.

3. Créez l'index manquant sur les colonnes WHERE et JOIN ON.

4. Re-vérifiez avec EXPLAIN — le type devrait passer à ref ou const.

LEÇON 48

Procédures Stockées et Triggers

Les procédures stockées encapsulent la logique métier dans la base. Les triggers s'exécutent automatiquement en réponse aux opérations DML. Deux outils puissants pour garantir la cohérence indépendamment de l'application.

// procédure stockée avancéeDELIMITER //

CREATE PROCEDURE TraiterCommande(
  IN  p_client_id  INT,
  IN  p_produits   JSON,          -- [{"id":1,"qte":2},{"id":3,"qte":1}]
  OUT p_commande_id INT,
  OUT p_total       DECIMAL(10,2)
)
BEGIN
  DECLARE EXIT HANDLER FOR SQLEXCEPTION
  BEGIN
    ROLLBACK;
    RESIGNAL;  -- Repropage l'erreur
  END;

  START TRANSACTION;

  -- Créer la commande
  INSERT INTO commandes (client_id) VALUES (p_client_id);
  SET p_commande_id = LAST_INSERT_ID();
  SET p_total = 0;

  -- Insérer chaque ligne (simplifié)
  INSERT INTO lignes_commande (commande_id, produit_id, qte, prix_unitaire)
  SELECT p_commande_id, p.id, j.qte, p.prix
  FROM JSON_TABLE(p_produits, '$[*]' COLUMNS(id INT PATH '$.id', qte INT PATH '$.qte')) j
  JOIN produits p ON p.id = j.id;

  SELECT SUM(qte * prix_unitaire) INTO p_total
  FROM   lignes_commande WHERE commande_id = p_commande_id;

  UPDATE commandes SET montant_total = p_total WHERE id = p_commande_id;

  COMMIT;
END //

DELIMITER ;

-- Trigger : archiver avant suppression
DELIMITER //
CREATE TRIGGER trig_archive_client
BEFORE DELETE ON clients FOR EACH ROW
BEGIN
  INSERT INTO clients_archive SELECT *, NOW() FROM clients WHERE id = OLD.id;
END //
DELIMITER ;
LEÇON 49

Injections SQL — Comprendre et Se Protéger

L'injection SQL est classée dans le Top 3 OWASP des vulnérabilités web depuis plus d'une décennie. Elle permet à un attaquant de lire, modifier, supprimer vos données, ou même exécuter des commandes système — simplement en manipulant un champ de formulaire.

// démonstration d'injection SQL-- ❌ DANGEREUX : concaténation directe d'input utilisateur
-- Champ login : l'utilisateur entre → ' OR '1'='1
requête = "SELECT * FROM users WHERE login = '" + input_login + "'"
-- Résultat : SELECT * FROM users WHERE login = '' OR '1'='1'
-- Retourne TOUS les utilisateurs → authentification contournée

-- Attaque destructrice :
-- Input : '; DROP TABLE users; --
-- Résultat : SELECT ... WHERE login = ''; DROP TABLE users; --'
-- Efface toute la table users !

-- Exfiltration de données :
-- Input : ' UNION SELECT login, password, NULL FROM admins --
// protection : requêtes préparées dans différents langages-- PHP avec PDO (recommandé)
$stmt = $pdo->prepare("SELECT * FROM users WHERE login = ? AND password = ?");
$stmt->execute([$_POST['login'], hash('sha256', $_POST['password'])]);

-- Node.js avec mysql2
const [rows] = await pool.execute(
  "SELECT * FROM users WHERE login = ? AND password = ?",
  [req.body.login, sha256(req.body.password)]
);

-- Python avec mysql-connector
cursor.execute(
  "SELECT * FROM users WHERE login = %s AND password = %s",
  (login, sha256(password))
)

-- Java avec PreparedStatement
PreparedStatement stmt = conn.prepareStatement(
  "SELECT * FROM users WHERE login = ? AND password = ?"
);
stmt.setString(1, login);
stmt.setString(2, sha256Password);
⚠️
Règle absolue : Jamais de concaténation de chaînes avec des données utilisateur dans une requête SQL. Toujours des requêtes préparées (paramétrisées) ou un ORM. Sans exception, même pour "une simple requête". Les requêtes préparées séparent le code SQL des données — le moteur interprète les paramètres comme des valeurs pures, jamais comme du code.
🔬
Défense en profondeur : En plus des requêtes préparées : (1) Validez les types côté serveur (int, email, etc.). (2) Utilisez le principe du moindre privilège — l'utilisateur DB de l'app n'a que SELECT/INSERT/UPDATE/DELETE, jamais DROP/ALTER. (3) Activez le WAF (Web Application Firewall). (4) Loggez les erreurs SQL sans les exposer à l'utilisateur final.
🎓
LEÇON 50

Bonnes Pratiques & Récapitulatif Final

Vous avez parcouru les 50 leçons — des fondements jusqu'aux techniques avancées d'optimisation. Voici les principes qui distinguent un développeur SQL professionnel et des recommandations pour la suite.

01
Clé primaire sur chaque tableChaque table doit avoir un INT AUTO_INCREMENT PRIMARY KEY. Sans identifiant unique, les mises à jour ciblées, les jointures, et la suppression précise deviennent impossibles.
02
Spécifier les colonnes dans INSERTÉcrivez toujours INSERT INTO t (col1, col2) VALUES (...). Jamais INSERT INTO t VALUES (...). Une colonne ajoutée ou réordonnée suffit à casser silencieusement vos insertions.
03
SELECT avant tout UPDATE/DELETEExécutez le SELECT correspondant et vérifiez les lignes ciblées avant tout UPDATE ou DELETE. Un WHERE manquant peut modifier ou effacer des millions de lignes en une seconde.
04
EXPLAIN sur les requêtes lentesTout type = ALL sur une grande table est un problème à résoudre. Vérifiez les colonnes WHERE et JOIN avec EXPLAIN. Créez les index manquants. Remesurez.
05
Transactions pour les opérations critiquesTout transfert d'argent, toute opération multi-tables logiquement liée doit être dans une transaction. Si une étape échoue, tout est annulé — la base reste cohérente.
06
Requêtes préparées — zéro exceptionJamais de concaténation d'input utilisateur dans du SQL. Toujours des requêtes préparées ou un ORM. Une seule exception à cette règle peut compromettre toute votre base.
07
utf8mb4 par défautCréez vos bases avec CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci. Pas de douleurs avec les emojis, l'arabe, le chinois. Une fois les données corrompues, la migration est pénible.
08
Sauvegardes automatiques testéesConfigurez un cron job de mysqldump quotidien. Testez la restauration chaque mois sur une machine de staging. Une sauvegarde non testée n'est pas une sauvegarde — c'est une fausse sécurité.

La prochaine étape : construire de vrais projets. Une API REST avec Node.js ou Python, un tableau de bord analytique, un système de facturation. Chaque requête concrète consolide la compréhension plus que dix leçons théoriques. Le voyage ne fait que commencer. 🚀