PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Używanie JSONB w PostgreSQL:Jak skutecznie przechowywać i indeksować dane JSON w PostgreSQL

JSON oznacza JavaScript Object Notation. Jest to otwarty format standardowy, który organizuje dane w pary klucz/wartość i tablice wyszczególnione w RFC 7159. JSON jest najczęściej używanym formatem używanym przez usługi sieciowe do wymiany danych, przechowywania dokumentów, danych nieustrukturyzowanych itp. W tym poście będziemy aby pokazać wskazówki i techniki dotyczące efektywnego przechowywania i indeksowania danych JSON w PostgreSQL.

Możesz również obejrzeć nasze webinarium Praca z danymi JSON w PostgreSQL vs. MongoDB we współpracy z PostgresConf, aby dowiedzieć się więcej na ten temat, i sprawdzić naszą stronę SlideShare aby pobrać slajdy.

Dlaczego przechowywać JSON w PostgreSQL?

Dlaczego relacyjna baza danych powinna w ogóle dbać o dane nieustrukturyzowane? Okazuje się, że jest kilka scenariuszy, w których jest to przydatne.

  1. Elastyczność schematu

    Jednym z głównych powodów przechowywania danych w formacie JSON jest elastyczność schematu. Przechowywanie danych w formacie JSON jest przydatne, gdy schemat jest płynny i często się zmienia. Jeśli przechowujesz każdy z kluczy jako kolumny, spowoduje to częste operacje DML – może to być trudne, gdy zestaw danych jest duży – na przykład śledzenie zdarzeń, analityka, tagi itp. Uwaga:jeśli określony klucz jest zawsze obecny w twoim dokumencie, sensowne może być zapisanie go jako kolumny pierwszej klasy. Omówimy więcej o tym podejściu w sekcji „Wzorce i antywzorce JSON” poniżej.

  2. Obiekty zagnieżdżone

    Jeśli Twój zestaw danych zawiera obiekty zagnieżdżone (jedno- lub wielopoziomowe), w niektórych przypadkach łatwiej jest obsługiwać je w formacie JSON, zamiast denormalizować dane do kolumn lub wielu tabel.

  3. Synchronizacja z zewnętrznymi źródłami danych

    Często system zewnętrzny dostarcza dane w formacie JSON, więc może to być tymczasowy magazyn przed pozyskiwaniem danych do innych części systemu. Na przykład transakcje Stripe.

Oś czasu obsługi JSON w PostgreSQL

Obsługa JSON w PostgreSQL została wprowadzona w wersji 9.2 i jest stale ulepszana w każdym kolejnym wydaniu.

  • Wave 1:PostgreSQL 9.2 (2012) dodał obsługę typu danych JSON

    Baza danych JSON w wersji 9.2 była dość ograniczona (i prawdopodobnie przesadzona w tym momencie) – w zasadzie gloryfikowany ciąg z wrzuconą walidacją JSON. Przydatne jest sprawdzanie poprawności przychodzącego JSON i przechowywanie w bazie danych. Więcej szczegółów podano poniżej.

  • Wave 2:PostgreSQL 9.4 (2014) dodał obsługę typu danych JSONB

    JSONB oznacza „JSON Binary” lub „JSON better” w zależności od tego, kogo zapytasz. Jest to zdekomponowany format binarny do przechowywania JSON. JSONB obsługuje indeksowanie danych JSON i bardzo wydajnie analizuje i odpytuje dane JSON. W większości przypadków, gdy pracujesz z JSON w PostgreSQL, powinieneś używać JSONB.

  • Wave 3:PostgreSQL 12 (2019) dodał obsługę zapytań SQL/JSON i JSONPATH

    JSONPath wprowadza potężny silnik zapytań JSON do PostgreSQL.

Kiedy należy używać JSON czy JSONB?

W większości przypadków powinieneś używać JSONB. Istnieją jednak konkretne przypadki, w których JSON działa lepiej:

  • JSON zachowuje oryginalne formatowanie (czyli białe znaki) i kolejność kluczy.
  • JSON zachowuje zduplikowane klucze.
  • JSON jest szybszy do przetworzenia w porównaniu z JSONB – jednak jeśli wykonasz jakiekolwiek dalsze przetwarzanie, JSONB będzie szybszy.

Na przykład, jeśli tylko pozyskujesz logi JSON i nie wysyłasz do nich żadnych zapytań, JSON może być dla Ciebie lepszą opcją. Na potrzeby tego bloga, kiedy będziemy odnosić się do obsługi JSON w PostgreSQL, będziemy odnosić się do JSONB w przyszłości.

Używanie JSONB w PostgreSQL:Jak skutecznie przechowywać i indeksować dane JSON w PostgreSQLKliknij, aby tweetować

