TiDB 查詢優化及調優系列(二)TiDB 查詢計劃簡介

PingCAP發表於2022-04-27

「TiDB 查詢優化及調優」系列文章將通過一些具體的案例,向大家介紹 TiDB 查詢及優化相關的原理和應用,在上一篇文章中我們簡要介紹了 TiDB 查詢優化器的優化流程。

查詢計劃(execution plan)展現了資料庫執行 SQL 語句的具體步驟,例如通過索引還是全表掃描訪問表中的資料,連線查詢的實現方式和連線的順序等。查閱及理解 TiDB 的查詢計劃是查詢調優的基礎。本文為系列文章的第二篇,將著重介紹 TiDB 查詢計劃以及如何檢視。

運算元及 Task

在上文的 TiDB 查詢優化流程簡介中有提到過,TiDB 的查詢計劃是由一系列的執行運算元構成,這些運算元是為返回查詢結果而執行的特定步驟,例如表掃描運算元,聚合運算元,Join 運算元等,下面以表掃描運算元為例,其它運算元的具體解釋可以參看下文檢視執行計劃的小結。

執行表掃描(讀盤或者讀 TiKV Block Cache)操作的運算元有如下幾類:

  • TableFullScan:全表掃描。
  • TableRangeScan:帶有範圍的表資料掃描。
  • TableRowIDScan:根據上層傳遞下來的 RowID 掃描表資料。時常在索引讀操作後檢索符合條件的行。
  • IndexFullScan:另一種“全表掃描”,掃的是索引資料,不是表資料。

目前 TiDB 的計算任務分為兩種不同的 task:cop task 和 root task。Cop task 是指使用 TiKV 中的 Coprocessor 執行的計算任務,root task 是指在 TiDB 中執行的計算任務。

SQL 優化的目標之一是將計算儘可能地下推到 TiKV 中執行。TiKV 中的 Coprocessor 能支援大部分 SQL 內建函式(包括聚合函式和標量函式)、SQL LIMIT 操作、索引掃描和表掃描。但是,所有的 Join 操作都只能作為 root task 在 TiDB 上執行。

利用 EXPLAIN 檢視分析查詢計劃

與其它主流商業資料庫一樣,TiDB 中可以通過 EXPLAIN 語句返回的結果檢視某條 SQL 的執行計劃。

EXPLAIN 語句

目前 TiDB 的 EXPLAIN 主要輸出 5 列,分別是:idestRowstaskaccess objectoperator info。執行計劃中每個運算元都由這 5 列屬性來描述,EXPLAIN結果中每一行描述一個運算元。每個屬性的具體含義如下:

1.png

EXPLAIN ANALYZE 語句

EXPLAIN 不同,EXPLAIN ANALYZE 會執行對應的 SQL 語句,記錄其執行時資訊,和執行計劃一併返回出來,可以視為 EXPLAIN 語句的擴充套件。EXPLAIN ANALYZE 語句的返回結果中增加了 actRows, execution info, memory, disk 這幾列資訊:

2.png

例如在下例中,優化器估算的 estRows 和實際執行中統計得到的 actRows 幾乎是相等的,說明優化器估算的行數與實際行數的誤差很小。同時 IndexLookUp_10 運算元在實際執行過程中使用了約 9 KB 的記憶體,該 SQL 在執行過程中,沒有觸發過任何運算元的落盤操作。

mysql> explain analyze select * from t where a < 10;
+-------------------------------+---------+---------+-----------+-------------------------+------------------------------------------------------------------------+-----------------------------------------------------+---------------+------+
| id                            | estRows | actRows | task      | access object           | execution info                                                         | operator info                                       | memory        | disk |
+-------------------------------+---------+---------+-----------+-------------------------+------------------------------------------------------------------------+-----------------------------------------------------+---------------+------+
| IndexLookUp_10                | 9.00    | 9       | root      |                         | time:641.245µs, loops:2, rpc num: 1, rpc time:242.648µs, proc keys:0   |                                                     | 9.23046875 KB | N/A  |
| ├─IndexRangeScan_8(Build)     | 9.00    | 9       | cop[tikv] | table:t, index:idx_a(a) | time:142.94µs, loops:10,                                               | range:[-inf,10), keep order:false                   | N/A           | N/A  |
| └─TableRowIDScan_9(Probe)     | 9.00    | 9       | cop[tikv] | table:t                 | time:141.128µs, loops:10                                               | keep order:false                                    | N/A           | N/A  |
+-------------------------------+---------+---------+-----------+-------------------------+------------------------------------------------------------------------+-----------------------------------------------------+---------------+------+
3 rows in set (0.00 sec)

