sql >> Databasteknik >  >> RDS >> PostgreSQL

Använda JSONB i PostgreSQL:Hur man effektivt lagrar och indexerar JSON-data i PostgreSQL

JSON står för JavaScript Object Notation. Det är ett öppet standardformat som organiserar data i nyckel/värdepar och arrayer som beskrivs i RFC 7159. JSON är det vanligaste formatet som används av webbtjänster för att utbyta data, lagra dokument, ostrukturerad data, etc. I det här inlägget ska vi gå för att visa dig tips och tekniker om hur du effektivt lagrar och indexerar JSON-data i PostgreSQL.

Du kan också kolla in vårt webbseminarium Arbeta med JSON-data i PostgreSQL vs. MongoDB i samarbete med PostgresConf för att lära dig mer om ämnet, och kolla in vår SlideShare-sida för att ladda ner bilderna.

Varför lagra JSON i PostgreSQL?

Varför ska en relationsdatabas ens bry sig om ostrukturerad data? Det visar sig att det finns några scenarier där det är användbart.

  1. Schemaflexibilitet

    En av de främsta anledningarna till att lagra data med JSON-formatet är schemaflexibilitet. Att lagra dina data i JSON är användbart när ditt schema är flytande och ändras ofta. Om du lagrar var och en av nycklarna som kolumner kommer det att resultera i frekventa DML-operationer – detta kan vara svårt när din datamängd är stor – till exempel händelsespårning, analyser, taggar, etc. Obs! Om en viss nyckel alltid finns närvarande i ditt dokument kan det vara meningsfullt att lagra det som en förstaklasskolumn. Vi diskuterar mer om detta tillvägagångssätt i avsnittet "JSON-mönster och antimönster" nedan.

  2. Inkapslade objekt

    Om din datamängd har kapslade objekt (enkla eller flera nivåer) är det i vissa fall lättare att hantera dem i JSON istället för att denormalisera data till kolumner eller flera tabeller.

  3. Synkronisera med externa datakällor

    Ofta tillhandahåller ett externt system data som JSON, så det kan vara en tillfällig lagring innan data matas in i andra delar av systemet. Till exempel Stripe-transaktioner.

Tidslinje för JSON-stöd i PostgreSQL

JSON-stöd i PostgreSQL introducerades i 9.2 och har stadigt förbättrats i varje version framöver.

  • Våg 1:PostgreSQL 9.2  (2012) lade till stöd för JSON-datatyp

    JSON-databasen i 9.2 var ganska begränsad (och förmodligen överhypad vid den tidpunkten) – i princip en glorifierad sträng med viss JSON-validering inlagd. Det är användbart att validera inkommande JSON och lagra i databasen. Mer information finns nedan.

  • Våg 2:PostgreSQL 9.4 (2014) lade till stöd för JSONB-datatyp

    JSONB står för "JSON Binary" eller "JSON better" beroende på vem du frågar. Det är ett nedbrutet binärt format för att lagra JSON. JSONB stöder indexering av JSON-data och är mycket effektiv på att analysera och fråga efter JSON-data. I de flesta fall, när du arbetar med JSON i PostgreSQL, bör du använda JSONB.

  • Våg 3:PostgreSQL 12 (2019) lade till stöd för SQL/JSON-standard och JSONPATH-frågor

    JSONPath tar med en kraftfull JSON-frågemotor till PostgreSQL.

När ska du använda JSON vs. JSONB?

I de flesta fall är JSONB det du ska använda. Det finns dock några specifika fall där JSON fungerar bättre:

  • JSON bevarar den ursprungliga formateringen (a.k.a blanksteg) och ordningen för nycklarna.
  • JSON bevarar dubbletter av nycklar.
  • JSON är snabbare att inta jämfört med JSONB – men om du gör någon ytterligare bearbetning kommer JSONB att vara snabbare.

Om du till exempel bara matar in JSON-loggar och inte frågar efter dem på något sätt, kan JSON vara ett bättre alternativ för dig. För den här bloggens syften, när vi hänvisar till JSON-stöd i PostgreSQL, kommer vi att hänvisa till JSONB framöver.

