在Postgres jsonb中查询数组结构的正确索引是什么?

时间:2014-10-22 02:04:31

标签: sql postgresql indexing set-returning-functions jsonb

我尝试在Postgres 9.4的Postgres jsonb字段中保留以下值:

[{"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数组元素。

2 个答案:

答案 0 :(得分:39)

首先,您不能像这样访问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'

但您可能不希望将搜索限制为数组的第一个元素。使用Postgres 9.4中的jsonb数据类型,您可以获得其他运算符和索引支持。要索引数组的元素,您需要一个GIN索引。

GIN索引的内置运算符类不支持“大于”或“小于”运算符 > >= < <= 。这也适用于jsonb,您可以在两个运算符类之间进行选择。 Per documentation

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,我们不需要两次检查。仅检查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以允许仅索引扫描。 (请参阅manual pagePostgres 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);

答案 1 :(得分:0)

CREATE INDEX json_array_elements_index ON
    json_array_elements ((events_arr->>'event_slug'));

应该让你开始朝着正确的方向前进。