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.

中使用的相同的查询计划