web-dev-qa-db-fra.com

Comment rendre l'index d'utilisation des requêtes JOIN?

J'ai deux tables:

CREATE TABLE `articles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(1000) DEFAULT NULL,
  `last_updated` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `last_updated` (`last_updated`),
) ENGINE=InnoDB AUTO_INCREMENT=799681 DEFAULT CHARSET=utf8 

CREATE TABLE `article_categories` (
  `article_id` int(11) NOT NULL DEFAULT '0',
  `category_id` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`article_id`,`category_id`),
  KEY `category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |

Voici ma requête:

SELECT a.*
FROM
    articles AS a,
    article_categories AS c
WHERE
    a.id = c.article_id
    AND c.category_id = 78
    AND a.comment_cnt > 0
    AND a.deleted = 0
ORDER BY a.last_updated
LIMIT 100, 20

Et un EXPLAIN pour cela:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
         type: index
possible_keys: PRIMARY
          key: last_updated
      key_len: 9
          ref: NULL
         rows: 2040
        Extra: Using where
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: c
         type: eq_ref
possible_keys: PRIMARY,fandom_id
          key: PRIMARY
      key_len: 8
          ref: db.a.id,const
         rows: 1
        Extra: Using index

Il utilise un balayage d'index complet de last_updated sur la première table pour le tri, mais n'utilise pas d'index y pour la jointure (type: index dans expliquer). C'est très mauvais pour les performances et tue tout le serveur de base de données, car il s'agit d'une requête très fréquente.

J'ai essayé d'inverser l'ordre des tables avec STRAIGHT_JOIN, mais cela donne filesort, using_temporary, ce qui est encore pire.

Existe-t-il un moyen de faire en sorte que mysql utilise l'index pour la jointure et le tri en même temps?

=== mise à jour ===

Je suis vraiment désespéré là-dedans. Peut-être qu'une sorte de dénormalisation peut aider ici?

18
Silver Light

Si vous avez beaucoup de catégories, cette requête ne peut pas être rendue efficace. Aucun index ne peut couvrir deux tables à la fois dans MySQL.

Vous devez faire une dénormalisation: ajoutez last_updated, has_comments et deleted dans article_categories:

