search
LoginSignup
1

More than 1 year has passed since last update.

posted at

updated at

PostgreSQL に Santa Clause(サンタ句)を実装する

本記事は「PostgreSQL Advent Calendar 2020」の 24日目です。

昨日は @sawada_masahiko さんの「シーケンシャル・スキャンだからといってデータがテーブルの先頭から返ってくるとは限らない、という話」でした。シーケンシャル・スキャンの挙動の解析のために集約関数を作るところが素敵です。さすがです。

はじめに

@yugo-nです。PostgreSQLの機能開発なんかをやってまして、現在の取り組みについては「PostgreSQLのマテリアライズドビューの自動&高速リフレッシュ機能を開発中」(「PostgreSQL Advent Calendar 2020」の14日目)で書かせていただきました。

Advent Calender をふと見ていると24日が空いてることに気づいたので、せっかくなのでもう一本書いてみることにしました。

今回はSQLの構文を拡張して PostgreSQL に機能を追加する方法をダイジェストでお伝えしてみたいと思います。

前提

PostgreSQL はC言語で書かれたRDBMSですので、SQLとC言語の知識は多少必要ですが雰囲気でなんとかなると思います。

PostgreSQLのコードは執筆時点のmasterブランチを想定しています。ビルドの方法はドキュメントやGoogle先生で。

どんな機能を作るか?

今日はクリスマス・イブということで、SELECT文に「SANTA句(SANTA clause12)」という新しい機能を追加してみることにします。

SANTA句を使うと、テーブルから取得された各行の数を増やすことができます。実際に何倍になるかはSANTA句に指定した数字を上限にランダムに決まります。例えば以下のようなテーブルがある場合:

santa=# SELECT * FROM box ;
  present  
-----------
 chocolate
 book
 game
(3 rows)

SELECT文の最後にSANTA 5を付けると、最大で行数が5倍に増えます。やった!プレゼントが増えた!!3

santa=# SELECT * FROM box SANTA 5;
INFO:  Merry Christmas!  Santa will give you 2 times rows as a present!!
  present  
-----------
 chocolate
 chocolate
 book
 book
 game
 game
(6 rows)

santa=# SELECT * FROM box SANTA 5;
INFO:  Merry Christmas!  Santa will give you 5 times rows as a present!!
  present  
-----------
 chocolate
 chocolate
 chocolate
 chocolate
 chocolate
 book
 book
 book
 book
 book
 game
 game
 game
 game
 game
(15 rows)

ただし、SANTA句を指定しても行数が増えない場合もあります。

santa=# SELECT * FROM box SANTA 5;
INFO:  Oh, were you a bad boy this year? Santa will not bring you any present on Christmas...
  present  
-----------
 chocolate
 book
 game
(3 rows)

良い子にしていれば望んだ通りのプレゼントが贈られますが、悪い子だった人にはサンタさんはプレゼントを持ってきてはくれません・・・4

クエリ処理の流れ

SANTA句の実装の前に、PostgreSQLにおけるクエリ処理の流れを簡単に説明します。

クエリ処理の流れ

SQL文字列はまずパーサによって構文解析されます。この段階では「与えられたSQLが文法を満たしているかどうか」がチェックされ、文法を満たしている場合にはその文を表現する木構造「パースツリー」が生成されます。

次にアナライズ処理が行われます。これは意味解析に相当し、SQL文の中で使用されているテーブルやカラム、演算子などが実際にデータベース内に存在するオブジェクトに対応付けされます。また、構文の表現であるパースツリーは、実行すべきクエリを表現する「クエリツリー」に変換されます。

次の書換システムでは、予め定義されたルールに基づきクエリツリーの書換が行われます。具体的には例えば「ビューにアクセス」をしているクエリを、「ビュー定義内のテーブルにアクセス」しているクエリに書き換えるといった処理が行われます。

クエリツリーから実行計画を生成するのがプランナです。クエリツリーが「何を実行するか」を表すのに対して、実行計画は「どう実行するか」を表す木構造です。例えば、同じテーブルの結合でも、どの種類の結合(ネストループ結合、マージ結合、ハッシュ結合)を使われ、テーブルがどう走査されるか(シーケンシャルスキャン、インデックススキャン、・・・)が具体的に決められます。最適化によりコストが最小となるような実行計画を生成することから、オプティマイザとも呼ばれます。

実行計画は EXPLAIN コマンドで確認することができます。実行計画の各ノード(ネストループ結合、インデックススキャン、シーケンシャルスキャンなど)はプランノードと呼ばれています。

postgres=# EXPLAIN SELECT * FROM pgbench_accounts JOIN pgbench_branches USING (bid) WHERE aid = 1;
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Nested Loop  (cost=0.42..9.67 rows=1 width=457)
   Join Filter: (pgbench_accounts.bid = pgbench_branches.bid)
   ->  Index Scan using pgbench_accounts_pkey on pgbench_accounts  (cost=0.42..8.44 rows=1 width=97)
         Index Cond: (aid = 1)
   ->  Seq Scan on pgbench_branches  (cost=0.00..1.10 rows=10 width=364)
(5 rows)

最後にエクゼキュータが生成された実行計画を実行して、その結果として抽出された行をクライアントに返します。

SANTA句の実装

それでは、上で説明した処理の流れに沿ってSANTA句を実装していきましょう。

構文解析

構文解析関係のコードは主に src/backend/parser/ 以下に格納されています。

まずは新しいキーワードSANTAをsrc/include/parser/kwlist.h に追加しておきます。

