web-dev-qa-db-ja.com

Postgres jsonbの配列の構造をクエリするための適切なインデックスは何ですか?

Postgres9.4のPostgresjsonbフィールドに次のような値を保持することを実験しています。

[{"event_slug":"test_1","start_time":"2014-10-08","end_time":"2014-10-12"},
 {"event_slug":"test_2","start_time":"2013-06-24","end_time":"2013-07-02"},
 {"event_slug":"test_3","start_time":"2014-03-26","end_time":"2014-03-30"}]

私は次のようなクエリを実行しています:

SELECT * FROM locations
WHERE EXISTS (
  SELECT 1 FROM jsonb_array_elements(events) AS e
  WHERE (
    e->>'event_slug' = 'test_1' AND
    (
      e->>'start_time' >= '2014-10-30 14:04:06 -0400' OR
      e->>'end_time' >= '2014-10-30 14:04:06 -0400'
    )
  )
)

上記のようなクエリを利用するために、そのデータにインデックスを作成するにはどうすればよいですか?これは、それぞれがその列に最大10個のイベントを含む数百万行に対して妥当な設計に聞こえますか?

私はまだ次のコマンドで順次スキャンを取得しているようです。

CREATE INDEX events_gin_idx ON some_table USING GIN (events);

私が推測しているのは、クエリで最初に行うことは、データをjson配列要素に変換することだからです。

19
Tony

まず第一に、そのようなJSON配列値にアクセスすることはできません。特定のjson値に対して

_[{"event_slug":"test_1","start_time":"2014-10-08","end_time":"2014-10-12"},
 {"event_slug":"test_2","start_time":"2013-06-24","end_time":"2013-07-02"},
 {"event_slug":"test_3","start_time":"2014-03-26","end_time":"2014-03-30"}]
_

最初の配列要素に対する有効なテストは次のとおりです。

_WHERE e->0->>'event_slug' = 'test_1'_

ただし、検索を配列の最初の要素に限定したくない場合があります。 Postgres9.4のjsonbデータ型では、追加の演算子とインデックスのサポートがあります。配列の要素にインデックスを付けるには、GINインデックスが必要です。

GINインデックスの組み込み演算子クラスは、「より大きい」または「より小さい」演算子をサポートしていません。 _> >= < <=_。これはjsonbにも当てはまり、2つの演算子クラスから選択できます。 ドキュメントごと

_Name             Indexed Data Type  Indexable Operators
...
jsonb_ops        jsonb              ? ?& ?| @>
jsonb_path_ops   jsonb              @>
_

(_jsonb_ops_がデフォルトです。)同等性テストをカバーできますが、これらの演算子はどちらも_>=_比較の要件をカバーしません。 btreeインデックスが必要になります。

基本的な解決策

インデックスを使用した等価性チェックをサポートするには:

_CREATE INDEX locations_events_gin_idx ON locations
USING gin (events jsonb_path_ops);

SELECT * FROM locations WHERE events @> '[{"event_slug":"test_1"}]';
_

フィルタが十分に選択的である場合、これで十分かもしれません。
_end_time >= start_time_と仮定すると、2つのチェックは必要ありません。 _end_time_のみをチェックする方が安価で同等です。

_SELECT l.*
FROM   locations l
     , jsonb_array_elements(l.events) e
WHERE  l.events @> '[{"event_slug":"test_1"}]'
AND   (e->>'end_time')::timestamp >= '2014-10-30 14:04:06 -0400'::timestamptz;
_

暗黙の_JOIN LATERAL_を利用します。詳細(最終章):

さまざまなデータ型に注意してください! JSON値にあるものは_timestamp [without time zone]_のように見えますが、述語は_timestamp with time zone_リテラルを使用します。 timestamp値は、現在のタイムゾーン設定に従って解釈されますが、指定されたtimestamptzリテラルはtimestamptzを明示的に指定しないと、タイムゾーンが無視されます。上記のクエリは、必要に応じて機能するはずです。詳細な説明:

jsonb_array_elements()の詳細:

高度なソリューション

上記が十分でない場合は、関連する属性を正規化された形式で格納する _MATERIALIZED VIEW_ を検討します。これにより、プレーンなbtreeインデックスが可能になります。

このコードは、JSON値が質問に表示されている一貫した形式であることを前提としています。

セットアップ:

_CREATE TYPE event_type AS (
 , event_slug  text
 , start_time  timestamp
 , end_time    timestamp
);

CREATE MATERIALIZED VIEW loc_event AS
SELECT l.location_id, e.event_slug, e.end_time  -- start_time not needed
FROM   locations l, jsonb_populate_recordset(null::event_type, l.events) e;
_

jsonb_populate_recordset()の関連回答:

_CREATE INDEX loc_event_idx ON loc_event (event_slug, end_time, location_id);
_

インデックスのみのスキャンを許可するために_location_id_も含めます。 ( マニュアルページ および Postgres Wiki を参照してください。)

クエリ:

_SELECT *
FROM   loc_event
WHERE  event_slug = 'test_1'
AND    end_time  >= '2014-10-30 14:04:06 -0400'::timestamptz;
_

または、基になるlocationsテーブルの完全な行が必要な場合:

_SELECT l.*
FROM  (
   SELECT DISTINCT location_id
   FROM   loc_event
   WHERE  event_slug = 'test_1'
   AND    end_time  >= '2014-10-30 14:04:06 -0400'::timestamptz
   ) le
JOIN locations l USING (location_id);
_
41
CREATE INDEX json_array_elements_index ON
    json_array_elements ((events_arr->>'event_slug'));

あなたが正しい方向に始める必要があります。

0
Momer