9.4新機能?覗き見(ニッチなFDW編)

このエントリはPostgreSQL Advent Calendar 2013の12/12分です。

相変わらずのニッチなFDWネタです。みんなに便利な機能は誰かまじめな人が書いてくれるはず。

前口上

9.2でPostgreSQL対応、9.3で書き込み可能になった外部データラッパ(FDW)ですが、現在開発中の9.4でも機能追加が試みられています。というわけで、まだコミットされていない機能ですが紹介してみたいと思います。「いいね!」と思ったら、開発コミュニティに「欲しがっている人いるで〜」とプッシュいたしますので是非コメントをください。

Custom Scan Provider

「Custom Scan Provider」という名前からはどんな機能か分かりづらいですが、ひとことでいうと「独自のスキャン処理を定義」できるようになります。スキャン処理と言えば全ページなめる「Seq Scan」やインデックスを使う「Index Scan」がありますが、それと同レベルの独自処理をプランナーに追加の選択肢として提供することができるというものです。プランナーは標準的なスキャンと「Custom Scan」をコスト値で比較し最も速そうなものを選択します。

なお、この機能はSE-PostgreSQLやWritable-FDWを開発された海外さんが提案されています。最新版パッチでは、Proof-of-Concept(PoC)として、CTIDスキャンの高速版がついてきます。これは、

SELECT * FROM table WHERE ctid > '(100,10)'::tid;  -- 100ページ目の10個目より後のデータを検索

といったCTIDシステム列を条件にした検索で、不要なブロックを読み飛ばすというものです。上記の例では、通常のPostgreSQLでは先頭ページから地道に読んでいくところを、99ページスキップしていきなり100ページ目から読み始めます。これはPoCなので実用性がどこまであるかは?なところもありますが、リファレンス実装としてはなかなかのものだと思います。

え?これが何でFDWに絡むの?とお思いでしょう。私も思いました。外部データのスキャンだけならもうFDWでできていますからね。このパッチのすごいところは、結合処理もカスタマイズできるところです。「Custom Scan」ならぬ「Custom Join」ですね。この機能はもちろん普通のテーブルにも使えるんですが、外部テーブルで使うと、なんと外部テーブル同士の結合をリモートサーバ側で実行できるようになります。
(以前私も外部テーブル同士の結合サポートを提案したんですが、見事にrejectされました)

このパッチに含まれる改造版postgres_fdwで定義した、同じサーバ上の外部テーブル同士を結合すると…

9.3のpostgres_fdw
postgres=# EXPLAIN VERBOSE SELECT count(*) FROM pgbench1_branches b JOIN pgbench1_accounts a ON a.bid = b.bid WHERE aid < 100;
                                             QUERY PLAN                                             
----------------------------------------------------------------------------------------------------
 Aggregate  (cost=6454.00..6454.01 rows=1 width=0)
   Output: count(*)
   ->  Hash Join  (cost=201.21..6453.81 rows=77 width=0)
         Hash Cond: (a.bid = b.bid)
         ->  Foreign Scan on public.pgbench1_accounts a  (cost=100.00..6351.54 rows=77 width=4)
               Output: a.aid, a.bid, a.abalance, a.filler
               Remote SQL: SELECT bid FROM public.pgbench_accounts WHERE ((aid < 100))
         ->  Hash  (cost=101.15..101.15 rows=5 width=4)
               Output: b.bid
               ->  Foreign Scan on public.pgbench1_branches b  (cost=100.00..101.15 rows=5 width=4)
                     Output: b.bid
                     Remote SQL: SELECT bid FROM public.pgbench_branches
(12 rows)
Custom Scan Provider対応のpostgres_fdw
postgres=# EXPLAIN VERBOSE SELECT count(*) FROM pgbench1_branches b JOIN pgbench1_accounts a ON a.bid = b.bid WHERE aid < 100;
                                                                   QUERY PLAN                                                                    
-------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=101.60..101.61 rows=1 width=0)
   Output: count(*)
   ->  Custom Scan (postgres-fdw)  (cost=100.00..101.43 rows=71 width=0)
         Remote SQL: SELECT NULL FROM (public.pgbench_branches r1 JOIN public.pgbench_accounts r2 ON ((r1.bid = r2.bid))) WHERE ((r2.aid < 100))
(4 rows)

といったように、二つのテーブルを結合するSQLを生成し、その実行結果をスキャン結果として使えるのです!もちろん、別のサーバ上にある外部テーブルやローカルのテーブルとはローカルで結合してくれます。この「Join push-down」という手法によってサーバ間のデータ転送量やローカルでの結合処理に必要なCPU演算量などが減るので、処理の大幅な高速化が期待できます。

