PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Utilizzo di JSONB in ​​PostgreSQL:come archiviare e indicizzare in modo efficace i dati JSON in PostgreSQL

JSON sta per JavaScript Object Notation. È un formato standard aperto che organizza i dati in coppie chiave/valore e array dettagliati in RFC 7159. JSON è il formato più comune utilizzato dai servizi Web per scambiare dati, archiviare documenti, dati non strutturati, ecc. In questo post, andremo per mostrarti suggerimenti e tecniche su come archiviare e indicizzare efficacemente i dati JSON in PostgreSQL.

Puoi anche dare un'occhiata al nostro webinar Lavorare con i dati JSON in PostgreSQL e MongoDB in collaborazione con PostgresConf per saperne di più sull'argomento e dai un'occhiata alla nostra pagina SlideShare per scaricare le diapositive.

Perché archiviare JSON in PostgreSQL?

Perché un database relazionale dovrebbe preoccuparsi anche dei dati non strutturati? Si scopre che ci sono alcuni scenari in cui è utile.

  1. Flessibilità dello schema

    Uno dei motivi principali per archiviare i dati utilizzando il formato JSON è la flessibilità dello schema. L'archiviazione dei dati in JSON è utile quando lo schema è fluido e cambia frequentemente. Se memorizzi ciascuna delle chiavi come colonne, si verificheranno frequenti operazioni DML, ciò può essere difficile quando il tuo set di dati è di grandi dimensioni, ad esempio monitoraggio degli eventi, analisi, tag e così via. Nota:se una chiave particolare è sempre presente nel tuo documento, potrebbe avere senso memorizzarlo come una colonna di prima classe. Discutiamo di più su questo approccio nella sezione "Modelli e antipattern JSON" di seguito.

  2. Oggetti nidificati

    Se il tuo set di dati ha oggetti nidificati (a livello singolo o multilivello), in alcuni casi è più facile gestirli in JSON invece di denormalizzare i dati in colonne o più tabelle.

  3. Sincronizzazione con origini dati esterne

    Spesso un sistema esterno fornisce dati come JSON, quindi potrebbe essere un archivio temporaneo prima che i dati vengano inseriti in altre parti del sistema. Ad esempio, le transazioni Stripe.

Cronologia del supporto JSON in PostgreSQL

Il supporto JSON in PostgreSQL è stato introdotto nella versione 9.2 ed è costantemente migliorato in ogni versione successiva.

  • Wave 1:PostgreSQL 9.2  (2012) ha aggiunto il supporto per il tipo di dati JSON

    Il database JSON nella 9.2 era abbastanza limitato (e probabilmente sovrastimato a quel punto), fondamentalmente una stringa glorificata con una convalida JSON inserita. È utile convalidare il JSON in entrata e archiviarlo nel database. Maggiori dettagli sono forniti di seguito.

  • Wave 2:PostgreSQL 9.4 (2014) ha aggiunto il supporto per il tipo di dati JSONB

    JSONB sta per "JSON Binary" o "JSON Better" a seconda di chi chiedi. È un formato binario scomposto per archiviare JSON. JSONB supporta l'indicizzazione dei dati JSON ed è molto efficiente nell'analisi e nell'esecuzione di query sui dati JSON. Nella maggior parte dei casi, quando lavori con JSON in PostgreSQL, dovresti usare JSONB.

  • Wave 3:PostgreSQL 12 (2019) ha aggiunto il supporto per SQL/JSON standard e query JSONPATH

    JSONPath porta un potente motore di query JSON in PostgreSQL.

Quando dovresti usare JSON rispetto a JSONB?

Nella maggior parte dei casi, JSONB è quello che dovresti usare. Tuttavia, ci sono alcuni casi specifici in cui JSON funziona meglio:

  • JSON conserva la formattazione originale (aka spazi bianchi) e l'ordine delle chiavi.
  • JSON conserva le chiavi duplicate.
  • JSON è più veloce da assimilare rispetto a JSONB, tuttavia, se esegui ulteriori elaborazioni, JSONB sarà più veloce.

