sql >> Databáze >  >> RDS >> PostgreSQL

Použití JSONB v PostgreSQL:Jak efektivně ukládat a indexovat data JSON v PostgreSQL

JSON je zkratka pro JavaScript Object Notation. Jedná se o otevřený standardní formát, který organizuje data do párů klíč/hodnota a polí podrobně popsaných v RFC 7159. JSON je nejběžnější formát používaný webovými službami k výměně dat, ukládání dokumentů, nestrukturovaných dat atd. V tomto příspěvku budeme abychom vám ukázali tipy a techniky, jak efektivně ukládat a indexovat data JSON v PostgreSQL.

Můžete se také podívat na náš webový seminář Práce s daty JSON v PostgreSQL vs. MongoDB ve spolupráci s PostgresConf, kde se dozvíte více na toto téma, a podívejte se na naši stránku SlideShare ke stažení snímků.

Proč ukládat JSON v PostgreSQL?

Proč by se relační databáze vůbec měla starat o nestrukturovaná data? Ukazuje se, že existuje několik scénářů, kde je to užitečné.

  1. Flexibilita schématu

    Jedním z hlavních důvodů pro ukládání dat pomocí formátu JSON je flexibilita schématu. Ukládání dat v JSON je užitečné, když je vaše schéma proměnlivé a často se mění. Pokud uložíte každý z klíčů jako sloupec, povede to k častým operacím DML – to může být obtížné, když je vaše datová sada velká – například sledování událostí, analýzy, značky atd. Poznámka:Pokud je konkrétní klíč vždy přítomen ve vašem dokumentu může mít smysl uložit jej jako sloupec první třídy. Více o tomto přístupu diskutujeme v části „Vzory a antipatterny JSON“ níže.

  2. Vnořené objekty

    Pokud vaše sada dat obsahuje vnořené objekty (jednoúrovňové nebo víceúrovňové), v některých případech je snazší je zpracovávat v JSON namísto denormalizace dat do sloupců nebo více tabulek.

  3. Synchronizace s externími zdroji dat

    Externí systém často poskytuje data jako JSON, takže to může být dočasné úložiště, než budou data přijata do jiných částí systému. Například transakce Stripe.

Časová osa podpory JSON v PostgreSQL

Podpora JSON v PostgreSQL byla zavedena ve verzi 9.2 a neustále se zlepšuje v každém dalším vydání.

  • Vlna 1:PostgreSQL 9.2  (2012) přidala podporu pro datový typ JSON

    Databáze JSON ve verzi 9.2 byla poměrně omezená (a pravděpodobně v tu chvíli přehnaná) – v podstatě oslavovaný řetězec s určitou validací JSON. Je užitečné ověřit příchozí JSON a uložit do databáze. Další podrobnosti jsou uvedeny níže.

  • Vlna 2:PostgreSQL 9.4 (2014) přidala podporu pro datový typ JSONB

    JSONB znamená „JSON Binary“ nebo „JSON lepší“ v závislosti na tom, koho se ptáte. Jedná se o rozložený binární formát pro uložení JSON. JSONB podporuje indexování dat JSON a je velmi efektivní při analýze a dotazování na data JSON. Ve většině případů, když pracujete s JSON v PostgreSQL, měli byste používat JSONB.

  • Vlna 3:PostgreSQL 12 (2019) přidala podporu pro SQL/JSON standard a dotazy JSONPATH

    JSONPath přináší do PostgreSQL výkonný dotazovací stroj JSON.

Kdy byste měli používat JSON vs. JSONB?

Ve většině případů byste měli používat JSONB. Existují však některé konkrétní případy, kdy JSON funguje lépe:

  • JSON zachovává původní formátování (neboli mezery) a pořadí klíčů.
  • JSON zachovává duplicitní klíče.
  • JSON je rychlejší ke zpracování než JSONB – pokud však provedete další zpracování, JSONB bude rychlejší.

Pokud například pouze přijímáte protokoly JSON a žádným způsobem se na ně neptáte, může být pro vás JSON lepší volbou. Pro účely tohoto blogu, když mluvíme o podpoře JSON v PostgreSQL, budeme v budoucnu odkazovat na JSONB.

