PostgreSQL Unconference
(2015-12-12)
postgres_fdw を
久々に使ってみた
ぬこ@横浜 (@nuko_yokohama)
自己紹介
ぬこ@横浜です
面倒なのでググッてください
今日のお題
postgres_fdw と
いろんな機能の組み合わせ
と、その前に
postgres_fdw の
簡単な説明を。
postgres_fdw
http://www.postgresql.jp/document/9.4/html/postgres-fdw.html
主開発者の花田=サンと
PostgreSQL コミュニティの
タツジンの見事な
ワザマエが光る一品。
実際スゴイ!
postgres_fdw
外部の PostgreSQL サーバに格納されたデータを
自分の DB 内のテーブルであるかのように
アクセスするために使用する、外部データラッパ
類似モジュールとして dblink があるが、
postgres_fdw のほうが、より自然なクエリが書ける。
参照だけでなく、更新 (insert/update/delete) も可。
PostgreSQL 9.3 からサポートされている。
と、古事記 PostgreSQL 文書にも書かれている。
postgres_fdw を使って
別データベース上の表と
結合ヤッター!
で、全文検索
ぬこは激怒した。
かの邪智暴虐の
AWS RDS PostgreSQL で
日本語全文検索を
せねばならぬと決意した。
AWS RDS PostgreSQL は
DB 管理の手間が省けて便利な
サービスだけど、
使える拡張機能が限定&独自の
拡張の追加ができない。
AWS RDS PostgreSQL
pg_trgm は入っている
pg_bigm は入ってない
textsearch_ja も入ってない
AWS RDS PostgreSQL 上で
日本語全文検索が
できねーじゃねーか
( pg_trgm は実質的に
日本語は使えないので)
ということで
postgres_fdw の出番ですよ
リモートサーバ側
リモートサーバに
pg_bigm をインストール
検索対象とするテキスト列に
pg_bigm が提供する
全文検索インデックスを設定
ローカルサーバ側
postgres_fdw をインストール
FOREIGN SERVER を定義
FOREIGN TABLE を定義
FOREIGN TABLE に対して
全文検索クエリを実行
リモートサーバの設定
test=# d sangokushi
テーブル "public.sangokushi"
列 | 型 | 修飾語
------+---------+---------------------------------------------------------
id | integer | not null default nextval('sangokushi_id_seq'::regclass)
data | text |
インデックス :
"meros_gin" gin (data gin_bigm_ops)
sangokushi テーブルの data 列に対して
pg_bigm による N-gram インデックス
を設定している。
ローカルサーバの設定
test=# des sv2_test
外部サーバー一覧
名前 | 所有者 | 外部データラッパー
----------+--------+--------------------
sv2_test | nuko | postgres_fdw
(1 行 )
test=# d sangokushi
外部テーブル "public.sangokushi"
列 | 型 | 修飾語 | FDW オプション
------+---------+--------+---------------
id | integer | |
data | text | |
Server: sv2_test
外部サーバ (sv2_test) 上に
外部表 sangokushi を定義する
検索してみる
test=# SELECT data FROM sangokushi WHERE data LIKE '% 兀突骨 %' LIMIT 3;
「ここから東南《たつみ》の方、七百里に、一つの国がある。烏戈国《うかこく》とい
て、国王は兀突骨《ごつとつこつ》という者です。五穀を食《は》まず、火食せず、猛獣
|蛇魚《だぎょ》を喰い、身には鱗《うろこ》が生えているとか聞きます。また、彼の手
下には、藤甲軍《とうこうぐん》と呼ぶ兵が約三万はおりましょう」
「なるほど、それでは無敵だろう。ひとつ兀突骨《ごつとつこつ》に会ってこの急場を
んでみよう」
 議にも及ばず、兀突骨は「よろしい」と大きくうなずいた。即座に三万の部下は藤甲
