パラレル実行(Parallel Execution)とは、大きなタスクを複数の小さなタスクに分割し、複数のスレッドまたはプロセスを起動してこれらの小さなタスクを並列処理することで、より多くのCPUおよびI/Oリソースを活用し、操作の応答時間を短縮する手法です。
パラレル実行は、パラレルクエリ(Parallel Query)、パラレルDDL(Parallel DDL)、およびパラレルDML(Parallel DML)に分類されます。
パラレルクエリを開始する方法は以下の2つです:
PARALLELヒントを使用して並列度(DOP)を指定し、パラレルクエリを開始します。クエリ対象のパーティション数が1より多いパーティションテーブルでは、自動的にパラレルクエリが開始されます。
パーティションテーブルの並列クエリを有効にする
パーティションテーブルに対するクエリでは、クエリ対象のパーティション数が1より多い場合、システムは自動的に並列クエリを有効にします。dop の値はシステムデフォルトで1に設定されます。
以下の例のように、パーティションテーブル ptable を作成し、ptable の全表データに対するスキャン操作を実行します。EXPLAIN コマンドを使用して生成された実行計画を確認します。
obclient> CREATE TABLE PTABLE(c1 INT , c2 INT) PARTITION BY HASH(c1) PARTITIONS 16;
Query OK, 0 rows affected
obclient> EXPLAIN SELECT * FROM ptable;
Query Plan:
=======================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
-------------------------------------------------------
|0 |EXCHANGE IN DISTR | |1600000 |1246946|
|1 | EXCHANGE OUT DISTR |:EX10000|1600000 |1095490|
|2 | PX PARTITION ITERATOR| |1600000 |1095490|
|3 | TABLE SCAN |ptable |1600000 |1095490|
=======================================================
Outputs & filters:
-------------------------------------
0 - output([ptable.c1], [ptable.c2]), filter(nil)
1 - output([ptable.c1], [ptable.c2]), filter(nil), dop=1
2 - output([ptable.c1], [ptable.c2]), filter(nil)
3 - output([ptable.c1], [ptable.c2]), filter(nil),
access([ptable.c1], [ptable.c2]), partitions(p[0-15])
実行計画からわかるように、パーティションテーブルのデフォルトの並列クエリ dop は1です。OceanBaseクラスタに合計3つのOBServerノードがあり、テーブル ptable の16個のパーティションが3つのOBServerノードに分散している場合、各OBServerノードはワーカースレッド(Worker Thread)を1つ起動してパーティションデータのスキャンを実行します。テーブルのスキャンを実行するためには、合計3つのワーカースレッドを起動する必要があります。
パーティションテーブルに対して、PARALLEL ヒントを追加して並列クエリを開始し、dop 値を指定します。EXPLAIN コマンドを使用して生成された実行計画を確認します。
obclient> EXPLAIN SELECT /*+ PARALLEL(8) */ * FROM ptable;
Query Plan:
=======================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
-------------------------------------------------------
|0 |EXCHANGE IN DISTR | |1600000 |1246946|
|1 | EXCHANGE OUT DISTR |:EX10000|1600000 |1095490|
|2 | PX PARTITION ITERATOR| |1600000 |1095490|
|3 | TABLE SCAN |ptable |1600000 |1095490|
=======================================================
Outputs & filters:
-------------------------------------
0 - output([ptable.c1], [ptable.c2]), filter(nil)
1 - output([ptable.c1], [ptable.c2]), filter(nil), dop=8
2 - output([ptable.c1], [ptable.c2]), filter(nil)
3 - output([ptable.c1], [ptable.c2]), filter(nil),
access([ptable.c1], [ptable.c2]), partitions(p[0-15])
実行計画からわかるように、並列クエリの dop 値は8です。クエリ対象のパーティションが存在するOBServerノードの数が dop 値以下の場合、ワーカースレッド(合計数は dop 値と同じ)は一定の戦略に従って関連するOBServerノードに割り当てられます。クエリ対象のパーティションが存在するOBServerノードの数が dop 値を超える場合、各OBServerは少なくとも1つのワーカースレッドを起動し、必要なワーカースレッドの総数は dop 値を超えます。
例えば、dop=8 の場合、16個のパーティションが4台のOBServerノードに均等に分散している場合、各OBServerノードは対応するパーティションをスキャンするために2つのワーカースレッドを起動します(合計8つのワーカースレッド)。16個のパーティションが16台のOBServerノードに分散している場合(各ノードに1つのパーティション)、各OBServerノードは対応するパーティションをスキャンするために1つのワーカースレッドを起動します(合計16つのワーカースレッド)。
パーティションテーブルに対するクエリで、クエリ対象のパーティション数が1以下の場合、システムは並列クエリを開始しません。以下の例のように、ptable のクエリにフィルター条件 c1=1 を追加します。
obclient> EXPLAIN SELECT * FROM ptable WHERE c1 = 1;
Query Plan:
======================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
--------------------------------------
|0 |TABLE SCAN|ptable|990 |85222|
======================================
Outputs & filters:
-------------------------------------
0 - output([ptable.c1], [ptable.c2]), filter([ptable.c1 = 1]),
access([ptable.c1], [ptable.c2]), partitions(p1)
計画からわかるように、クエリの対象パーティション数は1で、システムは並列クエリを開始していません。1つのパーティションに対するクエリでも並列実行を希望する場合は、PARALLEL ヒントを追加してパーティション内の並列クエリを実行するしかありません。EXPLAIN コマンドを使用して生成された実行計画を確認します。
obclient> EXPLAIN SELECT /*+ PARALLEL(8) */ * FROM ptable WHERE c1 = 1;
Query Plan:
=================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
-------------------------------------------------
|0 |EXCHANGE IN DISTR | |990 |85316|
|1 | EXCHANGE OUT DISTR|:EX10000|990 |85222|
|2 | PX BLOCK ITERATOR| |990 |85222|
|3 | TABLE SCAN |ptable |990 |85222|
=================================================
Outputs & filters:
-------------------------------------
0 - output([ptable.c1], [ptable.c2]), filter(nil)
1 - output([ptable.c1], [ptable.c2]), filter(nil), dop=8
2 - output([ptable.c1], [ptable.c2]), filter(nil)
3 - output([ptable.c1], [ptable.c2]), filter([ptable.c1 = 1]),
access([ptable.c1], [ptable.c2]), partitions(p1)
注意
- クエリ対象のパーティション数が1の場合でもヒントを使用してパーティション内の並列クエリを実行したい場合は、対応する
dop値が2以上である必要があります。 dop値が空または2未満の場合、並列クエリは開始されません。
非パーティションテーブルの並列クエリを有効にする
非パーティションテーブルは本質的にパーティションが1つしかないパーティションテーブルであるため、非パーティションテーブルに対するクエリでは、PARALLEL ヒントを追加することでのみパーティション内の並列クエリを開始できます。そうでない場合は、並列クエリは開始されません。
以下の例のように、非パーティションテーブル stable を作成し、stable に対して全表データのスキャン操作を実行します。EXPLAIN コマンドで生成された実行計画を確認します。
obclient> CREATE TABLE stable(c1 INT, c2 INT);
Query OK, 0 rows affected
obclient> EXPLAIN SELECT * FROM stable;
Query Plan:
======================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
--------------------------------------
|0 |TABLE SCAN|stable|100000 |68478|
======================================
Outputs & filters:
-------------------------------------
0 - output([stable.c1], [stable.c2]), filter(nil),
access([stable.c1], [stable.c2]), partitions(p0)
実行計画からわかるように、ヒントを使用しない場合、非パーティションテーブルでは並列クエリは開始されません。
非パーティションテーブルに対して、PARALLEL ヒントを追加してパーティション内の並列クエリを開始し、dop 値(2以上)を指定します。EXPLAIN コマンドで生成された実行計画を確認します。
obclient> EXPLAIN SELECT /*+ PARALLEL(4)*/ * FROM stable;
Query Plan:
=================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
-------------------------------------------------
|0 |EXCHANGE IN DISTR | |100000 |77944|
|1 | EXCHANGE OUT DISTR|:EX10000|100000 |68478|
|2 | PX BLOCK ITERATOR| |100000 |68478|
|3 | TABLE SCAN |stable |100000 |68478|
=================================================
Outputs & filters:
-------------------------------------
0 - output([stable.c1], [stable.c2]), filter(nil)
1 - output([stable.c1], [stable.c2]), filter(nil), dop=4
2 - output([stable.c1], [stable.c2]), filter(nil)
3 - output([stable.c1], [stable.c2]), filter(nil),
access([stable.c1], [stable.c2]), partitions(p0)
複数テーブルの並列クエリを有効にする
クエリでは、複数テーブルのJOINが最も一般的です。
以下の例のように、まず2つのパーティションテーブル p1table と p2table を作成します:
obclient> CREATE TABLE p1table(c1 INT ,c2 INT) PARTITION BY HASH(c1) PARTITIONS 2;
Query OK, 0 rows affected
obclient> CREATE TABLE p2table(c1 INT ,c2 INT) PARTITION BY HASH(c1) PARTITIONS 4;
Query OK, 0 rows affected
p1table と p2table の JOIN 結果を照会します。JOIN 条件は p1table.c1=p2table.c2 です。実行計画は以下のとおりです:
obclient> EXPLAIN SELECT * FROM p1table p1 JOIN p2table p2 ON p1.c1=p2.c2;
Query Plan:
====================================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
--------------------------------------------------------------------
|0 |EXCHANGE IN DISTR | |784080000|614282633|
|1 | EXCHANGE OUT DISTR |:EX10001|784080000|465840503|
|2 | HASH JOIN | |784080000|465840503|
|3 | EXCHANGE IN DISTR | |200000 |155887 |
|4 | EXCHANGE OUT DISTR (BROADCAST)|:EX10000|200000 |136955 |
|5 | PX PARTITION ITERATOR | |200000 |136955 |
|6 | TABLE SCAN |p1 |200000 |136955 |
|7 | PX PARTITION ITERATOR | |400000 |273873 |
|8 | TABLE SCAN |p2 |400000 |273873 |
====================================================================
Outputs & filters:
-------------------------------------
0 - output([p1.c1], [p1.c2], [p2.c1], [p2.c2]), filter(nil)
1 - output([p1.c1], [p1.c2], [p2.c1], [p2.c2]), filter(nil), dop=1
2 - output([p1.c1], [p1.c2], [p2.c1], [p2.c2]), filter(nil),
equal_conds([p1.c1 = p2.c2]), other_conds(nil)
3 - output([p1.c1], [p1.c2]), filter(nil)
4 - output([p1.c1], [p1.c2]), filter(nil), dop=1
5 - output([p1.c1], [p1.c2]), filter(nil)
6 - output([p1.c1], [p1.c2]), filter(nil),
access([p1.c1], [p1.c2]), partitions(p[0-1])
7 - output([p2.c1], [p2.c2]), filter(nil)
8 - output([p2.c1], [p2.c2]), filter(nil),
access([p2.c1], [p2.c2]), partitions(p[0-3])
デフォルトでは、p1table と p2table(両方のテーブルのクエリ対象パーティション数が1より多い場合)に対して並列クエリが採用され、デフォルトの dop 値は1です。同様に、PARALLEL ヒントを使用することで並列度を変更できます。
以下の例のように、JOIN の条件を p1table.c1=p2table.c2 と p2table.c1=1 に変更すると、p2table では単一のパーティションのみが選択されます。実行計画は以下のとおりです:
obclient> EXPLAIN SELECT * FROM p1table p1 JOIN p2table p2 ON p1.c1=p2.c2 AND p2.c1=1;
Query Plan:
=============================================================
|ID|OPERATOR |NAME |EST. ROWS|COST |
-------------------------------------------------------------
|0 |EXCHANGE IN DISTR | |1940598 |1807515|
|1 | EXCHANGE OUT DISTR |:EX10001|1940598 |1440121|
|2 | HASH JOIN | |1940598 |1440121|
|3 | EXCHANGE IN DISTR | |990 |85316 |
|4 | EXCHANGE OUT DISTR (PKEY)|:EX10000|990 |85222 |
|5 | TABLE SCAN |p2 |990 |85222 |
|6 | PX PARTITION ITERATOR | |200000 |136955 |
|7 | TABLE SCAN |p1 |200000 |136955 |
=============================================================
Outputs & filters:
-------------------------------------
0 - output([p1.c1], [p1.c2], [p2.c1], [p2.c2]), filter(nil)
1 - output([p1.c1], [p1.c2], [p2.c1], [p2.c2]), filter(nil), dop=1
2 - output([p1.c1], [p1.c2], [p2.c1], [p2.c2]), filter(nil),
equal_conds([p1.c1 = p2.c2]), other_conds(nil)
3 - output([p2.c1], [p2.c2]), filter(nil)
4 - (#keys=1, [p2.c2]), output([p2.c1], [p2.c2]), filter(nil), dop=1
5 - output([p2.c1], [p2.c2]), filter([p2.c1 = 1]),
access([p2.c1], [p2.c2]), partitions(p1)
6 - output([p1.c1], [p1.c2]), filter(nil)
7 - output([p1.c1], [p1.c2]), filter(nil),
access([p1.c1], [p1.c2]), partitions(p[0-1])
計画からわかるように、p2table は1つのパーティションのみをスキャンする必要があり、デフォルトでは並列クエリは実行されません。p1table は2つのパーティションをスキャンする必要があり、デフォルトでは並列クエリが実行されます。同様に、PARALLEL ヒントを追加することで並列度を変更し、p2table の1パーティションに対するクエリをパーティション内の並列クエリに変更できます。
パラレル実行関連のシステムビュー
OceanBaseデータベースは、パラレル実行の実行状態および統計情報を確認するためのシステムビュー(G)V$OB_SQL_AUDITを提供しています。
(G)V$OB_SQL_AUDITには多数のフィールドが含まれていますが、そのうちパラレル実行に関連するフィールドはqc_id、dfo_id、sqc_id、worker_idです。
詳細については、(G)V$OB_SQL_AUDITを参照してください。