Použití JSONB v PostgreSQL:Jak efektivně ukládat a indexovat data JSON v PostgreSQL Kliknutím na Tweet

Vzory a antipatterny JSONB

Pokud má PostgreSQL skvělou podporu pro JSONB, proč už potřebujeme sloupce? Proč prostě nevytvořit tabulku s objektem BLOB JSONB a zbavit se všech sloupců, jako je schéma níže:

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

Na konci dne jsou sloupce stále nejúčinnější technikou pro práci s daty. Úložiště JSONB má oproti tradičním sloupcům určité nevýhody:

  • PostreSQL neukládá statistiky sloupců pro sloupce JSONB

    PostgreSQL udržuje statistiky o rozložení hodnot v každém sloupci tabulky – nejběžnější hodnoty (MCV), položky NULL, histogram distribuce. Na základě těchto dat dělá plánovač dotazů PostgreSQL chytrá rozhodnutí o plánu, který se má pro dotaz použít. V tomto okamžiku PostgreSQL neukládá žádné statistiky pro sloupce nebo klíče JSONB. To může někdy vést ke špatným volbám, jako je použití spojení vnořených smyček vs. spojení hash atd. Podrobnější příklad je uveden v tomto příspěvku na blogu – Kdy se vyhnout JSONB ve schématu PostgreSQL.

  • Úložiště JSONB má za následek větší prostor úložiště

    Úložiště JSONB nededuplikuje názvy klíčů v JSON. To může mít za následek podstatně větší nároky na úložiště ve srovnání s MongoDB BSON na WiredTiger nebo tradičním sloupcovém úložiště. Provedl jsem jednoduchý test s níže uvedeným modelem JSONB, který ukládá asi 10 milionů řádků dat, a zde jsou výsledky – V některých ohledech je to podobné modelu úložiště MongoDB MMAPV1, kde byly klíče v JSONB uloženy tak, jak jsou, bez jakékoli komprese. Jednou dlouhodobou opravou je přesunout názvy klíčů do slovníku na úrovni tabulky a odkazovat na tento slovník namísto opakovaného ukládání názvů klíčů. Do té doby může být řešením používat kompaktnější názvy (ve stylu unixu) namísto popisnějších názvů. Pokud například ukládáte miliony instancí konkrétního klíče, bylo by z hlediska úložiště lepší pojmenovat jej „pb“ namísto „název vydavatele“.

Nejúčinnějším způsobem, jak využít JSONB v PostgreSQL, je kombinovat sloupce a JSONB. Pokud se klíč ve vašich objektech BLOB JSONB objevuje velmi často, je pravděpodobně lepší jej uložit jako sloupec. Použijte JSONB jako „chytit vše“ ke zpracování proměnných částí vašeho schématu a zároveň využijte tradiční sloupce pro pole, která jsou stabilnější.

Datové struktury JSONB

JSONB i MongoDB BSON jsou v podstatě stromové struktury, které k ukládání analyzovaných dat JSONB používají víceúrovňové uzly. MongoDB BSON má velmi podobnou strukturu.

Zdroj obrázků

JSONB &TOAST

Dalším důležitým hlediskem pro úložiště je, jak JSONB interaguje s TOAST (technika úložiště atributů nadměrné velikosti). Obvykle, když velikost vašeho sloupce přesáhne TOAST_TUPLE_THRESHOLD (výchozí 2 kb), PostgreSQL se pokusí komprimovat data a vejít se do 2 kb. Pokud to nefunguje, data se přesunou do úložiště mimo linku. Tomu se říká „TOASTOVÁNÍ“ dat. Když jsou data načtena, musí proběhnout opačný proces „deTOASTting“. Můžete také ovládat strategii úložiště TOAST:

  • Rozšířeno – Umožňuje ukládání a kompresi mimo linku (pomocí pglz). Toto je výchozí možnost.
  • Externí – Umožňuje ukládání mimo linku, ale ne kompresi.

Pokud dochází ke zpožděním kvůli kompresi nebo dekompresi TOAST, jednou z možností je proaktivně nastavit úložiště sloupců na „EXTENDED“. Všechny podrobnosti naleznete v tomto dokumentu PostgreSQL.

