サブクエリを使用するステートメントの説明

TiDB はサブクエリのパフォーマンスを向上させるためにいくつかの最適化を実行します。このドキュメントでは、一般的なサブクエリに対するこれらの最適化のいくつかと、 EXPLAINの出力を解釈する方法について説明します。

このドキュメントの例は、次のサンプル データに基づいています。

CREATE TABLE t1 (id BIGINT NOT NULL PRIMARY KEY auto_increment, pad1 BLOB, pad2 BLOB, pad3 BLOB, int_col INT NOT NULL DEFAULT 0); CREATE TABLE t2 (id BIGINT NOT NULL PRIMARY KEY auto_increment, t1_id BIGINT NOT NULL, pad1 BLOB, pad2 BLOB, pad3 BLOB, INDEX(t1_id)); CREATE TABLE t3 ( id INT NOT NULL PRIMARY KEY auto_increment, t1_id INT NOT NULL, UNIQUE (t1_id) ); INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM dual; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t1 SELECT NULL, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024), 0 FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; INSERT INTO t2 SELECT NULL, a.id, RANDOM_BYTES(1024), RANDOM_BYTES(1024), RANDOM_BYTES(1024) FROM t1 a JOIN t1 b JOIN t1 c LIMIT 10000; UPDATE t1 SET int_col = 1 WHERE pad1 = (SELECT pad1 FROM t1 ORDER BY RAND() LIMIT 1); INSERT INTO t3 SELECT NULL, id FROM t1 WHERE id < 1000; SELECT SLEEP(1); ANALYZE TABLE t1, t2, t3;

内部結合 (一意でないサブクエリ)

次の例では、 INサブクエリがテーブルt2から ID のリストを検索します。セマンティックの正確性のために、TiDB は列t1_idが一意であることを保証する必要があります。 EXPLAINを使用すると、重複を削除してINNER JOIN操作を実行するために使用される実行計画を確認できます。

EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t2);
+----------------------------------+----------+-----------+------------------------------+---------------------------------------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------+----------+-----------+------------------------------+---------------------------------------------------------------------------------------------------------------------------+ | IndexJoin_14 | 5.00 | root | | inner join, inner:IndexLookUp_13, outer key:test.t2.t1_id, inner key:test.t1.id, equal cond:eq(test.t2.t1_id, test.t1.id) | | ├─StreamAgg_49(Build) | 5.00 | root | | group by:test.t2.t1_id, funcs:firstrow(test.t2.t1_id)->test.t2.t1_id | | │ └─IndexReader_50 | 5.00 | root | | index:StreamAgg_39 | | │ └─StreamAgg_39 | 5.00 | cop[tikv] | | group by:test.t2.t1_id, | | │ └─IndexFullScan_31 | 50000.00 | cop[tikv] | table:t2, index:t1_id(t1_id) | keep order:true | | └─IndexLookUp_13(Probe) | 1.00 | root | | | | ├─IndexRangeScan_11(Build) | 1.00 | cop[tikv] | table:t1, index:PRIMARY(id) | range: decided by [eq(test.t1.id, test.t2.t1_id)], keep order:false | | └─TableRowIDScan_12(Probe) | 1.00 | cop[tikv] | table:t1 | keep order:false | +----------------------------------+----------+-----------+------------------------------+---------------------------------------------------------------------------------------------------------------------------+ 8 rows in set (0.00 sec)

上記のクエリ結果から、TiDB がインデックス結合操作| IndexJoin_14を使用してサブクエリを結合および変換していることがわかります。実行計画では、実行プロセスは次のとおりです。

  1. TiKV 側のインデックススキャンオペレータ└─IndexFullScan_31は、 t2.t1_id列の値を読み取ります。
  2. └─StreamAgg_39オペレーターの一部のタスクは、TiKV のt1_idの値を重複排除します。
  3. ├─StreamAgg_49(Build)オペレーターのいくつかのタスクは、TiDB でt1_idの値を重複排除します。重複排除は集約機能firstrow(test.t2.t1_id)によって行われる。
  4. 演算結果はt1テーブルの主キーに結合されます。結合条件はeq(test.t1.id, test.t2.t1_id)です。

内部結合 (一意のサブクエリ)

前の例では、テーブルt1に対して結合する前に、 t1_idの値が一意であることを確認するために集計が必要です。ただし、次の例では、 UNIQUEの制約により、 t3.t1_idは既に一意であることが保証されています。

EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t3);
+----------------------------------+---------+-----------+-----------------------------+---------------------------------------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +----------------------------------+---------+-----------+-----------------------------+---------------------------------------------------------------------------------------------------------------------------+ | IndexJoin_17 | 1978.13 | root | | inner join, inner:IndexLookUp_16, outer key:test.t3.t1_id, inner key:test.t1.id, equal cond:eq(test.t3.t1_id, test.t1.id) | | ├─TableReader_44(Build) | 1978.00 | root | | data:TableFullScan_43 | | │ └─TableFullScan_43 | 1978.00 | cop[tikv] | table:t3 | keep order:false | | └─IndexLookUp_16(Probe) | 1.00 | root | | | | ├─IndexRangeScan_14(Build) | 1.00 | cop[tikv] | table:t1, index:PRIMARY(id) | range: decided by [eq(test.t1.id, test.t3.t1_id)], keep order:false | | └─TableRowIDScan_15(Probe) | 1.00 | cop[tikv] | table:t1 | keep order:false | +----------------------------------+---------+-----------+-----------------------------+---------------------------------------------------------------------------------------------------------------------------+ 6 rows in set (0.01 sec)

意味的にt3.t1_idは一意であることが保証されているため、 INNER JOINとして直接実行できます。

準結合 (相関サブクエリ)

前の 2 つの例では、TiDB は、サブクエリ内のデータが ( StreamAggによって) 一意になるか、一意であることが保証された後、 INNER JOIN操作を実行できます。両方の結合は、Index Join を使用して実行されます。

この例では、TiDB は別の実行計画を選択します。

EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT t1_id FROM t2 WHERE t1_id != t1.int_col);
+-----------------------------+-----------+-----------+------------------------------+--------------------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +-----------------------------+-----------+-----------+------------------------------+--------------------------------------------------------------------------------------------------------+ | MergeJoin_9 | 45446.40 | root | | semi join, left key:test.t1.id, right key:test.t2.t1_id, other cond:ne(test.t2.t1_id, test.t1.int_col) | | ├─IndexReader_24(Build) | 180000.00 | root | | index:IndexFullScan_23 | | │ └─IndexFullScan_23 | 180000.00 | cop[tikv] | table:t2, index:t1_id(t1_id) | keep order:true | | └─TableReader_22(Probe) | 56808.00 | root | | data:Selection_21 | | └─Selection_21 | 56808.00 | cop[tikv] | | ne(test.t1.id, test.t1.int_col) | | └─TableFullScan_20 | 71010.00 | cop[tikv] | table:t1 | keep order:true | +-----------------------------+-----------+-----------+------------------------------+--------------------------------------------------------------------------------------------------------+ 6 rows in set (0.00 sec)

上記の結果から、TiDB がSemi Joinアルゴリズムを使用していることがわかります。半結合は内部結合とは異なります。半結合は右側のキー ( t2.t1_id ) の最初の値のみを許可します。つまり、重複は結合演算子タスクの一部として削除されます。結合アルゴリズムも Merge Join です。これは、演算子がソートされた順序で左側と右側の両方からデータを読み取るため、効率的なジッパー マージのようなものです。

サブクエリはサブクエリの外部に存在する列 ( t1.int_col ) を参照するため、元のステートメントは相関サブクエリと見なされます。ただし、 EXPLAINの出力は、 サブクエリ非相関最適化が適用された後の実行計画を示しています。条件t1_id != t1.int_colt1.id != t1.int_colに書き換える。 TiDB は、テーブルからデータを読み取るときに└─Selection_21でこれを実行できますt1

反準結合 ( NOT INサブクエリ)

次の例では、サブクエリにt3.t1_idない限り、クエリは意味的にテーブルt3からすべての行を返します。

EXPLAIN SELECT * FROM t3 WHERE t1_id NOT IN (SELECT id FROM t1 WHERE int_col < 100);
+-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------+ | IndexMergeJoin_20 | 1598.40 | root | | anti semi join, inner:TableReader_15, outer key:test.t3.t1_id, inner key:test.t1.id | | ├─TableReader_28(Build) | 1998.00 | root | | data:TableFullScan_27 | | │ └─TableFullScan_27 | 1998.00 | cop[tikv] | table:t3 | keep order:false | | └─TableReader_15(Probe) | 1.00 | root | | data:Selection_14 | | └─Selection_14 | 1.00 | cop[tikv] | | lt(test.t1.int_col, 100) | | └─TableRangeScan_13 | 1.00 | cop[tikv] | table:t1 | range: decided by [test.t3.t1_id], keep order:true | +-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------+ 6 rows in set (0.00 sec)

このクエリは、最初にテーブルt3を読み取り、次にPRIMARY KEYに基づいてテーブルt1をプローブします。結合タイプはアンチセミ結合です。 anti-この例は値 ( NOT IN ) が存在しないためであり、半結合は、結合が拒否される前に最初の行のみが一致する必要があるためです。

