PostgreSQLのインデックスメカニズム 、 アクセス方法のインターフェイス 、およびハッシュインデックス 、 Bツリー 、 GiST 、 SP-GiST 、 GINなどのすべての基本的なアクセス方法については既に検討し ました 。 そして、このパートでは、ジンをラム酒に変えることを見てみましょう。
ラム
著者はジンは強力な精神であると主張しているが、飲料の話題は依然として勝ち続けている。次世代のGINはRUMと呼ばれていた。
このアクセス方法は、GINに組み込まれたアイデアを開発し、全文検索をさらに高速に実行できるようにします。 これは、標準のPostgreSQLパッケージの一部ではなく、サードパーティの拡張機能であるこの記事シリーズの唯一の方法です。 インストールにはいくつかのオプションがあります。
- PGDGリポジトリからyumまたはaptパッケージを取得します 。 たとえば、postgresql-10パッケージからPostgreSQLをインストールした場合、postgresql-10-rum。
- githubのソースコードから自己アセンブルしてインストールします (同じ場所にある手順)。
- Postgres Pro Enterpriseの一部として使用します(または、少なくともそこからドキュメントを読んでください)。
GINの制限
RUMが克服できるGINインデックスの制限は何ですか?
まず、tsvectorデータ型には、トークン自体に加えて、ドキュメント内での位置に関する情報が含まれています。 GINインデックスでは、 前回見たように 、この情報は保存されません。 このため、バージョン9.6 で登場したフレーズ検索操作は 、GINインデックスによって非効率的に処理され、検証のためにソースデータにアクセスする必要があります。
第二に、検索エンジンは通常、関連性のある順に結果を返します(それが何であれ)。 これを行うには、 ランキング関数ts_rankおよびts_rank_cdを使用できますが、結果の行ごとに計算する必要がありますが、もちろん遅いです。
最初の近似では、RUMアクセス方法はGINと見なすことができ、位置情報が追加され、目的の順序で結果の出力をサポートします( GiSTが最近傍を発行する方法と同様)。 順番に行きましょう。
フレーズ検索
全文検索のクエリには、トークン間の距離を考慮した特別な構造が含まれる場合があります。 たとえば、祖母と祖父を別の単語で区切った文書を見つけることができます。
postgres=# select to_tsvector(' , ...') @@
to_tsquery(' <2> ');
?column?
----------
t
(1 row)
または、単語が互いの後ろに立つことを示します。
postgres=# select to_tsvector(' , ...') @@
to_tsquery(' <-> ');
?column?
----------
t
(1 row)
通常のGINインデックスは両方のトークンを含むドキュメントを生成できますが、tsvectorを見るだけでそれらの間の距離を確認できます。
postgres=# select to_tsvector(' , ...');
to_tsvector
------------------------------
'':1 '':3,4 '':6
(1 row)
RUMインデックスでは、各トークンはテーブル行を参照するだけではありません。各TIDとともに、ドキュメント内でトークンが表示される位置のリストがあります。 以下は、白biで既に馴染みのあるテーブルに作成されたインデックスを想像する方法です(デフォルトでは、tsvectorにrum_tsvector_ops演算子クラスが使用されます)。
postgres=# create extension rum;
CREATE EXTENSION
postgres=# create index on ts using rum(doc_tsv);
CREATE INDEX

