web-dev-qa-db-ja.com

アクセントなしで右端のみのワイルドカードを使用したILIKEの使用

私はPostgresql 9.4を使用していて、fooという名前の大きなテーブルがあります。それを検索したいのですが、検索テキストが非常に短い場合(「v」など)または長い場合(たとえば、「これはテーブルfoo%にginを使用した検索の例です」)の実行時間が長くなります。この場合、私のインデックスは無視されます。ここに私の検索クエリ:

_EXPLAIN (ANALYZE, TIMING)
SELECT  "foo".* FROM "foo" WHERE "foo"."locale" = 'de'
AND f_unaccent(foo.name) ILIKE f_unaccent('v%')
AND foo.configuration->'bar' @> '{"is":["a"]}'
LIMIT 100;
_

これは私のインデックスです:

_CREATE INDEX index_foo_on_name_de_gin ON foo USING gin(f_unaccent(name) gin_trgm_ops) WHERE locale = 'de';
_

インデックスが無視され、seq scanおよび/またはBitmap heap scanが使用されるのはなぜですか? )?この問題を解決するために他のインデックスをどのように追加できますか?

なぜ再チェックするのですか?

_Recheck Cond: ((f_unaccent((name)::text) ~~* 'v%'::text) AND ((locale)::text = 'de'::text))
_

関数_f_unaccent_:

_CREATE OR REPLACE FUNCTION f_unaccent(text)
         RETURNS text AS
         $func$
         SELECT unaccent('unaccent', $1)
         $func$  LANGUAGE sql IMMUTABLE SET search_path = public, pg_temp;
_

クエリプラン:

_ Limit  (cost=24412.85..67568.91 rows=100 width=301) (actual time=21838.473..21838.473 rows=0 loops=1)
   Buffers: shared hit=1 read=749976
   ->  Bitmap Heap Scan on foo  (cost=24412.85..4595502.73 rows=10592 width=301) (actual time=21838.470..21838.470 rows=0 loops=1)
         Recheck Cond: ((f_unaccent((name)::text) ~~* 'v%'::text) AND ((locale)::text = 'de'::text))
         Rows Removed by Index Recheck: 5416739
         Filter: ((configuration -> 'bar'::text) @> '{"is": ["a"]}'::jsonb)
         Rows Removed by Filter: 2196
         Heap Blocks: exact=749172
         Buffers: shared hit=1 read=749976
         ->  Bitmap Index Scan on index_foo_on_name_de_gin  (cost=0.00..24410.20 rows=10591544 width=0) (actual time=641.532..641.532 rows=5418935 loops=1)
               Index Cond: (f_unaccent((name)::text) ~~* 'v%'::text)
               Buffers: shared hit=1 read=804
 Planning time: 0.767 ms
 Execution time: 21838.549 ms
_

テーブル定義:

_    Column     |            Type             |                          Modifiers                           | Storage  | Stats target | Description 
---------------+-----------------------------+--------------------------------------------------------------+----------+--------------+-------------
 id            | integer                     | not null default nextval('foo_id_seq'::regclass)             | plain    |              | 
 locale        | character varying           | not null                                                     | extended |              | 
 name          | character varying           | not null                                                     | extended |              | 
 configuration | jsonb                       | not null default '{}'::jsonb                                 | extended |              | 

"index_foo_on_configuration" gin (configuration)
"index_foo_on_name_de_gin" gin (f_unaccent(name::text) gin_trgm_ops) WHERE locale::text = 'de'::text
_

_foo.configuration_フィルターを使用しない場合、クエリは非常に高速です(1.021ミリ秒)。しかし、私はこのフィルターが必要です。ここでは、フィルターなしのクエリ:

_EXPLAIN (ANALYZE, BUFFERS)
SELECT  "foo".* FROM "foo" WHERE "foo"."locale" = 'de'
AND f_unaccent(foo.name) ILIKE f_unaccent('v%')
LIMIT 100;
_

変更による結果

  • _f_unnacent_関数を更新する
  • Btreeインデックスを追加CREATE INDEX index_foo_on_name_de ON foo (f_unaccent(name) text_pattern_ops) WHERE locale = 'de';
  • 構成にginインデックスを追加CREATE INDEX index_foo_on_configuration ON foo USING gin(configuration jsonb_path_ops);
  • 古いインデックスを削除しました

A)クエリ:

_EXPLAIN (ANALYZE, BUFFERS)
SELECT  "foo".* FROM "foo" WHERE "foo"."locale" = 'de' 
AND f_unaccent(foo.name) ILIKE f_unaccent('v%')
AND foo.configuration->'bar' @> '{"0":["s"]}' 
LIMIT 100;
_

A)クエリプラン:

_ Limit  (cost=0.00..121248.83 rows=100 width=301) (actual time=16319.267..16319.267 rows=0 loops=1)
   Buffers: shared hit=262079 read=1449294
   ->  Seq Scan on foo  (cost=0.00..12842675.96 rows=10592 width=301) (actual time=16319.261..16319.261 rows=0 loops=1)
         Filter: (((locale)::text = 'de'::text) AND ((configuration -> 'bar'::text) @> '{"is": ["a"]}'::jsonb) AND (f_unaccent((name)::text) ~~* 'v%'::text))
         Rows Removed by Filter: 41227048
         Buffers: shared hit=262079 read=1449294
 Planning time: 0.765 ms
 Execution time: 16319.313 ms and more!!!
