web-dev-qa-db-ja.com

Postgresから効率的にクエリを実行して特別な単語を選択するにはどうすればよいですか?

非常に多くのレコードを持つwordsというテーブルがあるとします。
列はidおよびnameです。

私が持っているwordsテーブルには、例えば:

 'systematic', 'سلام','gear','synthesis','mysterious', etc.  

注:utf8ワードもあります。
どの単語が文字を含むかを確認するために効率的にクエリする方法's''m'および'e'(すべて)?

出力は次のようになります。

systematic,mysterious

私はそのようなことをする方法を知りません。それ以外の場合はサーバーが影響を受けるため、効率的でなければなりません。

3
ALH

簡単な方法は、各Wordに対応する文字の配列を検討し、その内部を_@>_(含む)配列演算子で検索することです。 マニュアルの例 に示すように、これは文字の位置とは無関係に機能します。つまり、_ARRAY[1,4,3] @> ARRAY[3,1]_はtrueです。

この配列はregexp_split_to_array(name, '')で簡単に取得できます。
[[〜#〜]編集[〜#〜]@ Erwinの回答string_to_array(name, NULL)の方が高速なので、より使いやすくなります。それは残りの答えのドロップイン交換です]

以下は、英語とフランス語の単語(〜511000行、平均長= 13文字)の混合を含むテストテーブルの列として配列を最初に具体化するデモです。次に、配列を列として追加しない2番目のテストテーブルです。

_=> CREATE TABLE tstword AS
    SELECT Word_id as id,
    wordtext as name,
    regexp_split_to_array(wordtext, '') as arr FROM words;
_

比較的多数の単語を見つけるには:

_=> select count(*) from tstword where arr @> array['s','m','e'];
 count 
-------
 42268
(1 row)

Time: 268.809 ms
_

EXPLAIN ANALYZEで示されているように、これは順次スキャンを実行します。

_ explain analyze select name from tstword where arr @> array['s','m','e'];
                                                   QUERY PLAN                                                   
----------------------------------------------------------------------------------------------------------------
 Seq Scan on tstword  (cost=0.00..17554.46 rows=21256 width=14) (actual time=0.020..268.525 rows=42268 loops=1)
   Filter: (arr @> '{s,m,e}'::text[])
   Rows Removed by Filter: 468729
 Total runtime: 269.927 ms
(4 rows)

Time: 270.414 ms
_

しかし、GINインデックスで配列にインデックスを付けることができます。

_CREATE INDEX idx_tst on tstword using gin(arr);
_

そして、それはかなり高速です:

_explain analyze select name from tstword where arr @> array['s','m','e'];
                                                         QUERY PLAN                                                         
----------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on tstword  (cost=252.74..11815.73 rows=21256 width=14) (actual time=46.378..60.203 rows=42268 loops=1)
   Recheck Cond: (arr @> '{s,m,e}'::text[])
   ->  Bitmap Index Scan on idx_tst  (cost=0.00..247.42 rows=21256 width=0) (actual time=45.202..45.202 rows=42268 loops=1)
         Index Cond: (arr @> '{s,m,e}'::text[])
 Total runtime: 61.677 ms
(5 rows)

Time: 70.185 ms
_

Postgresは関数型インデックスをサポートしているため、式に直接インデックスを付けることで、配列を列として具体化することを回避することもできます。

_create table tstword2 as select Word_id as id,wordtext as name from words;
create index idx_tst2 on tstword2  using gin(regexp_split_to_array(name, ''));
_

次に、まったく同じ式で検索を行う必要があり、インデックスが使用されます。

_ explain analyze select name from tstword2 where regexp_split_to_array(name, '') @> array['s','m','e'];
                                                       QUERY PLAN                                                       
------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on tstword2  (cost=40.00..44.02 rows=1 width=14) (actual time=39.390..48.435 rows=42268 loops=1)
   Recheck Cond: (regexp_split_to_array((name)::text, ''::text) @> '{s,m,e}'::text[])
   ->  Bitmap Index Scan on idx_tst2  (cost=0.00..40.00 rows=1 width=0) (actual time=39.053..39.053 rows=42268 loops=1)
         Index Cond: (regexp_split_to_array((name)::text, ''::text) @> '{s,m,e}'::text[])
 Total runtime: 49.748 ms
(5 rows)

Time: 50.193 ms
_

これらのタイプのインデックスに関する警告については、マニュアルの GistおよびGINインデックスタイプ を参照してください。

5
Daniel Vérité

テストケース

中途半端な現実的なテストケースを作成しました。

_CREATE TABLE Word(word_id int, Word text);
INSERT INTO Word (Word_id, Word)
SELECT g%(2500000/25) -- max length 25
      ,left(string_agg(chr(97 + (random()^2 * 31)::int), ''), 3 + (random()^2 * 25)::int)
FROM  (SELECT generate_series(1, 2500000) AS g) g
GROUP  BY 1
ORDER  BY 1;  --> 100k rows
_

3〜25文字の小文字の単語。ASCII以外の文字の代用としていくつかの追加文字。短い単語のほうが一般的で、一部の文字は他よりも一般的です。傾斜したデータ分布を取得するためのrandom()^2の使用。

これを使用して、いくつかのテストを実行し、いくつかの代替案と比較しました @ Danielのアプローチ

Word LIKE ALL(arr)

このクエリは、インデックスがなくても驚くほど効果的であることがわかりました。

_SELECT * FROM Word
WHERE  Word LIKE ALL('{%l%,%w%,%x%,%y%,%z%,%~%}'::text[]);
_

これを機能させるには、文字に_%_を埋め込みます。つまり:a-> _%a%_とし、上記のような配列を形成します。

GINインデックスに基づく他のソリューションと同じくらい高速で、100k行の順次スキャンを使用できます。また、1万行と4万行とPostgres 9.1でテストしました。同様の結果。

これは、テーブルがさらに大きくなると悪化します。ただし、テーブルの行数が10万行以下の場合(および他の大きな列がない場合)、この単純なクエリで十分です。

機能指数

string_to_array()

まず、string_to_array(Word, NULL)の代わりにregexp_split_to_array(Word, ''))を使用します。 Postgres 9.1では5〜6倍、Postgres 9.3では2〜3倍高速であることがわかりました。主にインデックスのメンテナンスに影響しますが、クエリのパフォーマンスもそれほど影響を受けません。

これらの式のどちらでも、重複の可能性のあるソートされていない配列になります。

_CREATE INDEX Word_arr_gin_idx ON Word USING gin(string_to_array(Word, NULL));
_

効果のないバリアント

配列から重複を削除することは、GINアルゴリズムがそれ自体で行うため、効果がありません。 マニュアルの引用:

各キー値は1回だけ格納されるため、GINインデックスは、同じキーが何度も出現する場合に非常にコンパクトです。

いずれにせよ、この効果のないバリアントをフィドルに含めて、要点を証明するとともに、概念実証としても使用します。ユーザー定義のIMMUTABLE関数をインデックスで使用する方法:

_CREATE OR REPLACE FUNCTION uniq_arr(text)
  RETURNS text[] AS
$func$
SELECT ARRAY(
   SELECT DISTINCT i
   FROM   unnest(string_to_array($1, NULL)) t(i))
$func$ LANGUAGE sql IMMUTABLE;

CREATE INDEX Word_uniq_arr_gin_idx ON Word USING gin(uniq_arr(Word));
_

個別のsorted配列のインデックスも同じ検索時間を示しますが、コストが高くなります。含まれていません。

-> SQLfiddleデモ

折りたたみ

ただし、上記のようなIMMUTABLE関数を使用して文字を折りたたむことができます。典型的な使用例は unaccent module を使用してアクセント(分音符号)を削除することです。つまり、検索時に_é_、_è_なども検索します。 e

これは非常に効果的な解決策です:

_CREATE OR REPLACE FUNCTION unaccent_arr(text) RETURNS text[] AS
$func$
SELECT string_to_array(unaccent('unaccent', $1), NULL)
$func$ LANGUAGE sql IMMUTABLE SET search_path = public, pg_temp;

CREATE INDEX Word_unaccent_arr_gin_idx ON Word USING gin(unaccent_arr(Word));
_

SOに関するこの関連する回答の詳細な説明(必ずお読みください):
PostgreSQLは「アクセントを区別しない」照合をサポートしていますか?

Postgres 9.1と9.3でローカルでテストしましたが、追加モジュールをインストールできないフィドルに含めることはできません。

2

Postgresqlには多くのインデックスオプションがあります。また、これらのインデックスオプションに基づいて構築された強力なコンポーネントもいくつかあります。

そのような機能の1つが全文検索です。 http://www.postgresql.org/docs/9.3/static/textsearch.html を参照してください

1
Colin 't Hart