Postgres 9.1データベースには、約150万行と列label
を持つテーブル_table1
_があります(この質問のために簡略化した名前)。
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
_
また、検索がより厳密であるにもかかわらず、単語を追加することはさらに遅くなります。
最初のWordに対してサブクエリを実行し、次に完全な検索文字列を使用してクエリを実行する簡単なトリックを試しましたが、(悲しいことに)クエリプランナーは私の巧妙さを理解しました。
_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) (unaccent((label):: text))~~ '%someword%' :: text)AND(lower(unaccent((label):: text))~~ '%someword and some more%' :: text) ) -> table1_label_hun_gin_trgm(cost = 0.00..16216.01 rows = 1 width = 0)のビットマップインデックススキャン(実際の時間= 1823.900..1823.900 rows = 1 loops = 1) インデックス条件: ((lower(unaccent((label):: text))~~ '%someword%' :: text)AND(lower(unaccent((label):: text))~~ '%someword and some more%': :text)) 合計実行時間:1824.064 ms
私の最終的な問題は、検索文字列が非常に長い文字列を送信する可能性があるため、非常に遅く、DOSベクトルを構成する可能性があるWebインターフェイスからのものであるということです。
だから私の質問は:
PostgreSQL 9.6では、pg_trgmの新しいバージョン1.2が提供される予定です。少しの努力で、この新しいバージョンをPostgreSQL 9.4で動作させることもできます(パッチを適用し、拡張モジュールを自分でコンパイルしてインストールする必要があります)。
最も古いバージョンでは、クエリ内の各トライグラムを検索し、それらの和集合を取得して、フィルターを適用します。新しいバージョンが行うことは、クエリで最もまれなトライグラムを選択し、その1つだけを検索して、残りを後でフィルタリングすることです。
これを行うための機構は、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%だけにインデックスを使用し、それらの行だけにフィルターを適用します。
また、常に単語全体を検索する場合は、関数を使用して文字列を単語の配列にトークン化し、その配列を返す関数で(pg_trgmではなく)通常の組み込みGINインデックスを使用できます。
クエリプランナーを詐欺する方法を見つけました。これは非常に単純なハックです。
_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) 条件の再確認:(lower( unaccent((label_hun):: text))~~ '%someword%' :: text) フィルター:(lower(lower(unaccent((label):: text)))~~ '%someword and some% ':: text) ->テーブル1_label_hun_gin_trgmのビットマップインデックススキャン(コスト= 0.00..6749.11行= 147幅= 0)(実際の時間= 256.499..256.499行= 1ループ= 1) インデックス条件:(lower(unaccent((label):: text))~~ '%someword%' :: text) 合計実行時間:256.653 ms
したがって、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%'))
_
もちろん、これはヒューリスティックであり、インデックススキャンで使用されるカットアウト部分がvery commonである場合、うまく機能しない可能性があります。しかし、私たちのデータベースでは、10〜15文字程度を使用しても、それほど多くの繰り返しはありません。
残りの2つの小さな質問があります。