MySQL JOIN 不使用 < > 运算符过滤 WHERE 子句,因为从 MySQL 5.6 -> 5.7 移动
MySQL JOIN not filtering on WHERE clause with < > operators, since moving from MySQL 5.6 -> 5.7
我们正在将我们的数据库系统从 MySQL 5.6 升级到 MySQL 5.7,自升级以来,一些查询 运行 非常慢。
经过一些调查,我们将其缩小到几个 JOIN 查询,这些查询在使用 'larger than' > 或 'smaller than' < 运算符时突然不再听从 'WHERE' 子句。使用“=”运算符时,它确实按预期工作。查询较大的 table 时,这会导致 100% CPU 的持续使用。
已简化查询以解释手头的问题;使用解释时,我们得到以下输出:
explain
select * from TableA as A
left join
(
select
DATE_FORMAT(created_at,'%H:%i:00') as `time`
FROM
TableB
WHERE
created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR)
)
as V ON V.time = A.time
输出
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL
1 SIMPLE TableB NULL index created_at created_at 4 NULL 488389 100.00 Using where; Using index; Using join buffer (Block Nested Loop)
如您所见,它是 querying/matching 488389 行并且没有使用 where 子句,因为这是 table 中的总记录。
现在 运行 相同的查询,但使用 LIMIT 99999999 命令或使用“=”运算符:
explain
select * from TableA as A
left join
(
select
DATE_FORMAT(created_at,'%H:%i:00') as `time`
FROM
TableB
WHERE
created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) LIMIT 999999999
)
as V ON V.time = A.time
输出
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL
1 PRIMARY <derived2> NULL ALL NULL NULL NULL NULL 244194 100.00 Using where; Using join buffer (Block Nested Loop)
2 DERIVED TableB NULL range created_at created_at 4 NULL 244194 100.00 Using where; Using index
您可以看到它突然只匹配 table 的一部分的 '244194' 行,或者使用 '=' 运算符:
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL
1 SIMPLE TableB NULL ref created_at created_at 4 const 1 100.00 Using where; Using index
正如预期的那样只有 1 行。
所以现在的问题是,我们是不是以错误的方式进行查询并且
刚刚在升级时发现或自那以后发生了变化
MySQL5.6? = 运算符起作用似乎很奇怪,但是 <
和 > 由于某种原因被忽略,除非使用 LIMIT?..
我们四处搜索但找不到此问题的原因,出于显而易见的原因,我们宁愿不在我们的代码中使用 limit 9999999 解决方案。
注意 当 运行 只是连接内的查询时,它也会按预期工作。
注意我们也在 MariaDB 10.1 上进行了相同的测试,同样的问题。运行。
explain
row
-输出只是猜测它将命中多少行。它基于统计数据,已随您的更新重新设置。如果我不得不猜测所有现有行中有多少行比昨天晚上 9 点还旧,我也猜它更接近 "all rows" 而不是 "just some rows"。 'limit 99999999' 显示另一个行数的原因是相同的:它只是猜测限制会产生影响;在这种情况下,mysql 猜测它会是 恰好 行的一半(如果为真,那将是一个奇怪的巧合),当然,它实际上并不是查看限制值,因为当您只有 500k 行时,999999999 不会限制任何内容;甚至“=”中的“1”也只是一个猜测(可能更多时候是 0 而不是 1,有时甚至更多)。
这个估计会帮助选择正确的执行计划,这个估计错了,如果选错的话,只是一个问题;不过,您的执行计划看起来不错,否则没有太多选择。它完全符合预期:使用 created_at 上的索引扫描所有日期的索引。由于您执行的是左联接,因此即使您从内部查询开始,也不能跳过 tableA
中的值,因此实际上没有可用的替代执行计划。 (优化器实际上在5.7.已经改变了,但是这里没有效果。)
如果那是您的实际查询,没有真正的理由说明为什么它会比以前慢(仅关于此查询;当然有很多可能具有间接影响的一般性能选项,例如缓存策略, buffersizes, ..., 但对于标准选项,它不应该在这里产生影响)。
如果没有,你例如实际上在子查询中使用了来自 TableB
的附加列(通常很难猜测哪些可能重要的事情在问题中得到了 "simplified away"),因此需要访问实际的 table,它可能取决于您的数据的结构(或更好:您添加它的顺序)。您可以尝试 Optimize table TableB
让您的 table 和索引焕然一新,这不会有什么坏处(但会暂时锁定您的 table)。
使用 mysql 5.7.,您现在可以添加生成的列,因此可能值得尝试生成一个清理过的列 time as DATE_FORMAT(created_at,'%H:%i:00')
,这样您就不必再计算它了.也许将它添加到您的索引中,这样您就不必再对它进行排序来改进 block nested join
,但这可能取决于您的实际查询以及您使用它的频率(垃圾索引会增加开销并使用 space).
使用 JOIN
而不是 LEFT JOIN
除非你需要 'right' table 是可选的。
避免JOIN ( SELECT ... )
。虽然 5.6 和 5.7 增加了一些特性来处理它,但通常最好将子查询变成更简单的 JOIN
.
你的时间表达到昨天晚上9点;您的意思是“3 小时前”吗?
看看这是否给出了预期的结果并运行得更快:
select A.*, DATE_FORMAT(B.created_at,'%H:%i:00') as `time`
from TableA as A
JOIN TableB as B ON B.time = A.time
WHERE B.created_at < NOW() - INTERVAL 3 HOUR -- (assuming "3 hours ago")
至于 5.6 与 5.7...5.7 有一个新的 'better' 优化器,基于 "cost model"。但是,您的特定查询使优化器几乎不可能产生良好的成本。我猜 5.6 发生在更好的 EXPLAIN
上,而 5.7 发生在更差的人身上。通过简化查询,我认为两个优化器将有更好的机会更快地执行查询。
您确实需要这些索引:
B: INDEX(time, created_at) -- in that order
A: INDEX(time)
在MySQL 5.7 中,派生的tables(FROM 子句中的子查询)将尽可能合并到外部查询中。这通常是一个优势,因为可以避免子查询的结果存储在临时 table 中。但是,对于您的查询,MySQL 5.6 将在此临时 table 上创建一个可用于连接执行的索引。
合并查询的问题在于,当列是函数的参数时,无法使用 TableB.created_at 上的索引。如果您可以更改查询以便对连接左侧的列进行转换,则可以使用索引访问右侧的 table。类似于:
select * from TableA as A
left join
(
select created_at as time
FROM TableB
WHERE created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR)
)
as V ON V.time = func(A.time)
或者,如果您可以使用内连接而不是左连接,MySQL 可以反转连接顺序,以便 tableA.time 上的索引可以用于连接。
如果子查询使用LIMIT,则不能合并。因此,通过使用 LIMIT,您将获得与 MySQL 5.6.
中使用的相同的查询计划
我们正在将我们的数据库系统从 MySQL 5.6 升级到 MySQL 5.7,自升级以来,一些查询 运行 非常慢。
经过一些调查,我们将其缩小到几个 JOIN 查询,这些查询在使用 'larger than' > 或 'smaller than' < 运算符时突然不再听从 'WHERE' 子句。使用“=”运算符时,它确实按预期工作。查询较大的 table 时,这会导致 100% CPU 的持续使用。
已简化查询以解释手头的问题;使用解释时,我们得到以下输出:
explain
select * from TableA as A
left join
(
select
DATE_FORMAT(created_at,'%H:%i:00') as `time`
FROM
TableB
WHERE
created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR)
)
as V ON V.time = A.time
输出
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL
1 SIMPLE TableB NULL index created_at created_at 4 NULL 488389 100.00 Using where; Using index; Using join buffer (Block Nested Loop)
如您所见,它是 querying/matching 488389 行并且没有使用 where 子句,因为这是 table 中的总记录。
现在 运行 相同的查询,但使用 LIMIT 99999999 命令或使用“=”运算符:
explain
select * from TableA as A
left join
(
select
DATE_FORMAT(created_at,'%H:%i:00') as `time`
FROM
TableB
WHERE
created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) LIMIT 999999999
)
as V ON V.time = A.time
输出
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 PRIMARY A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL
1 PRIMARY <derived2> NULL ALL NULL NULL NULL NULL 244194 100.00 Using where; Using join buffer (Block Nested Loop)
2 DERIVED TableB NULL range created_at created_at 4 NULL 244194 100.00 Using where; Using index
您可以看到它突然只匹配 table 的一部分的 '244194' 行,或者使用 '=' 运算符:
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE A NULL ALL NULL NULL NULL NULL 10080 100.00 NULL
1 SIMPLE TableB NULL ref created_at created_at 4 const 1 100.00 Using where; Using index
正如预期的那样只有 1 行。
所以现在的问题是,我们是不是以错误的方式进行查询并且 刚刚在升级时发现或自那以后发生了变化 MySQL5.6? = 运算符起作用似乎很奇怪,但是 < 和 > 由于某种原因被忽略,除非使用 LIMIT?..
我们四处搜索但找不到此问题的原因,出于显而易见的原因,我们宁愿不在我们的代码中使用 limit 9999999 解决方案。
注意 当 运行 只是连接内的查询时,它也会按预期工作。
注意我们也在 MariaDB 10.1 上进行了相同的测试,同样的问题。运行。
explain
row
-输出只是猜测它将命中多少行。它基于统计数据,已随您的更新重新设置。如果我不得不猜测所有现有行中有多少行比昨天晚上 9 点还旧,我也猜它更接近 "all rows" 而不是 "just some rows"。 'limit 99999999' 显示另一个行数的原因是相同的:它只是猜测限制会产生影响;在这种情况下,mysql 猜测它会是 恰好 行的一半(如果为真,那将是一个奇怪的巧合),当然,它实际上并不是查看限制值,因为当您只有 500k 行时,999999999 不会限制任何内容;甚至“=”中的“1”也只是一个猜测(可能更多时候是 0 而不是 1,有时甚至更多)。
这个估计会帮助选择正确的执行计划,这个估计错了,如果选错的话,只是一个问题;不过,您的执行计划看起来不错,否则没有太多选择。它完全符合预期:使用 created_at 上的索引扫描所有日期的索引。由于您执行的是左联接,因此即使您从内部查询开始,也不能跳过 tableA
中的值,因此实际上没有可用的替代执行计划。 (优化器实际上在5.7.已经改变了,但是这里没有效果。)
如果那是您的实际查询,没有真正的理由说明为什么它会比以前慢(仅关于此查询;当然有很多可能具有间接影响的一般性能选项,例如缓存策略, buffersizes, ..., 但对于标准选项,它不应该在这里产生影响)。
如果没有,你例如实际上在子查询中使用了来自 TableB
的附加列(通常很难猜测哪些可能重要的事情在问题中得到了 "simplified away"),因此需要访问实际的 table,它可能取决于您的数据的结构(或更好:您添加它的顺序)。您可以尝试 Optimize table TableB
让您的 table 和索引焕然一新,这不会有什么坏处(但会暂时锁定您的 table)。
使用 mysql 5.7.,您现在可以添加生成的列,因此可能值得尝试生成一个清理过的列 time as DATE_FORMAT(created_at,'%H:%i:00')
,这样您就不必再计算它了.也许将它添加到您的索引中,这样您就不必再对它进行排序来改进 block nested join
,但这可能取决于您的实际查询以及您使用它的频率(垃圾索引会增加开销并使用 space).
使用 JOIN
而不是 LEFT JOIN
除非你需要 'right' table 是可选的。
避免JOIN ( SELECT ... )
。虽然 5.6 和 5.7 增加了一些特性来处理它,但通常最好将子查询变成更简单的 JOIN
.
你的时间表达到昨天晚上9点;您的意思是“3 小时前”吗?
看看这是否给出了预期的结果并运行得更快:
select A.*, DATE_FORMAT(B.created_at,'%H:%i:00') as `time`
from TableA as A
JOIN TableB as B ON B.time = A.time
WHERE B.created_at < NOW() - INTERVAL 3 HOUR -- (assuming "3 hours ago")
至于 5.6 与 5.7...5.7 有一个新的 'better' 优化器,基于 "cost model"。但是,您的特定查询使优化器几乎不可能产生良好的成本。我猜 5.6 发生在更好的 EXPLAIN
上,而 5.7 发生在更差的人身上。通过简化查询,我认为两个优化器将有更好的机会更快地执行查询。
您确实需要这些索引:
B: INDEX(time, created_at) -- in that order
A: INDEX(time)
在MySQL 5.7 中,派生的tables(FROM 子句中的子查询)将尽可能合并到外部查询中。这通常是一个优势,因为可以避免子查询的结果存储在临时 table 中。但是,对于您的查询,MySQL 5.6 将在此临时 table 上创建一个可用于连接执行的索引。
合并查询的问题在于,当列是函数的参数时,无法使用 TableB.created_at 上的索引。如果您可以更改查询以便对连接左侧的列进行转换,则可以使用索引访问右侧的 table。类似于:
select * from TableA as A
left join
(
select created_at as time
FROM TableB
WHERE created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR)
)
as V ON V.time = func(A.time)
或者,如果您可以使用内连接而不是左连接,MySQL 可以反转连接顺序,以便 tableA.time 上的索引可以用于连接。
如果子查询使用LIMIT,则不能合并。因此,通过使用 LIMIT,您将获得与 MySQL 5.6.
中使用的相同的查询计划