最近剛上線的一個sql出現了慢查,經過分析發現了一個很有意思的問題,具體表現爲,select * from tablename where a=xx and b=xx and c=xxx and time<xxx order by time desc; 這條sql的查詢很快的,在60ms以內,但是如果再最後加上了limit 10,查詢就會要2s多。當這個慢查被運維拉出來的時候,感覺非常不能理解。
我先把表結構儘量簡化一下。
CREATE TABLE `tablename` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT
`typeId` tinyint(1) unsigned NOT NULL DEFAULT '0'
`userId` int(11) unsigned NOT NULL COMMENT '用戶id',
`orderId` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '訂單id',
`tradeStatus` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '交易狀態 0.交易中 1.交易關閉 2.交易成功',
`otherStatus` tinyint(1) unsigned NOT NULL DEFAULT '0' ,
`addTime` int(10) unsigned NOT NULL COMMENT '添加時間',
PRIMARY KEY (`id`),
KEY `IDX_tradeStatus_otherStatus_addTime` (`tradeStatus`,`otherStatus`,`addTime`) USING BTREE,
KEY `IDX_tradeStatus_otherStatus_typeId_orderId` (`tradeStatus`,`otherStatus`,`typeId`,`orderId`),
KEY `IDX_addTime` (`addTime`),
KEY `IDX_userId_tradeStatus_addTime` (`userId`,`tradeStatus`,`addTime`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='test'
我最初的查詢sql是
select *
from tablename
where userid= 123
and tradestatus= 2
and orderid= 0
and addTime< 1566139866
order by addtime desc limit 10
看了一下執行計劃,當沒有limit的時候,執行計劃走了最後一條索引,執行計劃的type是ref,rows是22800。當加上limit以後,執行計劃只走了addTime的索引,並且type退化成了range,rows幾乎是全表了。
這是一個很詭異的事情,研究了幾分鐘後發現搞不定,決定先優化它,再研究問題。然後發現了兩種解決方案。
方案一,將查詢語句和limit語句分隔開:
select *
from(
select *
from tablename
where userid= 3307619
and tradestatus= 2
and orderid= 0
and addTime< 1566139866
order by addtime desc) tab limit 10
方案二,使用強制索引語句:
select *
from tablename force index(IDX_userId_tradeStatus_addTime)
where userid= 123
and tradestatus= 2
and orderid= 0
and addTime< 1566139866
order by addtime desc limit 10
處理完後發現這兩個方案其實都是在強行使用了mysql更好的索引,並且第一個解決方案因爲避開了limit反而使用了正確的索引,所以我懷疑有可能是mysql的查詢優化器在遇到limit以後會失效。帶着這個疑問又去查詢資料,終於被我找到一篇帖子。
https://yq.aliyun.com/articles/51065/
這裏把bug產生的原因貼出來,具體的內容大家可以自己去查看。
- 第一階段,優化器選擇了索引 iabc,採用 range 訪問;
- 第二階段,優化器試圖進一步優化執行計劃,使用 order by 的列訪問,並清空了第一階段的結果;
- 第三階段,優化器發現使用 order by 的列訪問,代價比第一階段的結果更大,但是第一階段結果已經被清空了,無法還原,於是選擇了代價較大的訪問方式(index_scan),觸發了bug。