Ad esempio, se stai solo importando i log JSON e non li stai interrogando in alcun modo, allora JSON potrebbe essere un'opzione migliore per te. Ai fini di questo blog, quando ci riferiremo al supporto JSON in PostgreSQL, faremo riferimento a JSONB in ​​futuro.

Utilizzo di JSONB in ​​PostgreSQL:come archiviare e indicizzare in modo efficace i dati JSON in PostgreSQLFai clic per twittare

Modelli e antimodelli JSONB

Se PostgreSQL ha un ottimo supporto per JSONB, perché abbiamo più bisogno di colonne? Perché non creare semplicemente una tabella con un BLOB JSONB ed eliminare tutte le colonne come lo schema seguente:

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

Alla fine della giornata, le colonne sono ancora la tecnica più efficiente per lavorare con i tuoi dati. L'archiviazione JSONB presenta alcuni inconvenienti rispetto alle colonne tradizionali:

  • PostreSQL non memorizza le statistiche delle colonne per le colonne JSONB

    PostgreSQL mantiene statistiche sulle distribuzioni dei valori in ogni colonna della tabella: valori più comuni (MCV), voci NULL, istogramma di distribuzione. Sulla base di questi dati, il pianificatore di query PostgreSQL prende decisioni intelligenti sul piano da utilizzare per la query. A questo punto, PostgreSQL non memorizza alcuna statistica per le colonne o le chiavi JSONB. Questo a volte può comportare scelte sbagliate come l'utilizzo di join di loop nidificati rispetto a join hash, ecc. Un esempio più dettagliato di ciò è fornito in questo post del blog:Quando evitare JSONB in ​​uno schema PostgreSQL.

  • L'archiviazione JSONB si traduce in un footprint di archiviazione maggiore

    L'archiviazione JSONB non deduplica i nomi delle chiavi nel JSON. Ciò può comportare un footprint di archiviazione considerevolmente maggiore rispetto a MongoDB BSON su WiredTiger o allo storage a colonna tradizionale. Ho eseguito un semplice test con il modello JSONB sottostante che memorizza circa 10 milioni di righe di dati, ed ecco i risultati:in qualche modo è simile al modello di archiviazione MMAPV1 di MongoDB in cui le chiavi in ​​JSONB sono state archiviate così come sono senza alcuna compressione. Una soluzione a lungo termine consiste nello spostare i nomi delle chiavi in ​​un dizionario a livello di tabella e fare riferimento a questo dizionario invece di memorizzare ripetutamente i nomi delle chiavi. Fino ad allora, la soluzione potrebbe consistere nell'utilizzare nomi più compatti (stile unix) invece di nomi più descrittivi. Ad esempio, se stai archiviando milioni di istanze di una particolare chiave, sarebbe meglio in termini di archiviazione denominarla "pb" anziché "publisherName".

Il modo più efficiente per sfruttare JSONB in ​​PostgreSQL è combinare colonne e JSONB. Se una chiave appare molto frequentemente nei tuoi BLOB JSONB, probabilmente è meglio che venga archiviata come colonna. Usa JSONB come "catch all" per gestire le parti variabili del tuo schema sfruttando le colonne tradizionali per i campi più stabili.

Strutture di dati JSONB

Sia JSONB che MongoDB BSON sono essenzialmente strutture ad albero, che utilizzano nodi multilivello per archiviare i dati JSONB analizzati. MongoDB BSON ha una struttura molto simile.

Fonte immagini

JSONB &TOAST

Un'altra considerazione importante per lo storage è il modo in cui JSONB interagisce con TOAST (The Oversize Attribute Storage Technique). In genere, quando la dimensione della tua colonna supera TOAST_TUPLE_THRESHOLD (2kb predefinito), PostgreSQL tenterà di comprimere i dati e adattarsi a 2kb. Se ciò non funziona, i dati vengono spostati nell'archiviazione fuori linea. Questo è ciò che chiamano "tostare" i dati. Quando i dati vengono recuperati, è necessario che si verifichi il processo inverso di "deTOASTting". Puoi anche controllare la strategia di archiviazione TOAST:

  • esteso – Consente l'archiviazione e la compressione fuori linea (tramite pglz). Questa è l'opzione predefinita.
  • Esterno – Consente l'archiviazione fuori linea, ma non la compressione.

