Est-il en effet nécessaire que toutes les colonnes sélectionnées soient indexées pour que MySQL choisisse d'utiliser l'index?
Il s'agit d'une question délicate car il existe des facteurs qui déterminent si un indice mérite d'être utilisé.
FACTEUR # 1
Pour un indice donné, quelle est la population clé? En d'autres termes, quelle est la cardinalité (nombre distinct) de tous les tuples enregistrés dans l'index?
FACTEUR # 2
Quel moteur de stockage utilisez-vous? Toutes les colonnes nécessaires sont-elles accessibles à partir d'un index?
ET APRÈS ???
Prenons un exemple simple: un tableau qui contient deux valeurs (mâle et femelle)
Laissez créer une telle table avec un test d'utilisation de l'index
USE test
DROP TABLE IF EXISTS mf;
CREATE TABLE mf
(
id int not null auto_increment,
gender char(1),
primary key (id),
key (gender)
) ENGINE=InnODB;
INSERT INTO mf (gender) VALUES
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
ANALYZE TABLE mf;
EXPLAIN SELECT gender FROM mf WHERE gender='F';
EXPLAIN SELECT gender FROM mf WHERE gender='M';
EXPLAIN SELECT id FROM mf WHERE gender='F';
EXPLAIN SELECT id FROM mf WHERE gender='M';
TEST InnoDB
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=InnoDB;
Query OK, 0 rows affected (0.07 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.06 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 37 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql>
TEST MyISAM
mysql> USE test
Database changed
mysql> DROP TABLE IF EXISTS mf;
Query OK, 0 rows affected (0.00 sec)
mysql> CREATE TABLE mf
-> (
-> id int not null auto_increment,
-> gender char(1),
-> primary key (id),
-> key (gender)
-> ) ENGINE=MyISAM;
Query OK, 0 rows affected (0.05 sec)
mysql> INSERT INTO mf (gender) VALUES
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('F'),('F'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('M'),('M'),('M'),('M'),('M'),('M'),('M'),('M'),
-> ('F'),('M'),('M'),('M'),('M'),('M'),('M'),('M');
Query OK, 40 rows affected (0.00 sec)
Records: 40 Duplicates: 0 Warnings: 0
mysql> ANALYZE TABLE mf;
+---------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------+---------+----------+----------+
| test.mf | analyze | status | OK |
+---------+---------+----------+----------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT gender FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 36 | Using where; Using index |
+----+-------------+-------+------+---------------+--------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='F';
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
| 1 | SIMPLE | mf | ref | gender | gender | 2 | const | 3 | Using where |
+----+-------------+-------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec)
mysql> EXPLAIN SELECT id FROM mf WHERE gender='M';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | mf | ALL | gender | NULL | NULL | NULL | 40 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
mysql>
Analyse pour InnoDB
Lorsque les données ont été chargées en tant qu'InnoDB, veuillez noter que les quatre EXPLAIN
plans ont utilisé l' gender
index. Les troisième et quatrième EXPLAIN
plans ont utilisé l' gender
indice même si les données demandées l'étaient id
. Pourquoi? Parce que se id
trouve dans le PRIMARY KEY
et tous les index secondaires ont des pointeurs de référence vers le PRIMARY KEY
(via le gen_clust_index ).
Analyse pour MyISAM
Lorsque les données ont été chargées en tant que MyISAM, veuillez noter que les trois premiers EXPLAIN
plans ont utilisé l' gender
index. Dans le quatrième EXPLAIN
plan, l'Optimiseur de requête a décidé de ne pas utiliser du tout d'index. Il a plutôt opté pour une analyse complète de la table. Pourquoi?
Quel que soit le SGBD, les optimiseurs de requête fonctionnent sur une règle empirique très simple: si un index est filtré en tant que candidat à utiliser pour effectuer la recherche et Query Optimizer calcule qu'il doit rechercher plus de 5% du nombre total de lignes du tableau:
- une analyse complète de l'index est effectuée si toutes les colonnes nécessaires à la récupération se trouvent dans l'index sélectionné
- une analyse complète de la table sinon
CONCLUSION
Si vous n'avez pas d'index de couverture appropriés ou si la population clé pour un tuple donné est supérieure à 5% du tableau, six choses doivent se produire:
- Venez à la réalisation que vous devez profiler les requêtes
- Trouver tous
WHERE
, GROUP BY
et l' ordre des clauses BY` de ces requêtes
- Formuler des index dans cet ordre
WHERE
colonnes de clause avec des valeurs statiques
GROUP BY
Colonnes
ORDER BY
Colonnes
- Évitez les analyses de table complètes (les requêtes manquant d'une
WHERE
clause raisonnable )
- Évitez les populations de clés incorrectes (ou du moins mettez en cache ces populations de clés incorrectes)
- Décidez du meilleur moteur de stockage MySQL ( InnoDB ou MyISAM ) pour les tables
J'ai écrit sur cette règle empirique de 5% dans le passé:
MISE À JOUR 2012-11-14 13:05 EDT
J'ai jeté un coup d'œil à votre question et au message SO original . Ensuite, j'ai pensé à mon que Analysis for InnoDB
j'ai mentionné auparavant. Il coïncide avec le person
tableau. Pourquoi?
Pour les tables mf
etperson
- Le moteur de stockage est InnoDB
- La clé primaire est
id
- L'accès à la table se fait par index secondaire
- Si la table était MyISAM, nous verrions un
EXPLAIN
plan complètement différent
Maintenant, regardez la requête de la question SO: select * from person order by age\G
. Puisqu'il n'y a aucune WHERE
clause, vous avez explicitement demandé une analyse complète de la table . L'ordre de tri par défaut de la table serait par id
(PRIMARY KEY) en raison de son auto_increment et le gen_clust_index (aka Clustered Index) est ordonné par rowid interne . Lorsque vous avez ordonné par l'index, gardez à l'esprit que les index secondaires InnoDB ont le rowid attaché à chaque entrée d'index. Cela crée le besoin interne d'un accès complet aux lignes à chaque fois.
La configuration ORDER BY
sur une table InnoDB peut être une tâche plutôt intimidante si vous ignorez ces faits sur la façon dont les index InnoDB sont organisés.
Pour en revenir à cette requête SO, puisque vous avez explicitement demandé une analyse complète de la table , à mon humble avis, MySQL Query Optimizer a fait la bonne chose (ou du moins, a choisi le chemin de moindre résistance). En ce qui concerne InnoDB et la requête SO, il est beaucoup plus facile d'effectuer une analyse complète de la table, puis certaines filesort
plutôt que de faire une analyse complète de l'index et une recherche de ligne via gen_clust_index pour chaque entrée d'index secondaire.
Je ne suis pas partisan de l'utilisation d'index car il ignore le plan EXPLAIN. Néanmoins, si vous connaissez vraiment mieux vos données qu'InnoDB, vous devrez recourir aux indices d'index, en particulier pour les requêtes sans WHERE
clause.
MISE À JOUR 2012-11-14 14:21 EDT
Selon le livre Understanding MySQL Internals
Le paragraphe 7 dit ce qui suit:
Les données sont stockées dans une structure spéciale appelée index clusterisé , qui est un arbre B avec la clé primaire agissant comme valeur de clé, et l'enregistrement réel (plutôt qu'un pointeur) dans la partie données. Ainsi, chaque table InnoDB doit avoir une clé primaire. Si aucun n'est fourni, une colonne d'ID de ligne spéciale qui n'est normalement pas visible par l'utilisateur est ajoutée pour agir comme clé primaire. Une clé secondaire stockera la valeur de la clé primaire qui identifie l'enregistrement. Le code de l'arbre B se trouve dans innobase / btr / btr0btr.c .
C'est pourquoi je l'ai dit plus tôt: il est beaucoup plus facile d'effectuer une analyse complète de la table, puis un tri de fichiers plutôt que d' effectuer une analyse complète de l'index et une recherche de ligne via gen_clust_index pour chaque entrée d'index secondaire . InnoDB va faire une recherche à double index à chaque fois . Cela semble plutôt brutal, mais ce ne sont que les faits. Encore une fois, prenez en considération l'absence de WHERE
clause. Ceci, en soi, est l'indice de l'optimiseur de requêtes MySQL pour effectuer une analyse complète de la table.
FOR ORDER BY
(ce qui est le cas spécifique dans cette question). La question indiquait que dans ce cas, le moteur de stockage étaitInnoDB
(et la question SO d'origine montre que les 10 000 lignes sont réparties assez uniformément sur 8 éléments, la cardinalité ne devrait pas être un problème ici non plus). Malheureusement, je ne pense pas que cela réponde à la question.filesort
sélection a été décidée par l'Optimiseur de requête pour une raison simple: il manque une connaissance préalable des données dont vous disposez. Si votre choix d'utiliser des indices d'index (basé sur le problème # 2) vous apporte un temps de fonctionnement satisfaisant, alors allez-y. La réponse que j'ai fournie était juste un exercice académique pour montrer à quel point le MySQL Query Optimizer peut être capricieux et suggérer des pistes d'action.Adapté (avec permission) de la réponse de Denis à une autre question sur SO:
Étant donné que tous les enregistrements (ou presque tous) seront récupérés par la requête, vous êtes généralement mieux sans aucun index. La raison en est qu'il en coûte quelque chose pour lire un index.
Comme vous optez pour la table entière, la lecture séquentielle de la table et le tri de ses lignes en mémoire peuvent être votre plan le moins cher. Si vous n'avez besoin que de quelques lignes et que la plupart correspondront à la clause where, opter pour le plus petit index fera l'affaire.
Pour comprendre pourquoi, imaginez les E / S disque impliquées.
Supposons que vous souhaitiez la table entière sans index. Pour ce faire, vous lisez data_page1, data_page2, data_page3, etc., en visitant les différentes pages de disque concernées dans l'ordre, jusqu'à la fin du tableau. Vous triez ensuite et retournez.
Si vous voulez les 5 premières lignes sans index, vous devez lire séquentiellement l'intégralité du tableau comme précédemment, tout en triant en tas les 5 premières lignes. Certes, c'est beaucoup de lecture et de tri pour une poignée de lignes.
Supposons maintenant que vous vouliez la table entière avec un index. Pour ce faire, vous lisez successivement index_page1, index_page2, etc. Cela vous amène alors à visiter, disons, data_page3, puis data_page1, puis data_page3 à nouveau, puis data_page2, etc., dans un ordre complètement aléatoire (celui par lequel les lignes triées apparaissent dans les données). L'IO impliqué rend moins cher la lecture séquentielle de l'ensemble du désordre et le tri du sac de maintien en mémoire.
Si vous voulez simplement les 5 premières lignes d'une table indexée, en revanche, l'utilisation de l'index devient la bonne stratégie. Dans le pire des cas, vous chargez 5 pages de données en mémoire et continuez.
Un bon planificateur de requêtes SQL, btw, décidera d'utiliser ou non un index en fonction de la fragmentation de vos données. Si la récupération des lignes dans l'ordre signifie un zoom avant et arrière sur la table, un bon planificateur peut décider que cela ne vaut pas la peine d'utiliser l'index. En revanche, si la table est regroupée en utilisant ce même index, les lignes sont garanties d'être en ordre, augmentant ainsi la probabilité qu'elle soit utilisée.
Mais ensuite, si vous joignez la même requête avec une autre table et que cette autre table a une clause where extrêmement sélective qui peut utiliser un petit index, le planificateur peut décider qu'il est en fait préférable de, par exemple, récupérer tous les ID des lignes marquées comme
foo
, hachage rejoindre les tables et les trier en mémoire.la source