按 id 排序时非常慢,但按时间戳 id 排序时很快
Very slow when order by id, but fast when order by timestamp, id
遇到一个很费解的优化案例。我不是 SQL 专家,但这个案例似乎违背了我对聚集键原则的理解。
我有以下 table 架构:
CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`chargeQuote` tinyint(1) NOT NULL,
`features` int(11) NOT NULL,
`sequenceIndex` int(11) NOT NULL,
`createdAt` bigint(20) NOT NULL,
`previousSeqId` bigint(20) NOT NULL,
`refOrderId` bigint(20) NOT NULL,
`refSeqId` bigint(20) NOT NULL,
`seqId` bigint(20) NOT NULL,
`updatedAt` bigint(20) NOT NULL,
`userId` bigint(20) NOT NULL,
`version` bigint(20) NOT NULL,
`amount` decimal(36,18) NOT NULL,
`fee` decimal(36,18) NOT NULL,
`filledAmount` decimal(36,18) NOT NULL,
`makerFeeRate` decimal(36,18) NOT NULL,
`price` decimal(36,18) NOT NULL,
`takerFeeRate` decimal(36,18) NOT NULL,
`triggerOn` decimal(36,18) NOT NULL,
`source` varchar(32) NOT NULL,
`status` varchar(50) NOT NULL,
`symbol` varchar(32) NOT NULL,
`type` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
KEY `IDX_STATUS` (`status`) USING BTREE,
KEY `IDX_USERID_SYMBOL_STATUS_TYPE` (`userId`,`symbol`,`status`,`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7937243 DEFAULT CHARSET=utf8mb4;
这是一个很大的table。一亿行。它已经被 createdAt
分片了,所以 1 亿 = 1 个月的订单价值。
我有一个慢速查询。查询非常简单:
select id,chargeQuote,features,sequenceIndex,createdAt,previousSeqId,refOrderId,refSeqId,seqId,updatedAt,userId,version,amount,fee,filledAmount,makerFeeRate,price,takerFeeRate,triggerOn,source,`status`,symbol,type
from orders where 1=1
and userId=100000
and createdAt >= '1567775174000' and createdAt <= '1567947974000'
and symbol in ( 'BTC_USDT' )
and status in ( 'FULLY_FILLED' , 'PARTIAL_CANCELLED' , 'FULLY_CANCELLED' )
and type in ( 'BUY_LIMIT' , 'BUY_MARKET' , 'SELL_LIMIT' , 'SELL_MARKET' )
order by id desc limit 0,20;
这个查询需要 24 秒。满足userId=100000
的行数很少,在100左右。而满足整个where子句的行数是0。
但是当我做了一个小调整,即我改变了子句的顺序:
order by id desc limit 0,20; -- before
order by createdAt desc, id desc limit 0,20; -- after
变得非常快,0.03秒。
我可以看到它在 MySQL 引擎中产生了很大的不同,因为 explain
给出了这一点,在更改之前它使用 key: PRIMARY
并且在它最终使用 key: IDX_USERID_SYMBOL_STATUS_TYPE
之后,正如预期的那样,我想因此非常快。这是解释计划:
select_type table partitions type possible_keys key key_len ref rows filtered Extra
SIMPLE orders index IDX_STATUS,IDX_USERID_SYMBOL_STATUS_TYPE PRIMARY 8 20360 0.02 Using where
SIMPLE orders range IDX_STATUS,IDX_USERID_SYMBOL_STATUS_TYPE IDX_USERID_SYMBOL_STATUS_TYPE 542 26220 11.11 Using index condition; Using where; Using filesort
那么是什么原因呢?事实上,我很惊讶它不是按 id(这是 PRIMARY KEY)自然排序的。这不是MySQL中的簇键吗?为什么它在按id排序时选择不使用索引?
我很困惑,因为要求更高的查询(按 2 个条件排序)非常快,但更宽松的查询很慢。
不,我试过了 ANALYZE TABLE orders;
但没有任何反应。
MySQL 对于 ORDER BY ... LIMIT n:
的查询有两个备选查询计划
- 读取所有符合条件的行,对它们进行排序,然后选择前 n 行。
- 按排序顺序读取行,当找到 n 个符合条件的行时停止。
为了决定哪个是更好的选择,优化器需要估计你的WHERE条件的过滤效果。这不是直截了当的,特别是对于没有索引的列,或者对于值相关的列。在您的情况下, MySQL 优化器显然认为第二种策略是最好的。也就是说,它没有看到WHERE子句不会被任何行满足,而是认为有2%的行会满足WHERE子句,只扫描其中的一部分就能找到20行table 主键顺序向后。
WHERE 子句的过滤效果如何估计在 5.6、5.7 和 8.0 之间差异很大。如果您使用的是 MySQL 8.0,您可以尝试为涉及的列创建直方图,看看是否可以改进估计。如果没有,我认为您唯一的选择是使用 FORCE INDEX 提示让优化器选择所需的索引。
对于您的快速查询,第二种策略不是一个选项,因为 createdAt 上没有可用于避免排序的索引。
更新:
阅读 Rick 的回答后,我意识到仅 userId
上的索引应该会加快您的 ORDER BY id
查询。在这样的索引中,给定 userId
的条目将按主键排序。因此,使用此索引既可以只访问请求的 userId
的行,也可以访问按请求的排序顺序(按 id
)的行。
给定
and userId=100000
and createdAt >= '1567775174000' and createdAt <= '1567947974000'
and ... -- I am not making use of the other items
order by createdAt DESC, id desc -- I am assuming this change
limit 0,20;
我会试试
INDEX(userId, createdAt, id) -- in this order
userId
先由=
测试,从而缩小索引的部分来看。
省略 IN
测试的列。如果IN
中有多个值,我们就不能使用步骤4.
createdAt
按范围进一步过滤。
createdAt
和id
在相同方向(DESC
)进行比较。 (是的,我知道 8.0 有改进,但我不认为你想要 (ASC, DESC))。
主过滤器与基数估计器配合得很好。当 order by 使用 limit 时,这会自动成为另一个过滤器,因为数据需要进一步过滤。这可能会将基数估计器重定向到容易出现不准确估计的情况,最终导致 selected 计划不佳。为了证明这一点,运行 没有限制子句的 24 秒查询。它也应该响应 0.3 作为你的把戏。
为了解决这个问题,如果你有一个标准的非常好的性能,只使用主过滤器,select 首先,然后在第二次过滤,结果集将明显小于整个 table .使用类似:
select * 来自 (select ...main select 语句)
按 x 排序,按 y 限制
……或者……
插入 temp select ...main select 语句
select 来自 temp order by x limit by y
遇到一个很费解的优化案例。我不是 SQL 专家,但这个案例似乎违背了我对聚集键原则的理解。
我有以下 table 架构:
CREATE TABLE `orders` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`chargeQuote` tinyint(1) NOT NULL,
`features` int(11) NOT NULL,
`sequenceIndex` int(11) NOT NULL,
`createdAt` bigint(20) NOT NULL,
`previousSeqId` bigint(20) NOT NULL,
`refOrderId` bigint(20) NOT NULL,
`refSeqId` bigint(20) NOT NULL,
`seqId` bigint(20) NOT NULL,
`updatedAt` bigint(20) NOT NULL,
`userId` bigint(20) NOT NULL,
`version` bigint(20) NOT NULL,
`amount` decimal(36,18) NOT NULL,
`fee` decimal(36,18) NOT NULL,
`filledAmount` decimal(36,18) NOT NULL,
`makerFeeRate` decimal(36,18) NOT NULL,
`price` decimal(36,18) NOT NULL,
`takerFeeRate` decimal(36,18) NOT NULL,
`triggerOn` decimal(36,18) NOT NULL,
`source` varchar(32) NOT NULL,
`status` varchar(50) NOT NULL,
`symbol` varchar(32) NOT NULL,
`type` varchar(50) NOT NULL,
PRIMARY KEY (`id`),
KEY `IDX_STATUS` (`status`) USING BTREE,
KEY `IDX_USERID_SYMBOL_STATUS_TYPE` (`userId`,`symbol`,`status`,`type`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7937243 DEFAULT CHARSET=utf8mb4;
这是一个很大的table。一亿行。它已经被 createdAt
分片了,所以 1 亿 = 1 个月的订单价值。
我有一个慢速查询。查询非常简单:
select id,chargeQuote,features,sequenceIndex,createdAt,previousSeqId,refOrderId,refSeqId,seqId,updatedAt,userId,version,amount,fee,filledAmount,makerFeeRate,price,takerFeeRate,triggerOn,source,`status`,symbol,type
from orders where 1=1
and userId=100000
and createdAt >= '1567775174000' and createdAt <= '1567947974000'
and symbol in ( 'BTC_USDT' )
and status in ( 'FULLY_FILLED' , 'PARTIAL_CANCELLED' , 'FULLY_CANCELLED' )
and type in ( 'BUY_LIMIT' , 'BUY_MARKET' , 'SELL_LIMIT' , 'SELL_MARKET' )
order by id desc limit 0,20;
这个查询需要 24 秒。满足userId=100000
的行数很少,在100左右。而满足整个where子句的行数是0。
但是当我做了一个小调整,即我改变了子句的顺序:
order by id desc limit 0,20; -- before
order by createdAt desc, id desc limit 0,20; -- after
变得非常快,0.03秒。
我可以看到它在 MySQL 引擎中产生了很大的不同,因为 explain
给出了这一点,在更改之前它使用 key: PRIMARY
并且在它最终使用 key: IDX_USERID_SYMBOL_STATUS_TYPE
之后,正如预期的那样,我想因此非常快。这是解释计划:
select_type table partitions type possible_keys key key_len ref rows filtered Extra
SIMPLE orders index IDX_STATUS,IDX_USERID_SYMBOL_STATUS_TYPE PRIMARY 8 20360 0.02 Using where
SIMPLE orders range IDX_STATUS,IDX_USERID_SYMBOL_STATUS_TYPE IDX_USERID_SYMBOL_STATUS_TYPE 542 26220 11.11 Using index condition; Using where; Using filesort
那么是什么原因呢?事实上,我很惊讶它不是按 id(这是 PRIMARY KEY)自然排序的。这不是MySQL中的簇键吗?为什么它在按id排序时选择不使用索引?
我很困惑,因为要求更高的查询(按 2 个条件排序)非常快,但更宽松的查询很慢。
不,我试过了 ANALYZE TABLE orders;
但没有任何反应。
MySQL 对于 ORDER BY ... LIMIT n:
的查询有两个备选查询计划- 读取所有符合条件的行,对它们进行排序,然后选择前 n 行。
- 按排序顺序读取行,当找到 n 个符合条件的行时停止。
为了决定哪个是更好的选择,优化器需要估计你的WHERE条件的过滤效果。这不是直截了当的,特别是对于没有索引的列,或者对于值相关的列。在您的情况下, MySQL 优化器显然认为第二种策略是最好的。也就是说,它没有看到WHERE子句不会被任何行满足,而是认为有2%的行会满足WHERE子句,只扫描其中的一部分就能找到20行table 主键顺序向后。
WHERE 子句的过滤效果如何估计在 5.6、5.7 和 8.0 之间差异很大。如果您使用的是 MySQL 8.0,您可以尝试为涉及的列创建直方图,看看是否可以改进估计。如果没有,我认为您唯一的选择是使用 FORCE INDEX 提示让优化器选择所需的索引。
对于您的快速查询,第二种策略不是一个选项,因为 createdAt 上没有可用于避免排序的索引。
更新:
阅读 Rick 的回答后,我意识到仅 userId
上的索引应该会加快您的 ORDER BY id
查询。在这样的索引中,给定 userId
的条目将按主键排序。因此,使用此索引既可以只访问请求的 userId
的行,也可以访问按请求的排序顺序(按 id
)的行。
给定
and userId=100000
and createdAt >= '1567775174000' and createdAt <= '1567947974000'
and ... -- I am not making use of the other items
order by createdAt DESC, id desc -- I am assuming this change
limit 0,20;
我会试试
INDEX(userId, createdAt, id) -- in this order
userId
先由=
测试,从而缩小索引的部分来看。省略
IN
测试的列。如果IN
中有多个值,我们就不能使用步骤4.createdAt
按范围进一步过滤。createdAt
和id
在相同方向(DESC
)进行比较。 (是的,我知道 8.0 有改进,但我不认为你想要 (ASC, DESC))。
主过滤器与基数估计器配合得很好。当 order by 使用 limit 时,这会自动成为另一个过滤器,因为数据需要进一步过滤。这可能会将基数估计器重定向到容易出现不准确估计的情况,最终导致 selected 计划不佳。为了证明这一点,运行 没有限制子句的 24 秒查询。它也应该响应 0.3 作为你的把戏。 为了解决这个问题,如果你有一个标准的非常好的性能,只使用主过滤器,select 首先,然后在第二次过滤,结果集将明显小于整个 table .使用类似:
select * 来自 (select ...main select 语句) 按 x 排序,按 y 限制
……或者…… 插入 temp select ...main select 语句 select 来自 temp order by x limit by y