web-dev-qa-db-fra.com

Index pour la recherche d'un élément dans un tableau JSON

J'ai une table qui ressemble à ceci:

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"}]');

Plusieurs autres colonnes ne sont pas pertinentes pour cette question. Il y a une raison pour les stocker en tant que JSON.

Ce que j'essaie de faire, c'est de rechercher une piste qui a un nom de l'artiste (correspondance exacte) spécifique.

J'utilise cette requête:

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

par exemple

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

Cependant, cela effectue une analyse complète de la table et ce n'est pas très rapide. J'ai essayé de créer un index GIN en utilisant une fonction names_as_array(artists), et utilisé 'ARTIST NAME' = ANY names_as_array(artists), mais l'index n'est pas utilisé et la requête est considérablement plus lente.

74
JeffS

jsonb dans Postgres 9.4+

Avec le nouveau type de données JSON binaire jsonb , Postgres 9.4 introduit largement amélioré les options d’index . Vous pouvez maintenant avoir un index GIN sur un tableau jsonb directement:

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

Pas besoin d'une fonction pour convertir le tableau. Cela prend en charge une requête:

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

@> étant le nouvel opérateur jsonb "contient" , qui peut utiliser le GIN indice. (Pas pour le type json, seulement jsonb!)

ou vous utilisez la classe d'opérateurs GIN la plus spécialisée, autre que celle par défaut jsonb_path_ops pour l'index:

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

Même requête.

Actuellement, jsonb_path_ops Ne prend en charge que l'opérateur @>. Mais c'est généralement beaucoup plus petit et plus rapide. Il y a plus d'options d'index, détails dans le manuel .


Si artists ne contient que les noms indiqués dans l'exemple, il serait plus efficace de stocker une valeur JSON moins redondante pour commencer: juste le valeurs sous forme de texte primitives et le redondant clé peut être dans le nom de la colonne.

Notez la différence entre les objets JSON et les types primitifs:

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);

Question:

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

? ne fonctionne pas pour l'objet valeurs, seulement keys et éléments de tableau.
Ou (plus efficace si les noms sont répétés souvent):

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

Question:

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

json dans Postgres 9.3+

Cela devrait fonctionner avec une fonction 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)';

Créez cet index fonctionnel ) :

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

Et utilisez une requête comme celle-ci. L'expression dans la clause WHERE doit correspondre à celle de l'index:

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

Mis à jour avec des commentaires dans les commentaires. Nous devons utiliser opérateurs de tableaux pour prendre en charge l'index GIN.
Le "est contenu par" l'opérateur <@ dans ce cas.

Notes sur la volatilité des fonctions

Vous pouvez déclarer votre fonction IMMUTABLE même si json_array_elements() n'est pas n'était pas.
Auparavant, la plupart des fonctions JSON n'étaient que STABLE et non pas IMMUTABLE. Il y a eu une discussion sur la liste des hackers pour changer cela. La plupart sont IMMUTABLE maintenant. Vérifier avec:

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%';

Les index fonctionnels ne fonctionnent qu'avec les fonctions IMMUTABLE.

119
Erwin Brandstetter