檢視計劃中運算元的執行順序

TiDB 的執行計劃是一個樹形結構,樹中每個節點即是運算元。考慮到每個運算元內多執行緒併發執行的情況,在一條 SQL 執行的過程中,如果能夠有一個手術刀把這棵樹切開看看,大家可能會發現所有的運算元都正在消耗 CPU 和記憶體處理資料,從這個角度來看,運算元是沒有執行順序的。

但是如果從一行資料先後被哪些運算元處理的角度來看,一條資料在運算元上的執行是有順序的。這個順序可以通過下面這個規則簡單總結出來:

Build總是先於 Probe 執行,並且 Build 總是出現 Probe 前面

這個原則的前半句是說:如果一個運算元有多個子節點,子節點 ID 後面有 Build 關鍵字的運算元總是先於有 Probe 關鍵字的運算元執行。後半句是說:TiDB 在展現執行計劃的時候,Build 端總是第一個出現,接著才是 Probe 端。例如:

TiDB(root@127.0.0.1:test) > explain select * from t use index(idx_a) where a = 1;
+-------------------------------+---------+-----------+-------------------------+---------------------------------------------+
| id                            | estRows | task      | access object           | operator info                               |
+-------------------------------+---------+-----------+-------------------------+---------------------------------------------+
| IndexLookUp_7                 | 10.00   | root      |                         |                                             |
| ├─IndexRangeScan_5(Build)     | 10.00   | cop[tikv] | table:t, index:idx_a(a) | range:[1,1], keep order:false, stats:pseudo |
| └─TableRowIDScan_6(Probe)     | 10.00   | cop[tikv] | table:t                 | keep order:false, stats:pseudo              |
+-------------------------------+---------+-----------+-------------------------+---------------------------------------------+
3 rows in set (0.00 sec)

這裡 IndexLookUp_7 運算元有兩個孩子節點:IndexRangeScan_5(Build)TableRowIDScan_6(Probe)。可以看到,IndexRangeScan_5(Build) 是第一個出現的,並且基於上面這條規則,要得到一條資料,需要先執行它得到一個 RowID 以後,再由 TableRowIDScan_6(Probe) 根據前者讀上來的 RowID 去獲取完整的一行資料。

這種規則隱含的另一個資訊是:在同一層級的節點中,出現在最前面的運算元可能是最先被執行的,而出現在最末尾的運算元可能是最後被執行的。

例如下面這個例子:

TiDB(root@127.0.0.1:test) > explain select * from t t1 use index(idx_a) join t t2 use index() where t1.a = t2.a;
+----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+
| id                               | estRows  | task      | access object            | operator info                                                    |
+----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+
| HashJoin_22                      | 12487.50 | root      |                          | inner join, inner:TableReader_26, equal:[eq(test.t.a, test.t.a)] |
| ├─TableReader_26(Build)          | 9990.00  | root      |                          | data:Selection_25                                                |
| │ └─Selection_25                 | 9990.00  | cop[tikv] |                          | not(isnull(test.t.a))                                            |
| │   └─TableFullScan_24           | 10000.00 | cop[tikv] | table:t2                 | keep order:false, stats:pseudo                                   |
| └─IndexLookUp_29(Probe)          | 9990.00  | root      |                          |                                                                  |
|   ├─IndexFullScan_27(Build)      | 9990.00  | cop[tikv] | table:t1, index:idx_a(a) | keep order:false, stats:pseudo                                   |
|   └─TableRowIDScan_28(Probe)     | 9990.00  | cop[tikv] | table:t1                 | keep order:false, stats:pseudo                                   |
+----------------------------------+----------+-----------+--------------------------+------------------------------------------------------------------+
7 rows in set (0.00 sec)

