5
votes

Requête MySQL Slow JOIN lors de l'utilisation de ORDERBY

J'ai un problème avec cette requête:

1   SIMPLE  s   const   PRIMARY PRIMARY 4   const   1   Using index; Using temporary; Using filesort
1   SIMPLE  str ref PRIMARY,ressort_id  PRIMARY 4   const   13  Using index
1   SIMPLE  atr ref PRIMARY,article_id  PRIMARY 4   com.nps.lvz-prod.str.ressort_id 1262    Using index
1   SIMPLE  a   eq_ref  PRIMARY PRIMARY 4   com.nps.lvz-prod.atr.article_id 1   

Celle-ci est vraiment lente, elle prend parfois 14 secondes.

EXPLIQUEZ le montrer:

1   SIMPLE  s   const   PRIMARY PRIMARY 4   const   1   Using index; Using temporary; Using filesort
1   SIMPLE  str ref PRIMARY,ressort_id  PRIMARY 4   const   1   Using index
1   SIMPLE  atr ref PRIMARY,article_id  PRIMARY 4   com.nps.lvz-prod.str.ressort_id 1262    Using index
1   SIMPLE  a   eq_ref  PRIMARY PRIMARY 4   com.nps.lvz-prod.atr.article_id 1   

donc le dernier type "tout" est vraiment mauvais. Mais j'ai déjà essayé de forcer l'utilisation de l'index, sans succès.

Le tableau des articles ressemble à ceci:

CREATE TABLE `article` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`node_id` varchar(255) NOT NULL DEFAULT '',
`object_id` varchar(255) DEFAULT NULL,
`headline_1` varchar(255) NOT NULL DEFAULT '',
`created_at` datetime(3) NOT NULL,
`updated_at` datetime(3) NOT NULL,
`teaser_text` longtext NOT NULL,
`content_text` longtext NOT NULL,
PRIMARY KEY (`id`),
KEY `article_nodeid` (`node_id`),
KEY `article_objectid` (`object_id`),
KEY `source_created` (`created_at`)
) ENGINE=InnoDB AUTO_INCREMENT=161116 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

Lorsque je supprime l'index FORCE, l'explication s'améliore, mais la requête est toujours lente.

Expliquer sans index de force:

1   SIMPLE  s   const   PRIMARY PRIMARY 4   const   1   Using index; Using temporary; Using filesort
1   SIMPLE  str ref PRIMARY,ressort_id  PRIMARY 4   const   1   Using index
1   SIMPLE  atr ref PRIMARY,article_id  PRIMARY 4   com.nps.lvz-prod.str.ressort_id 1262    Using index
1   SIMPLE  a   ALL NULL    NULL    NULL    NULL    146677  Using where; Using join buffer (flat, BNL join)

Et pour un autre smartressort id (3), cela ressemble à ceci:

SELECT a.*
FROM smartressort AS s
JOIN smartressort_to_ressort AS str
    ON s.id = str.smartressort_id
JOIN article_to_ressort AS atr
    ON str.ressort_id = atr.ressort_id
JOIN article AS a FORCE INDEX (source_created)
    ON atr.article_id = a.id    
WHERE
    s.id = 1
ORDER BY
    a.created_at DESC
LIMIT 25;

Ici, nous avons 13 Ressorts pour un Smartressort. Lignes: 1x1x13x1262x1 = 16.406

1) Que puis-je faire pour accélérer cette demande?

2) Quel est le problème avec l'index source_created ? P >


10 commentaires

Vous avez omis peut-être la partie la plus critique, qui est la définition de l'index source_created . Veuillez inclure cela.


@Cid Je ne pense pas que cela ferait généralement une grande différence.


Forcer l'index source_created est inutile, vous n'utilisez pas la colonne indexée dans la jointure, seulement après.


@Cid La clause WHERE est appliquée avant la jointure se produit, pas après :-)


@TimBiegeleisen haha ​​alors je me suis trompé, merci de l'avoir signalé, ce soir, je vais dormir un peu moins bête :)


"L'indication FORCE INDEX agit comme USE INDEX (index_list), avec l'ajout qu'une analyse de table est supposée être très coûteuse. En d'autres termes, une analyse de table n'est utilisée que s'il n'y a aucun moyen d'utiliser l'un des index nommés pour trouver des lignes dans le tableau. " À mon avis, dans la requête, mais ne peut pas utiliser uniquement l'index nommé (source_created), il effectue une analyse complète. Pouvez-vous essayer avec la clé primaire et nous montrer le résultat?