Använda JSONB i PostgreSQL:Hur man effektivt lagrar och indexerar JSON-data i PostgreSQLClicka för att tweeta

JSONB-mönster och antimönster

Om PostgreSQL har bra stöd för JSONB, varför behöver vi kolumner längre? Varför inte bara skapa en tabell med en JSONB-blobb och bli av med alla kolumner som schemat nedan:

CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));

I slutet av dagen är kolumner fortfarande den mest effektiva tekniken för att arbeta med din data. JSONB-lagring har några nackdelar jämfört med traditionella kolumner:

  • PostreSQL lagrar inte kolumnstatistik för JSONB-kolumner

    PostgreSQL upprätthåller statistik om fördelningarna av värden i varje kolumn i tabellen – mest vanliga värden (MCV), NULL-poster, distributionshistogram. Baserat på dessa data fattar PostgreSQL-frågeplaneraren smarta beslut om planen som ska användas för frågan. För närvarande lagrar PostgreSQL ingen statistik för JSONB-kolumner eller -nycklar. Detta kan ibland resultera i dåliga val som att använda kapslade loop-joins vs hash-joins, etc. Ett mer detaljerat exempel på detta finns i det här blogginlägget – When To Avoid JSONB In A PostgreSQL Schema.

  • JSONB-lagring resulterar i ett större lagringsutrymme

    JSONB-lagring deduplicerar inte nyckelnamnen i JSON. Detta kan resultera i betydligt större lagringsutrymme jämfört med MongoDB BSON på WiredTiger eller traditionell kolonnlagring. Jag körde ett enkelt test med JSONB-modellen nedan och lagrade cirka 10 miljoner rader med data, och här är resultaten - På vissa sätt liknar detta MongoDB MMAPV1-lagringsmodellen där nycklarna i JSONB lagrades som de är utan någon komprimering. En långsiktig lösning är att flytta nyckelnamnen till en ordbok på tabellnivå och hänvisa till denna ordbok istället för att lagra nyckelnamnen upprepade gånger. Tills dess kan lösningen vara att använda mer kompakta namn (unix-stil) istället för mer beskrivande namn. Om du till exempel lagrar miljontals instanser av en viss nyckel, skulle det vara bättre lagringsmässigt att namnge den "pb" istället för "publisherName".

Det mest effektiva sättet att utnyttja JSONB i PostgreSQL är att kombinera kolumner och JSONB. Om en nyckel dyker upp mycket ofta i dina JSONB-blobbar är det förmodligen bättre att lagra den som en kolumn. Använd JSONB som en "fånga allt" för att hantera de variabla delarna av ditt schema samtidigt som du använder traditionella kolumner för fält som är mer stabila.

JSONB-datastrukturer

Både JSONB och MongoDB BSON är i huvudsak trädstrukturer som använder flernivånoder för att lagra den analyserade JSONB-datan. MongoDB BSON har en mycket liknande struktur.

Bildkälla

JSONB &TOAST

En annan viktig faktor för lagring är hur JSONB interagerar med TOAST (The Oversize Attribute Storage Technique). Vanligtvis, när storleken på din kolumn överstiger TOAST_TUPLE_THRESHOLD (2kb standard), kommer PostgreSQL att försöka komprimera data och passa in 2kb. Om det inte fungerar flyttas data till out-of-line lagring. Det här är vad de kallar "ROSTA" data. När data hämtas måste den omvända processen "deTOASTting" ske. Du kan också styra TOAST-lagringsstrategin:

  • Utökad – Tillåter out-of-line lagring och komprimering (med pglz). Detta är standardalternativet.
  • Extern – Tillåter out-of-line lagring, men inte komprimering.

Om du upplever förseningar på grund av TOAST-komprimeringen eller dekomprimeringen, är ett alternativ att proaktivt ställa in kolumnlagringen till "EXTENDED". För all information, se detta PostgreSQL-dokument.

JSONB-operatörer och funktioner

PostgreSQL tillhandahåller en mängd olika operatörer att arbeta med JSONB. Från dokumenten:

