1. 程式人生 > 其它 >Mysql 原始碼解讀-執行器

Mysql 原始碼解讀-執行器

Mysql 原始碼解讀-執行器

一條 sql 執行過程中,首先進行詞法分析和語法分析,然後將由優化器進行判斷,如何執行更有效率,生成執行計劃,後面的任務就交給了執行器。在執行的過程中,執行器就會和儲存引擎互動了,互動是以記錄為單位的。本文我們介紹下 MySQL8的執行器。

在前幾篇文章中,我們講述了 MySQL 的詞法分析和語法分析,以及一條 sql 語句是如何生產 AST 樹的。MySQL 在做完語法解析後,呼叫函式 mysql_execute_command 進入查詢優化器。查詢優化器對 sql 語句進行了一系列的轉換,重寫,優化最終生產了 AccessPath(訪問路徑),並且根據AccessPath建立Iterator迭代器。

火山模型

火山模型是資料庫查詢執行最著名的模型,也是在各種資料庫系統中應用最廣泛的模型。SQL語句在資料庫中經過語法解析生產 AST 語法樹,然後遍歷語法樹,生成執行樹。執行樹的每個節點為代數運算子(Operator)。火山模型把Operator看成迭代器,每個迭代器都會提供一個next() 介面。一般Operator的next() 介面實現分為三步
(1)呼叫子節點Operator的next() 介面獲取一行資料(tuple)
(2)對tuple進行Operator特定的處理(如filter 或project 等)
(3)返回處理後的tuple

因此,查詢執行時會由查詢樹自頂向下的呼叫next() 介面,資料則自底向上的被拉取處理。這種處理方式也稱為拉取執行模型(Pull Based)。例如以下 SQL:

SELECT Id, Name, Age, (Age - 30) * 50 AS Bonus
FROM People
WHERE Age > 30

對應火山模型如下:

其中——

User:客戶端;

Project:垂直分割(投影),選擇欄位;

Select(或 Filter):水平分割(選擇),用於過濾行,也稱為謂詞;

Scan:掃描資料。

這裡包含了 3 個 Operator,首先 User 呼叫最上方的 Operator(Project)希望得到 next tuple,Project 呼叫子節點(Select),而 Select 又呼叫子節點(Scan),Scan 獲得表中的 tuple 返回給 Select,Select 會檢查是否滿足過濾條件,如果滿足則返回給 Project,如果不滿足則請求 Scan 獲取 next tuple。Project 會對每一個 tuple 選擇需要的欄位或者計算新欄位並返回新的 tuple 給 User。當 Scan 發現沒有資料可以獲取時,則返回一個結束標記告訴上游已結束。

為了更好地理解一個 Operator 中發生了什麼,下面通過虛擬碼來理解 Select Operator:

Tuple Select::next() {
    while (true) {
        Tuple candidate = child->next(); // 從子節點中獲取 next tuple
        if (candidate == EndOfStream) // 是否得到結束標記
            return EndOfStream;
        if (condition->check(candidate)) // 是否滿足過濾條件
            return candidate; // 返回 tuple
    }
}

可以看出火山模型的優點在於:簡單,每個 Operator 可以單獨抽象實現、不需要關心其他 Operator 的邏輯。

那麼缺點呢?也夠明顯吧?每次都是計算一個 tuple(Tuple-at-a-time),這樣會造成多次呼叫 next ,也就是造成大量的虛擬函式呼叫,這樣會造成 CPU 的利用率不高。當然也有優化方式,請參考: SQL優化之火山模型、向量化、編譯執行

迭代器模式介紹

MySQL8.0對執行器進行了改進,建立一個新的用於迭代訪問記錄的API,它足夠通用。主要實現了一個通用的 C++ 類介面,叫做 RowIterator,它具有以下成員和函式:

構造和解構函式

