Индекс для поиска элемента в массиве JSON

85

У меня есть таблица, которая выглядит так:

CREATE TABLE tracks (id SERIAL, artists JSON);

INSERT INTO tracks (id, artists) 
  VALUES (1, '[{"name": "blink-182"}]');

INSERT INTO tracks (id, artists) 
  VALUES (2, '[{"name": "The Dirty Heads"}, {"name": "Louis Richards"}]');

Есть несколько других столбцов, которые не имеют отношения к этому вопросу. Есть причина хранить их как JSON.

Я пытаюсь найти трек с конкретным именем исполнителя (точное совпадение).

Я использую этот запрос:

SELECT * FROM tracks 
  WHERE 'ARTIST NAME' IN
    (SELECT value->>'name' FROM json_array_elements(artists))

например

SELECT * FROM tracks
  WHERE 'The Dirty Heads' IN 
    (SELECT value->>'name' FROM json_array_elements(artists))

Однако при этом выполняется полное сканирование таблицы, и это не очень быстро. Я попытался создать индекс GIN с помощью функции names_as_array(artists)и использовал 'ARTIST NAME' = ANY names_as_array(artists), однако индекс не используется, и запрос на самом деле значительно медленнее.

JeffS
источник
Я сделал дополнительный вопрос на основе этого: dba.stackexchange.com/questions/71546/…
Кен Ли

Ответы:

142

jsonb в Postgres 9.4+

С новым двоичным типом данных JSON в jsonbPostgres 9.4 были значительно улучшены параметры индексации . Теперь вы можете иметь индекс GIN jsonbнепосредственно в массиве:

CREATE TABLE tracks (id serial, artists jsonb);
CREATE INDEX tracks_artists_gin_idx ON tracks USING gin (artists);

Нет необходимости в функции для преобразования массива. Это поддержит запрос:

SELECT * FROM tracks WHERE artists @> '[{"name": "The Dirty Heads"}]';

@>является новым jsonbоператором "содержит" , который может использовать индекс GIN. (Не для типа json, только jsonb!)

Или вы используете jsonb_path_opsдля индекса более специализированный класс операторов GIN, отличный от стандартного:

CREATE INDEX tracks_artists_gin_idx ON tracks
USING  gin (artists jsonb_path_ops);

Тот же запрос.

На данный момент jsonb_path_opsподдерживает только @>оператор. Но обычно он намного меньше и быстрее. Есть еще варианты индекса, подробности в инструкции .


Если artists хранятся только имена, показанные в примере, было бы более эффективно сохранить для начала менее избыточное значение JSON: в имени столбца могут быть только значения в виде текстовых примитивов и избыточный ключ .

Обратите внимание на разницу между объектами JSON и примитивными типами:

CREATE TABLE tracks (id serial, artistnames jsonb);
INSERT INTO tracks  VALUES (2, '["The Dirty Heads", "Louis Richards"]');

CREATE INDEX tracks_artistnames_gin_idx ON tracks USING gin (artistnames);

Запрос:

SELECT * FROM tracks WHERE artistnames ? 'The Dirty Heads';

?не работает для значений объекта , только для ключей и элементов массива .
Или (более эффективно, если имена повторяются часто):

CREATE INDEX tracks_artistnames_gin_idx ON tracks
USING  gin (artistnames jsonb_path_ops);

Запрос:

SELECT * FROM tracks WHERE artistnames @> '"The Dirty Heads"'::jsonb;

json в Postgres 9.3+

Это должно работать с IMMUTABLE функцией :

CREATE OR REPLACE FUNCTION json2arr(_j json, _key text)
  RETURNS text[] LANGUAGE sql IMMUTABLE AS
'SELECT ARRAY(SELECT elem->>_key FROM json_array_elements(_j) elem)';

Создайте этот функциональный индекс :

CREATE INDEX tracks_artists_gin_idx ON tracks
USING  gin (json2arr(artists, 'name'));

И используйте такой запрос . Выражение в WHEREпредложении должно совпадать с выражением в индексе:

SELECT * FROM tracks
WHERE  '{"The Dirty Heads"}'::text[] <@ (json2arr(artists, 'name'));

Обновлено с учетом отзывов в комментариях. Нам нужно использовать операторы массива для поддержки индекса GIN.
В этом случае оператор "содержится в"<@ .

Примечания по изменчивости функций

Вы можете объявить свою функцию, IMMUTABLEдаже если json_array_elements() это не так.
Большинство JSONфункций раньше были только STABLE, а не IMMUTABLE. В списке хакеров было обсуждение, чтобы это изменить. Большинство IMMUTABLEсейчас. Проверить с:

SELECT p.proname, p.provolatile
FROM   pg_proc p
JOIN   pg_namespace n ON n.oid = p.pronamespace
WHERE  n.nspname = 'pg_catalog'
AND    p.proname ~~* '%json%';

Функциональные индексы работают только с IMMUTABLEфункциями.

Эрвин Брандштеттер
источник
2
Это не работает, потому что возврат SETOFнельзя использовать в индексе. Удалив его, я могу создать индекс, однако он не используется планировщиком запросов. Кроме того, json_array_elements и array_aggIMMUTABLE
JeffS
2
@Tony: Извините, я смешивал имя столбца и имя ключа. Исправлено и добавлено еще.
Эрвин Брандштеттер
1
@PyWebDesign: запросы содержания jsonb обычно должны соответствовать той же структуре, что и содержащий объект (поэтому поиск объекта внутри массива означает, что вы должны запросить, используя объект внутри массива). Для примитивных типов внутри массива есть специальное исключение; подробнее здесь: stackoverflow.com/a/29947194/818187
potatosalad
3
@PyWebDesign: теперь я вижу, что в одном примере отсутствовал слой массива. Исправлена. Индекс будет использоваться только в таблице, достаточно большой, чтобы это было дешевле для Postgres, чем последовательное сканирование.
Эрвин Брандштеттер,
2
@PyWebDesign: запустить в сеансе SET enable_seqscan = off;(только для целей отладки) stackoverflow.com/questions/14554302/… .
Эрвин Брандштеттер,