着こんで、洞市《どうし》に集まった。
検索はできたけど、インデックスは
きちんと使っているのか?
EXPLAIN 結果
test=# EXPLAIN ANALYZE VERBOSE SELECT data FROM sangokushi WHERE data LIKE '%
兀突骨 %' LIMIT 3;
Limit (cost=100.00..128.29 rows=1 width=32) (actual time=0.516..0.518 rows=3
loops=1)
Output: data
-> Foreign Scan on public.sangokushi (cost=100.00..128.29 rows=1
width=32) (actual time=0.515..0.515 rows=3 loops=1)
Output: data
Remote SQL: SELECT data FROM public.sangokushi WHERE ((data ~~ '% 兀突
骨 %'::text))
Planning time: 0.091 ms
Execution time: 0.821 ms
リモート側に条件は渡されている
リモート側の auto_explain 結果
LOG: duration: 0.040 ms plan:
Query Text: DECLARE c1 CURSOR FOR
SELECT data FROM public.sangokushi WHERE ((data ~~ '% 兀突骨 %'::text))
Bitmap Heap Scan on sangokushi (cost=14.03..21.81 rows=4 width=148)
Recheck Cond: (data ~~ '% 兀突骨 %'::text)
-> Bitmap Index Scan on meros_gin (cost=0.00..14.03 rows=4 width=0)
Index Cond: (data ~~ '% 兀突骨 %'::text)
リモート側で pg_bigm の
全文検索インデックスを使った
クエリが実行されている。
textsearch_ja も同様に
リモート側で textsearch_ja を入れて
gin インデックス設定
ローカル側で FDW を設定。
EXPLAIN 結果
test=# EXPLAIN ANALYZE VERBOSE SELECT * FROM sangokushi_tsj WHERE
to_tsvector('japanese', data) @@ to_tsquery('japanese', ' 孔明 ') LIMIT 3;
QUERY PLAN
------------------------------------------------------------------------------
-------------------------------------------------
-----------
Limit (cost=100.00..257.91 rows=3 width=36) (actual time=0.747..0.748 rows=3
loops=1)
Output: id, data
-> Foreign Scan on public.sangokushi_tsj (cost=100.00..468.45 rows=7
width=36) (actual time=0.746..0.746 rows=3 loops=1)
Output: id, data
Remote SQL: SELECT id, data FROM public.sangokushi_tsj WHERE
((to_tsvector('japanese'::regconfig, data) @@ ''' 孔明 ''':
:tsquery))
Planning time: 0.099 ms
Execution time: 1.152 ms
(7 行 )
リモート側に条件は渡されている
リモート側の auto_explain 結果
LOG: duration: 0.343 ms plan:
Query Text: DECLARE c1 CURSOR FOR
SELECT id, data FROM public.sangokushi_tsj WHERE
((to_tsvector('japanese'::regconfig, data) @@ ''' 孔明 '''::tsquery))
Bitmap Heap Scan on sangokushi_tsj (cost=7.62..352.41 rows=209 width=151)
Recheck Cond: (to_tsvector('japanese'::regconfig, data) @@ ''' 孔
明 '''::tsquery)
-> Bitmap Index Scan on idx (cost=0.00..7.57 rows=209 width=0)
Index Cond: (to_tsvector('japanese'::regconfig, data) @@ ''' 孔
明 '''::tsquery)
リモート側で textsearch_ja の
gin インデックスを使った
クエリが実行されている。
つまり
ぬこ「めうちゃんは日本語全文検索がしたい」
めう「日本語全文検索したいめう」
ぬこ「でも RDS には pg_bigm がない」
めう「 RDS だと日本語全文検索できないめう・・・」
ぬこ「だから EC2 に pg_bigm を入れる」
めう「いれるめう」
ぬこ「 RDS 側から FDW 経由で EC2 に接続する」
めう「接続するめう」
ぬこ「すると RDS から日本語全文検索できる」
めう「やっためう!すごいめう!」
RDS 上で pg_bigm と textsearch_ja
が使えるのでは!
まあ RDS PostgreSQL が
pg_bigm や textserach_ja を
サポートしてくれれば
こんなことしなくても
いいんですけどねw
で、 JSONB
例えば、外部テーブルの
ソースとなるサーバに以下のような
JSONB カラムを持つ
test テーブルがあるとする。
bar=# d test
Table "public.test"
Column | Type | Modifiers
--------+-------+-----------
data | jsonb |
Indexes:
"test_id_idx" btree ((data ->> 'id'::text))
"test_idx" gin (data)
ローカルな問い合わせなら
btree 式インデックスも
gin インデックスも使える。
bar=# EXPLAIN SELECT * FROM test WHERE data->>'id' = '10000';
QUERY PLAN
-------------------------------------------------------------------------
Index Scan using test_id_idx on test (cost=0.42..8.44 rows=1 width=70)
Index Cond: ((data ->> 'id'::text) = '10000'::text)
(2 rows)
bar=# EXPLAIN SELECT * FROM test WHERE data @> '{"id":10000}';
QUERY PLAN
--------------------------------------------------------------------------
Bitmap Heap Scan on test (cost=28.77..336.47 rows=100 width=70)
Recheck Cond: (data @> '{"id": 10000}'::jsonb)
-> Bitmap Index Scan on test_idx (cost=0.00..28.75 rows=100 width=0)
Index Cond: (data @> '{"id": 10000}'::jsonb)
(4 rows)
あからさまにインデックス検索なのだ!
しかし
外部テーブル
経由だと
@> 演算子 { キー : 値 } の場合
WHERE 句を pushdown して、
リモート側でも GIN インデックスを使用
foo=# EXPLAIN ANALYZE SELECT * FROM test WHERE data @> '{"id":10000}';
QUERY PLAN
--------------------------------------------------------------
Foreign Scan on test (cost=100.00..128.29 rows=1 width=32)
(actual time=0.579..0.579 rows=1 loops=1)
Planning time: 0.052 ms
Execution time: 0.867 ms
(3 rows)
Time: 1.204 ms
外部テーブルから返却された時点で
1件になっていることに注目重点!
実際速い!
->>' キー ' 演算子 値 の場合
WHERE 句は pushdown されず、
リモート側でフルスキャン全件取得!
アイエエエ!フルスキャン!
フルスキャンナンデ!
foo=# EXPLAIN ANALYZE SELECT * FROM test WHERE data->>'id' = '10000';
QUERY PLAN
----------------------------------------------------------------------
Foreign Scan on test (cost=100.00..159.93 rows=7 width=32)
(actual time=31.028..273.480 rows=1 loops=1)
Filter: ((data ->> 'id'::text) = '10000'::text)
Rows Removed by Filter: 99999
Planning time: 0.046 ms
Execution time: 273.581 ms
(5 rows)
そして外部テーブルからは全件 (10 万件 ) 返却されている!
あからさまにフルスキャンなのだ!ヤンナルネ・・・
実際遅い!
postgres_fdw 外部テーブルに渡す
WHERE 句の書き方によって
Pushdwon されたり
されなかったりする。
おかしいと思いませんか?あなた
古事記 PostgreSQL 文書には
こう書かれている
F.31.4. リモート問合せの最適化
外部サーバからのデータ転送量を削減するため、
postgres_fdw はリモート問合せを最適化しようと試みます。
これは問い合わせの WHERE 句をリモートサーバに送出する事、
およびクエリで必要とされていないカラムを取得しない事により行われます。
問い合わせの誤作動のリスクを下げるため、
ビルトインのデータ型、演算子、関数だけを用いたものでない限り、
リモートサーバに WHERE 句は送出されません。
また、 WHERE 句で使われる演算子と関数は IMMUTABLE でなければなりません。
カラム ->>' キー名 ' の結果に対する
比較演算だとダメっぽい?
要するに、特定の条件式が
Pushdown されない問題は
「バグではない。いいね?」
「アッハイ」
JSON 型 /XML 型のように
関数インデックスを必要とする型と
postgres_fdw は相性は良くない?
で、この問題(制約)って
9.6 では解消されてるのかな?
(本当は 9.6-dev でも試したかったけど時間切れ・・・)
おまけ
postgres_fdw の細かい制約
その 1
COPY
グワーッ!
test=# COPY sangokushi FROM '/tmp/sangoku.txt' CSV;
ERROR: cannot copy to foreign table "sangokushi"
test=#
外部テーブルに
直接 COPY はできない。
表
リモートサーバ
外部表
ローカルサーバ
COPY データ COPY
COPY の問題は以下の
ジツを使って回避可能
1. リモートテーブルに直接 COPY
2. INSERT 文を使用
3. file_fdw+ バルク INSERT
(3 . の方法の詳細は次ページで説明)
備えよう。
file_fdw+ バルク INSERT
こんなアトモスフィアなシェル作成!
#!/bin/sh
DBNAME=$1
FOREIGN_TABLE_ARG=$2
LOAD_FILE_NAME=$3
LOAD_TABLE_NAME=$4
CREATE_SERVER_SQL="CREATE SERVER __cp_server FOREIGN DATA WRAPPER file_fdw"
psql ${DBNAME} -c "${CREATE_SERVER_SQL}"
CREATE_FOREIGN_TABLE_SQL="CREATE FOREIGN TABLE __cp_table ( ${FOREIGN_TABLE_ARG} ) SERVER __cp_server OPTIONS (filename '${LOAD_FILE_NAME}')"
psql ${DBNAME} -c "${CREATE_FOREIGN_TABLE_SQL}"
INSERT_SQL="INSERT INTO ${LOAD_TABLE_NAME} (SELECT data FROM __cp_table )"
psql ${DBNAME} -c "${INSERT_SQL}"
DROP_FOREIGN_TABLE_SQL="DROP FOREIGN TABLE __cp_table"
psql ${DBNAME} -c "${DROP_FOREIGN_TABLE_SQL}"
DROP_SERVER_SQL="DROP SERVER __cp_server"
psql ${DBNAME} -c "${DROP_SERVER_SQL}"
contrib/file_fdw の外部サーバ / 外部表を定義
外部表を SELECT で全件検索した結果を
INSERT 文に流し込む。
最後に外部表と外部サーバを爆発四散!
file_fdw+ バルク INSERT
前ページのシェルを実行!
$ ./fdw_cp foo "data jsonb" "/tmp/json.txt" "test"
CREATE SERVER
CREATE FOREIGN TABLE
INSERT 0 100000
DROP FOREIGN TABLE
DROP SERVER
$
ゴウランガ!
COPY で使うファイルをそのまま使って
外部表へロード可能!
(COPY より遅いのは仕方がない。いいね?)
このシェルは postgres_fdw 専用というわけでなく
挿入操作が可能な FDW なら使用可能だと思う
その 2
TRUNCATE
アバーッ!
test=# TRUNCATE sangokushi;
ERROR: "sangokushi" is not a table
test=#
外部テーブルに対する
TRUNCATE はどの FDW でも不可っぽい
なのでリモートの実表に対して
TRUNCATE が必要!
あるいは DELETE とかしなさい
ということで
postgres_fdw は便利ですが
使用時には注意重点な。
という話でした。

Pgunconf 20121212-postgeres fdw