MYSQL與TiDB的執行計划


前言

這里采用了tpc-h一個數據庫的數據量來進行查詢計划的對比。並借助tpc-h中的22條查詢語句進行執行計划分析。

mysql采用的是標准安裝,TiDB采用的是單機測試版,這里的性能結果不能說明其性能差異

本文章主要目的是對比Mysql與TiDB在執行sql查詢時的差異。

mysql版本5.7   TiDB版本v2.0.0-rc.4

 

准備階段

數據導入TiDB后是缺少統計信息的:

SHOW STATS_META

 

可以手工進行統計信息的刷新

ANALYZE TABLE nation,region,part,supplier,partsupp,customer,orders,lineitem

刷新后再次查看SHOW STATS_META

 

首先選擇Q17做為例子,進行查詢

select
    sum(l_extendedprice) / 7.0 as avg_yearly
from
    lineitem,
    part
where
    p_partkey = l_partkey
    and p_brand = 'Brand#23' # 指定品牌。 BRAND=’Brand#MN’ ,M和N是兩個字母,代表兩個數值,相互獨立,取值在1到5之間
    and p_container = 'MED BOX' # 指定包裝類型。在TPC-H標准指定的范圍內隨機選擇
    and l_quantity < (
        select
            0.2 * avg(l_quantity)
        from
            lineitem
        where
            l_partkey = p_partkey
    );

 

表結構

CREATE TABLE IF NOT EXISTS part  ( P_PARTKEY     INTEGER NOT NULL,
                          P_NAME        VARCHAR(55) NOT NULL,
                          P_MFGR        CHAR(25) NOT NULL,
                          P_BRAND       CHAR(10) NOT NULL,
                          P_TYPE        VARCHAR(25) NOT NULL,
                          P_SIZE        INTEGER NOT NULL,
                          P_CONTAINER   CHAR(10) NOT NULL,
                          P_RETAILPRICE DECIMAL(15,2) NOT NULL,
                          P_COMMENT     VARCHAR(23) NOT NULL,
              PRIMARY KEY (P_PARTKEY));

CREATE TABLE IF NOT EXISTS lineitem ( L_ORDERKEY    INTEGER NOT NULL,
                             L_PARTKEY     INTEGER NOT NULL,
                             L_SUPPKEY     INTEGER NOT NULL,
                             L_LINENUMBER  INTEGER NOT NULL,
                             L_QUANTITY    DECIMAL(15,2) NOT NULL,
                             L_EXTENDEDPRICE  DECIMAL(15,2) NOT NULL,
                             L_DISCOUNT    DECIMAL(15,2) NOT NULL,
                             L_TAX         DECIMAL(15,2) NOT NULL,
                             L_RETURNFLAG  CHAR(1) NOT NULL,
                             L_LINESTATUS  CHAR(1) NOT NULL,
                             L_SHIPDATE    DATE NOT NULL,
                             L_COMMITDATE  DATE NOT NULL,
                             L_RECEIPTDATE DATE NOT NULL,
                             L_SHIPINSTRUCT CHAR(25) NOT NULL,
                             L_SHIPMODE     CHAR(10) NOT NULL,
                             L_COMMENT      VARCHAR(44) NOT NULL,
                 PRIMARY KEY (L_ORDERKEY,L_LINENUMBER),
                 CONSTRAINT FOREIGN KEY LINEITEM_FK1 (L_ORDERKEY)  references orders(O_ORDERKEY),
                 CONSTRAINT FOREIGN KEY LINEITEM_FK2 (L_PARTKEY,L_SUPPKEY) references partsupp(PS_PARTKEY, PS_SUPPKEY));

part表是20萬 ,而lineitem是600萬,mysql在建立約束時,會自動創建一個索引LINEITEM_FK2(L_PARTKEY, L_SUPPKEY),而TiDB則不會

mysql的查詢時間大概是1秒左右,TiDB的查詢時間大概是30秒。

 

mysql的執行計划:

 mysql首先對part表進行了查詢,由於經過where的處理20萬數據已經被過濾到幾百條了。再與lineitem關聯,最后再處理子查詢。