仕組みに興味がある方は、開発者Wikiに海外さんの整理した解説(英語)があるのでぜひ。

外部テーブルの継承サポート

まだ正式なパッチは出ていませんが、外部テーブルを子テーブルとして使えるようにする機能も提案されています。これが実現すると、パーティションテーブルとして外部テーブルが使えるようになるので、シャーディングが実現でき処理のオフロードも期待できます。ちなみに、これを使うとこんなSQLが実行できるようになります。

postgres=# \d
                    List of relations
 Schema |         Name         |     Type      |  Owner   
--------+----------------------+---------------+----------
 public | pgbench1_accounts    | table         | postgres
 public | pgbench1_accounts_c1 | foreign table | postgres
 public | pgbench1_accounts_c2 | foreign table | postgres
 public | pgbench1_accounts_c3 | foreign table | postgres
 public | pgbench1_accounts_c4 | foreign table | postgres
 public | pgbench1_accounts_c5 | foreign table | postgres
(6 rows)

postgres=# \d+ pgbench1_accounts
                       Table "public.pgbench1_accounts"
  Column  |     Type      | Modifiers | Storage  | Stats target | Description 
----------+---------------+-----------+----------+--------------+-------------
 aid      | integer       |           | plain    |              | 
 bid      | integer       |           | plain    |              | 
 abalance | integer       |           | plain    |              | 
 filler   | character(84) |           | extended |              | 
Child tables: pgbench1_accounts_c1,
              pgbench1_accounts_c2,
              pgbench1_accounts_c3,
              pgbench1_accounts_c4,
              pgbench1_accounts_c5
Has OIDs: no

postgres=# \d pgbench1_accounts_c1
    Foreign table "public.pgbench1_accounts_c1"
  Column  |     Type      | Modifiers | FDW Options 
----------+---------------+-----------+-------------
 aid      | integer       |           | 
 bid      | integer       |           | 
 abalance | integer       |           | 
 filler   | character(84) |           | 
Check constraints:
    "accounts_c1_bid_check" CHECK (bid = 1)
Server: pgbench1
FDW Options: (table_name 'pgbench_accounts_c1')
Inherits: pgbench1_accounts

このように外部テーブルを子テーブルとしてbid別に定義して、パーティションキー(ここではbid列)にチェック制約を定義しておくと…

postgres=# postgres=# EXPLAIN (COSTS false) SELECT * FROM pgbench1_accounts;
                 QUERY PLAN                 
--------------------------------------------
 Append
   ->  Seq Scan on pgbench1_accounts
   ->  Foreign Scan on pgbench1_accounts_c1
   ->  Foreign Scan on pgbench1_accounts_c2
   ->  Foreign Scan on pgbench1_accounts_c3
   ->  Foreign Scan on pgbench1_accounts_c4
   ->  Foreign Scan on pgbench1_accounts_c5
(7 rows)

postgres=# EXPLAIN (COSTS false) SELECT * FROM pgbench1_accounts WHERE bid = 1;
                 QUERY PLAN                 
--------------------------------------------
 Append
   ->  Seq Scan on pgbench1_accounts
         Filter: (bid = 1)
   ->  Foreign Scan on pgbench1_accounts_c1
(4 rows)

postgres=# EXPLAIN (COSTS false) SELECT * FROM pgbench1_accounts WHERE bid < 3;
                 QUERY PLAN                 
--------------------------------------------
 Append
   ->  Seq Scan on pgbench1_accounts
         Filter: (bid < 3)
   ->  Foreign Scan on pgbench1_accounts_c1
   ->  Foreign Scan on pgbench1_accounts_c2
(5 rows)

Constraint Exclusionが効いて、必要な外部テーブルにだけ検索が実行されます。もし外部テーブルアクセスを非同期にできたら、なんちゃってパラレルクエリにできるのでは?なんて夢も広がります。

今のPostgreSQLではConstraint Exclusionの仕組みにチェック制約(検査制約)が必要なのですが、外部テーブルに検査制約をつけても完全な強制ができるわけではないので、通常のチェック制約と区別できる文法が必要なのでは?といった議論が進んでいます。

さいごに

このように、9.4でも外部テーブルに関する機能追加は地道に行われています(まだコミットはされてませんが)。もし興味を持った方がいたら、開発に参加してみませんか?(無理矢理