为什么此查询运行时间超过 5 秒?

Jam*_*lls 4 mysql datetime query-optimization

我有一个 MySQL 表,其中大约有 2m 行。我正在尝试运行以下查询,每次都需要 5 秒以上才能获得结果。我在列上有一个索引created_at。下面是EXPLAIN输出。

这是预期的吗?

提前致谢。

SELECT
  DATE(created_at) AS grouped_date,
  HOUR(created_at) AS grouped_hour,
  count(*) AS requests
FROM
  `advert_requests`
WHERE
  DATE(created_at) BETWEEN '2022-09-09' AND '2022-09-12'
GROUP BY
  grouped_date,
  grouped_hour
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

Bil*_*win 5

EXPLAIN 显示type: index哪个是索引扫描。也就是说,它使用索引,但它会迭代索引中的每个条目,就像表扫描对表中的行所做的那样。这得到了支持,rows: 2861816它告诉您优化器对其将检查的索引条目数量的估计(这是一个粗略的数字)。这比仅检查与条件匹配的行要昂贵得多,而这正是我们从索引中寻求的好处。

那么这是为什么呢?

当您在搜索中的索引列上使用任何函数时,如下所示:

WHERE
  DATE(created_at) BETWEEN '2022-09-09' AND '2022-09-12'
Run Code Online (Sandbox Code Playgroud)

它破坏了索引减少检查行数的好处。

MySQL的优化器对函数的结果没有任何智能,因此它无法推断返回值的顺序将与索引的顺序相同。因此它不能利用索引已排序的事实来缩小搜索范围。你和我都知道这与 的DATE(created_at)顺序相同是很自然的created_at,但查询优化器不知道这一点。还有其他函数,例如MONTH(created_at)结果肯定不是按排序顺序的,MySQL 的优化器不会尝试知道哪个函数的结果是可靠排序的。

要修复您的查询,您可以尝试以下两种方法之一:

使用表达式索引。这是MySQL 8.0的新特性:

ALTER TABLE `advert_requests` ADD INDEX ((DATE(created_at)))
Run Code Online (Sandbox Code Playgroud)

请注意多余的一对括号。定义表达式索引时需要这些。索引条目是该函数或表达式的结果,而不是列的原始值。

如果您在查询中使用相同的表达式,优化器会识别并使用索引。

mysql> explain SELECT   DATE(created_at) AS grouped_date,   HOUR(created_at) AS grouped_hour,   count(*) AS requests FROM   `advert_requests` WHERE   DATE(created_at) BETWEEN '2022-09-09' AND '2022-09-12' GROUP BY   grouped_date,   grouped_hour\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: advert_requests
   partitions: NULL
         type: range          <-- much better than 'index'
possible_keys: functional_index
          key: functional_index
      key_len: 4
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where; Using temporary
Run Code Online (Sandbox Code Playgroud)

如果使用MySQL 5.7,则不能直接使用表达式索引,但可以使用虚拟列并在虚拟列上定义索引:

ALTER TABLE advert_requests
  ADD COLUMN created_at_date DATE AS (DATE(created_at)),
  ADD INDEX (created_at_date);
Run Code Online (Sandbox Code Playgroud)

优化器识别表达式的技巧仍然有效。

如果您使用的 MySQL 版本早于 5.7,则无论如何都应该升级。MySQL 5.6 及更早版本现已结束生命周期,并且存在安全风险。

您可以做的第二件事是重构您的查询,使该created_at列不在函数内。

WHERE
  created_at >= '2022-09-09' AND created_at < '2022-09-13'
Run Code Online (Sandbox Code Playgroud)

将日期时间与日期值进行比较时,日期值隐式为 00:00:00.000 时间。要包含截至 2022-09-12 23:59:59.999 的每一分之一秒,只需使用< '2022-09-13'.

其 EXPLAIN 显示它使用 上的现有索引created_at

mysql> explain SELECT   DATE(created_at) AS grouped_date,   HOUR(created_at) AS grouped_hour,   count(*) AS requests FROM   `advert_requests` WHERE   created_at >= '2022-09-09' AND created_at < '2022-09-13' GROUP BY   grouped_date,   grouped_hour\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: advert_requests
   partitions: NULL
         type: range        <-- not 'index'
possible_keys: created_at
          key: created_at
      key_len: 6
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index condition; Using temporary
Run Code Online (Sandbox Code Playgroud)

该解决方案适用于旧版本的 MySQL 以及 5.7 和 8.0。