Índice para encontrar un elemento en una matriz JSON

84

Tengo una mesa que se parece a esto:

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

Hay varias otras columnas que no son relevantes para esta pregunta. Hay una razón para almacenarlos como JSON.

Lo que estoy tratando de hacer es buscar una pista que tenga un nombre de artista específico (coincidencia exacta).

Estoy usando esta consulta:

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

por ejemplo

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

Sin embargo, esto hace un escaneo completo de la tabla y no es muy rápido. Intenté crear un índice GIN usando una función names_as_array(artists)y lo usé 'ARTIST NAME' = ANY names_as_array(artists), sin embargo, el índice no se usa y la consulta es significativamente más lenta.

JeffS
fuente
Hice una pregunta de seguimiento basada en esta: dba.stackexchange.com/questions/71546/…
Ken Li

Respuestas:

138

jsonb en Postgres 9.4+

Con el nuevo tipo de datos JSON binario jsonb, Postgres 9.4 introdujo opciones de índice ampliamente mejoradas . Ahora puede tener un índice GIN en una jsonbmatriz directamente:

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

No se necesita una función para convertir la matriz. Esto apoyaría una consulta:

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

@>siendo el nuevo jsonboperador "contiene" , que puede utilizar el índice GIN. (¡No para tipo json, solo jsonb!)

O usa la clase de operador GIN más especializada y no predeterminada jsonb_path_opspara el índice:

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

Misma consulta.

Actualmente jsonb_path_opssolo es compatible con el @>operador. Pero normalmente es mucho más pequeño y rápido. Hay más opciones de índice, detalles en el manual .


Si artists solo contiene nombres como se muestra en el ejemplo, sería más eficiente almacenar un valor JSON menos redundante para empezar: solo los valores como primitivas de texto y la clave redundante pueden estar en el nombre de la columna.

Tenga en cuenta la diferencia entre los objetos JSON y los tipos primitivos:

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

Consulta:

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

?no funciona para valores de objeto , solo claves y elementos de matriz .
O (más eficiente si los nombres se repiten con frecuencia):

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

Consulta:

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

json en Postgres 9.3+

Esto debería funcionar con una IMMUTABLE función :

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

Cree este índice funcional :

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

Y usa una consulta como esta. La expresión de la WHEREcláusula debe coincidir con la del índice:

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

Actualizado con retroalimentación en comentarios. Necesitamos usar operadores de matriz para admitir el índice GIN.
El operador "está contenido por"<@ en este caso.

Notas sobre la volatilidad de la función

Puede declarar su función IMMUTABLEincluso si json_array_elements() no lo fue.
La mayoría de las JSONfunciones solían ser únicas STABLE, no IMMUTABLE. Hubo una discusión en la lista de hackers para cambiar eso. La mayoría lo son IMMUTABLEahora. Comprueba con:

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

Los índices funcionales solo funcionan con IMMUTABLEfunciones.

Erwin Brandstetter
fuente
2
Esto no funciona porque la devolución SETOFno se puede usar en un índice. Al eliminar eso, puedo crear el índice, sin embargo, el planificador de consultas no lo usa. Además, tanto json_array_elements como array_agg sonIMMUTABLE
JeffS
2
@Tony: Lo siento, estaba mezclando el nombre de la columna y el nombre de la clave. Arreglado y agregado más.
Erwin Brandstetter
1
@PyWebDesign: las consultas de contención de jsonb generalmente deben coincidir con la misma estructura que el objeto contenedor (por lo que buscar un objeto dentro de una matriz significa que debe consultar utilizando un objeto dentro de una matriz). Existe una excepción especial para los tipos primitivos dentro de una matriz; más detalles aquí: stackoverflow.com/a/29947194/818187
potatosalad
3
@PyWebDesign: Veo ahora, faltaba la capa de matriz en un ejemplo. Fijo. El índice solo se usará en una tabla lo suficientemente grande como para que sea más barato para Postgres que un escaneo secuencial.
Erwin Brandstetter
2
@PyWebDesign: Ejecutar en su sesión SET enable_seqscan = off;(solo con fines de depuración) stackoverflow.com/questions/14554302/… .
Erwin Brandstetter