読者です 読者をやめる 読者になる 読者になる

LudiaでPostgreSQLに全文検索を(2) EXPLAIN

Ludia

Ludia-0.8.0 では EXPLAIN が返すコストの見積もりが正しくない(常に 0.00..0.01 ?) ため、常に fulltext index が優先して使われるようだ。

普通の index (entry_pkey) が張ってある id カラムと、fulltext index (entry_ftidx) がある body カラムを持つテーブルに対して、

# EXPLAIN ANALYZE SELECT * FROM entry WHERE id=24354;
                       QUERY PLAN
-------------------------------------------------------------------------
 Index Scan using entry_pkey on entry  (cost=0.00..3.20 rows=1 width=730)
 (actual time=0.022..0.024 rows=1 loops=1)
   Index Cond: (id = 24354)
 Total runtime: 0.064 ms

# EXPLAIN ANALYZE SELECT * FROM entry WHERE id=24354 AND body @@ 'wiki';
                       QUERY PLAN
--------------------------------------------------------------------------
 Index Scan using entry_ftidx on entry  (cost=0.00..0.01 rows=1 width=730)
 (actual time=0.462..1.503 rows=1 loops=1)
   Index Cond: (body @@ 'wiki'::text)
   Filter: (id = 24354)
 Total runtime: 2.094 ms

entry_pkey で抽出する方が高速 (actual time=0.022..0.024) なのだが、entry_ftidx (actual time=0.462..1.503) が使われている。

ここで SET enable_indexscan TO 'off'; してみるとどうなるかというと……

# EXPLAIN ANALYZE SELECT * FROM entry WHERE id=24354 AND body @@ 'wiki';
                       QUERY PLAN
--------------------------------------------------------------
 Bitmap Heap Scan on entry  (cost=0.00..4.01 rows=1 width=730)
 (actual time=1.112..1.844 rows=1 loops=1)
   Recheck Cond: (body @@ 'wiki'::text)
   Filter: (id = 24354)
   ->  Bitmap Index Scan on entry_ftidx  (cost=0.00..0.00 rows=1 width=0)
         (actual time=0.424..0.424 rows=1016 loops=1)
         Index Cond: (body @@ 'wiki'::text)
 Total runtime: 2.476 ms

entry_ftidx を使って Bitmap Index Scan をするようになった。
Recheck Cond: (body @@ 'wiki'::text) てので得られた行が条件を満たすかどうか確認できているようなので、EXPLAIN のコスト見積もりが実情に即した値を返すようになれば

  • entry_pkey で Index Scan
  • Recheck Cond: で entry_ftidx にマッチしているかどうか確認

という流れで検索するようにオプティマイザが判断してくれる、かな?

PostgreSQL で、特定の index のみ使わないでくれ、って指示はできるんだろうか。

[追記]
pgsenna2.c の 630行目、

qual_arg_cost = 0.0;

qual_arg_cost = 10.0;

として無理矢理 cost を 10 にしたところ、実行計画が変わった。

# EXPLAIN ANALYZE SELECT * FROM entry WHERE body @@ '+plagger' AND id =13967;
                     QUERY PLAN
-------------------------------------------------------------------------
 Index Scan using entry_pkey on entry  (cost=0.00..3.20 rows=1 width=730)
 (actual time=0.108..0.108 rows=0 loops=1)
   Index Cond: (id = 13967)
   Filter: (body @@ '+plagger'::text)
 Total runtime: 0.151 ms

てことは cost 計算がそれなりにできれば解決か? と思ったのだが、今度は Filter: (body @@ '+plagger'::text) の部分が問題に。

ここで senna の検索式として Filter が動いてくれない。文字列の部分一致として評価されているようだ。
ソースを (よく分からないなりに) 眺めると、 pgsenna2.sql で @@ が定義してあって

CREATE OPERATOR @@ (
        LEFTARG = text,
        RIGHTARG = text,
        PROCEDURE = pgs2contain,
        COMMUTATOR = '@@',
        RESTRICT = contsel,
        JOIN = contjoinsel
);

pgs2contain は文字列の一致を見るだけの関数になっているからか。

Datum 
pgs2contain(PG_FUNCTION_ARGS)
{
  text *t = PG_GETARG_TEXT_P(0);
  text *q = PG_GETARG_TEXT_P(1);
  char *ct = text2cstr(t);
  char *cq = text2cstr(q);
  bool res = strstr(ct, cq) ? true : false;
  pfree(ct);
  pfree(cq);
  PG_RETURN_BOOL(res);
}

惜しい。
例えば Senna で1000件ヒットし、PostgreSQL の index で 10件ヒットするとしたら……

  • Senna index で検索 -> Filter、だと 1000件分のデータが PostgreSQL のバッファに読み込まれ、それから条件に合うものが 10件抽出される
  • PostgreSQL index で検索 -> Senna index を検索し tid があるかどうかチェック、ならばバッファに読まれるのは 10件のみ

となるはずなので、(結局 Senna の検索はするにしても) 効率的には後者の方がいいやね。