前言
這里采用了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 的原理是將參與連接的小表預先裝載到內存中,讀取大表的所有數據進行連接。 部分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);