查詢過程中借用索引,所以大大加快了查詢速度。

 

TiDB的執行計划

TiDB的執行計划比較復雜,需要轉換為查詢樹后,才能看到比較清楚

從下而上的執行,上層收到下層的數據處理后,再向上遞交

84、85、86讀取part表

74、75、76讀取lineitem表

77 將兩者進行join

 

54、49、56、55匯總lineitme表,並進行分組的平均值的計算

在72進行55和77進行融合和再過濾

71、70、20、15過濾匯總和計算,得到最終結果。

 

但由於part表是小表,對linetiem的兩次掃描和計算都很浪費。所以性能不佳。

 

 

Mysql與TiDB的執行計划規則與解讀

由於大家對Mysql和TiDB的執行計划規則不了解,所以解讀會比較困難,但如果掌握了如何解讀執行計划,能夠理解數據庫的執行方式以及進行對應的優化

下面學習一下,執行計划規則與解讀,我們將分別學習兩種數據庫的執行計划,這樣也有利於進行對比

 Mysql執行計划

在SQL語句前添加EXPLAIN可以查詢到對應SQL的執行計划,例如:EXPLAIN select * from part

執行計划共有12列

id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra

 

類別  可選值  解釋說明
 id  

標識執行的順序,按數值大小進行執行,如果數值一樣大,按排列順序執行

id列為null的就表示這是一個結果集,不需要對其進行執行

 select_type    查詢類型
   SIMPLE 簡單查詢(不使用UNION或子查詢)
   PRIMARY 最外層的SELECT語句
   UNION 在UNION結構中的第二個及以上的SELECT語句
   DEPENDENT UNION 在UNION結構中的第二個及以上的SELECT語句,依賴外層查詢
   UNION RESULT UNION的結果
   SUBQUERY 子查詢中的第一個SELECT語句
  DEPENDENT SUBQUERY 子查詢中的第一個SELECT語句,依賴於外層查詢
  DERIVED 子查詢中FROM后面的語句
  MATERIALIZED 物化視圖子查詢
  UNCACHEABLE SUBQUERY 查詢結果沒有被緩存且需要重新外層查詢計算每行數據的子查詢
  UNCACHEABLE UNION 結構中第二個及之后的SELECT語句且沒有生成查詢緩存
table   被查詢的表名
partitions   表的分區,若不是分區表該字段為null,如果是分區表則顯示用到的分區名稱
type  

表連接的類型

性能按排列順序從好至壞,除了all之外,其他的type都可以使用到索引

  system

表中只有一行數據或者是空表,且只能用於myisam和memory表。

如果是Innodb引擎表,type列在這個情況通常都是all或者index

  const 使用唯一索引或者主鍵,返回記錄一定是1行記錄的等值where條件時,通常type是const。其他數據庫也叫做唯一索引掃描
  eq_ref

出現在要連接過個表的查詢計划中,驅動表只返回一行數據,且這行數據是第二個表的主鍵或者唯一索引,且必須為not null,

唯一索引和主鍵是多列時,只有所有的列都用作比較時才會出現eq_ref

  ref

每次和之前的表做連接時,讀取所有符合條件的索引值。

如果連接使用索引的最左邊前綴字段,或者索引不是主鍵或UNIQUE索引,會用到這種連接方式,

也就是說如果連接不能基於每個符合連接條件的索引值選擇出單獨的一行,則會使用這種連接方式。

  fulltext

使用FULLTEXT索引來建立連接

全文索引的優先級很高,若全文索引和普通索引同時存在時,mysql不管代價,優先選擇使用全文索引

  ref_or_null 連接類型類似ref,除此之外,MySQL會額外掃描出包含NULL值的行。這種連接方式通常用於有子查詢的情形下。
  unique_subquery

這種連接方式在某種情況下會代替eq_ref,如value IN (SELECT primary_key FROM single_table WHERE some_expr),

這種方式使用索引查詢功能代替子查詢,以獲得更好的執行效率。

  index_subquery