Operátory a funkce JSONB

PostgreSQL poskytuje řadu operátorů pro práci s JSONB. Z dokumentů:

Operátor Popis
-> Získat prvek pole JSON (indexováno od nuly, záporná celá čísla se počítají od konce)
-> Získat pole objektu JSON podle klíče
->> Získat prvek pole JSON jako text
->> Získat pole objektu JSON jako text
#> Získat objekt JSON na zadané cestě
#>> Získat objekt JSON na zadané cestě jako text
@> Obsahuje levá hodnota JSON pravé položky cesty/hodnoty JSON na nejvyšší úrovni?
<@ Jsou levé položky cesty/hodnoty JSON obsaženy na nejvyšší úrovni v rámci pravé hodnoty JSON?
? řetězec existuje jako klíč nejvyšší úrovně v rámci hodnoty JSON?
?| Proveďte některý z těchto řetězců pole existují jako klíče nejvyšší úrovně?
?& Proveďte všechny tyto řetězce pole existují jako klíče nejvyšší úrovně?
|| Zřetězit dvě hodnoty jsonb do nové hodnoty jsonb
- Smazat pár klíč/hodnota nebo řetězec prvek z levého operandu. Páry klíč/hodnota se shodují na základě jejich hodnoty klíče.
- Smažte více párů klíč/hodnota nebo řetězec prvky z levého operandu. Páry klíč/hodnota se shodují na základě jejich hodnoty klíče.
- Smažte prvek pole se zadaným indexem (záporná celá čísla se počítají od konce). Vyvolá chybu, pokud kontejner nejvyšší úrovně není pole.
#- Smažte pole nebo prvek se zadanou cestou (u polí JSON se záporná celá čísla počítají od konce)
@? Vrátí cesta JSON nějakou položku pro zadanou hodnotu JSON?
@@ Vrátí výsledek kontroly predikátu cesty JSON pro zadanou hodnotu JSON. V úvahu se bere pouze první položka výsledku. Pokud výsledek není booleovský, vrátí se null.

PostgreSQL také poskytuje řadu funkcí pro vytváření a zpracování pro práci s daty JSONB.

Indexy JSONB

JSONB poskytuje širokou škálu možností indexování dat JSON. Na vysoké úrovni se budeme zabývat 3 různými typy indexů – GIN, BTREE a HASH. Ne všechny typy indexů podporují všechny třídy operátorů, takže návrh indexů je potřeba naplánovat na základě typu operátorů a dotazů, které plánujete používat.

Indexy GIN

GIN znamená „Generalized Inverted indexes“. Z dokumentů:

„GIN je navržen pro zpracování případů, kdy položky, které mají být indexovány, jsou složené hodnoty a dotazy, které má index zpracovat, musí hledat prvek hodnoty, které se objevují ve složených položkách. Položky mohou být například dokumenty a dotazy mohou být hledání dokumentů obsahujících konkrétní slova.“

GIN podporuje dvě třídy operátorů:

  • jsonb_ops (výchozí) – ?, ?|, ?&, @>, @@, @? [Indexujte každý klíč a hodnotu v prvku JSONB]
  • jsonb_pathops – @>, @@, @? [Indexujte pouze hodnoty v prvku JSONB]
CREATE INDEX datagin ON books USING gin (data);

Operátory existence (?, ?|, ?&)

Tyto operátory lze použít ke kontrole existence klíčů nejvyšší úrovně v JSONB. Vytvořme index GIN na datovém sloupci JSONB. Najděte například všechny knihy dostupné v Braillově písmu. JSON vypadá asi takto:

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

Jak můžete vidět z výstupu vysvětlení, pro vyhledávání se používá index GIN, který jsme vytvořili. Co kdybychom chtěli najít knihy v Braillově písmu nebo v pevné vazbě?

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)

Index GIN podporuje operátory „existence“ pouze na klíčích „top-level“. Pokud klíč není na nejvyšší úrovni, index nebude použit. Výsledkem bude sekvenční skenování:

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)

