帶你讀 MySQL 源碼:Limit,Offset

我一直想寫(xiě) MySQL 源碼分析文章,希望能夠達(dá)成 2 個(gè)目標(biāo):
- 不想研究源碼的朋友,可以通過(guò)文章了解 MySQL 常用功能的實(shí)現(xiàn)邏輯,做到知其然,也知其所以然。
- 想研究源碼的朋友,能夠以文章為切入點(diǎn),邁進(jìn) MySQL 源碼研究之門(mén)。
目標(biāo)是明確的,任務(wù)是艱巨的。
MySQL 源碼數(shù)量龐大,各種功能的代碼盤(pán)根錯(cuò)節(jié),相互交織在一起,形成一張復(fù)雜的網(wǎng)。
想要把這張網(wǎng)中的某些部分拎出來(lái)寫(xiě)成文章,還要做到通俗易懂,這并不是件容易的事,我也就遲遲沒(méi)有動(dòng)手。
萬(wàn)事開(kāi)頭難,但是再難,總得開(kāi)始,才能有后續(xù),所以,就有了這篇文章。
寫(xiě)文章是件費(fèi)時(shí)費(fèi)力的事,寫(xiě)出來(lái)了總希望有更多人看,否則就沒(méi)有寫(xiě)下去的動(dòng)力了。
對(duì) MySQL 源碼感興趣的朋友們,如果想看到源碼分析系列的更多文章,請(qǐng)幫忙把文章傳播出去,分享給更多人。
嘮叨完前因后果,再說(shuō)說(shuō)我準(zhǔn)備怎么寫(xiě)這個(gè)系列文章:
- 我會(huì)挑一些常用功能,每篇文章介紹一個(gè)單點(diǎn)功能的源碼,從簡(jiǎn)單功能開(kāi)始,逐漸過(guò)渡到復(fù)雜功能。
- 每篇文章只會(huì)介紹核心源碼邏輯,源碼之中增加注釋?zhuān)创a之外盡可能用文字展開(kāi)介紹源碼邏輯,以幫助大家更好的理解源碼。
- 每篇文章不會(huì)太長(zhǎng),如果功能復(fù)雜導(dǎo)致內(nèi)容太長(zhǎng),我會(huì)拆分文章,盡量降低大家的閱讀負(fù)擔(dān)。
接下來(lái),我們開(kāi)始源碼分析系列的第 1 篇文章。
本文內(nèi)容基于 MySQL 8.0.32 源碼。
正文
1、準(zhǔn)備工作
創(chuàng)建測(cè)試表:
插入測(cè)試數(shù)據(jù):
示例 SQL:
2、整體介紹
我們先通過(guò) explain 來(lái)看一下執(zhí)行計(jì)劃:

