【问题标题】:MySQL query too slow, 70k rows 12 secMySQL 查询太慢,70k 行 12 秒
【发布时间】:2014-08-17 22:04:50
【问题描述】:

此查询在 VPS 上运行 12 秒以上。它连接了 3 个表。只有第一个“topics”大约有 70k 行,其他大约 20 行,“post_cc”大约 1500 行。

SELECT topics.*, employee.username, accounts.ac_name, accounts.ac_mail
FROM topics
INNER JOIN employee ON employee.id_user = topics.id_owner 
INNER JOIN accounts ON accounts.id_account = topics.id_account 
WHERE topics.status  IN  ('1','3') AND ( topics.id_owner IN (12, 5) OR topics.id_post IN 
    (SELECT DISTINCT(id_post) FROM post_cc WHERE id_employee IN (12, 5) ) )
ORDER BY topics.creationdate DESC LIMIT 0,25

我已经尝试(没有任何改进)删除子查询和第一个“员工”加入。如果我删除“accounts”连接,查询将在 0.1 秒内运行,但在分页期间需要所有表数据进行排序。

解释:

+----+--------------------+------------+-----------------+-----------------------+---------+---------+-----------------+-------+----------------------------------------------+
| id | select_type        | table      | type            | possible_keys         | key     | key_len | ref             | rows  | Extra                                        |
+----+--------------------+------------+-----------------+-----------------------+---------+---------+-----------------+-------+----------------------------------------------+
|  1 | PRIMARY            | topics     | ALL             | id_owner,id_account   | NULL    | NULL    | NULL            | 75069 | Using where; Using temporary; Using filesort |
|  1 | PRIMARY            | accounts   | ALL             | PRIMARY               | NULL    | NULL    | NULL            |     5 | Using where; Using join buffer               |
|  1 | PRIMARY            | employee   | eq_ref          | PRIMARY               | PRIMARY | 3       | topics.st_owner |     1 | Using where                                  |
|  2 | DEPENDENT SUBQUERY | post_cc    | unique_subquery | PRIMARY               | PRIMARY | 8       | func,const      |     1 | Using index; Using where                     |
+----+--------------------+------------+-----------------+-----------------------+---------+---------+-----------------+-------+----------------------------------------------+

我已经添加了建议的键作为索引,它缩短了 2 秒的时间,但它仍然太慢了。

短表:

topics
+--------------------+---------------------+------+-----+---------+----------------+
| Field              | Type                | Null | Key | Default | Extra          |
+--------------------+---------------------+------+-----+---------+----------------+
| id_post            | int(10) unsigned    | NO   | PRI | NULL    | auto_increment |
| id_account         | int(10) unsigned    | YES  | MUL | 0       |                |
| mail               | varchar(256)        | YES  | MUL | NULL    |                |
| from_name          | varchar(512)        | YES  |     | NULL    |                |
| title              | varchar(512)        | YES  |     | NULL    |                |
| content            | text                | YES  |     | NULL    |                |
| id_owner           | int(10) unsigned    | YES  | MUL | NULL    |                |
| creationdate       | datetime            | YES  |     | NULL    |                |
+--------------------+---------------------+------+-----+---------+----------------+

employee
+---------------------+-----------------------+------+-----+---------+----------------+
| Field               | Type                  | Null | Key | Default | Extra          |
+---------------------+-----------------------+------+-----+---------+----------------+
| id_employee         | mediumint(8) unsigned | NO   | PRI | NULL    | auto_increment |
| id_user             | mediumint(8) unsigned | NO   |     | NULL    |                |
| id_owner            | tinyint(1)            | YES  |     | 0       |                |
| active              | tinyint(1)            | YES  |     | 1       |                |
| username            | varchar(64)           | YES  |     | NULL    |                |
| email               | varchar(128)          | YES  |     | NULL    |                |
+---------------------+-----------------------+------+-----+---------+----------------+

