如何使JOIN查询使用索引?

时间:2013-05-05 14:45:19

标签: mysql sql join query-optimization

我有两张桌子:

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 |

这是我的疑问:

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

EXPLAIN为它:

*************************** 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

它在第一个表上使用last_updated的完整索引扫描进行排序,但不使用y索引进行连接(解释中为type: index)。这对性能非常不利并且会导致整个数据库服务器死亡,因为这是一个非常频繁的查询。

我已尝试使用STRAIGHT_JOIN撤消表格顺序,但这会产生filesort, using_temporary,这更糟糕。

有没有办法让mysql同时使用索引进行连接和排序?

=== update ===

我真的很沮丧。也许某种非规范化可以帮到这里吗?

6 个答案:

答案 0 :(得分:16)

如果您有很多类别,则无法提高此查询的效率。在MySQL中,没有一个索引可以同时覆盖两个表。

您必须进行非规范化:将last_updatedhas_commentsdeleted添加到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

并运行此查询:

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

当您更新article_categories中的相关列时,您也应该更新article。这可以在触发器中完成。

请注意,列has_comments是布尔值:这将允许使用等式谓词对索引进行单个范围扫描。

另请注意,LIMIT会进入子查询。这使得MySQL使用后期行查找,默认情况下不使用它。请参阅我的博客中的这篇文章,了解它们为什么会提高性能:

如果您使用的是SQL Server,则可以对查询进行可索引的查看,这实际上会使article_categories的非规范化索引副本带有附加字段,并由服务器自动生成。

不幸的是,MySQL不支持此功能,您必须手动创建此类表并编写其他代码以使其与基表保持同步。

答案 1 :(得分:9)

在了解特定查询之前,了解索引的工作原理非常重要。

使用适当的统计信息,此查询:

select * from foo where bar = 'bar'
如果有选择性的话,

...会在foo(bar)上使用索引。这意味着,如果bar = 'bar'相当于选择表的大部分行,那么只需读取表并消除不适用的行就会更快。相反,如果bar = 'bar'表示只选择少数行,则读取索引是有意义的。

假设我们现在抛出一个订单子句,并且您对foo(bar)foo(baz)中的每一个都进行索引:

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

如果bar = 'bar'非常有选择性,那么抓住所有符合要求的行并在内存中对它们进行排序是很便宜的。如果它没有选择性,那么foo(baz)上的索引没有多大意义,因为你无论如何都会获取整个表:使用它意味着在磁盘页面上来回按顺序读取行,这非常昂贵

然而,在限制条款中,foo(baz)可能会突然变得有意义:

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

如果bar = 'bar'非常有选择性,那么它仍然是一个不错的选择。如果它没有选择性,你可以通过扫描foo(baz)上的索引快速找到10个匹配的行 - 你可能会读取10行或50行,但很快就会找到10行。

假设后一个查询的索引位于foo(bar, baz)foo(baz, bar)上。索引从左到右读取。一个人对这个潜在的查询非常有意义,另一个可能根本没有。把它们想象成这样:

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

正如您所看到的,foo(bar, baz)上的索引允许从('bar', 'aaa')开始读取并从该点开始按顺序提取行。

相反,foo(baz, bar)上的索引会产生按baz排序的行,而不管bar可能包含的内容。如果bar = 'bar'完全没有选择作为标准,那么您将很快遇到查询的匹配行,在这种情况下使用它是有意义的。如果它非常有选择性,你可能会在找到足够的匹配bar = 'bar'之前迭代gazillions的行 - 它可能仍然是一个不错的选择,但它是最佳的。

有了这个问题,让我们回到原始查询...

您需要加入包含类别的文章,过滤特定类别的文章,包含多个未删除的评论,然后按日期对其进行排序,然后抓取其中一些。

我认为大多数文章都没有被删除,因此关于该标准的索引不会有多大用处 - 它只会减慢写入和查询计划的速度。

我认为大多数文章都有评论或更多,所以也不会有选择性。即也没有必要将其编入索引。

如果没有您的类别过滤器,索引选项相当明显:articles(last_updated);可能右边是注释计数列,左边是删除标记。

使用您的类别过滤器,一切都取决于......

如果您的类别过滤器非常有选择性,那么选择该类别中的所有行,在内存中对它们进行排序以及选择最匹配的行实际上是非常有意义的。

如果您的类别过滤器完全没有选择性并且几乎产生文章,那么articles(last_update)上的索引就有意义:有效的行遍布整个地方,因此请按顺序读取行,直到找到足够的匹配项和

在更一般的情况下,它只是模糊的选择性。据我所知,所收集的统计数据并未考虑相关性。因此,规划师没有很好的方法来估计它是否能够足够快地找到具有正确类别的文章,以便值得阅读后一索引。在内存中加入和排序通常会更便宜,所以规划人员也会这样做。

无论如何,你有两种选择来强制使用索引。

一个是承认查询规划器并不完美并且使用提示:

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

要小心,因为有时计划程序实际上是正确的,不想使用你喜欢的索引或副版本。此外,它可能在未来的MySQL版本中变得正确,因此在您多年来维护代码时请记住这一点。

编辑:STRAIGHT_JOIN,正如DRap指出的那样,也有类似的警告。

另一个是维护一个额外的列来标记经常选择的文章(例如,一个tinyint字段,当它们属于您的特定类别时设置为1),然后添加一个索引,例如articles(cat_78, last_updated)。使用触发器维护它,你会做得很好。

答案 2 :(得分:2)

使用非覆盖指数是昂贵的。对于每一行,必须使用主键从基表中检索任何未覆盖的列。所以我首先尝试在articles上覆盖索引。这可能有助于说服MySQL查询优化器该索引是有用的。例如:

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

如果这没有帮助,您可以使用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

强制执行主键的索引名称始终为“primary”。

答案 3 :(得分:2)

您可以使用影响力MySQL来使用 KEYS INDEXES

对于

  • 订购,
  • 分组,
  • 加入

有关其他信息,请按this link进行操作。我打算用它来加入(即USE INDEX FOR JOIN (My_Index)但它没有按预期工作。删除FOR JOIN部分显着加快了我的查询,从超过3.5小时到1-2秒。因为MySQL被迫使用正确的索引。

答案 4 :(得分:1)

我会提供以下索引

文章表 - INDEX(已删除,last_updated,comment_cnt)

article_categories表 - INDEX(article_id,category_id) - 您已经拥有此索引

然后添加Straight_Join以强制执行列出的查询,而不是通过它可能有助于查询的任何统计信息来尝试使用article_categories表。

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

根据评论/反馈,我会考虑基于集合进行逆转,如果类别记录的基础更小......例如

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

在这种情况下,我会通过

确保文章表上的索引

index - (id,deleted,last_updated)

答案 5 :(得分:1)

首先,我建议您阅读文章3 ways MySQL uses indexes

现在,当您了解基础知识时,您可以优化此特定查询。

MySQL不能使用索引进行排序,它只能按索引的顺序输出数据。由于MySQL使用嵌套循环进行连接,因此您要排序的字段应位于连接的第一个表中(您可以在EXPLAIN结果中看到连接的顺序,并且可以通过创建特定索引来影响它(如果它没有帮助)通过强制所需的索引)。

另一个重要的事情是,在订购之前,您从a表中获取所有已过滤行的所有列,然后可能会跳过大部分列。获取所需行id的列表并仅获取那些行更加有效。

要完成这项工作,您需要在表(deleted, comment_cnt, last_updated)上使用覆盖索引a,现在您可以按如下方式重写查询:

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。表a的表定义不包含comment_cnt列;)