@TimBiegeleisen le source_created est ici: KEY source_created (created_at) donc c'est juste un index pour created_at


@DanielE. forcer la clé primaire car l'index est également très lent. Le problème principal est la commande. La requête sans la commande par ne prend que ~ 35 ms.


Avez-vous résolu le problème? quelle était la solution?


En plus des réponses données , suivez les conseils sur de nombreux: nombreux tableaux ici .


5 Réponses :


4
votes

Le SELECT * que vous avez dans votre requête est moche, et cela peut souvent être un tueur d'index. Cela peut empêcher l'utilisation d'un index, car la plupart des index que vous définiriez ne couvriraient pas toutes les colonnes demandées par SELECT * . L'approche de cette réponse est d'indexer toutes les autres tables de votre requête, ce qui inciterait donc MySQL à ne faire qu'une seule analyse de la table article .

CREATE INDEX idx1 ON article_to_ressort (article_id, ressort_id);
CREATE INDEX idx2 ON smartressort_to_ressort (ressort_id, smartressort_id);

Ces deux indices devraient accélérer le processus de jonction. Notez que je n'ai pas défini d'index pour la table smartressort , en supposant que sa colonne id est déjà une clé primaire. J'écrirais probablement votre requête en commençant par la table article et en la rejoignant vers l'extérieur, mais cela ne devrait pas vraiment avoir d'importance.

De plus, forcer un index est généralement une mauvaise idée ou non nécessaire. L'optimiseur peut généralement déterminer quand il est préférable d'utiliser un index.


4 commentaires

Notez que SELECT a. * n'est pas si mal dans ce cas particulier, OP a besoin de toutes les données, donc la ligne doit être récupérée de toute façon - en général vous avez raison, il y a des cas où toutes les données pourraient être servi à partir d'un index - de la même manière que vos nouveaux index.


@gaborsch Eh bien, MySQL doit scanner quelque chose. S'il peut trouver un moyen efficace d'analyser la table article , en gérant les jointures, alors il devrait le faire.


se mettre d'accord. Peut-être qu'un index sur article , contenant id et created_at aiderait le ORDER BY , évitant d'avoir à accéder à tous les lignes pour le tri?


Le fait est que l'index que vous décrivez ne couvrirait pas toutes les colonnes nécessaires à SELECT a. * , il n'est donc pas clair si MySQL choisirait de l'utiliser.



2
votes

CHOISIR beaucoup de colonnes DANS les tables ORDER PAR quelque chose LIMIT quelques est un anti-modèle de performance notoire; il doit récupérer et ordonner tout un désordre de lignes et de colonnes, juste pour supprimer toutes les lignes sauf quelques lignes du jeu de résultats.

L'astuce consiste à déterminer les valeurs de article.id dont vous avez besoin dans votre jeu de résultats, puis à récupérer uniquement ces valeurs. Cela s'appelle une jointure différée .

Cela devrait vous donner cet ensemble de valeurs id . Il n'est probablement pas nécessaire de rejoindre la table smartressort car smartressort_to_ressort contient les valeurs id dont vous avez besoin.

SELECT a.*
  FROM article a
 WHERE a.id IN (
                 SELECT a.id
                   FROM article a
                   JOIN article_to_ressort atr ON a.id = atr.article_id
                   JOIN smartressort_to_ressort str ON atr.ressort_id = str.ressort_id
                  WHERE str.smartressort_id = 1
                  ORDER BY a.created_at DESC
                  LIMIT 25
               )
 ORDER BY a.created_at DESC


3 commentaires

@Macx idéalement, l'optimiseur devrait d'abord accéder à la table a.created_at parce que vous commandez par là-dessus, donc fondamentalement O.Jones oblige l'optimiseur MySQL à un meilleur plan ici .. MySQL 8.0 optimiserait automatiquement votre requête et éviterait "Utilisation temporaire" et fonctionnerait très bien


je ne peux pas utiliser la limite dans la sous-requête. Mais cela fonctionne et est beaucoup plus rapide: SELECT ar. * FROM article ar WHERE ar.id IN (SELECT atr.article_id from article_to_ressort atr JOIN smartressort_to_ressort str ON atr.ressort_id = str.ressort_id WHERE str.smartressort_id = 2) ORDER BY ar.created_at DESC LIMIT 25;


