PostgreSQLでXML(4): [補足] 9.0と8.4のインデックスに関する振る舞いの差
さて、PostgreSQLでXMLを使うシリーズです。
第2回でインデックスがないとPostgreSQLでのXML検索は性能悲惨、という話を書いたのですが、実はこのへんにはさらに深い沼が広がっているようです。
今回は小ネタで、私もまだきちんと把握出来ていない場所なのですが、 PostgreSQL 8.4とPostgreSQL 9.0で振る舞いの違う場所があったので、 第2回の補足、という位置づけです。
なお、今回も引き続き次のようなテーブルを前提とします。
CREATE TABLE testxml ( id SERIAL PRIMARY KEY, xmldoc XML );
コンテンツも引き続き、本サイトのXMLデータ(約1500件)を、 xmldocに突っ込んであります。idは連番です。
PostgreSQL 9.0でのXMLへのB-treeインデックス
第2回では、XML要素へのインデックスは通常配列を対象とするGINインデックスを用いるが、次のようなテクニックを使うことで、B-treeのインデックスを貼ることができ、前方一致検索が可能となる、ということを紹介しました。
CREATE INDEX testxml_title_btree_idx ON testxml USING BTREE (array_to_string(xpath(E'//title/text()'::text, xmldoc)::text[], ' '));
ところが、第2回はPostgreSQL 8.4でテストしていたのですが、このインデックスはPostgreSQL 9.0ではエラーが出て作成できません。
testxml=# CREATE INDEX testxml_title_btree_idx ON testxml testxml-# USING BTREE testxml-# (array_to_string(xpath(E'//title/text()'::text, xmldoc)::text[], ' ')); ERROR: functions in index expression must be marked IMMUTABLE testxml=#
私にはなぜarray_to_string()
がIMMUTABLEでないかが理解出来ないのですが
(ご存じの方、
タイトルのところの「ツイートする」ボタンでつぶやいて教えてくださいませ…)、
エラーが出るのは仕方がありません。
回避するには、次のようなIMUUTABLE指定した関数array_to_string2()
を作り…。
CREATE FUNCTION array_to_string2(text[]) RETURNS text LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT array_to_string($1, ' ') $$
これを使ってインデックスを作ればいいんですけどね。
CREATE INDEX testxml_title_btree_idx ON testxml USING BTREE (array_to_string2(xpath(E'//title/text()'::text, xmldoc)::text[]));
まあ、関数作ってまでやるくらいなら、array_to_string()
を使わずに、配列要素への参照を直接用いて、配列の最初の要素を返す次のような関数array_first()
を使ったほうがシンプルそうですが。
CREATE FUNCTION array_first(text[]) RETURNS text LANGUAGE sql IMMUTABLE STRICT AS $$ SELECT $1[1] $$;
なんと言っても、いくら私が解せなくても、array_to_string()
がIMMUTABLEでないとエラーを返すPostgreSQLの判断にも何らかの理由があるのかも知れないし…。
ちなみに、そもそもなぜ、今までこの例と同じ(xpath('...', ...)::text[])[1]
にインデックスを貼らなかったのかというと、実際には次のようにCREATE INDEXがエラーを返してきて、インデックスが貼れないためです。
何故でしょうね? 関数を作る以外に何かスマートな回避の方法をご存じの方、教えてください。
testxml=# CREATE INDEX testxml_title_btree2_idx ON testxml testxml-# USING BTREE testxml-# ((xpath(E'//title/text()'::text, xmldoc)::text[])[1]); ERROR: syntax error at or near "[" 行 3: ((xpath(E'//title/text()'::text, xmldoc)::text[])[1]); ^
まあとりあえず、このarray_first()
を使えば普通にインデックスを作ることができます。めでたしめでたし。
testxml=# CREATE INDEX testxml_title_btree2_idx ON testxml testxml-# USING BTREE testxml-# (array_first(xpath(E'//title/text()'::text, xmldoc)::text[])); CREATE INDEX
もちろん、検索の際もarray_first()
を使ってあげないとインデックスは適用されませんよ。こんな感じに。B-treeなので、LIKEの前方一致検索もできます。
SELECT id, array_first(xpath(E'//title/text()'::text, xmldoc)::text[]) FROM testxml WHERE array_first(xpath(E'//title/text()'::text, xmldoc)::text[]) LIKE '謎%'
念のためEXPLAIN ANALYZEしてみましょうか。きちんと、testxml_title_btree2_idx
というインデックスにBitmap Index Scanを行っていることが確認できますね。
testxml=# SELECT id, testxml-# array_first(xpath(E'//title/text()'::text, xmldoc)::text[]) testxml-# FROM testxml testxml-# WHERE array_first(xpath(E'//title/text()'::text, xmldoc)::text[]) testxml-# LIKE '謎%'; id | array_first ------+------------------------------------ 309 | 謎の赤い虫(?) 559 | 謎肉ヌードル確保 1221 | 謎肉ヌードル「ミートキング」登場! (3 行) testxml=# EXPLAIN ANALYZE SELECT id, testxml-# array_first(xpath(E'//title/text()'::text, xmldoc)::text[]) testxml-# FROM testxml testxml-# WHERE array_first(xpath(E'//title/text()'::text, xmldoc)::text[]) testxml-# LIKE '謎%'; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ Bitmap Heap Scan on testxml (cost=4.34..31.90 rows=8 width=36) (actual time=0.784..1.835 rows=3 loops=1) Filter: (((xpath('//title/text()'::text, xmldoc, '{}'::text[]))::text[])[1] ~~ '謎%'::text) -> Bitmap Index Scan on testxml_title_btree2_idx (cost=0.00..4.34 rows=8 width=0) (actual time=0.040..0.040 rows=3 loops=1) Index Cond: ((((xpath('//title/text()'::text, xmldoc, '{}'::text[]))::text[])[1] >= '謎'::text) AND (((xpath('//title/text()'::text, xmldoc, '{}'::text[]))::text[])[1] < '謏'::text)) Total runtime: 1.954 ms (5 行)
では、次回からもさらにインデックスに関する話を続ける予定です。