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までにちゃんと提案できるブツを作れてよかった。
あとはドキュメントとソースコードコメントをちゃんと書いて投稿である。