为啥 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
我在 duration 和 begin_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 > 86400000)
。对于匹配第一个条件的那些行,读取完整的表行以针对 where 子句的其余部分进行测试。
不看解释结果,如果你比较 where 子句的两个部分,很容易对第一部分使用索引(abt.duration > 86400000)
,但对于第二部分没有abt.begin_timestamp<=1465185600000
和1465185600000<=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 < 14656... AND 14651... < 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很大会影响性能