Pourquoi MySQL ignore-t-il l'index même en force pour cette commande par?


14

Je dirige un EXPLAIN:

mysql> explain select last_name from employees order by last_name;
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Les index de ma table:

mysql> show index from employees;  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| Table     | Non_unique | Key_name      | Seq_in_index | Column_name   | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
| employees |          0 | PRIMARY       |            1 | subsidiary_id | A         |           6 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          0 | PRIMARY       |            2 | employee_id   | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
| employees |          1 | idx_last_name |            1 | last_name     | A         |       10031 |      700 | NULL   |      | BTREE      |         |               |  
| employees |          1 | date_of_birth |            1 | date_of_birth | A         |       10031 |     NULL | NULL   | YES  | BTREE      |         |               |  
| employees |          1 | date_of_birth |            2 | subsidiary_id | A         |       10031 |     NULL | NULL   |      | BTREE      |         |               |  
+-----------+------------+---------------+--------------+---------------+-----------+-------------+----------+--------+------+------------+---------+---------------+  
5 rows in set (0.02 sec)  

Il existe un index sur last_name mais l'optimiseur ne l'utilise pas.
Moi aussi:

mysql> explain select last_name from employees force index(idx_last_name) order by last_name;  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows  | Extra          |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 10031 | Using filesort |  
+----+-------------+-----------+------+---------------+------+---------+------+-------+----------------+  
1 row in set (0.00 sec)  

Mais encore l'indice est pas utilisé! Qu'est-ce que je fais mal ici?
Cela a-t-il à voir avec le fait que l'indice est NON_UNIQUE? BTW le nom estVARCHAR(1000)

Mise à jour demandée par @RolandoMySQLDBA

mysql> SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;  
+---------------+  
| DistinctCount |  
+---------------+  
|         10000 |  
+---------------+  
1 row in set (0.05 sec)  


mysql> SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;  
+----------+  
| COUNT(1) |  
+----------+  
|        0 |  
+----------+  
1 row in set (0.15 sec)  

Veuillez exécuter ces deux requêtes: 1) SELECT COUNT(DISTINCT last_name) DistinctCount FROM employees;2) SELECT COUNT(1) FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A;. Quel est le résultat de chaque comptage?
RolandoMySQLDBA

@RolandoMySQLDBA: J'ai mis à jour OP avec les informations que vous avez demandées.
Cratyle

Deux autres requêtes, s'il vous plaît: 1) SELECT COUNT(1) FullTableCount FROM employees;et 2) SELECT * FROM (SELECT COUNT(1) Count500,last_name FROM employees GROUP BY last_name HAVING COUNT(1) > 500) A LIMIT 10;.
RolandoMySQLDBA

Tant pis, je vois l'expliquer avec ce dont j'ai besoin.
RolandoMySQLDBA

2
@Cratylus vous avez accepté une mauvaise réponse, vous devez accepter la bonne réponse de Michael-sqlbot
miracle173

Réponses:


6

PROBLÈME # 1

Regardez la requête

select last_name from employees order by last_name;

Je ne vois pas de clause WHERE significative, pas plus que MySQL Query Optimizer. Il n'y a aucune incitation à utiliser un indice.

PROBLÈME # 2

Regardez la requête

select last_name from employees force index(idx_last_name) order by last_name; 

Vous lui avez donné un index, mais le Query Opitmizer a pris le relais. J'ai déjà vu ce comportement ( Comment puis-je forcer un JOIN à utiliser un index spécifique dans MySQL? )

Pourquoi cela devrait-il arriver?

Sans WHEREclause, Query Optimizer se dit ce qui suit:

  • Ceci est une table InnoDB
  • C'est une colonne indexée
  • L'index a le row_id de gen_clust_index (aka Clustered Index)
  • Pourquoi devrais-je regarder l'index quand
    • il n'y a pas de WHEREclause?
    • Il faudrait toujours que je rebondisse sur la table?
  • Étant donné que toutes les lignes d'une table InnoDB résident dans les mêmes blocs de 16 Ko que gen_clust_index, je vais plutôt effectuer une analyse complète de la table.

L'optimiseur de requête a choisi le chemin de moindre résistance.

Vous allez être sous le choc, mais ça y est: saviez-vous que l'Optimiseur de requête traitera MyISAM de manière très différente?

Vous dites probablement HUH ???? COMMENT ????

MyISAM stocke les données dans un .MYDfichier et tous les index dans le .MYIfichier.

La même requête produira un plan EXPLAIN différent car l'index réside dans un fichier différent des données. Pourquoi ? Voici pourquoi:

  • Les données nécessaires ( last_namecolonne) sont déjà commandées dans le.MYI
  • Dans le pire des cas, vous aurez un scan d'index complet
  • Vous n'accéderez à la colonne qu'à last_namepartir de l'index
  • Vous n'avez pas besoin de passer au crible les éléments indésirables
  • Vous ne déclencherez pas la création de fichiers temporaires pour le tri