這種連接方式類似unique_subquery。它會代替IN子查詢,但是它適用於非unique索引的子查詢,

用於in形式子查詢使用到了輔助索引或者in常數列表,子查詢可能返回重復值,可以使用索引將子查詢去重

如value IN (SELECT key_column FROM single_table WHERE some_expr)

  range

使用索引掃描出指定范圍的行。key字段指示使用的索引。key_len指示索引的最大長度。ref字段會顯示NULL

常見於使用>,<,is null,between ,in ,like等運算符的查詢中。

  index_merge

使用索引合並的連接方式。在這種情況下,key字段會包含使用的索引,key_len包含使用索引的最長索引部分。

表示查詢使用了兩個以上的索引,最后取交集或者並集,常見and ,or的條件使用了不同的索引,

官方排序這個在ref_or_null之后,但是實際上由於要讀取數個索引,性能可能大部分時間都不如range

  index 

這種索引連接類型和ALL相同,除了索引樹被掃描到。這會出現在兩種情況下:一、如果該索引是一個覆蓋索引查詢,且只掃描出索引樹。

在這種情況下,Extra字段會顯示Using index。二、通過索引順序來執行全表掃描。

  all 全表掃描數據文件,然后再在server層進行過濾返回符合要求的記錄。
possible_keys      可供選擇的索引。可以有多個用逗號分隔
 key  

實際選擇的索引

select_type為index_merge時,這里可能出現兩個以上的索引,其他的select_type這里只會出現一個

 key_len  

用於處理查詢的索引長度,如果是單列索引,那就整個索引長度算進去,

如果是多列索引,那么查詢不一定都能使用到所有的列,具體使用到了多少個列的索引,這里就會計算進去,沒有使用到的列,這里不會計算進去。

留意下這個列的值,算一下你的多列索引總長度就知道有沒有使用到所有的列了。

要注意,mysql的ICP特性使用到的索引不會計入其中。另外,key_len只計算where條件用到的索引長度,而排序和分組就算用到了索引,也不會計算到key_len中。

 ref  

如果是使用的常數等值查詢,這里會顯示const,如果是連接查詢,被驅動表的執行計划這里會顯示驅動表的關聯字段

如果是條件使用了表達式或者函數,或者條件列發生了內部隱式轉換,這里可能顯示為func

例如:tpch.partsupp.PS_PARTKEY,tpch.partsupp.PS_SUPPKEY表示使用了partsupp表的兩個字段與當前表的索引進行比較

 rows   執行計划中估算的掃描行數,不是精確值
 filtered    表示存儲引擎返回的數據在server層過濾后,剩下多少滿足查詢的記錄數量的比例,注意是百分比,不是具體記錄數。
 extra   查詢的描述信息,種類非常多。這里只列一些常用的。一個查詢中可以有多個種類,使用逗號進行分隔
  using temporary 表示使用了臨時表存儲中間結果。臨時表可以是內存臨時表和磁盤臨時表,執行計划中看不出來,需要查看status變量,used_tmp_table,used_tmp_disk_table才能看出來。
  using where

表示存儲引擎返回的記錄並不是所有的都滿足查詢條件,需要在server層進行過濾。查詢條件中分為限制條件和檢查條件,

5.6之前,存儲引擎只能根據限制條件掃描數據並返回,然后server層根據檢查條件進行過濾再返回真正符合查詢的數據。

5.6.x之后支持ICP特性,可以把檢查條件也下推到存儲引擎層,不符合檢查條件和限制條件的數據,直接不讀取,這樣就大大減少了存儲引擎掃描的記錄數量。extra列顯示using index condition

  distinct 在select部分使用了distinc關鍵字
  no tables used 不帶from字句的查詢或者From dual查詢
  using index 查詢時不需要全表查詢,直接通過索引就可以獲取查詢的數據。
  using intersect 表示使用and的各個索引的條件時,該信息表示是從處理結果獲取交集
  using union 表示使用or連接各個使用索引的條件時,該信息表示從處理結果獲取並集
  using filesort 排序時無法使用到索引時,就會出現這個。常見於order by和group by語句中
  firstmatch(tb_name) 5.6.x開始引入的優化子查詢的新特性之一,常見於where字句含有in()類型的子查詢。如果內表的數據量比較大,就可能出現這個
  loosescan(m..n) 5.6.x之后引入的優化子查詢的新特性之一,在in()類型的子查詢中,子查詢返回的可能有重復記錄時,就可能出現這個

 