accounts
+----------------------------+---------------------+------+-----+---------+----------------+
| Field                      | Type                | Null | Key | Default | Extra          |
+----------------------------+---------------------+------+-----+---------+----------------+
| id_account                 | int(10) unsigned    | NO   | PRI | NULL    | auto_increment |
| ac_mail                    | int(10) unsigned    | YES  | UNI | NULL    |                |
| ac_name                    | varchar(512)        | YES  |     | NULL    |                |
| last_sync_time             | datetime            | YES  |     | NULL    |                |
+----------------------------+---------------------+------+-----+---------+----------------+

post_cc
+------------------------+---------------------+------+-----+---------+-------+
| Field                  | Type                | Null | Key | Default | Extra |
+------------------------+---------------------+------+-----+---------+-------+
| id_post                | int(10) unsigned    | NO   | PRI | NULL    |       |
| id_employee            | int(10) unsigned    | NO   | PRI | NULL    |       |
| notifications          | tinyint(3) unsigned | YES  |     | 1       |       |
+------------------------+---------------------+------+-----+---------+-------+

【问题讨论】:

  • +1 表示写得好、问题详细和简短的表格。
  • 如果你把它分成两个查询,每个查询都没有OR,只是为了看看它是否有帮助?我最好的猜测是,在这种情况下,OR 是造成问题的原因。
  • 在同一个key中添加status、id_owner和id_post的索引。
  • 添加了索引,但没有帮助。用左连接替换内部连接似乎正在工作:) 如果我尝试按不在“发件人”表中的列排序,问题仍然存在。知道如何解决吗?我什至已经替换了帐户表的 from/join 并且工作速度非常快,但如果我对员工表执行相同操作,则不会。

标签: php mysql query-optimization


【解决方案1】:

一个可能的嫌疑人是依赖子查询。