Wzorce i antywzorce JSONB

Jeśli PostgreSQL ma świetne wsparcie dla JSONB, po co nam więcej kolumn? Dlaczego po prostu nie utworzyć tabeli z obiektem blob JSONB i pozbyć się wszystkich kolumn, jak w poniższym schemacie:

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

Pod koniec dnia kolumny są nadal najbardziej wydajną techniką pracy z danymi. Pamięć JSONB ma pewne wady w porównaniu z tradycyjnymi kolumnami:

  • PostreSQL nie przechowuje statystyk kolumn dla kolumn JSONB

    PostgreSQL przechowuje statystyki dotyczące dystrybucji wartości w każdej kolumnie tabeli – najczęstsze wartości (MCV), wpisy NULL, histogram dystrybucji. Na podstawie tych danych planer zapytań PostgreSQL podejmuje mądre decyzje dotyczące planu użytego dla zapytania. W tym momencie PostgreSQL nie przechowuje żadnych statystyk dla kolumn ani kluczy JSONB. Może to czasami skutkować złymi wyborami, takimi jak używanie złączeń zagnieżdżonych w pętli lub złączeń mieszających itp. Bardziej szczegółowy przykład tego znajduje się w tym poście na blogu – Kiedy unikać JSONB w schemacie PostgreSQL.

  • Pamięć JSONB zapewnia większą powierzchnię pamięci

    Magazyn JSONB nie deduplikuje nazw kluczy w JSON. Może to skutkować znacznie większą powierzchnią magazynową w porównaniu z MongoDB BSON na WiredTiger lub tradycyjnym magazynowaniu kolumnowym. Przeprowadziłem prosty test z poniższym modelem JSONB przechowującym około 10 milionów wierszy danych, a oto wyniki – pod pewnymi względami jest to podobne do modelu przechowywania MongoDB MMAPV1, w którym klucze w JSONB były przechowywane bez kompresji. Jednym z długoterminowych rozwiązań jest przeniesienie nazw kluczy do słownika na poziomie tabeli i odwoływanie się do tego słownika zamiast wielokrotnego przechowywania nazw kluczy. Do tego czasu obejściem może być użycie bardziej zwartych nazw (w stylu uniksowym) zamiast bardziej opisowych nazw. Na przykład, jeśli przechowujesz miliony wystąpień określonego klucza, lepiej byłoby nazwać go „pb” zamiast „publisherName”.

Najbardziej efektywnym sposobem wykorzystania JSONB w PostgreSQL jest łączenie kolumn i JSONB. Jeśli klucz pojawia się bardzo często w obiektach BLOB JSONB, prawdopodobnie lepiej jest przechowywać go jako kolumnę. Użyj JSONB jako „chwytania wszystkiego”, aby obsłużyć zmienne części schematu, jednocześnie wykorzystując tradycyjne kolumny do pól, które są bardziej stabilne.

Struktury danych JSONB

Zarówno JSONB, jak i MongoDB BSON są zasadniczo strukturami drzewiastymi, wykorzystującymi wielopoziomowe węzły do ​​przechowywania przeanalizowanych danych JSONB. MongoDB BSON ma bardzo podobną strukturę.

Źródło obrazów

JSONB &TOAST

Kolejną ważną kwestią dotyczącą pamięci masowej jest interakcja JSONB z TOAST (Technika przechowywania atrybutów oversize). Zazwyczaj, gdy rozmiar Twojej kolumny przekracza TOAST_TUPLE_THRESHOLD (domyślnie 2kb), PostgreSQL spróbuje skompresować dane i zmieścić się w 2kb. Jeśli to nie zadziała, dane zostaną przeniesione do pamięci poza siecią. To właśnie nazywają „TOASTINGIEM” danych. Po pobraniu danych musi nastąpić odwrotny proces „deTOASTting”. Możesz także kontrolować strategię przechowywania TOAST:

  • Rozszerzony – Pozwala na przechowywanie i kompresję poza linią (przy użyciu pglz). To jest opcja domyślna.
  • Zewnętrzny – Pozwala na przechowywanie poza linią, ale nie kompresję.

Jeśli występują opóźnienia z powodu kompresji lub dekompresji TOAST, jedną z opcji jest aktywne ustawienie przechowywania kolumn na „EXTENDED”. Aby uzyskać wszystkie szczegóły, zapoznaj się z tym dokumentem PostgreSQL.

Operatory i funkcje JSONB

PostgreSQL udostępnia różne operatory do pracy z JSONB. Z dokumentów:

