膚淺的聊聊關聯子查詢,數據集鏈接,TiDB代碼,關係代數,等等

本章涉及的內容是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中進行運算,而後把記錄作各類鏈接;

相關文章
相關標籤/搜索