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