Null-aware semi join ( INおよび= ANYサブクエリ)

INまたは= ANYセット演算子の値は、3 つの値 ( truefalse 、およびNULL ) です。 2 つの演算子のいずれかから変換された結合タイプの場合、TiDB は結合キーの両側のNULLを認識し、特別な方法で処理する必要があります。

IN個と= ANYの演算子を含むサブクエリは、それぞれセミ ジョインと左外部セミ ジョインに変換されます。前のセミジョインの例では、結合キーの両側の列test.t1.idtest.t2.t1_idnot NULLであるため、セミ結合を null 認識と見なす必要はありません ( NULLは特別に処理されません)。 TiDB は、特別な最適化を行わずに、デカルト積とフィルターに基づいて null 認識セミ結合を処理します。次に例を示します。

CREATE TABLE t(a INT, b INT); CREATE TABLE s(a INT, b INT); EXPLAIN SELECT (a,b) IN (SELECT * FROM s) FROM t; EXPLAIN SELECT * FROM t WHERE (a,b) IN (SELECT * FROM s);
tidb> EXPLAIN SELECT (a,b) IN (SELECT * FROM s) FROM t; +-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------------+ | HashJoin_8 | 1.00 | root | | CARTESIAN left outer semi join, other cond:eq(test.t.a, test.s.a), eq(test.t.b, test.s.b) | | ├─TableReader_12(Build) | 1.00 | root | | data:TableFullScan_11 | | │ └─TableFullScan_11 | 1.00 | cop[tikv] | table:s | keep order:false, stats:pseudo | | └─TableReader_10(Probe) | 1.00 | root | | data:TableFullScan_9 | | └─TableFullScan_9 | 1.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-----------------------------+---------+-----------+---------------+-------------------------------------------------------------------------------------------+ 5 rows in set (0.00 sec) tidb> EXPLAIN SELECT * FROM t WHERE (a,b) IN (SELECT * FROM s); +------------------------------+---------+-----------+---------------+-----------------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +------------------------------+---------+-----------+---------------+-----------------------------------------------------------------------------------------------------+ | HashJoin_11 | 1.00 | root | | inner join, equal:[eq(test.t.a, test.s.a) eq(test.t.b, test.s.b)] | | ├─TableReader_14(Build) | 1.00 | root | | data:Selection_13 | | │ └─Selection_13 | 1.00 | cop[tikv] | | not(isnull(test.t.a)), not(isnull(test.t.b)) | | │ └─TableFullScan_12 | 1.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | | └─HashAgg_17(Probe) | 1.00 | root | | group by:test.s.a, test.s.b, funcs:firstrow(test.s.a)->test.s.a, funcs:firstrow(test.s.b)->test.s.b | | └─TableReader_24 | 1.00 | root | | data:Selection_23 | | └─Selection_23 | 1.00 | cop[tikv] | | not(isnull(test.s.a)), not(isnull(test.s.b)) | | └─TableFullScan_22 | 1.00 | cop[tikv] | table:s | keep order:false, stats:pseudo | +------------------------------+---------+-----------+---------------+-----------------------------------------------------------------------------------------------------+ 8 rows in set (0.01 sec)

最初のクエリ ステートメントEXPLAIN SELECT (a,b) IN (SELECT * FROM s) FROM t;では、テーブルtsの列abが NULLABLE であるため、 INサブクエリによって変換された左外部セミ ジョインは NULL を認識します。具体的には、デカルト積が最初に計算され、次にINまたは= ANYで接続された列が、フィルター処理のための通常の等価クエリとして別の条件に入れられます。

2 番目のクエリ ステートメントEXPLAIN SELECT * FROM t WHERE (a,b) IN (SELECT * FROM s);では、テーブルtsの列abが NULLABLE であるため、 INのサブクエリは NULL を認識するセミ ジョインに変換されているはずです。しかし、TiDB はセミ結合を内部結合と集約に変換することで最適化します。これは、非スカラー出力のINサブクエリでNULLfalseが同等であるためです。プッシュダウン フィルターのNULL行は、 WHERE句の否定的なセマンティクスになります。したがって、これらの行は事前に無視できます。

ノート:

Exists演算子も準結合に変換されますが、null に対応していません。

Null-aware anti semi join ( NOT INおよび!= ALLサブクエリ)

NOT INまたは!= ALLセット演算子の値は、3 つの値 ( truefalse 、およびNULL ) です。 2 つの演算子のいずれかから変換された結合タイプの場合、TiDB は結合キーの両側のNULLを認識し、特別な方法で処理する必要があります。

