JSON staat voor JavaScript Object Notation. Het is een open standaardindeling die gegevens organiseert in sleutel/waarde-paren en arrays die worden beschreven in RFC 7159. JSON is de meest voorkomende indeling die door webservices wordt gebruikt om gegevens uit te wisselen, documenten op te slaan, ongestructureerde gegevens, enz. In dit bericht gaan we om u tips en technieken te tonen voor het effectief opslaan en indexeren van JSON-gegevens in PostgreSQL.
Je kunt ook onze Working with JSON Data in PostgreSQL vs. MongoDB webinar bekijken in samenwerking met PostgresConf voor meer informatie over het onderwerp, en onze SlideShare-pagina bekijken om de dia's te downloaden.
Waarom JSON opslaan in PostgreSQL?
Waarom zou een relationele database zich zelfs zorgen moeten maken over ongestructureerde gegevens? Het blijkt dat er een paar scenario's zijn waarin het nuttig is.
-
Schemaflexibiliteit
Een van de belangrijkste redenen om gegevens op te slaan met het JSON-formaat is schemaflexibiliteit. Het opslaan van uw gegevens in JSON is handig wanneer uw schema vloeiend is en regelmatig verandert. Als u elk van de sleutels als kolommen opslaat, leidt dit tot frequente DML-bewerkingen. Dit kan moeilijk zijn als uw dataset groot is, bijvoorbeeld het bijhouden van gebeurtenissen, analyses, tags, enzovoort. Opmerking:als een bepaalde sleutel altijd aanwezig is in uw document kan het zinvol zijn om het op te slaan als een eersteklas kolom. We bespreken meer over deze aanpak in de sectie "JSON-patronen en antipatronen" hieronder.
-
Genste objecten
Als uw gegevensset geneste objecten heeft (op één of meerdere niveaus), is het in sommige gevallen gemakkelijker om ze in JSON te verwerken in plaats van de gegevens te denormaliseren in kolommen of meerdere tabellen.
-
Synchroniseren met externe gegevensbronnen
Vaak levert een extern systeem gegevens als JSON, dus het kan een tijdelijke opslag zijn voordat gegevens in andere delen van het systeem worden opgenomen. Bijvoorbeeld Stripe-transacties.
Tijdlijn van JSON-ondersteuning in PostgreSQL
JSON-ondersteuning in PostgreSQL is geïntroduceerd in 9.2 en is gestaag verbeterd in elke toekomstige release.
-
Wave 1:PostgreSQL 9.2 (2012) heeft ondersteuning toegevoegd voor JSON-gegevenstype
De JSON-database in 9.2 was redelijk beperkt (en op dat moment waarschijnlijk overhyped) - in feite een veredelde string met wat JSON-validatie erin. Het is handig om inkomende JSON te valideren en op te slaan in de database. Hieronder vindt u meer informatie.
-
Wave 2:PostgreSQL 9.4 (2014) heeft ondersteuning toegevoegd voor JSONB-gegevenstype
JSONB staat voor "JSON Binary" of "JSON better", afhankelijk van aan wie je het vraagt. Het is een ontleed binair formaat om JSON op te slaan. JSONB ondersteunt het indexeren van de JSON-gegevens en is zeer efficiënt in het ontleden en opvragen van de JSON-gegevens. In de meeste gevallen, wanneer u met JSON in PostgreSQL werkt, zou u JSONB moeten gebruiken.
-
Wave 3:PostgreSQL 12 (2019) heeft ondersteuning toegevoegd voor SQL/JSON-standaard en JSONPATH-query's
JSONPath brengt een krachtige JSON-query-engine naar PostgreSQL.
Wanneer moet je JSON versus JSONB gebruiken?
In de meeste gevallen is JSONB wat u zou moeten gebruiken. Er zijn echter enkele specifieke gevallen waarin JSON beter werkt:
- JSON behoudt de originele opmaak (ook wel witruimte genoemd) en volgorde van de toetsen.
- JSON behoudt dubbele sleutels.
- JSON kan sneller worden opgenomen dan JSONB, maar als u verdere verwerking uitvoert, zal JSONB sneller zijn.
Als u bijvoorbeeld alleen JSON-logboeken opneemt en er op geen enkele manier naar op zoek bent, is JSON wellicht een betere optie voor u. Voor de doeleinden van deze blog, wanneer we verwijzen naar JSON-ondersteuning in PostgreSQL, zullen we in de toekomst verwijzen naar JSONB.
JSONB gebruiken in PostgreSQL:effectief JSON-gegevens opslaan en indexeren in PostgreSQLKlik om te tweetenJSONB-patronen en antipatronen
Als PostgreSQL geweldige ondersteuning biedt voor JSONB, waarom hebben we dan nog kolommen nodig? Waarom maak je niet gewoon een tabel met een JSONB-blob en verwijder je alle kolommen zoals het onderstaande schema:
CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));
Uiteindelijk zijn kolommen nog steeds de meest efficiënte techniek om met uw gegevens te werken. JSONB-opslag heeft enkele nadelen ten opzichte van traditionele kolommen:
-
PostreSQL slaat geen kolomstatistieken op voor JSONB-kolommen
PostgreSQL houdt statistieken bij over de distributies van waarden in elke kolom van de tabel:de meest voorkomende waarden (MCV), NULL-vermeldingen, histogram van distributie. Op basis van deze gegevens neemt de PostgreSQL-queryplanner slimme beslissingen over het plan dat voor de query moet worden gebruikt. Op dit moment slaat PostgreSQL geen statistieken op voor JSONB-kolommen of -sleutels. Dit kan soms resulteren in slechte keuzes, zoals het gebruik van geneste lus-joins versus hash-joins, enz. Een meer gedetailleerd voorbeeld hiervan wordt gegeven in deze blogpost - Wanneer JSONB in een PostgreSQL-schema moet worden vermeden.
-
JSONB-opslag resulteert in een grotere opslagvoetafdruk
JSONB-opslag ontdubbelt de sleutelnamen in de JSON niet. Dit kan resulteren in een aanzienlijk grotere opslagvoetafdruk in vergelijking met MongoDB BSON op WiredTiger of traditionele kolomopslag. Ik heb een eenvoudige test uitgevoerd met het onderstaande JSONB-model waarin ongeveer 10 miljoen rijen gegevens zijn opgeslagen, en hier zijn de resultaten:in sommige opzichten is dit vergelijkbaar met het MongoDB MMAPV1-opslagmodel waarbij de sleutels in JSONB ongewijzigd werden opgeslagen zonder enige compressie. Een langetermijnoplossing is om de sleutelnamen naar een woordenboek op tabelniveau te verplaatsen en naar dit woordenboek te verwijzen in plaats van de sleutelnamen herhaaldelijk op te slaan. Tot die tijd kan de oplossing zijn om compactere namen (unix-stijl) te gebruiken in plaats van meer beschrijvende namen. Als u bijvoorbeeld miljoenen exemplaren van een bepaalde sleutel opslaat, is het qua opslag beter om deze 'pb' te noemen in plaats van 'publisherName'.
De meest efficiënte manier om JSONB in PostgreSQL te gebruiken, is door kolommen en JSONB te combineren. Als een sleutel heel vaak voorkomt in uw JSONB-blobs, kunt u deze waarschijnlijk beter als een kolom opslaan. Gebruik JSONB als een "catch all" om de variabele delen van uw schema af te handelen, terwijl u gebruik maakt van traditionele kolommen voor velden die stabieler zijn.
JSONB-gegevensstructuren
Zowel JSONB als MongoDB BSON zijn in wezen boomstructuren, waarbij knooppunten op meerdere niveaus worden gebruikt om de geparseerde JSONB-gegevens op te slaan. MongoDB BSON heeft een zeer vergelijkbare structuur.
Beeldbron
JSONB &TOAST
Een andere belangrijke overweging voor opslag is hoe JSONB samenwerkt met TOAST (The Oversize Attribute Storage Technique). Wanneer de grootte van uw kolom de TOAST_TUPLE_THRESHOLD (standaardwaarde van 2 kb) overschrijdt, zal PostgreSQL doorgaans proberen de gegevens te comprimeren en in 2 kb te passen. Als dat niet werkt, worden de gegevens verplaatst naar out-of-line opslag. Dit is wat ze het 'TOASTEN' van de gegevens noemen. Wanneer de gegevens worden opgehaald, moet het omgekeerde proces "deTOASTting" plaatsvinden. U kunt ook de TOAST-opslagstrategie beheren:
- Uitgebreid – Zorgt voor out-of-line opslag en compressie (met behulp van pglz). Dit is de standaardoptie.
- Extern – Staat out-of-line opslag toe, maar geen compressie.
Als u vertragingen ondervindt vanwege de TOAST-compressie of decompressie, is een optie om de kolomopslag proactief in te stellen op 'EXTENDED'. Raadpleeg dit PostgreSQL-document voor alle details.
JSONB-operators en -functies
PostgreSQL biedt een verscheidenheid aan operators om aan JSONB te werken. Uit de documenten:
Operator | Beschrijving |
---|---|
-> | JSON-array-element ophalen (geïndexeerd vanaf nul, negatieve gehele getallen tellen vanaf het einde) |
-> | JSON-objectveld per sleutel ophalen |
->> | JSON-array-element ophalen als tekst |
->> | JSON-objectveld ophalen als tekst |
#> | JSON-object ophalen op het opgegeven pad |
#>> | JSON-object op het opgegeven pad als tekst ophalen |
@> | Bevat de linker JSON-waarde de juiste JSON-pad/waarde-items op het hoogste niveau? |
<@ | Zijn de linker JSON-pad/waarde-vermeldingen op het hoogste niveau binnen de juiste JSON-waarde? |
? | Doet de string bestaan als een sleutel op het hoogste niveau binnen de JSON-waarde? |
?| | Voer een van deze array strings uit bestaan als sleutels op het hoogste niveau? |
?& | Doe al deze array strings bestaan als sleutels op het hoogste niveau? |
|| | Voeg twee jsonb-waarden samen tot een nieuwe jsonb-waarde |
- | Verwijder sleutel/waarde-paar of string element uit de linker operand. Sleutel/waarde-paren worden gematcht op basis van hun sleutelwaarde. |
- | Verwijder meerdere sleutel/waarde-paren of tekenreeks elementen uit de linker operand. Sleutel/waarde-paren worden gematcht op basis van hun sleutelwaarde. |
- | Verwijder het array-element met de opgegeven index (negatieve gehele getallen tellen vanaf het einde). Geeft een fout als de container op het hoogste niveau geen array is. |
#- | Verwijder het veld of element met het opgegeven pad (voor JSON-arrays tellen negatieve gehele getallen vanaf het einde) |
@? | Retourneert het JSON-pad een item voor de opgegeven JSON-waarde? |
@@ | Retourneert het resultaat van de JSON-padpredikaatcontrole voor de opgegeven JSON-waarde. Alleen het eerste item van het resultaat wordt in aanmerking genomen. Als het resultaat niet Booleaans is, wordt null geretourneerd. |
PostgreSQL biedt ook een verscheidenheid aan aanmaakfuncties en verwerkingsfuncties om met de JSONB-gegevens te werken.
JSONB-indexen
JSONB biedt een breed scala aan opties om uw JSON-gegevens te indexeren. Op hoog niveau gaan we in op 3 verschillende soorten indexen:GIN, BTREE en HASH. Niet alle indextypen ondersteunen alle operatorklassen, dus planning is nodig om uw indexen te ontwerpen op basis van het type operators en query's dat u van plan bent te gebruiken.
GIN-indexen
GIN staat voor "Generalized Inverted indexes". Uit de documenten:
"GIN is ontworpen voor het afhandelen van gevallen waarin de items die moeten worden geïndexeerd samengestelde waarden zijn en de zoekopdrachten die door de index moeten worden afgehandeld, naar een element moeten zoeken waarden die binnen de samengestelde items verschijnen. De items kunnen bijvoorbeeld documenten zijn en de zoekopdrachten kunnen zoekopdrachten zijn naar documenten die specifieke woorden bevatten.”
GIN ondersteunt twee operatorklassen:
- jsonb_ops (standaard) – ?, ?|, ?&, @>, @@, @? [Indexeer elke sleutel en waarde in het JSONB-element]
- jsonb_pathops – @>, @@, @? [Indexeer alleen de waarden in het JSONB-element]
CREATE INDEX datagin ON books USING gin (data);
Bestaansoperators (?, ?|, ?&)
Deze operators kunnen worden gebruikt om te controleren op het bestaan van sleutels op het hoogste niveau in de JSONB. Laten we een GIN-index maken in de gegevens-JSONB-kolom. Zoek bijvoorbeeld alle boeken die in braille beschikbaar zijn. De JSON ziet er ongeveer zo uit:
"{"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)
Zoals je kunt zien aan de uitvoer van de uitleg, wordt de GIN-index die we hebben gemaakt voor de zoekopdracht gebruikt. Wat als we boeken wilden vinden die in braille of hardcover waren?
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)
De GIN-index ondersteunt de "existence"-operators alleen op "top-level"-toetsen. Als de sleutel zich niet op het hoogste niveau bevindt, wordt de index niet gebruikt. Het zal resulteren in een sequentiële 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)
De manier om te controleren op bestaan in geneste documenten is door gebruik te maken van "expressie-indexen". Laten we een index maken op data->tags:
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)
Opmerking:een alternatief hier is om de @>-operator te gebruiken:
select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;
Dit werkt echter alleen als de waarde een object is. Dus als u niet zeker weet of de waarde een object of een primitieve waarde is, kan dit tot onjuiste resultaten leiden.
Padoperators @>, <@
De operator "path" kan worden gebruikt voor query's op meerdere niveaus van uw JSONB-gegevens. Laten we het vergelijkbaar gebruiken met de ? operator hierboven:
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)
De padoperators ondersteunen het opvragen van geneste objecten of objecten op het hoogste niveau:
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
De zoekopdrachten kunnen ook op meerdere niveaus zijn:
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” Operator Class
GIN ondersteunt ook een "pathops"-optie om de grootte van de GIN-index te verkleinen. Wanneer u de pathops-optie gebruikt, is de enige operatorondersteuning de "@>", dus u moet voorzichtig zijn met uw vragen. Uit de documenten:
“Het technische verschil tussen een jsonb_ops en een jsonb_path_ops GIN-index is dat de eerste onafhankelijke indexitems maakt voor elke sleutel en waarde in de gegevens, terwijl de laatste alleen indexitems maakt voor elke waarde in de gegevens”
U kunt als volgt een GIN pathops-index maken:
CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);
In mijn kleine dataset van 1 miljoen boeken kun je zien dat de pathops GIN-index kleiner is. Test met je dataset om de besparingen te begrijpen:
public | dataginpathops | index | sgpostgres | books | 67 MB | public | datatagsgin | index | sgpostgres | books | 84 MB |
Laten we onze eerdere zoekopdracht herhalen met de pathops-index:
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)
Echter, zoals hierboven vermeld, ondersteunt de optie "pathops" niet alle scenario's die de standaardoperatorklasse ondersteunt. Met een "pathops" GIN-index kunnen al deze zoekopdrachten geen gebruik maken van de GIN-index. Om samen te vatten:u heeft een kleinere index, maar deze ondersteunt een beperkter gebruik.
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-indexen
B-tree-indexen zijn het meest voorkomende indextype in relationele databases. Als u echter een volledige JSONB-kolom indexeert met een B-tree-index, zijn de enige bruikbare operators "=", <, <=,>,>=. In wezen kan dit alleen worden gebruikt voor vergelijkingen van hele objecten, wat een zeer beperkte use case heeft.
Een gebruikelijker scenario is het gebruik van B-tree "expressie-indexen". Voor een inleiding, zie hier – Indexen op uitdrukkingen. B-tree expressie-indexen kunnen de gebruikelijke vergelijkingsoperatoren '=', '<', '>', '>=', '<=' ondersteunen. Zoals u zich wellicht herinnert, ondersteunen GIN-indexen deze operators niet. Laten we eens kijken naar het geval waarin we alle boeken willen ophalen met een data->kritiek> 4. Je zou dus een query maken als volgt:
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.
Nou, dat werkt niet omdat de '->'-operator een JSONB-type retourneert. Dus we moeten zoiets als dit gebruiken:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Als je een versie gebruikt die ouder is dan PostgreSQL 11, wordt deze nog lelijker. U moet eerst als tekst query uitvoeren en deze vervolgens casten naar integer:
demo=# select * from books where (data->'criticrating')::int4 > 4;
Voor expressie-indexen moet de index exact overeenkomen met de query-expressie. Onze index zou er dus ongeveer zo uitzien:
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-indexen
Als je alleen geïnteresseerd bent in de "=" operator, dan worden hash-indexen interessant. Denk bijvoorbeeld aan het geval wanneer we op zoek zijn naar een bepaalde tag op een boek. Het te indexeren element kan een element op het hoogste niveau zijn of diep genest.
Bijv. tags->publisher =XlekfkLOtL
CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));
Hash-indexen zijn meestal ook kleiner dan B-tree- of GIN-indexen. Dit hangt natuurlijk uiteindelijk af van je dataset.
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)
Speciale vermelding:GIN Trigram-indexen
PostgreSQL ondersteunt string-matching met behulp van trigram-indexen. Trigram-indexen werken door tekst op te splitsen in trigrammen. 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. Uit de documenten:
- jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON waarde.
- 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.
|