CREATE TABLE `article_categories` (
  `article_id` int(11) NOT NULL DEFAULT '0',
  `category_id` int(11) NOT NULL DEFAULT '0',
  `last_updated` timestamp NOT NULL,
  `has_comments` boolean NOT NULL,
  `deleted` boolean NOT NULL,
  PRIMARY KEY (`article_id`,`category_id`),
  KEY `category_id` (`category_id`),
  KEY `ix_articlecategories_category_comments_deleted_updated` (category_id, has_comments, deleted, last_updated)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

et exécutez cette requête:

SELECT  *
FROM    (
        SELECT  article_id
        FROM    article_categories
        WHERE   (category_id, has_comments, deleted) = (78, 1, 0)
        ORDER BY
                last_updated DESC
        LIMIT   100, 20
        ) q
JOIN    articles a
ON      a.id = q.article_id

Bien sûr, vous devez mettre à jour article_categories également lorsque vous mettez à jour les colonnes pertinentes dans article. Cela peut être fait dans un déclencheur.

Notez que la colonne has_comments est booléen: cela permettra d'utiliser un prédicat d'égalité pour effectuer un balayage de plage unique sur l'index.

Notez également que LIMIT va dans la sous-requête. Cela fait que MySQL utilise des recherches de lignes tardives qu'il n'utilise pas par défaut. Voir cet article dans mon blog pour savoir pourquoi ils augmentent les performances:

Si vous étiez sur SQL Server, vous pourriez créer une vue indexable sur votre requête, ce qui ferait essentiellement une copie indexée dénormalisée de article_categories avec les champs supplémentaires, automatiquement maintenus par le serveur.

Malheureusement, MySQL ne prend pas cela en charge et vous devrez créer une telle table manuellement et écrire du code supplémentaire pour la synchroniser avec les tables de base.

16
Quassnoi

Avant d'accéder à votre requête spécifique, il est important de comprendre le fonctionnement d'un index.

Avec des statistiques appropriées, cette requête:

select * from foo where bar = 'bar'

... utilisera un index sur foo(bar) s'il est sélectif. Cela signifie que si bar = 'bar' Revient à sélectionner la plupart des lignes du tableau, il ira plus vite pour simplement lire le tableau et éliminer les lignes qui ne s'appliquent pas. En revanche, si bar = 'bar' Signifie uniquement sélectionner une poignée de lignes, la lecture de l'index est logique.

Supposons que nous lançions maintenant une clause d'ordre et que vous ayez des index sur chacun de foo(bar) et foo(baz):

select * from foo where bar = 'bar' order by baz

Si bar = 'bar' Est très sélectif, il est bon marché de récupérer toutes les lignes conformes et de les trier en mémoire. S'il n'est pas du tout sélectif, l'index sur foo(baz) n'a pas de sens car vous récupérerez de toute façon la table entière: l'utiliser signifierait aller et venir sur les pages du disque pour lire les lignes dans l'ordre, ce qui est très cher.

Ajoutez une clause limite, cependant, et foo(baz) pourrait soudainement avoir du sens:

select * from foo where bar = 'bar' order by baz limit 10

Si bar = 'bar' Est très sélectif, c'est toujours une bonne option. Si ce n'est pas du tout sélectif, vous trouverez rapidement 10 lignes correspondantes en scannant l'index sur foo(baz) - vous pouvez lire 10 lignes, ou 50, mais vous en trouverez 10 bonnes assez tôt.

Supposons que la dernière requête avec des index sur foo(bar, baz) et foo(baz, bar) à la place. Les index sont lus de gauche à droite. L'une est très logique pour cette requête potentielle, l'autre peut n'en faire aucune. Pensez à eux comme ceci:

bar   baz    baz   bar
---------    ---------
bad   aaa    aaa   bad
bad   bbb    aaa   bar
bar   aaa    bbb   bad
bar   bbb    bbb   bar

Comme vous pouvez le voir, l'index sur foo(bar, baz) permet de commencer la lecture à ('bar', 'aaa') Et de récupérer les lignes dans l'ordre à partir de ce point.

L'index sur foo(baz, bar), au contraire, produit des lignes triées par baz indépendamment de ce que bar peut contenir. Si bar = 'bar' N'est pas du tout sélectif comme critère, vous rencontrerez rapidement des lignes correspondantes pour votre requête, auquel cas il est judicieux de l'utiliser. Si c'est très sélectif, vous pouvez finir par itérer des gazillions de lignes avant d'en trouver suffisamment qui correspondent à bar = 'bar' - cela pourrait toujours être une bonne option, mais c'est aussi optimal.

Cela fait, revenons à votre requête d'origine ...

Vous devez joindre des articles avec des catégories, filtrer les articles qui se trouvent dans une catégorie particulière, avec plus d'un commentaire, qui ne sont pas supprimés, puis les trier par date, puis en saisir une poignée.

Je suppose que la plupart des articles ne sont pas supprimés, donc un index sur ces critères ne sera pas très utile - cela ne fera que ralentir les écritures et la planification des requêtes.

Je suppose que la plupart des articles ont un commentaire ou plus, donc ce ne sera pas non plus sélectif. C'est à dire. il n'y a pas non plus besoin de l'indexer.

Sans votre filtre de catégorie, les options d'index sont raisonnablement évidentes: articles(last_updated); éventuellement avec la colonne du nombre de commentaires à droite et l'indicateur supprimé à gauche.

Avec votre filtre de catégorie, tout dépend ...

Si votre filtre de catégorie est très sélectif, il est très judicieux de sélectionner toutes les lignes de cette catégorie, de les trier en mémoire et de sélectionner les premières lignes correspondantes.

Si votre filtre de catégorie n'est pas du tout sélectif et donne presque un article, l'index sur articles(last_update) a du sens: les lignes valides sont partout, alors lisez les lignes dans l'ordre jusqu'à ce que vous trouviez suffisamment de correspondance et voilà .

Dans le cas plus général, c'est juste vaguement sélectif. À ma connaissance, les statistiques collectées ne se penchent pas beaucoup sur les corrélations. Ainsi, le planificateur n'a aucun bon moyen d'estimer s'il trouvera assez rapidement les articles de la bonne catégorie pour mériter la lecture de ce dernier indice. La jonction et le tri en mémoire sont généralement moins chers, le planificateur va donc de pair avec cela.

Quoi qu'il en soit, vous avez deux options pour forcer l'utilisation d'un index.

L'une consiste à reconnaître que le planificateur de requêtes n'est pas parfait et à utiliser un indice:

http://dev.mysql.com/doc/refman/5.5/en/index-hints.html

Méfiez-vous cependant, car parfois le planificateur a en fait raison de ne pas vouloir utiliser l'index que vous souhaitez ou la version vice. En outre, cela pourrait devenir correct dans une future version de MySQL, alors gardez cela à l'esprit lorsque vous maintenez votre code au fil des ans.

Edit: STRAIGHT_JOIN, Comme le fait remarquer DRap fonctionne aussi, avec des mises en garde similaires.

L'autre consiste à conserver une colonne supplémentaire pour baliser les articles fréquemment sélectionnés (par exemple, un champ tinyint, qui est défini sur 1 lorsqu'ils appartiennent à votre catégorie spécifique), puis ajouter un index sur par exemple articles(cat_78, last_updated). Maintenez-le à l'aide d'un déclencheur et tout ira bien.