je dois également ajouter: CREATE INDEX idx3 ON article (id, created_at)



0
votes

En plus de la bonne réponse de @TimBiegelsen, je recommanderais de modifier votre index source_created :

...
KEY `source_created` (`id`, `created_at`)

Le gain serait que MySQL pourrait l'utiliser pour tri, et n'aurait pas besoin de récupérer les 16406 lignes. Cela peut aider ou non, mais cela vaut la peine d'essayer (peut-être avec une déclaration explicite pour l'utiliser)


0 commentaires

0
votes

Pour commencer: vous pouvez supprimer la table smartressort de votre requête, car elle n'y ajoute rien.

Voici votre requête réécrite. Nous voulons tous les ressorts pour smart ressort # 1 puis tous les articles pour ces ressorts. Parmi ceux-ci, nous montrons le plus récent 25.

CREATE INDEX idx1 ON smartressort_to_ressort (smartressort_id, ressort_id);
CREATE INDEX idx2 ON article_to_ressort (ressort_id, article_id);
CREATE INDEX idx3 ON article (id, created_at);

Maintenant, quels index seraient nécessaires pour aider le SGBD avec cela? Commencez par la table interne ( smartressort_to_ressort ). Nous accédons à tous les enregistrements avec un smartressort_id donné et nous voulons obtenir le ressort_id associé. L'index doit donc contenir ces deux colonnes dans cet ordre. Idem pour article_to_ressort et son ressort_id et article_id . Enfin, nous voulons sélectionner les articles en fonction des identifiants des articles trouvés et les classer par created_at .

SELECT *
FROM article
WHERE id IN
(
  SELECT article_id
  FROM article_to_ressort 
  WHERE ressort_id IN
  (
    SELECT ressort_id
    FROM smartressort_to_ressort
    WHERE smartressort_id = 1
  )
)
ORDER BY created_at DESC
LIMIT 25;

Quoi qu'il en soit, ces index ne sont qu'une offre à le SGBD. Il peut se prononcer contre eux. Cela est particulièrement vrai pour l'index de la table article . À combien de lignes le SGBD s'attend-il à accéder pour un smartressort_id , c'est-à-dire combien de lignes peuvent figurer dans la clause IN ? Si le SGBD pense que cela pourrait bien représenter environ 10% de tous les ID d'article, il peut déjà décider de plutôt lire le tableau séquentiellement plutôt que de se frayer un chemin dans l'index pour autant de lignes.


1 commentaires

C'est beaucoup plus rapide (~ 35 ms). la requête explicative montre que seul le idx3 est utilisé. lignes montre 1262 x 1 x 1 x 1262.



0
votes

Donc pour moi, la solution était la suivante:

1   PRIMARY s   const   PRIMARY PRIMARY 4   const   1   Using index
1   PRIMARY a   index   NULL    source_created  7   NULL    25  
1   PRIMARY str ref PRIMARY,ressort_id,idx1 PRIMARY 4   const   1   Using index
1   PRIMARY atr eq_ref  PRIMARY,article_id,idx2 PRIMARY 8   com.nps.lvz-prod.str.ressort_id,com.nps.lvz-prod.a.id   1   Using index; FirstMatch(a)

Cela ne nécessite que ~ 35 ms. Explain ressemble à ceci:

explain SELECT a.*, NOW()
FROM article as a  USE INDEX (source_created)
where a.id in (SELECT atr.article_id
    FROM smartressort AS s
    JOIN smartressort_to_ressort AS str
    ON s.id = str.smartressort_id
    JOIN article_to_ressort AS atr
    ON str.ressort_id = atr.ressort_id
    WHERE s.id = 1
) 
ORDER BY a.created_at DESC
LIMIT 25;

Malgré tout, cette requête Explain me convient mieux, mais je ne sais pas pourquoi exactement:

1   PRIMARY a   index   NULL    source_created  7   NULL    1   
1   PRIMARY <subquery2> eq_ref  distinct_key    distinct_key    4   func    1
2   MATERIALIZED    str ref PRIMARY,ressort_id,idx1 PRIMARY 4   const   1   Using index
2   MATERIALIZED    atr ref PRIMARY,article_id,idx2 PRIMARY 4   com.nps.lvz-prod.str.ressort_id 1262    Using index


1 commentaires

Puisque cela indique MATERIALIZED , vous pouvez tester avec une version différente.