redis_fdwを使ってみた
Redisを入れてみた
先日、ちょっとRadisについて調べる機会があったので、手元の環境にRedis 2.6.14を入れてみた。
で、クライアントAPIとしてC言語用のライブラリ(hiredis)も入れて、C言語アプリケーションから簡単な性能検証をやってた。
radis_fdwも使ってみた
で、PostgreSQLのFDWのとして、以前からRedis用のFDW(redis_fdw: Redis FDW for PostgreSQL 9.1+ / PostgreSQL Extension Network)というのがあるのは知っていたが、今までRedisを使うこともなかったので全然使っていなかった。
せっかくの機会なので使って見ることにする。
幸い、依存ライブラリは既にインストール済みのhiredisだけだったので、さっさとredis_fdwをビルドしてみる・・・
ビルド
最初は9.3-beta2上でビルドしようとしたが、ビルドにあっさり失敗する。9.2でも同様。
どうやら9.2以降に追随していないっぽい・・・。
9.2以降に追随するためにはFDWのインタフェース変更に対応するだけでいいとは思うが、それはとりあえず後回しにする。まずはどういう動きになるのかを見ておきたいので、9.1環境を復旧させてredis_fdwをビルドする。今度はさくっと成功。
登録から外部テーブル作成まで
で、データベースを作成してFDWの登録と外部サーバ・外部テーブルを作成してみる。
$ psql -e redis -f fdw_test.sql Welcome to nuko! CREATE EXTENSION redis_fdw ; CREATE EXTENSION Objects in extension "redis_fdw" Object Description ------------------------------------------ foreign-data wrapper redis_fdw function redis_fdw_handler() function redis_fdw_validator(text[],oid) (3 rows) CREATE SERVER redis_server FOREIGN DATA WRAPPER redis_fdw OPTIONS (address '127.0.0.1', port '6379'); CREATE SERVER CREATE FOREIGN TABLE test1 (key text, value text) SERVER redis_server OPTIONS (database '0'); CREATE FOREIGN TABLE CREATE USER MAPPING FOR PUBLIC SERVER redis_server; CREATE USER MAPPING
- CREATE SERVERのオプションにはRedisサーバのアドレスとポートを指定する。今回はローカルマシン・デフォルトポートでRedisサーバを起動している。
- CREATE FOREIGN TABLEの列の定義にはある程度の制約がある。keyという名前のTEXT型と、もう一つ別のTEXT型を指定しなくてはならない。
- Redisにも簡単な認証機構はあるが、今回はそれは特に設定しないので、CREATE USER MAPPINGにはOPTIONは設定しない。
- Redis側にパスワードを設定している場合には、OPTIONに"password"を指定する必要がある(多分)。
外部テーブルへの検索
作成した外部テーブルへ検索を行なってみる。
外部テーブルの接続先であるRedisサーバには以下のような10000件のデータを格納してある。
$ redis-cli redis 127.0.0.1:6379> GET key-5000 "value-5000" redis 127.0.0.1:6379>
- キー"key-xxxx"に対して値(value-xxxx)を格納している。
- xxxxは0000〜9999の十進数字
これに対して外部テーブル経由でSELECTしてみる。
redis=# \d test1 Foreign table "public.test1" Column | Type | Modifiers --------+------+----------- key | text | value | text | Server: redis_server redis=# SELECT * FROM test1 WHERE key = 'key-5000'; key | value ----------+------------ key-5000 | value-5000 (1 row)
無事に検索できた。
プラン確認・btreeとの比較
EXPLAIN ANALYZEでプランを確認する。
redis=# EXPLAIN ANALYZE SELECT * FROM test1 WHERE key = 'key-5000'; QUERY PLAN ------------------------------------------------------------------------------------------------------------- Foreign Scan on test1 (cost=10.00..10010.00 rows=10000 width=64) (actual time=0.102..0.104 rows=1 loops=1) Filter: (key = 'key-5000'::text) Foreign Redis Database Size: 10000 Total runtime: 1.540 ms (4 rows)
actual timeに注目。Redisに対するScan自体は0.1msくらいだが、Total Runtimeは1.5ms程度かかっている。この差異がFDWというフレームワークのオーバヘッドなのかもしれない。
pushdownのルール
Redis FDWではシンプルなルールで条件pushdownを行なっている。
- 単一の条件であること。
- 条件カラムの名前が"key"であること。
- TEXT型の"="比較演算であること。
- 条件値は定数値として評価されていること。
このあたりのルールは
static void
redisGetQual(Node *node, TupleDesc tupdesc, char **key, char **value, bool *pushdown)
という関数に記述されている。
なお、pushdownされない場合には、名前空間(データベース)内の全てのキーと値をフルスキャンして、PostgreSQL側で評価してしまうので非常に遅い。
実質上、pushddownできない指定で使ってはダメなんだと思う。
以下、pushdownされない場合の例。Total RuntimeとForeign Scanのactual timeに注目。
2つ以上の条件演算を指定したとき。
OR演算のみ(あるいはIN述語)対応だけでも出来ていると嬉しいんだけどねえ・・・。
redis=# EXPLAIN ANALYZE SELECT * FROM test1 WHERE key = 'key-4000' OR key = 'key-5000'; QUERY PLAN ----------------------------------------------------------------------------------------------------------------- Foreign Scan on test1 (cost=10.00..10010.00 rows=10000 width=64) (actual time=236.934..775.785 rows=2 loops=1) Filter: ((key = 'key-4000'::text) OR (key = 'key-5000'::text)) Foreign Redis Database Size: 10000 Total runtime: 785.781 ms
条件カラムの名前がkeyでないと、pushdownされない。
redis=# EXPLAIN ANALYZE SELECT * FROM test1 WHERE value = 'value-5000'; QUERY PLAN ----------------------------------------------------------------------------------------------------------------- Foreign Scan on test1 (cost=10.00..10010.00 rows=10000 width=64) (actual time=234.118..767.727 rows=1 loops=1) Filter: (value = 'value-5000'::text) Foreign Redis Database Size: 10000 Total runtime: 778.702 ms
演算子が"="でないと、pushdownされない。
redis=# EXPLAIN ANALYZE SELECT * FROM test1 WHERE key >= 'key-9999'; QUERY PLAN ----------------------------------------------------------------------------------------------------------------- Foreign Scan on test1 (cost=10.00..10010.00 rows=10000 width=64) (actual time=510.695..784.040 rows=1 loops=1) Filter: (key >= 'key-9999'::text) Foreign Redis Database Size: 10000 Total runtime: 794.920 ms
条件値に関数が含まれているとpushdownされない。
redis=# EXPLAIN ANALYZE SELECT * FROM test1 WHERE key = concat('key', '-5000'); QUERY PLAN ----------------------------------------------------------------------------------------------------------------- Foreign Scan on test1 (cost=10.00..10010.00 rows=10000 width=64) (actual time=244.817..803.572 rows=1 loops=1) Filter: (key = pg_catalog.concat('key', '-5000')) Foreign Redis Database Size: 10000 Total runtime: 814.650 ms
ちなみに、"||"演算子による定数同士の連結なら大丈夫。そりゃそうだ。
redis=# EXPLAIN ANALYZE SELECT * FROM test1 WHERE key = 'key' || '-5000'; QUERY PLAN ------------------------------------------------------------------------------------------------------------- Foreign Scan on test1 (cost=10.00..10010.00 rows=10000 width=64) (actual time=0.102..0.104 rows=1 loops=1) Filter: (key = 'key-5000'::text) Foreign Redis Database Size: 10000 Total runtime: 1.701 ms
btreeとの比較
なお、Redisに投入したキーと値と同じものを、PostgreSQLの通常のTEXT型のカラムに挿入し、keyをbtreeインデクスで作成して同じようにプランを見てみる。
redis=# CREATE TABLE test2 (key text, value text); CREATE TABLE redis=# COPY test2 FROM '/tmp/redis-data.txt'; COPY 10000 redis=# CREATE INDEX test2_idx ON test2 USING btree (key); CREATE INDEX redis=# EXPLAIN ANALYZE SELECT * FROM test2 WHERE key = 'key-5000'; QUERY PLAN ------------------------------------------------------------------------------------------------------------------ Index Scan using test2_idx on test2 (cost=0.00..8.27 rows=1 width=18) (actual time=0.092..0.095 rows=1 loops=1) Index Cond: (key = 'key-5000'::text) Total runtime: 0.128 ms (3 rows)
Total RuntimeはもちろんFDWよりも短いが、IndexScanのactual timeに関しては、こうやってみるとRedis FDWも遜色ないのかも。
今回はキーも値も短いものを使ったが、もしかすると長めの文字列をキーにする場合には、Scanのactual timeはRedis FDWが上回るケースもあるかもしれない。それがFDWフレームワーク自体のオーバヘッドを上回るのであればRedis FDWを導入したほうが性能上も得なケースがあるかも・・・。
Redis FDWの拡張
なかなか使いどころによっては役に立ちそうなFDWはなんだけど、9.2以降に対応していないのはもったいない。DLしたソースを元に、9.2対応に書き換えてみよう。
あと、実は9.3以降のwritable-fdwにも簡単に対応できないかなと。
- INSERTならEXISTでキー存在を確認
- 存在していなければSETでキーと値を追加
- 存在していればキー重複エラー
- UPDATEならEXISTでキー存在を確認
- 存在していなければ何もしない。
- 存在していればSETでキーと値を上書き
- DELETEの場合は、単純にDELコマンドを発行。
- キーが存在しないときにはDELは何もしないから。