Comment en être si sûr? J'ai testé cette théorie de travail sur la façon dont l'utilisation d'un stockage différent générera un plan EXPLAIN différent (parfois un meilleur): un index doit-il couvrir toutes les colonnes sélectionnées pour qu'il puisse être utilisé pour ORDER BY?


1
-1 @Rolando cette réponse n'est pas moins précise que la bonne réponse de Michael-sqlbot mais elle est fausse, par exemple le manuel dit: "MySQL utilise des index pour ces opérations: (...) Pour trier ou grouper une table si le tri ou le regroupement se fait sur le préfixe le plus à gauche d'un index utilisable (...) ". Certaines des autres déclarations de votre message sont également contestables. Je vous recommande de supprimer cette réponse ou de la retravailler.
miracle173

Cette réponse n'est pas correcte. Un index peut toujours être utilisé même s'il n'y a pas de clause WHERE s'il évite le tri.
2017 à 7h44

19

En fait, le problème ici est que cela ressemble à un index de préfixe. Je ne vois pas la définition du tableau dans la question, mais sub_part= 700? Vous n'avez pas indexé la colonne entière, donc l'index ne peut pas être utilisé pour le tri et n'est pas utile non plus comme index de couverture. Il ne pouvait être utilisé que pour trouver les lignes qui "pouvaient" correspondre à a WHEREet la couche serveur (au-dessus du moteur de stockage) devrait filtrer davantage les lignes correspondantes. Avez-vous vraiment besoin de 1000 caractères pour un nom de famille?


mise à jour pour illustrer: J'ai une table de test de table avec un peu plus de 500 lignes, chacune avec le nom de domaine d'un site Web dans une colonne domain_name VARCHAR(254) NOT NULLet sans index.

mysql> alter table keydemo add key(domain_name);
Query OK, 0 rows affected (0.17 sec)
Records: 0  Duplicates: 0  Warnings: 0

Avec la colonne complète indexée, la requête utilise l'index:

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
| id | select_type | table   | type  | possible_keys | key         | key_len | ref  | rows | Extra       |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
|  1 | SIMPLE      | keydemo | index | NULL          | domain_name | 764     | NULL |  541 | Using index |
+----+-------------+---------+-------+---------------+-------------+---------+------+------+-------------+
1 row in set (0.01 sec)

Donc, maintenant, je vais supprimer cet index et simplement indexer les 200 premiers caractères de nom_domaine.