Způsob, jak zkontrolovat existenci ve vnořených dokumentech, je použít „indexy výrazů“. Pojďme vytvořit index na 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)

Poznámka:Alternativou je použití operátoru @>:

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

To však funguje pouze v případě, že hodnotou je objekt. Pokud si tedy nejste jisti, zda je hodnotou objekt nebo primitivní hodnota, může to vést k nesprávným výsledkům.

Operátory cesty @>, <@

Operátor „path“ lze použít pro víceúrovňové dotazy na vaše data JSONB. Použijme to podobně jako ? operátor výše:

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)

Operátory cesty podporují dotazování vnořených objektů nebo objektů nejvyšší úrovně:

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

Dotazy mohou být také víceúrovňové:

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)

Index GIN „pathops“ třída operátorů

GIN také podporuje možnost „pathops“, která snižuje velikost indexu GIN. Když použijete možnost pathops, jedinou podporou operátora je „@>“, takže musíte být opatrní při svých dotazech. Z dokumentů:

„Technický rozdíl mezi indexem GIN jsonb_ops a jsonb_path_ops spočívá v tom, že první index vytváří nezávislé položky indexu pro každý klíč a hodnotu v datech, zatímco druhý vytváří položky indexu pouze pro každá hodnota v datech“

Index GIN pathops můžete vytvořit následovně:

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

Na mé malé datové sadě 1 milionu knih můžete vidět, že pathops GIN index je menší – měli byste otestovat svou datovou sadu, abyste pochopili úspory:

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

Pojďme znovu spustit náš předchozí dotaz s indexem pathops:

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)

Jak je však uvedeno výše, možnost „pathops“ nepodporuje všechny scénáře, které podporuje výchozí třída operátorů. S indexem GIN „pathops“ nejsou všechny tyto dotazy schopny využít index GIN. Abych to shrnul, máte menší index, ale podporuje omezenější případ použití.

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

Indexy B-stromu

Indexy B-stromu jsou nejběžnějším typem indexu v relačních databázích. Pokud však indexujete celý sloupec JSONB pomocí indexu B-stromu, jediné užitečné operátory jsou „=“, <, <=,>,>=. V podstatě to lze použít pouze pro porovnávání celých objektů, což má velmi omezený případ použití.

Běžnějším scénářem je použití „indexů výrazů“ B-stromu. Základní informace naleznete zde – Indexy výrazů. Indexy výrazů B-stromu mohou podporovat běžné porovnávací operátory ‚=‘, ‚<‘, ‚>‘, ‚>=‘, ‚<=‘. Jak si možná vzpomínáte, indexy GIN tyto operátory nepodporují. Podívejme se na případ, kdy chceme načíst všechny knihy s daty->kritizace> 4. Vytvořili byste tedy dotaz přibližně takto:

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.

No, to nefunguje, protože operátor „->“ vrací typ JSONB. Takže musíme použít něco takového:

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

Pokud používáte verzi před PostgreSQL 11, bude ošklivější. Nejprve musíte zadat dotaz jako text a poté jej přetypovat na celé číslo:

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

U indexů výrazu musí index přesně odpovídat výrazu dotazu. Náš index by tedy vypadal asi takto:

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 indexy

Pokud vás zajímá pouze operátor "=", pak jsou hash indexy zajímavé. Vezměme si například případ, kdy hledáme konkrétní značku na knize. Prvek, který má být indexován, může být prvek nejvyšší úrovně nebo hluboce vnořený.

Např. tags->publisher =XlekfkLOtL

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

Hash indexy také mívají menší velikost než indexy B-tree nebo GIN. To samozřejmě v konečném důsledku závisí na vaší datové sadě.

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)

Zvláštní zmínka:Indexy trigramů GIN

PostgreSQL podporuje párování řetězců pomocí trigramových indexů. 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. Nasazení datového centra Cloudera CDP na Oracle Cloud Infrastructure (OCI)

  2. Prozkoumání online indexových operací na úrovni oddílu v SQL Server 2014 CTP1

  3. Jak nastavím parametry ORDER BY pomocí připraveného příkazu PDO?

  4. Spouštění úloh údržby databáze SQL pomocí SQLCMD