Operator Beskrivning
-> Hämta JSON-matriselement (indexeras från noll, negativa heltal räknas från slutet)
-> Hämta JSON-objektfält med nyckel
->> Hämta JSON-arrayelement som text
->> Hämta JSON-objektfält som text
#> Hämta JSON-objekt på den angivna sökvägen
#>> Hämta JSON-objekt på den angivna sökvägen som text
@> Innehåller det vänstra JSON-värdet den högra JSON-sökvägen/värdeposterna på översta nivån?
<@ Finns den vänstra JSON-sökvägen/värdeposterna på översta nivån inom det högra JSON-värdet?
? Gör strängen existerar som en nyckel på toppnivå inom JSON-värdet?
?| Gör någon av dessa array-strängar existerar som nycklar på toppnivå?
?& Gör alla dessa array-strängar existerar som nycklar på toppnivå?
|| Sätt ihop två jsonb-värden till ett nytt jsonb-värde
- Ta bort nyckel/värdepar eller sträng element från vänster operand. Nyckel/värdepar matchas baserat på deras nyckelvärde.
- Ta bort flera nyckel-/värdepar eller sträng element från vänster operand. Nyckel/värdepar matchas baserat på deras nyckelvärde.
- Ta bort arrayelementet med specificerat index (negativa heltal räknas från slutet). Skickar ett fel om behållare på toppnivå inte är en array.
#- Ta bort fältet eller elementet med angiven sökväg (för JSON-matriser, negativa heltal räknas från slutet)
@? Returnerar JSON-sökväg något objekt för det angivna JSON-värdet?
@@ Returnerar resultatet av JSON-sökvägspredikatkontroll för det angivna JSON-värdet. Endast den första punkten i resultatet tas med i beräkningen. Om resultatet inte är booleskt returneras null.

PostgreSQL tillhandahåller också en mängd olika skapande- och bearbetningsfunktioner för att arbeta med JSONB-data.

JSONB-index

JSONB erbjuder ett brett utbud av alternativ för att indexera din JSON-data. På en hög nivå kommer vi att gräva i tre olika typer av index – GIN, BTREE och HASH. Alla indextyper stöder inte alla operatörsklasser, så det krävs planering för att designa dina index baserat på den typ av operatorer och frågor som du planerar att använda.

GIN-index

GIN står för "Generalized Inverted indexes". Från dokumenten:

"GIN är utformat för att hantera fall där objekten som ska indexeras är sammansatta värden och frågorna som ska hanteras av indexet behöver söka efter element värden som visas i de sammansatta objekten. Till exempel kan objekten vara dokument, och frågorna kan vara sökningar efter dokument som innehåller specifika ord.”

GIN stöder två operatörsklasser:

  • jsonb_ops (standard) – ?, ?|, ?&, @>, @@, @? [Indexera varje nyckel och värde i JSONB-elementet]
  • jsonb_pathops – @>, @@, @? [Indexera endast värdena i JSONB-elementet]
CREATE INDEX datagin ON books USING gin (data);

Existensoperatörer (?, ?|, ?&)

Dessa operatorer kan användas för att kontrollera om det finns nycklar på toppnivå i JSONB. Låt oss skapa ett GIN-index på data-JSONB-kolumnen. Hitta till exempel alla böcker som finns i punktskrift. JSON ser ut ungefär så här:

"{"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)

Som du kan se från den förklarade utdatan används GIN-indexet som vi skapade för sökningen. Tänk om vi ville hitta böcker som var i punktskrift eller inbunden?

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)

GIN-indexet stöder "existens"-operatorerna endast på "top-level"-tangenter. Om nyckeln inte är på toppnivån kommer indexet inte att användas. Det kommer att resultera i en sekventiell genomsökning:

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)

Sättet att kontrollera om det finns i kapslade dokument är att använda "uttrycksindex". Låt oss skapa ett index på data->taggar:

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)

Obs! Ett alternativ här är att använda @>-operatorn:

select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;

Detta fungerar dock bara om värdet är ett objekt. Så om du är osäker på om värdet är ett objekt eller ett primitivt värde kan det leda till felaktiga resultat.

Path Operators @>, <@

Operatorn "sökväg" kan användas för flernivåfrågor av dina JSONB-data. Låt oss använda det liknande ? operatör ovan:

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)