src/include/parser/kwlist.h
 PG_KEYWORD("rule", RULE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("santa", SANTA, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("savepoint", SAVEPOINT, UNRESERVED_KEYWORD, BARE_LABEL)

次にSELECT文の文法を拡張します。PostgreSQLではSQLの文法は src/backend/parser/gram.y に Bison で記述されています。例えば以下は SELECT 文の構文の1つですが、「初めに SELECT キーワードがあって、その後にDISTINCT句、ターゲットリスト、INTO句、FROM句、WHERE句、GROUP BY句、HAVING句、WINDOW句が続く」といった具合です。5

src/backend/parser/gram.y
simple_select:
(中略)
            | SELECT distinct_clause target_list
            into_clause from_clause where_clause
            group_clause having_clause window_clause
                {
                    SelectStmt *n = makeNode(SelectStmt);
                    n->distinctClause = $2;
                    n->targetList = $3;
                    n->intoClause = $4;
                    n->fromClause = $5;
                    n->whereClause = $6;
                    n->groupClause = $7;
                    n->havingClause = $8;
                    n->windowClause = $9;
                    $$ = (Node *)n;
                }

SQL文字列がこの構文にマッチした場合の処理はC言語で書かれています。この場合は、SELECT文の解析結果が格納された SelectStmt 構造体が生成されており、これが最終的には構文解析の結果(パースツリー)となります。$$という変数が解析結果を表しています。

また、$nという変数は構文中の「n番目の要素の解析結果」を意味しています。例えば、$5from_clauseの解析結果で、それは以下のように定義されています。詳細は省きますが、要は「FROM句は無い場合があり、ある場合は FROMに続いてテーブル、またはテーブルのカンマ区切りリストが来る」であることを意味しています。

src/backend/parser/gram.y
from_clause:
            FROM from_list                          { $$ = $2; }
            | /*EMPTY*/                             { $$ = NIL; }
        ;

from_list:
            table_ref                               { $$ = list_make1($1); }
            | from_list ',' table_ref               { $$ = lappend($1, $3); }
        ;

SELECT文の最後にSANTA句を追加するには、以下のように書き換えます。

src/backend/parser/gram.y
simple_select:
(中略)
            | SELECT distinct_clause target_list
-           into_clause from_clause where_clause
+           into_clause from_clause where_clause santa_clause
            group_clause having_clause window_clause
                {
                    SelectStmt *n = makeNode(SelectStmt);
                    n->distinctClause = $2;
                    n->targetList = $3;
                    n->intoClause = $4;
                    n->fromClause = $5;
                    n->whereClause = $6;
                    n->groupClause = $7;
                    n->havingClause = $8;
                    n->windowClause = $9;
+                   n->santaClause = $10;

                    $$ = (Node *)n;
                }

santa_clause の定義も追加しておきましょう。キーワードSANTAの後ろのIconstは整数の定数で、この値を上限とする乱数値を結果としています。ついでに、INFO メッセージも出力させています。

src/backend/parser/gram.y
+santa_clause:
+            SANTA Iconst
+                {
+                    if ($2 > 1)
+                    {
+                        int santa = random() % $2 + 1;
+
+                        if (santa > 1)
+                            ereport(INFO, errmsg("Merry Christmas!  Santa will give you %d times rows as a present!!", santa));
+                        else
+                            ereport(INFO, errmsg("Oh, were you a bad boy this year? Santa will not bring you any present on Christmas..."));
+                        $$ = santa;
+                    }
+                    else
+                        $$ = 0;
+                }
+            | /*EMPTY*/         { $$ = 0; }
+        ;

SANTA句の解析結果を格納するメンバ santaClauseSelectStmt 構造体に追加しておきましょう。

src/include/nodes/parsenodes.h
typedef struct SelectStmt
{
(中略)
    List       *targetList;     /* the target list (of ResTarget) */
    List       *fromClause;     /* the FROM clause */
    Node       *whereClause;    /* WHERE qualification */
    List       *groupClause;    /* GROUP BY clauses */
    Node       *havingClause;   /* HAVING conditional-expression */
    List       *windowClause;   /* WINDOW window_name AS (...), ... */
+   int         santaClause;

アナライズ処理

アナライズ処理関係のコードも src/backend/parser/ 以下にあり、そのメイン処理は src/backend/parser/analyze.c にかかれています。

今回は特に難しい解析は行わず、パースツリー内にあるSANTA句の情報を、クエリツリーに移し替えるだけです。まずクエリツリーを表すQuery構造体に santa メンバを追加しておきます。

src/include/nodes/parsenodes.h
typedef struct Query
{
    NodeTag     type;

    CmdType     commandType;    /* select|insert|update|delete|utility */
(中略)

    List       *withCheckOptions;   /* a list of WithCheckOption's (added
                                     * during rewrite) */
+   int         santa;
(中略)
    int         stmt_len;       /* length in bytes; 0 means "rest of string" */
} Query;

アナライズ処理でSELECT文の解析のエントリポイントとなる関数は transformSelectStmt です。ここで、SelectStmt 構造体の santaClauseQuery 構造体の santa メンバにコピーしておけばいいでしょう。

src/backend/parser/analyze.c
/*
 * transformSelectStmt -
 *    transforms a Select Statement
 *
 * Note: this covers only cases with no set operations and no VALUES lists;
 * see below for the other cases.
 */
static Query *
transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
{
    Query      *qry = makeNode(Query);
    Node       *qual;
    ListCell   *l;

    qry->commandType = CMD_SELECT;
(中略)
    /* SANTA clause */
+   qry->santa = Max(stmt->santaClause, 0);

    return qry;
}

プランナ

書換システムについては何もすることがないので、次はプランナのコードを書きます。とは言っても、SANTA句に特に最適化する要素はない6ので、やることはクエリツリー内のSANTA情報を実行計画にも含めるだけです。

SANTA句が影響するのは、テーブルから行を取り出す以下のプランノードです。

  • シーケンシャルスキャン(SeqScan)
  • インデックススキャン(IndexScan)
  • インデックスオンリースキャン(IndexOnlyScan)
  • ビットマップヒープスキャン(BitmapHeapScan)
  • サンプルスキャン(SampleScan)
  • TIDスキャン(TiScan)

これらのプランノードを表す構造体は全て共通のScan構造体から派生しているので、そこにsantaメンバを追加しておきます。

src/include/nodes/plannodes.h
/*
 * ==========
 * Scan nodes
 * ==========
 */
typedef struct Scan
{
    Plan        plan;
    Index       scanrelid;      /* relid is index into the range table */
+   int         santa;
} Scan;

プランナの中で実際にプランの生成を行うコードは src/backend/optimizer/plan/createplan.c にかかれています。各プランノードを作成する関数がありますが、ここではシーケンシャルスキャン(SeqScan)のみを見ていくことにしましょう。

SeqScan ノードを生成する関数は create_seqscan_plan です。この中で呼ばれる make_seqscan が実際に SeqScan ノードを作っているので、これにクエリツリーの santa メンバを渡してあげましょう。少しわかりにくいですが、root->parse がクエリツリーを指しています。

src/backend/optimizer/plan/createplan.c
static SeqScan *
create_seqscan_plan(PlannerInfo *root, Path *best_path,
                    List *tlist, List *scan_clauses)
{
    SeqScan    *scan_plan;
    Index       scan_relid = best_path->parent->relid;

(中略)

    scan_plan = make_seqscan(tlist,
                             scan_clauses,
-                            scan_relid);
+                            scan_relid, root->parse->santa);

    copy_generic_path_info(&scan_plan->plan, best_path);

    return scan_plan;
}

make_seqscan の引数には santa が追加されており、その値を生成された SeqScan ノードの santa メンバに代入しています。

src/backend/optimizer/plan/createplan.c
static SeqScan *
make_seqscan(List *qptlist,
             List *qpqual,
-            Index scanrelid)
+            Index scanrelid,
+            int santa)
{
    SeqScan    *node = makeNode(SeqScan);
    Plan       *plan = &node->plan;

    plan->targetlist = qptlist;
    plan->qual = qpqual;
    plan->lefttree = NULL;
    plan->righttree = NULL;
    node->scanrelid = scanrelid;
+   node->santa = santa;

    return node;
}

省略していますが、他のインデックススキャン(IndexScan)、インデックスオンリースキャン(IndexOnlyScan)、ビットマップヒープスキャン(BitmapHeapScan)、サンプルスキャン(SampleScan)、TIDスキャン(TiScan)についても同様の修正をしています。

エクゼキュータ

いよいよエクゼキュータのコードに手を入れます。エクゼキュータのコードは src/backend/executor/ 以下に納められており、そのファイル名はプランノード毎に nodeXXXXX.c となっています。例えば、シーケンシャルスキャンのコードは nodeSeqscan.c に書かれています。

プランナのときと同じく、ここではシーケンシャルスキャン(SeqScan)についてのみ見ていきましょう。ここでしたいのは、テーブルから取り出した各行を santa で指定された数だけ繰り返して出力することです。

まず、ScanState構造体にメンバを追加します。この構造体は全てのスキャンで共通して使われる「状態ノード」です。状態ノードには「プランを実行中に変化するプランノード固有の状態」が格納されます。今回は各行を出力すべき行数(ss_santa)と、これまでに出力した行数(ss_santa_count)をここで管理することにします。

src/include/nodes/execnodes.h
typedef struct ScanState
{
    PlanState   ps;             /* its first field is NodeTag */
    Relation    ss_currentRelation;
    struct TableScanDescData *ss_currentScanDesc;
    TupleTableSlot *ss_ScanTupleSlot;
+   int         ss_santa;
+   int         ss_santa_count;
} ScanState;

エクゼキュータは実際のプラン実行の前に状態ノードの初期化を行います。SeqScanの場合、その処理は ExecInitSeqScan で行われるので、ここで上記のメンバの初期化を行いましょう。

src/backend/executor/nodeSeqscan.c
SeqScanState *
ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
{
    SeqScanState *scanstate;
(中略)
    scanstate = makeNode(SeqScanState);
    scanstate->ss.ps.plan = (Plan *) node;
    scanstate->ss.ps.state = estate;
    scanstate->ss.ps.ExecProcNode = ExecSeqScan;
+   scanstate->ss.ss_santa = node->santa;
+   scanstate->ss.ss_santa_count = 0;
(中略)
    return scanstate;
}

そして、SeqScan で「次の行を取得」する処理を行っているのは SeqNext です。これを、同じ行を santa 回数出力するように改造したら目的は達成です。

src/backend/executor/nodeSeqscan.c
static TupleTableSlot *
SeqNext(SeqScanState *node)
{
    TableScanDesc scandesc;
    EState     *estate;
    ScanDirection direction;
    TupleTableSlot *slot;

    /*
     * get information from the estate and scan state
     */
    scandesc = node->ss.ss_currentScanDesc;
    estate = node->ss.ps.state;
    direction = estate->es_direction;
    slot = node->ss.ss_ScanTupleSlot;

    if (scandesc == NULL)
    {
        /*
         * We reach here if the scan is not parallel, or if we're serially
         * executing a scan that was planned to be parallel.
         */
        scandesc = table_beginscan(node->ss.ss_currentRelation,
                                   estate->es_snapshot,
                                   0, NULL);
        node->ss.ss_currentScanDesc = scandesc;
    }

+    /* If SANTA clause exists, repeate each tuple "santa" times. */
+    else if (node->ss.ss_santa > 1)
+    {
+        if (++node->ss.ss_santa_count < node->ss.ss_santa)
+            return slot;
+        else
+            node->ss.ss_santa_count = 0;
+    }
+
    /*
     * get the next tuple from the table
     */
    if (table_scan_getnextslot(scandesc, direction, slot))
        return slot;
    return NULL;
}

他のスキャン(IndexScan、IndexOnlyScan、BitmapHeapScan、SampleScan、TiScan)のエクゼキュータのコードについても同様の改造を行えば、SANTA句の実装は完了です。

シーケンシャルスキャン以外のプランノードと細かい所は省略して説明しましたが、最終的なパッチを以下に置いておきますので詳細は実際のコードをご参照ください。

最終的なパッチ(671行)
diff --git a/src/backend/executor/nodeBitmapHeapscan.c b/src/backend/executor/nodeBitmapHeapscan.c
index 5a5c410106..6c023177e7 100644
--- a/src/backend/executor/nodeBitmapHeapscan.c
+++ b/src/backend/executor/nodeBitmapHeapscan.c
@@ -184,6 +184,15 @@ BitmapHeapNext(BitmapHeapScanState *node)
        node->initialized = true;
    }

