Predicate Push Down(PPD)

このドキュメントでは、TiDB のロジック最適化ルールの 1 つである Predicate Push Down (PPD) を紹介します。述語のプッシュ ダウンを理解し、その適用可能なシナリオと適用できないシナリオを理解できるようにすることを目的としています。

PPD は、データ フィルタリングをできるだけ早く完了するために、選択演算子をデータ ソースのできるだけ近くにプッシュ ダウンします。これにより、データ送信または計算のコストが大幅に削減されます。

次のケースでは、PPD の最適化について説明します。ケース 1、2、および 3 は PPD が適用されるシナリオであり、ケース 4、5、および 6 は PPD が適用されないシナリオです。

ケース 1: 述語をストレージレイヤーにプッシュする

create table t(id int primary key, a int); explain select * from t where a < 1; +-------------------------+----------+-----------+---------------+--------------------------------+ | id | estRows | task | access object | operator info | +-------------------------+----------+-----------+---------------+--------------------------------+ | TableReader_7 | 3323.33 | root | | data:Selection_6 | | └─Selection_6 | 3323.33 | cop[tikv] | | lt(test.t.a, 1) | | └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------+----------+-----------+---------------+--------------------------------+ 3 rows in set (0.00 sec)

このクエリでは、述語a < 1を TiKVレイヤーにプッシュ ダウンしてデータをフィルター処理することで、ネットワーク送信のオーバーヘッドを削減できます。

ケース 2: 述語をストレージレイヤーにプッシュする

create table t(id int primary key, a int not null); explain select * from t where a < substring('123', 1, 1); +-------------------------+----------+-----------+---------------+--------------------------------+ | id | estRows | task | access object | operator info | +-------------------------+----------+-----------+---------------+--------------------------------+ | TableReader_7 | 3323.33 | root | | data:Selection_6 | | └─Selection_6 | 3323.33 | cop[tikv] | | lt(test.t.a, 1) | | └─TableFullScan_5 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------+----------+-----------+---------------+--------------------------------+

このクエリは、述語a < substring('123', 1, 1)substringの入力パラメーターが定数であるため、ケース 1 のクエリと同じ実行プランを持ち、事前に計算することができます。次に、述語は同等の述語a < 1に簡略化されます。その後、TiDB はa < 1を TiKV にプッシュできます。

ケース 3: 結合演算子の下に述語をプッシュする

create table t(id int primary key, a int not null); create table s(id int primary key, a int not null); explain select * from t join s on t.a = s.a where t.a < 1; +------------------------------+----------+-----------+---------------+--------------------------------------------+ | id | estRows | task | access object | operator info | +------------------------------+----------+-----------+---------------+--------------------------------------------+ | HashJoin_8 | 4154.17 | root | | inner join, equal:[eq(test.t.a, test.s.a)] | | ├─TableReader_15(Build) | 3323.33 | root | | data:Selection_14 | | │ └─Selection_14 | 3323.33 | cop[tikv] | | lt(test.s.a, 1) | | │ └─TableFullScan_13 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo | | └─TableReader_12(Probe) | 3323.33 | root | | data:Selection_11 | | └─Selection_11 | 3323.33 | cop[tikv] | | lt(test.t.a, 1) | | └─TableFullScan_10 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +------------------------------+----------+-----------+---------------+--------------------------------------------+ 7 rows in set (0.00 sec)

このクエリでは、述語t.a < 1を join の下にプッシュして事前にフィルター処理することで、join の計算オーバーヘッドを削減できます。

また、このSQL文は内部結合が実行されており、 ON条件はt.a = s.aです。述語s.a <1t.a < 1から派生し、結合演算子の下のsテーブルにプッシュダウンできます。 sテーブルをフィルタリングすると、結合の計算オーバーヘッドをさらに削減できます。

ケース 4: ストレージ レイヤーでサポートされていない述語をプッシュ ダウンできない

