⚡ Sous-requêtes

Une sous-requête est un SELECT imbriqué dans une autre requête. Elle peut apparaître dans le WHERE, le FROM, ou le SELECT.

Dans WHERE (scalaire)

-- Produits au-dessus de la moyenne
SELECT nom, prix
FROM produits
WHERE prix > (SELECT AVG(prix) FROM produits);

-- Clients ayant passé une commande livrée
SELECT nom FROM clients
WHERE id IN (SELECT DISTINCT client_id FROM commandes WHERE statut = 'livree');

Sous-requête corrélée

-- Produits au-dessus de la moyenne de LEUR catégorie
SELECT p.nom, p.prix,
       (SELECT AVG(prix) FROM produits WHERE categorie_id = p.categorie_id) AS moy_cat
FROM produits p
WHERE p.prix > (SELECT AVG(prix) FROM produits WHERE categorie_id = p.categorie_id);

Dans FROM (table dérivée)

-- Clients avec plus de 500€ de commandes
SELECT cl.nom, stats.total
FROM clients cl
JOIN (
  SELECT client_id, SUM(total) AS total
  FROM commandes
  GROUP BY client_id
  HAVING total > 500
) stats ON cl.id = stats.client_id;

EXISTS

-- Plus efficace que IN sur de grandes tables
SELECT nom FROM clients cl
WHERE EXISTS (
  SELECT 1 FROM commandes
  WHERE client_id = cl.id AND statut = 'livree'
);

📋 CTE — Common Table Expressions

Les CTE (WITH ... AS) définissent des sous-requêtes nommées pour améliorer la lisibilité.

-- CTE simple
WITH clients_actifs AS (
  SELECT client_id, SUM(total) AS ca
  FROM commandes WHERE statut != 'annulee'
  GROUP BY client_id
)
SELECT cl.nom, ca.ca
FROM clients cl
JOIN clients_actifs ca ON cl.id = ca.client_id
ORDER BY ca.ca DESC;

-- Plusieurs CTE chaînées
WITH
  stats_par_categorie AS (
    SELECT categorie_id, AVG(prix) AS moy_cat
    FROM produits GROUP BY categorie_id
  ),
  produits_premium AS (
    SELECT p.id, p.nom, p.prix, s.moy_cat
    FROM produits p
    JOIN stats_par_categorie s ON p.categorie_id = s.categorie_id
    WHERE p.prix > s.moy_cat * 1.5
  )
SELECT * FROM produits_premium ORDER BY prix DESC;

👁️ Vues (VIEWS)

Une vue est une requête stockée sous un nom. Elle est ré-exécutée à chaque appel.

-- Créer une vue
CREATE OR REPLACE VIEW vue_produits_complet AS
  SELECT p.id, p.nom, p.prix, p.stock, c.nom AS categorie
  FROM produits p
  LEFT JOIN categories c ON p.categorie_id = c.id;

-- Utiliser la vue comme une table normale
SELECT * FROM vue_produits_complet WHERE categorie = 'Electronique';
SELECT * FROM vue_produits_complet WHERE stock < 20 ORDER BY stock;

-- Supprimer une vue
DROP VIEW IF EXISTS vue_produits_complet;

-- Voir la définition d'une vue
SHOW CREATE VIEW vue_produits_complet;
Une vue simple (1 table, pas d'agrégation, pas de DISTINCT) peut être mise à jour via INSERT/UPDATE/DELETE. Une vue complexe est en lecture seule.

🔒 Transactions ACID

Une transaction groupe plusieurs opérations : soit toutes réussissent (COMMIT), soit toutes sont annulées (ROLLBACK).

START TRANSACTION;  -- ou BEGIN;

  -- Opérations...
  INSERT INTO commandes (client_id, total) VALUES (1, 150.00);
  SET @id = LAST_INSERT_ID();
  INSERT INTO commande_produits VALUES (@id, 3, 2, 29.99);
  UPDATE produits SET stock = stock - 2 WHERE id = 3;

COMMIT;   -- Valide tout
-- ou
ROLLBACK; -- Annule tout

Niveaux d'isolation

-- Voir le niveau actuel
SELECT @@transaction_isolation;

-- Changer pour la session
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Niveaux (du moins au plus isolé) :
-- READ UNCOMMITTED  : voit les changements non-committed (dirty reads)
-- READ COMMITTED    : voit seulement les committed
-- REPEATABLE READ   : défaut InnoDB, même résultat si la requête est relancée
-- SERIALIZABLE      : transactions totalement séquentielles

Savepoints

START TRANSACTION;
  INSERT INTO logs VALUES ('debut');
  SAVEPOINT sp1;

  INSERT INTO produits (nom, prix) VALUES ('Test', -1);  -- sera annulé
  ROLLBACK TO sp1;  -- annule seulement jusqu'au savepoint

  INSERT INTO logs VALUES ('fin');
COMMIT;  -- le premier et le dernier INSERT sont committed

⚙️ Procédures stockées

DELIMITER $$

CREATE PROCEDURE passer_commande(
  IN p_client_id INT,
  IN p_produit_id INT,
  IN p_quantite INT
)
BEGIN
  DECLARE v_prix DECIMAL(10,2);
  DECLARE v_stock INT;
  DECLARE v_cmd_id INT;

  -- Vérifier le stock
  SELECT prix, stock INTO v_prix, v_stock
  FROM produits WHERE id = p_produit_id FOR UPDATE;

  IF v_stock < p_quantite THEN
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Stock insuffisant';
  END IF;

  START TRANSACTION;
    INSERT INTO commandes (client_id, total)
    VALUES (p_client_id, v_prix * p_quantite);
    SET v_cmd_id = LAST_INSERT_ID();

    INSERT INTO commande_produits (commande_id, produit_id, quantite, prix_unitaire)
    VALUES (v_cmd_id, p_produit_id, p_quantite, v_prix);

    UPDATE produits SET stock = stock - p_quantite WHERE id = p_produit_id;
  COMMIT;

  SELECT v_cmd_id AS commande_id;
END$$

DELIMITER ;

-- Appel
CALL passer_commande(1, 3, 2);

⚡ Triggers

-- Mettre à jour un compteur à chaque INSERT
DELIMITER $$
CREATE TRIGGER after_commande_insert
AFTER INSERT ON commandes
FOR EACH ROW
BEGIN
  UPDATE clients
  SET nb_commandes = nb_commandes + 1
  WHERE id = NEW.client_id;
END$$
DELIMITER ;

-- NEW = valeurs de la ligne insérée
-- OLD = valeurs avant modification/suppression (UPDATE/DELETE)
Les triggers sont puissants mais difficiles à déboguer. Préférez la logique applicative quand c'est possible. Documentez soigneusement chaque trigger.