JSON steht für JavaScript Object Notation. Es ist ein offenes Standardformat, das Daten in Schlüssel/Wert-Paaren und Arrays organisiert, die in RFC 7159 beschrieben sind. JSON ist das gängigste Format, das von Webdiensten zum Austausch von Daten, zum Speichern von Dokumenten, unstrukturierten Daten usw. verwendet wird. In diesem Beitrag gehen wir vor um Ihnen Tipps und Techniken zum effektiven Speichern und Indizieren von JSON-Daten in PostgreSQL zu zeigen.
Sie können auch unser Webinar Arbeiten mit JSON-Daten in PostgreSQL vs. MongoDB in Partnerschaft mit PostgresConf besuchen, um mehr zu diesem Thema zu erfahren, und unsere SlideShare-Seite besuchen um die Folien herunterzuladen.
Warum JSON in PostgreSQL speichern?
Warum sollte sich eine relationale Datenbank überhaupt um unstrukturierte Daten kümmern? Es stellt sich heraus, dass es einige Szenarien gibt, in denen es nützlich ist.
-
Schemaflexibilität
Einer der Hauptgründe für das Speichern von Daten im JSON-Format ist die Flexibilität des Schemas. Das Speichern Ihrer Daten in JSON ist nützlich, wenn Ihr Schema fließend ist und sich häufig ändert. Wenn Sie jeden der Schlüssel als Spalten speichern, führt dies zu häufigen DML-Vorgängen – dies kann schwierig sein, wenn Ihr Datensatz groß ist – z. B. Ereignisverfolgung, Analysen, Tags usw. Hinweis:Wenn ein bestimmter Schlüssel immer vorhanden ist in Ihrem Dokument kann es sinnvoll sein, es als erstklassige Spalte zu speichern. Wir diskutieren mehr über diesen Ansatz im Abschnitt „JSON-Patterns &Antipatterns“ weiter unten.
-
Verschachtelte Objekte
Wenn Ihr Datensatz verschachtelte Objekte (ein- oder mehrstufig) enthält, ist es in einigen Fällen einfacher, sie in JSON zu handhaben, anstatt die Daten in Spalten oder mehrere Tabellen zu denormalisieren.
-
Synchronisierung mit externen Datenquellen
Häufig stellt ein externes System Daten als JSON bereit, sodass es sich möglicherweise um einen temporären Speicher handelt, bevor Daten in andere Teile des Systems aufgenommen werden. Zum Beispiel Stripe-Transaktionen.
Zeitachse der JSON-Unterstützung in PostgreSQL
JSON-Unterstützung in PostgreSQL wurde in 9.2 eingeführt und hat sich mit jeder zukünftigen Version stetig verbessert.
-
Welle 1:PostgreSQL 9.2 (2012) hat Unterstützung für den JSON-Datentyp hinzugefügt
Die JSON-Datenbank in 9.2 war ziemlich begrenzt (und wahrscheinlich zu diesem Zeitpunkt überbewertet) – im Grunde eine verherrlichte Zeichenfolge mit einer eingefügten JSON-Validierung. Es ist nützlich, eingehendes JSON zu validieren und in der Datenbank zu speichern. Weitere Einzelheiten finden Sie unten.
-
Welle 2:PostgreSQL 9.4 (2014) hat Unterstützung für den JSONB-Datentyp hinzugefügt
JSONB steht für „JSON Binary“ oder „JSON better“, je nachdem, wen Sie fragen. Es ist ein zerlegtes Binärformat zum Speichern von JSON. JSONB unterstützt die Indizierung der JSON-Daten und ist sehr effizient beim Analysieren und Abfragen der JSON-Daten. Wenn Sie mit JSON in PostgreSQL arbeiten, sollten Sie in den meisten Fällen JSONB verwenden.
-
Welle 3:PostgreSQL 12 (2019) hat Unterstützung für SQL/JSON-Standard- und JSONPATH-Abfragen hinzugefügt
JSONPath bringt eine leistungsstarke JSON-Abfrage-Engine in PostgreSQL.
Wann sollten Sie JSON im Vergleich zu JSONB verwenden?
In den meisten Fällen sollten Sie JSONB verwenden. Es gibt jedoch einige spezifische Fälle, in denen JSON besser funktioniert:
- JSON behält die ursprüngliche Formatierung (auch bekannt als Leerzeichen) und die Reihenfolge der Schlüssel bei.
- JSON behält doppelte Schlüssel bei.
- JSON ist schneller zu erfassen als JSONB – wenn Sie jedoch weiterverarbeiten, ist JSONB schneller.
Wenn Sie beispielsweise nur JSON-Protokolle aufnehmen und sie in keiner Weise abfragen, ist JSON möglicherweise die bessere Option für Sie. Wenn wir uns in diesem Blog auf die JSON-Unterstützung in PostgreSQL beziehen, beziehen wir uns in Zukunft auf JSONB.
Verwenden von JSONB in PostgreSQL:Effektives Speichern und Indexieren von JSON-Daten in PostgreSQLClick To TweetJSONB-Muster und Antimuster
Wenn PostgreSQL JSONB hervorragend unterstützt, warum brauchen wir dann noch Spalten? Warum erstellen Sie nicht einfach eine Tabelle mit einem JSONB-Blob und entfernen alle Spalten wie im folgenden Schema:
CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));
Letztendlich sind Spalten immer noch die effizienteste Technik, um mit Ihren Daten zu arbeiten. JSONB-Speicher hat einige Nachteile gegenüber herkömmlichen Spalten:
-
PostreSQL speichert keine Spaltenstatistiken für JSONB-Spalten
PostgreSQL verwaltet Statistiken über die Verteilungen von Werten in jeder Spalte der Tabelle – häufigste Werte (MCV), NULL-Einträge, Histogramm der Verteilung. Basierend auf diesen Daten trifft der PostgreSQL-Abfrageplaner intelligente Entscheidungen über den für die Abfrage zu verwendenden Plan. Zu diesem Zeitpunkt speichert PostgreSQL keine Statistiken für JSONB-Spalten oder -Schlüssel. Dies kann manchmal zu schlechten Entscheidungen führen, wie z. B. die Verwendung von Nested-Loop-Joins im Vergleich zu Hash-Joins usw. Ein ausführlicheres Beispiel dafür finden Sie in diesem Blogbeitrag – Wann man JSONB in einem PostgreSQL-Schema vermeidet.
-
JSONB-Speicher führt zu einem größeren Speicherbedarf
Der JSONB-Speicher dedupliziert die Schlüsselnamen im JSON nicht. Dies kann zu einem erheblich größeren Speicherbedarf im Vergleich zu MongoDB BSON auf WiredTiger oder herkömmlichem Spaltenspeicher führen. Ich habe einen einfachen Test mit dem untenstehenden JSONB-Modell durchgeführt, das etwa 10 Millionen Datenzeilen speichert, und hier sind die Ergebnisse – In gewisser Weise ähnelt dies dem MongoDB MMAPV1-Speichermodell, bei dem die Schlüssel in JSONB unverändert ohne Komprimierung gespeichert wurden. Eine langfristige Lösung besteht darin, die Schlüsselnamen in ein Wörterbuch auf Tabellenebene zu verschieben und auf dieses Wörterbuch zu verweisen, anstatt die Schlüsselnamen wiederholt zu speichern. Bis dahin besteht die Problemumgehung möglicherweise darin, kompaktere Namen (im Unix-Stil) anstelle von aussagekräftigeren Namen zu verwenden. Wenn Sie beispielsweise Millionen von Instanzen eines bestimmten Schlüssels speichern, wäre es speichertechnisch besser, ihn „pb“ statt „publisherName“ zu nennen.
Der effizienteste Weg, JSONB in PostgreSQL zu nutzen, ist die Kombination von Spalten und JSONB. Wenn ein Schlüssel sehr häufig in Ihren JSONB-Blobs vorkommt, ist es wahrscheinlich besser, ihn als Spalte zu speichern. Verwenden Sie JSONB als „Auffangbehälter“, um die variablen Teile Ihres Schemas zu handhaben, während Sie traditionelle Spalten für stabilere Felder nutzen.
JSONB-Datenstrukturen
Sowohl JSONB als auch MongoDB BSON sind im Wesentlichen Baumstrukturen, die mehrstufige Knoten verwenden, um die geparsten JSONB-Daten zu speichern. MongoDB BSON hat eine sehr ähnliche Struktur.
Quelle der Bilder
JSONB &TOAST
Ein weiterer wichtiger Aspekt für die Speicherung ist die Interaktion von JSONB mit TOAST (The Oversize Attribute Storage Technique). Wenn die Größe Ihrer Spalte den TOAST_TUPLE_THRESHOLD (2 KB Standardwert) überschreitet, versucht PostgreSQL normalerweise, die Daten zu komprimieren und in 2 KB einzupassen. Wenn das nicht funktioniert, werden die Daten in den Out-of-Line-Speicher verschoben. Dies wird als „TOASTen“ der Daten bezeichnet. Wenn die Daten abgerufen werden, muss der umgekehrte Prozess „deTOASTting“ erfolgen. Sie können auch die TOAST-Speicherstrategie steuern:
- Erweitert – Ermöglicht Out-of-Line-Speicherung und -Komprimierung (unter Verwendung von pglz). Dies ist die Standardoption.
- Extern – Ermöglicht Out-of-Line-Speicherung, aber keine Komprimierung.
Wenn Verzögerungen aufgrund der TOAST-Komprimierung oder -Dekomprimierung auftreten, besteht eine Möglichkeit darin, den Spaltenspeicher proaktiv auf „ERWEITERT“ festzulegen. Alle Details finden Sie in diesem PostgreSQL-Dokument.
JSONB-Operatoren und -Funktionen
PostgreSQL bietet eine Vielzahl von Operatoren für die Arbeit mit JSONB. Aus der Dokumentation:
Operator | Beschreibung |
---|---|
-> | JSON-Array-Element abrufen (indiziert von Null, negative Ganzzahlen zählen vom Ende) |
-> | JSON-Objektfeld nach Schlüssel abrufen |
->> | JSON-Array-Element als Text erhalten |
->> | JSON-Objektfeld als Text erhalten |
#> | JSON-Objekt vom angegebenen Pfad abrufen |
#>> | JSON-Objekt im angegebenen Pfad als Text abrufen |
@> | Enthält der linke JSON-Wert die richtigen JSON-Pfad-/Wert-Einträge auf der obersten Ebene? |
<@ | Sind die linken JSON-Pfad-/Werteinträge auf der obersten Ebene innerhalb des rechten JSON-Werts enthalten? |
? | Führt die Zeichenfolge aus als Schlüssel der obersten Ebene innerhalb des JSON-Werts vorhanden sind? |
?| | Führen Sie einen dieser Array-Strings aus als Schlüssel der obersten Ebene existieren? |
?& | Machen Sie alle diese Array-Strings als Schlüssel der obersten Ebene existieren? |
|| | Zwei jsonb-Werte zu einem neuen jsonb-Wert verketten |
- | Schlüssel/Wert-Paar oder String löschen Element vom linken Operanden. Schlüssel/Wert-Paare werden basierend auf ihrem Schlüsselwert abgeglichen. |
- | Löschen Sie mehrere Schlüssel/Wert-Paare oder String Elemente vom linken Operanden. Schlüssel/Wert-Paare werden basierend auf ihrem Schlüsselwert abgeglichen. |
- | Lösche das Array-Element mit dem angegebenen Index (Negative Ganzzahlen zählen vom Ende). Löst einen Fehler aus, wenn der Container der obersten Ebene kein Array ist. |
#- | Löschen Sie das Feld oder Element mit dem angegebenen Pfad (bei JSON-Arrays zählen negative Ganzzahlen vom Ende an) |
@? | Gibt der JSON-Pfad irgendein Element für den angegebenen JSON-Wert zurück? |
@@ | Gibt das Ergebnis der JSON-Pfadprädikatprüfung für den angegebenen JSON-Wert zurück. Nur das erste Element des Ergebnisses wird berücksichtigt. Wenn das Ergebnis kein boolescher Wert ist, wird null zurückgegeben. |
PostgreSQL bietet auch eine Vielzahl von Erstellungsfunktionen und Verarbeitungsfunktionen, um mit den JSONB-Daten zu arbeiten.
JSONB-Indizes
JSONB bietet eine Vielzahl von Optionen zum Indizieren Ihrer JSON-Daten. Auf hoher Ebene werden wir uns mit drei verschiedenen Arten von Indizes befassen – GIN, BTREE und HASH. Nicht alle Indextypen unterstützen alle Operatorklassen, daher ist eine Planung erforderlich, um Ihre Indizes basierend auf den Typen von Operatoren und Abfragen zu entwerfen, die Sie verwenden möchten.
GIN-Indizes
GIN steht für „Generalized Inverted Indexes“. Aus der Dokumentation:
„GIN ist für die Behandlung von Fällen konzipiert, in denen die zu indizierenden Elemente zusammengesetzte Werte sind und die vom Index zu verarbeitenden Abfragen nach Elementen suchen müssen Werte, die in den zusammengesetzten Elementen erscheinen. Beispielsweise könnten die Elemente Dokumente sein, und die Abfragen könnten Suchen nach Dokumenten sein, die bestimmte Wörter enthalten.“
GIN unterstützt zwei Operatorklassen:
- jsonb_ops (Standard) – ?, ?|, ?&, @>, @@, @? [Indizieren Sie jeden Schlüssel und Wert im JSONB-Element]
- jsonb_pathops – @>, @@, @? [Indizieren Sie nur die Werte im JSONB-Element]
CREATE INDEX datagin ON books USING gin (data);
Existenzoperatoren (?, ?|, ?&)
Diese Operatoren können verwendet werden, um zu prüfen, ob Schlüssel der obersten Ebene in der JSONB vorhanden sind. Lassen Sie uns einen GIN-Index für die Daten-JSONB-Spalte erstellen. Finden Sie beispielsweise alle Bücher, die in Blindenschrift verfügbar sind. Der JSON sieht etwa so aus:
"{"tags": {"nk594127": {"ik71786": "iv678771"}}, "braille": false, "keywords": ["abc", "kef", "keh"], "hardcover": true, "publisher": "EfgdxUdvB0", "criticrating": 1}
demo=# select * from books where data ? 'braille'; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} ..... demo=# explain analyze select * from books where data ? 'braille'; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158) (actual time=0.033..0.039 rows=15 loops=1) Recheck Cond: (data ? 'braille'::text) Heap Blocks: exact=2 -> Bitmap Index Scan on datagin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.022..0.022 rows=15 loops=1) Index Cond: (data ? 'braille'::text) Planning Time: 0.102 ms Execution Time: 0.067 ms (7 rows)
Wie Sie der EXPLAIN-Ausgabe entnehmen können, wird der von uns erstellte GIN-Index für die Suche verwendet. Was wäre, wenn wir Bücher in Braille- oder Hardcover-Schrift finden wollten?
demo=# explain analyze select * from books where data ?| array['braille','hardcover']; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.029..0.035 rows=15 loops=1) Recheck Cond: (data ?| '{braille,hardcover}'::text[]) Heap Blocks: exact=2 -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.023..0.023 rows=15 loops=1) Index Cond: (data ?| '{braille,hardcover}'::text[]) Planning Time: 0.138 ms Execution Time: 0.057 ms (7 rows)
Der GIN-Index unterstützt die „Existenz“-Operatoren nur auf „Top-Level“-Schlüsseln. Wenn sich der Schlüssel nicht auf der obersten Ebene befindet, wird der Index nicht verwendet. Dies führt zu einem sequentiellen Scan:
demo=# select * from books where data->'tags' ? 'nk455671'; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} 685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0} (2 rows) demo=# explain analyze select * from books where data->'tags' ? 'nk455671'; QUERY PLAN ---------------------------------------------------------------------------------------------------------- Seq Scan on books (cost=0.00..38807.29 rows=1000 width=158) (actual time=0.018..270.641 rows=2 loops=1) Filter: ((data -> 'tags'::text) ? 'nk455671'::text) Rows Removed by Filter: 1000017 Planning Time: 0.078 ms Execution Time: 270.728 ms (5 rows)
Der Weg, das Vorhandensein in verschachtelten Dokumenten zu überprüfen, ist die Verwendung von „Expression-Indizes“. Lassen Sie uns einen Index für Daten->Tags erstellen:
CREATE INDEX datatagsgin ON books USING gin (data->'tags');
demo=# select * from books where data->'tags' ? 'nk455671'; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} 685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags": {"nk455671": {"ik615925": "iv253423"}}, "publisher": "b2NwVg7VY3", "criticrating": 0} (2 rows) demo=# explain analyze select * from books where data->'tags' ? 'nk455671'; QUERY PLAN ------------------------------------------------------------------------------------------------------------------------ Bitmap Heap Scan on books (cost=12.75..1007.75 rows=1000 width=158) (actual time=0.031..0.035 rows=2 loops=1) Recheck Cond: ((data ->'tags'::text) ? 'nk455671'::text) Heap Blocks: exact=2 -> Bitmap Index Scan on datatagsgin (cost=0.00..12.50 rows=1000 width=0) (actual time=0.021..0.021 rows=2 loops=1) Index Cond: ((data ->'tags'::text) ? 'nk455671'::text) Planning Time: 0.098 ms Execution Time: 0.061 ms (7 rows)
Hinweis:Eine Alternative ist hier die Verwendung des @>-Operators:
select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;
Dies funktioniert jedoch nur, wenn der Wert ein Objekt ist. Wenn Sie sich also nicht sicher sind, ob der Wert ein Objekt oder ein primitiver Wert ist, kann dies zu falschen Ergebnissen führen.
Pfadoperatoren @>, <@
Der „Pfad“-Operator kann für mehrstufige Abfragen Ihrer JSONB-Daten verwendet werden. Verwenden wir es ähnlich wie das ? obiger Operator:
select * from books where data @> '{"braille":true}'::jsonb; demo=# explain analyze select * from books where data @> '{"braille":true}'::jsonb; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.040..0.048 rows=6 loops=1) Recheck Cond: (data @> '{"braille": true}'::jsonb) Rows Removed by Index Recheck: 9 Heap Blocks: exact=2 -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.030..0.030 rows=15 loops=1) Index Cond: (data @> '{"braille": true}'::jsonb) Planning Time: 0.100 ms Execution Time: 0.076 ms (8 rows)
Die Pfadoperatoren unterstützen das Abfragen von verschachtelten Objekten oder Objekten der obersten Ebene:
demo=# select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb; id | author | isbn | rating | data -----+-----------------+------------+--------+------------------------------------------------------------------------------------- 346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3} (1 row) demo=# explain analyze select * from books where data @> '{"publisher":"XlekfkLOtL"}'::jsonb; QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=16.75..1009.25 rows=1000 width=158) (actual time=0.491..0.492 rows=1 loops=1) Recheck Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb) Heap Blocks: exact=1 -> Bitmap Index Scan on datagin (cost=0.00..16.50 rows=1000 width=0) (actual time=0.092..0.092 rows=1 loops=1) Index Cond: (data @> '{"publisher": "XlekfkLOtL"}'::jsonb) Planning Time: 0.090 ms Execution Time: 0.523 ms
Die Abfragen können auch mehrstufig sein:
demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} (1 row)
GIN Index „pathops“-Operatorklasse
GIN unterstützt auch eine „pathops“-Option, um die Größe des GIN-Index zu reduzieren. Wenn Sie die Option pathops verwenden, ist die einzige Operatorunterstützung das „@>“, also müssen Sie mit Ihren Abfragen vorsichtig sein. Aus der Dokumentation:
„Der technische Unterschied zwischen einem jsonb_ops- und einem jsonb_path_ops-GIN-Index besteht darin, dass ersterer unabhängige Indexelemente für jeden Schlüssel und Wert in den Daten erstellt, während letzterer Indexelemente nur für erstellt jeden Wert in den Daten“
Sie können einen GIN-Pathops-Index wie folgt erstellen:
CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);
Auf meinem kleinen Datensatz von 1 Million Büchern können Sie sehen, dass der Pathops-GIN-Index kleiner ist – Sie sollten mit Ihrem Datensatz testen, um die Einsparungen zu verstehen:
public | dataginpathops | index | sgpostgres | books | 67 MB | public | datatagsgin | index | sgpostgres | books | 84 MB |
Lassen Sie uns unsere vorherige Abfrage mit dem Pathops-Index wiederholen:
demo=# select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb; id | author | isbn | rating | data ---------+-----------------+------------+--------+------------------------------------------------------------------------------------------------------------------------------------------------------ ------------------ 1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags": {"nk455671": {"ik937456": "iv506075"}}, "braille": true, "keywords": ["abc", "kef", "keh"], "hardcover": false, "publisher": "zSfZIAjGGs", " criticrating": 4} (1 row) demo=# explain select * from books where data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb; QUERY PLAN ----------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=12.75..1005.25 rows=1000 width=158) Recheck Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb) -> Bitmap Index Scan on dataginpathops (cost=0.00..12.50 rows=1000 width=0) Index Cond: (data @> '{"tags": {"nk455671": {"ik937456": "iv506075"}}}'::jsonb) (4 rows)
Wie oben erwähnt, unterstützt die Option „pathops“ jedoch nicht alle Szenarien, die die Standardoperatorklasse unterstützt. Mit einem „Patops“-GIN-Index können all diese Abfragen den GIN-Index nicht nutzen. Zusammenfassend lässt sich sagen, dass Sie einen kleineren Index haben, der jedoch einen eingeschränkteren Anwendungsfall unterstützt.
select * from books where data ? 'tags'; => Sequential scan select * from books where data @> '{"tags" :{}}'; => Sequential scan select * from books where data @> '{"tags" :{"k7888":{}}}' => Sequential scan
B-Tree-Indizes
B-Tree-Indizes sind der häufigste Indextyp in relationalen Datenbanken. Wenn Sie jedoch eine ganze JSONB-Spalte mit einem B-Tree-Index indizieren, sind die einzigen nützlichen Operatoren „=“, <, <=,>,>=. Im Wesentlichen kann dies nur für ganze Objektvergleiche verwendet werden, was einen sehr begrenzten Anwendungsfall hat.
Ein häufigeres Szenario ist die Verwendung von B-Baum-„Ausdrucksindizes“. Eine Einführung finden Sie hier – Indexes on Expressions. B-Tree-Ausdrucksindizes können die gängigen Vergleichsoperatoren „=“, „<“, „>“, „>=“, „<=“ unterstützen. Wie Sie sich vielleicht erinnern, unterstützen GIN-Indizes diese Operatoren nicht. Betrachten wir den Fall, in dem wir alle Bücher mit einer data->criticrating> 4 abrufen möchten. Sie würden also eine Abfrage wie folgt erstellen:
demo=# select * from books where data->'criticrating' > 4; ERROR: operator does not exist: jsonb >= integer LINE 1: select * from books where data->'criticrating' >= 4; ^ HINT: No operator matches the given name and argument types. You might need to add explicit type casts.
Nun, das funktioniert nicht, da der Operator „->“ einen JSONB-Typ zurückgibt. Also müssen wir so etwas verwenden:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Wenn Sie eine Version vor PostgreSQL 11 verwenden, wird es hässlicher. Sie müssen zuerst als Text abfragen und ihn dann in eine Ganzzahl umwandeln:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Für Ausdrucksindizes muss der Index genau mit dem Abfrageausdruck übereinstimmen. Unser Index würde also etwa so aussehen:
demo=# CREATE INDEX criticrating ON books USING BTREE (((data->'criticrating')::int4)); CREATE INDEX demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------- Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1) Index Cond: (((data -> 'criticrating'::text))::integer = 3) Planning Time: 0.103 ms Execution Time: 79.019 ms (4 rows) demo=# explain analyze select * from books where (data->'criticrating')::int4 = 3; QUERY PLAN ---------------------------------------------------------------------------------------------------------------------------------- Index Scan using criticrating on books (cost=0.42..4626.93 rows=5000 width=158) (actual time=0.069..70.221 rows=199883 loops=1) Index Cond: (((data -> 'criticrating'::text))::integer = 3) Planning Time: 0.103 ms Execution Time: 79.019 ms (4 rows) 1 From above we can see that the BTREE index is being used as expected.
Hash-Indizes
Wenn Sie sich nur für den "="-Operator interessieren, dann werden Hash-Indizes interessant. Betrachten Sie zum Beispiel den Fall, wenn wir nach einem bestimmten Tag in einem Buch suchen. Das zu indizierende Element kann ein Element der obersten Ebene oder tief verschachtelt sein.
z.B. tags->publisher =XlekfkLOtL
CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));
Hash-Indizes sind in der Regel auch kleiner als B-Tree- oder GIN-Indizes. Dies hängt natürlich letztendlich von Ihrem Datensatz ab.
demo=# select * from books where data->'publisher' = 'XlekfkLOtL' demo-# ; id | author | isbn | rating | data -----+-----------------+------------+--------+------------------------------------------------------------------------------------- 346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags": {"nk88": {"ik37": "iv161"}}, "publisher": "XlekfkLOtL", "criticrating": 3} (1 row) demo=# explain analyze select * from books where data->'publisher' = 'XlekfkLOtL'; QUERY PLAN ----------------------------------------------------------------------------------------------------------------------- Index Scan using publisherhash on books (cost=0.00..2.02 rows=1 width=158) (actual time=0.016..0.017 rows=1 loops=1) Index Cond: ((data -> 'publisher'::text) = 'XlekfkLOtL'::text) Planning Time: 0.080 ms Execution Time: 0.035 ms (4 rows)
Besondere Erwähnung:GIN-Trigramm-Indizes
PostgreSQL unterstützt String-Matching mithilfe von Trigram-Indizes. Trigramm-Indizes funktionieren, indem sie Text in Trigramme aufteilen. Trigrams are basically words broken up into sequences of 3 letters. More information can be found in the documentation. GIN indexes support the “gin_trgm_ops” class that can be used to index the data in JSONB. You can choose to use expression indexes to build the trigram index on a particular column.
CREATE EXTENSION pg_trgm; CREATE INDEX publisher ON books USING GIN ((data->'publisher') gin_trgm_ops); demo=# select * from books where data->'publisher' LIKE '%I0UB%'; id | author | isbn | rating | data ----+-----------------+------------+--------+--------------------------------------------------------------------------------- 4 | KiEk3xjqvTpmZeS | EYqXO9Nwmm | 0 | {"tags": {"nk3": {"ik1": "iv1"}}, "publisher": "MI0UBqZJDt", "criticrating": 1} (1 row)
As you can see in the query above, we can search for any arbitrary string occurring at any potion. Unlike the B-tree indexes, we are not restricted to left anchored expressions.
demo=# explain analyze select * from books where data->'publisher' LIKE '%I0UB%'; QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=9.78..111.28 rows=100 width=158) (actual time=0.033..0.033 rows=1 loops=1) Recheck Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text) Heap Blocks: exact=1 -> Bitmap Index Scan on publisher (cost=0.00..9.75 rows=100 width=0) (actual time=0.025..0.025 rows=1 loops=1) Index Cond: ((data -> 'publisher'::text) ~~ '%I0UB%'::text) Planning Time: 0.213 ms Execution Time: 0.058 ms (7 rows)
Special Mention:GIN Array Indexes
JSONB has great built-in support for indexing arrays. Let's consider an example of indexing an array of strings using a GIN index in the case when our JSONB data contains a "keyword" element and we would like to find rows with particular keywords:
{"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2} CREATE INDEX keywords ON books USING GIN ((data->'keywords') jsonb_path_ops); demo=# select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb; id | author | isbn | rating | data ---------+-----------------+------------+--------+----------------------------------------------------------------------------------------------------------------------------------- 1000003 | zEG406sLKQ2IU8O | viPdlu3DZm | 4 | {"tags": {"nk263020": {"ik203820": "iv817928"}}, "keywords": ["abc", "kef", "keh"], "publisher": "7NClevxuTM", "criticrating": 2} 1000004 | GCe9NypHYKDH4rD | so6TQDYzZ3 | 4 | {"tags": {"nk780341": {"ik397357": "iv632731"}}, "keywords": ["abc", "kef", "keh"], "publisher": "fqaJuAdjP5", "criticrating": 2} (2 rows) demo=# explain analyze select * from books where data->'keywords' @> '["abc", "keh"]'::jsonb; QUERY PLAN --------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=54.75..1049.75 rows=1000 width=158) (actual time=0.026..0.028 rows=2 loops=1) Recheck Cond: ((data -> 'keywords'::text) @> '["abc", "keh"]'::jsonb) Heap Blocks: exact=1 -> Bitmap Index Scan on keywords (cost=0.00..54.50 rows=1000 width=0) (actual time=0.014..0.014 rows=2 loops=1) Index Cond: ((data -> 'keywords'::text) @&amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; '["abc", "keh"]'::jsonb) Planning Time: 0.131 ms Execution Time: 0.063 ms (7 rows)
The order of the items in the array on the right does not matter. For example, the following query would return the same result as the previous:
demo=# explain analyze select * from books where data->'keywords' @> '["keh","abc"]'::jsonb;
All elements in the right side array of the containment operator need to be present - basically like an "AND" operator. If you want "OR" behavior, you can construct it in the WHERE clause:
demo=# explain analyze select * from books where (data->'keywords' @> '["abc"]'::jsonb OR data->'keywords' @> '["keh"]'::jsonb);
More details on the behavior of the containment operators with arrays can be found in the documentation.
SQL/JSON &JSONPath
SQL standard added support for JSON in SQL - SQL/JSON Standard-2016. With the PostgreSQL 12/13 releases, PostgreSQL has one of the best implementations of the SQL/JSON standard. For more details refer to the PostgreSQL 12 announcement.
One of the core features of SQL/JSON is support for the JSONPath language to query JSONB data. JSONPath allows you to specify an expression (using a syntax similar to the property access notation in Javascript) to query your JSONB data. This makes it simple and intuitive, but is also very powerful to query your JSONB data. Think of JSONPath as the logical equivalent of XPath for XML.
.key | Returns an object member with the specified key. |
[*] | Wildcard array element accessor that returns all array elements. |
.* | Wildcard member accessor that returns the values of all members located at the top level of the current object. |
.** | Recursive wildcard member accessor that processes all levels of the JSON hierarchy of the current object and returns all the member values, regardless of their nesting level. |
Refer to JSONPath documentation for the full list of operators. JSONPath also supports a variety of filter expressions.
JSONPath Functions
PostgreSQL 12 provides several functions to use JSONPath to query your JSONB data. Aus der Dokumentation:
- jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON Wert.
- jsonb_path_match - Returns the result of JSONB path predicate check for the specified JSONB value. Only the first item of the result is taken into account. If the result is not Boolean, then null is returned.
- jsonb_path_query - Gets all JSONB items returned by JSONB path for the specified JSONB value. There are also a couple of other variants of this function that handle arrays of objects.
Let's start with a simple query - finding books by publisher:
demo=# select * from books where data @@ '$.publisher == "ktjKEZ1tvq"'; id | author | isbn | rating | data ---------+-----------------+------------+--------+---------------------------------------------------------------------------------------------------------------------------------- 1000001 | 4RNsovI2haTgU7l | GwSoX67gLS | 2 | {"tags": {"nk542369": {"ik55240": "iv305393"}}, "keywords": ["abc", "def", "geh"], "publisher": "ktjKEZ1tvq", "criticrating": 0} (1 row) demo=# explain analyze select * from books where data @@ '$.publisher == "ktjKEZ1tvq"'; QUERY PLAN -------------------------------------------------------------------------------------------------------------------- Bitmap Heap Scan on books (cost=21.75..1014.25 rows=1000 width=158) (actual time=0.123..0.124 rows=1 loops=1) Recheck Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath) Heap Blocks: exact=1 -> Bitmap Index Scan on datagin (cost=0.00..21.50 rows=1000 width=0) (actual time=0.110..0.110 rows=1 loops=1) Index Cond: (data @@ '($."publisher" == "ktjKEZ1tvq")'::jsonpath) Planning Time: 0.137 ms Execution Time: 0.194 ms (7 rows)
You can rewrite this expression as a JSONPath filter:
demo=# select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")');
You can also use very complex query expressions. For example, let's select books where print style =hardcover and price =100:
select * from books where jsonb_path_exists(data, '$.prints[*] ?(@.style=="hc" &amp;amp;amp;amp;&amp;amp;amp;amp; @.price == 100)');
However, index support for JSONPath is very limited at this point - this makes it dangerous to use JSONPath in the where clause. JSONPath support for indexes will be improved in subsequent releases.
demo=# explain analyze select * from books where jsonb_path_exists(data,'$.publisher ?(@ == "ktjKEZ1tvq")'); QUERY PLAN ------------------------------------------------------------------------------------------------------------ Seq Scan on books (cost=0.00..36307.24 rows=333340 width=158) (actual time=0.019..480.268 rows=1 loops=1) Filter: jsonb_path_exists(data, '$."publisher"?(@ == "ktjKEZ1tvq")'::jsonpath, '{}'::jsonb, false) Rows Removed by Filter: 1000028 Planning Time: 0.095 ms Execution Time: 480.348 ms (5 rows)
Projecting Partial JSON
Another great use case for JSONPath is projecting partial JSONB from the row that matches. Consider the following sample JSONB:
demo=# select jsonb_pretty(data) from books where id = 1000029; jsonb_pretty ----------------------------------- { "tags": { "nk678947": { "ik159670": "iv32358 } }, "prints": [ { "price": 100, "style": "hc" }, { "price": 50, "style": "pb" } ], "braille": false, "keywords": [ "abc", "kef", "keh" ], "hardcover": true, "publisher": "ppc3YXL8kK", "criticrating": 3 }
Select only the publisher field:
demo=# select jsonb_path_query(data, '$.publisher') from books where id = 1000029; jsonb_path_query ------------------ "ppc3YXL8kK" (1 row)
Select the prints field (which is an array of objects):
demo=# select jsonb_path_query(data, '$.prints') from books where id = 1000029; jsonb_path_query --------------------------------------------------------------- [{"price": 100, "style": "hc"}, {"price": 50, "style": "pb"}] (1 row)
Select the first element in the array prints:
demo=# select jsonb_path_query(data, '$.prints[0]') from books where id = 1000029; jsonb_path_query ------------------------------- {"price": 100, "style": "hc"} (1 row)
Select the last element in the array prints:
demo=# select jsonb_path_query(data, '$.prints[$.size()]') from books where id = 1000029; jsonb_path_query ------------------------------ {"price": 50, "style": "pb"} (1 row)
Select only the hardcover prints from the array:
demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc")') from books where id = 1000029; jsonb_path_query ------------------------------- {"price": 100, "style": "hc"} (1 row)
We can also chain the filters:
demo=# select jsonb_path_query(data, '$.prints[*] ?(@.style=="hc") ?(@.price ==100)') from books where id = 1000029; jsonb_path_query ------------------------------- {"price": 100, "style": "hc"} (1 row)
In summary, PostgreSQL provides a powerful and versatile platform to store and process JSON data. There are several gotcha's that you need to be aware of, but we are optimistic that it will be fixed in future releases.
|