JSON står for JavaScript Object Notation. Det er et åbent standardformat, som organiserer data i nøgle/værdi-par og arrays beskrevet i RFC 7159. JSON er det mest almindelige format, der bruges af webtjenester til at udveksle data, gemme dokumenter, ustrukturerede data osv. I dette indlæg går vi for at vise dig tips og teknikker til, hvordan du effektivt gemmer og indekserer JSON-data i PostgreSQL.
Du kan også tjekke vores Arbejde med JSON-data i PostgreSQL vs. MongoDB-webinar i samarbejde med PostgresConf for at lære mere om emnet, og se vores SlideShare-side for at downloade slides.
Hvorfor gemme JSON i PostgreSQL?
Hvorfor skal en relationsdatabase overhovedet bekymre sig om ustrukturerede data? Det viser sig, at der er et par scenarier, hvor det er nyttigt.
-
Skemafleksibilitet
En af hovedårsagerne til at gemme data ved hjælp af JSON-formatet er skemafleksibilitet. Det er nyttigt at gemme dine data i JSON, når dit skema er flydende og ændrer sig ofte. Hvis du gemmer hver af nøglerne som kolonner, vil det resultere i hyppige DML-handlinger – dette kan være svært, når dit datasæt er stort – for eksempel hændelsessporing, analyser, tags osv. Bemærk:Hvis en bestemt nøgle altid er til stede i dit dokument, kan det være fornuftigt at gemme det som en førsteklasses kolonne. Vi diskuterer mere om denne tilgang i afsnittet "JSON-mønstre og antimønstre" nedenfor.
-
Indlejrede objekter
Hvis dit datasæt har indlejrede objekter (enkelt eller flere niveauer), er det i nogle tilfælde nemmere at håndtere dem i JSON i stedet for at denormalisere dataene i kolonner eller flere tabeller.
-
Synkronisering med eksterne datakilder
Ofte leverer et eksternt system data som JSON, så det kan være et midlertidigt lager, før data indlæses i andre dele af systemet. For eksempel Stripe-transaktioner.
Tidslinje for JSON-understøttelse i PostgreSQL
JSON-understøttelse i PostgreSQL blev introduceret i 9.2 og er støt forbedret i hver udgivelse fremover.
-
Wave 1:PostgreSQL 9.2 (2012) tilføjede understøttelse af JSON-datatypen
JSON-databasen i 9.2 var ret begrænset (og sandsynligvis overhypet på det tidspunkt) – dybest set en glorificeret streng med en vis JSON-validering indsat. Det er nyttigt at validere indgående JSON og gemme i databasen. Flere detaljer findes nedenfor.
-
Wave 2:PostgreSQL 9.4 (2014) tilføjede understøttelse af JSONB-datatypen
JSONB står for "JSON Binary" eller "JSON better", afhængigt af hvem du spørger. Det er et dekomponeret binært format til at gemme JSON. JSONB understøtter indeksering af JSON-dataene og er meget effektiv til at parse og forespørge JSON-dataene. I de fleste tilfælde, når du arbejder med JSON i PostgreSQL, bør du bruge JSONB.
-
Wave 3:PostgreSQL 12 (2019) tilføjede understøttelse af SQL/JSON-standard- og JSONPATH-forespørgsler
JSONPath bringer en kraftfuld JSON-forespørgselsmotor til PostgreSQL.
Hvornår skal du bruge JSON vs. JSONB?
I de fleste tilfælde er JSONB det, du skal bruge. Der er dog nogle specifikke tilfælde, hvor JSON fungerer bedre:
- JSON bevarer den originale formatering (a.k.a et mellemrum) og rækkefølgen af tasterne.
- JSON bevarer dublerede nøgler.
- JSON er hurtigere at indtage i forhold til JSONB – men hvis du foretager yderligere behandling, vil JSONB være hurtigere.
For eksempel, hvis du bare indtager JSON-logfiler og ikke forespørger på dem på nogen måde, så er JSON måske en bedre mulighed for dig. I forbindelse med denne blog, når vi henviser til JSON-understøttelse i PostgreSQL, vil vi henvise til JSONB fremover.
Brug af JSONB i PostgreSQL:Sådan gemmes og indekseres JSON-data effektivt i PostgreSQLClik for at tweeteJSONB-mønstre og antimønstre
Hvis PostgreSQL har stor understøttelse af JSONB, hvorfor har vi så brug for kolonner længere? Hvorfor ikke bare oprette en tabel med en JSONB-blob og slippe af med alle kolonner som skemaet nedenfor:
CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));
I slutningen af dagen er kolonner stadig den mest effektive teknik til at arbejde med dine data. JSONB-lagring har nogle ulemper i forhold til traditionelle kolonner:
-
PostreSQL gemmer ikke kolonnestatistikker for JSONB-kolonner
PostgreSQL vedligeholder statistik om fordelingen af værdier i hver kolonne i tabellen - mest almindelige værdier (MCV), NULL-indgange, distributionshistogram. Baseret på disse data træffer PostgreSQL-forespørgselsplanlæggeren smarte beslutninger om den plan, der skal bruges til forespørgslen. På dette tidspunkt gemmer PostgreSQL ingen statistik for JSONB-kolonner eller -nøgler. Dette kan nogle gange resultere i dårlige valg som f.eks. brug af indlejrede loop-joins vs. hash-joins osv. Et mere detaljeret eksempel på dette findes i dette blogindlæg – When To Avoid JSONB In A PostgreSQL Schema.
-
JSONB-lagring resulterer i et større lagerplads
JSONB-lageret dedublerer ikke nøglenavnene i JSON. Dette kan resultere i et betydeligt større lagringsfodaftryk sammenlignet med MongoDB BSON på WiredTiger eller traditionel kolonnelagring. Jeg kørte en simpel test med nedenstående JSONB-model, der lagrer omkring 10 millioner rækker af data, og her er resultaterne - På nogle måder ligner dette MongoDB MMAPV1-lagermodellen, hvor nøglerne i JSONB blev gemt som de er uden nogen komprimering. En langsigtet løsning er at flytte nøglenavnene til en ordbog på tabelniveau og henvise til denne ordbog i stedet for at gemme nøglenavnene gentagne gange. Indtil da kan løsningen være at bruge mere kompakte navne (unix-stil) i stedet for mere beskrivende navne. Hvis du f.eks. gemmer millioner af forekomster af en bestemt nøgle, ville det være bedre lagringsmæssigt at navngive den "pb" i stedet for "publisherName".
Den mest effektive måde at udnytte JSONB i PostgreSQL er at kombinere kolonner og JSONB. Hvis en nøgle optræder meget ofte i dine JSONB-blobs, er den sandsynligvis bedre at blive gemt som en kolonne. Brug JSONB som en "fang alt" til at håndtere de variable dele af dit skema, mens du udnytter traditionelle kolonner til felter, der er mere stabile.
JSONB-datastrukturer
Både JSONB og MongoDB BSON er i det væsentlige træstrukturer, der bruger multi-level noder til at gemme de parsede JSONB-data. MongoDB BSON har en meget lignende struktur.
Billedkilde
JSONB &TOAST
En anden vigtig overvejelse for opbevaring er, hvordan JSONB interagerer med TOAST (The Oversize Attribute Storage Technique). Typisk, når størrelsen af din kolonne overstiger TOAST_TUPLE_THRESHOLD (2kb standard), vil PostgreSQL forsøge at komprimere dataene og passe ind i 2kb. Hvis det ikke virker, flyttes dataene til out-of-line lagring. Dette er, hvad de kalder "TOASTING" af dataene. Når dataene er hentet, skal den omvendte proces "deTOASTting" ske. Du kan også styre TOAST-lagringsstrategien:
- Udvidet – Giver mulighed for out-of-line lagring og komprimering (ved hjælp af pglz). Dette er standardindstillingen.
- Ekstern – Giver mulighed for out-of-line lagring, men ikke komprimering.
Hvis du oplever forsinkelser på grund af TOAST-komprimeringen eller dekomprimeringen, er en mulighed proaktivt at indstille kolonnelageret til "UDLÆGET". For alle detaljerne henvises til dette PostgreSQL-dokument.
JSONB-operatører og funktioner
PostgreSQL giver en række forskellige operatører til at arbejde på JSONB. Fra dokumenterne:
Operator | Beskrivelse |
---|---|
-> | Hent JSON-array-element (indekseret fra nul, negative heltal tæller fra slutningen) |
-> | Hent JSON-objektfelt med nøgle |
->> | Hent JSON-array-element som tekst |
->> | Hent JSON-objektfelt som tekst |
#> | Hent JSON-objekt på den angivne sti |
#>> | Hent JSON-objekt på den angivne sti som tekst |
@> | Indeholder den venstre JSON-værdi den højre JSON-sti/værdiindgang på øverste niveau? |
<@ | Er den venstre JSON-sti-/værdiposter indeholdt på øverste niveau inden for den højre JSON-værdi? |
? | Lager strengen eksisterer som en nøgle på øverste niveau i JSON-værdien? |
?| | Gør nogen af disse array strenge eksisterer som nøgler på øverste niveau? |
?& | Gør alle disse array strenge eksisterer som nøgler på øverste niveau? |
|| | Sæt to jsonb-værdier sammen til en ny jsonb-værdi |
- | Slet nøgle/værdi-par eller streng element fra venstre operand. Nøgle/værdi-par matches baseret på deres nøgleværdi. |
- | Slet flere nøgle/værdi-par eller streng elementer fra venstre operand. Nøgle/værdi-par matches baseret på deres nøgleværdi. |
- | Slet array-elementet med specificeret indeks (negative heltal tæller fra slutningen). Sender en fejl, hvis container på øverste niveau ikke er et array. |
#- | Slet feltet eller elementet med specificeret sti (for JSON-arrays tæller negative heltal fra slutningen) |
@? | Returnerer JSON-stien noget element for den angivne JSON-værdi? |
@@ | Returnerer resultatet af JSON-stiprædikatcheck for den angivne JSON-værdi. Kun det første punkt i resultatet tages i betragtning. Hvis resultatet ikke er boolesk, returneres null. |
PostgreSQL tilbyder også en række oprettelsesfunktioner og behandlingsfunktioner til at arbejde med JSONB-dataene.
JSONB-indekser
JSONB tilbyder en bred vifte af muligheder for at indeksere dine JSON-data. På et højt niveau skal vi grave i 3 forskellige typer indekser – GIN, BTREE og HASH. Ikke alle indekstyper understøtter alle operatørklasser, så planlægning er nødvendig for at designe dine indekser baseret på den type operatører og forespørgsler, du planlægger at bruge.
GIN-indekser
GIN står for "Generalized Inverted Indexes". Fra dokumenterne:
"GIN er designet til at håndtere tilfælde, hvor de elementer, der skal indekseres, er sammensatte værdier, og de forespørgsler, der skal håndteres af indekset, skal søge efter element værdier, der vises i de sammensatte elementer. For eksempel kunne emnerne være dokumenter, og forespørgslerne kunne være søgninger efter dokumenter, der indeholder specifikke ord."
GIN understøtter to operatørklasser:
- jsonb_ops (standard) – ?, ?|, ?&, @>, @@, @? [Indeksér hver nøgle og værdi i JSONB-elementet]
- jsonb_pathops – @>, @@, @? [Indeksér kun værdierne i JSONB-elementet]
OPRET INDEX datagin PÅ bøger VED HJÆLP AF gin (data);
Eksistensoperatører (?, ?|, ?&)
Disse operatorer kan bruges til at kontrollere, om der findes nøgler på øverste niveau i JSONB. Lad os oprette et GIN-indeks på data-JSONB-kolonnen. Find for eksempel alle bøger, der er tilgængelige i punktskrift. JSON ser nogenlunde sådan her ud:
"{"tags":{"nk594127":{"ik71786":"iv678771"}}, "braille":false, "keywords":["abc", "kef", "keh"], " hardcover":true, "publisher":"EfgdxUdvB0", "criticrating":1}
demo=# vælg * fra bøger, hvor data ? 'braille';id | forfatter | isbn | vurdering | data---------+-----------------+------------+-------- +------------------------------------------------ -------------------------------------------------- -------------------------------------------------- ------------------1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags":{"nk455671":{"ik937456":"iv506075"}}, "braille":true, "keywords":["abc", "kef", "keh"], "hardcover":falsk , "publisher":"zSfZIAjGGs", "criticrating":4}.....demo=# forklar analyser vælg * fra bøger, hvor data ? 'braille'; FORESPØRGSPLAN--------------------------------------------- -------------------------------------------------- -----------------------Bitmap Heap Scan på bøger (pris=12.75..1005.25 rows=1000 width=158) (faktisk tid=0.033..0.039 rows=15 loops=1)Kontroller igen tilstand:(data ? 'braille'::text)Heap Blocks:exact=2-> Bitmap Index Scan på datagin (pris=0.00..12.50 rows=1000 width=0) (faktisk tid) =0.022..0.022 rækker=15 sløjfer=1)Indekstilstand:(data ? 'braille'::tekst)Planlægningstid:0,102 msUdførelsestid:0,067 ms(7 rækker)
Som du kan se fra forklaringsoutputtet, bliver GIN-indekset, som vi oprettede, brugt til søgningen. Hvad hvis vi ville finde bøger, der var i blindeskrift eller i hardcover?
demo=# forklar analyse vælg * fra bøger, hvor data ?| array['braille','hardcover']; QUERY PLAN------------------------------------------------ -------------------------------------------------- ------------------------------Bitmap Heap Scan på bøger (pris=16.75..1009.25 rækker=1000 bredde=158) ( faktisk tid=0.029..0.035 rækker=15 sløjfer=1)Tjek igen tilstand:(data ?| '{braille,hardcover}'::text[])Heap Blocks:exact=2-> Bitmap Index Scan på datagin (cost=0.00..16.50 rows=1000 width=0) (faktisk tid=0.023..0.023 rows=15 loops=1)Indeks Cond:(data ?| '{braille,hardcover}'::text[])Planlægningstid:0.138 msExecution Time:0,057 ms(7 rows)
GIN-indekset understøtter kun "existence"-operatorerne på "top-level"-taster. Hvis nøglen ikke er på det øverste niveau, vil indekset ikke blive brugt. Det vil resultere i en sekventiel scanning:
demo=# vælg * fra bøger, hvor data->'tags' ? 'nk455671';id | forfatter | isbn | vurdering | data---------+-----------------+------------+-------- +------------------------------------------------ -------------------------------------------------- -------------------------------------------------- ------------------1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags":{"nk455671":{"ik937456":"iv506075"}}, "braille":true, "keywords":["abc", "kef", "keh"], "hardcover":falsk , "publisher":"zSfZIAjGGs", "criticrating":4}685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags":{"nk455671":{"ik615925":"iv253423"}}, "publisher":"b2NwVg7VY3", "criticrating":0}(2 rækker)demo=# forklar analyser vælg * fra bøger, hvor data ->'tags'? 'nk455671'; FORESPØRGSPLAN -------------------------------------------- -------------------------------------------------- ------------Seq Scan på bøger (pris=0.00..38807.29 rækker=1000 bredde=158) (faktisk tid=0.018..270.641 rækker=2 sløjfer=1)Filter:((data -> 'tags'::text) ? 'nk455671'::text)Rækker fjernet af filter:1000017Planlægningstid:0,078 msUdførelsestid:270,728 ms(5 rækker)
Måden til at kontrollere, om der findes i indlejrede dokumenter, er at bruge "udtryksindekser". Lad os oprette et indeks over data->tags:
OPRET INDEX datatagsgin PÅ bøger VED HJÆLP AF gin (data->'tags');
demo=# vælg * fra bøger, hvor data->'tags' ? 'nk455671';id | forfatter | isbn | vurdering | data---------+-----------------+------------+-------- +------------------------------------------------ -------------------------------------------------- -------------------------------------------------- ------------------1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags":{"nk455671":{"ik937456":"iv506075"}}, "braille":true, "keywords":["abc", "kef", "keh"], "hardcover":falsk , "publisher":"zSfZIAjGGs", "criticrating":4}685122 | GWfuvKfQ1PCe1IL | jnyhYYcF66 | 3 | {"tags":{"nk455671":{"ik615925":"iv253423"}}, "publisher":"b2NwVg7VY3", "criticrating":0}(2 rækker)demo=# forklar analyser vælg * fra bøger, hvor data ->'tags'? 'nk455671'; FORESPØRGSPLAN -------------------------------------------- -------------------------------------------------- --------------------------Bitmap Heap Scan på bøger (pris=12.75..1007.75 rækker=1000 bredde=158) (faktisk tid=0,031 ..0.035 rows=2 loops=1)Kontrollér igen tilstand:((data ->'tags'::text) ? 'nk455671'::text)Heap Blocks:exact=2-> Bitmap Index Scan på datatagsgin (cost=0.00 ..12.50 rows=1000 width=0) (faktisk tid=0.021..0.021 rows=2 loops=1)Indeks Cond:((data ->'tags'::text) ? 'nk455671'::text)Planlægningstid :0,098 ms Udførelsestid:0,061 ms(7 rækker)
Bemærk:Et alternativ her er at bruge @>-operatoren:
vælg * fra bøger, hvor data @> '{"tags":{"nk455671":{}}}'::jsonb;
Dette virker dog kun, hvis værdien er et objekt. Så hvis du er usikker på, om værdien er et objekt eller en primitiv værdi, kan det føre til forkerte resultater.
Stioperatører @>, <@
"sti"-operatoren kan bruges til forespørgsler på flere niveauer af dine JSONB-data. Lad os bruge det svarende til ? operatør ovenfor:
vælg * fra bøger hvor data @> '{"braille":true}'::jsonb;demo=# forklar analyser vælg * fra bøger hvor data @> '{"braille":true}'::jsonb; FORESPØRGSPLAN -------------------------------------------- -------------------------------------------------- -------------------Bitmap Heap Scan på bøger (pris=16.75..1009.25 rækker=1000 bredde=158) (faktisk tid=0.040..0.048 rækker=6 sløjfer =1)Gentjek tilstand:(data @> '{"braille":true}'::jsonb)Rækker fjernet af indeks Gentjek:9Heap Blocks:exact=2-> Bitmap Index Scan på datagin (pris=0.00..16.50 rækker) =1000 bredde=0) (faktisk tid=0,030..0.030 rækker=15 sløjfer=1)Indeks Cond:(data @> '{"braille":true}'::jsonb)Planlægningstid:0,100 msUdførelsestid:0,076 ms (8 rækker)
Stioperatorerne understøtter forespørgsler på indlejrede objekter eller objekter på øverste niveau:
demo=# vælg * fra bøger, hvor data @> '{"publisher":"XlekfkLOtL"}'::jsonb;id | forfatter | isbn | vurdering | data-----+-----------------+------------+--------+--- -------------------------------------------------- ----------------------------------346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags":{"nk88":{"ik37":"iv161"}}, "publisher":"XlekfkLOtL", "criticrating":3}(1 række)demo=# forklar analyser vælg * fra bøger, hvor data @> '{"publisher":"XlekfkLOtL"}'::jsonb; FORESPØRGSPLAN---------------------------------------- -------------------------------------------------- -----------------------------------Bitmap Heap Scan på bøger (pris=16.75..1009.25 rækker=1000 width=158) (faktisk tid=0.491..0.492 rækker=1 sløjfer=1) Tjek igen tilstand:(data @> '{"publisher":"XlekfkLOtL"}'::jsonb)Heap Blocks:exact=1-> Bitmap Indeksscanning på datagin (pris=0.00..16.50 rækker=1000 bredde=0) (faktisk tid=0.092..0.092 rækker=1 sløjfer=1) Indeks Cond:(data @> '{"publisher":"XlekfkLOtL"} '::jsonb)Planlægningstid:0,090 msUdførelsestid:0,523 ms
Forespørgslerne kan også være på flere niveauer:
demo=# vælg * fra bøger, hvor data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;id | forfatter | isbn | vurdering | data---------+-----------------+------------+-------- +------------------------------------------------ -------------------------------------------------- -------------------------------------------------- ------------------1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags":{"nk455671":{"ik937456":"iv506075"}}, "braille":true, "keywords":["abc", "kef", "keh"], "hardcover":falsk , "publisher":"zSfZIAjGGs", "criticrating":4}(1 række)
GIN Index "pathops" Operator Class
GIN understøtter også en "pathops"-mulighed for at reducere størrelsen af GIN-indekset. Når du bruger pathops-indstillingen, er den eneste operatørsupport "@>", så du skal være forsigtig med dine forespørgsler. Fra dokumenterne:
"Den tekniske forskel mellem et jsonb_ops og et jsonb_path_ops GIN-indeks er, at førstnævnte opretter uafhængige indekselementer for hver nøgle og værdi i dataene, mens sidstnævnte kun opretter indekselementer for hver værdi i dataene”
Du kan oprette et GIN pathops-indeks som følger:
OPRET INDEKS dataginpathops PÅ bøger VED HJÆLP AF gin (data jsonb_path_ops);
På mit lille datasæt på 1 million bøger kan du se, at pathops GIN-indeks er mindre – du bør teste med dit datasæt for at forstå besparelserne:
Lad os køre vores forespørgsel igen fra før med pathops-indekset:
demo=# vælg * fra bøger, hvor data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}'::jsonb;id | forfatter | isbn | vurdering | data---------+-----------------+------------+-------- +------------------------------------------------ -------------------------------------------------- -------------------------------------------------- ------------------1000005 | XEI7xShT8bPu6H7 | 2kD5XJDZUF | 0 | {"tags":{"nk455671":{"ik937456":"iv506075"}}, "braille":true, "keywords":["abc", "kef", "keh"], "hardcover":falsk , "publisher":"zSfZIAjGGs", "criticrating":4}(1 række)demo=# forklar vælg * fra bøger, hvor data @> '{"tags":{"nk455671":{"ik937456":"iv506075" }}}'::jsonb;FORREGNINGSPLAN---------------------------------------- --------------------------------------------------Bitmap Heap Scan på bøger (pris=12.75..1005.25 rows=1000 width=158) Tjek igen tilstand:(data @> '{"tags":{"nk455671":{"ik937456":"iv506075"}}}':jsonb)-> Bitmap Index Scan på dataginpathops (cost=0.00..12.50 rows=1000 width=0)Index Cond:(data @> '{"tags":{"nk455671":{"ik937456":"iv506075"} }}'::jsonb)(4 rækker)
Men som nævnt ovenfor understøtter "pathops"-indstillingen ikke alle de scenarier, som standardoperatørklassen understøtter. Med et "pathops" GIN-indeks er alle disse forespørgsler ikke i stand til at udnytte GIN-indekset. For at opsummere har du et mindre indeks, men det understøtter en mere begrænset anvendelse.
vælg * fra bøger, hvor data ? 'tags'; => Sekventiel skanning * fra bøger, hvor data @> '{"tags" :{}}'; => Sekventiel skanning * fra bøger, hvor data @> '{"tags" :{"k7888":{}}}' => Sekventiel scanning
B-Tree indekser
B-træindekser er den mest almindelige indekstype i relationelle databaser. Men hvis du indekserer en hel JSONB-kolonne med et B-træindeks, er de eneste nyttige operatorer "=", <, <=,>,>=. I bund og grund kan dette kun bruges til sammenligning af hele objekter, som har et meget begrænset brugstilfælde.
Et mere almindeligt scenarie er at bruge B-træet "udtryksindekser". For en primer, se her – Indekser på udtryk. B-træudtryksindekser kan understøtte de almindelige sammenligningsoperatorer '=', '<', '>', '>=', '<='. Som du måske husker, understøtter GIN-indekser ikke disse operatører. Lad os overveje tilfældet, når vi ønsker at hente alle bøger med en data->kritisk> 4. Så du ville bygge en forespørgsel noget som denne:
demo=# vælg * fra bøger, hvor data->'kritisker'> 4;FEJL:operator findes ikke:jsonb>=heltalLINE 1:vælg * fra bøger, hvor data->'kriterer'>=4;^TIP :Ingen operator matcher det angivne navn og argumenttyper. Du skal muligvis tilføje eksplicitte casts.
Det virker ikke, da operatoren '->' returnerer en JSONB-type. Så vi skal bruge noget som dette:
demo=# vælg * fra bøger hvor (data->'kritisker')::int4> 4;
Hvis du bruger en version før PostgreSQL 11, bliver den mere grim. Du skal først forespørge som tekst og derefter caste den til heltal:
demo=# vælg * fra bøger hvor (data->'kritisker')::int4> 4;
For udtryksindekser skal indekset være et nøjagtigt match med forespørgselsudtrykket. Så vores indeks ville se sådan ud:
demo=# CREATE INDEX criticrating PÅ bøger VED HJÆLP AF BTREE (((data->'criticrating')::int4));CREATE INDEXdemo=# forklar analyser vælg * fra bøger hvor (data->'criticrating')::int4 =3; FORESPØRGSPLAN--------------------------------------------- -------------------------------------------------- ------------------------------------Indeksscanning ved hjælp af kritik på bøger (pris=0.42..4626.93 rækker =5000 bredde=158) (faktisk tid=0,069..70.221 rækker=199883 sløjfer=1)Indeks Cond:(((data -> 'criticrating'::text))::heltal =3)Planlægningstid:0,103 msExecution Time :79.019 ms(4 rows)demo=# forklar analyse vælg * fra bøger hvor (data->'kritiske')::int4 =3;QUERY PLAN----------------- -------------------------------------------------- -------------------------------------------------- -------------Indeksscanning ved hjælp af kritik på bøger (pris=0.42..4626.93 rækker=5000 bredde=158) (faktisk tid=0.069..70.221 rækker=199883 sløjfer=1) Indeks Cond :(((data -> 'kritisk'::tekst))::heltal =3)Planlægningstid:0,103 msE xecution Time:79.019 ms(4 rows)1Fra oven kan vi se, at BTREE-indekset bliver brugt som forventet.
Hash-indekser
Hvis du kun er interesseret i "="-operatoren, bliver Hash-indekser interessante. Overvej for eksempel tilfældet, når vi leder efter et bestemt tag på en bog. Elementet, der skal indekseres, kan være et element på øverste niveau eller dybt indlejret.
F.eks. tags->udgiver =XlekfkLOtL
OPRET INDEX publisherhash PÅ bøger VED HJÆLP AF HASH ((data->'publisher'));
Hash-indekser har også en tendens til at være mindre i størrelse end B-tree- eller GIN-indekser. Dette afhænger selvfølgelig i sidste ende af dit datasæt.
demo=# vælg * fra bøger, hvor data->'publisher' ='XlekfkLOtL'demo-#;id | forfatter | isbn | vurdering | data-----+-----------------+------------+--------+--- -------------------------------------------------- ----------------------------------346 | uD3QOvHfJdxq2ez | KiAaIRu8QE | 1 | {"tags":{"nk88":{"ik37":"iv161"}}, "publisher":"XlekfkLOtL", "criticrating":3}(1 række)demo=# forklar analyser vælg * fra bøger, hvor data ->'publisher' ='XlekfkLOtL'; FORESPØRGSPLAN------------------------------------------------ -------------------------------------------------- -------------------------------Indeksscanning ved hjælp af publisherhash på bøger (pris=0,00..2.02 rækker=1 bredde=158 ) (faktisk tid=0.016..0.017 rækker=1 sløjfer=1)Indeks Cond:((data -> 'publisher'::text) ='XlekfkLOtL'::text)Planlægningstid:0,080 msExecution Time:0,035 ms(4) rækker)
Special Mention:GIN Trigram Indexes
PostgreSQL supports string matching using trigram indexes. Trigram indexes work by breaking up text into trigrams. 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;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. From the docs:
- jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON value.
- 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 msExecution 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; @.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:1000028Planning Time:0.095 msExecution 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.
|