Operator Opis
-> Pobierz element tablicy JSON (indeksowany od zera, ujemne liczby całkowite liczą się od końca)
-> Pobierz pole obiektu JSON według klucza
->> Pobierz element tablicy JSON jako tekst
->> Pobierz pole obiektu JSON jako tekst
#> Pobierz obiekt JSON w określonej ścieżce
#>> Pobierz obiekt JSON w określonej ścieżce jako tekst
@> Czy lewa wartość JSON zawiera prawe wpisy ścieżki/wartości JSON na najwyższym poziomie?
><@ Czy lewe wpisy ścieżki/wartości JSON znajdują się na najwyższym poziomie w prawej wartości JSON?
? Czy ciąg istnieje jako klucz najwyższego poziomu w wartości JSON?
?| Wykonaj dowolną z tych tablic ciągów istnieją jako klucze najwyższego poziomu?
?& Wykonaj wszystkie te tablice ciągi istnieją jako klucze najwyższego poziomu?
|| Połącz dwie wartości jsonb w nową wartość jsonb
- Usuń parę klucz/wartość lub ciąg element z lewego operandu. Pary klucz/wartość są dopasowywane na podstawie ich wartości klucza.
- Usuń wiele par klucz/wartość lub ciąg elementy z lewego operandu. Pary klucz/wartość są dopasowywane na podstawie ich wartości klucza.
- Usuń element tablicy z określonym indeksem (ujemne liczby całkowite liczą się od końca). Zgłasza błąd, jeśli kontener najwyższego poziomu nie jest tablicą.
#- Usuń pole lub element z określoną ścieżką (w przypadku tablic JSON ujemne liczby całkowite liczą się od końca)
@? Czy ścieżka JSON zwraca jakikolwiek element dla określonej wartości JSON?
@@ Zwraca wynik sprawdzenia predykatu ścieżki JSON dla określonej wartości JSON. Uwzględniana jest tylko pierwsza pozycja wyniku. Jeśli wynik nie jest logiczny, zwracany jest null.

PostgreSQL zapewnia również różnorodne funkcje tworzenia i przetwarzania do pracy z danymi JSONB.

Indeksy JSONB

JSONB zapewnia szeroką gamę opcji indeksowania danych JSON. Na wysokim poziomie zajmiemy się 3 różnymi rodzajami indeksów – GIN, BTREE i HASH. Nie wszystkie typy indeksów obsługują wszystkie klasy operatorów, dlatego konieczne jest planowanie, aby zaprojektować indeksy w oparciu o typ operatorów i zapytań, których planujesz używać.

Indeksy WZ

GIN oznacza „Uogólnione indeksy odwrócone”. Z dokumentów:

"WZ jest przeznaczony do obsługi przypadków, w których indeksowane elementy są wartościami złożonymi, a zapytania, które mają być obsługiwane przez indeks, muszą szukać elementu wartości, które pojawiają się w elementach złożonych. Na przykład pozycjami mogą być dokumenty, a zapytania mogą być wyszukiwaniami dokumentów zawierających określone słowa”.

GIN obsługuje dwie klasy operatorów:

  • jsonb_ops (domyślnie) – ?, ?|, ?&, @>, @@, @? [Indeksuj każdy klucz i wartość w elemencie JSONB]
  • jsonb_pathops – @>, @@, @? [Indeksuj tylko wartości w elemencie JSONB]
CREATE INDEX datagin ON books USING gin (data);

Operatory istnienia (?, ?|, ?&)

Te operatory mogą służyć do sprawdzania istnienia kluczy najwyższego poziomu w JSONB. Utwórzmy indeks GIN na kolumnie danych JSONB. Na przykład znajdź wszystkie książki dostępne w alfabecie Braille'a. JSON wygląda mniej więcej tak:

"{"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 widać z danych wyjściowych wyjaśnienia, utworzony przez nas indeks GIN jest używany do wyszukiwania. Co by było, gdybyśmy chcieli znaleźć książki w alfabecie Braille'a lub w twardej oprawie?

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)

Indeks GIN obsługuje operatory „istnienia” tylko na kluczach „najwyższego poziomu”. Jeśli klucz nie znajduje się na najwyższym poziomie, indeks nie zostanie użyty. Spowoduje to skanowanie sekwencyjne:

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)

Sposób sprawdzania istnienia w zagnieżdżonych dokumentach polega na użyciu „indeksów wyrażeń”. Stwórzmy indeks danych->tagów:

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)

Uwaga:Alternatywą jest tutaj użycie operatora @>:

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

Jednak działa to tylko wtedy, gdy wartość jest obiektem. Tak więc, jeśli nie masz pewności, czy wartość jest obiektem, czy wartością pierwotną, może to prowadzić do błędnych wyników.

Operatory ścieżki @>, <@

