web-dev-qa-db-ja.com

postgres JSONインデックスは、従来の正規化テーブルと比較して十分効率的ですか?

現在のPostgresqlバージョンではJSONコンテンツにさまざまな機能が導入されていますが、実際に使用する必要があるかどうか心配です。つまり、機能するものと機能しないものについて確立された「ベストプラクティス」がないか、少なくとも私はできません。それを見つける。

特定の例があります-特にオブジェクトに関する代替名のリストを含むオブジェクトに関するテーブルがあります。そのすべてのデータは、検索のためにJSON列にも含まれます。たとえば、(他のすべての無関係なフィールドをスキップします)。

create table stuff (id serial primary key, data json);
insert into stuff(data) values('{"AltNames":["Name1","Name2","Name3"]}')

「altnameの1つが 'foobar'であるすべてのオブジェクトをリストする」という形式のクエリが必要です。予想されるテーブルサイズは、数百万レコード程度です。そのためにPostgres JSONクエリを使用でき、インデックスを付けることもできます(たとえば、 JSON配列で要素を見つけるためのインデックス )。しかし、それはそのように行われるべきですか、それとも推奨されない不正な回避策ですか?

もちろん、古典的な代替手段は、メインテーブルへの名前と外部キーを含む、その1対多のリレーションのテーブルを追加することです。そのパフォーマンスはよく理解されています。ただし、これには、そのテーブルとJSONの間のデータの重複(整合性リスクの可能性あり)のいずれかを意味するため、それ自体に欠点があります。または、JSONを作成すると、要求ごとに動的にデータが動的に返されます。これにより、パフォーマンスが低下します。

34
Peteris

「altnameの1つが 'foobar'であるすべてのオブジェクトをリストする」という形式のクエリが必要です。予想されるテーブルサイズは、数百万レコード程度です。そのためにPostgres JSONクエリを使用でき、インデックスを付けることもできます(たとえば、JSON配列のFinding Elementのインデックス)。しかし、それはそのように行われるべきですか、それとも推奨されない不正な回避策ですか?

それはcanのように行われますが、そうする必要があるという意味ではありません。ある意味で、ベストプラクティスは、すべての目的と実用的な目的(検証と構文以外)で新しいデータ型を使用して既に十分に文書化されています(たとえば、hstoreの使用vs XMLの使用vs EAVの使用vs別のテーブルの使用を参照)。以前の非構造化または半構造化オプションから。

別の言い方をすると、それは新しい化粧をした同じ古い豚です。

JSONは、hstore、配列型、およびtsvectorと同じように、逆検索ツリーインデックスを使用する機能を提供します。これらは正常に動作しますが、主に辞書式の順序で値のリストを抽出するためではなく、距離によって順序付けられた(ジオメトリタイプと考える)近隣のポイントを抽出するために設計されていることに注意してください。

例として、Romanの回答で概説されている2つの計画を取り上げます。

  • インデックススキャンを実行するものは、ディスクページを直接通過し、インデックスで示された順序で行を取得します。
  • ビットマップインデックススキャンを実行するものは、行を含む可能性のあるすべてのディスクページを特定することから始まり、ディスクに表示されたとおりに、まるでそれが実際に無駄な領域をスキップするシーケンススキャン。

質問に戻る:乱雑でサイズが大きい逆ツリーインデックスは、巨大なJSONストアとしてPostgresテーブルを使用する場合、実際にアプリのパフォーマンスを向上させます。しかし、それらも特効薬ではなく、ボトルネックに対処する場合、適切なリレーショナルデザインの範囲まで到達できません。

要するに、最終的には、hstoreまたはEAVを使用することを決定したときに得られるものと何の違いもありません。

  1. インデックスが必要な場合(つまり、where句に頻繁に出現する場合や、さらに重要なのは結合句に出現する場合)、データを別のフィールドに配置する必要があります。
  2. それが主に表面的なものである場合、JSON/hstore/EAV/XML/whatever-makes-you-sleep-at-nightは問題なく機能します。
25

試してみる価値があると思います。私はいくつかのテスト(100000レコード、JSON配列の最大10要素)を作成し、それがどのように機能するかを確認しました。

create table test1 (id serial primary key, data json);
create table test1_altnames (id int, name text);

create or replace function array_from_json(_j json)
returns text[] as
$func$
    select array_agg(x.elem::text)
    from json_array_elements(_j) as x(elem)
$func$
language sql immutable;

with cte as (
    select
        (random() * 100000)::int as grp, (random() * 1000000)::int as name
    from generate_series(1, 1000000)
), cte2 as (
    select
        array_agg(Name) as "AltNames"
    from cte
    group by grp
)
insert into test1 (data)
select row_to_json(t)
from cte2 as t

insert into test1_altnames (id, name)
select id, json_array_elements(data->'AltNames')::text
from test1

create index ix_test1 on test1 using gin(array_from_json(data->'AltNames'));
create index ix_test1_altnames on test1_altnames (name);

クエリJSON(ms私のマシン上):

select * from test1 where '{489147}' <@ array_from_json(data->'AltNames');

"Bitmap Heap Scan on test1  (cost=224.13..1551.41 rows=500 width=36)"
"  Recheck Cond: ('{489147}'::text[] <@ array_from_json((data -> 'AltNames'::text)))"
"  ->  Bitmap Index Scan on ix_test1  (cost=0.00..224.00 rows=500 width=0)"
"        Index Cond: ('{489147}'::text[] <@ array_from_json((data -> 'AltNames'::text)))"

名前付きのクエリテーブル(私のマシンでは15ms):

select * from test1 as t where t.id in (select tt.id from test1_altnames as tt where tt.name = '489147');

"Nested Loop  (cost=12.76..20.80 rows=2 width=36)"
"  ->  HashAggregate  (cost=12.46..12.47 rows=1 width=4)"
"        ->  Index Scan using ix_test1_altnames on test1_altnames tt  (cost=0.42..12.46 rows=2 width=4)"
"              Index Cond: (name = '489147'::text)"
"  ->  Index Scan using test1_pkey on test1 t  (cost=0.29..8.31 rows=1 width=36)"
"        Index Cond: (id = tt.id)"

また、名前を付けてテーブルに行を挿入/削除するにはいくつかのコストがかかります(test1_altnames)なので、行を選択するよりも少し複雑です。個人的には、JSONによるソリューションが好きです。

20
Roman Pekar