Se riscontri ritardi dovuti alla compressione o decompressione TOAST, un'opzione consiste nell'impostare in modo proattivo lo spazio di archiviazione della colonna su "ESTESA". Per tutti i dettagli, fare riferimento a questo documento PostgreSQL.

Operatori e funzioni JSONB

PostgreSQL fornisce una varietà di operatori per lavorare su JSONB. Dai documenti:

Operatore Descrizione
-> Ottieni l'elemento dell'array JSON (indicizzato da zero, gli interi negativi contano dalla fine)
-> Ottieni campo oggetto JSON per chiave
->> Ottieni elemento array JSON come testo
->> Ottieni campo oggetto JSON come testo
#> Ottieni l'oggetto JSON nel percorso specificato
#>> Ottieni l'oggetto JSON nel percorso specificato come testo
@> Il valore JSON di sinistra contiene le voci di percorso/valore JSON di destra al livello superiore?
<@ Le voci di percorso/valore JSON di sinistra sono contenute al livello superiore all'interno del valore JSON di destra?
? La stringa esiste come chiave di primo livello all'interno del valore JSON?
?| Esegui una di queste stringhe di array esistono come chiavi di primo livello?
?& Esegui tutte queste stringhe di array esistono come chiavi di primo livello?
|| Concatena due valori jsonb in un nuovo valore jsonb
- Elimina coppia chiave/valore o stringa elemento dall'operando sinistro. Le coppie chiave/valore vengono abbinate in base al loro valore chiave.
- Elimina più coppie chiave/valore o string elementi dall'operando sinistro. Le coppie chiave/valore vengono abbinate in base al loro valore chiave.
- Elimina l'elemento dell'array con l'indice specificato (gli interi negativi contano dalla fine). Genera un errore se il contenitore di primo livello non è un array.
#- Elimina il campo o l'elemento con il percorso specificato (per gli array JSON, gli interi negativi contano dalla fine)
@? Il percorso JSON restituisce un elemento per il valore JSON specificato?
@@ Restituisce il risultato del controllo del predicato del percorso JSON per il valore JSON specificato. Viene preso in considerazione solo il primo elemento del risultato. Se il risultato non è booleano, viene restituito null.

PostgreSQL fornisce anche una varietà di funzioni di creazione e di elaborazione per lavorare con i dati JSONB.

Indici JSONB

JSONB fornisce un'ampia gamma di opzioni per indicizzare i dati JSON. Ad alto livello, analizzeremo 3 diversi tipi di indici:GIN, BTREE e HASH. Non tutti i tipi di indici supportano tutte le classi di operatori, quindi è necessaria una pianificazione per progettare i tuoi indici in base al tipo di operatori e query che prevedi di utilizzare.

Indici GIN

GIN sta per "Indici invertiti generalizzati". Dai documenti:

"GIN è progettato per gestire i casi in cui gli elementi da indicizzare sono valori composti e le query che devono essere gestite dall'indice devono cercare l'elemento valori che appaiono all'interno degli elementi composti. Ad esempio, gli elementi potrebbero essere documenti e le query potrebbero essere ricerche di documenti contenenti parole specifiche."

GIN supporta due classi di operatori:

  • jsonb_ops (predefinito) – ?, ?|, ?&, @>, @@, @? [Indicizza ogni chiave e valore nell'elemento JSONB]
  • jsonb_patops – @>, @@, @? [Indicizza solo i valori nell'elemento JSONB]
CREATE INDEX datagin ON books USING gin (data);

Operatori di esistenza (?, ?|, ?&)

Questi operatori possono essere utilizzati per verificare l'esistenza di chiavi di primo livello nel JSONB. Creiamo un indice GIN sulla colonna JSONB dei dati. Ad esempio, trova tutti i libri disponibili in braille. Il JSON è simile a questo:

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

Come puoi vedere dall'output di spiegazione, l'indice GIN che abbiamo creato viene utilizzato per la ricerca. E se volessimo trovare libri in braille o con copertina rigida?

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)

L'indice GIN supporta gli operatori di "esistenza" solo sulle chiavi di "livello superiore". Se la chiave non è al livello superiore, l'indice non verrà utilizzato. Risulterà in una scansione sequenziale:

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)

