L'ordre des colonnes dans un index composite change tout
Un index sur (a, b, c) ne sert pas les requêtes qui filtrent uniquement sur b ou c : MySQL utilise l'index de gauche à droite, en s'arrêtant à la première colonne absente du filtre. La règle pratique reste égalité avant plage : place les colonnes filtrées avec = en premier, puis celle utilisée avec >, < ou BETWEEN en dernier.
-- Index utile pour : WHERE tenant_id = ? AND status = ? AND created_at > ?
CREATE INDEX idx_orders_lookup
ON orders (tenant_id, status, created_at);
Ajouter created_at avant status casserait l'égalité sur status, forçant un scan partiel de l'index.
Vérifier avec EXPLAIN ANALYZE, pas seulement EXPLAIN
EXPLAIN donne un plan théorique basé sur des statistiques ; EXPLAIN ANALYZE (disponible depuis MySQL 8.0.18) exécute réellement la requête et affiche les coûts et le nombre de lignes mesurés à chaque étape. C'est l'outil à privilégier quand un index existe mais que l'optimiseur choisit quand même un full table scan — souvent un signe de statistiques périmées (ANALYZE TABLE) ou de faible sélectivité.
Le piège classique : REPEATABLE READ et les lectures fantômes évitées, mais pas tout
MySQL/InnoDB utilise REPEATABLE READ par défaut, plus strict que le standard SQL grâce aux next-key locks qui bloquent les insertions dans les plages verrouillées. Mais un SELECT simple lit un instantané cohérent (MVCC), alors qu'un SELECT ... FOR UPDATE lit les données les plus récentes et pose des verrous. Mélanger les deux dans la même transaction est une source classique de bugs difficiles à reproduire :
START TRANSACTION;
SELECT stock FROM products WHERE id = 42; -- snapshot, peut être obsolète
SELECT stock FROM products WHERE id = 42 FOR UPDATE; -- valeur fraîche, verrouillée
UPDATE products SET stock = stock - 1 WHERE id = 42;
COMMIT;
Pour une décrémentation, éviter le read-then-write et préférer une opération atomique : UPDATE products SET stock = stock - 1 WHERE id = 42 AND stock > 0;, puis vérifier ROW_COUNT().
Colonnes JSON : indexer via une colonne générée
MySQL ne permet pas d'indexer directement un chemin JSON, mais une colonne générée virtuelle le rend possible :
ALTER TABLE events
ADD COLUMN user_id INT
GENERATED ALWAYS AS (payload->>'$.user_id') STORED,
ADD INDEX idx_events_user (user_id);
Cela évite de scanner et parser du JSON à chaque requête, tout en gardant un schéma flexible pour le reste du document.
À vérifier régulièrement
SHOW ENGINE INNODB STATUSpour repérer les deadlocks récents.performance_schemapour identifier les requêtes les plus coûteuses en production.- La taille du
innodb_buffer_pool_sizepar rapport au volume de données réellement actif.