limit offset慢查詢背后的原因與解法

問(wèn)題

問(wèn)題起源于一個(gè)涉及到數(shù)據(jù)遍歷的腳本。

該腳本會(huì)對(duì)一個(gè)MySQL表中的數(shù)據(jù)進(jìn)行有條件的全表遍歷。SQL如下:

select * from table where update_time < CURDATE() order by update_time desc limit 100 offset 10000;

這樣寫看起來(lái)很正常系吭,但實(shí)際在數(shù)據(jù)量大了之后,使用起來(lái)開始出現(xiàn)問(wèn)題颗品,越來(lái)越慢肯尺,慢到不可接受,甚至影響其他的讀寫操作躯枢。

分析

原因就是limit offset這個(gè)語(yǔ)句则吟,并不如人們望文生義想的那樣,直接定位到第10000位然后取后面的100條記錄锄蹂。

而是令人發(fā)指的先一直一條一條讀取到10100條氓仲,然后再根據(jù)offset的設(shè)置,舍棄前10000條記錄,返回后面的100條記錄敬扛。

其實(shí)原因也好理解晰洒,MySQL的數(shù)據(jù)存儲(chǔ)并不是一個(gè)數(shù)組,可以直接根據(jù)下標(biāo)獲取第X位啥箭。即使給你搜索的字段加了索引谍珊,也只是使用該字段的值去建立一個(gè)新的二叉樹(索引二叉樹),來(lái)方便你快速找到數(shù)據(jù)位置急侥。

但是試想一下砌滞,當(dāng)你要在二叉樹中找到第n大的數(shù)時(shí),你并不能像找一個(gè)具體的值一樣利用二叉樹的能力快速找到坏怪,因?yàn)槟阋膊恢烂總€(gè)節(jié)點(diǎn)的左子樹和右子樹分別有多少記錄贝润。

因此只能借用索引二叉樹是個(gè)B+樹這一特點(diǎn),去利用葉子節(jié)點(diǎn)上的鏈表铝宵,去遍歷你要數(shù)的所有節(jié)點(diǎn)打掘。

這還不止。

MySQL不僅僅會(huì)讓你遍歷一遍索引值鹏秋,我們知道MySQL默認(rèn)的InnoDB引擎分為主鍵索引二叉樹和輔助索引二叉樹尊蚁,你使用其他自己定義的索引時(shí),只是得到主鍵拼岳,真正取數(shù)據(jù)還得根據(jù)索引得到的主鍵枝誊,去主鍵索引二叉樹獲取到具體的數(shù)據(jù)况芒。

那此時(shí)惜纸,實(shí)際上你不僅在無(wú)效遍歷前10000個(gè)索引節(jié)點(diǎn),MySQL還會(huì)讓你去根據(jù)遍歷到的這10000個(gè)無(wú)效索引節(jié)點(diǎn)去真正地查10000次數(shù)據(jù)绝骚,這就是10000次無(wú)效的數(shù)據(jù)查詢耐版。

為什么MySQL一定要讓你去查這些無(wú)效數(shù)據(jù)呢?因?yàn)镸ySQL的實(shí)現(xiàn)分為引擎層和數(shù)據(jù)層压汪,limit offset只能作用于引擎層返回的結(jié)果集粪牲,因此對(duì)引擎層來(lái)說(shuō),他也不知道前10000個(gè)是會(huì)扔掉的數(shù)據(jù)止剖,只能先一股腦地往上傳腺阳。

更進(jìn)一步的,為什么MySQL不把limit offset直接傳給引擎層呢穿香?是因?yàn)椴樵冋Z(yǔ)句實(shí)際是由一個(gè)個(gè)算子組合起來(lái)的亭引,比如有選擇算子(where條件)、連接算子(join)皮获、投影算則(select的字段)焙蚓、數(shù)據(jù)源等,不同的算子有計(jì)算順序,導(dǎo)致底層的算子是不知道上層計(jì)算條件的购公。

總得來(lái)說(shuō)萌京,這種實(shí)現(xiàn)就導(dǎo)致,數(shù)據(jù)量越大宏浩,offset得越多知残,速度就會(huì)越慢,對(duì)MySQL的壓力就會(huì)越大绘闷。

解法

知道了問(wèn)題根源之后橡庞,就可以對(duì)應(yīng)地找解法。

