First of all, you cannot access JSON array values like that. For a given json value:
[{"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"}]
A valid test against the first array element would be:
WHERE e->0->>'event_slug' = 'test_1'
But you probably don't want to limit your search to the first element of the array. With the jsonb
data type you have additional operators and index support.
At the time of asking, there was no built-in "greater than" or "less than" operator for jsonb
columns. This changed with added SQL/JSON path functionality in Postgres 12.
You can choose between two operator classes for your GIN index. The manual:
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
being the default.) You can cover the equality test, but your requirement for >=
comparison is only met with a jsonpath
operator. (You need a btree index in older versions.)
CREATE INDEX locations_events_gin_idx ON locations
USING gin (events jsonb_path_ops);
Basic solution
Postgres 12 or later
SELECT l.*
FROM locations l
WHERE l.events @? '$[*] ? (@.event_slug == "test_1")
? (@.end_time.datetime() < "2014-10-13".datetime()'
Or, if you really need to "OR" two filters (see below):
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())'
This is much simpler now than my original answer for older versions.
Any Postgres version
SELECT * FROM locations WHERE events @> '[{"event_slug":"test_1"}]';
This might be good enough if the filter is selective enough.
Assuming end_time >= start_time
, so we don't need two checks. Checking only end_time
is cheaper and equivalent:
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;
Related:
Utilizing an implicit JOIN LATERAL
. Details (last chapter):
Careful with the different data types! What you have in the JSON value looks like timestamp [without time zone]
, while your predicates use timestamp with time zone
literals. The timestamp
value is interpreted according to the current time zone setting, while the given timestamptz
literals must be cast to timestamptz
explicitly or the time zone would be ignored! Above query should work as desired. Detailed explanation:
More explanation for jsonb_array_elements()
:
Advanced solution
If the above is not good enough, I would consider a MATERIALIZED VIEW
that stores relevant attributes in normalized form. This allows plain btree indexes.
The code assumes that your JSON values have a consistent format as displayed in the question.
Setup:
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;
Related answer for jsonb_populate_recordset()
:
CREATE INDEX loc_event_idx ON loc_event (event_slug, end_time, location_id);
Also including location_id
to allow index-only scans. (See manual page and Postgres Wiki.)
Query:
SELECT *
FROM loc_event
WHERE event_slug = 'test_1'
AND end_time >= '2014-10-30 14:04:06 -0400'::timestamptz;
Or, if you need full rows from the underlying locations
table:
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);
e
as column name, in the index we seeevents
. Please add a table definition (CREATE TABLE
script) to avoid confusion. And your version of Postgres. You taggedjsonb
, but speak of "Postgres json". Again, the table definition would clarify. – Elfriedeelfstan