按 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:

的查询有两个备选查询计划
  1. 读取所有符合条件的行,对它们进行排序,然后选择前 n 行。
  2. 按排序顺序读取行,当找到 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
  1. userId先由=测试,从而缩小索引的部分来看。

  2. 省略 IN 测试的列。如果IN中有多个值,我们就不能使用步骤4.

  3. createdAt 按范围进一步过滤。

  4. createdAtid相同方向(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