+   /* If SANTA clause exists, repeate each tuple "santa" times. */
+   else if (node->ss.ss_santa > 1)
+   {
+       if (++node->ss.ss_santa_count < node->ss.ss_santa)
+           return slot;
+       else
+           node->ss.ss_santa_count = 0;
+   }
+
    for (;;)
    {
        bool        skip_fetch;
@@ -724,6 +733,8 @@ ExecInitBitmapHeapScan(BitmapHeapScan *node, EState *estate, int eflags)
    scanstate->ss.ps.plan = (Plan *) node;
    scanstate->ss.ps.state = estate;
    scanstate->ss.ps.ExecProcNode = ExecBitmapHeapScan;
+   scanstate->ss.ss_santa = node->scan.santa;
+   scanstate->ss.ss_santa_count = 0;

    scanstate->tbm = NULL;
    scanstate->tbmiterator = NULL;
diff --git a/src/backend/executor/nodeIndexonlyscan.c b/src/backend/executor/nodeIndexonlyscan.c
index 5617ac29e7..b4a4f5392a 100644
--- a/src/backend/executor/nodeIndexonlyscan.c
+++ b/src/backend/executor/nodeIndexonlyscan.c
@@ -115,6 +115,15 @@ IndexOnlyNext(IndexOnlyScanState *node)
                         node->ioss_NumOrderByKeys);
    }

