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)
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。