図の灰色の四角-位置情報を追加しました:
postgres=# select ctid, doc, doc_tsv from ts;
ctid | doc | doc_tsv
--------+-------------------------+--------------------------------
(0,1) | | '':3 '':2 '':4
(0,2) | | '':3 '':2 '':4
(0,3) | , , | '':1,2 '':3
(0,4) | , , | '':1,2 '':3
(1,1) | | '':2 '':3 '':1
(1,2) | | '':3 '':2 '':1
(1,3) | , , | '':3 '':1,2
(1,4) | , , | '':3 '':1,2
(2,1) | | '':3 '':2
(2,2) | | '':1 '':2 '':3
(2,3) | , , | '':3 '':1,2
(2,4) | , , | '':3 '':1,2
(12 rows)
fastupdateパラメーターを指定すると、GINに遅延挿入がまだあります。 RUMはこの機能を削除しました。
インデックスが実際のデータでどのように機能するかを確認するには、知っているpgsql-hackersメーリングリストアーカイブを使用します。
fts=# alter table mail_messages add column tsv tsvector;
ALTER TABLE
fts=# set default_text_search_config = default;
SET
fts=# update mail_messages
set tsv = to_tsvector(body_plain);
...
UPDATE 356125
GINインデックスを使用して、フレーズ検索を使用したクエリを実行する方法は次のとおりです。
fts=# create index tsv_gin on mail_messages using gin(tsv);
CREATE INDEX
fts=# explain (costs off, analyze)
select * from mail_messages where tsv @@ to_tsquery('hello <-> hackers');
QUERY PLAN
---------------------------------------------------------------------------------
Bitmap Heap Scan on mail_messages (actual time=2.490..18.088 rows=259 loops=1)
Recheck Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Rows Removed by Index Recheck: 1517
Heap Blocks: exact=1503
-> Bitmap Index Scan on tsv_gin (actual time=2.204..2.204 rows=1776 loops=1)
Index Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Planning time: 0.266 ms
Execution time: 18.151 ms
(8 rows)
計画からわかるように、GINインデックスが使用されますが、1776の潜在的な一致が返され、そのうち259が残り、1517は再チェックの段階で破棄されます。
GINインデックスを削除して、RUMを作成します。
fts=# drop index tsv_gin;
DROP INDEX
fts=# create index tsv_rum on mail_messages using rum(tsv);
CREATE INDEX
これで、インデックスに必要なすべての情報が含まれ、検索が正確に実行されます。
fts=# explain (costs off, analyze)
select * from mail_messages
where tsv @@ to_tsquery('hello <-> hackers');
QUERY PLAN
--------------------------------------------------------------------------------
Bitmap Heap Scan on mail_messages (actual time=2.798..3.015 rows=259 loops=1)
Recheck Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Heap Blocks: exact=250
-> Bitmap Index Scan on tsv_rum (actual time=2.768..2.768 rows=259 loops=1)
Index Cond: (tsv @@ to_tsquery('hello <-> hackers'::text))
Planning time: 0.245 ms
Execution time: 3.053 ms
(7 rows)
関連性の並べ替え
ドキュメントを正しい順序ですぐに発行するために、RUMインデックスは順序演算子をサポートしています。これについては、 GiSTに関する部分で説明しました。 ラム拡張子は、ドキュメント(tsvector)とクエリ(tsquery)間の特定の距離を返す演算子
<=>
定義します。 例:
fts=# select to_tsvector(' , ...') <=> to_tsquery('');
?column?
----------
16.4493
(1 row)
fts=# select to_tsvector(' , ...') <=> to_tsquery('');
?column?
----------
13.1595
(1 row)
この文書は、2番目の要求よりも最初の要求に関連していることが判明しました。文書に単語が頻繁に現れるほど、「価値のある」ものではなくなります。
繰り返しますが、比較的大量のデータでGINとRUMを比較してみてください。「hello」と「hackers」を含む最も関連性の高い10個のドキュメントを選択します。
fts=# explain (costs off, analyze)
select * from mail_messages
where tsv @@ to_tsquery('hello & hackers')
order by ts_rank(tsv,to_tsquery('hello & hackers'))
limit 10;
QUERY PLAN
---------------------------------------------------------------------------------------------
Limit (actual time=27.076..27.078 rows=10 loops=1)
-> Sort (actual time=27.075..27.076 rows=10 loops=1)
Sort Key: (ts_rank(tsv, to_tsquery('hello & hackers'::text)))
Sort Method: top-N heapsort Memory: 29kB
-> Bitmap Heap Scan on mail_messages (actual ... rows=1776 loops=1)
Recheck Cond: (tsv @@ to_tsquery('hello & hackers'::text))
Heap Blocks: exact=1503
-> Bitmap Index Scan on tsv_gin (actual ... rows=1776 loops=1)
Index Cond: (tsv @@ to_tsquery('hello & hackers'::text))
Planning time: 0.276 ms
Execution time: 27.121 ms
(11 rows)
GINインデックスは1776件の一致を返します。これらは個別にソートされて、最も適切な10個が選択されます。
RUMインデックスでは、クエリは単純なインデックススキャンによって実行されます。余分なドキュメントはスキャンされず、個別の並べ替えは必要ありません。
fts=# explain (costs off, analyze)
select * from mail_messages
where tsv @@ to_tsquery('hello & hackers')
order by tsv <=> to_tsquery('hello & hackers')
limit 10;
QUERY PLAN
--------------------------------------------------------------------------------------------
Limit (actual time=5.083..5.171 rows=10 loops=1)
-> Index Scan using tsv_rum on mail_messages (actual ... rows=10 loops=1)
Index Cond: (tsv @@ to_tsquery('hello & hackers'::text))
Order By: (tsv <=> to_tsquery('hello & hackers'::text))
Planning time: 0.244 ms
Execution time: 5.207 ms
(6 rows)
追加情報
GINと同様に、RUMインデックスはいくつかのフィールドで構築できます。 ただし、異なる列のGINトークンが互いに独立して格納されている場合、RUMを使用すると、メインフィールド(この場合はtsvector)を追加のフィールドと「接続」できます。 これを行うには、特別なrum_tsvector_addon_ops演算子クラスを使用します。
fts=# create index on mail_messages using rum(tsv rum_tsvector_addon_ops, sent)
with (attach='sent', to='tsv');
CREATE INDEX
このようなインデックスを使用して、追加フィールドによるソート順で結果を表示できます。
fts=# select id, sent, sent <=> '2017-01-01 15:00:00'
from mail_messages
where tsv @@ to_tsquery('hello')
order by sent <=> '2017-01-01 15:00:00'
limit 10;
id | sent | ?column?
---------+---------------------+----------
2298548 | 2017-01-01 15:03:22 | 202
2298547 | 2017-01-01 14:53:13 | 407
2298545 | 2017-01-01 13:28:12 | 5508
2298554 | 2017-01-01 18:30:45 | 12645
2298530 | 2016-12-31 20:28:48 | 66672
2298587 | 2017-01-02 12:39:26 | 77966
2298588 | 2017-01-02 12:43:22 | 78202
2298597 | 2017-01-02 13:48:02 | 82082
2298606 | 2017-01-02 15:50:50 | 89450
2298628 | 2017-01-02 18:55:49 | 100549
(10 rows)
ここでは、指定された日付にできるだけ近い場所にある適切な行を探しますが、遅かれ早かれ関係ありません。 厳密に日付に先行する(または後続する)結果を取得するには、操作
<=|
を使用する必要があります
<=|
(または
|=>
)。
予想どおり、クエリは単純なインデックススキャンによって実行されます。
ts=# explain (costs off)
select id, sent, sent <=> '2017-01-01 15:00:00'
from mail_messages
where tsv @@ to_tsquery('hello')
order by sent <=> '2017-01-01 15:00:00'
limit 10;
QUERY PLAN
---------------------------------------------------------------------------------
Limit
-> Index Scan using mail_messages_tsv_sent_idx on mail_messages
Index Cond: (tsv @@ to_tsquery('hello'::text))
Order By: (sent <=> '2017-01-01 15:00:00'::timestamp without time zone)
(4 rows)
フィールドの関係に関する追加情報なしでインデックスを作成した場合、同様のクエリでは、インデックスから受け取ったすべての結果をソートする必要があります。
もちろん、日付に加えて、フィールドや他のデータ型をRUMインデックスに追加できます-ほとんどすべての基本的な型がサポートされています。 たとえば、オンラインストアでは、ノベルティ(日付)、価格(数値)、人気または割引サイズ(整数または浮動小数点)で製品をすばやく表示できます。
その他の演算子クラス
完全を期すために、他の利用可能な演算子のクラスについて言及する価値があります。
rum_tsvector_hash_opsとrum_tsvector_hash_addon_opsから始めましょう。 すべての点で、これらはすでに上記で検討したrum_tsvector_opsおよびrum_tsvector_addon_opsと似ていますが、トークン自体ではなく、そのハッシュコードはインデックスに格納されます。 これにより、インデックスのサイズを小さくすることができますが、もちろん、検索の精度が低下し、二重チェックが必要になります。 さらに、インデックスは部分一致の検索をサポートしなくなりました。
rum_tsquery_ops演算子クラスは好奇心。盛です。 「逆」問題を解決することができます:ドキュメントに一致するクエリを検索します。 なぜこれが必要なのでしょうか? たとえば、ユーザーをフィルターで新しい製品にサブスクライブします。 または、新しいドキュメントを自動的に分類します。 以下に簡単な例を示します。
fts=# create table categories(query tsquery, category text);
CREATE TABLE
fts=# insert into categories values
(to_tsquery('vacuum | autovacuum | freeze'), 'vacuum'),
(to_tsquery('xmin | xmax | snapshot | isolation'), 'mvcc'),
(to_tsquery('wal | (write & ahead & log) | durability'), 'wal');
INSERT 0 3
fts=# create index on categories using rum(query);
CREATE INDEX
fts=# select array_agg(category)
from categories
where to_tsvector(
'Hello hackers, the attached patch greatly improves performance of tuple
freezing and also reduces size of generated write-ahead logs.'
) @@ query;
array_agg
--------------
{vacuum,wal}
(1 row)
演算子クラスrum_anyarray_opsとrum_anyarray_addon_opsは残ります -それらはtsvectorではなく配列で動作するように設計されています。 GINの場合、これは既に最後と見なされているため、繰り返す理由はありません。
インデックスと事前記録のログサイズ
RUMにはGINよりも多くの情報が含まれているため、より多くのスペースを占有することは明らかです。 前回、さまざまなインデックスのサイズを比較しました。 このテーブルとRUMに追加します。
rum | gin | gist | btree
--------+--------+--------+--------
457 MB | 179 MB | 125 MB | 546 MB
ご覧のとおり、ボリュームが大幅に増加しています-これはクイック検索の料金です。
注意が必要なもう1つの明らかな点は、RUMは拡張機能であるということです。つまり、システムのカーネルに変更を加えずにインストールすることができます。 これは、バージョン9.6でAlexander Korotkovが作成したパッチのおかげで可能になりました。 解決する必要があったタスクの1つは、ジャーナルエントリの生成でした。 ジャーナリングメカニズムは絶対に信頼できるものでなければならないため、このキッチンへの拡張は許可されません。 拡張機能が独自のタイプのジャーナルエントリを作成できるようにする代わりに、次のことが行われます:拡張コードは、ページを変更する意図を通知し、変更を加えて完了を通知し、システムのカーネルはページの古いバージョンと新しいバージョンをすでに比較し、必要な統合されたジャーナルを生成しますレコード。
現在の生成アルゴリズムは、ページをバイト単位で比較し、変更されたフラグメントを見つけて、ページの先頭からのオフセットとともにそのような各フラグメントを記録します。 これは、数バイトのみを変更する場合、およびページが完全に変更された場合にうまく機能します。 ただし、残りのコンテンツを下に移動して(または逆にコンテンツを上に移動してフラグメントを削除して)ページ内にフラグメントを追加すると、実際に追加または削除されたバイト数よりもかなり多くのバイトが正式に変更されます。
このため、RUMインデックスを積極的に変更すると、GIN(拡張機能ではなく、カーネルの一部であるジャーナル自体を管理する)よりも大幅に大きいサイズのジャーナルエントリを生成できます。 この不快な効果の程度は実際の負荷に大きく依存しますが、何らかの問題を感じるために、特定の行を数回削除して追加し、これらのアクションをクリーニング(真空)と交互に試してみましょう。 ログエントリのサイズは次のように見積もることができます。最初と最後に、pg_current_wal_location関数(最大10バージョン-pg_current_xlog_location)を使用してログの位置を記憶し、その違いを確認します。
ここでは、もちろん、多くの要因に留意する必要があります。 1人のユーザーのみがシステムで作業していることを確認する必要があります。そうしないと、「余分な」エントリが考慮されます。 この場合でも、RUMだけでなく、テーブル自体と主キーをサポートするインデックスの変更も考慮します。 構成パラメーターの値も影響します(ここでは、圧縮なしでレプリカログレベルを使用しました)。 しかし、まだ試してみてください。
fts=# select pg_current_wal_location() as start_lsn \gset
fts=# insert into mail_messages(parent_id, sent, subject, author, body_plain, tsv)
select parent_id, sent, subject, author, body_plain, tsv
from mail_messages where id % 100 = 0;
INSERT 0 3576
fts=# delete from mail_messages where id % 100 = 99;
DELETE 3590
fts=# vacuum mail_messages;
VACUUM
fts=# insert into mail_messages(parent_id, sent, subject, author, body_plain, tsv)
select parent_id, sent, subject, author, body_plain, tsv
from mail_messages where id % 100 = 1;
INSERT 0 3605
fts=# delete from mail_messages where id % 100 = 98;
DELETE 3637
fts=# vacuum mail_messages;
VACUUM
fts=# insert into mail_messages(parent_id, sent, subject, author, body_plain, tsv)
select parent_id, sent, subject, author, body_plain, tsv from mail_messages
where id % 100 = 2;
INSERT 0 3625
fts=# delete from mail_messages where id % 100 = 97;
DELETE 3668
fts=# vacuum mail_messages;
VACUUM
fts=# select pg_current_wal_location() as end_lsn \gset
fts=# select pg_size_pretty(:'end_lsn'::pg_lsn - :'start_lsn'::pg_lsn);
pg_size_pretty
----------------
3114 MB
(1 row)
そのため、約3 GBになりました。 GINインデックスを使用して同じ実験を繰り返した場合、約700 MBしかありません。
したがって、diffユーティリティの動作と同様に、あるページの状態を別のページの状態に移動できる最小数の挿入および削除操作を見つける別のアルゴリズムが必要です。 そのようなアルゴリズムはすでにOleg Ivanovによって実装されており、彼のパッチは議論されています。 上記の例では、このパッチはわずかな速度低下を犠牲にして、ジャーナルエントリのボリュームを1.5倍、1900 MBに減らすことができます。
プロパティ
伝統的に、私たちはginとの違いに注意を払って、ラムアクセスメソッドのプロパティを調べます(リクエストは以前に与えられました )。
メソッドのプロパティ:
amname | name | pg_indexam_has_property
--------+---------------+-------------------------
rum | can_order | f
rum | can_unique | f
rum | can_multi_col | t
rum | can_exclude | t -- f gin
インデックスプロパティ:
name | pg_index_has_property
---------------+-----------------------
clusterable | f
index_scan | t -- f gin
bitmap_scan | t
backward_scan | f
RUMはGINとは異なり、インデックススキャンをサポートしていることに注意してください。さもないと、フレーズ制限のあるクエリで必要な数の結果を正確に取得できません。 したがって、gin_fuzzy_search_limitパラメーターの類似物は必要ありません。 その結果、インデックスを使用して除外の制限をサポートできます。
列レベルのプロパティ:
name | pg_index_column_has_property
--------------------+------------------------------
asc | f
desc | f
nulls_first | f
nulls_last | f
orderable | f
distance_orderable | t -- f gin
returnable | f
search_array | f
search_nulls | f
ここでの違いは、RUMが照合演算子をサポートしていることです。 すべての演算子クラスではありませんが、たとえば、tsquery_opsの場合はfalseになります。
継続する 。