要完成 HashJoin_22,需要先執行 TableReader_26(Build) 再執行 IndexLookUp_29(Probe)。而在執行 IndexLookUp_29(Probe) 的時候,又需要先執行 IndexFullScan_27(Build) 再執行 TableRowIDScan_28(Probe)。所以從整條執行鏈路來看,TableRowIDScan_28(Probe) 是最後被喚起執行的。

檢視錶掃描的執行計劃

在上文介紹運算元和任務時已經提到過表掃描運算元,這裡再稍微重複介紹一下,分為執行表掃描操作的運算元和對掃描資料進行匯聚和計算的運算元:

執行表掃描(讀盤或者讀 TiKV Block Cache)操作的運算元有如下幾類:

  • TableFullScan:全表掃描。
  • TableRangeScan:帶有範圍的表資料掃描。
  • TableRowIDScan:根據上層傳遞下來的 RowID 掃描表資料。時常在索引讀操作後檢索符合條件的行。
  • IndexFullScan:另一種“全表掃描”,掃的是索引資料,不是表資料。
  • IndexRangeScan:帶有範圍的索引資料掃描操作。

TiDB 會匯聚 TiKV/TiFlash 上掃描的資料或者計算結果,這種“資料匯聚”運算元目前有如下幾類:

  • TableReader:將 TiKV 上底層掃表運算元 TableFullScan 或 TableRangeScan 得到的資料進行彙總。
  • IndexReader:將 TiKV 上底層掃表運算元 IndexFullScan 或 IndexRangeScan 得到的資料進行彙總。
  • IndexLookUp:先彙總 Build 端 TiKV 掃描上來的 RowID,再去 Probe 端上根據這些 RowID 精確地讀取 TiKV 上的資料。Build 端是 IndexFullScanIndexRangeScan 型別的運算元,Probe 端是 TableRowIDScan 型別的運算元。
  • IndexMerge:和 IndexLookupReader 類似,可以看做是它的擴充套件,可以同時讀取多個索引的資料,有多個 Build 端,一個 Probe 端。執行過程也很類似,先彙總所有 Build 端 TiKV 掃描上來的 RowID,再去 Probe 端上根據這些 RowID 精確地讀取 TiKV 上的資料。Build 端是 IndexFullScanIndexRangeScan 型別的運算元,Probe 端是 TableRowIDScan 型別的運算元。

IndexLookUp 示例:

mysql> explain select * from t use index(idx_a);
+-------------------------------+----------+-----------+-------------------------+--------------------------------+
| id                            | estRows  | task      | access object           | operator info                  |
+-------------------------------+----------+-----------+-------------------------+--------------------------------+
| IndexLookUp_6                 | 10000.00 | root      |                         |                                |
| ├─IndexFullScan_4(Build)      | 10000.00 | cop[tikv] | table:t, index:idx_a(a) | keep order:false, stats:pseudo |
| └─TableRowIDScan_5(Probe)     | 10000.00 | cop[tikv] | table:t                 | keep order:false, stats:pseudo |
+-------------------------------+----------+-----------+-------------------------+--------------------------------+
3 rows in set (0.00 sec)

這裡 IndexLookUp_6 運算元有兩個孩子節點:IndexFullScan_4(Build)TableRowIDScan_5(Probe)。可以看到,IndexFullScan_4(Build) 執行索引全表掃,掃描索引 a 的所有資料,因為是全範圍掃,這個操作將獲得表中所有資料的 RowID,之後再由 TableRowIDScan_5(Probe) 去根據這些 RowID 去掃描所有的表資料。可以預見的是,這個執行計劃不如直接使用 TableReader 進行全表掃,因為同樣都是全表掃,這裡的 IndexLookUp 多掃了一次索引,帶來了額外的開銷。

TableReader 示例:

mysql> explain select * from t where a > 1 or b >100;
+-------------------------+----------+-----------+---------------+----------------------------------------+
| id                      | estRows  | task      | access object | operator info                          |
+-------------------------+----------+-----------+---------------+----------------------------------------+
| TableReader_7           | 8000.00  | root      |               | data:Selection_6                       |
| └─Selection_6           | 8000.00  | cop[tikv] |               | or(gt(test.t.a, 1), gt(test.t.b, 100)) |
|   └─TableFullScan_5     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo         |
+-------------------------+----------+-----------+---------------+----------------------------------------+
3 rows in set (0.00 sec)

