Pourquoi mysql utilise-t-il le mauvais index pour la commande par requête?

9

Voici ma table avec ~ 10 000 000 de données de lignes

CREATE TABLE `votes` (
  `subject_name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `subject_id` int(11) NOT NULL,
  `voter_id` int(11) NOT NULL,
  `rate` int(11) NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`subject_name`,`subject_id`,`voter_id`),
  KEY `IDX_518B7ACFEBB4B8AD` (`voter_id`),
  KEY `subject_timestamp` (`subject_name`,`subject_id`,`updated_at`),
  KEY `voter_timestamp` (`voter_id`,`updated_at`),
  CONSTRAINT `FK_518B7ACFEBB4B8AD` FOREIGN KEY (`voter_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

Voici les index cardinalités

entrez la description de l'image ici

Donc, quand je fais cette requête:

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Je m'attendais à ce qu'il utilise index voter_timestamp mais mysql choisit de l'utiliser à la place:

explain select SQL_NO_CACHE * from votes  where subject_name = 'medium' and voter_id = 1001 and rate = 1 order by updated_at desc limit 20 offset 100;`

type:
    index_merge
possible_keys: 
    PRIMARY,IDX_518B7ACFEBB4B8AD,subject_timestamp,voter_timestamp
key:
    IDX_518B7ACFEBB4B8AD,PRIMARY
key_len:
    102,98
ref:
    NULL
rows:
    9255
filtered:
    10.00
Extra:
    Using intersect(IDX_518B7ACFEBB4B8AD,PRIMARY); Using where; Using filesort

Et j'ai eu un temps de requête de 200 à 400 ms.

Si je le force à utiliser le bon index comme:

SELECT SQL_NO_CACHE * FROM votes USE INDEX (voter_timestamp) WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

Mysql peut retourner les résultats en 1-2 ms

et voici l'expliquer:

type:
    ref
possible_keys:
    voter_timestamp
key:
    voter_timestamp
key_len:
    4
ref:
    const
rows:
    18714
filtered:
    1.00
Extra:
    Using where

Alors pourquoi mysql n'a-t-il pas choisi l' voter_timestampindex pour ma requête d'origine?

Ce que j'avais essayé analyze table votes, optimize table votesc'est de supprimer cet index et de l'ajouter à nouveau, mais mysql utilise toujours le mauvais index. ne sais pas vraiment quel est le problème.

Phénix
la source
1
@ ypercubeᵀᴹ Je ne pense pas qu'il soit nécessaire d'indexer toutes les colonnes dans la condition where, comme vous voyez si je force à utiliser l'index (voter_id, updated_at), il peut l'utiliser et être très efficace. Si je retire la subject_name = "medium"pièce, elle peut également choisir le bon index, pas besoin d'indexerrate
Phoenix
Pourtant, l'indice à 4 colonnes sera plus efficace que le 2 (voter_id, updated_at). Un autre indice serait (voter_id, subject_name, updated_at)ou (subject_name, voter_id, updated_at)(sans le taux).
ypercubeᵀᴹ
1
Et oui, vous avez - à un moment donné - raison. Vous n'avez pas besoin de l'index à 4 colonnes. C'est juste le meilleur index possible pour cette requête. Les 2 colonnes (que vous pensez être "correctes") conviennent peut-être aux données et à la distribution dont vous disposez actuellement. Avec une distribution différente, cela pourrait être horrible. Exemple: supposons que 99% des lignes aient un taux> 1 et seulement 1% un taux = 1. Pensez-vous que l'utilisation de l'index à 2 colonnes serait efficace?
ypercubeᵀᴹ
Il devrait parcourir une grande partie de l'index et effectuer des milliers de recherches sur la table, pour trouver ce taux> 1 et rejeter les lignes, jusqu'à ce qu'il en trouve 120 qui correspondent aux critères qui ne peuvent pas être jugés par l'index ( subject_name='medium' and rate=1)
ypercubeᵀᴹ
ypercube, Phoenix - MySQL n'atteindra pas le LIMITni même le ORDER BYsauf si l'index satisfait d'abord tout le filtrage. Autrement dit, sans les 4 colonnes complètes, il collectera toutes les lignes pertinentes, les triera toutes, puis sélectionnera le LIMIT. Avec l'index à 4 colonnes, la requête peut éviter le tri et s'arrêter après avoir lu uniquement les LIMITlignes.
Rick James

Réponses:

5

MySQL utilise un modèle de coût relativement simple (plus simple que les autres SGBDR) pour planifier les requêtes dans lesquelles le filtrage de votre ensemble de données a une priorité assez élevée. Dans votre première requête avec l'index de fusion, il est estimé que la numérisation ~ 9000 lignes va être nécessaire tandis que la seconde avec l'index d'index exigera 18000. Je parie que cela pèse suffisamment dans le calcul pour déplacer l'échelle vers la fusion. . Vous pouvez le confirmer (ou trouver d'autres raisons) en activant optimizer_trace, exécutez votre requête et évaluez les résultats.

set global optimizer_trace='enabled=on';

-- run your query 

SELECT SQL_NO_CACHE * FROM votes WHERE 
    voter_id = 1099 AND 
    rate = 1 AND 
    subject_name = 'medium'
ORDER BY updated_at DESC
LIMIT 20 OFFSET 100;

select * from information_schema.`OPTIMIZER_TRACE`;

Une remarque sur index_merge: dans la plupart des cas, vous constaterez que c'est assez cher. Bien que très utile pour les scénarios de type OLAP, il peut ne pas être très bien adapté à OLTP car l'opération peut prendre un temps considérable de votre requête et comme vous pouvez le voir parfois, le plan d'exécution sous-optimal est en fait plus rapide.

Heureusement, MySQL fournit des commutateurs pour l'optimiseur afin que vous puissiez le personnaliser à votre guise.

Pour toutes les options que vous pouvez exécuter:

show global variables like 'optimizer_switch';

Pour en changer un, vous n'avez pas besoin de copier-coller toute la chaîne. Cela fonctionne comme dict.update()en python.

 set global optimizer_switch='index_merge=off';

Si possible, je voudrais également jeter un œil à la structure de votre table et m'améliorer. Il n'est pas vraiment conseillé d'avoir une clé primaire de ~ 100 octets avec de nombreuses clés secondaires.

Vous avez quatre clés secondaires et certaines d'entre elles sont superflues, par exemple, l' (voter_id)index est un sous-ensemble de(voter_id, updated_at)

Károly Nagy
la source
"Index merge intersect" est rarement utilisé par MySQL. Dans tous les cas peut-être, il est nettement préférable d'avoir un index avec plus de colonnes. "Index merge union" est parfois utile; se transformer ORen UNIONest souvent aussi bon ou meilleur.
Rick James
5

Pour cette requête, vous avez besoin de cet index:

INDEX(voter_id, rate, subject_name, updated_at)

Le updated_atdoit être le dernier; les trois autres peuvent être dans n'importe quel ordre. (Les index à 3 colonnes de ypercube ne sont pas très utiles car ils ne terminent pas les WHEREcolonnes avant de frapper la ORDER BYcolonne.)

Lorsque vous ajoutez cet index, vous pouvez probablement vous débarrasser de toutes les autres clés secondaires:

KEY IDX_518B7ACFEBB4B8AD( voter_id), - Le FK peut utiliser mon index de clé subject_timestamp( subject_name, subject_id, updated_at), - KEY essentiellement redondante voter_timestamp( voter_id, updated_at), - peut avoir été votre tentative

Avec l'index à 4 colonnes, vous avez une chance d'optimiser la "pagination" et d'éviter OFFSET. Voir ce blog.

Sur un autre sujet ... Quand je vois X_nameet X_id, je suppose que la "normalisation" est en cours. Je m'attendrais à voir ces deux colonnes dans un tableau, avec pratiquement rien d'autre. Je ne m'attendrais pas à voir les deux dans un autre tableau.

(voter_id, updated_at)ne passera pas voter_idcar il n'a pas fini de filtrer (le WHERE). Puis, comme l'autre index est plus petit, il est choisi. Le mien a 3 colonnes pour s'occuper du filtrage, puis la colonne pour ORDER BY.

Rick James
la source