はじめに
こんにちは。kkystです。
開発を担当しているプロダクトではpg_bigmを利用して全文検索機能を提供しています。
今回、その全文検索を行っているテーブルにINSERTを行う一部の処理で、応答時間が増えていることを検知しました。
そこでその原因を調査していったところ、GINインデックスのGIN高速更新手法にたどり着き、待機リストの有無による応答時間の検証を行いました。
その結果として、GIN高速更新手法の有効性を確認することができたので、検証の記録を残しておきたいと思います。
概要
GIN高速更新手法とは
GINインデックスは全文検索向けのインデックスで文書中の単語の位置を保持しているため、使用することで特定の単語の検索を効率的に行えるようになります。
基本的には英文(英単語)を意識したものとなっていますが、pg_bigmなどのモジュールを導入することで日本語でも利用できます。
GINインデックスの高速更新手法については、PostgreSQLのリファレンスには下記のように記載されています。
1つのヒープ行の挿入または更新によりインデックスへの挿入が多く発生するという、転置インデックスの本質的な性質のためGINインデックスの更新は低速になりがちです。 (各キー用のヒープ行はインデックス付けされた項目から取り出されます。) PostgreSQL 8.4からGINは、新しいタプルを一時的なソートされていない、待機中の項目リストに挿入することにより、この作業の大部分を遅延させることができるようになりました。
(中略)
この手法の大きな欠点は、検索時に通常のインデックス検索に加え待機中の項目リストのスキャンを行わなければならない点です。 このため、待機中の項目リストが大きくなると検索が顕著に遅くなります。 他の欠点は、ほとんどの更新は高速ですが、待機中の項目リストが「大きくなりすぎる」きっかけとなった更新は即時の整理処理を招くことになり、他の更新に比べ大きく低速になります。 自動バキュームを適切に使用することで、これらの両方の問題を最小化することができます。
「整理処理を行う更新処理が大きく低速となる」という部分が実際にどれくらい低速となるの?という疑問が浮んだので、この部分に着目して実際の処理時間を計測することにしました。
検証環境
create table sample_table(id integer, note text);
create index sample_table_ix1 on sample_table using gin (note gin_bigm_ops);
※待機リストを利用しない場合は、WITH (FASTUPDATE = OFF)を付与してインデックス再作成を実施。
検証内容
以下の状態でASCIIコードのランダム1000バイトの文字列データを1件挿入し、処理時間を計測する。
- 初期データなし
- 待機リストあり(空)
- 待機リストあり(最大≒gin_pending_list_limit(4MB):1000バイト * 170件)
- 待機リストを利用しない
- 初期データ100万件(各行1000バイト)
- 待機リストあり(空)
- 待機リストあり(最大≒gin_pending_list_limit(4MB):1000バイト * 170件)
- 待機リストを利用しない
検証結果
|
待機リスト(空) |
待機リスト(最大) |
待機リストなし |
初期データなし |
89.034ms |
97.462ms |
83.937ms |
初期データあり |
70.088ms |
863.200ms |
827.526ms |
この結果より、インデックスへの整理処理そのものに時間がかかり、整理する対象のデータ件数による差はほとんどありませんでした。
また、今回用意したパターンであれば、その整理処理自体も1秒を切っているので、他の処理との兼ね合いもありますがオンラインで利用しても許容できる速度であることがわかりました。
このことから、大半の更新処理が高速になるGIN高速更新手法は非常に有効であり、特別な要件がない限りは有効にしておくべきものだということがわかりました。
1.1. 初期データなし-待機リストあり(空)
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 2 | 1 | 0 | 0 | 2
dev=# INSERT INTO sample_table SELECT 1000000, STRING_AGG(str, '') FROM ( SELECT chr(40 + (RANDOM() * 1000)::INT % 84 ) AS str FROM GENERATE_SERIES(1, 1000) length) t;
INSERT 0 1
時間: 89.034 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
2 | 4 | 5680 | 3 | 1 | 2 | 1 | 0 | 0 | 2
1.2. 初期データなし-待機リストあり(最大)
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
347 | 4 | 5660 | 177 | 59 | 2 | 1 | 0 | 0 | 2
dev=# INSERT INTO sample_table SELECT 1000000, STRING_AGG(str, '') FROM ( SELECT chr(40 + (RANDOM() * 1000)::INT % 84 ) AS str FROM GENERATE_SERIES(1, 1000) length) t;
INSERT 0 1
時間: 97.462 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
2 | 4 | 5660 | 3 | 1 | 2 | 1 | 0 | 0 | 2
1.3. 初期データなし-待機リストを利用しない
dev=# create index sample_table_ix1 on sample_table using gin (note gin_bigm_ops) WITH (FASTUPDATE = OFF);
CREATE INDEX
時間: 43.528 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 2 | 1 | 0 | 0 | 2
dev=# INSERT INTO sample_table SELECT 1000000, STRING_AGG(str, '') FROM ( SELECT chr(40 + (RANDOM() * 1000)::INT % 84 ) AS str FROM GENERATE_SERIES(1, 1000) length) t;
INSERT 0 1
時間: 83.937 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 2 | 1 | 0 | 0 | 2
2.1. 初期データあり-待機リストあり(空)
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 212649 | 2764 | 209881 | 7224 | 2
dev=# INSERT INTO sample_table SELECT 1000001, STRING_AGG(str, '') FROM ( SELECT chr(40 + (RANDOM() * 1000)::INT % 84 ) AS str FROM GENERATE_SERIES(1, 1000) length) t;
INSERT 0 1
時間: 70.088 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
212646 | 212648 | 5740 | 3 | 1 | 212649 | 2764 | 209881 | 7224 | 2
2.2. 初期データあり-待機リストあり(最大)
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
212646 | 213158 | 5940 | 513 | 171 | 212649 | 2764 | 209881 | 7224 | 2
dev=# INSERT INTO sample_table SELECT 1000001, STRING_AGG(str, '') FROM ( SELECT chr(40 + (RANDOM() * 1000)::INT % 84 ) AS str FROM GENERATE_SERIES(1, 1000) length) t;
INSERT 0 1
時間: 863.200 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 212649 | 2764 | 209881 | 7224 | 2
2.3. 初期データあり-を利用しない
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 212649 | 2764 | 209881 | 7224 | 2
(1 行)
時間: 63.233 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 212651 | 2764 | 209886 | 772968 | 2
dev=# INSERT INTO sample_table SELECT 1000001, STRING_AGG(str, '') FROM ( SELECT chr(40 + (RANDOM() * 1000)::INT % 84 ) AS str FROM GENERATE_SERIES(1, 1000) length) t;
INSERT 0 1
時間: 827.526 ミリ秒
dev=# SELECT * FROM gin_metapage_info(get_raw_page('sample_table_ix1', 0));
pending_head | pending_tail | tail_free_size | n_pending_pages | n_pending_tuples | n_total_pages | n_entry_pages | n_data_pages | n_entries | version
--------------+--------------+----------------+-----------------+------------------+---------------+---------------+--------------+-----------+---------
4294967295 | 4294967295 | 0 | 0 | 0 | 212651 | 2764 | 209886 | 772968 | 2
おわりに
今回は、GIN高速更新手法の待機リストの整理処理による低速化に着目して検証し、GIN高速更新手法の有効性を実感することができました。
時間の都合もあり現時点における検証はここまでですが、gin_pending_list_limitなどの適切な設定値やデータ量や偏りとの関連などもありそうなので、今後もこのGIN高速更新手法について別な切り口で検証していきたいと思います。