在上面例子中 TableReader_7 運算元的孩子節點是 Selection_6。以這個孩子節點為根的子樹被當做了一個 Cop Task 下發給了相應的 TiKV,這個 Cop Task 使用 TableFullScan_5 運算元執行掃表操作。Selection 表示 SQL 語句中的選擇條件,可能來自 SQL 語句中的 WHERE/HAVING/ON 子句。由 TableFullScan_5 可以看到,這個執行計劃使用了一個全表掃描的操作,叢集的負載將因此而上升,可能會影響到叢集中正在執行的其他查詢。這時候如果能夠建立合適的索引,並且使用 IndexMerge 運算元,將能夠極大的提升查詢的效能,降低叢集的負載。

IndexMerge 示例:

注意:目前 TIDB 的 Index Merge 為實驗特性在 5.3 及以前版本中預設關閉,同時 5.0 中的 Index Merge 目前支援的場景僅限於析取正規化(or 連線的表示式),對合取正規化(and 連線的表示式)將在之後的版本中支援。 開啟 Index Merge 特性,可通過在客戶端中設定 session 或者 global 變數完成:set @@tidb_enable_index_merge = 1;

mysql> set @@tidb_enable_index_merge = 1;
mysql> explain select * from t use index(idx_a, idx_b) where a > 1 or b > 1;
+------------------------------+---------+-----------+-------------------------+------------------------------------------------+
| id                           | estRows | task      | access object           | operator info                                  |
+------------------------------+---------+-----------+-------------------------+------------------------------------------------+
| IndexMerge_16                | 6666.67 | root      |                         |                                                |
| ├─IndexRangeScan_13(Build)   | 3333.33 | cop[tikv] | table:t, index:idx_a(a) | range:(1,+inf], keep order:false, stats:pseudo |
| ├─IndexRangeScan_14(Build)   | 3333.33 | cop[tikv] | table:t, index:idx_b(b) | range:(1,+inf], keep order:false, stats:pseudo |
| └─TableRowIDScan_15(Probe)   | 6666.67 | cop[tikv] | table:t                 | keep order:false, stats:pseudo                 |
+------------------------------+---------+-----------+-------------------------+------------------------------------------------+
4 rows in set (0.00 sec)

IndexMerge 使得資料庫在掃描表資料時可以使用多個索引。這裡 IndexMerge_16 運算元有三個孩子節點,其中 IndexRangeScan_13IndexRangeScan_14 根據範圍掃描得到符合條件的所有 RowID,再由 TableRowIDScan_15 運算元根據這些 RowID 精確的讀取所有滿足條件的資料。

檢視聚合計算的執行計劃

Hash Aggregate 示例:

TiDB 上的 Hash Aggregation 運算元採用多執行緒併發優化,執行速度快,但會消耗較多記憶體。下面是一個 Hash Aggregate 的例子:

TiDB(root@127.0.0.1:test) > explain select /*+ HASH_AGG() */ count(*) from t;
+---------------------------+----------+-----------+---------------+---------------------------------+
| id                        | estRows  | task      | access object | operator info                   |
+---------------------------+----------+-----------+---------------+---------------------------------+
| HashAgg_11                | 1.00     | root      |               | funcs:count(Column#7)->Column#4 |
| └─TableReader_12          | 1.00     | root      |               | data:HashAgg_5                  |
|   └─HashAgg_5             | 1.00     | cop[tikv] |               | funcs:count(1)->Column#7        |
|     └─TableFullScan_8     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo  |
+---------------------------+----------+-----------+---------------+---------------------------------+
4 rows in set (0.00 sec)

一般而言 TiDB 的 Hash Aggregate 會分成兩個階段執行,一個在 TiKV/TiFlash 的 Coprocessor 上,計算聚合函式的中間結果。另一個在 TiDB 層,彙總所有 Coprocessor Task 的中間結果後,得到最終結果。

Stream Aggregate 示例:

TiDB Stream Aggregation 運算元通常會比 Hash Aggregate 佔用更少的記憶體,有些場景中也會比 Hash Aggregate 執行的更快。當資料量太大或者系統記憶體不足時,可以試試 Stream Aggregate 運算元。一個 Stream Aggregate 的例子如下:

TiDB(root@127.0.0.1:test) > explain select /*+ STREAM_AGG() */ count(*) from t;
+----------------------------+----------+-----------+---------------+---------------------------------+
| id                         | estRows  | task      | access object | operator info                   |
+----------------------------+----------+-----------+---------------+---------------------------------+
| StreamAgg_16               | 1.00     | root      |               | funcs:count(Column#7)->Column#4 |
| └─TableReader_17           | 1.00     | root      |               | data:StreamAgg_8                |
|   └─StreamAgg_8            | 1.00     | cop[tikv] |               | funcs:count(1)->Column#7        |
|     └─TableFullScan_13     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo  |
+----------------------------+----------+-----------+---------------+---------------------------------+
4 rows in set (0.00 sec)

Hash Aggregate 類似,一般而言 TiDB 的 Stream Aggregate 也會分成兩個階段執行,一個在 TiKV/TiFlash 的 Coprocessor 上,計算聚合函式的中間結果。另一個在 TiDB 層,彙總所有 Coprocessor Task 的中間結果後,得到最終結果。

檢視 Join 的執行計劃

TiDB 的 Join 演算法包括如下幾類:

  • Hash Join
  • Merge Join
  • Index Hash Join
  • Index Merge Join

Apply

下面分別通過一些例子來解釋這些 Join 演算法的執行過程

Hash Join 示例:

TiDB 的 Hash Join 運算元採用了多執行緒優化,執行速度較快,但會消耗較多記憶體。一個 Hash Join 的例子如下:

mysql> explain select /*+ HASH_JOIN(t1, t2) */ * from t t1 join t2 on t1.a = t2.a;
+------------------------------+----------+-----------+---------------+-------------------------------------------------------------------+
| id                           | estRows  | task      | access object | operator info                                                     |
+------------------------------+----------+-----------+---------------+-------------------------------------------------------------------+
| HashJoin_33                  | 10000.00 | root      |               | inner join, inner:TableReader_43, equal:[eq(test.t.a, test.t2.a)] |
| ├─TableReader_43(Build)      | 10000.00 | root      |               | data:Selection_42                                                 |
| │ └─Selection_42             | 10000.00 | cop[tikv] |               | not(isnull(test.t2.a))                                            |
| │   └─TableFullScan_41       | 10000.00 | cop[tikv] | table:t2      | keep order:false                                                  |
| └─TableReader_37(Probe)      | 10000.00 | root      |               | data:Selection_36                                                 |
|   └─Selection_36             | 10000.00 | cop[tikv] |               | not(isnull(test.t.a))                                             |
|     └─TableFullScan_35       | 10000.00 | cop[tikv] | table:t1      | keep order:false                                                  |
+------------------------------+----------+-----------+---------------+-------------------------------------------------------------------+
7 rows in set (0.00 sec)

Hash Join 會將 Build 端的資料快取在記憶體中,根據這些資料構造出一個 Hash Table,然後讀取 Probe 端的資料,用 Probe 端的資料去探測(Probe)Build 端構造出來的 Hash Table,將符合條件的資料返回給使用者。

Merge Join 示例:
TiDB 的 Merge Join 運算元相比於 Hash Join 通常會佔用更少的記憶體,但可能執行時間會更久。當資料量太大,或系統記憶體不足時,建議嘗試使用。下面是一個 Merge Join 的例子:

mysql> explain select /*+ SM_JOIN(t1) */ * from t t1 join t t2 on t1.a = t2.a;
+------------------------------------+----------+-----------+--------------------------+---------------------------------------------------+
| id                                 | estRows  | task      | access object            | operator info                                     |
+------------------------------------+----------+-----------+--------------------------+---------------------------------------------------+
| MergeJoin_6                        | 10000.00 | root      |                          | inner join, left key:test.t.a, right key:test.t.a |
| ├─IndexLookUp_13(Build)            | 10000.00 | root      |                          |                                                   |
| │ ├─IndexFullScan_11(Build)        | 10000.00 | cop[tikv] | table:t2, index:idx_a(a) | keep order:true                                   |
| │ └─TableRowIDScan_12(Probe)       | 10000.00 | cop[tikv] | table:t2                 | keep order:false                                  |
| └─IndexLookUp_10(Probe)            | 10000.00 | root      |                          |                                                   |
|   ├─IndexFullScan_8(Build)         | 10000.00 | cop[tikv] | table:t1, index:idx_a(a) | keep order:true                                   |
|   └─TableRowIDScan_9(Probe)        | 10000.00 | cop[tikv] | table:t1                 | keep order:false                                  |
+------------------------------------+----------+-----------+--------------------------+---------------------------------------------------+
7 rows in set (0.00 sec)

Merge Join 運算元在執行時,會從 Build 端把一個 Join Group 的資料全部讀取到記憶體中,接著再去讀 Probe 端的資料,用 Probe 端的每行資料去和 Build 端的完整的一個 Join Group 依次去看是否匹配(除了滿足等值條件以外,還有其他非等值條件,這裡的 “匹配” 主要是指檢視是否滿足非等值職條件)。Join Group 指的是所有 Join Key 上值相同的資料。

Index Hash Join 示例:

INL_HASH_JOIN(t1_name [, tl_name]) 提示優化器使用 Index Nested Loop Hash Join 演算法。該演算法與 Index Nested Loop Join 使用條件完全一樣,但在某些場景下會更為節省記憶體資源。

mysql> explain select /*+ INL_HASH_JOIN(t1) */ * from t t1 join t t2 on t1.a = t2.a;
+----------------------------------+----------+-----------+--------------------------+--------------------------------------------------------------------------+
| id                               | estRows  | task      | access object            | operator info                                                            |
+----------------------------------+----------+-----------+--------------------------+--------------------------------------------------------------------------+
| IndexHashJoin_32                 | 10000.00 | root      |                          | inner join, inner:IndexLookUp_23, outer key:test.t.a, inner key:test.t.a |
| ├─TableReader_35(Build)          | 10000.00 | root      |                          | data:Selection_34                                                        |
| │ └─Selection_34                 | 10000.00 | cop[tikv] |                          | not(isnull(test.t.a))                                                    |
| │   └─TableFullScan_33           | 10000.00 | cop[tikv] | table:t2                 | keep order:false                                                         |
| └─IndexLookUp_23(Probe)          | 1.00     | root      |                          |                                                                          |
|   ├─Selection_22(Build)          | 1.00     | cop[tikv] |                          | not(isnull(test.t.a))                                                    |
|   │ └─IndexRangeScan_20          | 1.00     | cop[tikv] | table:t1, index:idx_a(a) | range: decided by [eq(test.t.a, test.t.a)], keep order:false             |
|   └─TableRowIDScan_21(Probe)     | 1.00     | cop[tikv] | table:t1                 | keep order:false                                                         |
+----------------------------------+----------+-----------+--------------------------+--------------------------------------------------------------------------+
8 rows in set (0.00 sec)

Index Merge Join 示例:
INL_MERGE_JOIN(t1_name [, tl_name]) 提示優化器使用 Index Nested Loop Merge Join 演算法。該演算法相比於 INL_JOIN 會更節省記憶體。該演算法使用條件包含 INL_JOIN 的所有使用條件,但還需要新增一條:join keys 中的內表列集合是內表使用的 index 的字首,或內表使用的 indexjoin keys 中的內表列集合的字首。

mysql> explain select /*+ INL_MERGE_JOIN(t2@sel_2) */ * from t t1 where  t1.a  in ( select t2.a from t t2 where t2.b < t1.b);
+---------------------------------+---------+-----------+--------------------------+-----------------------------------------------------------------------------------------------------------+
| id                              | estRows | task      | access object            | operator info                                                                                             |
+---------------------------------+---------+-----------+--------------------------+-----------------------------------------------------------------------------------------------------------+
| IndexMergeJoin_23               | 6.39    | root      |                          | semi join, inner:Projection_21, outer key:test.t.a, inner key:test.t.a, other cond:lt(test.t.b, test.t.b) |
| ├─TableReader_28(Build)         | 7.98    | root      |                          | data:Selection_27                                                                                         |
| │ └─Selection_27                | 7.98    | cop[tikv] |                          | not(isnull(test.t.a)), not(isnull(test.t.b))                                                              |
| │   └─TableFullScan_26          | 8.00    | cop[tikv] | table:t1                 | keep order:false, stats:pseudo                                                                            |
| └─Projection_21(Probe)          | 1.25    | root      |                          | test.t.a, test.t.b                                                                                        |
|   └─IndexLookUp_20              | 1.25    | root      |                          |                                                                                                           |
|     ├─Selection_18(Build)       | 1.25    | cop[tikv] |                          | not(isnull(test.t.a))                                                                                     |
|     │ └─IndexRangeScan_16       | 1.25    | cop[tikv] | table:t2, index:idx_a(a) | range: decided by [eq(test.t.a, test.t.a)], keep order:true, stats:pseudo                                 |
|     └─Selection_19(Probe)       | 1.25    | cop[tikv] |                          | not(isnull(test.t.b))                                                                                     |
|       └─TableRowIDScan_17       | 1.25    | cop[tikv] | table:t2                 | keep order:false, stats:pseudo                                                                            |
+---------------------------------+---------+-----------+--------------------------+-----------------------------------------------------------------------------------------------------------+
10 rows in set (0.01 sec)

Apply 示例:

mysql> explain select * from t t1 where  t1.a  in ( select avg(t2.a) from t2 where t2.b < t1.b);
+----------------------------------+----------+-----------+---------------+-------------------------------------------------------------------------------+
| id                               | estRows  | task      | access object | operator info                                                                 |
+----------------------------------+----------+-----------+---------------+-------------------------------------------------------------------------------+
| Projection_10                    | 10000.00 | root      |               | test.t.id, test.t.a, test.t.b                                                 |
| └─Apply_12                       | 10000.00 | root      |               | semi join, inner:StreamAgg_30, equal:[eq(Column#8, Column#7)]                 |
|   ├─Projection_13(Build)         | 10000.00 | root      |               | test.t.id, test.t.a, test.t.b, cast(test.t.a, decimal(20,0) BINARY)->Column#8 |
|   │ └─TableReader_15             | 10000.00 | root      |               | data:TableFullScan_14                                                         |
|   │   └─TableFullScan_14         | 10000.00 | cop[tikv] | table:t1      | keep order:false                                                              |
|   └─StreamAgg_30(Probe)          | 1.00     | root      |               | funcs:avg(Column#12, Column#13)->Column#7                                     |
|     └─TableReader_31             | 1.00     | root      |               | data:StreamAgg_19                                                             |
|       └─StreamAgg_19             | 1.00     | cop[tikv] |               | funcs:count(test.t2.a)->Column#12, funcs:sum(test.t2.a)->Column#13            |
|         └─Selection_29           | 8000.00  | cop[tikv] |               | lt(test.t2.b, test.t.b)                                                       |
|           └─TableFullScan_28     | 10000.00 | cop[tikv] | table:t2      | keep order:false                                                              |
+----------------------------------+----------+-----------+-----------------------------------------------------------------------------------------------+
10 rows in set, 1 warning (0.00 sec)

其它關於 EXPLAIN 的說明

EXPLAIN FOR CONNECTION 用於獲得一個連線中最後執行的查詢的執行計劃,其輸出格式與 EXPLAIN 完全一致。但 TiDB 中的實現與 MySQL 不同,除了輸出格式之外,還有以下區別:

MySQL 返回的是正在執行的查詢計劃,而 TiDB 返回的是最後執行的查詢計劃。

MySQL 的文件中指出,MySQL 要求登入使用者與被查詢的連線相同,或者擁有 PROCESS 許可權,而 TiDB 則要求登入使用者與被查詢的連線相同,或者擁有 SUPER 許可權。

本文為「TiDB 查詢優化及調優」系列文章的第二篇,後續將繼續對 TiDB 慢查詢診斷監控及排查、調整及優化查詢執行計劃以及其他優化器開發或規劃中的診斷調優功能等進行介紹。如果您對 TiDB 的產品有任何建議,歡迎來到 https://internals.tidb.io 與我們交流。

點選檢視更多 TiDB 查詢優化及調優文章

相關文章