使用 GROUP BY ... HAVING 优化 MySQL 查询时遇到问题

Posted

技术标签:

【中文标题】使用 GROUP BY ... HAVING 优化 MySQL 查询时遇到问题【英文标题】:Having trouble optimizing MySQL query with GROUP BY ... HAVING 【发布时间】:2013-09-03 12:50:09 【问题描述】:

我正在尝试优化快速优化一些用 php 编写的过时论坛软件的搜索功能。我将我的工作归结为一个如下所示的查询:

SELECT thread.threadid
FROM thread AS thread
INNER JOIN word AS word ON (word.title LIKE 'word1' OR word.title LIKE 'word2')
INNER JOIN postindex AS postindex ON (postindex.wordid = word.wordid)
INNER JOIN post AS postquery ON (postquery.postid = postindex.postid)
WHERE thread.threadid = postquery.threadid
GROUP BY thread.threadid
HAVING COUNT(DISTINCT word.wordid) = 2
LIMIT 25;

word1word2 是示例;可以有任意数量的单词。查询末尾的数字是单词的总数。这个想法是,一个线程最包含搜索查询中的所有单词,分布在任意数量的帖子中。

这个查询经常超过 60 秒,只有两个词,并且超时。我很难过;我不知道如何进一步优化这个可怕的搜索引擎。

据我所知,所有内容都已正确编入索引,并且我最近运行了ANALYZE。大多数数据库都在 InnoDB 上运行。这是EXPLAIN的输出:

+----+-------------+-----------+--------+----------------------------------------------------------------------------------------+---------+---------+------------------------------+------+-----------------------------------------------------------+
| id | select_type | table     | type   | possible_keys                                                                          | key     | key_len | ref                          | rows | Extra                                                     |
+----+-------------+-----------+--------+----------------------------------------------------------------------------------------+---------+---------+------------------------------+------+-----------------------------------------------------------+
|  1 | SIMPLE      | word      | range  | PRIMARY,title                                                                          | title   | 150     | NULL                         |    2 | Using where; Using index; Using temporary; Using filesort |
|  1 | SIMPLE      | postindex | ref    | wordid,temp_ix                                                                         | temp_ix | 4       | database1.word.wordid        |    3 | Using index condition                                     |
|  1 | SIMPLE      | postquery | eq_ref | PRIMARY,threadid,showthread                                                            | PRIMARY | 4       | database1.postindex.postid   |    1 | NULL                                                      |
|  1 | SIMPLE      | thread    | eq_ref | PRIMARY,forumid,postuserid,pollid,title,lastpost,dateline,prefixid,tweeted,firstpostid | PRIMARY | 4       | database1.postquery.threadid |    1 | Using index                                               |
+----+-------------+-----------+--------+----------------------------------------------------------------------------------------+---------+---------+------------------------------+------+-----------------------------------------------------------+

更新

LIMIT 25 似乎没有多大帮助。它可能会比通常返回数百个结果的查询减少第二次。

澄清

使 mysql 变慢的部分是 GROUP BY ... HAVING ... 位。对于GROUP BYLIMIT 对于提高性能几乎毫无用处。没有GROUP BY,只要LIMIT还在,查询速度还是蛮快的。

SQL 信息

SHOW CREATE TABLE postindex; 的输出:

CREATE TABLE `postindex` (
  `wordid` int(10) unsigned NOT NULL DEFAULT '0',
  `postid` int(10) unsigned NOT NULL DEFAULT '0',
  `intitle` smallint(5) unsigned NOT NULL DEFAULT '0',
  `score` smallint(5) unsigned NOT NULL DEFAULT '0',
  UNIQUE KEY `wordid` (`wordid`,`postid`),
  KEY `temp_ix` (`wordid`),
  KEY `postid` (`postid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

我没有制作表格,所以我不知道为什么 wordid 上有重复的索引;但是,我不愿意删除它,因为这是一个古老的、变化无常的软件。

【问题讨论】:

这看起来很脏,很脏:“INNER JOIN word AS word ON (word.title LIKE 'word1' OR word.title LIKE 'word2')” @PieterB 关于word AS word:第一个word不保证是word;它可能有一个前缀。此查询由 PHP 生成。 word.title上有索引。我相信它被整理为 utf8_swedish_ci(它是从使用 latin1_swedish_ci 的数据库中导入的)。在 PHP 中规范化字符串并使用相等而不是 LIKE 可能会有所帮助。 问题在于“LIKE”,再多的索引也不会变得更快,据我所知,如果我错了,有人纠正我 INNODB 也不支持全文搜索. @PieterB 我们使用的是 5.6; InnoDB 从 5.6 开始支持全文搜索。 【参考方案1】:

您可以尝试多次重写并比较执行计划和时间。

使用 2 个EXISTS 子查询(每个要检查的单词一个):

SELECT t.threadid
FROM thread AS t
WHERE EXISTS
      ( SELECT 1
        FROM post AS p
          JOIN postindex AS pi
            ON pi.postid = p.postid
          JOIN word AS w
            ON pi.wordid = w.wordid
        WHERE w.title = 'word1'
          AND t.threadid = p.threadid
      )
  AND EXISTS
      ( SELECT 1
        FROM post AS p
          JOIN postindex AS pi
            ON pi.postid = p.postid
          JOIN word AS w
            ON pi.wordid = w.wordid
        WHERE w.title = 'word2'
          AND t.threadid = p.threadid
      ) ;

使用一个EXISTS 子查询:

SELECT t.threadid
FROM thread AS t
WHERE EXISTS
      ( SELECT 1
        FROM post AS p1
          JOIN postindex AS pi1
            ON  pi1.postid = p1.postid
          JOIN word AS w1
            ON  w1.wordid = pi1.wordid
            AND w1.title = 'word1'

          JOIN post AS p2
            ON  p2.threadid = p1.threadid
          JOIN postindex AS pi2
            ON  pi2.postid = p2.postid
          JOIN word AS w2
            ON  w2.wordid = pi2.wordid
            AND w2.title = 'word2'

        WHERE t.threadid = p1.threadid
          AND t.threadid = p2.threadid
      ) ;

具有多个连接和GROUP BY 的单个查询仅用于删除重复的threadid

SELECT t.threadid
FROM thread AS t

  JOIN post AS p1
    ON  p1.threadid = t.threadid
  JOIN postindex AS pi1
    ON  pi1.postid = p1.postid
  JOIN word AS w1
    ON  w1.wordid = pi1.wordid
    AND w1.title = 'word1'

  JOIN post AS p2
    ON  p1.threadid = t.threadid
  JOIN postindex AS pi2
    ON  pi2.postid = p2.postid
  JOIN word AS w2
    ON  w2.wordid = pi2.wordid
    AND w2.title = 'word2'

WHERE p1.threadid = p2.threadid        -- this line is redundant
GROUP BY t.threadid ;

【讨论】:

这似乎在有很多结果的查询上表现得很好(大约需要一秒钟),但在结果很少的查询上却很困难。似乎结果越少,运行速度就越慢。对于单个结果的查询,耗时 42.67 秒。 您是否添加了postindex (postid, wordid)postindex (wordid, postid) 索引? 不是postid, wordid,因为它以前不适用。我会试试然后回复你。大约有 12M 行,所以需要几分钟。不过,这在正常情况下肯定更有效。 gist.github.com/Zenexer/c4cf69aa38e90b0c5d4f EXPLAIN 用于第一个查询,这似乎是使用 PHP 最容易生成的。未使用索引postindex (postid, wordid);据我所知,它不适用。对于最后一个示例,您可以将 GROUP BY ... 替换为 SELECT DISTINCT 这是我最终使用的查询:gist.github.com 这与您的第一个示例几乎相同,但我将 w.title = '...' 移至 ON 位以提高性能。【参考方案2】:

我首先创建临时表,并存储与您的搜索匹配的不同(thread.threadid、word.wordid)。然后选择thread.threadid where count() = 搜索词数。

【讨论】:

根据EXPLAIN,MySQL 正在创建一个临时表。我认为这样做不会加快速度。你能提供一个示例查询吗?

以上是关于使用 GROUP BY ... HAVING 优化 MySQL 查询时遇到问题的主要内容,如果未能解决你的问题,请参考以下文章

group by后接的having语句怎样使用才是有效的,我为啥不行的

group by having用法举例

having和group by的区别?

Lambda表达式 group by having问题

having是否依赖于group by

使用 GROUP_CONCAT、GROUP BY、HAVING 进行选择