Un index doit-il couvrir toutes les colonnes sélectionnées pour pouvoir être utilisé pour COMMANDER PAR?

15

Chez SO, quelqu'un a récemment demandé pourquoi ORDER BY n'utilisait pas l'index?

La situation impliquait une simple table InnoDB dans MySQL comprenant trois colonnes et 10k lignes. L'une des colonnes, un entier, a été indexée - et l'OP a cherché à récupérer l'intégralité de sa table triée sur cette colonne:

SELECT * FROM person ORDER BY age

Il a joint une EXPLAINsortie montrant que cette requête a été résolue avec un filesort(plutôt qu'avec l'index) et a demandé pourquoi.

Malgré l' indication FORCE INDEX FOR ORDER BY (age) qui fait que l'index est utilisé , quelqu'un a répondu (avec les commentaires / votes positifs des autres) qu'un index n'est utilisé pour le tri que lorsque les colonnes sélectionnées sont toutes lues à partir de l'index (c'est-à-dire comme cela serait normalement indiqué par Using indexdans la Extracolonne de EXPLAINsortie). Une explication a été donnée plus tard que la traversée de l'index puis la récupération des colonnes de la table entraînent des E / S aléatoires, ce que MySQL considère comme plus cher qu'un a filesort.

Cela semble aller à l'encontre du chapitre manuel sur l' ORDER BYoptimisation , qui donne non seulement l'impression forte que la satisfaction ORDER BYd'un index est préférable à un tri supplémentaire (en fait, filesortc'est une combinaison de tri rapide et de fusion et donc doit avoir une limite inférieure de ; tout en parcourant l’index dans l’ordre et en cherchant dans le tableau devrait être - donc cela est parfaitement logique), mais il néglige également de mentionner cette prétendue «optimisation» tout en déclarant:Ω(nlog n)O(n)

Les requêtes suivantes utilisent l'index pour résoudre la ORDER BYpièce:

SELECT * FROM t1
  ORDER BY key_part1,key_part2,... ;

À ma lecture, c'est précisément le cas dans cette situation (pourtant l'index n'était pas utilisé sans un indice explicite).

Mes questions sont:

  • Est-il en effet nécessaire que toutes les colonnes sélectionnées soient indexées pour que MySQL choisisse d'utiliser l'index?

    • Si oui, où est-ce documenté (le cas échéant)?

    • Sinon, que se passait-il ici?

eggyal
la source

Réponses:

14

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 EXPLAINplans ont utilisé l' genderindex. Les troisième et quatrième EXPLAINplans ont utilisé l' genderindice même si les données demandées l'étaient id. Pourquoi? Parce que se idtrouve dans le PRIMARY KEYet 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 EXPLAINplans ont utilisé l' genderindex. Dans le quatrième EXPLAINplan, 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:

  1. Venez à la réalisation que vous devez profiler les requêtes
  2. Trouver tous WHERE, GROUP BYet l' ordre des clauses BY` de ces requêtes
  3. Formuler des index dans cet ordre
    • WHERE colonnes de clause avec des valeurs statiques
    • GROUP BY Colonnes
    • ORDER BY Colonnes
  4. Évitez les analyses de table complètes (les requêtes manquant d'une WHEREclause raisonnable )
  5. Évitez les populations de clés incorrectes (ou du moins mettez en cache ces populations de clés incorrectes)
  6. 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 InnoDBj'ai mentionné auparavant. Il coïncide avec le persontableau. Pourquoi?

Pour les tables mfetperson

  • 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 EXPLAINplan 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 WHEREclause, 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 BYsur 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 filesortplutô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 WHEREclause.

MISE À JOUR 2012-11-14 14:21 EDT

Selon le livre Understanding MySQL Internals

entrez la description de l'image ici

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 WHEREclause. Ceci, en soi, est l'indice de l'optimiseur de requêtes MySQL pour effectuer une analyse complète de la table.

RolandoMySQLDBA
la source
Rolando, merci pour cette réponse si complète et détaillée. Cependant, il ne semble pas pertinent pour la sélection des index FOR ORDER BY(ce qui est le cas spécifique dans cette question). La question indiquait que dans ce cas, le moteur de stockage était InnoDB(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.
eggyal
C'est intéressant, car la première partie a également été mon premier instinct (il n'avait pas une bonne cardinalité, alors mysql a choisi d'utiliser l'analyse complète). Mais plus je lis, cette règle ne semble pas s'appliquer à la commande par optimisation. Êtes-vous sûr qu'il trie par clé primaire pour les index cluster innodb? Ce message indique que la clé primaire est ajoutée à la fin, donc le tri ne serait-il pas encore sur la ou les colonnes explicites de l'index? Bref, je suis toujours perplexe!
Derek Downey
1
La filesortsé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.
RolandoMySQLDBA
1
J'ai lu et relu cet article et d'autres articles, et je peux seulement convenir que cela a à voir avec l'ordre innodb sur la clé primaire puisque nous sélectionnons tous (et non un index de couverture). Je suis surpris qu'il n'y ait aucune mention de cette bizarrerie spécifique à InnoDB dans la page du document d'optimisation ORDER BY. Quoi qu'il en soit, +1 à Rolando
Derek Downey
1
@eggyal Ceci a été écrit cette semaine. Notez le même plan EXPLAIN et l'analyse complète prend plus de temps si l'ensemble de données ne tient pas en mémoire.
Derek Downey
0

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.

eggyal
la source