Custom Executor 試作版

現状、PostgreSQLのエグゼキュータを拡張するにはいくつか方法が考えられる。

  1. Executor(Start|Run|End)_hookを使う
    • エグゼキュータ全体を乗っ取る。逆に言えば、一部の処理(例えば集約演算)だけを俺様実装にしたい時には、本体側のコードをコピーするなりしないとダメ。
  2. Foreign Data Wrapperを使う
    • 特定のテーブルスキャンに限定すればアリ。しかし、集約演算やJOIN、ソートといった処理を俺様実装にする用途には使用できない。
  3. ブランチして俺様パッチを当てる
    • まぁ、これならどうにでもできますがね…。

しかし、あまり使い勝手がよろしくない。
例えば、GPUを使って集約演算を1000倍早くしたいと考えても、それを実装するために、モジュール側でエグゼキュータ全体のお守りをしなければならないというのは、全く嬉しくない。
という訳で、プラン木の一部を差し替えて、特定のエグゼキュータ・ノードだけをモジュール側で実装したコードで実行できるよう機能拡張にトライしてみた。名称は Custom Executor Node で、名付け主はRobert Haasなり。

イメージとしては以下の通り。
f:id:kaigai:20130831231525p:plain

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