按ID排序时非常慢,但按时间戳记ID时很快

时间:2019-09-08 07:07:32

标签: mysql indexing sql-order-by primary-key

我遇到了一个非常令人困惑的优化案例。我不是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;

这是一张大桌子。 1亿行。 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。

但是,当我进行一些细微调整时,即更改了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,而在最终使用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;,但没有任何反应。

3 个答案:

答案 0 :(得分:1)

对于针对ORDER BY ... LIMIT n的查询,MySQL有两个备选查询计划:

  1. 阅读所有符合条件的行,对它们进行排序,然后选择n个前几行。
  2. 按排序顺序读取行,并在找到n个符合条件的行时停止。

为了确定哪个是更好的选择,优化器需要估计WHERE条件的过滤效果。这不是直截了当的,特别是对于没有索引的列或与值相关的列。在您的情况下,MySQL优化器显然认为第二种策略是最好的。换句话说,它没有看到WHERE子句不会被任何行满足,而是认为2%的行将满足WHERE子句,并且仅通过扫描部分WHERE子句就能找到20行。表格以PRIMARY键顺序倒退。

如何估计WHERE子句的过滤效果在5.6、5.7和8.0之间变化很大。如果您使用的是MySQL 8.0,则可以尝试为所涉及的列创建直方图,以查看是否可以改善估计。如果没有,我认为您唯一的选择是使用FORCE INDEX提示使优化器选择所需的索引。

对于您的快速查询,第二个策略不是一个选项,因为createdAt上没有可用于避免排序的索引。

更新: 阅读Rick的答案,我意识到只有userId上的索引才可以加快ORDER BY id查询的速度。在这样的索引中,给定userId的条目将按主键排序。因此,使用此索引既可以只访问请求的userId的行,又可以按照请求的排序顺序(按id来访问行)。

答案 1 :(得分:1)

主过滤器与基数估计器一起使用效果很好。当使用限制订购时,这将自动是另一个筛选器,因为数据需要进一步筛选。这可能会使基数估计器重定向到容易产生不正确的估计的过程,最终导致选择不佳的计划。为了证明这一点,请在不使用limit子句的情况下运行24秒查询。它也应以0.3的响应作为技巧。 为了解决此问题,如果仅主过滤器具有标准的非常好的性能,请首先选择此过滤器,然后再选择第二个过滤器,此时结果集将大大小于整个表格。使用类似的东西:

select * from(选择...主选择语句) 按x排序,按y限制

...或... 插入到temp select ... main select语句中 从温度顺序中按x限制,由y选择

答案 2 :(得分:0)

给予

movie

我会尝试

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;
  1. INDEX(userId, createdAt, id) -- in this order 首先由userId测试,从而缩小了要查看的索引部分。

  2. 省略=测试的列。如果IN中有多个值,则无法使用步骤4。

  3. IN根据范围进一步过滤。

  4. createdAtcreatedAt相同方向(id)中进行比较。 (是的,我知道8.0有所改进,但我认为您不想要(ASC,DESC)。)