本章涉及的內容是TiDB的計算層代碼,就是咱們編譯完 TiDB 後在bin目錄下生成的 tidb-server 的可執行文件,它是用 go 實現的,裏面對 TiPD 和 TiKV實現了Mock,能夠單獨運行;node
用explain語句能夠看到一條sql在TiDB中生成的最終執行計劃,例如:咱們有一條關聯子查詢: select * from t1 where t1.a in (select t2.a from t2 where t2.b = t1.b);mysql
tidb> explain select * from t1 where t1.a in (select t2.a from t2 where t2.b = t1.b); +--------------------------+-------+------+--------------------------------------------------------------------------------------------+ | id | count | task | operator info | +--------------------------+-------+------+--------------------------------------------------------------------------------------------+ | HashLeftJoin_9 | 4.79 | root | semi join, inner:TableReader_15, equal:[eq(test.t1.b, test.t2.b) eq(test.t1.a, test.t2.a)] | | ├─TableReader_12 | 5.99 | root | data:Selection_11 | | │ └─Selection_11 | 5.99 | cop | not(isnull(test.t1.a)), not(isnull(test.t1.b)) | | │ └─TableScan_10 | 6.00 | cop | table:t1, range:[-inf,+inf], keep order:false, stats:pseudo | | └─TableReader_15 | 5.99 | root | data:Selection_14 | | └─Selection_14 | 5.99 | cop | not(isnull(test.t2.a)), not(isnull(test.t2.b)) | | └─TableScan_13 | 6.00 | cop | table:t2, range:[-inf,+inf], keep order:false, stats:pseudo | +--------------------------+-------+------+--------------------------------------------------------------------------------------------+
sql算子差很少是這個樣子的:sql
上面是一個物理查詢計劃樹,頂層算子是Join算子,t1表和t2表的數據關聯操做用的是Hash Join,由於只返回左表(Outer Plan)的數據,因此用了左鏈接(Left Join); Join條件是 t1.b = t2.b,t1.a = t2.a,其中內表數據取自 TableReader_15---就是t2表的數據,t2表做爲 Inner Plan的數據;數據庫
下層左右兩邊的算子相似,都是TableReader接了Selection算子,Selection算子負責過濾掉空數據;底層算子是2個最基本的掃表算子(TableScan),分別掃 t1 和 t2 的數據,返回給上層算子;session
TiDB代碼中,explain語句和select語句相似,都是下面這樣的處理邏輯:架構
clientConn.handleQuery---處理mysql客戶端來的請求 TiDBContext.Execute session.execute session.ParseSQL---解析SQL Compiler.Compile---遍歷SQL語法樹,生成邏輯計劃樹和物理計劃樹 session.executeStatement clientConn.writeResultset clientConn.writeChunks ResultSet.Next---Next函數驅動數據行的獲取 clientConn.writePacket---將數據寫回客戶端
explain語句結果生成的代碼在這裏:oracle
ExplainExec.Next ExplainExec.generateExplainInfo Explain.RenderResult Explain.explainPlanInRowFormat---遍歷物理執行計劃樹,格式化輸出
Explain.explainPlanInRowFormat 會從根節點開始遞歸的訪問PhysicalPlan和子樹,遍歷物理執行計劃樹,生成explain的結果集;框架
explain顯示的信息是最終優化生成的執行計劃;函數
咱們接着來看看中間生成的執行計劃是怎樣的:oop
sql的解析和生成邏輯計劃樹的代碼在這裏:
func Optimize(ctx sessionctx.Context, node ast.Node, is infoschema.InfoSchema) (plannercore.Plan, error) { //根據sql語法樹(ast.Node)生成邏輯執行計劃(LogicalPlan) func (b *PlanBuilder) Build(node ast.Node) (Plan, error) { func (b *PlanBuilder) buildExplainPlan(targetPlan Plan, format string, analyze bool, execStmt ast.StmtNode) (Plan, error) { func (b *PlanBuilder) buildSelect(sel *ast.SelectStmt) (p LogicalPlan, err error) { // DoOptimize optimizes a logical plan to a physical plan. //將邏輯執行計劃樹(LogicalPlan)轉爲物理執行計劃樹(PhysicalPlan) func DoOptimize(flag uint64, logic LogicalPlan) (PhysicalPlan, error) {
buildExplain 調用 buildSelect 來生成 select 語句的邏輯執行計劃樹 (LogicalPlan),接着調用DoOptimize來進行優化;好比相似這樣的優化:對關聯子查詢進行改寫,應用關係代數的一些規則,將 in子句 轉爲 semi join,由於 semi join 能夠有多種方式進行高效的數據集鏈接操做;
而後,咱們看看優化以前的執行計劃:
這個執行計劃大致是這樣的,執行計劃的根節點是一個投影算子(Projection),取表 t1 的兩個列 t1.a 和 t1.b;根節點下面是一個 Apply 算子,Apply 算子是爲了知足關聯子查詢的須要,子查詢語句中用到了外面的結果集,就像咱們示例的 sql,子查詢裏面的選擇算子是 t2.b = t1.b,t1.b 就是關聯到外部執行計劃的列;
Apply關聯了兩個算子,一個是上圖中左邊的DataSource,這個DataSource算子是對t1的掃表操做;另外一個算子是上圖中的 下面那個 LogicalProjection 算子,這個算子下面接了選擇算子(LogicalSelection),至關於執行 t1.b = t2.b 的操做,而後是掃表算子,對 t2 進行掃表;
相對於咱們這條語句,Apply算子大概是這樣的執行步驟:
從掃表算子(TableScan)中獲取一條 t1 的數據;
從投影算子(Projection)獲取一條 t2 的數據;
投影算子調用選擇算子(Selection);
選擇算子拿的外部關聯的數據 t1.a;
選擇算子對 t2 掃表,條件是 t2.b = t1.b,獲取 t2.a;其中,t1.b 相對於Apply 算子是外部計劃的列;
執行一個標量算子,判斷 t1.a = t2.a;無論 t2.a 的記錄有多少,只要有一條知足,即成功;
能夠看到,對關聯子查詢,須要執行嵌套循環(NestedLoop),對 t1 的每條記錄循環,傳給 Apply算子, Apply算子再用這條記錄去 t2 表對每條記錄執行循環;
而咱們看到 explain 語句顯示的最終執行計劃,是用 semi join 來改寫的,t1 和 t2 用 semi join 來鏈接,鏈接條件是 t1.a = t2.a and t1.b = t2.b,鏈接方式是左半鏈接;
說的直白一點就是:t1 和 t2 作 天然鏈接獲取左表數據,而後對結果集去重;
semi join有不少種策略,mysql支持差很少5種 Semi Join;
TiDB執行計劃優化代碼在這裏:
// DoOptimize optimizes a logical plan to a physical plan. func DoOptimize(flag uint64, logic LogicalPlan) (PhysicalPlan, error) { ...... logic, err := logicalOptimize(flag, logic) ...... physical, err := physicalOptimize(logic) ...... finalPlan := postOptimize(physical) return finalPlan, nil }
調用 PlanBuilder.buildSelect 生成的邏輯計劃(LogicalPlan),會傳到 DoOptimize 進行優化,DoOptimize主要作了兩件事:
一件事作基於規則的優化(RBO),應用關係代數規則,對關係代數進行等價改寫,生成更優的執行計劃,例如:一些簡單的關係代數轉換---謂詞下推,常量摺疊,子查詢展開,投影合併等等;或者一些更高級的關係代數轉換,子查詢轉 semi join,子查詢轉各類鏈接,等等;基本上涉及到數據集鏈接或者有子集嵌套的sql優化起來難度要大不少;要應用到的關係代數規則也更多;
另外一件事是作基於代價的優化(CBO),這個我也不懂 😄;
在傳統數據庫如 mysql, oracle中,這些優化作完以後已是極限了,可是在NewSQL中,還有更多的優化空間,例如:謂詞下推,列剪裁這種簡單的規則,能夠從計算節點下推到KV節點上;再好比:NewSQL中的KV是多副本和Range分片的,這樣能夠將計算分佈在不一樣的節點上;
傳統數據庫都是基於關係代數設計的,因此對集合鏈接,集合條件過濾操做很是友好;可是隨着數據的增加,咱們在數據庫架構上不得不使用分片來提升海量數據的讀寫能力;
分片能夠得到優良的單條數據的讀寫能力,可是失去了數據庫的事務性;比起犧牲事務性,更大的問題是犧牲了傳統的關係代數運算,分片以後,多個表的數據鏈接變得異常困難,對於子查詢再嵌套子查詢,幾乎是沒法完成的任務;
爲了折衷,大部分企業在分片的數據庫集羣下游接了一個OLAP集羣,來知足傳統意義上的關係代數操做;
關係代數最先是由早期的SQL提出的;
後來各類大數據平臺Hadoop,HBase和 MapReduce 計算框架出現後,碰到不少數據集和數據過濾的問題,不得不經過關係代數來解決,因而人們將目光轉回到了傳統的關係代數上,Hive,Calcite這種支持SQL的外掛引擎出現了,這些引擎實現了SQL的解析和規則優化,咱們只須要將算子應用到底層的數據存儲就能夠了;
結尾;
附:關係代數幾個經常使用的符號,這些符號能夠對應到SQL裏面的算子,算子優化是經過關係代數的等價轉換來進行的:
σF(R):對集合R選擇;--- 至關於 where t1.b=t2.b;
ΠA(R):對集合R投影;--- 至關於 select t1.a, t1.b;
R×S:鏈接;這是關係代數最重要的操做,鏈接有不少種,天然鏈接,左鏈接,右鏈接,笛卡爾積,Semi Join 等等,子查詢這種場景一般被轉換爲了各類鏈接;
R A⊗ E:Apply,對R中的每條記錄,代入E中進行運算,而後把記錄作各類鏈接;