Operatora „ścieżka” można używać do wielopoziomowych zapytań dotyczących danych JSONB. Użyjmy go podobnie do ? operator powyżej:

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)

Operatory ścieżki obsługują zapytania o obiekty zagnieżdżone lub obiekty najwyższego poziomu:

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

Zapytania mogą być również wielopoziomowe:

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)

Klasa operatora „pathops” indeksu GIN

GIN obsługuje również opcję „pathops”, aby zmniejszyć rozmiar indeksu GIN. Kiedy używasz opcji pathops, jedynym wsparciem operatora jest „@>”, więc musisz być ostrożny ze swoimi zapytaniami. Z dokumentów:

"Techniczna różnica między indeksem GIN jsonb_ops i jsonb_path_ops polega na tym, że ten pierwszy tworzy niezależne elementy indeksu dla każdego klucza i wartości w danych, podczas gdy ten drugi tworzy elementy indeksu tylko dla każda wartość w danych”

Możesz utworzyć indeks GIN pathops w następujący sposób:

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

Na moim małym zbiorze danych obejmującym 1 milion książek widać, że indeks pathops GIN jest mniejszy – powinieneś przetestować swój zestaw danych, aby zrozumieć oszczędności:

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

Przeprowadźmy ponownie nasze zapytanie z indeksem 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)

Jednak, jak wspomniano powyżej, opcja „pathops” nie obsługuje wszystkich scenariuszy obsługiwanych przez domyślną klasę operatora. W przypadku indeksu GIN „pathops” wszystkie te zapytania nie są w stanie wykorzystać indeksu GIN. Podsumowując, masz mniejszy indeks, ale obsługuje on bardziej ograniczone przypadki użycia.

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

Indeksy B-Tree

Indeksy B-drzewa są najczęstszym typem indeksu w relacyjnych bazach danych. Jeśli jednak indeksujesz całą kolumnę JSONB za pomocą indeksu B-drzewa, jedynymi przydatnymi operatorami są „=”, <, <=,>,>=. Zasadniczo można tego używać tylko do porównywania całych obiektów, co ma bardzo ograniczony przypadek użycia.

Bardziej powszechnym scenariuszem jest użycie „indeksów wyrażeń” B-drzewa. Podstawowe informacje znajdziesz tutaj – Indeksy wyrażeń. Indeksy wyrażeń B-drzewa mogą obsługiwać typowe operatory porównania „=”, „<”, „>”, „>=”, „<=”. Jak być może pamiętasz, indeksy GIN nie obsługują tych operatorów. Rozważmy przypadek, w którym chcemy pobrać wszystkie książki z danymi->krytyka> 4. Więc zbuduj zapytanie podobne do tego:

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.

Cóż, to nie działa, ponieważ operator „->” zwraca typ JSONB. Więc musimy użyć czegoś takiego:

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

Jeśli używasz wersji wcześniejszej niż PostgreSQL 11, robi się bardziej brzydka. Musisz najpierw wykonać zapytanie jako tekst, a następnie przerzucić je na liczbę całkowitą:

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

W przypadku indeksów wyrażeń indeks musi być dokładnie zgodny z wyrażeniem zapytania. Nasz indeks wyglądałby więc mniej więcej tak:

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.

Indeksy haszujące

Jeśli interesuje Cię tylko operator „=", to indeksy haszujące stają się interesujące. Rozważmy na przykład przypadek, gdy szukamy konkretnego tagu w książce. Indeksowany element może być elementem najwyższego poziomu lub głęboko zagnieżdżonym.

Np. tagi->wydawca =XlekfkLOtL

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

Indeksy haszujące mają zwykle mniejszy rozmiar niż indeksy B-drzewa lub GIN. Oczywiście ostatecznie zależy to od zestawu danych.

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)

Wyróżnienie specjalne:Indeksy trygramów GIN

PostgreSQL obsługuje dopasowywanie ciągów za pomocą indeksów trygramowych. Indeksy trygramów działają, dzieląc tekst na trygramy. Trygramy to w zasadzie słowa podzielone na sekwencje 3 liter. Więcej informacji można znaleźć w dokumentacji. 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. Z dokumentów:

  • jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON wartość.
  • 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. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak włączyć logowanie powolnych zapytań w PostgreSQL?

  2. psql:nie można połączyć się z serwerem:Połączenie odrzucone Błąd podczas łączenia się ze zdalną bazą danych

  3. Najlepszy sposób na zainstalowanie hstore na wielu schematach w bazie danych Postgres?

  4. SpringBoot+Kotlin+Postgres i JSONB:org.hibernate.MappingException:Brak mapowania dialektu dla typu JDBC

  5. Używanie COALESCE do obsługi wartości NULL w PostgreSQL