サブクエリ関連の最適化

この記事では、主にサブクエリ関連の最適化について紹介します。

サブクエリは通常、次の状況で表示されます。

  • NOT IN (SELECT ... FROM ...)
  • NOT EXISTS (SELECT ... FROM ...)
  • IN (SELECT ... FROM ..)
  • EXISTS (SELECT ... FROM ...)
  • ... >/>=/</<=/=/!= (SELECT ... FROM ...)

サブクエリには、 select * from t where t.a in (select * from t2 where t.b=t2.b)などのサブクエリ以外の列が含まれる場合があります。サブクエリのt.b列目はサブクエリに属さず、サブクエリの外部から導入されます。このようなサブクエリは通常「相関サブクエリ」と呼ばれ、外部から導入された列は「相関列」と呼ばれます。相関サブクエリに関する最適化については、 相関サブクエリの非相関を参照してください。この記事では、相関列を含まないサブクエリに焦点を当てています。

デフォルトでは、サブクエリはTiDB 実行計画についてsemi joinを実行方法として使用します。一部の特別なサブクエリでは、TiDB はパフォーマンスを向上させるために論理的な書き直しを行います。

... < ALL (SELECT ... FROM ...)または... > ANY (SELECT ... FROM ...)

この場合、 ALLANYMAXMINに置き換えることができます。テーブルが空の場合、 MAX(EXPR)MIN(EXPR)の結果は NULL です。 EXPRの結果にNULLが含まれる場合も同様です。 EXPRの結果にNULLが含まれるかどうかは、式の最終結果に影響を与える可能性があるため、完全な書き直しは次の形式で与えられます。

  • t.id < all (select s.id from s)t.id < min(s.id) and if(sum(s.id is null) != 0, null, true)に書き換えられます
  • t.id < any (select s.id from s)t.id < max(s.id) or if(sum(s.id is null) != 0, null, false)に書き換えられます

... != ANY (SELECT ... FROM ...)

この場合、サブクエリからのすべての値が異なる場合、クエリをそれらと比較するだけで十分です。サブクエリ内の異なる値の数が複数の場合、不等式が存在する必要があります。したがって、このようなサブクエリは次のように書き直すことができます。

  • select * from t where t.id != any (select s.id from s)select t.* from t, (select s.id, count(distinct s.id) as cnt_distinct from s) where (t.id != s.id or cnt_distinct > 1)に書き換えられます

... = ALL (SELECT ... FROM ...)

この場合、サブクエリ内の異なる値の数が複数ある場合、この式の結果は false になる必要があります。したがって、そのようなサブクエリは、TiDB では次の形式に書き換えられます。

  • select * from t where t.id = all (select s.id from s)select t.* from t, (select s.id, count(distinct s.id) as cnt_distinct from s ) where (t.id = s.id and cnt_distinct <= 1)に書き換えられます

... IN (SELECT ... FROM ...)

この場合、 INのサブクエリはSELECT ... FROM ... GROUP ...に書き換えられ、その後通常の形式のJOINに書き換えられます。

たとえば、 select * from t1 where t1.a in (select t2.a from t2)select t1.* from t1, (select distinct(a) a from t2) t2 where t1.a = t2. The form of aに書き換えられます。ここのDISTINCT属性は、 t2.aUNIQUE属性を持つ場合、自動的に削除できます。

explain select * from t1 where t1.a in (select t2.a from t2);
+------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+
| id                           | estRows | task      | access object          | operator info                                                              |
+------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+
| IndexJoin_12                 | 9990.00 | root      |                        | inner join, inner:TableReader_11, outer key:test.t2.a, inner key:test.t1.a |
| ├─HashAgg_21(Build)          | 7992.00 | root      |                        | group by:test.t2.a, funcs:firstrow(test.t2.a)->test.t2.a                   |
| │ └─IndexReader_28           | 9990.00 | root      |                        | index:IndexFullScan_27                                                     |
| │   └─IndexFullScan_27       | 9990.00 | cop[tikv] | table:t2, index:idx(a) | keep order:false, stats:pseudo                                             |
| └─TableReader_11(Probe)      | 1.00    | root      |                        | data:TableRangeScan_10                                                     |
|   └─TableRangeScan_10        | 1.00    | cop[tikv] | table:t1               | range: decided by [test.t2.a], keep order:false, stats:pseudo              |
+------------------------------+---------+-----------+------------------------+----------------------------------------------------------------------------+

INサブクエリが比較的小さく、外部クエリが比較的大きい場合、この書き換えによりパフォーマンスが向上します。これは、書き換えなしでは、駆動テーブルとして t2 でindex joinを使用することが不可能であるためです。ただし、不利な点は、再書き込み中にt2を自動的に削除できず、テーブルが比較的大きい場合、この再書き込みがクエリのパフォーマンスに影響を与えることです。現在、この最適化を制御するために変数tidb_opt_insubq_to_join_and_aggが使用されています。この最適化が適切でない場合は、手動で無効にすることができます。

EXISTSサブクエリと... >/>=/</<=/=/!= (SELECT ... FROM ...)

現在、このようなシナリオのサブクエリでは、サブクエリが相関サブクエリでない場合、TiDB は最適化段階で事前に評価し、直接結果セットに置き換えます。下図のように、サブクエリEXISTSは事前に最適化段階でTRUEと評価されるため、最終的な実行結果には反映されません。

create table t1(a int);
create table t2(a int);
insert into t2 values(1);
explain select * from t where exists (select * from t2);
+------------------------+----------+-----------+---------------+--------------------------------+
| id                     | estRows  | task      | access object | operator info                  |
+------------------------+----------+-----------+---------------+--------------------------------+
| TableReader_12         | 10000.00 | root      |               | data:TableFullScan_11          |
| └─TableFullScan_11     | 10000.00 | cop[tikv] | table:t       | keep order:false, stats:pseudo |
+------------------------+----------+-----------+---------------+--------------------------------+

前述の最適化では、オプティマイザは文の実行を自動的に最適化します。さらに、ステートメントをさらに書き直すためにSEMI_JOIN_REWRITEヒントを追加することもできます。

このヒントを使用してクエリを書き直さない場合、実行プランでハッシュ結合が選択されている場合、セミ結合クエリはサブクエリを使用してハッシュ テーブルを構築することしかできません。この場合、サブクエリの結果が外側のクエリの結果よりも大きい場合、実行速度が予想よりも遅くなる可能性があります。

同様に、実行計画でインデックス結合が選択されている場合、セミ結合クエリは外部クエリのみを駆動テーブルとして使用できます。この場合、サブクエリの結果が外側のクエリの結果よりも小さい場合、実行速度が予想よりも遅くなる可能性があります。

SEMI_JOIN_REWRITE()を使用してクエリを書き換えると、オプティマイザーは選択範囲を拡張して、より適切な実行プランを選択できます。

エコシステム
TiDB
TiKV
TiSpark
Chaos Mesh
© 2022 PingCAP. All Rights Reserved.