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型を指定しなくてはならない。
    • OPTIONにはRedisの名前空間(他のDBMSでいうところのデータベース)を示す識別子を指定する。今回はデフォルト名前空間の0に登録されているものを外部テーブルで参照する。
  • 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は何もしないから。