web-dev-qa-db-fra.com

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

Je lance 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'index est pas utilisé! Qu'est-ce que je fais mal ici?
Cela a-t-il à voir avec le fait que l'index est NON_UNIQUE? BTW le nom de famille est VARCHAR(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)  
14
Cratylus

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 l'optimiseur de requêtes MySQL. 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 forcer un JOIN à utiliser un index spécifique dans MySQL? )

Pourquoi cela devrait-il arriver?

Sans clause WHERE, l'Optimiseur de requête se dit ce qui suit:

  • Ceci est une table InnoDB
  • C'est une colonne indexée
  • L'index a le row_id du gen_clust_index (a.k.a. Clustered Index)
  • Pourquoi devrais-je regarder l'index quand
    • il n'y a pas de clause WHERE?
    • 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 avoir un petit choc, mais ça y est: Saviez-vous que l'Optimiseur de requête va gérer MyISAM tout à fait différemment?

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

MyISAM stocke les données dans un .MYD fichier et tous les index du .MYI fichier.

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_name) est déjà commandé dans le .MYI
  • Dans le pire des cas, vous aurez un scan d'index complet
  • Vous n'accéderez qu'à la colonne last_name 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 va générer un plan EXPLAIN différent (parfois un meilleur): n index doit-il couvrir toutes les colonnes sélectionnées pour qu'il puisse être utilisé pour ORDER BY?

7
RolandoMySQLDBA

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 à un WHERE et 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 NULL et aucun 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 différence uniquement dans ce cas est que le nombre InnoDB pour rows est légèrement inférieur (541) tandis que MyISAM affiche le nombre exact de lignes (563) 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 INDEX Syntaxe". Par exemple, vous pouvez atteindre cette limite avec un index de préfixe de colonne de plus de 255 caractères sur une colonne TEXT ou VARCHAR, en supposant un jeu de caractères UTF-8 Et un maximum de 3 octets pour chaque caractère. Lorsque l'option de configuration innodb_large_prefix est activée, cette limite de longueur est augmentée à 3072 octets, pour les tables InnoDB qui utilisent les DYNAMIC et COMPRESSED formats de ligne.

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

19
Michael - sqlbot

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 déjà la valeur était mutée ... oui un thread doit attendre donc vous obtenez quelque chose comme une fonctionnalité aléatoire de Nice comme verrouillage de thread ou si vous avez plus de tables sans votre propre clé primaire ou clé unique alors vous auriez une fonctionnalité Nice avec innodb 'verrouillage de table' n'est-ce pas la raison pour laquelle MyISAM a été remplacé par InnoDB parce que la fonctionnalité Nice est 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 c'est un bon optimiseur, l'optimiseur C corrigera le code "lourd" à la version plus légère

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

2
Raymond Nijland