Init()  開啟所有必須的資源,也有可能執行部分功能性操作。比如SortingIterator中會進行排序操作,這個函式可以多次呼叫,每次呼叫都會重置迭代器的指示位置。 read()  讀取一行,將行放入記錄快取中 UlockRow()  將一行過濾出結果集後,允許低事務隔離級別釋放該行的所有鎖。
迭代器型別 說明
TableScanIterator 順序掃描,呼叫儲存引擎介面ha_rnd_next獲取一行記錄
IndexScanIterator 全量索引掃描,根據掃描順序,分別呼叫ha_index_next或者ha_index_prev來獲取一行記錄
IndexRangeScanIterator 範圍索引掃描,包裝了下QUICK_SELECT_I,呼叫QUICK_SELECT_I::get_next來獲取一行記錄
SortingIterator 對另一個迭代器輸出進行排序
SortBufferIterator 從緩衝區讀取已經排好序的結果集,(主要給SortingIterator呼叫)
SortBufferIndirectIterator 從緩衝區讀取行ID然後從表中讀取對應的行(由SortingIterator和某些形式的unique操作使用)
SortFileIterator 從檔案中讀取已經排好序的結果集(主要給SortingIterator呼叫)
SortFileIndirectIterator 從檔案讀取行ID然後從表中讀取對應的行(由SortingIterator和某些形式的unique操作使用)
RefIterator 從連線右表中讀取指定key的行
RefOrNullIterator 從連線右表中讀取指定key或者為NULL的行
EQRefIterator 使用唯一key來從連線的右表中讀取行
ConstIterator 從一個只可能匹配出一行的表(Const Table)中讀取一行資料
FullTextSearchIterator 使用全文檢索索引讀取一行資料
DynamicRangeIterator 為每一行呼叫範圍優化器,然後根據需要包裝QUICK_SELECT_I或表掃描
PushedJoinRefIterator 讀取已下推到NDB的連線的輸出
FilterIterator 讀取一系列行,輸出符合條件的行,用來實現WHERE/HAVING
LimitOffsetIterator 從offset開始讀取行,直到滿足limit限制,用來實現LIMIT/OFFSET
AggregateIterator 實現聚集函式並且如果需要的話進行分組操作
NestedLoopiterator  使用巢狀迴圈演算法連線兩個迭代器(內連線,外連線或反連線)
MaterializeIterator 從另一個迭代器讀取結果,並放入臨時表,然後讀取臨時表記錄
FakeSingleRowIterator 返回單行,然後結束。 僅在某些使用const表情況下才使用(例如只有const表,仍然需要一個迭代器來讀)

函式呼叫棧

如下圖所示:呼叫Query_expression::execute函式進入執行階段:

函式ExecuteIteratorQuery淺析

1、is_simple()函式用來判斷一個查詢表示式是否有union或者多級order,如果沒有說明這個查詢語句簡單。就執行​​add_select_number。

2、執行​​ClearForExecution​​函式。在初始化root迭代器之前,把之前的執行迭代器的資料清除。

3、執行​​get_field_list()​​​,獲取查詢表示式的欄位列表,並將所有欄位都放到一個deque中,即​​mem_root_deque<Item*>​​;對於查詢塊的並集,返回在準備期間生成的欄位列表,對於單個查詢塊,儘可能返回欄位列表

4、執行​​start_execution​​,準備執行查詢表示式或DML查詢

5、接下來的一些操作與第二引擎有關,關於該引擎見https://www.h5w3.com/123061.html。Secondary Engine實際上是MySQL sever上同時支援兩個儲存引擎,把一部分主引擎上的資料,在Secondary Engine上也儲存一份,然後查詢的時候會根據優化器的的選擇決定在哪個引擎上處理資料。

6、如果該查詢用於子查詢,那麼重新reset,指向子查詢。

7、接下來是對於複雜句以及簡單句的不同處理,從而給​​send_records_ptr​​賦值。

函式對於這個情況的解釋如下:

We need to accumulate in the first join's send_records as long as  we support SQL_CALC_FOUND_ROWS, since LimitOffsetIterator will use it  for reporting rows skipped by OFFSET or LIMIT. When we get rid of   SQL_CALC_FOUND_ROWS, we can use a local variable here instead.

情況一:如果該查詢塊具有UNION或者多級的ORDER BY/LIMIT的話  UNION with LIMIT的話,​​found_rows()​​用於最外層  LimitOffsetIterator​​​跳過偏移量行寫入​​send_records​​

情況二:如果是個簡單句的話  found_rows()​​直接用到join上。LimitOffsetIterator​​​跳過偏移量行寫入​​send_records​​

情況三:如果是UNION,但是沒有LIMIT。found_rows()​​用於最外層。

8、重置計數器

9、接下來是一個對查詢塊遍歷,逐個釋放記憶體的操作,用以增加併發性並減少記憶體消耗。

10、初始化根迭代器

11、然後for迴圈,從根迭代器一直到引擎的handler,呼叫讀取資料。如果出錯就直接返回。如果收到kill訊號,也返回。在迴圈中對​​send_records_ptr​​進行累加。行計數器++,指向下一行。

12、將​​send_records_ptr​​​賦值給該執行緒的​​current_found_rows​​