TiDB執行計划

TiDB的數據存儲與ti-kv,而數據處理在ti-server分屬與不同應用。ti-server需要數據時,需要先調用ti-kv進行掃描,然后再從ti-kv拿到數據。

在ti-server層面需要執行的過濾/匯總/分組,送到ti-kv掃描時去運行就可以減少傳輸的數據量,加快處理速度。這個操作稱為“下推”

 

同Mysql一樣,在SQL語句前添加EXPLAIN可以查詢到對應SQL的執行計划,例如:EXPLAIN select * from part

執行計划共有6列

id | parents | children | task | operator info | count

 

類別 可選值 解釋說明
id  

operator 的 id,在整個執行計划中唯一的標識一個 operator。

id由兩部分組成:操作類型+序號。

操作類型有很多種,也代表提供了不同的處理能力,如TableReader 和 TableScan等等

序號是創立執行計划時生成的,大小無作用,只是為了避免重復。

 

執行計划從上至下的方式運行,任務內可能會有並行,例如到多個ti-kv上提取數據

任務間也會有並行,具體看任務實現,union算子就會驅動所有的child同時執行。

而對於無關聯的任務,可能就不會並行了。(未得到官方確認)

 parents  

這個 operator 的 parent。目前的執行計划可以看做是一個 operator 構成的樹狀結構

數據從 child 流向 parent,每個 operator 的 parent 有且僅有一個

 children    這個 operator 的 children,也即是這個 operator 的數據來源
 task  

 當前的執行計划在 task 級別的拓撲關系是一個 root task 后面可以跟許多 cop task,

root task 使用 cop task 的輸出結果作為輸入。

cop task 中執行的也即是 tidb 下推到 tikv 上的任務,每個 cop task 分散在 tikv 集群中,由多個進程共同執行

  root 在tidb-server 上執行的任務
  cop 在tikv上執行的任務
 count    預計當前 operator 將會輸出的數據條數,基於統計信息以及 operator 的執行邏輯估算而來
 operator info    操作類型中會輸出的明細信息,需要結合操作類型一起看

 

下面這個表格專門對operator相關內容進行說明

類別 操作類型 info信息樣例值 解釋說明
數據讀      
  TableScan   在ti-kv上進行數據掃描
    table:part 操作的表名,這里指的是操作part表
    range:[-inf,+inf] range的范圍從-inf開始到+inf結束。如果沒有開始或者結束使用<nil>,例如range:[<nil>,+inf]
    keep order:false 是否進行排序:true排序,false不排序
  TableReader   ti-server從ti-kv讀取數據的操作
    data:Selection_85 這里是指Ti-Server拿到ti-kv掃描結果Selection_85的數據
索引讀    

 

  IndexReader  

直接從索引中讀取索引列,適用於 SQL 語句中僅引用了該索引相關的列或主鍵;

  IndexLookUp

index:IndexScan_74,

table:TableScan_75

表示從索引中過濾部分數據,僅返回這些數據的 Handle ID,通過 Handle ID 再次查找表數據,

這種方式需要兩次從 TiKV 獲取數據。Index 的讀取方式是由優化器自動選擇的。

  IndexScan   官網沒有說明
    table:partsupp,  操作的表
    index:PS_PARTKEY, PS_SUPPKEY 索引列
    range:[<nil>,+inf] 范圍
過濾   keep order:false  排序
  Selection  

表示 SQL 語句中的選擇條件,通常出現在 WHERE/HAVING/ON 子句中。

