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

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

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), однако индекс не используется, и запрос на самом деле значительно медленнее.


person JeffS    schedule 23.08.2013    source источник
comment
Я сделал дополнительный вопрос на основе этого: dba.stackexchange.com/questions/71546/   -  person Ken Li    schedule 15.07.2014


Ответы (1)


jsonb в Postgres 9.4+

Бинарный тип данных JSON jsonb значительно улучшает параметры индекса. Теперь вы можете иметь индекс 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!)

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

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 функциями.

person Erwin Brandstetter    schedule 23.08.2013
comment
Это не работает, потому что возврат SETOF нельзя использовать в индексе. Удалив это, я могу создать индекс, однако он не используется планировщиком запросов. Кроме того, как json_array_elements, так и array_agg IMMUTABLE - person JeffS; 23.08.2013
comment
@ErwinBrandstetter. Это по-прежнему лучший способ выйти с выпуском Postgres 9.4? - person Tony; 30.12.2014
comment
@Tony: Нет, теперь есть способы получше. Я бы использовал jsonb и собственный индекс GIN в стр. 9.4. Добавил главу к ответу. - person Erwin Brandstetter; 30.12.2014
comment
@ErwinBrandstetter Как индекс узнает, что имя вложено в массив? Я пытаюсь сделать что-то подобное здесь: stackoverflow.com/questions/26499266/, но не могу заставить мои запросы использовать индекс - person Tony; 30.12.2014
comment
@Tony: Извините, я смешивал имя столбца и имя ключа. Исправлено и добавлено еще. - person Erwin Brandstetter; 30.12.2014
comment
Как ни странно, мне нужно добавить [], чтобы он работал, И он никогда не использует индекс, я бы хотел узнать, почему! SELECT * FROM tracks WHERE artists @> '[{"name": "The Dirty Heads"}]'; - person PyWebDesign; 23.04.2015
comment
@PyWebDesign: запросы содержания jsonb обычно должны соответствовать той же структуре, что и содержащий объект (поэтому поиск объекта внутри массива означает, что вы должны запросить, используя объект внутри массива). Для примитивных типов внутри массива есть специальное исключение; подробнее здесь: stackoverflow.com/a/29947194/818187 - person potatosalad; 29.04.2015
comment
@PyWebDesign: теперь я вижу, что в одном примере отсутствовал слой массива. Фиксированный. Индекс будет использоваться только в таблице, достаточно большой, чтобы это было дешевле для Postgres, чем последовательное сканирование. - person Erwin Brandstetter; 29.04.2015
comment
@ErwinBrandstetter, спасибо, это именно то, что я искал, вы знаете, могу ли я заставить его использовать индекс, может быть, один раз, чтобы я мог подтвердить, что он работает. - person PyWebDesign; 30.04.2015
comment
@PyWebDesign: запускать в сеансе SET enable_seqscan = off; (только для целей отладки) stackoverflow.com/questions/14554302/. - person Erwin Brandstetter; 30.04.2015