20

我有两张桌子:

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同时使用索引进行连接和排序?

=== 更新 ===

我对此真的很绝望。也许某种非规范化可以在这里提供帮助?

4

6 回答 6

20

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

您必须进行非规范化:addlast_updatedhas_commentsinto :deletedarticle_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不支持这一点,您必须手动创建这样的表并编写额外的代码以使其与基表保持同步。

于 2013-05-07T17:20:46.060 回答
13

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

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

select * from foo where bar = 'bar'

foo(bar)...如果它是选择性的,将使用一个索引。这意味着,如果bar = 'bar'相当于选择表格的大部分行,那么仅读取表格并消除不适用的行会更快。相反,如果bar = 'bar'意味着只选择少数几行,那么读取索引是有意义的。

假设我们现在加入一个 order 子句,并且您已经对每个foo(bar)and进行了索引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'是非常有选择性的,它仍然是一个不错的选择。如果它完全没有选择性,您将通过扫描索引快速找到 10 个匹配的行foo(baz)——您可能会读取 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'——它可能仍然是一个不错的选择,但它是最优的。

解决了这个问题,让我们回到你原来的查询......

您需要将文章与类别连接起来,以过滤特定类别中的文章,其中包含多个评论,未删除,然后按日期对它们进行排序,然后抓取其中的一小部分。

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

我想大多数文章都有评论或更多,所以也不会有选择性。即几乎不需要索引它。

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

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

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

如果您的类别过滤器根本没有选择性并且产生几乎所有文章,那么索引就articles(last_update)很有意义:有效的行到处都是,因此请按顺序阅读行,直到找到足够的匹配项,然后

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

无论如何,您有两个选项可以强制使用索引。

一种是承认查询计划器并不完美并使用提示:

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

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

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

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

于 2013-05-07T16:03:02.950 回答
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”。

于 2013-05-05T15:00:46.937 回答
2

首先,我建议阅读文章MySQL 使用索引的 3 种方式

现在,当您了解基础知识后,您就可以优化这个特定的查询了。

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

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

要完成这项工作,您需要(deleted, comment_cnt, last_updated)在 table 上建立一个覆盖索引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);

PS您的表定义a不包含comment_cnt列;)

于 2013-05-07T16:47:42.520 回答
2

您可以使用影响 MySQL 来使用KEYSINDEXES

为了

  • 订购,
  • 分组,
  • 加入

如需更多信息,请点击此链接。我打算使用它来加入(即USE INDEX FOR JOIN (My_Index),但它没有按预期工作。删除该FOR JOIN部分显着加快了我的查询,从超过 3.5 小时到 1-2 秒。仅仅是因为 MySQL 被迫使用正确的索引。

于 2013-10-23T14:59:23.807 回答
1

我将有以下可用索引

文章表 -- INDEX (deleted, 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

在这种情况下,我会确保在文章表上建立索引

索引 -- (id, 已删除, last_updated)

于 2013-05-07T15:03:39.640 回答