検索文字列が長くなると、トライグラム検索が非常に遅くなります


16

Postgres 9.1データベースには、table1約150万行と1列のテーブルがありますlabel(この質問のために簡略化された名前)。

機能的なtrigram-indexがありますlower(unaccent(label))(インデックスでunaccent()使用できるように不変にされています)。

次のクエリは非常に高速です。

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword%')));
 count 
-------
     1
(1 row)

Time: 394,295 ms

ただし、次のクエリは遅くなります。

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword and some more%')));
 count 
-------
     1
(1 row)

Time: 1405,749 ms

また、検索がより厳密であっても、単語の追加はさらに遅くなります。

私は最初の単語のサブクエリを実行し、次に完全な検索文字列でクエリを実行する簡単なトリックを試しましたが、クエリプランナは(悲しいことに)私の陰謀を見ました:

EXPLAIN ANALYZE
SELECT * FROM (
   SELECT id, title, label from table1
   WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(unaccent(label)) like lower(unaccent('%someword and some more%'));
テーブル1のビットマップヒープスキャン(コスト= 16216.01..16220.04行= 1幅= 212)(実際の時間= 1824.017..1824.019行= 1ループ= 1)
  Condの再チェック:((lower(unaccent((label):: text))~~ '%someword%' :: text)AND(lower(unaccent((label):: text))~~ '%somewordなど%'::テキスト))
  -> table1_label_hun_gin_trgmのビットマップインデックススキャン(cost = 0.00..16216.01 rows = 1 width = 0)(actual time = 1823.900..1823.900 rows = 1 loops = 1)
        インデックス条件:((lower(unaccent((label):: text))~~ '%someword%' :: text)AND(lower(unaccent((label):: text))~~ '%somewordなど%'::テキスト))
合計ランタイム:1824.064ミリ秒

私の究極の問題は、検索文字列が非常に長い文字列を送信するWebインターフェースに由来するため、非常に遅くなり、DOSベクトルを構成する可能性があることです。

だから私の質問は:

  • クエリを高速化する方法は?
  • より速くなるようにサブクエリに分割する方法はありますか?
  • 後のバージョンのPostgresの方が良いのでしょうか?(私は9.4を試してみましたが、速くは見えません。それでも同じ効果です。後のバージョンでしょうか?)
  • 別のインデックス作成戦略が必要な場合がありますか?

1
unaccent()追加モジュールによって提供されることにも言及する必要があります。Postgresはデフォルトではないため、関数のインデックスをサポートしていませIMMUTABLE。何かを変更したに違いないので、質問で正確に何をしたかを述べてください。私の立ってアドバイス:stackoverflow.com/a/11007216/939860。また、トライグラムインデックスは、大文字と小文字を区別しない一致なしのマッチングをサポートします。次のように単純化できます。- WHERE f_unaccent(label) ILIKE f_unaccent('%someword%')一致するインデックス。詳細:stackoverflow.com/a/28636000/939860
アーウィンブランドステッター

私は単にunaccent不変を宣言しました。これを質問に追加しました。
P.Peter

unaccentモジュールを更新すると、ハックが上書きされることに注意してください。代わりに関数ラッパーを提案する理由の1つ。
アーウィンブランドステッター

回答:


34

PostgreSQL 9.6には、pg_trgmの新しいバージョン1.2があり、これについてははるかに改善されます。少し手間をかけるだけで、この新しいバージョンをPostgreSQL 9.4で動作させることもできます(パッチを適用し、拡張モジュールを自分でコンパイルしてインストールする必要があります)。

最も古いバージョンでは、クエリ内の各トライグラムを検索し、それらの和集合を取得してからフィルターを適用します。新しいバージョンが行うことは、クエリで最もまれなトライグラムを選択し、そのトライグラムのみを検索し、残りを後でフィルタリングします。

これを行うための機械は9.1には存在しません。9.4では、その機械が追加されましたが、そのときpg_trgmはそれを利用するように適合されていませんでした。

悪意のある人が一般的なトライグラムのみを含むクエリを作成できるため、DOSの潜在的な問題が引き続き発生します。「%and%」、または「%a%」など


pg_trgm 1.2にアップグレードできない場合、プランナーをだます別の方法は次のとおりです。

WHERE (lower(unaccent(label)) like lower(unaccent('%someword%'))) 
AND   (lower(unaccent(label||'')) like 
      lower(unaccent('%someword and some more%')));

空の文字列を連結してラベルを付けることにより、プランナーをだまして、where句のその部分のインデックスを使用できないと考えさせます。したがって、%someword%だけでインデックスを使用し、それらの行だけにフィルターを適用します。


また、常に単語全体を検索する場合は、関数を使用して文字列を単語の配列にトークン化し、その配列を返す関数で通常の組み込みGINインデックス(pg_trgmではない)を使用できます。


13
あなたがパッチを書いたのはあなただと言っておく価値があります。そして、予備的な性能テストは印象的です。これは本当に多くの賛成に値する(現在のバージョンでの説明と回避策のためにも)。
アーウィンブランドステッター

少なくとも、9.1にはなかったパッチの実装に使用した機械への参照にもっと興味があります。しかし、私はエルウィンの悪いお尻の答えに同意します。
エヴァンキャロル

3

クエリプランナーを詐欺する方法を見つけました。これは非常に単純なハックです。

SELECT *
FROM (
   select id, title, label
   from   table1
   where  lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

EXPLAIN 出力:

テーブル1のビットマップヒープスキャン(コスト= 6749.11..7332.71行= 1幅= 212)(実際の時間= 256.607..256.609行= 1ループ= 1)
  Condの再確認:(lower(unaccent((label_hun):: text))~~ '%someword%' :: text)
  フィルター:(lower(lower(unaccent((label):: text)))~~ '%someword and some more%' :: text)
  -> table1_label_hun_gin_trgmのビットマップインデックススキャン(cost = 0.00..6749.11 rows = 147 width = 0)(actual time = 256.499..256.499 rows = 1 loops = 1)
        インデックス条件:(lower(unaccent((label):: text))~~ '%someword%' :: text)
合計ランタイム:256.653ミリ秒

そのためlower(lower(unaccent(label)))、のインデックスがないため、順次スキャンが作成されるため、単純なフィルターに変換されます。さらに、単純なANDも同じことを行います。

SELECT id, title, label
FROM table1
WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
AND   lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

もちろん、インデックススキャンで使用される切り抜き部分が非常に一般的である場合、これはうまく機能しない可能性があるヒューリスティックです。しかし、データベースでは、約10〜15文字を使用する場合、それほど多くの繰り返しはありません。

残りの2つの小さな質問があります。

  • なぜpostgresはこのようなものが有益であると理解できないのでしょうか?
  • postgresは0..256.499の時間範囲で何をしますか(出力の分析を参照)。

1
0〜256.499の時間範囲では、ビットマップを構築しています。256.499では、ビットマップである最初の出力を生成します。単一の出力、つまり単一の完成したビットマップのみを生成するため、これも最後の出力です。
-jjanes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.