10

L'utilisation d'un indice non couvrant coûte cher. Pour chaque ligne, toutes les colonnes non couvertes doivent être extraites de la table de base, à l'aide de la clé primaire. Donc j'essaierais d'abord de faire l'index sur articles couvrant. Cela pourrait aider à convaincre l'optimiseur de requêtes MySQL que l'index est utile. Par exemple:

KEY IX_Articles_last_updated (last_updated, id, title, comment_cnt, deleted),

Si cela ne vous aide pas, vous pouvez jouer avec FORCE INDEX:

SELECT  a.*
FROM    article_categories AS c FORCE INDEX (IX_Articles_last_updated)
JOIN    articles AS a FORCE INDEX (PRIMARY)
ON      a.id = c.article_id
WHERE   c.category_id = 78
        AND a.comment_cnt > 0
        AND a.deleted = 0
ORDER BY 
        a.last_updated
LIMIT   100, 20

Le nom de l'index appliquant la clé primaire est toujours "primaire".

2
Andomar

Vous pouvez utiliser l'influence MySQL pour utiliser les touches [~ # ~] [~ # ~] ou [~ # ~ ] index [~ # ~]

For

  • Commande, ou
  • Regroupement, ou
  • Joindre

Pour plus d'informations, suivez ce lien . J'avais l'intention de l'utiliser pour joindre (c'est-à-dire USE INDEX FOR JOIN (My_Index) mais cela n'a pas fonctionné comme prévu. Supprimer le FOR JOIN part a accéléré considérablement ma requête, passant de plus de 3,5 heures à 1 à 2 secondes. Tout simplement parce que MySQL a été forcé d'utiliser le bon index.

2
Muhammad Gelbana

Tout d'abord, je recommanderais de lire l'article façons dont MySQL utilise les index .

Et maintenant, lorsque vous connaissez les bases, vous pouvez optimiser cette requête particulière.

MySQL ne peut pas utiliser d'index pour la commande, il peut simplement produire des données dans un ordre d'index. Étant donné que MySQL utilise des boucles imbriquées pour la jointure, le champ que vous souhaitez classer doit être dans la première table de la jointure (vous voyez l'ordre de la jointure dans les résultats EXPLAIN, et pouvez l'affecter en créant des index spécifiques et (si cela n'aide pas) ) en forçant les index requis).

Une autre chose importante est qu'avant de commander, vous récupérez toutes les colonnes pour toutes les lignes filtrées de la table a, puis vous ignorez probablement la plupart d'entre elles. Il est beaucoup plus efficace d'obtenir une liste des identifiants de ligne requis et de récupérer uniquement ces lignes.

Pour que cela fonctionne, vous aurez besoin d'un indice de couverture (deleted, comment_cnt, last_updated) sur la table a, et maintenant vous pouvez réécrire la requête comme suit:

SELECT *
FROM (
  SELECT a.id
  FROM articles AS a,
  JOIN article_categories AS c
    ON a.id = c.article_id AND c.category_id = 78
  WHERE a.comment_cnt > 0 AND a.deleted = 0
  ORDER BY a.last_updated
  LIMIT 100, 20
) as ids
JOIN articles USING (id);

P.S. La définition de votre table pour la table a ne contient pas comment_cnt colonne;)

1
newtover

Je disposerais des index suivants

table des articles - INDEX (supprimé, last_updated, comment_cnt)

table article_categories - INDEX (article_id, category_id) - vous avez déjà cet index

puis ajoutez Straight_Join pour forcer l'exécution de la requête comme indiqué au lieu d'essayer d'utiliser la table article_categories via toutes les statistiques dont elle peut disposer pour aider la requête.

SELECT STRAIGHT_JOIN
      a.*
   FROM
      articles AS a
         JOIN article_categories AS c
            ON a.id = c.article_id
            AND c.category_id = 78
   WHERE
          a.deleted = 0
      AND a.comment_cnt > 0
   ORDER BY 
      a.last_updated
   LIMIT 
      100, 20

Selon les commentaires/commentaires, j'envisagerais d'inverser en fonction de l'ensemble si les enregistrements de catégorie sont beaucoup plus petits ... comme

SELECT STRAIGHT_JOIN
      a.*
   FROM
      article_categories AS c
         JOIN articles as a
            ON c.article_id = a.id
           AND a.deleted = 0
           AND a.Comment_cnt > 0
   WHERE
      c.category_id = 78
   ORDER BY 
      a.last_updated
   LIMIT 
      100, 20

Dans ce cas, je garantirais un index sur le tableau des articles par

index - (id, supprimé, dernière mise à jour)

1
DRapp