我遇到了一个非常令人困惑的优化案例。我不是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;
,但没有任何反应。
答案 0 :(得分:1)
对于针对ORDER BY ... LIMIT n的查询,MySQL有两个备选查询计划:
为了确定哪个是更好的选择,优化器需要估计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;
INDEX(userId, createdAt, id) -- in this order
首先由userId
测试,从而缩小了要查看的索引部分。
省略=
测试的列。如果IN
中有多个值,则无法使用步骤4。
IN
根据范围进一步过滤。
createdAt
和createdAt
在相同方向(id
)中进行比较。 (是的,我知道8.0有所改进,但我认为您不想要(ASC,DESC)。)