Sökvägsoperatorerna stöder sökning av kapslade objekt eller objekt på toppnivå:

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

Frågorna kan också vara på flera nivåer:

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” Operatörsklass

GIN stöder också ett "pathops"-alternativ för att minska storleken på GIN-indexet. När du använder alternativet pathops är det enda operatörsstödet "@>" så du måste vara försiktig med dina frågor. Från dokumenten:

"Den tekniska skillnaden mellan ett jsonb_ops och ett jsonb_path_ops GIN-index är att det förra skapar oberoende indexobjekt för varje nyckel och värde i datan, medan det senare skapar indexobjekt endast för varje värde i datan”

Du kan skapa ett GIN pathops-index enligt följande:

CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);

På min lilla dataset med 1 miljon böcker kan du se att pathops GIN-index är mindre – du bör testa med din datauppsättning för att förstå besparingarna:

public | dataginpathops | index | sgpostgres | books | 67 MB |
public | datatagsgin | index | sgpostgres | books | 84 MB |

Låt oss köra om vår fråga från tidigare med pathops-indexet:

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)

Men, som nämnts ovan, stöder "pathops"-alternativet inte alla scenarier som standardoperatörsklassen stöder. Med ett "pathops" GIN-index kan alla dessa frågor inte utnyttja GIN-indexet. Sammanfattningsvis har du ett mindre index men det stöder ett mer begränsat användningsfall.

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 index

B-trädindex är den vanligaste indextypen i relationsdatabaser. Men om du indexerar en hel JSONB-kolumn med ett B-trädindex är de enda användbara operatorerna "=", <, <=,>,>=. I huvudsak kan detta endast användas för jämförelser av hela objekt, som har ett mycket begränsat användningsfall.

Ett vanligare scenario är att använda B-trädets "uttrycksindex". För en primer, se här – Indexer på uttryck. B-träduttrycksindex kan stödja de vanliga jämförelseoperatorerna '=', '<', '>', '>=', '<='. Som du kanske minns stöder inte GIN-index dessa operatörer. Låt oss överväga fallet när vi vill hämta alla böcker med en data->kritisk> 4. Så, du skulle skapa en fråga ungefär så här:

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.

Det fungerar inte eftersom operatorn '->' returnerar en JSONB-typ. Så vi måste använda något sånt här:

demo=# select * from books where (data->'criticrating')::int4 > 4;

Om du använder en version före PostgreSQL 11 blir den fulare. Du måste först fråga som text och sedan casta den till heltal:

demo=# select * from books where (data->'criticrating')::int4 > 4;

För uttrycksindex måste indexet vara exakt matchat med frågeuttrycket. Så vårt index skulle se ut ungefär så här:

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.

Hashindex

Om du bara är intresserad av "="-operatorn blir Hash-index intressanta. Tänk till exempel på fallet när vi letar efter en viss tagg på en bok. Elementet som ska indexeras kan vara ett element på toppnivå eller djupt kapslat.

T.ex. tags->publisher =XlekfkLOtL

CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));

Hashindex tenderar också att vara mindre i storlek än B-tree eller GIN-index. Naturligtvis beror detta i slutändan på din datauppsättning.

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)

Särskilt omnämnande: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;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 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;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.

More tips for you

Which Is the Best PostgreSQL GUI?

PostgreSQL graphical user interface (GUI) tools help these open source database users to manage, manipulate, and visualize their data. In this post, we discuss the top 5 GUI tools for administering your PostgreSQL deployments. Learn more

Managing High Availability in PostgreSQL

Managing high availability in your PostgreSQL hosting is very important to ensuring your clusters maintain exceptional uptime and strong operational performance so your data is always available to your application. Learn more

PostgreSQL Connection Pooling:Part 1 – Pros &Cons

In modern apps, clients open a lot of connections. Developers are discouraged from holding a database connection while other operations take place. “Open a connection as late as possible, close as soon as possible”. Learn more


  1. Förbättra frågehastighet:enkelt SELECT i stora postgres-tabellen

  2. Hur dödar du alla nuvarande anslutningar till en SQL Server 2005-databas?

  3. Hur man löser ORA-28000 kontot är låst

  4. Tips för att migrera från HAProxy till ProxySQL