create table t(id int primary key, a int not null); desc select * from t where substring('123', a, 1) = '1'; +-------------------------+---------+-----------+---------------+----------------------------------------+ | id | estRows | task | access object | operator info | +-------------------------+---------+-----------+---------------+----------------------------------------+ | Selection_7 | 2.00 | root | | eq(substring("123", test.t.a, 1), "1") | | └─TableReader_6 | 2.00 | root | | data:TableFullScan_5 | | └─TableFullScan_5 | 2.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------+---------+-----------+---------------+----------------------------------------+

このクエリには、述語substring('123', a, 1) = '1'があります。

explainの結果から、述語が計算のために TiKV にプッシュ ダウンされていないことがわかります。これは、TiKV コプロセッサが組み込み関数substringをサポートしていないためです。

ケース 5: 外部結合の内部テーブルの述語をプッシュ ダウンできない

create table t(id int primary key, a int not null); create table s(id int primary key, a int not null); explain select * from t left join s on t.a = s.a where s.a is null; +-------------------------------+----------+-----------+---------------+-------------------------------------------------+ | id | estRows | task | access object | operator info | +-------------------------------+----------+-----------+---------------+-------------------------------------------------+ | Selection_7 | 10000.00 | root | | isnull(test.s.a) | | └─HashJoin_8 | 12500.00 | root | | left outer join, equal:[eq(test.t.a, test.s.a)] | | ├─TableReader_13(Build) | 10000.00 | root | | data:TableFullScan_12 | | │ └─TableFullScan_12 | 10000.00 | cop[tikv] | table:s | keep order:false, stats:pseudo | | └─TableReader_11(Probe) | 10000.00 | root | | data:TableFullScan_10 | | └─TableFullScan_10 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------------+----------+-----------+---------------+-------------------------------------------------+ 6 rows in set (0.00 sec)

この問合せでは、内部表sに述語s.a is nullがあります。

explainの結果から、述語が結合演算子の下にプッシュされていないことがわかります。これは、 onの条件が満たされない場合、外部結合によって内部テーブルにNULLの値が入力され、結合後の結果をフィルター処理するために述語s.a is nullが使用されるためです。結合の下の内部テーブルにプッシュ ダウンされると、実行計画は元の実行計画と同等ではなくなります。

ケース 6: ユーザー変数を含む述語をプッシュダウンできない

create table t(id int primary key, a char); set @a = 1; explain select * from t where a < @a; +-------------------------+----------+-----------+---------------+--------------------------------+ | id | estRows | task | access object | operator info | +-------------------------+----------+-----------+---------------+--------------------------------+ | Selection_5 | 8000.00 | root | | lt(test.t.a, getvar("a")) | | └─TableReader_7 | 10000.00 | root | | data:TableFullScan_6 | | └─TableFullScan_6 | 10000.00 | cop[tikv] | table:t | keep order:false, stats:pseudo | +-------------------------+----------+-----------+---------------+--------------------------------+ 3 rows in set (0.00 sec)

このクエリでは、テーブルtに述語a < @aがあります。述語の@aはユーザー変数です。

explainの結果からわかるように、述語はケース 2 のようではなく、 a < 1に単純化され、TiKV にプッシュダウンされます。これは、ユーザー変数@aの値が計算中に変更される可能性があり、TiKV が変更を認識しないためです。そのため、TiDB は@a1に置き換えたり、TiKV にプッシュしたりしません。

理解に役立つ例は次のとおりです。

create table t(id int primary key, a int); insert into t values(1, 1), (2,2); set @a = 1; select id, a, @a:=@a+1 from t where a = @a; +----+------+----------+ | id | a | @a:=@a+1 | +----+------+----------+ | 1 | 1 | 2 | | 2 | 2 | 3 | +----+------+----------+ 2 rows in set (0.00 sec)

このクエリからわかるように、 @aの値はクエリ中に変化します。したがって、 a = @aa = 1に置き換えて TiKV にプッシュすると、同等の実行計画にはなりません。