Індекс для пошуку елемента в масиві JSON


84

У мене є таблиця, яка виглядає так:

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


Я зробив наступне запитання на основі цього: dba.stackexchange.com/questions/71546/…
Кен Лі

Відповіді:


138

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_agg єIMMUTABLE
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/… .
Ервін Брандштеттер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.