_

B)設定なしのクエリ:

_EXPLAIN (ANALYZE, BUFFERS)
SELECT  "foo".* FROM "foo" WHERE "foo"."locale" = 'de' 
AND f_unaccent(foo.name) ILIKE f_unaccent('v%') LIMIT 100;
_

B)クエリプラン:

_ Limit  (cost=0.00..119.31 rows=100 width=301) (actual time=0.227..2.912 rows=100 loops=1)
   Buffers: shared read=31
   ->  Seq Scan on foo  (cost=0.00..12636540.72 rows=10591544 width=301) (actual time=0.221..2.864 rows=100 loops=1)
         Filter: (((locale)::text = 'de'::text) AND (f_unaccent((name)::text) ~~* 'v%'::text))
         Rows Removed by Filter: 691
         Buffers: shared read=31
 Planning time: 0.501 ms
 Execution time: 2.985 ms
_

C)設定と制限なしのクエリ:

_EXPLAIN (ANALYZE, BUFFERS)
SELECT  "foo".* FROM "foo" WHERE "foo"."locale" = 'de' 
AND f_unaccent(foo.name) ILIKE f_unaccent('v%');
_

C)クエリプラン:

_ Bitmap Heap Scan on foo  (cost=346203.46..4864616.26 rows=10591544 width=301) (actual time=23526.443..30050.008 rows=2196 loops=1)
   Recheck Cond: ((locale)::text = 'de'::text)
   Rows Removed by Index Recheck: 14094842
   Filter: (f_unaccent((name)::text) ~~* 'v%'::text)
   Rows Removed by Filter: 10781095
   Heap Blocks: exact=572873 lossy=847868
   Buffers: shared read=1494015
   ->  Bitmap Index Scan on index_foo_on_name_de  (cost=0.00..343555.58 rows=10592603 width=0) (actual time=1788.454..1788.454 rows=10783291 loops=1)
         Buffers: shared read=73274
 Planning time: 0.528 ms
 Execution time: 30050.168 ms
_
3
phlegx

1. f_unaccent()

ここで定義されている私の関数を使用しているようです:

今行った更新に注意してください。これの方が良い:

_CREATE OR REPLACE FUNCTION f_unaccent(text)
  RETURNS text AS
$func$
SELECT public.unaccent('public.unaccent', $1)  -- schema-qualify function and dictionary
$func$  LANGUAGE sql IMMUTABLE;
_

あそこの詳しい説明。

2.再確認

なぜ再チェックするのですか?

「条件の再確認:」行は、ビットマップインデックススキャンのEXPLAIN出力に常にあります。心配無用。詳細な説明:

3.インデックスとクエリプラン

インデックスが無視されるのはなぜですか

それは誤解です。インデックスは明らかに無視されません。 Postgresが十分な行を見つけるためにメインリレーションの一部のデータページを2回以上訪問する必要があると予想する場合(明らかに_rows=10591544_の場合)、インデックススキャンからビットマップインデックススキャンに切り替わります。実際のタプルを取得するための「ビットマップヒープスキャン」。詳細:

このクエリが本当に高価になるのは、複数の不幸な要因の組み合わせです

  1. インデックス(バッファ:共有ヒット= 1読み取り= 804)もテーブル(_Buffers: shared hit=1 read=749976_)もキャッシュされませんでした。そのクエリをすぐに繰り返すと、それまでにすべてがキャッシュされるため、much速くなります。これは最悪の場合可能です

  2. 検索パターンf_unaccent('v%')-または単に_'v%'_は非常に悪いケーストライグラムインデックスの場合。それほど選択的ではありませんが、実際の順次スキャンの代わりに使用するのに十分選択的です。この場合、_text_pattern_ops_インデックスの方がはるかに高速です。下記参照。
    より選択的なパターン(長い文字列)も、はるかに高速になります。

  3. あなたは_LIMIT 100_を持っていたので、Postgresは楽観的に100行をすばやく見つけたいと考え始めました。しかし、クエリは0行で戻ります(_rows=0_)。これは、Postgresがallの候補行をすべて処理しなければならなかったことを意味します。別の最悪の場合シナリオ。あなたの2番目の述語はここで非難することです:

    _AND foo.configuration->'bar' @> '{"is":["a"]}'
    _

    Postgresのjsonb列の統計は非常に限られています。その状態がどの程度選択的であるかはわかりません。 _configuration->'bar'_に対するクエリが多い場合、別の式インデックスを使用して状況を大幅に改善できます...

    おそらく複数列のインデックスです。

4. _text_pattern_ops_

左にアンカーされたパターン(「右端のワイルドカード」)の場合は、トライグラムインデックスなしで済ますことができます。ただし、「C」ロケール(事実上「ロケールなし」)以外のDBのロケールを使用している場合、プレーンなbtreeインデックスは機能しません。そうでなければ、ロケールを無視するために特別な演算子クラスが必要です。お気に入り:

_CREATE INDEX index_foo_name_pattern_ops_de ON foo (f_unaccent(name) text_pattern_ops)
WHERE locale = 'de';
_

詳細:

4