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?
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.
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.
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".
Vous pouvez utiliser l'influence MySQL pour utiliser les touches [~ # ~] [~ # ~] ou [~ # ~ ] index [~ # ~]
For
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.
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;)
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)