如果task為cop,則表示比較選擇條件進行了下推。

    eq(tpch.part.p_brand, Brand#23)

內容一般是選擇的條件

包括:eg/le/lt/ge

映射      
  Projection   對應 SQL 語句中的 SELECT 列表,功能是將每一條輸入數據映射成新的輸出數據。
    tpch.part.p_container, mul(0.2, 7_col_0) 一般是映射的字段列表
    offset:0  
    count:10  

聚集

Aggregation

    對應 SQL 語句中的 Group By 語句或者沒有 Group By 語句但是存在聚合函數,例如 count 或 sum 函數等。
  HashAgg  

是基於哈希的聚合算法,如果 Hash Aggregation 緊鄰 Table 或者 Index 的讀取算子,

則聚合算子會在 TiKV 端進行預聚合,以提高計算的並行度和減少網絡開銷。

    group by:tpch.lineitem.l_partkey 分組 
    funcs:avg(tpch.lineitem.l_quantity) 函數
  StreamAgg   官方沒有說明
    funcs:sum(tpch.lineitem.l_extendedprice) 函數

聯合

join

   

Hash Join 的原理是將參與連接的小表預先裝載到內存中,讀取大表的所有數據進行連接。
Sort Merge Join 會利用輸入數據的有序信息,同時讀取兩張表的數據並依次進行比較。
Index Look Up Join 會讀取外表的數據,並對內表進行主鍵或索引鍵查詢

部分join方式還沒有遇到過,暫時沒有添加進來

  IndexJoin   官方沒有說明
    inner join  
   

index:IndexScan_74

inner key:tpch.lineitem.l_partkey

 
   

outer:TableReader_86 

outer key:tpch.part.p_partkey

 
  HashLeftJoin   官方沒有說明
    inner join  
    left outer join  
    inner:HashAgg_55  
    equal:[eq(tpch.part.p_partkey, tpch.lineitem.l_partkey)]  
Apply    

用來描述子查詢的一種算子,行為類似於 Nested Loop,即每次從外表中取一條數據,

帶入到內表的關聯列中,並執行,最后根據 Apply 內聯的 Join 算法進行連接計算。

Apply 一般會被查詢優化器自動轉換為 Join 操作。用戶在編寫 SQL 的過程中應盡量避免 Apply 算子的出現。

  暫時沒有遇到過    
其它      
  Sort tpch.lineitem.l_returnflag:asc 排序處理,一般是字段名:asc(desc)

 

 

執行器的接口在executor.go文件中,實現一般是*Exec命名的

type executor interface {
    SetSrcExec(executor)
    GetSrcExec() executor
    ResetCounts()
    Counts() []int64
    Next(ctx context.Context) ([][]byte, error)
    // Cursor returns the key gonna to be scanned by the Next() function.
    Cursor() (key []byte, desc bool)
}

indexScan的實現
type indexScanExec struct {
    *tipb.IndexScan
    colsLen        int
    kvRanges       []kv.KeyRange
    startTS        uint64
    isolationLevel kvrpcpb.IsolationLevel
    mvccStore      MVCCStore
    cursor         int
    seekKey        []byte
    pkStatus       int
    start          int
    counts         []int64

    src executor
}

 

練習

聯系解讀一下,以下sql的執行計划

select
    sum(l_extendedprice * l_discount) as revenue # 潛在的收入增加量
from
    lineitem
where
    l_shipdate >= '1994-01-01' # DATE是從[1993, 1997]中隨機選擇的一年的1月1日
    and l_shipdate < date_add('1994-01-01', interval '1' year) # 一年內
    and l_discount between 0.06 - 0.01 and 0.06 + 0.01
    and l_quantity < 24; # QUANTITY在區間[24, 25]中隨機選擇

 

select
    100.00 * sum(case
        when p_type like 'PROMO%' # 促銷零件
            then l_extendedprice * (1 - l_discount) # 某一特定時間的收入
        else 0
    end) / sum(l_extendedprice * (1 - l_discount)) as promo_revenue
from
    lineitem,
    part
where
    l_partkey = p_partkey
    and l_shipdate >= '1995-09-01' # DATE是從1993年到1997年中任一年的任一月的一號
    and l_shipdate < date_add('1995-09-01', interval '1' month);

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM