为啥 MySQL 查询在使用 LIMIT 和 Order BY 时会变慢?

Posted

技术标签:

【中文标题】为啥 MySQL 查询在使用 LIMIT 和 Order BY 时会变慢?【英文标题】:Why the MySQL query slows down while using LIMIT with Order BY?为什么 MySQL 查询在使用 LIMIT 和 Order BY 时会变慢? 【发布时间】:2016-07-20 06:22:51 【问题描述】:

我有一个问题:

    SELECT abt.duration, a.id, a.a_id, a.a_tag
    FROM active_begin_times AS abt INNER JOIN sources AS a ON a.id = abt.a_source 
    AND a.u IN (29, 28, 27, 26, 25, 24) 
    WHERE (abt.duration > 86400000) 
    and (abt.begin_timestamp<=1465185600000 and 1465617600000<=abt.end_timestamp
    OR 1465185600000<=abt.begin_timestamp and abt.begin_timestamp<=1465617600000 
    OR 1465185600000<=abt.end_timestamp and abt.end_timestamp<=1465617600000) 
    order by abt.begin_timestamp asc LIMIT 0, 10

该数据库有大约 500 万个条目。此查询大约需要 30 秒才能运行。但是,如果我将表的顺序更改为 duration,它会在几分之一秒内计算出来,如下面的查询所示:

SELECT abt.duration, a.id, a.a_id, a.a_tag
FROM active_begin_times AS abt INNER JOIN sources AS a ON a.id = abt.a_source 
AND a.u IN (29, 28, 27, 26, 25, 24) 
WHERE (abt.duration > 86400000) 
    and (abt.begin_timestamp<=1465185600000 and 1465617600000<=abt.end_timestamp
    OR 1465185600000<=abt.begin_timestamp and abt.begin_timestamp<=1465617600000 
    OR 1465185600000<=abt.end_timestamp and abt.end_timestamp<=1465617600000) 
    order by abt.duration asc LIMIT 0, 10

我在 durationbegin_timestamp 上都有索引。而当我把order by改成abt.id时,又要花很多时间去计算,也有索引。

另外,我注意到对于这组特定的条件,查询返回 2 行。但是,如果我更改变量并让这个查询返回 20 到 30 奇数行,那么计算是即时的。

有人可以解释一下,以上两种情况吗?我尝试查看 SO,但无法理解行为或对解释不满意。

第一个查询返回的EXPLAIN

| id | select_type | table | type   | possible_keys                                            | key             | key_len | ref                   | rows | Extra       |
|----|-------------|-------|--------|----------------------------------------------------------|-----------------|---------|-----------------------|------|-------------|
| 1  | SIMPLE      | abet  | index  | FK2681F9347A6A34B,begin_timestamp,end_timestamp,duration | begin_timestamp | 9       | \N                    | 6094 | Using where |
| 1  | SIMPLE      | a     | eq_ref | PRIMARY,FK722DBCA9F603AF                                 | PRIMARY         | 4       | db.abt.alarm_source   | 1    | Using where |

从第二个查询我得到:

| id | select_type | table | type   | possible_keys                                            | key      | key_len | ref                   | rows | Extra                              |
|----|-------------|-------|--------|----------------------------------------------------------|----------|---------|-----------------------|------|------------------------------------|
| 1  | SIMPLE      | abet  | range  | FK2681F9347A6A34B,begin_timestamp,end_timestamp,duration | duration | 9       | \N                    | 8597 | Using index condition; Using where |
| 1  | SIMPLE      | a     | eq_ref | PRIMARY,FK722DBCA9F603AF                                 | PRIMARY  | 4       | db.abt.alarm_source   | 1    | Using where                        |

我可以看到的明显区别是在 Extras 列中,其中 2 查询是 Using index condition;,我不知道该怎么做。

SHOW CREATE TABLE active_begin_time 的输出:

CREATE TABLE active_begin_times (
  id int(11) NOT NULL AUTO_INCREMENT,
  begin_timezone_offset int(11) DEFAULT NULL,
  begin_timezone_suffix varchar(100) DEFAULT NULL,
  begin_timestamp bigint(20) DEFAULT NULL,
  begin_timestamp_date varchar(100) DEFAULT NULL,
  duration bigint(20) DEFAULT NULL,
  end_timezone_offset int(11) DEFAULT NULL,
  end_timezone_suffix varchar(100) DEFAULT NULL,
  end_timestamp bigint(20) DEFAULT NULL,
  end_timestamp_date varchar(100) DEFAULT NULL,
  incomplete int(11) DEFAULT NULL,
  a_source int(11) DEFAULT NULL,
  PRIMARY KEY (id),
  KEY FK2681F9347A6A34B (alarm_source),
  KEY begin_timestamp (begin_timestamp),
  KEY end_timestamp (end_timestamp),
  KEY begin_timezone_offset (begin_timezone_offset),
  KEY end_timezone_offset (end_timezone_offset),
  KEY duration (duration),
  KEY begin_timestamp_date (begin_timestamp_date),
  KEY end_timestamp_date (end_timestamp_date)
) ENGINE=MyISAM AUTO_INCREMENT=6164640 DEFAULT CHARSET=latin1

【问题讨论】:

你能发布 SHOW CREATE TABLE active_begin_times 的输出吗? 我建议使用 BETWEEN 重写查询:( abt.end_timestamp BETWEEN 1465185600000 and 1465617600000) BETWEEN 等价。 如果你正在处理时区,但没有使用TIMESTAMP,我怀疑你的结果会有错误。 【参考方案1】:

对于第一个查询,索引用于按索引指定的顺序定位和读取完整的表行,然后针对每一行测试整个 where 子句。所以这就像是按照索引指定的顺序进行全表扫描。

第二个更快,因为它首先使用索引来限制行(type: range),并且它在存储引擎级别执行此操作(extras: using index condition),而无需读取整个表行来测试(abt.duration &gt; 86400000)。对于匹配第一个条件的那些行,读取完整的表行以针对 where 子句的其余部分进行测试。

不看解释结果,如果你比较 where 子句的两个部分,很容易对第一部分使用索引(abt.duration &gt; 86400000),但对于第二部分没有abt.begin_timestamp&lt;=14651856000001465185600000&lt;=abt.begin_timestamp 甚至是end_timestamp 上的第三个条件,而不是begin_timestamp

延伸阅读:Index Condition Pushdown Optimization

【讨论】:

【参考方案2】:
WHERE    duration > 1234 AND ...
ORDER BY duration LIMIT 10

可以非常有效地使用INDEX(duration)

    在索引中找到 1234 获取满足WHERE 其余部分的接下来的 10 行。然后停止。注意:如果还有其他过滤(AND ...),并且这10行很快就会出现,这很快。另一方面,如果 10 行在表中出现得更晚,则速度很慢。

您的其他情况更复杂,因为 begin_timestamp 不在简单的 where 子句中。相反,它必须:

    查找与WHERE 匹配的所有行(EXPLAIN 估计为数千行)。 按照begin_timestamp排序(无索引有用) 交付 10 行。

事实上,这可能比上面的“慢”情况要快。这取决于数据值的分布和优化器不太了解的其他事情。所以...有时优化器会选择“错误”的方式来评估这种查询,最终会比应有的速度慢。

关于“覆盖”索引的评论不正确,因为它不包含a_source

【讨论】:

那为什么第一个查询在输出更多行时会更快,我在问题中提到的情况?还有一个观察,当限制更改为LIMIT 0, 1000 时,第一个查询只需要 2 秒的计算时间。 查看我的编辑。 可能是优化器说“哦,我需要'很多'行,所以我将使用排序机制。而这恰好工作得更快。 哦,简化一下:可以消除 OR:(begin &lt; 14656... AND 14651... &lt; end) 消除 OR可能也可以加快查询速度。【参考方案3】:

因为您的表在duration(不是另一个)上有一个索引,所以它会获取前 10 行,然后就完成了。事实上,它上面有一个covering 索引,超级快。它是composite 键的一部分,满足查询中的所有内容,无需跳转到数据页面。当然,有一个连接。涉及两个表。

【讨论】:

嗨,Drew,我没听明白,“因为你的表有一个持续时间的索引(而不是另一个)”是什么意思? 啊,如果你有一个key `comp` (begin_timestamp,end_timestamp,duration),那将是一个覆盖组合,你可以在begin_timestamp 上放弃当前键,因为这个新组合将覆盖它,因为它是最左边的 (第一的)。也就是说,请注意不要过度索引,特别是对于不经常使用的情况。由于索引会减慢插入速度并且可以减慢更新速度。只有当关键部分发生变化时,索引才能减慢更新速度,而非关键部分则不会。当使用键来查找要更新的数据时,索引可以加快更新速度。人们应该经常检查他们的索引选择 Shadow 和我一周前还在为这件事讨价还价 here 。有点相关,有点不相关。没有两种情况是相同的。所以要明确一点,只有您应该决定您想要和知道的内容最适合您的系统和索引选择。在我说“只需创建这个新索引”之前,我总是尝试抛出警告声明。否则就是我的廉价和不负责任。

以上是关于为啥 MySQL 查询在使用 LIMIT 和 Order BY 时会变慢?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 MYSQL 更高的 LIMIT 偏移量会减慢查询速度?

mysql 证明为啥用limit时,offset很大会影响性能

在 MySQL 中优化 ORDER BY LIMIT 查询

MySQL 查询语句SELECT和数据条件过滤

当加入一个非常小/空的表时,尽管我使用“LIMIT”,为啥 MySQL 会进行全面扫描?

MySQL面试题