+   /* If SANTA clause exists, repeate each tuple "santa" times. */
+   else if (node->ss.ss_santa > 1)
+   {
+       if (++node->ss.ss_santa_count < node->ss.ss_santa)
+           return slot;
+       else
+           node->ss.ss_santa_count = 0;
+   }
+
    /*
     * OK, now that we have what we need, fetch the next tuple.
     */
@@ -504,6 +513,8 @@ ExecInitIndexOnlyScan(IndexOnlyScan *node, EState *estate, int eflags)
    indexstate->ss.ps.plan = (Plan *) node;
    indexstate->ss.ps.state = estate;
    indexstate->ss.ps.ExecProcNode = ExecIndexOnlyScan;
+   indexstate->ss.ss_santa = node->scan.santa;
+   indexstate->ss.ss_santa_count = 0;

    /*
     * Miscellaneous initialization
diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c
index d0a96a38e0..6dd12248ab 100644
--- a/src/backend/executor/nodeIndexscan.c
+++ b/src/backend/executor/nodeIndexscan.c
@@ -127,6 +127,15 @@ IndexNext(IndexScanState *node)
                         node->iss_OrderByKeys, node->iss_NumOrderByKeys);
    }

+   /* If SANTA clause exists, repeate each tuple "santa" times. */
+   else if (node->ss.ss_santa > 1)
+   {
+       if (++node->ss.ss_santa_count < node->ss.ss_santa)
+           return slot;
+       else
+           node->ss.ss_santa_count = 0;
+   }
+
    /*
     * ok, now that we have what we need, fetch the next tuple.
     */
@@ -222,6 +231,15 @@ IndexNextWithReorder(IndexScanState *node)
                         node->iss_OrderByKeys, node->iss_NumOrderByKeys);
    }

+   /* If SANTA clause exists, repeate each tuple "santa" times. */
+   else if (node->ss.ss_santa > 1)
+   {
+       if (++node->ss.ss_santa_count < node->ss.ss_santa)
+           return slot;
+       else
+           node->ss.ss_santa_count = 0;
+   }
+
    for (;;)
    {
        CHECK_FOR_INTERRUPTS();
@@ -910,6 +928,8 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags)
    indexstate->ss.ps.plan = (Plan *) node;
    indexstate->ss.ps.state = estate;
    indexstate->ss.ps.ExecProcNode = ExecIndexScan;
+   indexstate->ss.ss_santa = node->scan.santa;
+   indexstate->ss.ss_santa_count = 0;

    /*
     * Miscellaneous initialization
diff --git a/src/backend/executor/nodeSamplescan.c b/src/backend/executor/nodeSamplescan.c
index 4732c926f7..83c073f07e 100644
--- a/src/backend/executor/nodeSamplescan.c
+++ b/src/backend/executor/nodeSamplescan.c
@@ -109,6 +109,8 @@ ExecInitSampleScan(SampleScan *node, EState *estate, int eflags)
    scanstate->ss.ps.plan = (Plan *) node;
    scanstate->ss.ps.state = estate;
    scanstate->ss.ps.ExecProcNode = ExecSampleScan;
+   scanstate->ss.ss_santa = node->scan.santa;
+   scanstate->ss.ss_santa_count = 0;

    /*
     * Miscellaneous initialization
@@ -337,6 +339,15 @@ tablesample_getnext(SampleScanState *scanstate)
    TableScanDesc scan = scanstate->ss.ss_currentScanDesc;
    TupleTableSlot *slot = scanstate->ss.ss_ScanTupleSlot;

+   /* If SANTA clause exists, repeate each tuple "santa" times. */
+   if (scanstate->donetuples > 0 && scanstate->ss.ss_santa > 1)
+   {
+       if (++scanstate->ss.ss_santa_count < scanstate->ss.ss_santa)
+           return slot;
+       else
+           scanstate->ss.ss_santa_count = 0;
+   }
+
    ExecClearTuple(slot);

    if (scanstate->done)
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index 1a7c1e919f..69803b1982 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -74,6 +74,15 @@ SeqNext(SeqScanState *node)
        node->ss.ss_currentScanDesc = scandesc;
    }

