На първо място, не можете да получите достъп до стойности на 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
тип данни в Postgres 9.4 имате допълнителни оператори и поддръжка на индекси. За да индексирате елементи от масив, имате нужда от GIN индекс.
Вградените операторни класове за GIN индекси не поддържат оператори "по-голямо от" или "по-малко от" . Това е вярно за > >= < <=
jsonb
също така, където можете да избирате между два операторски класа. По документация:
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
. Подробности (последна глава):
- PostgreSQL unnest() с номер на елемент
Внимавайте с различнитетипове данни ! Това, което имате в стойността на JSON, изглежда като timestamp [without time zone]
, докато вашите предикати използват timestamp with time zone
литерали. timestamp
стойността се интерпретира според текущата часова зона настройка, докато даденият timestamptz
литералите трябва да се прехвърлят към timestamptz
изрично или часовата зона ще бъде игнорирана! Горната заявка трябва да работи по желание. Подробно обяснение:
- Изцяло игнориране на часовите зони в Rails и PostgreSQL
Още обяснение за jsonb_array_elements()
:
- Присъединяване към PostgreSQL чрез JSONB
Разширено решение
Ако горното не е достатъчно добро, бих помислил за 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()
:
- Как да конвертирам jsonb типа на PostgreSQL 9.4 в float
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);