首先,您不能像那样访问 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 page和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);