在資料庫中處理查詢請求時,如果可以盡早的將無關資料過濾掉,那麼後續的運算元就可以少做無用功,提公升整個 sql 的執行效率。過濾資料最常用的手段是使用索引,tidb 的優化器也會盡量採用索引過濾的方式處理請求,利用索引有序的特點來提公升查詢效率。比如當查詢條件為a = 1
時,如果 a 這一列上有索引,我們就可以利用索引很快的把滿足a = 1
的資料拿出來,而不需要逐行檢查 a 的值是否為 1。當然是否會選擇索引過濾也取決於代價估算。
索引分為單列索引和多列索引(組合索引),篩選條件也往往不會是簡單的乙個等值條件,可能是非常複雜的條件組合。tidb 是如何分析這些複雜條件,來得到這些條件在對應的索引上的邏輯區間範圍(range),就是本文要介紹的內容。
關於 tidb 如何構建索引,如何儲存索引資料,希望讀者能夠有基本的了解(參考閱讀:三篇文章了解 tidb 技術內幕 - 說計算 )。
這裡是乙個例子,展示這裡所說的索引範圍計算是做什麼的,建表語句和查詢語句如下:
create table t (a int primary key, b int, c int);
select * from t where ((a > 1 and a < 5 and b > 2) or (a > 8 and a < 10 and c > 3)) and d = 5;
複製**
計算索引邏輯區間範圍的流程如下:
從上圖可以看出,整個流程分為從 filter 中抽取可用索引的表示式以及利用選出的表示式構造資料範圍兩個步驟,接下來分別描述。
這個步驟是從 filter 中將能夠用上索引的表示式選出來。由於單列索引和多列索引在處理邏輯上有很大的不同,所以會分單列索引和多列索引兩中情況進行講解。
單列索引的情況相對來說比較簡單。很多滿足 column op constant 形式的簡單表示式都可以用來計算 range,單個表示式的判斷邏輯在 checker.go 的 conditionchecker 中。而對於包含了 and 或者 or 的複雜情況,我們可以按照下述規則進行處理:
and 表示式無關的 filter 並不會影響其可以計算 range 的子項。所以直接捨去無關的表示即可。以流程圖中的乙個子表示式a > 1 and a < 5 and b > 2
為例,我們只要將b > 2
扔掉,保留a > 1 and a < 5
即可。
or 表示式中,每個子項都要可以用來計算 range,如果有不可以計算 range 的子項,那麼這整個表示式都不可用來計算 range。以a = 1 or b = 2
為例,b = 2
這一子項不可以用來計算 a 的 range,所以這個表示式整體上無法計算 a 的 range。而如果是a > 1 or ( a < 2 and b = 1)
,根據 1 中的規則,第二個子項會留下a < 2
的部分,可以用來計算 a 的 range,因此整個表示式會返回a > 1 and a < 2
來供接下來計算 range 的部分處理。
這裡補充說明一點,tidb 的主鍵在實現方式上限定了只有整數型別的單列主鍵會把主鍵值當做 rowid,然後編碼成 rowkey,和這行資料儲存在一起。其他型別的單列主鍵會作為普通的 unique key 看待,當查詢的列包含索引上沒有的列時,需要一次查索引 + 一次掃表。所以我們將這種整數型別作為主鍵的索引處理邏輯單獨抽取出來,其入口函式為 detachcondsfortablerange 。其中對 and 表示式和 or 表示式的處理入口分別為 detachcolumncnfconditions 和 detachcolumndnfconditions。這兩個函式也用來處理其他型別的主鍵或者索引的的 range 計算。
and 表示式中,只有當之前的列均為點查的情況下,才會考慮下乙個列。
e.g. 對於索引 (a, b, c),有條件a > 1 and b = 1
,那麼會被選中的只有a > 1
。對於條件a in (1, 2, 3) and b > 1
,兩個條件均會被選到用來計算 range。
由於非點查的部分只會涉及到乙個列,所以可以直接復用detachcolumncnfconditions
。
or 表示式中,每個子項會視為 and 表示式分開考慮。與單列索引的情況一樣,如果其中乙個子項無法用來計算索引,那麼該 or 表示式便完全無法計算索引。
多列索引處理的入口函式為 detachcondandbuildrangeforindex,and 表示式和 or 表示式的處理入口分別為 detachcnfcondandbuildrangeforindex 和 detachdnfcondandbuildrangeforindex。(由於多列索引對 range 的處理相對單列索引而言會複雜一些,所以沒有拆分為 detachcondition 和 buildrange 兩部分,而是由 detachcondandbuildrangeforindex 處理。)
由於邏輯進行到了簡化,因此目前 tidb 的 ranger 存在無法正確處理的情況。比如:
這一步驟中,利用上一步抽取出來的表示式估算出資料的邏輯區間範圍,後續會根據這個邏輯區間以及資料編碼方式構造物理區間進行資料訪問。我們仍然分為單列索引和多列索引兩個情況來介紹。
這種情況下,輸入的表示式為 column op constant 形式的簡單表示式由 or 以及 and 連線而成。我們對每乙個具體的操作符,都設計了一段對應的計算 range 的邏輯,當遇到 and 或者 or 時,會對兩個區間求交或者求並。在 point.go 中有乙個 builder 的結構體用來處理上述邏輯。
在這個階段我們記錄 range 時用 rangepoint 的結構來儲存 range。
// point is the end point of range interval.
type point struct
複製**
每個 point 代表區間的乙個端點,其中的 excl 表示端點為開區間的端點還是閉區間的端點。start 表示這個端點是左端點還是右端點。
builder 中每個 buildfrom*** 的方法都是計算乙個具體函式的 range 的方法。比如 buildfromin 便是處理 in 函式 的方法。可以看到它首先對 in 函式的值列表的每個值都構造了乙個 rangpoint 的單點區間,然後對這些區間放在乙個 slice 中做排序以及去重。最終將去重後的結果輸出。
在 pointer.go 中還包含其他各類的函式的處理,具體可以翻閱源**。
除了對具體函式的處理外,pointer.go 中還有區間交和區間並的操作。intersection 和 union 分別代表區間交和區間並。兩個函式的邏輯均通過 merge 方法進行處理,通過傳入乙個 flag 來區分。merge 函式做了如下兩個假設:
merge 函式使用 inrangecount 來記錄當前位置被 a, b 兩個區間序列覆蓋的情況。區間求並的情況時,只要 a, b 兩個區間序列中有乙個區間序列覆蓋便可以作為解輸出,被兩個區間同時覆蓋的端點必然是屬於乙個更大的區間的內部不需要輸出。所以當 inrangecount 為 1 時,即為需要輸出的區間端點。
當區間求交時,需要兩個序列都覆蓋到才是可以輸出的端點,所以當 inrangecount 為 2 時,即為需要輸出的區間端點。
在得到最後的區間端點序列後,由 points2tableranges 轉化為對外暴露的 range 結構,由 buildtablerange 輸出到 plan package。
// newrange represents a range generated in physical plan building phase.
type newrange struct
複製**
在現在的 tidb 中,單列索引和多列索引使用了相同的 range 結構,所以這裡的端點值為 slice 的形式。
對於 or 表示式的情況,由於此時 range 已經無法轉回 point 的結構。所以這裡重新實現了一下區間並的操作。實現的形式便是比較常見的將區間按左端點排序,在依次掃過區間的同時,記錄當前所有重疊過的區間的最右右端點來進行做區間並的演算法。區間並的具體的實現可見 unionranges 方法。
TiDB 原始碼閱讀系列文章(十三)索引範圍計算簡介
在資料庫中處理查詢請求時,如果可以盡早的將無關資料過濾掉,那麼後續的運算元就可以少做無用功,提公升整個 sql 的執行效率。過濾資料最常用的手段是使用索引,tidb 的優化器也會盡量採用索引過濾的方式處理請求,利用索引有序的特點來提公升查詢效率。比如當查詢條件為a 1時,如果 a 這一列上有索引,我...
TiDB 原始碼閱讀系列文章(一)序
tidb 目前獲得了廣泛的關注,特別是一些技術愛好者,希望能夠參與這個專案。由於整個系統的複雜性,很多人並不能很好的理解整個專案。我們希望通過這一系列文章自頂向下,由淺入深,講述 tidb 的技術原理以及實現細節,幫助大家掌握這個專案。一些網路 作業系統的常識 除了上述比較通用的知識之外,還希望讀者...
Tomcat原始碼閱讀系列
再過十來天,就要不再是大三,而步入大四的殿堂了,求職面試的事會接踵而至,鑑於春招時的教訓,自己的學習比較缺乏系統性地整理,向他人交流自己所掌握的技術能力仍有待提高,為此將自己閱讀tomcat原始碼的過程記錄一下,並分享到部落格中,讓有同樣興趣的同學一起交流討論。注 如沒有特別說明的地方,tomcat...