このトピックでは、PolarDB-X 1.0 の SQL エグゼキュータが、ストレージレイヤーにプッシュダウンできない SQL クエリの一部を実行する方法について説明します。
基本概念
SQL エグゼキュータは、PolarDB-X 1.0 において論理レイヤーで演算子を実行するコンポーネントです。単純なポイントクエリの場合、クエリ全体が MySQL ストレージレイヤーにプッシュダウンされることがよくあります。このような場合、エグゼキュータの作業は最小限に抑えられ、MySQL の結果が直接クライアントに返されます。しかし、複雑な SQL クエリの場合、一部の演算子はプッシュダウンできません。その場合、PolarDB-X 1.0 のクエリエグゼキュータが残りの計算を実行する必要があります。
例えば、次のクエリを考えてみます。
SELECT l_orderkey, sum(l_extendedprice *(1 - l_discount)) AS revenue
FROM CUSTOMER, ORDERS, LINEITEM
WHERE c_mktsegment = 'AUTOMOBILE'
and c_custkey = o_custkey
and l_orderkey = o_orderkey
and o_orderdate < '1995-03-13'
and l_shipdate > '1995-03-13'
GROUP BY l_orderkey;
EXPLAIN コマンドを使用して、以下のように PolarDB-X 1.0 の実行計画を表示できます。
HashAgg(group="l_orderkey", revenue="SUM(*)")
HashJoin(condition="o_custkey = c_custkey", type="inner")
Gather(concurrent=true)
LogicalView(tables="ORDERS_[0-7],LINEITEM_[0-7]", shardCount=8, sql="SELECT `ORDERS`.`o_custkey`, `LINEITEM`.`l_orderkey`, (`LINEITEM`.`l_extendedprice` * (? - `LINEITEM`.`l_discount`)) AS `x` FROM `ORDERS` AS `ORDERS` INNER JOIN `LINEITEM` AS `LINEITEM` ON (((`ORDERS`.`o_orderkey` = `LINEITEM`.`l_orderkey`) AND (`ORDERS`.`o_orderdate` < ?)) AND (`LINEITEM`.`l_shipdate` > ?))")
Gather(concurrent=true)
LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)")
次の図に示すように、LogicalView 演算子の SQL は実行のために MySQL にプッシュダウンされます。PolarDB-X 1.0 のクエリエグゼキュータは、残りの演算子 (LogicalView 以外の演算子) を処理して最終結果を生成します。
Volcano モデル
PolarDB-X 1.0 は、他の多くのデータベースと同様に、Volcano モデルを使用しています。すべての演算子は、open() や next() などのメソッドを持つインターフェイスを実装しています。実行計画に基づいて、演算子はツリーを形成します。演算子は、子演算子の next() メソッドを呼び出してデータをプルし、そのデータを処理して、結果を親に渡します。最終的に、ルート演算子が最終的な結果セットを生成し、クライアントに返します。
次の例では、HashJoin 演算子がすでにハッシュテーブルを構築していると仮定します。親の `Project` 演算子がデータをリクエストすると、HashJoin 演算子はまず子である Gather 演算子からデータのバッチをリクエストします。次に、ハッシュテーブルを探索して JOIN の結果を見つけ、それを `Project` 演算子に返します。
場合によっては、演算子はすべての入力データを読み取ってメモリにキャッシュする必要があります。このプロセスはマテリアライズと呼ばれます。例えば、`HashJoin` 演算子は、メモリ内にハッシュテーブルを構築するために、内部テーブルからすべてのデータを読み取る必要があります。マテリアライズを実行する他の演算子には、集約のための `HashAgg` やソートのための `MemSort` があります。
メモリは有限のリソースであるため、マテリアライズされるデータ量がクエリごとの制限を超えたり、総メモリ使用量が PolarDB-X 1.0 ノードの制限を超えたりすると、メモリ不足エラー (OUT_OF_MEMORY) が発生します。
パラレルクエリ
パラレルクエリは、複数のスレッドを使用して単一の複雑なクエリを実行します。
パラレルクエリの実行計画は、標準の計画とは異なります。例えば、前と同じクエリを使用した場合、パラレル実行計画は次のようになります。
Gather(parallel=true)
ParallelHashAgg(group="o_orderdate,o_shippriority,l_orderkey", revenue="SUM(*)")
ParallelHashJoin(condition="o_custkey = c_custkey", type="inner")
LogicalView(tables="ORDERS_[0-7],LINEITEM_[0-7]", shardCount=8, sql="SELECT `ORDERS`.`o_custkey`, `ORDERS`.`o_orderdate`, `ORDERS`.`o_shippriority`, `LINEITEM`.`l_orderkey`, (`LINEITEM`.`l_extendedprice` * (? - `LINEITEM`.`l_discount`)) AS `x` FROM `ORDERS` AS `ORDERS` INNER JOIN `LINEITEM` AS `LINEITEM` ON (((`ORDERS`.`o_orderkey` = `LINEITEM`.`l_orderkey`) AND (`ORDERS`.`o_orderdate` < ?)) AND (`LINEITEM`.`l_shipdate` > ?))", parallel=true)
LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)", parallel=true)
この計画では、`Gather` 演算子はツリーの上位に配置されます。これは、その下のすべての演算子が並列で実行されることを意味します。その後、`Gather` 演算子が結果をマージします。
実行中、`Gather` の下の演算子は複数回インスタンス化され、各インスタンスが 1 つの並列度に対応します。デフォルトの並列度は、単一マシンの CPU コア数と同じです。Standard Edition インスタンスの場合、デフォルトの並列度は 8 で、Enterprise Edition インスタンスの場合は 16 です。
実行診断
`EXPLAIN` コマンドに加えて、次のコマンドを使用してパフォーマンスの問題を分析できます。
EXPLAIN ANALYZEコマンドは、PolarDB-X 1.0 サーバー内の各演算子のパフォーマンスメトリクスを分析します。EXPLAIN EXECUTEコマンドは、MySQL から集約された EXPLAIN 結果を返します。
次の例では、前のクエリを使用してそのパフォーマンスを分析する方法を示します。
EXPLAIN ANALYZE を実行すると、次の出力が得られます。明確にするために、一部の重要でない情報は削除されています。
explain analyze select l_orderkey, sum(l_extendedprice *(1 - l_discount)) as revenue from CUSTOMER, ORDERS, LINEITEM where c_mktsegment = 'AUTOMOBILE' and c_custkey = o_custkey and l_orderkey = o_orderkey and o_orderdate < '1995-03-13' and l_shipdate > '1995-03-13' group by l_orderkey;
HashAgg(group="o_orderdate,o_shippriority,l_orderkey", revenue="SUM(*)")
... actual time = 23.916 + 0.000, actual rowcount = 11479, actual memory = 1048576, instances = 1 ...
HashJoin(condition="o_custkey = c_custkey", type="inner")
... actual time = 0.290 + 23.584, actual rowcount = 30266, actual memory = 1048576, instances = 1 ...
Gather(concurrent=true)
... actual time = 0.000 + 23.556, actual rowcount = 151186, actual memory = 0, instances = 1 ...
LogicalView(tables="ORDERS_[0-7],LINEITEM_[0-7]", shardCount=8, sql="SELECT `ORDERS`.`o_custkey`, `ORDERS`.`o_orderdate`, `ORDERS`.`o_shippriority`, `LINEITEM`.`l_orderkey`, (`LINEITEM`.`l_extendedprice` * (? - `LINEITEM`.`l_discount`)) AS `x` FROM `ORDERS` AS `ORDERS` INNER JOIN `LINEITEM` AS `LINEITEM` ON (((`ORDERS`.`o_orderkey` = `LINEITEM`.`l_orderkey`) AND (`ORDERS`.`o_orderdate` < ?)) AND (`LINEITEM`.`l_shipdate` > ?))")
... actual time = 0.000 + 23.556, actual rowcount = 151186, actual memory = 0, instances = 4 ...
Gather(concurrent=true)
... actual time = 0.000 + 0.282, actual rowcount = 29752, actual memory = 0, instances = 1 ...
LogicalView(tables="CUSTOMER_[0-7]", shardCount=8, sql="SELECT `c_custkey` FROM `CUSTOMER` AS `CUSTOMER` WHERE (`c_mktsegment` = ?)")
... actual time = 0.000 + 0.282, actual rowcount = 29752, actual memory = 0, instances = 4 ...
出力のメトリクスは次のように定義されます。
actual time:演算子とその子の実行に費やされた実際の時間。プラス記号 (+) の前の値は `open` フェーズ (データの準備) の時間で、後の値は `next` フェーズ (データの生成) の時間です。actual rowcount:演算子によって生成された行数。actual memory:演算子が使用したメモリ量 (バイト単位)。instances:演算子インスタンスの数。非パラレルクエリの場合、この値は常に 1 です。パラレルクエリの場合、各並列度が 1 つのインスタンスを表します。インスタンス数が 1 より大きい場合、actual time、actual rowcount、およびactual memoryは、すべての並列インスタンスにわたる合計時間、合計行数、および合計メモリ使用量を表します。
actual time = 20,instances = 8 は、演算子の 8 つのインスタンスが並列で実行され、インスタンスあたりの平均実行時間が 2.5 秒であったことを意味します。出力は次のように解釈できます。
- `HashAgg` 演算子は、`open` フェーズで 23.916 秒をかけて子である `HashJoin` 演算子から出力をフェッチし、すべてのデータをグループ化して集約しました。このうち、23.601 秒が子からのデータフェッチに費やされ、グループ化と集約には約 0.3 秒しか使用されませんでした。
- `HashJoin` 演算子は、`open` フェーズで 0.290 秒をかけて右テーブル (下側の `Gather`) からデータをプルし、ハッシュテーブルを構築しました。`next` フェーズでは 23.584 秒をかけて左テーブルからデータをプルし、ハッシュテーブルを探索して JOIN 結果を生成しました。
- `Gather` 演算子は複数の結果セットをマージするだけなので、そのコストは通常低いです。
- 左側の `LogicalView` (計画の上側) は、データのフェッチに 23.556 秒かかりました。これにより、これがパフォーマンスボトルネックであると特定されます。
- 右側の `LogicalView` (下側) は、データのフェッチに 0.282 秒かかりました。
要約すると、パフォーマンスボトルネックは左側の `LogicalView` です。実行計画は、この演算子が `ORDERS` テーブルと `LINEITEM` テーブルに対して JOIN クエリを実行し、このクエリの MySQL での実行が遅いことを示しています。
次の EXPLAIN EXECUTE 文を実行して、MySQL からの EXPLAIN 結果を表示できます。
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
| 1 | SIMPLE | ORDERS | NULL | ALL | PRIMARY | NULL | NULL | NULL | 184795 | 33.33 | Using where |
| 1 | SIMPLE | LINEITEM | NULL | ref | PRIMARY | PRIMARY | 4 | qimu_0000.ORDERS.o_orderkey | 3 | 33.33 | Using where |
| 1 | SIMPLE | CUSTOMER | NULL | ALL | NULL | NULL | NULL | NULL | 18736 | 10.0 | Using where |
3 rows in set (0.17 sec)
結果では、最初の 2 行 (`ORDERS` と `LINEITEM`) が左側の `LogicalView` にプッシュダウンされたクエリを表し、3 行目 (`CUSTOMER`) が右側の `LogicalView` のクエリを表します。