PostgreSQLは追記型の構造を採用しているということが、その特徴として有名です。これはデータが更新・削除される場合に古いデータを残し、新しいデータを(空き領域の)末尾に追加するという手法です。この構造自体はトランザクションの仕組みのためのものですが、行を更新した場合でも行が追加されたのと同じことになるという点で、(全文検索)インデックスを作る際にも影響がでてきます。
前回の記事では、全文検索インデックスは「単語 → ctid」の対応表だと説明しました。ctidは行の物理的な位置をあらわす値です。追記型の構造が採用されていることによって、既存の行がUPDATEされればctidの値も変わることになります。
このことを簡単に実験して確認してみます。まず、次のようにしてテーブルを作成し、行のctidの値を確認します(もし前回の記事で作成したテスト用テーブルが残っていたら、まずそれを削除してください)。
# CREATE TABLE test (txt text);
|
CREATE TABLE
|
# CREATE INDEX idx ON test USING fulltext (txt);
|
CREATE INDEX
|
# INSERT INTO test VALUES ('hello, world');
|
INSERT 0 1
|
# INSERT INTO test VALUES ('goodby, world');
|
INSERT 0 1
|
# SELECT txt, ctid FROM test;
|
txt |
| |
ctid |
------------------------------- |
+ |
---------------- |
hello, world |
| |
(0,1) |
goodby, world |
| |
(0,2) |
(2 rows)
|
ctidが「0,1」「0,2」という値になっていることがわかります。ここでまず全文検索を実行してみると、この2行が結果として得られ、pgs2getnhits関数で得られる値も2です。
# SELECT * FROM test WHERE txt @@ 'world';
txt
---------------
hello, world
goodby, world
(2 rows)
# SELECT pgs2getnhits();
pgs2getnhits
--------------
2
次に、この1行目の値を更新すると、ctidの値が「0,1」から「0,3」にかわっていることがわかります。
# UPDATE test SET txt = 'new world' WHERE txt = 'hello, world';
|
UPDATE 1
|
# SELECT txt, ctid FROM test;
|
txt |
| |
ctid |
------------------------------------------------ |
+ |
--------------- |
goodby, world |
| |
(0,2) |
new world |
| |
(0,3) |
(2 rows)
|
もう一度同じ検索を行ってみると、得られる結果は2行のままなのに、pgs2getnhits関数で得られる値が3に変化しています。
# SELECT * FROM test WHERE txt @@ 'world';
txt
---------------
new world
goodby, world
(2 rows)
# SELECT pgs2getnhits();
pgs2getnhits
--------------
3
(1 row)
これは、UPDATEが実行されたときに、インデックス側ではINSERTが実行されたときとまったく同じ、行の追加の処理しか行われないからです。
上記の検索の場合、全文検索インデックスからは「0,1」「0,2」「0,3」という3つの行のctidが返され、PostgreSQL側で(0,1)はすでに更新によって無効になっているということをチェックして取り除くため、正しい結果が得られます。この「ctidが無効かどうかチェックする」という処理が必要なことが、PostgreSQLがインデックス内を検索するだけでは結果数を求めることができず、countの処理に時間がかかってしまうという原因にもなっています。
pgs2getnhits関数で取得される値はこのチェックが行われる前のものなので、無効になっている行のctidが数に入ってしまっているということです。実は無効になっている行のctidは、はじめに説明した検索ヒット数の上限の設定でも数に入ってきてしまいます。結局これらの誤差や無駄をなくすためには、ある程度更新が行われたらREINDEXを行うという運用が重要になるということです(B-Treeインデックスなどでは、VACUUMを実行することによってインデックス内の不要な行が削除されますが、Ludia 0.9.0では対応していません。これは今後のバージョンで対応予定です)。
|