+   /* If SANTA clause exists, repeate each tuple "santa" times. */
+   else if (node->ss.ss_santa > 1)
+   {
+       if (++node->ss.ss_santa_count < node->ss.ss_santa)
+           return slot;
+       else
+           node->ss.ss_santa_count = 0;
+   }
+
    /*
     * get the next tuple from the table
     */
@@ -138,6 +147,8 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
    scanstate->ss.ps.plan = (Plan *) node;
    scanstate->ss.ps.state = estate;
    scanstate->ss.ps.ExecProcNode = ExecSeqScan;
+   scanstate->ss.ss_santa = node->santa;
+   scanstate->ss.ss_santa_count = 0;

    /*
     * Miscellaneous initialization
diff --git a/src/backend/executor/nodeTidscan.c b/src/backend/executor/nodeTidscan.c
index 8049fdc64e..66f3fdc35b 100644
--- a/src/backend/executor/nodeTidscan.c
+++ b/src/backend/executor/nodeTidscan.c
@@ -338,6 +338,15 @@ TidNext(TidScanState *node)
    tidList = node->tss_TidList;
    numTids = node->tss_NumTids;

+   /* If SANTA clause exists, repeate each tuple "santa" times. */
+   if (node->tss_TidPtr >= 0 && node->ss.ss_santa > 1)
+   {
+       if (++node->ss.ss_santa_count < node->ss.ss_santa)
+           return slot;
+       else
+           node->ss.ss_santa_count = 0;
+   }
+
    /*
     * Initialize or advance scan position, depending on direction.
     */
@@ -507,6 +516,8 @@ ExecInitTidScan(TidScan *node, EState *estate, int eflags)
    tidstate->ss.ps.plan = (Plan *) node;
    tidstate->ss.ps.state = estate;
    tidstate->ss.ps.ExecProcNode = ExecTidScan;
