Optimisation de requêtes MySQL par indexation - Partie 1
Optimisation de requêtes MySQL - Partie 1 .
Optimisation de requêtes MySQL par indexation - Partie 1
L’indexation de variables est le principal levier d’amélioration de la performance des requêtes SQL. Dans cette série d’article nous allons présenter l’utilité des index, leur utilisation par MySQL pour l’exécution de requêtes, et leurs implications sur la performance.
Dans cette première partie, à l’aide de la fonction EXPLAIN, nous allons voir comment MySQL exécute une requête, et étudier l’impact des index sur leur vitesse d’exécution.
Données
Les données utilisées sont issues du playground Kaggle “predict future sales”, auxquelles j’ai rajouté une clé unique auto-incrémentée pour les tables sans identifiant unique de ligne.
Debug de requêtes
MySQL utilise les index pour optimiser de nombreuses opérations: les requêtes simples, les jointures, les tris, les groupements et agrégations.
En l’absence d’index, l’optimiseur parcourt toute la colonne à la recherche du critère indiqué dans la requête. Prenons l’exemple d’une requête basée sur la variable ‘index’ (clé primaire et donc index de la table sales_train), et de la variable item_id (simple colonne de la table, non indexée).
SELECT *
FROM sales_train
WHERE `index` = 2000000
SELECT *
FROM sales_train
WHERE item_id = 22169
La première requête s’exécute quasi-instantanément (4ms), et la seconde 225 fois plus longtemps (900ms). Mysql donne la possibilité d’étudier le schéma d’exécution grâce à la fonction EXPLAIN, à ajouter en préfixe de la requête.
EXPLAIN SELECT * FROM sales_train WHERE `index` = 2000000
EXPLAIN SELECT * FROM sales_train WHERE item_id = 22169
| table | type | possible_keys | key | rows | |
|---|---|---|---|---|---|
| 1ère requête | sales_train | const | PRIMARY | PRIMARY | 1 |
| 2de requête | sales_train | ALL | NULL | NULL | 2722275 |
Chaque requête précédée d’EXPLAIN, renvoie une table dont les colonnes qui nous concernent sont présentées dans le tableau ci-dessus.
- Colonne rows: On y voit que la première requête examine 1 seule ligne, alors que la seconde parcourt toute la table, à savoir les 2 722 275 lignes correspondant au nombre total d’observations. (note: la requête n’étant pas exécutée, il s’agit en réalité d’approximations)
- Colonnes key: On peut voir que la seconde requête n’utilise pas d’index pour son exécution, contrairement à la première
- Colonne type: ALL dans cette colonne, et NULL dans la colonne key nous indique un scan de toute la table
Optimisation par l’ajout d’index
Requête simple
-- Dupliquer la table et ajouter indexer item_id
CREATE TABLE sales_train_test LIKE sales_train;
INSERT INTO sales_train_test SELECT * FROM sales_train;
ALTER TABLE sales_train_test ADD INDEX `item_id` (`item_id`);
-- Requête
SELECT * FROM sales_train_test WHERE item_id = 22169;
EXPLAIN SELECT * FROM sales_train_test WHERE item_id = 22169;
Dans le premier bloc requête, une nouvelle table ‘sales_train_test’ est créée à partir de la table utilisée précédemment, et la variable ‘item_id’ y est indexée. Enfin, nous exécutons la même requête que dans la partie précédente, et nous voyons que cette fois-ci la requête s’exécute en 6ms (contre 900ms auparavant).
| table | type | possible_keys | key | rows | |
|---|---|---|---|---|---|
| Sans index | sales_train | ALL | NULL | NULL | 2722275 |
| Avec index | sales_train_test | ref | item_id | item_id | 1 |
La fonction EXPLAIN nous indique que l’optimiseur prend bien en compte l’index pour exécuter la requête, qui ne parcourt plus toutes les lignes à la recherche du critère.
L’ajout d’index n’améliore pas systématiquement la performance d’une requête. Nous en expliquerons les raisons dans les parties suivantes.
Jointure
Nous allons tester 3 cas de jointures. Le premier avec ‘item_id’ indexé dans les deux tables, le second dans une seule des deux tables, et enfin sans que la variable ne soit indexée. Le tableau ci dessous est le résultat de la fonction EXPLAIN, ainsi que le temps d’exécution pour un INNER JOIN des tables ‘sales_train’ (T1) et ‘items’ (T2) avec et sans index pour la variable de jointure ‘item_id’
| Temps | type | key | rows | |
|---|---|---|---|---|
| T1_index T2_index |
7ms | ref ALL |
item_id NULL |
124 22 418 |
| T1_noindex T2_index |
8ms | ALL eq_ref |
NULL PRIMARY |
2 722 275 1 |
| T1_index T2_noindex |
7ms | ref ALL |
item_id NULL |
124 21 432 |
| T1_noindex T2_noindex |
258ms | ALL ALL |
NULL NULL |
2 722 275 21 432 |
On observe qu’en l’absence d’index la requête met près de 40 fois plus de temps à s’exécuter.
Tri, groupements, agrégations
L’utilisation d’index se révèle particulièrement efficace pour les opérations de tri et de groupement (qui trient les données afin de pouvoir former les groupes). Nous pouvons voir dans le tableau produit par la fonction EXPLAIN, que l’utilisation de l’index est beaucoup plus efficace que l’utilisation d’une table temporaire pour grouper les données, qui par ailleurs est enregistrée sur le disque si elle ne peut être contenue en mémoire (colonne Extra)1
-- Table originale
SELECT item_id, count(*)
FROM sales_train
GROUP BY item_id
EXPLAIN SELECT ...
-- Table avec item_id en index
SELECT item_id, count(*)
FROM sales_train_test
GROUP BY item_id
EXPLAIN SELECT ...
| type | possible_keys | key | rows | Extra | |
|---|---|---|---|---|---|
| Sans index (1630ms) | ALL | NULL | NULL | 2722275 | Using temporary Using filesort |
| Avec index (7ms) | ref | item_id | item_id | 1 | Using index |
Avoir un index en variable de regroupement/tri ne garantit pas son utilisation par l’optimiseur
-
http://s.petrunia.net/blog/?p=24 ↩