從 explain 輸出可以看到,執(zhí)行計(jì)劃比較簡(jiǎn)單,SQL 執(zhí)行過(guò)程包含 2 個(gè)迭代器:
- Limit/Offset,對(duì)應(yīng) LimitOffsetIterator 迭代器。
- Table scan,對(duì)應(yīng) TableScanIterator 迭代器。
代碼執(zhí)行時(shí)堆棧如下:
3、源碼分析
TableScanIterator 迭代器用于從存儲(chǔ)引擎讀取記錄,留到以后的文章介紹。
limit, offset 由 LimitOffsetIterator 迭代器實(shí)現(xiàn),我們會(huì)介紹兩個(gè)方法的代碼:
- Query_expression::ExecuteIteratorQuery(THD*),這是查詢(xún)?nèi)肟诜椒?,介紹了它,流程才算完整。
- LimitOffsetIterator::Read(),limit, offset 的邏輯都在這個(gè)方法里實(shí)現(xiàn)。
(1)ExecuteIteratorQuery()
從以上代碼可以看到,select 查詢(xún)?nèi)肟诜椒ǖ闹黧w是一個(gè)無(wú)限 for 循環(huán)。
每一輪循環(huán)都會(huì)調(diào)用 m_root_iterator->Read() 方法從存儲(chǔ)引擎讀取一條記錄。
對(duì)于示例 SQL 來(lái)說(shuō),m_root_iterator->Read() 就是 LimitOffsetIterator::Read()。
for 循環(huán)會(huì)一直執(zhí)行,直到 m_root_iterator->Read() 的返回值命中以下任意一個(gè)條件才會(huì)結(jié)束:
- if (error > 0 || thd->is_error()),讀取出錯(cuò)了,以錯(cuò)誤狀態(tài)結(jié)束查詢(xún)。
- if (error < 0),已經(jīng)讀完所有符合條件的記錄,以正常狀態(tài)結(jié)束查詢(xún)。
- if (thd->killed),SQL 被客戶(hù)端通過(guò) kill <query_id> 干掉了,中止查詢(xún)。
<query_id> 為 show processlist 中的 Id 字段。
- for 循環(huán)中,每次從存儲(chǔ)引擎讀取到一條記錄,都會(huì)調(diào)用 query_result->send_data(thd, *fields) 方法。
對(duì)于示例 SQL 來(lái)說(shuō),這個(gè)方法的行為就是把記錄發(fā)送給客戶(hù)端。
(2)LimitOffsetIterator::Read()
除了處理 offset 邏輯之外,LimitOffsetIterator::Read() 每次只讀取一條記錄,這個(gè)方法的核心邏輯分為三部分:
第 1 部分:if (m_needs_offset),SQL 語(yǔ)句中指定了 offset,返回第一條記錄給客戶(hù)端之前,需要讀取 offset 條記錄并丟棄,從第 offset + 1 條記錄開(kāi)始返回給客戶(hù)端。
這部分的主要邏輯是一個(gè) for 循環(huán),會(huì)循環(huán) offset 次,每次讀取一條記錄。
如果讀取成功,就接著讀取下一條記錄,而不會(huì)對(duì)這條記錄做任何操作,也就相當(dāng)于丟棄了。
如果讀取失敗,直接返回錯(cuò)誤碼,讀取結(jié)束,客戶(hù)端會(huì)收到報(bào)錯(cuò)信息。
第 2 部分:if (m_seen_rows >= m_limit),表示已經(jīng)讀取了 m_limit 條記錄,返回 -1 表示讀取正常結(jié)束。
m_limit = SQL 中的 limit + offset。
第 3 部分:result = m_source->Read() 從存儲(chǔ)引擎讀取一條記錄,然后,把結(jié)果返回給 Query_expression::ExecuteIteratorQuery() 方法。
4、總結(jié)
limit, offset 邏輯比較簡(jiǎn)單,全部由 LimitOffsetIterator::Read() 實(shí)現(xiàn),核心邏輯總結(jié)如下:
- 從存儲(chǔ)引擎讀取返回給客戶(hù)端的第 1 條記錄之前,會(huì)先讀取 offset 條記錄并丟棄,然后再讀取一條記錄,用于返回給客戶(hù)端。
- 從存儲(chǔ)引擎讀取第 2 ~ limit + offset 條記錄時(shí),每讀取一條記錄,都返回給 Query_expression::ExecuteIteratorQuery(),由該方法把記錄返回給客戶(hù)端。
- 讀取 limit + offset 條記錄之后,返回 -1 表示讀取流程正常結(jié)束。
從 LimitOffsetIterator::Read() 的實(shí)現(xiàn)邏輯來(lái)看,offset 越大,讀取之后被丟棄的記錄就越多,讀取這些記錄所做的都是無(wú)用功。
為了提高 SQL 的執(zhí)行效率,可以通過(guò)改寫(xiě) SQL 讓 offset 盡可能小,理想狀態(tài)是 offset = 0。
本文轉(zhuǎn)載自微信公眾號(hào)「一樹(shù)一溪」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系一樹(shù)一溪公眾號(hào)。






