+   tidstate->ss.ss_santa = node->scan.santa;
+   tidstate->ss.ss_santa_count = 0;

    /*
     * Miscellaneous initialization
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 70f8b718e0..28cfcd3c95 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -412,6 +412,7 @@ CopyScanFields(const Scan *from, Scan *newnode)
    CopyPlanFields((const Plan *) from, (Plan *) newnode);

    COPY_SCALAR_FIELD(scanrelid);
+   COPY_SCALAR_FIELD(santa);
 }

 /*
@@ -3091,6 +3092,7 @@ _copyQuery(const Query *from)
    COPY_NODE_FIELD(setOperations);
    COPY_NODE_FIELD(constraintDeps);
    COPY_NODE_FIELD(withCheckOptions);
+   COPY_SCALAR_FIELD(santa);
    COPY_LOCATION_FIELD(stmt_location);
    COPY_SCALAR_FIELD(stmt_len);

@@ -3167,6 +3169,7 @@ _copySelectStmt(const SelectStmt *from)
    COPY_NODE_FIELD(groupClause);
    COPY_NODE_FIELD(havingClause);
    COPY_NODE_FIELD(windowClause);
+   COPY_SCALAR_FIELD(santaClause);
    COPY_NODE_FIELD(valuesLists);
    COPY_NODE_FIELD(sortClause);
    COPY_NODE_FIELD(limitOffset);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index 541e0e6b48..5079303493 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -989,6 +989,7 @@ _equalQuery(const Query *a, const Query *b)
    COMPARE_NODE_FIELD(setOperations);
    COMPARE_NODE_FIELD(constraintDeps);
    COMPARE_NODE_FIELD(withCheckOptions);
+   COMPARE_SCALAR_FIELD(santa);
    COMPARE_LOCATION_FIELD(stmt_location);
    COMPARE_SCALAR_FIELD(stmt_len);

@@ -1055,6 +1056,7 @@ _equalSelectStmt(const SelectStmt *a, const SelectStmt *b)
    COMPARE_NODE_FIELD(groupClause);
    COMPARE_NODE_FIELD(havingClause);
    COMPARE_NODE_FIELD(windowClause);
+   COMPARE_SCALAR_FIELD(santaClause);
    COMPARE_NODE_FIELD(valuesLists);
    COMPARE_NODE_FIELD(sortClause);
    COMPARE_NODE_FIELD(limitOffset);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index d78b16ed1d..f389fc3369 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -352,6 +352,7 @@ _outScanInfo(StringInfo str, const Scan *node)
    _outPlanInfo(str, (const Plan *) node);

    WRITE_UINT_FIELD(scanrelid);
+   WRITE_INT_FIELD(santa);
 }

 /*
@@ -2762,6 +2763,7 @@ _outSelectStmt(StringInfo str, const SelectStmt *node)
    WRITE_NODE_FIELD(groupClause);
    WRITE_NODE_FIELD(havingClause);
    WRITE_NODE_FIELD(windowClause);
+   WRITE_INT_FIELD(santaClause);
    WRITE_NODE_FIELD(valuesLists);
    WRITE_NODE_FIELD(sortClause);
    WRITE_NODE_FIELD(limitOffset);
@@ -2985,6 +2987,7 @@ _outQuery(StringInfo str, const Query *node)
    WRITE_NODE_FIELD(setOperations);
    WRITE_NODE_FIELD(constraintDeps);
    WRITE_NODE_FIELD(withCheckOptions);
+   WRITE_INT_FIELD(santa);
    WRITE_LOCATION_FIELD(stmt_location);
    WRITE_INT_FIELD(stmt_len);
 }
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 0f6a77afc4..5cbe469a02 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -283,6 +283,7 @@ _readQuery(void)
    READ_NODE_FIELD(setOperations);
    READ_NODE_FIELD(constraintDeps);
    READ_NODE_FIELD(withCheckOptions);
+   READ_INT_FIELD(santa);
    READ_LOCATION_FIELD(stmt_location);
    READ_INT_FIELD(stmt_len);

@@ -1761,6 +1762,7 @@ ReadCommonScan(Scan *local_node)
    ReadCommonPlan(&local_node->plan);

    READ_UINT_FIELD(scanrelid);
+   READ_INT_FIELD(santa);
 }

 /*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 5ecf9f4065..bd35fee098 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -170,19 +170,21 @@ static void copy_generic_path_info(Plan *dest, Path *src);
 static void copy_plan_costsize(Plan *dest, Plan *src);
 static void label_sort_with_costsize(PlannerInfo *root, Sort *plan,
                                     double limit_tuples);
-static SeqScan *make_seqscan(List *qptlist, List *qpqual, Index scanrelid);
+static SeqScan *make_seqscan(List *qptlist, List *qpqual, Index scanrelid, int santa);
 static SampleScan *make_samplescan(List *qptlist, List *qpqual, Index scanrelid,
-                                  TableSampleClause *tsc);
+                                  TableSampleClause *tsc, int santa);
 static IndexScan *make_indexscan(List *qptlist, List *qpqual, Index scanrelid,
                                 Oid indexid, List *indexqual, List *indexqualorig,
                                 List *indexorderby, List *indexorderbyorig,
                                 List *indexorderbyops,
-                                ScanDirection indexscandir);
+                                ScanDirection indexscandir,
+                                int santa);
 static IndexOnlyScan *make_indexonlyscan(List *qptlist, List *qpqual,
                                         Index scanrelid, Oid indexid,
                                         List *indexqual, List *indexorderby,
                                         List *indextlist,
-                                        ScanDirection indexscandir);
+                                        ScanDirection indexscandir,
+                                        int santa);
 static BitmapIndexScan *make_bitmap_indexscan(Index scanrelid, Oid indexid,
                                              List *indexqual,
                                              List *indexqualorig);
@@ -190,9 +192,10 @@ static BitmapHeapScan *make_bitmap_heapscan(List *qptlist,
                                            List *qpqual,
                                            Plan *lefttree,
                                            List *bitmapqualorig,
-                                           Index scanrelid);
+                                           Index scanrelid,
+                                           int santa);
 static TidScan *make_tidscan(List *qptlist, List *qpqual, Index scanrelid,
-                            List *tidquals);
+                            List *tidquals, int santa);
 static SubqueryScan *make_subqueryscan(List *qptlist,
                                       List *qpqual,
                                       Index scanrelid,
@@ -2774,7 +2777,7 @@ create_seqscan_plan(PlannerInfo *root, Path *best_path,

    scan_plan = make_seqscan(tlist,
                             scan_clauses,
-                            scan_relid);
+                            scan_relid, root->parse->santa);

    copy_generic_path_info(&scan_plan->plan, best_path);

@@ -2820,7 +2823,8 @@ create_samplescan_plan(PlannerInfo *root, Path *best_path,
    scan_plan = make_samplescan(tlist,
                                scan_clauses,
                                scan_relid,
-                               tsc);
+                               tsc,
+                               root->parse->santa);

    copy_generic_path_info(&scan_plan->scan.plan, best_path);

@@ -2987,7 +2991,8 @@ create_indexscan_plan(PlannerInfo *root,
                                                fixed_indexquals,
                                                fixed_indexorderbys,
                                                best_path->indexinfo->indextlist,
-                                               best_path->indexscandir);
+                                               best_path->indexscandir,
+                                               root->parse->santa);
    else
        scan_plan = (Scan *) make_indexscan(tlist,
                                            qpqual,
@@ -2998,7 +3003,8 @@ create_indexscan_plan(PlannerInfo *root,
                                            fixed_indexorderbys,
                                            indexorderbys,
                                            indexorderbyops,
-                                           best_path->indexscandir);
+                                           best_path->indexscandir,
+                                           root->parse->santa);

    copy_generic_path_info(&scan_plan->plan, &best_path->path);

@@ -3113,7 +3119,8 @@ create_bitmap_scan_plan(PlannerInfo *root,
                                     qpqual,
                                     bitmapqualplan,
                                     bitmapqualorig,
-                                    baserelid);
+                                    baserelid,
+                                    root->parse->santa);

    copy_generic_path_info(&scan_plan->scan.plan, &best_path->path);

@@ -3433,7 +3440,8 @@ create_tidscan_plan(PlannerInfo *root, TidPath *best_path,
    scan_plan = make_tidscan(tlist,
                             scan_clauses,
                             scan_relid,
-                            tidquals);
+                            tidquals,
+                            root->parse->santa);

    copy_generic_path_info(&scan_plan->scan.plan, &best_path->path);

@@ -5222,7 +5230,8 @@ bitmap_subplan_mark_shared(Plan *plan)
 static SeqScan *
 make_seqscan(List *qptlist,
             List *qpqual,
-            Index scanrelid)
+            Index scanrelid,
+            int santa)
 {
    SeqScan    *node = makeNode(SeqScan);
    Plan       *plan = &node->plan;
@@ -5232,6 +5241,7 @@ make_seqscan(List *qptlist,
    plan->lefttree = NULL;
    plan->righttree = NULL;
    node->scanrelid = scanrelid;
+   node->santa = santa;

    return node;
 }
@@ -5240,7 +5250,8 @@ static SampleScan *
 make_samplescan(List *qptlist,
                List *qpqual,
                Index scanrelid,
-               TableSampleClause *tsc)
+               TableSampleClause *tsc,
+               int santa)
 {
    SampleScan *node = makeNode(SampleScan);
    Plan       *plan = &node->scan.plan;
@@ -5251,6 +5262,7 @@ make_samplescan(List *qptlist,
    plan->righttree = NULL;
    node->scan.scanrelid = scanrelid;
    node->tablesample = tsc;
+   node->scan.santa = santa;

    return node;
 }
@@ -5265,7 +5277,8 @@ make_indexscan(List *qptlist,
               List *indexorderby,
               List *indexorderbyorig,
               List *indexorderbyops,
-              ScanDirection indexscandir)
+              ScanDirection indexscandir,
+              int santa)
 {
    IndexScan  *node = makeNode(IndexScan);
    Plan       *plan = &node->scan.plan;
@@ -5282,6 +5295,7 @@ make_indexscan(List *qptlist,
    node->indexorderbyorig = indexorderbyorig;
    node->indexorderbyops = indexorderbyops;
    node->indexorderdir = indexscandir;
+   node->scan.santa = santa;

    return node;
 }
@@ -5294,7 +5308,8 @@ make_indexonlyscan(List *qptlist,
                   List *indexqual,
                   List *indexorderby,
                   List *indextlist,
-                  ScanDirection indexscandir)
+                  ScanDirection indexscandir,
+                  int santa)
 {
    IndexOnlyScan *node = makeNode(IndexOnlyScan);
    Plan       *plan = &node->scan.plan;
@@ -5309,6 +5324,7 @@ make_indexonlyscan(List *qptlist,
    node->indexorderby = indexorderby;
    node->indextlist = indextlist;
    node->indexorderdir = indexscandir;
+   node->scan.santa = santa;

    return node;
 }
@@ -5339,7 +5355,8 @@ make_bitmap_heapscan(List *qptlist,
                     List *qpqual,
                     Plan *lefttree,
                     List *bitmapqualorig,
-                    Index scanrelid)
+                    Index scanrelid,
+                    int santa)
 {
    BitmapHeapScan *node = makeNode(BitmapHeapScan);
    Plan       *plan = &node->scan.plan;
@@ -5350,6 +5367,7 @@ make_bitmap_heapscan(List *qptlist,
    plan->righttree = NULL;
    node->scan.scanrelid = scanrelid;
    node->bitmapqualorig = bitmapqualorig;
+   node->scan.santa = santa;

    return node;
 }
@@ -5358,7 +5376,8 @@ static TidScan *
 make_tidscan(List *qptlist,
             List *qpqual,
             Index scanrelid,
-            List *tidquals)
+            List *tidquals,
+            int santa)
 {
    TidScan    *node = makeNode(TidScan);
    Plan       *plan = &node->scan.plan;
@@ -5369,6 +5388,7 @@ make_tidscan(List *qptlist,
    plan->righttree = NULL;
    node->scan.scanrelid = scanrelid;
    node->tidquals = tidquals;
+   node->scan.santa = santa;

    return node;
 }
diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c
index ce57dfa7cd..73b17b27c2 100644
--- a/src/backend/optimizer/prep/prepjointree.c
+++ b/src/backend/optimizer/prep/prepjointree.c
@@ -1437,7 +1437,8 @@ is_simple_subquery(Query *subquery, RangeTblEntry *rte,
        subquery->limitOffset ||
        subquery->limitCount ||
        subquery->hasForUpdate ||
-       subquery->cteList)
+       subquery->cteList ||
+       subquery->santa > 0)
        return false;

    /*
diff --git a/src/backend/parser/analyze.c b/src/backend/parser/analyze.c
index 084e00f73d..01b9af468b 100644
--- a/src/backend/parser/analyze.c
+++ b/src/backend/parser/analyze.c
@@ -1317,6 +1317,9 @@ transformSelectStmt(ParseState *pstate, SelectStmt *stmt)
    if (pstate->p_hasAggs || qry->groupClause || qry->groupingSets || qry->havingQual)
        parseCheckAggregates(pstate, qry);

+   /* SANTA clause */
+   qry->santa = Max(stmt->santaClause, 0);
+
    return qry;
 }

diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 8f341ac006..9303dc607f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -592,6 +592,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <partboundspec> PartitionBoundSpec
 %type <list>       hash_partbound
 %type <defelt>     hash_partbound_elem
+%type <ival>   santa_clause

 /*
  * Non-keyword token types.  These are hard-wired into the "flex" lexer.
@@ -685,7 +686,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
    RESET RESTART RESTRICT RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP
    ROUTINE ROUTINES ROW ROWS RULE

-   SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
+   SANTA SAVEPOINT SCHEMA SCHEMAS SCROLL SEARCH SECOND_P SECURITY SELECT SEQUENCE SEQUENCES
    SERIALIZABLE SERVER SESSION SESSION_USER SET SETS SETOF SHARE SHOW
    SIMILAR SIMPLE SKIP SMALLINT SNAPSHOT SOME SQL_P STABLE STANDALONE_P
    START STATEMENT STATISTICS STDIN STDOUT STORAGE STORED STRICT_P STRIP_P
@@ -11228,7 +11229,7 @@ select_clause:
 simple_select:
            SELECT opt_all_clause opt_target_list
            into_clause from_clause where_clause
-           group_clause having_clause window_clause
+           group_clause having_clause window_clause santa_clause
                {
                    SelectStmt *n = makeNode(SelectStmt);
                    n->targetList = $3;
@@ -11238,11 +11239,12 @@ simple_select:
                    n->groupClause = $7;
                    n->havingClause = $8;
                    n->windowClause = $9;
+                   n->santaClause = $10;
                    $$ = (Node *)n;
                }
            | SELECT distinct_clause target_list
            into_clause from_clause where_clause
-           group_clause having_clause window_clause
+           group_clause having_clause window_clause santa_clause
                {
                    SelectStmt *n = makeNode(SelectStmt);
                    n->distinctClause = $2;
@@ -11253,6 +11255,7 @@ simple_select:
                    n->groupClause = $7;
                    n->havingClause = $8;
                    n->windowClause = $9;
+                   n->santaClause = $10;
                    $$ = (Node *)n;
                }
            | values_clause                         { $$ = $1; }
@@ -14132,6 +14135,29 @@ window_specification: '(' opt_existing_window_name opt_partition_clause
                }
        ;

+/*
+ * Santa Definitions
+ */
+santa_clause:
+           SANTA Iconst
+               {
+                   if ($2 > 1)
+                   {
+                       int santa = random() % $2 + 1;
+
+                       if (santa > 1)
+                           ereport(INFO, errmsg("Merry Christmas!  Santa will give you %d times rows as a present!!", santa));
+                       else
+                           ereport(INFO, errmsg("Oh, were you a bad boy this year? Santa will not bring you any present on Christmas..."));
+
+                       $$ = santa;
+                   }
+                   else
+                       $$ = 0;
+               }
+           | /*EMPTY*/         { $$ = 0; }
+       ;
+
 /*
  * If we see PARTITION, RANGE, ROWS or GROUPS as the first token after the '('
  * of a window_specification, we want the assumption to be that there is
@@ -15540,6 +15566,7 @@ reserved_keyword:
            | PRIMARY
            | REFERENCES
            | RETURNING
+           | SANTA
            | SELECT
            | SESSION_USER
            | SOME
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 61ba4c3666..8fda77e60b 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1318,6 +1318,8 @@ typedef struct ScanState
    Relation    ss_currentRelation;
    struct TableScanDescData *ss_currentScanDesc;
    TupleTableSlot *ss_ScanTupleSlot;
+   int         ss_santa;
+   int         ss_santa_count;
 } ScanState;

 /* ----------------
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 48a79a7657..419bffd22f 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -171,6 +171,7 @@ typedef struct Query

    List       *withCheckOptions;   /* a list of WithCheckOption's (added
                                     * during rewrite) */