解法1

比如我這里是要遍歷數(shù)據(jù)印蔗,既然用offset遍歷有性能問(wèn)題扒最,那就直接用主鍵id的范圍條件來(lái)縮小范圍。

select * from table where id > 10000 limit 100;

根據(jù)上面的分析我們可以指導(dǎo)华嘹,這樣做吧趣,一方面直接省去了一次查詢索引二叉樹后再查主鍵二叉樹的過(guò)程,而是直接就查主鍵二叉樹并獲取其節(jié)點(diǎn)上的數(shù)據(jù)耙厚。

另一方面强挫,用大于的條件,從而利用好二叉樹的特性薛躬,快速查找到數(shù)據(jù)的起始節(jié)點(diǎn)俯渤,然后獲取其后的100條記錄數(shù)據(jù)即可。

理解清楚型宝,這和offset找第100001條節(jié)點(diǎn)的實(shí)現(xiàn)機(jī)制有本質(zhì)區(qū)別八匠。

不過(guò)如果此時(shí)使用explain對(duì)SQL性能進(jìn)行檢查,會(huì)發(fā)現(xiàn)rows的數(shù)量等于id > 10000后剩余的總記錄數(shù)趴酣,而不是我們limit的100梨树,比如總共如果有15000條記錄,那此時(shí)的rows會(huì)是5000岖寞。

那這是否說(shuō)明sql需要遍歷id > 10000的所有記錄呢抡四?

不是的。explain得出的rows只是一個(gè)估算值仗谆。

實(shí)際上根據(jù)《MySQL EXPLAIN limits and errors》 一文所說(shuō)指巡,explain時(shí),是不會(huì)考慮“LIMIT”的隶垮。

LIMIT is not taken into account while estimating number of rows

因此explain出的rows是id > 10000后剩余的總記錄數(shù)藻雪,是符合預(yù)期的,而實(shí)際執(zhí)行時(shí)岁疼,只會(huì)遍歷到limit的數(shù)量就會(huì)結(jié)束了阔涉。

這種做法在20W的數(shù)據(jù)量級(jí)下缆娃,經(jīng)過(guò)測(cè)試查詢性能可以提升43倍。

解法2

上面的做法基本只適用于遍歷的簡(jiǎn)單場(chǎng)景瑰排,從而可以直接使用主鍵去查詢贯要。

但大部分場(chǎng)景下,業(yè)務(wù)的查詢都是附帶條件的椭住,也就是說(shuō)必須要用到輔助的索引二叉樹崇渗。

前面說(shuō)了,如果用非主鍵的索引去遍歷京郑,會(huì)導(dǎo)致兩次對(duì)二叉樹的查詢操作:先查索引二叉樹找到節(jié)點(diǎn)的主鍵宅广,再查主鍵索引二叉樹取具體數(shù)據(jù)。

此時(shí)如果想實(shí)現(xiàn)一種條件下的翻頁(yè)效果些举,直觀可能會(huì)這樣寫SQL:

select * from table where update_time < CURDATE() limit 100 offset 10000;

此時(shí)MySQL經(jīng)歷的就是先根據(jù)條件找到10100條符合條件的記錄(經(jīng)過(guò)兩個(gè)二叉樹的查詢)跟狱,然后再拋棄前10000條。

那這里可以利用子查詢不會(huì)真正獲取數(shù)據(jù)的特性户魏,進(jìn)行優(yōu)化:

select * from table where id in (select id from table where update_time < CURDATE()) limit 100 offset 10000;

注意這里子查詢是根據(jù)輔助索引去查的驶臊,而主查詢只根據(jù)了主鍵去查。

在子查詢中并不會(huì)真正去訪問(wèn)主鍵索引二叉樹獲取數(shù)據(jù)叼丑,所以免去了10000次無(wú)效查詢关翎。

在子查詢獲取到id后,再用IN查詢?nèi)ピ谥麈I索引二叉樹上遍歷數(shù)據(jù)鸠信。

這種做法雖然也要查詢10000條無(wú)用的數(shù)據(jù)纵寝,但由于是直接使用主鍵索引,所以比直接查詢limit offset的做法會(huì)快兩倍左右星立。

解法3

用IN操作爽茴,對(duì)于量大的情況始終不太優(yōu)雅,因此還可以考慮用JOIN替代IN贞铣,自己JOIN自己:

select * from table as t1 inner join (select id from table where update_time < CURDATE() order by update_time desc limit 100 offset 10000) as t2 using (id);

這種做法經(jīng)過(guò)測(cè)試會(huì)比最原始的SQL快10倍闹啦。

這里還需要注意的是沮明,MySQL的JOIN有一個(gè)優(yōu)化點(diǎn)辕坝,即用小表做驅(qū)動(dòng)表去驅(qū)動(dòng)大表。

比如對(duì)于 t1 left join t2 的情況荐健,就建議把記錄數(shù)較小的表放在前面酱畅,前面的表示驅(qū)動(dòng)表,會(huì)掃描t1所有記錄然后再去t2查詢江场。

如果t1有M條記錄纺酸,t2 N條,使用t2的索引的情況下址否,時(shí)間復(fù)雜度是M * logN左右餐蔬,因此M的影響碎紊,也即t1的記錄數(shù)對(duì)時(shí)間影響更大。

不過(guò)這里由于使用的是INNER JOIN樊诺,MySQL對(duì)INNER JOIN會(huì)自動(dòng)使用小表仗考,因此問(wèn)題不大,實(shí)測(cè)下來(lái)耗時(shí)也相差無(wú)幾词爬。

更多解法

其實(shí)可以選擇的解法還有很多秃嗜,比如從業(yè)務(wù)層面限制要訪問(wèn)的數(shù)據(jù),比如分表顿膨,比如其他奇詭的索引用法锅锨。

此外,這里介紹的解法恋沃,也更多地針對(duì)MySQL默認(rèn)使用的InnoDB引擎去做優(yōu)化必搞,在不同的數(shù)據(jù)庫(kù)存儲(chǔ)引擎下,可能會(huì)有其他更合適的解法囊咏。


關(guān)注我的公眾號(hào)【月亮與二進(jìn)制】顾画,鵝廠程序員的敲碼間隙,也能讀書觀影練劍寫字匆笤,分享給你我的世界

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末研侣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子炮捧,更是在濱河造成了極大的恐慌庶诡,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,589評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咆课,死亡現(xiàn)場(chǎng)離奇詭異末誓,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)书蚪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,615評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門喇澡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人殊校,你說(shuō)我怎么就攤上這事晴玖。” “怎么了为流?”我有些...
    開封第一講書人閱讀 165,933評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵呕屎,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我敬察,道長(zhǎng)秀睛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,976評(píng)論 1 295
  • 正文 為了忘掉前任莲祸,我火速辦了婚禮蹂安,結(jié)果婚禮上椭迎,老公的妹妹穿的比我還像新娘。我一直安慰自己田盈,他們只是感情好侠碧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,999評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缠黍,像睡著了一般弄兜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓷式,一...
    開封第一講書人閱讀 51,775評(píng)論 1 307
  • 那天替饿,我揣著相機(jī)與錄音,去河邊找鬼贸典。 笑死视卢,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的廊驼。 我是一名探鬼主播据过,決...
    沈念sama閱讀 40,474評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼妒挎!你這毒婦竟也來(lái)了绳锅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,359評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤酝掩,失蹤者是張志新(化名)和其女友劉穎鳞芙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體期虾,經(jīng)...
    沈念sama閱讀 45,854評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡原朝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,007評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了镶苞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喳坠。...
    茶點(diǎn)故事閱讀 40,146評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖茂蚓,靈堂內(nèi)的尸體忽然破棺而出壕鹉,到底是詐尸還是另有隱情,我是刑警寧澤煌贴,帶...
    沈念sama閱讀 35,826評(píng)論 5 346
  • 正文 年R本政府宣布御板,位于F島的核電站锥忿,受9級(jí)特大地震影響牛郑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜膏执,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,484評(píng)論 3 331
  • 文/蒙蒙 一奈虾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧邦鲫,春花似錦础芍、人聲如沸杈抢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,029評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)惶楼。三九已至,卻和暖如春诊杆,著一層夾襖步出監(jiān)牢的瞬間歼捐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,153評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工晨汹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留豹储,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,420評(píng)論 3 373
  • 正文 我出身青樓淘这,卻偏偏與公主長(zhǎng)得像剥扣,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子铝穷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,107評(píng)論 2 356

推薦閱讀更多精彩內(nèi)容