Il modo per verificare l'esistenza nei documenti nidificati consiste nell'usare "indici di espressione". Creiamo un indice sui dati->tag:

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)

Nota:un'alternativa qui è usare l'operatore @>:

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

Tuttavia, funziona solo se il valore è un oggetto. Quindi, se non sei sicuro che il valore sia un oggetto o un valore primitivo, potrebbe portare a risultati errati.

Operatori di percorso @>, <@

L'operatore "percorso" può essere utilizzato per query multilivello dei dati JSONB. Usiamolo in modo simile al ? operatore sopra:

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)

Gli operatori di percorso supportano la query su oggetti nidificati o oggetti di primo livello:

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

Anche le query possono essere multilivello:

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" Classe operatore

GIN supporta anche un'opzione "pathops" per ridurre la dimensione dell'indice GIN. Quando usi l'opzione pathops, l'unico supporto dell'operatore è "@>", quindi devi stare attento con le tue domande. Dai documenti:

"La differenza tecnica tra un indice GIN jsonb_ops e un jsonb_path_ops è che il primo crea elementi di indice indipendenti per ogni chiave e valore nei dati, mentre il secondo crea elementi di indice solo per ogni valore nei dati”

Puoi creare un indice GIN pathops come segue:

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

Sul mio piccolo set di dati di 1 milione di libri, puoi vedere che l'indice GIN di pathops è più piccolo:dovresti testarlo con il tuo set di dati per capire i risparmi:

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

Eseguiamo nuovamente la nostra query di prima con l'indice 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)

Tuttavia, come accennato in precedenza, l'opzione "pathops" non supporta tutti gli scenari supportati dalla classe operatore predefinita. Con un indice GIN "percorso", tutte queste query non sono in grado di sfruttare l'indice GIN. Per riassumere, hai un indice più piccolo ma supporta un caso d'uso più limitato.

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

Indici B-Tree

Gli indici B-tree sono il tipo di indice più comune nei database relazionali. Tuttavia, se indicizzi un'intera colonna JSONB con un indice B-tree, gli unici operatori utili sono "=", <, <=,>,>=. In sostanza, questo può essere utilizzato solo per il confronto di oggetti interi, che ha un caso d'uso molto limitato.

Uno scenario più comune consiste nell'utilizzare gli "indici di espressione" dell'albero B. Per un primer, fare riferimento qui – Indici sulle espressioni. Gli indici delle espressioni B-tree possono supportare gli operatori di confronto comuni '=', '<', '>', '>=', '<='. Come ricorderete, gli indici GIN non supportano questi operatori. Consideriamo il caso in cui vogliamo recuperare tutti i libri con un data->criticare> 4. Quindi, costruiresti una query simile a questa:

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.

Beh, non funziona poiché l'operatore '->' restituisce un tipo JSONB. Quindi dobbiamo usare qualcosa del genere:

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

Se stai usando una versione precedente a PostgreSQL 11, diventa più brutto. Devi prima eseguire una query come testo e quindi eseguirne il cast su intero:

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

Per gli indici delle espressioni, l'indice deve corrispondere esattamente all'espressione della query. Quindi, il nostro indice sarebbe simile a questo:

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.

Indici hash

Se sei interessato solo all'operatore "=", gli indici hash diventano interessanti. Ad esempio, considera il caso in cui cerchiamo un tag particolare su un libro. L'elemento da indicizzare può essere un elemento di primo livello o profondamente annidato.

Es. tag->editore =XlekfkLOtL

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

Gli indici hash tendono anche ad essere di dimensioni inferiori rispetto agli indici B-tree o GIN. Ovviamente, questo dipende in definitiva dal tuo set di dati.

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)

Menzione speciale:GIN Trigram Indexes

PostgreSQL supporta la corrispondenza delle stringhe utilizzando gli indici del trigramma. Gli indici dei trigrammi funzionano suddividendo il testo in trigrammi. 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. Dai documenti:

  • jsonb_path_exists - Checks whether JSONB path returns any item for the specified JSON valore.
  • 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.

Altri suggerimenti per te

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. Ulteriori informazioni

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. Ulteriori informazioni

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”. Ulteriori informazioni