このトピックでは、ApsaraDB for SelectDBが提供するランタイムフィルター機能を使用して、結合パフォーマンスを最適化する方法について説明します。
概要
ランタイムフィルターは、実行時に特定の結合クエリに対して動的に生成され、スキャンまたは計算されるデータ量を削減し、不要な I/O またはネットワーク転送を防ぎます。これにより、クエリが高速化されます。ランタイムフィルターの設計、実装、およびパフォーマンスの詳細については、「ISSUE 6116」をご参照ください。
用語
左側のテーブル: 結合クエリの左側のテーブル。このテーブルはプローブ操作に使用されます。結合の並べ替え機能を使用して順序を調整できます。
右側のテーブル: 結合クエリの右側のテーブル。このテーブルはビルド操作に使用されます。結合の並べ替え機能を使用して順序を調整できます。
フラグメント: SQL ステートメントの一部。フロントエンド (FE) ノードは SQL ステートメントをフラグメントに分割し、分散クラスター内のバックエンド (BE) ノードに実行のために配信します。フラグメントは BE ノードで実行され、結果は集計されて FE ノードに返されます。
しくみ
ランタイムフィルターは、クエリ計画中に動的に生成されます。HashJoinNode は結合の右側のテーブルをフィルター条件に変換し、そのフィルター条件を OlapScanNode にプッシュダウンします。次に、OlapScanNode はテーブルスキャン中にフィルター条件に基づいて左側のテーブルをプルーニングします。これにより、クエリ中に読み取りおよび計算されるデータが大幅に削減され、クエリのパフォーマンスが向上します。
たとえば、HashJoin モードでテーブル T1 とテーブル T2 の間で結合クエリが実行されます。T1 は 1,000,000 行のデータを持つファクトテーブルです。T2 は 200 行のデータを持つディメンションテーブルです。次の図は、通常のハッシュ結合でスキャンする必要があるデータ量を示しています。
| > HashJoinNode <
| | |
| | 1000000 | 200
| | |
| OlapScanNode OlapScanNode
| ^ ^
| | 1000000 | 200
| T1 T2
|前の図に示すように、テーブル T1 の大量のデータをスキャンする必要があり、大量のハッシュ結合計算が実行されます。
システムがテーブル T2 でスキャンされたデータレコードを HashJoinNode にプロアクティブに送信する場合、HashJoinNode はテーブル T2 のデータに基づいてフィルター条件を生成できます (テーブル T2 のデータの最大値または最小値など、またはブルームフィルターを作成します)。次に、テーブル T1 のスキャンを待機している OlapScanNode にフィルター条件を送信します。OlapScanNode はこのフィルター条件を適用し、フィルターされたデータを HashJoinNode に送信します。これにより、ハッシュテーブルのプローブ操作の数とネットワークオーバーヘッドが削減されます。このフィルター条件は、ランタイムフィルターと呼ばれます。次の図は結果を示しています。
| > HashJoinNode <
| | |
| | 6000 | 200
| | |
| OlapScanNode OlapScanNode
| ^ ^
| | 1000000 | 200
| T1 T2
|ランタイムフィルターをストレージエンジンにさらにプッシュダウンできる場合は、場合によってはインデックスを使用してデータをプルーニングできます。これにより、実際に読み取られるデータ量とスキャン時間が大幅に削減されます。次の図は結果を示しています。
| > HashJoinNode <
| | |
| | 6000 | 200
| | |
| OlapScanNode OlapScanNode
| ^ ^
| | 6000 | 200
| T1 T2
|上記の分析に基づいて、ランタイムフィルターは述語プッシュダウンやパーティションプルーニングとは異なることがわかります。ランタイムフィルターは、実行時に動的に生成されるフィルター条件です。JOIN ON 句はクエリ中に解析されてフィルター式が決定され、式は左側のテーブルを読み取っている OlapScanNode にブロードキャストされます。これにより、クエリ中に読み取られ計算されるデータ量が削減され、クエリのパフォーマンスが大幅に向上します。
ランタイムフィルターの種類
ApsaraDB for SelectDB は、次の種類のランタイムフィルターを提供します。
IN 述語: HashSet 構造を使用して IN 述語を実装し、OlapScanNode にプッシュダウンします。IN 述語の利点は、フィルタリングが効果的で高速であることです。欠点としては、IN 述語はブロードキャスト結合にのみ適用され、右側のテーブルのデータサイズがしきい値を超えると無効になります。ApsaraDB for SelectDB のしきい値は 1,024 です。右側のテーブルの行数が 1024 を超えると、IN 述語は無効になります。
ブルームフィルター: ハッシュテーブルのデータに基づいてブルームフィルターが作成され、OlapScanNode にプッシュダウンされます。ブルームフィルターは汎用性があり、さまざまなデータ型に適用でき、優れたパフォーマンスを提供します。欠点は、構成が複雑で計算オーバーヘッドが高いことです。
MinMax フィルター: 右側のテーブルのデータに基づいて範囲が指定されると、その範囲を MinMax フィルターとして OlapScanNode にプッシュダウンできます。MinMax フィルターの利点は、オーバーヘッドが比較的低いことです。欠点は、MinMax フィルターが数値以外の列ではうまく機能しないことです。
シナリオ
ランタイムフィルターは、大きなテーブルと小さなテーブルの間の結合を最適化するために使用されます。左側のテーブルのデータ量が少なすぎる場合、または右側のテーブルのデータ量が大きすぎる場合、ランタイムフィルターは期待どおりに機能しない可能性があります。ランタイムフィルターは、次の要件を満たすシナリオに適用されます。
左側のテーブルが大きく、右側のテーブルが小さい。これは、ランタイムフィルターの作成にメモリオーバーヘッドなどの計算コストが発生するためです。
左側のテーブルと右側のテーブルの間の結合の結果が少ない。これは、ランタイムフィルターが左側のテーブルのほとんどのデータをフィルターできることを示しています。
使用方法
クエリでランタイムフィルターを使用する
デフォルトでは、ApsaraDB for SelectDB でランタイムフィルター機能が有効になっています。ApsaraDB for SelectDB がユーザーのクエリを処理するとき、ApsaraDB for SelectDB はテーブルとクエリステートメントに基づいて IN 述語とブルームフィルターを自動的に生成してクエリを最適化します。
ランタイムフィルターのパラメーター
パラメーター | 説明 |
runtime_filter_mode | ランタイムフィルターのプッシュダウンポリシー。有効な値: OFF、LOCAL、GLOBAL。デフォルト値: GLOBAL。 |
runtime_filter_type | ランタイムフィルターの種類。ほとんどの場合、このパラメーターのみを調整し、その他のパラメーターにはデフォルト値を使用する必要があります。 有効な値: IN、BLOOM_FILTER、MIN_MAX、IN_OR_BLOOM_FILTER、BITMAP_FILTER。デフォルト値: IN_OR_BLOOM_FILTER。場合によっては、BLOOM_FILTER、MIN_MAX、IN を同時に指定してパフォーマンスを向上させることができます。 |
runtime_filter_wait_time_ms | 左側のテーブルの OlapScanNode が各ランタイムフィルターを待機する最大時間。単位: ミリ秒。デフォルト値: 1000。 |
runtime_filters_max_num | 各クエリに適用できるブルームフィルターの最大数。デフォルト値: 10。 |
runtime_bloom_filter_min_size | ブルームフィルターの最小長。デフォルト値: 1048576 (1 MiB)。 |
runtime_bloom_filter_max_size | ブルームフィルターの最大長。デフォルト値: 16777216 (16 MiB)。 |
runtime_bloom_filter_size | ブルームフィルターのデフォルトの長さ。デフォルト値: 2097152 (2 MiB)。 |
runtime_filter_max_in_num | IN 述語を生成しないことを決定するためのしきい値。右側のテーブルの行数がこのしきい値より大きい場合、IN 述語は生成されません。デフォルト値: 1024。 |
runtime_filter_mode
このパラメーターは、クエリ実行の最小単位間のランタイムフィルターの転送範囲を指定します。
有効な値: 数値 0、1、2、または対応するニーモニック文字列 OFF、LOCAL、GLOBAL。デフォルト値: 2 (GLOBAL)。
LOCAL: このポリシーは比較的保守的です。作成されたランタイムフィルターは、同じクエリ実行の最小単位の同じフラグメント内でのみ使用できます。この場合、フィルターを作成するランタイムフィルタープロデューサー (HashJoinNode) とフィルターを使用するランタイムフィルターコンシューマー (OlapScanNode) は同じフラグメント内にあります。このポリシーは、通常、通常のブロードキャスト結合などのシナリオで使用されます。
GLOBAL: このポリシーは比較的積極的です。このポリシーを使用すると、ランタイムフィルターをマージし、ネットワーク経由で異なる実行単位の異なるフラグメントに転送できます。たとえば、ランタイムフィルターのプロデューサーとコンシューマーは、シャッフル結合中に異なるフラグメントに存在できます。
LOCAL ポリシーが適用可能なシナリオに加えて、GLOBAL ポリシーを使用して、より幅広いシナリオでクエリを最適化できます。ただし、一部のシャッフル結合では、ランタイムフィルターの生成とマージのオーバーヘッドがクエリの性能向上を上回ります。この場合、ポリシーを LOCAL に変更できます。クラスター内の結合クエリのパフォーマンスがランタイムフィルターの恩恵を受けられない場合は、このパラメーターを OFF に設定してランタイムフィルター機能を無効にすることができます。
異なるフラグメントでランタイムフィルターを作成および適用する際のランタイムフィルターのマージの理由とポリシーの詳細については、「ISSUE 6116」をご参照ください。
ランタイムフィルターの種類
ランタイムフィルターのタイプ。
有効な値: 数値 1、2、4、8、および 16、または対応するニーモニック文字列 IN、BLOOM_FILTER、MIN_MAX、IN_OR_BLOOM_FILTER、および BITMAP_FILTER。既定値: 8 (IN_OR_BLOOM_FILTER)。複数の値を区切るにはコンマ (,) を使用し、引用符 (") で囲みます。型を表す複数の数値を加算することもできます。例:
set runtime_filter_type="BLOOM_FILTER,IN,MIN_MAX";上記の値の設定は、次の設定と同じです。
set runtime_filter_type=7;次の表は、ランタイムフィルターの種類について説明しています。
値 | 説明 |
IN | 右テーブルの JOIN ON 句で指定されたキー列のすべての値に基づいて、IN 述語が作成されます。 IN 述語は、左テーブルのデータをフィルタリングするために使用されます。 IN 述語の作成と適用にかかるオーバーヘッドは、ブルームフィルタの作成と適用にかかるオーバーヘッドよりも低くなります。右テーブルのデータサイズが小さい場合、パフォーマンスが向上します。
|
BLOOM_FILTER | ブルームフィルタには一定の偽陽性率があり、予想よりもフィルタリングされるデータが少なくなります。ただし、これは不正確な結果を引き起こすことはありません。ほとんどの場合、ブルームフィルタはパフォーマンスを向上させるか、パフォーマンスに大きな影響を与えません。ごく少数のケースでは、ブルームフィルタによってパフォーマンスが低下します。
|
MIN_MAX | MinMax フィルタは、最小値よりも小さく、最大値よりも大きいデータをフィルタリングするために使用されます。 MinMax フィルタのフィルタリング効果は、JOIN ON 句のキー列のタイプと、左テーブルと右テーブルのデータの分布に関連しています。
|
IN_OR_BLOOM_FILTER | システムは、実行中の右テーブルの実際の行数に基づいて、IN 述語を使用するかブルームフィルタを使用するかを自動的に決定します。
|
BITMAP_FILTER |
|
runtime_filter_wait_time_ms
ランタイムフィルターの待機期間。
有効な値: 整数。単位: ms。既定値: 1000。
ランタイムフィルター機能が有効になると、左テーブルの OlapScanNode は、データスキャン前に、OlapScanNode に割り当てられた各ランタイムフィルターに対して一定時間待機します。たとえば、OlapScanNode に 3 つのランタイムフィルターが割り当てられている場合、OlapScanNode は最大 3,000 ミリ秒待機します。
ランタイムフィルターの作成とマージには時間がかかります。OlapScanNode は、待機期間内に到着したランタイムフィルターをストレージエンジンにプッシュしようとします。期間が経過した後、OlapScanNode は到着したランタイムフィルターを使用してデータをスキャンします。
OlapScanNode がスキャンを開始した後にランタイムフィルターが到着した場合、OlapScanNode はランタイムフィルターをストレージエンジンにプッシュダウンしません。代わりに、OlapScanNode は、ランタイムフィルターに基づいてストレージエンジンからスキャンされたデータに対して、式によるフィルタリングを実行します。ランタイムフィルターは、既にスキャンされたデータには適用されません。このように、取得される中間データのサイズは最適なサイズよりも大きくなりますが、深刻なクラッキングを回避できます。
クラスターがビジー状態で、リソースを大量に消費するクエリまたは時間のかかるクエリが多数実行されている場合は、待機時間を延長して、複雑なクエリが最適化の機会を逃さないようにすることができます。クラスターの負荷が軽く、数秒しかかからない小さなクエリが多数実行されている場合は、待機時間を短縮して、クエリごとに 1 秒のレイテンシが増加するのを防ぐことができます。
runtime_filters_max_num
各クエリに適用できるブルームフィルターの最大数。
有効な値: 整数。既定値: 10。
ブルームフィルターの作成と適用は、MinMaxフィルターまたはIN述語よりもコストがかかるため、ブルームフィルターの数だけが制限されます。
生成されるブルームフィルターの数が上限を超えた場合、選択性の高いブルームフィルターが保持されます。選択性の高いブルームフィルターは、より多くの行をフィルターすることが期待されます。このパラメーターは、ブルームフィルターが過剰なメモリオーバーヘッドを消費するのを防ぎ、潜在的な問題の発生を防ぎます。
Selectivity = (HashJoinNode cardinality/HashJoinNode left child cardinality)
-- FEノードによって取得されたカーディナリティは不正確です。 Bloomフィルター用に計算された選択度は、実際の選択度から逸脱しています。 その結果、Bloomフィルターがランダムに保持される可能性があります。時間のかかる大きなテーブル間の結合クエリを調整する場合にのみ、このパラメーターを調整する必要があります。
runtime_filter_type
runtime_bloom_filter_min_size、runtime_bloom_filter_max_size、および runtime_bloom_filter_size パラメーターは、ブルームフィルターで使用されるデータ構造のサイズをバイト単位で決定するために使用されます。
有効な値: 整数。
HashJoinNode によって作成されたブルームフィルターは、マージする前に同じ長さである必要があります。そのため、FE ノードはクエリ計画中にブルームフィルターの長さを計算します。
右テーブルの統計情報でカーディナリティを取得できる場合、システムはカーディナリティに基づいてブルームフィルターの最適なサイズを推定し、サイズを 2 の累乗(底が 2 のログ値)に丸めます。右テーブルのカーディナリティを取得できない場合は、runtime_bloom_filter_size パラメーターで指定されたデフォルトのブルームフィルター長が使用されます。runtime_bloom_filter_min_size パラメーターと runtime_bloom_filter_max_size パラメーターは、最終的に使用されるブルームフィルターの最小長と最大長を制限するために使用されます。
サイズが大きいブルームフィルターは、カーディナリティの高い入力セットの処理においてより効率的ですが、より多くのメモリを消費します。たとえば、数百万の異なる値を含む列など、カーディナリティの高い列をフィルタリングする必要がある場合は、runtime_bloom_filter_size パラメーターの値を増やし、ベンチマークテストを実行できます。これにより、ブルームフィルターによるフィルタリングの精度が向上し、パフォーマンスの改善が期待できます。
ブルームフィルターの効果は、クエリのデータ分布に依存します。したがって、特定のクエリに対してのみブルームフィルターの長さを調整する必要があります。グローバルに長さを調整する必要はありません。これらのパラメーターは、大きなテーブル間の時間のかかる結合クエリを調整する場合にのみ調整する必要があります。
クエリ用に生成されたランタイムフィルターを表示する
EXPLAIN ステートメントを実行して、クエリのクエリプランを表示できます。クエリプランには、各フラグメントで使用される JOIN ON 句に関する情報と、フラグメントで生成および使用されるランタイムフィルターに関するコメントが含まれています。必要な JOIN ON 句にランタイムフィルターが適用されているかどうかを確認できます。
フラグメントで生成されるランタイムフィルターに関するサンプルコメント:
runtime filters: filter_id[type] <- table.column。フラグメントで使用されるランタイムフィルターに関するサンプルコメント:
runtime filters: filter_id[type] -> table.column。
次の例では、ID が RF000 のランタイムフィルターがクエリに使用されています。
CREATE TABLE test (t1 INT) DISTRIBUTED BY HASH (t1) BUCKETS 2;
INSERT INTO test VALUES (1), (2), (3), (4);
CREATE TABLE test2 (t2 INT) DISTRIBUTED BY HASH (t2) BUCKETS 2;
INSERT INTO test2 VALUES (3), (4), (5);
EXPLAIN SELECT t1 FROM test JOIN test2 where test.t1 = test2.t2;
+-------------------------------------------------------------------+
| Explain String |
+-------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS:`t1` |
| |
| 4:EXCHANGE |
| |
| PLAN FRAGMENT 1 |
| OUTPUT EXPRS: |
| PARTITION: HASH_PARTITIONED: `default_cluster:ssb`.`test`.`t1` |
| |
| 2:HASH JOIN |
| | join op: INNER JOIN (BUCKET_SHUFFLE) |
| | equal join conjunct: `test`.`t1` = `test2`.`t2` |
| | runtime filters: RF000[in] <- `test2`.`t2` |
| | |
| |----3:EXCHANGE |
| | |
| 0:OlapScanNode |
| TABLE: test |
| runtime filters: RF000[in] -> `test`.`t1` |
| |
| PLAN FRAGMENT 2 |
| OUTPUT EXPRS: |
| PARTITION: HASH_PARTITIONED: `default_cluster:ssb`.`test2`.`t2` |
| |
| 1:OlapScanNode |
| TABLE: test2 |
+-------------------------------------------------------------------+
-- "runtime filters" を含む行は、ID が RF000 の IN 述語が PLAN FRAGMENT 1 の 2:HASH JOIN によって生成されることを示しています。
-- `test2`.`t2` の値は実行時にのみ認識されます。
-- IN 述語は、ノードが `test`.`t1` を読み取るときに不要なデータをフィルタリングするために 0:OlapScanNode によって使用されます。
SELECT t1 FROM test JOIN test2 where test.t1 = test2.t2;
-- 結果の 2 行 [3, 4] を返します。
-- クエリプロファイルを使用して、クエリが内部的にどのように実行されるかを確認できます。プロファイルを有効にするには、set enable_profile=true; コマンドを実行する必要があります。
-- プロファイルには、各ランタイムフィルターがプッシュダウンされたかどうか、待機時間、および OLAP_SCAN_NODE が準備されてからランタイムフィルターが受信されるまでの合計期間が含まれます。
RuntimeFilter:in:
- HasPushDownToEngine: true
- AWaitTimeCost: 0ns
- EffectTimeCost: 2.76ms
-- また、プロファイルの OLAP_SCAN_NODE セクションで、ランタイムフィルターがプッシュダウンされた後のフィルタリング効果と消費時間を表示できます。
- RowsVectorPredFiltered: 9.320008M (9320008)
- VectorPredEvalTime: 364.39msランタイムフィルターの計画ルール
ランタイムフィルターは、JOIN ON句の等価条件に対してのみ生成できます。左テーブルのNULL値が除外される可能性があるため、NULLセーフ条件は含まれません。
ランタイムフィルターは、左外部結合、完全外部結合、またはアンチ結合の左テーブルにプッシュダウンできません。
ソース式またはターゲット式は定数にすることはできません。
ソース式はターゲット式と同じにすることはできません。
ソース式の型は、
HLLまたはBITMAPと同じにすることはできません。ランタイムフィルターは、OlapScanNodeにのみプッシュダウンできます。
ターゲット式には、
COALESCE、IFNULL、CASEなどのNULLチェック式を含めることはできません。別のより上位層の結合のJOIN ON句にNULLチェック式が含まれていて、ランタイムフィルターが生成される場合、この外部結合のランタイムフィルターが外部結合の左テーブルにプッシュダウンされた後、結果が正しくない可能性があります。ターゲット式の列に対応する等価な列が元のテーブルに存在する必要があります。
列伝導は、以下のシナリオではサポートされていません。
JOIN ON句に
A.k = B.k and B.k = C.kが含まれている場合、C.kはA.kではなくB.kにのみプッシュダウンできます。JOIN ON句に
A.a + B.b = C.cが含まれていて、A.aとB.aが等価な列であるため、A.aをB.aに伝導でき、A.aをB.aに置き換えることができ、システムはBにランタイムフィルターをプッシュしようとします。 A.aとB.aが等価な列でない場合、ターゲット式は1つの左テーブルのみにバインドする必要があるため、ランタイムフィルターはBにプッシュダウンできません。
ブルームフィルターはハッシュベースのフィルターであるため、ソース式とターゲット式の型は同じである必要があります。型が異なる場合、システムはターゲット式の型をソース式の型に変換しようとします。
PlanNode.Conjunctsによって生成されたランタイムフィルターはプッシュダウンできません。 HashJoinNodeのeqJoinConjunctsおよびotherJoinConjunctsとは異なり、PlanNode.Conjunctsは、テストに基づいて正しくない結果につながる可能性のあるランタイムフィルターを生成します。たとえば、INサブクエリが結合に変換された場合、自動的に生成されたJOIN ON句はPlanNode.Conjunctsに保存されます。この場合、ランタイムフィルターによって結果の行が欠落する可能性があります。