NOT IN個と! = ALLの演算子を含むサブクエリは、それぞれ反セミ結合と反左外部セミ結合に変換されます。前のアンチセミジョインの例では、結合キーの両側の列test.t3.t1_idtest.t1.idnot NULLであるため、アンチ セミ結合を null 認識と見なす必要はありません ( NULLは特別に処理されません)。

TiDB v6.3.0 は、次のように null-aware anti join (NAAJ) を最適化します。

  • null 認識等値条件 (NA-EQ) を使用してハッシュ結合を構築する

    セット演算子は、条件の両側の演算子の値がNULLの場合に特別なプロセスを必要とする等式条件を導入します。 null 認識を必要とする等価条件は、NA-EQ と呼ばれます。以前のバージョンとは異なり、TiDB v6.3.0 は以前のように NA-EQ を処理しなくなりましたが、結合後に他の条件に配置し、デカルト積を照合した後に結果セットの正当性を判断します。

    TiDB v6.3.0 以降、弱められた等価条件である NA-EQ は、ハッシュ結合を構築するために引き続き使用されます。これにより、トラバースする必要がある照合データの量が減り、照合プロセスが高速化されます。ビルド テーブルの合計DISTINCT()値のパーセンテージがほぼ 100% になると、加速がより顕著になります。

  • NULLの特殊なプロパティを使用して、一致する結果を返す速度を上げます

    アンチセミ結合は結合正規形 (CNF) であるため、結合のいずれかの側にNULLがあると、明確な結果が得られます。このプロパティを使用して、一致プロセス全体の戻りを高速化できます。

次に例を示します。

CREATE TABLE t(a INT, b INT); CREATE TABLE s(a INT, b INT); EXPLAIN SELECT (a, b) NOT IN (SELECT * FROM s) FROM t; EXPLAIN SELECT * FROM t WHERE (a, b) NOT IN (SELECT * FROM s);
tidb> EXPLAIN SELECT (a, b) NOT IN (SELECT * FROM s) FROM t; +-----------------------------+----------+-----------+---------------+---------------------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +-----------------------------+----------+-----------+---------------+---------------------------------------------------------------------------------------------+ | HashJoin_8 | 10000.00 | root | | Null-aware anti left outer semi join, equal:[eq(test.t.b, test.s.b) eq(test.t.a, test.s.a)] | | ├─TableReader_12(Build) | 10000.00 | root | | data:TableFullScan_11 | | │ └─TableFullScan_11 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo | | └─TableReader_10(Probe) | 10000.00 | root | | data:TableFullScan_9 | | └─TableFullScan_9 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-----------------------------+----------+-----------+---------------+---------------------------------------------------------------------------------------------+ 5 rows in set (0.00 sec) tidb> EXPLAIN SELECT * FROM t WHERE (a, b) NOT IN (SELECT * FROM s); +-----------------------------+----------+-----------+---------------+----------------------------------------------------------------------------------+ | id | estRows | task | access object | operator info | +-----------------------------+----------+-----------+---------------+----------------------------------------------------------------------------------+ | HashJoin_8 | 8000.00 | root | | Null-aware anti semi join, equal:[eq(test.t.b, test.s.b) eq(test.t.a, test.s.a)] | | ├─TableReader_12(Build) | 10000.00 | root | | data:TableFullScan_11 | | │ └─TableFullScan_11 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo | | └─TableReader_10(Probe) | 10000.00 | root | | data:TableFullScan_9 | | └─TableFullScan_9 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-----------------------------+----------+-----------+---------------+----------------------------------------------------------------------------------+ 5 rows in set (0.00 sec)

最初のクエリ ステートメントEXPLAIN SELECT (a, b) NOT IN (SELECT * FROM s) FROM t;では、テーブルtsの列abが NULLABLE であるため、 NOT INのサブクエリによって変換された左外部セミ ジョインは NULL を認識します。違いは、NAAJ 最適化では NA-EQ もハッシュ結合条件として使用されるため、結合計算が大幅に高速化されることです。

2 番目のクエリ ステートメントEXPLAIN SELECT * FROM t WHERE (a, b) NOT IN (SELECT * FROM s);では、テーブルtsの列abが NULLABLE であるため、 NOT INサブクエリによって変換されるアンチ セミ ジョインは null を認識します。違いは、NAAJ 最適化では NA-EQ もハッシュ結合条件として使用されるため、結合計算が大幅に高速化されることです。

現在、TiDB はアンチ セミ ジョインとアンチ レフト アウター セミ ジョインの null 認識のみ可能です。ハッシュ結合タイプのみがサポートされており、そのビルド テーブルは正しいテーブルに固定する必要があります。

ノート:

Not Exists演算子もアンチセミ結合に変換されますが、null 認識ではありません。

他のタイプのサブクエリを使用した Explain ステートメント