Index zum Suchen eines Elements in einem JSON-Array

84

Ich habe einen Tisch, der so aussieht:

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

Es gibt mehrere andere Spalten, die für diese Frage nicht relevant sind. Es gibt einen Grund, sie als JSON zu speichern.

Ich versuche, einen Titel nachzuschlagen, der einen bestimmten Künstlernamen hat (genaue Übereinstimmung).

Ich benutze diese Abfrage:

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

zum Beispiel

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

Dies führt jedoch einen vollständigen Tabellenscan durch und ist nicht sehr schnell. Ich habe versucht, einen GIN-Index mit einer Funktion zu erstellen names_as_array(artists), und verwendet 'ARTIST NAME' = ANY names_as_array(artists), aber der Index wird nicht verwendet und die Abfrage ist tatsächlich erheblich langsamer.

JeffS
quelle
Ich habe eine Folgefrage basierend auf dieser gestellt: dba.stackexchange.com/questions/71546/…
Ken Li

Antworten:

138

jsonb in Postgres 9.4+

Mit dem neuen binären JSON-Datentyp jsonbführte Postgres 9.4 stark verbesserte Indexoptionen ein . Sie können jetzt jsonbdirekt einen GIN-Index für ein Array erstellen:

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

Zum Konvertieren des Arrays ist keine Funktion erforderlich. Dies würde eine Abfrage unterstützen:

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

@>Dies ist der neue jsonbOperator "enthält" , der den GIN-Index verwenden kann. (Nicht für den Typ json, nur jsonb!)

Oder Sie verwenden die speziellere, nicht standardmäßige GIN-Operatorklasse jsonb_path_opsfür den Index:

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

Gleiche Abfrage.

jsonb_path_opsUnterstützt derzeit nur den @>Operator. Aber es ist normalerweise viel kleiner und schneller. Es gibt weitere Indexoptionen, Details im Handbuch .


Wenn artists nur Namen enthalten sind, wie im Beispiel gezeigt, wäre es effizienter, zunächst einen weniger redundanten JSON-Wert zu speichern: Nur die Werte als Textprimitive und der redundante Schlüssel können im Spaltennamen enthalten sein.

Beachten Sie den Unterschied zwischen JSON-Objekten und primitiven Typen:

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

Abfrage:

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

?funktioniert nicht für die Objektwerte , nur Schlüssel und Array - Elemente .
Oder (effizienter, wenn Namen häufig wiederholt werden):

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

Abfrage:

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

json in Postgres 9.3+

Dies sollte mit einer IMMUTABLE Funktion funktionieren :

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

Erstellen Sie diesen Funktionsindex :

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

Und verwenden Sie eine solche Abfrage . Der Ausdruck in der WHEREKlausel muss mit dem im Index übereinstimmen:

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

Aktualisiert mit Feedback in Kommentaren. Wir müssen Array-Operatoren verwenden , um den GIN-Index zu unterstützen.
Der Operator "ist enthalten von" ist<@ in diesem Fall.

Hinweise zur Funktionsvolatilität

Sie können Ihre Funktion IMMUTABLEauch dann deklarieren , wenn dies json_array_elements() nicht der Fall ist.
Die meisten JSONFunktionen waren früher nur STABLE, nicht IMMUTABLE. Es gab eine Diskussion auf der Hackerliste, um das zu ändern. Die meisten sind IMMUTABLEjetzt. Überprüfen Sie mit:

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

Funktionsindizes funktionieren nur mit IMMUTABLEFunktionen.

Erwin Brandstetter
quelle
2
Dies funktioniert nicht, da die Rückgabe SETOFnicht in einem Index verwendet werden kann. Wenn ich das entferne, kann ich den Index erstellen, er wird jedoch vom Abfrageplaner nicht verwendet. Auch sind sowohl json_array_elements als auch array_aggIMMUTABLE
JeffS
2
@ Tony: Entschuldigung, ich habe den Spaltennamen und den Schlüsselnamen gemischt. Behoben und mehr hinzugefügt.
Erwin Brandstetter
1
@PyWebDesign: jsonb-Containment-Abfragen müssen im Allgemeinen mit der gleichen Struktur wie das enthaltende Objekt übereinstimmen (die Suche nach einem Objekt in einem Array bedeutet also, dass Sie mithilfe eines Objekts in einem Array abfragen müssen). Es gibt eine spezielle Ausnahme für primitive Typen innerhalb eines Arrays. Weitere Details hier: stackoverflow.com/a/29947194/818187
Potatosalad
3
@ PyWebDesign: Ich sehe jetzt, dass die Array-Ebene in einem Beispiel fehlte. Fest. Der Index wird nur in einer Tabelle verwendet, die groß genug ist, damit er für Postgres billiger ist als ein sequentieller Scan.
Erwin Brandstetter
2
@PyWebDesign: In Ihrer Sitzung ausführen SET enable_seqscan = off;(nur zu Debugging-Zwecken) stackoverflow.com/questions/14554302/… .
Erwin Brandstetter