2

我们正在将我们的数据库系统从 MySQL 5.6 升级到 MySQL 5.7,并且自从升级以来,一些查询的运行速度非常慢。

经过一番调查,我们将其范围缩小到一些 JOIN 查询,这些查询在使用“大于”> 或“小于”< 运算符时突然不再听“WHERE”子句。使用 '=' 运算符时,它确实按预期工作。当查询一个大表时,这会导致持续 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)

如您所见,它正在查询/匹配488389行而不使用 where 子句,因为这是该表中的总记录。

现在运行相同的查询,但使用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

您可以看到它突然只匹配作为表的一部分的 '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 行。

所以现在的问题是,我们是否一直在以错误的方式查询,只是在升级时才发现,还是自 MySQL 5.6 以来发生了变化?=运算符起作用似乎很奇怪,但由于某种原因, <>被忽略了,除非使用 LIMIT?..

我们四处搜索,找不到这个问题的原因,出于显而易见的原因,我们宁愿不在我们的代码中使用限制 9999999 的解决方案。

注意当只在连接内运行查询时,它也可以按预期工作。

注意我们还在 MariaDB 10.1 上运行了相同的测试,同样的问题。

4

3 回答 3

1

-outputexplain row只是猜测它将命中多少行。它基于已随您的更新重置的统计数据。如果我不得不猜测您现有的所有行中有多少行比昨天晚上 9 点旧,我也猜它更接近“所有行”而不是“只是一些行”。'limit 99999999'显示另一个rowcount的原因是一样的:它只是猜测限制会起作用;在这种情况下,mysql 猜测它将恰好是行的一半(如果为真,那将是一个奇怪的巧合),当然,它实际上并没有查看限制值,因为 999999999 不会限制任何时间你只有 500k 行;甚至在“=”的情况下的“1”也只是一个猜测(并且可能更常见的是0而不是1,

这个估计将有助于选择正确的执行计划,如果它会选择错误的,那么这个猜测中的错误只是一个问题;不过,您的执行计划看起来不错,否则没有太多选择。它完全符合预期:使用 created_at 上的索引扫描所有日期的索引。tableA由于您进行了左连接,因此即使您从内部查询开始也无法跳过值,因此实际上没有可用的替代执行计划。(优化器实际上在 5.7. 中已经更改,但这里没有效果。)

如果那是您的实际查询,则没有真正的理由为什么它应该比以前慢(仅关于此查询;当然有很多一般性能选项可能会产生间接影响,例如缓存策略、缓冲区大小、.. .,但使用标准选项,它在这里不应该有效果)。

如果不是,并且您实际上使用TableB了子查询中的其他列(通常很难猜测哪些可能重要的事情已经在问题中“简化”了),因此需要访问实际表,这可能取决于您如何数据是结构化的(或更好:按照您添加它的顺序)。而且您可能会尝试Optimize table TableB使您的表和索引焕然一新,这不会有什么坏处(但会锁定您的表一段时间)。

使用 mysql 5.7.,您现在可以添加生成的列,因此可能值得尝试生成清理的列time as DATE_FORMAT(created_at,'%H:%i:00'),这样您就不必再计算它了。并且也许将它添加到您的索引中,因此您不必再对其进行排序以改进block nested join,但这可能取决于您的实际查询以及您使用它的频率(垃圾邮件索引会增加开销并使用空间)。

于 2016-05-27T13:49:11.983 回答
0

除非您需要“正确”表是可选的,否则请使用JOIN而不是。LEFT JOIN

避免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 有一个新的、“更好”的优化器,它基于“成本模型”。但是,您的特定查询使优化器几乎不可能提出良好的成本。我猜 5.6 发生在更好EXPLAIN的 5.7 上,而 5.7 发生在更差的上。通过简化查询,我认为两个优化器将有更好的机会更快地执行查询。

您确实需要这些索引:

B:  INDEX(time, created_at) -- in that order
A:  INDEX(time)
于 2016-06-04T19:11:01.027 回答
0

在 MySQL 5.7 中,如果可能,派生表(FROM 子句中的子查询)将被合并到外部查询中。这通常是一个优势,因为可以避免将子查询的结果存储在临时表中。但是,对于您的查询,MySQL 5.6 将在此临时表上创建一个索引,该索引可用于连接执行。

合并查询的问题在于,当列是函数的参数时,无法使用 TableB.created_at 上的索引。如果您可以更改查询以便对连接左侧的列进行转换,则可以使用索引来访问右侧的表。就像是:

   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 中使用的相同的查询计划。

于 2016-10-02T08:25:38.793 回答