MySQL 正在为外部查询返回的每一行处理该子查询(尚未被其他谓词过滤掉。

要提高性能,请考虑将其重写为 JOIN 操作或 EXISTS 谓词。

要将其替换为 JOIN 操作,由于谓词中的 OR,它需要是 OUTER JOIN(而不是 INNER JOIN)。

作为一种方法的示例:

SELECT topics.*
     , employee.username
     , accounts.ac_name
     , accounts.ac_mail
  FROM topics
  JOIN employee ON employee.id_user = topics.id_owner 
  JOIN accounts ON accounts.id_account = topics.id_account
  LEFT
  JOIN ( SELECT DISTINCT q.id_post
           FROM post_cc q 
          WHERE q.id_employee IN (12, 5) 
       ) p
    ON p.id_post = topics.id_post   
 WHERE topics.status IN ('1','3') 
   AND ( topics.id_owner IN (12, 5) 
       OR p.id_post IS NOT NULL
       )
 ORDER BY topics.creationdate DESC LIMIT 0,25

我建议您在上面运行 EXPLAIN,看看效果如何。


另一种选择是考虑 EXISTS 谓词。有时我们可以让它表现得更好,但通常不是。

SELECT topics.*
     , employee.username
     , accounts.ac_name
     , accounts.ac_mail
  FROM topics
  JOIN employee ON employee.id_user = topics.id_owner 
  JOIN accounts ON accounts.id_account = topics.id_account
 WHERE topics.status IN ('1','3') 
   AND ( topics.id_owner IN (12, 5) 
       OR EXISTS ( SELECT 1 
                     FROM post_cc q
                    WHERE q.id_employee IN (12, 5)
                      AND q.id_post = topics.id_post
                 )
       )
 ORDER BY topics.creationdate DESC LIMIT 0,25

为了性能,这几乎需要为 EXISTS 子句中的子查询一个合适的覆盖索引,例如:

ON post_cc (id_post, id_employee)

您可以尝试运行 EXPLAIN 并查看其执行情况。


我们看到 MySQL 没有使用 topics 表上的索引。

如果我们有一个前导列为creationdate 的索引,我们可能会让 MySQL 避免昂贵的“使用文件排序”操作。

而部分问题很可能是谓词中的OR。我们可能会尝试将该查询重写为两个单独的查询,并将它们与UNION ALL 集合操作结合起来。但如果我们这样做,我们真的希望看到使用topic 上的索引(我们可能不会通过对 70,000 行进行两次扫描来提高性能。

SELECT topics.*
     , employee.username
     , accounts.ac_name
     , accounts.ac_mail
  FROM topics
  JOIN employee ON employee.id_user = topics.id_owner 
  JOIN accounts ON accounts.id_account = topics.id_account
 WHERE topics.status IN ('1','3')
   AND topics.id_owner IN (12, 5)

 UNION ALL

SELECT topics.*
     , employee.username
     , accounts.ac_name
     , accounts.ac_mail
  FROM topics
  JOIN employee ON employee.id_user = topics.id_owner 
  JOIN accounts ON accounts.id_account = topics.id_account
  JOIN ( SELECT DISTINCT q.id_post
           FROM post_cc q 
          WHERE q.id_employee IN (12, 5) 
       ) p
    ON p.id_post = topics.id_post  
 WHERE topics.status IN ('1','3')
   AND ( topics.id_owner NOT IN (12, 5) OR topics.id_owner IS NULL )

 ORDER BY 8 DESC LIMIT 0,25

通过这种形式的查询,我们更有可能让 MySQL 在主题表上使用合适的索引,

... ON topics (id_owner, status)

... ON topics (id_post, status, id_owner)

【讨论】:

  • 现代 mysql 版本中的 EXISTS 应该以与相关子查询和 JOIN 相同的方式进行优化。我敢打赌,使用UNION 切换到2 个查询以避免OR 将是赢家:-)
  • @zerkms:执行计划(由优化器生成)确实取决于 MySQL 的版本。它还取决于其他因素,列/表达式的可空性、基数、可用索引。我的偏好是尝试几种方法,并将预测与实际结果进行比较。
  • 公平点。仍然很想看到没有OR 效率的解决方案:-)
  • 我也很好奇,这就是为什么我在编辑的问题中包含了一个例子。 (最新版本的 MySQL 仍然可以使用 OR 谓词生成次优计划;我们通常可以使用 UNION ALL 集合运算符代替 OR 重写查询,有时会变得更好性能。
  • 确实如此。一个小提示:UNION 不是 UNION ALL(另外我会在每个查询中添加 ORDER BY + LIMIT
【解决方案2】:

为什么不对post_cc表使用左连接,然后使用条件?

类似的东西。

SELECT topics.*, 
employee.username, 
accounts.ac_name, 
accounts.ac_mail
FROM topics
INNER JOIN employee ON employee.id_user = topics.id_owner 
INNER JOIN accounts ON accounts.id_account = topics.id_account 
LEFT JOIN post_cc ON id_employee IN (12, 5)
WHERE topics.status  IN  ('1','3') 
AND ( topics.id_owner IN (12, 5) OR topics.id_post IN (post_cc.post_id)
ORDER BY topics.creationdate DESC LIMIT 0,25;

【讨论】:

  • 一个很好的建议,但请注意id_post 列在post_cc 表上不能保证是唯一的,因此此查询可能返回比原始查询更多的行(即“重复”,因为多个匹配来自post_cc 的行。)
  • 是的,我知道这一点,但这只是一个提示性查询,并且是一个可以给出方向的示例,而不是确切的解决方案。但是,您的回答很彻底,我完全同意您的看法。确切的解决方案是像您的示例中那样使用带有子查询的左连接。
【解决方案3】:

罪魁祸首:

75069(行)|使用哪里;使用临时的;使用文件排序

这就是我们需要摆脱的。

可能的解决方案:在topics.creationdate 上添加索引。

附带说明,查询还具有id_postst_ownerstatus 上的条件,因此topics(creationdate, id_post, st_owner, status) 上的复合索引(或最后三列的任何排列 - 使用您的数据进行测试set) 可能会有所帮助。但是,无论如何,您的查询似乎会拉取大部分表格,所以我希望一个简单的索引就足够了。

【讨论】:

    猜你喜欢
    • 2014-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多