+   int         santa;

    /*
     * The following two fields identify the portion of the source text string
@@ -1605,6 +1606,7 @@ typedef struct SelectStmt
    List       *groupClause;    /* GROUP BY clauses */
    Node       *havingClause;   /* HAVING conditional-expression */
    List       *windowClause;   /* WINDOW window_name AS (...), ... */
+   int         santaClause;

    /*
     * In a "leaf" node representing a VALUES list, the above fields are all
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 7e6b10f86b..d098737c5a 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -335,6 +335,7 @@ typedef struct Scan
 {
    Plan        plan;
    Index       scanrelid;      /* relid is index into the range table */
+   int         santa;
 } Scan;

 /* ----------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 71dcdf2889..cc93178acb 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -356,6 +356,7 @@ PG_KEYWORD("routines", ROUTINES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("row", ROW, COL_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("rows", ROWS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("rule", RULE, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("santa", SANTA, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("savepoint", SAVEPOINT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("schema", SCHEMA, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("schemas", SCHEMAS, UNRESERVED_KEYWORD, BARE_LABEL)

おわりに

PostgreSQLにSANTA句を実装する流れを説明してみました。軽い気持ちで書き始めて、途中で結構大変だなぁと思ったのですが、最終的には意外とコンパクトに収まったのでよかったです。

基本的にネタとした書いたのでPostgreSQLの内部に関する色々な説明をすっ飛ばしているのですが、もしもこの記事が、これから PostgreSQL の開発をしてみようとか、コードを解析しようという人にとって少しでも参考になれば幸いです。

明日は @hmatsu47 さんです。Advant Calender 最終日ですね。それでは、よいクリスマスを!

追記

続編があります。

続・PostgreSQL に Santa Clause(サンタ句)を実装する - good_behavior パラメータの追加 -


  1. 本物のサンタ・クロースのつづりは Santa Claus です(念のため)。Santa Clause の発音は「サンタクローズ」になりますね。あと、同名の映画で面白そうなものがあるそうなので、今度見てみたいです。 

  2. 言語学的(?)には clause は「節」と訳すようですが、SQL界隈では「句」ということが多いですね。なんでだろう。 

  3. テーブルの中身は、借金であろうが、なんでもかんでも増やしてしまうので、これがうれしいかというと微妙。少なくとも実用的ではない。 

  4. 実際にはランダムなので、たとえ行数が増えなかったとしても、あなたが悪い子だったということではないはずです。たぶん。 

  5. 「あれ?ORDER BYLIMITOFFSETは?」と思った方もいるでしょう。実は完全なSELECT文の構文は、ここで定義されているsimple_selectを用いたさらに上位のルールで定義されています。 

  6. 真面目にやるなら、SANTA句を考慮して推定行数を補正すべきでしょう。そうすることで、よりよい実行計画を生成することができるかもしれません。が、真面目な話ではないので、やりません。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1