我遇到了一个非常令人费解的优化案例。我不是 SQL 专家,但这个案例似乎仍然违背了我对集群关键原则的理解。
我有下表架构:
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 。 1 亿行。它已由 createdAt
进行分片,因此 1 亿 = 1 个月的订单值(value)。
我有一个慢速查询。查询非常简单:
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 子句:
order by id desc limit 0,20; -- before
order by createdAt desc, id desc limit 0,20; -- after
它变得非常快,0.03秒。
我可以看到它在 MySQL 引擎中产生了很大的差异,因为 explain
表明,在更改之前它使用 key: PRIMARY
,而在它最终使用 之后键: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(主键)自然排序这一事实感到非常惊讶。这不是MySQL中的聚集键吗?为什么它在按 id 排序时选择不使用索引?
我很困惑,因为要求更高的查询(按 2 个条件排序)速度非常快,但更宽松的查询却很慢。
不,我尝试了分析表订单;
但什么也没发生。
最佳答案
MySQL 对于使用 ORDER BY ... LIMIT n 的查询有两种替代查询计划:
- 读取所有符合条件的行,对它们进行排序,然后选择前 n 行。
- 按排序顺序读取行,并在找到 n 个符合条件的行时停止。
为了决定哪个是更好的选项,优化器需要估计 WHERE 条件的过滤效果。这并不简单,特别是对于未索引的列或值相关的列。在你的例子中,MySQL 优化器显然认为第二种策略是最好的。换句话说,它并没有看到任何行都不会满足 WHERE 子句,而是认为有 2% 的行满足 WHERE 子句,并且只扫描部分行就能找到 20 行。表按主键顺序向后。
如何估计 WHERE 子句的过滤效果在 5.6、5.7 和 8.0 之间存在很大差异。如果您使用的是 MySQL 8.0,您可以尝试为涉及的列创建直方图,看看是否可以改进估计。如果没有,我认为您唯一的选择是使用 FORCE INDEX 提示来使优化器选择所需的索引。
对于快速查询,第二种策略不是一个选项,因为createdAt上没有可用于避免排序的索引。
更新:
阅读 Rick 的回答,我意识到仅对 userId
建立索引应该可以加快您的 ORDER BY id
查询速度。在这样的索引中,给定 userId
的条目将按主键排序。因此,使用此索引既可以仅访问所请求的 userId
的行,也可以访问按请求的排序顺序(按 id
)的行。
关于mysql - 按 id 排序时非常慢,但按时间戳、id 排序时速度很快,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57839845/