Custom Executor 試作版
現状、PostgreSQLのエグゼキュータを拡張するにはいくつか方法が考えられる。
- Executor(Start|Run|End)_hookを使う
- エグゼキュータ全体を乗っ取る。逆に言えば、一部の処理(例えば集約演算)だけを俺様実装にしたい時には、本体側のコードをコピーするなりしないとダメ。
- Foreign Data Wrapperを使う
- 特定のテーブルスキャンに限定すればアリ。しかし、集約演算やJOIN、ソートといった処理を俺様実装にする用途には使用できない。
- ブランチして俺様パッチを当てる
- まぁ、これならどうにでもできますがね…。
しかし、あまり使い勝手がよろしくない。
例えば、GPUを使って集約演算を1000倍早くしたいと考えても、それを実装するために、モジュール側でエグゼキュータ全体のお守りをしなければならないというのは、全く嬉しくない。
という訳で、プラン木の一部を差し替えて、特定のエグゼキュータ・ノードだけをモジュール側で実装したコードで実行できるよう機能拡張にトライしてみた。名称は Custom Executor Node で、名付け主はRobert Haasなり。
イメージとしては以下の通り。

planner_hookを利用して、PostgreSQLのプラナーがPlannedStmtを作成した"後"でモジュールがプラン木の一部を書き換え、CustomExecノードを追加する、あるいはCustomExecノードで既存のエグゼキュータ・ノードを差し替える。
で、クエリ実行の開始、一行取り出し、クエリ実行の終了時にそれぞれモジュール側で実装されたコードが呼び出されるというワケだ。
APIは以下の通り。機能的に似ているのでFDWのAPIに似ておる。
void BeginCustomExec(CustomExecState *cestate, int eflags); TupleTableSlot *GetNextCustomExec(CustomExecState *node); void ReScanCustomExec(CustomExecState *node); void EndCustomExec(CustomExecState *node); void ExplainCustomExec(CustomExecState *node, ExplainState *es);
PoCとして、クエリ実行に要した時間を出力する xtime という拡張を自作してみた。
Custom Executorとして本体からコールバックを受けるには、予め自分自身を登録しておく必要がある。これがRegisterCustomExec()関数で、ここでは一連のコールバックの組に対して"xtime"という名前を付けている。
void
_PG_init(void)
{
CustomExecRoutine routine;
:
/*
* Registration of custom executor provider
*/
strcpy(routine.CustomExecName, "xtime");
routine.BeginCustomExec = xtime_begin;
routine.GetNextCustomExec = xtime_getnext;
routine.ReScanCustomExec = xtime_rescan;
routine.EndCustomExec = xtime_end;
routine.ExplainCustomExec = xtime_explain;
RegisterCustomExec(&routine);
:
}そして、プラン木に"xtime"のCustomExecノードを挿入するのがplanner_hookでの処理。標準のプラナーが生成したPlannedStmtに手を加えてCustomExecノードを追加する。(ここでは再帰的にプラン木を捜査して、各ノードの頭にCustomExecノードを追加している)
static PlannedStmt *
xtime_planner(Query *parse,
int cursorOptions,
ParamListInfo boundParams)
{
PlannedStmt *result;
if (original_planner_hook)
result = original_planner_hook(parse, cursorOptions,
boundParams);
else
result = standard_planner(parse, cursorOptions,
boundParams);
/* walk on underlying plan tree to inject custom-exec node */
result->planTree = xtime_subplan_walker(result->planTree, 0);
return result;
}これによって、クエリ実行計画はどのように修正されるのか。
例えば以下のようなクエリを実行するとする。
postgres=# EXPLAIN (costs off)
SELECT * FROM t1 JOIN t2 ON t1.a = t2.x
WHERE a < 5000 LIMIT 100;
QUERY PLAN
--------------------------------------------------
Limit
-> Hash Join
Hash Cond: (t2.x = t1.a)
-> Seq Scan on t2
-> Hash
-> Index Scan using t1_pkey on t1
Index Cond: (a < 5000)
(7 rows)これが、xtimeモジュールにより以下のように書き換えられる。
postgres=# LOAD '$libdir/xtime';
LOAD
postgres=# EXPLAIN (costs off)
SELECT * FROM t1 JOIN t2 ON t1.a = t2.x
WHERE a < 5000 LIMIT 100;
QUERY PLAN
--------------------------------------------------------------------
CustomExec:xtime
-> Limit
-> CustomExec:xtime
-> Hash Join
Hash Cond: (x = t1.a)
-> CustomExec:xtime on t2
-> Hash
-> CustomExec:xtime
-> Index Scan using t1_pkey on t1
Index Cond: (a < 5000)
(10 rows)数ヶ所にCustomExecノードが挿入されている。t2へのSeqScanが消えているのは、SeqScanの場合にはxtimeモジュールが自力でテーブルをスキャンするため(コードサンプルとして使う事を想定しているので)。
で、このクエリを実行すると以下のように表示される。CustomExecノードをプラン木の間に挟み込み、下位ノードの実行に要した時間を記録しているのである。
postgres=# \timing
Timing is on.
postgres=# SELECT * FROM t1 JOIN t2 ON t1.a = t2.x WHERE a < 5000 LIMIT 100;
INFO: execution time of Limit: 9.517 ms
INFO: execution time of Hash Join: 9.487 ms
INFO: execution time of CustomExec:xtime on t2: 0.047 ms
INFO: execution time of Index Scan on t1: 4.947 ms
:
(省略)
:
Time: 12.935 msひとまず、CommitFest:Sepまでにちゃんと提案できるブツを作れてよかった。
あとはドキュメントとソースコードコメントをちゃんと書いて投稿である。