Indexation — Stratégies de performance
Choisir et créer les bons index est souvent plus efficace qu’optimiser une requête.
Commandes de base
MySQL / MariaDB
-- Créer un index simple
CREATE INDEX idx_cmd_date ON commandes(cree_a);
-- Créer un index composite (ordre important)
CREATE INDEX idx_cmd_user_date ON commandes(utilisateur_id, cree_a);
-- Lister les index d'une table
SHOW INDEX FROM commandes;
-- Supprimer un index
ALTER TABLE commandes DROP INDEX idx_cmd_date;
-- Vérifier l'utilisation d'un index par EXPLAIN
EXPLAIN SELECT * FROM commandes WHERE utilisateur_id = 42 AND cree_a > '2024-01-01';PostgreSQL
-- Créer un index
CREATE INDEX idx_cmd_date ON commandes(cree_a);
-- Index Composite
CREATE INDEX idx_cmd_user_date ON commandes(utilisateur_id, cree_a);
-- Lister les index
SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'commandes';
-- Supprimer un index
DROP INDEX idx_cmd_date;
-- Analyser l'exécution
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM commandes WHERE utilisateur_id = 42;SQLite
CREATE INDEX idx_cmd_date ON commandes(cree_a);
PRAGMA index_list('commandes');
EXPLAIN QUERY PLAN SELECT * FROM commandes WHERE cree_a > '2024-01-01';Règles de conception
Ordre des colonnes en index composite
L’ordre compte. Suivre la règle equality first, then range :
-- ✅ Bon : equality sur utilisateur_id, range sur cree_a
CREATE INDEX idx ON commandes(utilisateur_id, cree_a);
-- ❌ Mauvais : la range sur cree_a coupe l'utilisation de utilisateur_id
CREATE INDEX idx ON commandes(cree_a, utilisateur_id);Pour une requête WHERE utilisateur_id = X AND cree_a > 'date', l’index cmd(user_id, cree_a) est efficace car MySQL/PostgreSQL peut utiliser les deux colonnes.
Index B-tree vs autres types
| Type | Usage | Exemple |
|---|---|---|
| B-tree (défaut) | Requêtes générales, tri, range | WHERE prix > 10 |
| HASH | Égalité uniquement | WHERE id = 42 |
| GIN (Postgres) | JSONB, full-text, tableaux | WHERE jsonb @> '{"key":"val"}' |
| GIST (Postgres) | Géospatiaux, opérateurs custom | PostGIS |
| Expression | Indexer le résultat d’une fonction | CREATE INDEX idx_lower ON t(LOWER(col)) |
Index UNIQUE
-- Protège contre les doublons et accélère les recherches
CREATE UNIQUE INDEX idx_cmd_code ON commandes(code_commande);Ajouter
UNIQUEquand la sémantique métier l’exige (email, code interne). Ça évite les bugs plus qu’il n’accélère.
Quand un index ralentit
Un index a un coût : chaque INSERT/UPDATE/DELETE doit mettre à jour tous les index.
Vérifier l’impact
-- MySQL : table information_schema
SELECT index_name, cardinality, seq_in_index, column_name
FROM information_schema.statistics
WHERE table_schema = 'mon_bdd' AND table_name = 'commandes'
ORDER BY index_name, seq_in_index;Indices inutilisés
-- PostgreSQL : vérifier les index jamais scannés
SELECT indexrelname, idx_scan
FROM pg_stat_user_indexes
ORDER BY idx_scan ASC;-- MySQL : activer la surveillance
SET GLOBAL stat_user_indexes=ON;
-- Then check performance_schemaSupprimer un index avec zéro scan depuis plus de 30 jours en production si la charge d’écriture est significative.
EXPLAIN — Lire un plan d’exécution
Indicateurs clés
| Critère | Signification |
|---|---|
| type: ALL | Full table scan — besoin d’un index |
| type: range | Utilisation d’un index avec plage |
| type: ref | Égalité via index (bon) |
| type: eq_ref | Égalité via clé primaire (idéal) |
| rows: élevé | Beaucoup de lignes examinées |
| Extra: Using filesort | Trie en mémoire — index inadéquat |
| Extra: Using temporary | Table temporaire — jointure/groupage coûteux |
Exemple
EXPLAIN SELECT * FROM commandes WHERE utilisateur_id = 42 AND cree_a > '2024-01-01';
type: range
key: idx_cmd_user_date
rows: 15
Extra: Using index condition15 lignes examinées vs des milliers en full scan = l’index fonctionne.
Index covering (covered queries)
Un index covering (ou index only scan) évite de lire les lignes de la table en retournant directement les valeurs depuis l’index. C’est souvent le gain de performance le plus important pour les requêtes de lecture.
Détection dans EXPLAIN
EXPLAIN SELECT nom, prenom FROM utilisateurs WHERE ville = 'Paris';
type: index
key: idx_ville_nom_prenom
Extra: Using indexLe Extra: Using index confirme que l’index couvre la requête. Pas de lecture de
lignes supplémentaires, pas de désérialisation.
Exemple avec BUFFERS
EXPLAIN (ANALYZE, BUFFERS)
SELECT nom, prenom FROM utilisateurs WHERE ville = 'Paris';
-> Seq Scan on utilisateurs (cost=0.00..45.00 rows=120 width=40) (actual time=0.012..0.890 rows=120 loops=1)
Filter: (ville = 'Paris')
Rows Removed by Filter: 9880
Buffers: shared hit=100 dirtied=11Interprétation
| Champ | Signification |
|---|---|
| actual time | Temps réel entre le premier row et le dernier row |
| rows (actual) | Nombre de lignes réellement extraites (pas estimé) |
| loops | Nombre de passages au niveau parent |
| Buffers: shared hit=100 | 100 pages satisfaites depuis le cache OS |
| Buffers: shared read=50 | 50 pages lues depuis le disque (coûteux) |
Objectif : maximiser
hitet minimiserread. Un index covering réduit drastiquement les lectures disque.
Concevoir un index covering
-- Requête courante
SELECT nom, prenom FROM utilisateurs WHERE ville = 'Paris' ORDER BY nom;
-- Index covering adapté
CREATE INDEX idx_u_ville_nom_prenom ON utilisateurs(ville, nom, prenom);Règles :
- Toutes les colonnes de la WHERE en premier (égalité d’abord, puis range)
- Toutes les colonnes de la SELECT ensuite (ordre croissant pour le ORDER BY)
- Éviter
SELECT *— un index covering ne fonctionne que si toutes les colonnes récupérées sont dans l’index.
Plus l’index est large, plus il occupe de mémoire et ralentit les écritures. Un index covering de 5 colonnes reste acceptable pour une table à forte lecture. Au-delà, peser le coût d’écriture contre le gain de lecture.
Pièges courants
| Piège | Symptôme | Solution |
|---|---|---|
| COLLATE/string utf8mb4 | Index inefficace sur chaînes | Vérifier le COLLATE dans EXPLAIN |
| Fonction dans WHERE | WHERE YEAR(date) = 2024 bypass | Utiliser une plage : WHERE date >= '2024-01-01' AND date < '2025-01-01' |
| Trop d’index | INSERT lent | Auditer les scans, supprimer les zéro-utilisation |
| Index sur colonnes low-cardinality | Index peu utile (BOOL, ENUM petits) | Privilégier les colonnes à haute cardinalité |
| ANALYZE nécessaire | Plan d’exécution périmé | Lancer ANALYZE TABLE après gros chargements |
L’index adapté vaut mieux que le code optimisé.