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

问题描述 投票:0回答:2

我正在尝试在 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 数组元素。

sql postgresql indexing set-returning-functions jsonb
2个回答
52
投票

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

但是您可能不想将搜索限制为数组的第一个元素。使用

jsonb
数据类型,您可以获得额外的运算符和索引支持。

在询问时,

jsonb
列没有内置的“大于”或“小于”运算符。随着 Postgres 12 中添加的 SQL/JSON 路径功能的出现,这种情况发生了变化。

您可以为 GIN 索引选择两个运算符类别。 手册

jsonb_ops
@> (jsonb,jsonb)
@? (jsonb,jsonpath)
@@ (jsonb,jsonpath)
? (jsonb,text)
?| (jsonb,text[])
?& (jsonb,text[])

jsonb_path_ops
@> (jsonb,jsonb)
@? (jsonb,jsonpath)
@@ (jsonb,jsonpath)

jsonb_ops
是默认值。)您可以覆盖相等测试,但您对
>=
比较的要求只能通过
jsonpath
运算符来满足。 (旧版本中需要 btree 索引。)

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

基本解决方案

Postgres 12 或更高版本

SELECT l.*
FROM   locations l
WHERE  l.events @? '$[*] ? (@.event_slug == "test_1")
                         ? (@.end_time.datetime() < "2014-10-13".datetime()' 

或者,如果您确实需要“或”两个过滤器(见下文):

SELECT l.*
FROM   locations l
WHERE  l.events @? '$[*] ? (@.event_slug == "test_1")
                         ? (@.start_time.datetime() < "2014-10-13".datetime()  || @.end_time.datetime() < "2014-10-13".datetime())' 

现在这比我对旧版本的原始答案简单得多。

任何 Postgres 版本

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'::timestamptz;

相关:

利用隐式

JOIN LATERAL
。详情(最后一章):

小心不同的数据类型! JSON 值中的内容看起来像

timestamp [without time zone]
,而您的谓词使用
timestamp with time zone
文字。
timestamp
值根据当前 time zone 设置进行解释,而给定的
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);

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

应该让您朝着正确的方向开始。

© www.soinside.com 2019 - 2024. All rights reserved.