mysql> alter table keydemo drop key domain_name;
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table keydemo add key(domain_name(200));
Query OK, 0 rows affected (0.08 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select domain_name from keydemo order by domain_name;
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table   | type | possible_keys | key  | key_len | ref  | rows | Extra          |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
|  1 | SIMPLE      | keydemo | ALL  | NULL          | NULL | NULL    | NULL |  541 | Using filesort |
+----+-------------+---------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

mysql>

Voila.

Notez également que l'index, à 200 caractères, est plus long que la valeur la plus longue de la colonne ...

mysql> select max(length(domain_name)) from keydemo;
+--------------------------+
| max(length(domain_name)) |
+--------------------------+
|                       43 |
+--------------------------+
1 row in set (0.04 sec)

... mais cela ne fait aucune différence. Un index déclaré avec une longueur de préfixe ne peut être utilisé que pour les recherches, pas pour le tri et pas comme index de couverture, car il ne contient pas la valeur de colonne complète, par définition.

En outre, les requêtes ci-dessus ont été exécutées sur une table InnoDB, mais leur exécution sur une table MyISAM donne des résultats pratiquement identiques. La seule différence dans ce cas est que le nombre d'InnoDB pour rowsest légèrement désactivé (541) tandis que MyISAM affiche le nombre exact de lignes (563), ce qui est un comportement normal car les deux moteurs de stockage gèrent les plongées d'index très différemment.

J'affirmerais toujours que la colonne last_name est probablement plus grande que nécessaire, mais il est toujours possible d'indexer la colonne entière, si vous utilisez InnoDB et exécutez MySQL 5.5 ou 5.6:

Par défaut, une clé d'index pour un index à colonne unique peut aller jusqu'à 767 octets. La même limite de longueur s'applique à tout préfixe de clé d'index. Voir Section 13.1.13, « CREATE INDEXSyntaxe». Par exemple, vous pouvez atteindre cette limite avec un index de préfixe de colonne de plus de 255 caractères sur une colonne TEXTor VARCHAR, en supposant un UTF-8jeu de caractères et un maximum de 3 octets pour chaque caractère. Lorsque l' innodb_large_prefixoption de configuration est activée, cette limite de longueur est augmentée à 3072 octets, pour les InnoDBtables qui utilisent les formats de ligne DYNAMICet COMPRESSED.

- http://dev.mysql.com/doc/refman/5.5/en/innodb-restrictions.html


Point de vue intéressant. La colonne est , varchar(1000)mais cela dépasse le maximum autorisé pour l' indice qui est ~ 750
Cratyle

8
Cette réponse doit être acceptée.
ypercubeᵀᴹ

1
@ypercube Cette réponse est plus précise que la mienne. +1 pour votre commentaire et +1 pour cette réponse. Puisse cela être accepté à la place sur le mien.
RolandoMySQLDBA

1
@Timo, c'est une question intéressante ... que je suggérerais de poster comme nouvelle question, ici, peut-être avec un lien vers cette réponse, pour le contexte. Publiez la sortie complète de EXPLAIN SELECT ..., ainsi que SHOW CREATE TABLE ...et SELECT @@VERSION;puisque les modifications apportées à l'optimiseur entre les versions peuvent être pertinentes.
Michael - sqlbot

1
À présent, je peux signaler que (au moins pour 5.7) un index de préfixe n'aide pas à l'indexation nulle, comme je l'ai demandé dans mon commentaire ci-dessus.
Timo

2

J'ai fait une réponse car un commentaire ne prend pas en charge le formatage et RolandoMySQL DBA a parlé de gen_clust_index et innodb. Et cela est très important sur une table basée sur innodb. Cela va plus loin que la connaissance DBA normale, car vous devez être en mesure d'analyser le code C.

Vous devez TOUJOURS TOUJOURS créer une CLÉ PRIMAIRE ou une CLÉ UNIQUE si vous utilisez Innodb. Si vous ne le faites pas, Innodb utilisera son propre ROW_ID généré qui pourrait vous faire plus de mal que de bien.

Je vais essayer de l'expliquer facilement car la preuve est basée sur le code C.

/**********************************************************************//**
Returns a new row id.
@return the new id */
UNIV_INLINE
row_id_t
dict_sys_get_new_row_id(void)
/*=========================*/
{
    row_id_t    id;

    mutex_enter(&(dict_sys->mutex));

    id = dict_sys->row_id;

    if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
          dict_hdr_flush_row_id();
    }

    dict_sys->row_id++;
    mutex_exit(&(dict_sys->mutex));
    return(id);
}

Premier problème

mutex_enter (& (dict_sys-> mutex));

Cette ligne garantit qu'un seul thread peut accéder à dict_sys-> mutex en même temps. Et si la valeur était déjà mutée ... oui un thread doit attendre donc vous obtenez quelque chose comme une fonctionnalité aléatoire sympa comme le verrouillage de thread ou si vous avez plus de tables sans votre propre CLÉ PRIMAIRE ou CLÉ UNIQUE alors vous auriez une fonctionnalité intéressante avec Innodb ' verrouillage de table ' n'est-ce pas la raison pour laquelle MyISAM a été remplacé par InnoDB parce que cette fonctionnalité intéressante appelée verrouillage basé sur les enregistrements / lignes ..

Deuxième problème

(0 == (id% DICT_HDR_ROW_ID_WRITE_MARGIN))

les calculs modulo (%) sont lents, pas bons si vous insérez par lot car il doit être recalculé à chaque fois ..., et parce que DICT_HDR_ROW_ID_WRITE_MARGIN (valeur 256) est une puissance de deux, cela pourrait être fait beaucoup plus rapidement ..

(0 == (id & (DICT_HDR_ROW_ID_WRITE_MARGIN - 1))))

Note latérale si le compilateur C a été configuré pour optimiser et que c'est un bon optimiseur, l'optimiseur C corrigera le code "lourd" à la version plus légère

devise de l'histoire toujours créer votre propre CLÉ PRIMAIRE ou assurez-vous d'avoir un index UNIQUE lorsque vous créez une table depuis le début


Ajoutez la réplication basée sur les lignes et le fait que les ID de ligne ne sont pas cohérents entre les serveurs, et l'argument de Raymond à propos de toujours créer une clé primaire est encore plus important.

Veuillez ne pas suggérer que cela UNIQUEsoit suffisant - il doit également inclure uniquement des colonnes non NULL pour que l'index unique soit promu en PK.
Rick James

"Les calculs modulo (%) sont lents" - Le plus important est le pourcentage du temps INSERTconsacré à cette fonction. Je soupçonne que c'est insignifiant. Contrastez l'effort de pelleter les colonnes, effectuez des opérations BTree, y compris un fractionnement de bloc occasionnel, divers mutex sur le pool de tampons, des éléments de tampon de changement, etc.
Rick James

Vrai @RickJames, les frais généraux peuvent être très petits, mais de nombreux petits nombres s'additionnent également (ce serait encore une micro-optimisation). En plus, le premier problème est le plus problématique pour certains
Raymond Nijland
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.