Full table scan vs Full index scan part1-2
1/ Le mystère de la variable Last_query_cost
MySQL utilise un optimiseur à base de coûts. Le plan d’exécution de la requête choisit est celui dont le coût est le plus faible. Ce dernier peut être visualisé grâce à la variable Last_query_cost.
Son unité est le coût (encore lui) des accès aléatoires en lecture de pages de 4ko.
Étrangement cette variable est assez peu/mal documentée. Voici ce qu’on retrouve dans la doc officielle de MySQL
Je cite:
Last_query_cost
The total cost of the last compiled query as computed by the query optimizer. This is useful for comparing the cost of different query plans for the same query. The default value of 0 means that no query has been compiled yet. The default value is 0.Last_query_cost has session scope.
TheLast_query_cost value can be computed accurately only for simple “flat” queries, not complex queries such as those with subqueries orUNION. For the latter, the value is set to 0.
La valeur de Last_query_cost est parfois déconcertante, même avec MySQL 5.6. Voyez par vous même…
Quelques informations avant d’entrer dans le vif du sujet.
Server version: 5.6.9-rc MySQL Community Server (GPL)
Configuration de l’instance:
Innodb buffer pool size = 32 Go
max_heap_table_size = tmp_table_size = 512 Mo
Table:
CREATE TABLE `bills` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`d` datetime NOT NULL,
`price` smallint(5) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `d` (`d`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci
SHOW TABLE STATUS:
*************************** 1. row ***************************
Name: bills
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 176268
Avg_row_length: 56
Data_length: 9961472
Max_data_length: 0
Index_length: 4210688
Data_free: 2097152
…
Collation: utf8_swedish_ci
=> Données + Index: environs 16 Mo
L’idée est d’exécuter la requête ci-dessous, avec différents “hint” est de voir les plans d’exécution (QEP) générés par MySQL ainsi que leurs coûts respectifs.
SELECT d,avg(price) FROM bills GROUP BY d
2 QEP possibles:
– Faire un full table scan, créer une table temporaire pour regrouper et ordonner (particularité du GROUP BY de MySQL) les données en fonction de la date (d) pour calculer la moyenne des prix (avg(price)).
– Faire un full index scan sur d, ce qui évite le tri et la création de la table temporaire. Cependant le “price” n’étant pas dans l’index, l’info doit être récupéré dans la couche “data”
Query 1/ EXPLAIN SELECT d,avg(price) FROM bills GROUP BY d\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: bills
type: index
possible_keys: NULL
key: d
key_len: 5
ref: NULL
rows: 176268
Extra: NULL
Le choix de l’optimiseur est assez intuitif, il se sert du fait que l’index d est trié (type: index = full index scan) pour optimiser le GROUP BY (regroupement + tri), c’est à dire ne pas créer de table temporaire.
Le coût de cette opération est:
SHOW STATUS LIKE ‘Last_query_cost;
+—————–+—————+
| Variable_name | Value |
+—————–+—————+
| Last_query_cost | 212129.599000 |
+—————–+—————+
Question de psychopathe, que se passe t’il si je force l’optimiseur à utiliser l’index qu’il dit qu’il va utiliser ? (c’est vicieux je sais :D)
Query 2/ EXPLAIN SELECT d,avg(price) FROM bills USE INDEX(d) GROUP BY d\G *************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: bills
type: index
possible_keys: NULL
key: d
key_len: 5
ref: NULL
rows: 176268
Extra: NULL
Alors pour être précis, là je ne le force pas vraiment, je l’incite (fortement) à l’utiliser. Mais sans surprise, il l’utilise (ouf !), en d’autre terme le QEP est le même que précédemment.
Et qu’en est il du coût ?
SHOW STATUS LIKE ‘Last_query_cost;
+—————–+—————+
| Variable_name | Value |
+—————–+—————+
| Last_query_cost | 212129.599000 |
+—————–+—————+
Pareil que précédemment, circuler il n’y a rien à voir !
Ok est si là je le force vraiment, avec FORCE INDEX (principale différence entre USE INDEX et FORCE INDEX est qu’avec le second l’optimiseur choisira (si c’est possible bien sûr) d’utiliser l’index en lieu est place d’un full table scan)
Query 3/ EXPLAIN SELECT d,avg(price) FROM bills FORCE INDEX(d) GROUP BY d\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: bills
type: index
possible_keys: NULL
key: d
key_len: 5
ref: NULL
rows: 176268
Extra: NULL
Là encore, (sans surprise) le QEP est le même que précédemment. Et le coût ?
SHOW STATUS LIKE ‘Last_query_cost;
+—————–+—————+
| Variable_name | Value |
+—————–+—————+
| Last_query_cost | 387790.599000 |
+—————–+—————+
… WTF …
Bon là pour être franc, je n’ai pas d’explications rationnelles. Serait ce une feature ? (rationnelle on à dit !!!) ok ok bah un bug alors.
Inutile de préciser que je me suis empressé de tester avec d’autres versions de MySQL (psychopate !) et le résultat est sans appel:
Avec MySQL 5.5, MariaDB 5.5 & Percona server 5.5 le coût du FORCE INDEX est identique aux 3 autres coûts.
J’opterai donc pour une petite régression de l’optimiseur, bref un BUG
Soit ! si j’empêche l’optimiseur d’utiliser l’index:
Query 4/ EXPLAIN SELECT d,avg(price) FROM bills IGNORE INDEX(d) GROUP BY d\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: bills
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 176268
Extra: Using temporary; Using filesort
C’est plutôt clair, full table scan (type: ALL), plus création d’un table temporaire (Using temporary) pour regrouper les données et les trier (Using filesort). C’est le prix à payer pour ne pas utiliser l’index. Ça à l’air vachement coûteux tout ça, Voyons ce qu’en pense MySQL
SHOW STATUS LIKE ‘Last_query_cost’;
+—————–+—————+
| Variable_name | Value |
+—————–+—————+
| Last_query_cost | 212129.599000 |
+—————–+—————+
Humm pour l’optimiseur, le coût est le même, pas forcément intuitif tout ça Pour rappel (ou pas) les paramètres pris en compte par l’optimiseur sont (entre autre):
– Pour les coûts I/O: information sur les enregistrements (nombre, taille,), informations sur les index(null, unique, cardinalité…)…
– Pour les coûts CPU: l’évaluation des conditions, comparaison du rapport index/enregistrements
Force est de constater que selon l’optimiseur, le coût du FTS + table temp + tri et équivalent, pour cette requête au FIS.
Bilan de cette première partie:
– Il y a manifestement un bug de l’optimiseur en MySQL 5.6.9 lors du calcul du coût d’une requête avec la clause FORCE INDEX
– Dans notre exemple, le coût d’un full index scan est le même qu’un full table scan + Using temporary + Using filesort
Mais alors, quel QEP donne t’il le meilleur temps d’exécution ?
Cette question est traitée dans la 2ème partie de cet article
Architecte Solution Cloud chez Oracle
MySQL Geek, Architecte, DBA, Consultant, Formateur, Auteur, Blogueur et Conférencier.
—–
Blog: www.dasini.net/blog/en/
Twitter: https://twitter.com/freshdaz
SlideShare: www.slideshare.net/freshdaz
Youtube: https://www.youtube.com/channel/UC12TulyJsJZHoCmby3Nm3WQ
—–