JSON signifie JavaScript Object Notation. Il s'agit d'un format standard ouvert qui organise les données en paires clé/valeur et en tableaux détaillés dans la RFC 7159. JSON est le format le plus couramment utilisé par les services Web pour échanger des données, stocker des documents, des données non structurées, etc. Dans cet article, nous allons pour vous montrer des astuces et des techniques sur la façon de stocker et d'indexer efficacement les données JSON dans PostgreSQL.
Vous pouvez également consulter notre webinaire Travailler avec les données JSON dans PostgreSQL vs. MongoDB en partenariat avec PostgresConf pour en savoir plus sur le sujet, et consulter notre page SlideShare pour télécharger les diapositives.
Pourquoi stocker JSON dans PostgreSQL ?
Pourquoi une base de données relationnelle devrait-elle même se soucier des données non structurées ? Il s'avère qu'il existe quelques scénarios où cela est utile.
-
Flexibilité du schéma
L'une des principales raisons de stocker des données au format JSON est la flexibilité du schéma. Stocker vos données en JSON est utile lorsque votre schéma est fluide et change fréquemment. Si vous stockez chacune des clés sous forme de colonnes, cela entraînera des opérations DML fréquentes (cela peut être difficile lorsque votre ensemble de données est volumineux), par exemple, le suivi des événements, les analyses, les balises, etc. Remarque :Si une clé particulière est toujours présente dans votre document, il peut être judicieux de le stocker en tant que colonne de première classe. Nous abordons plus en détail cette approche dans la section "JSON Patterns &Antipatterns" ci-dessous.
-
Objets imbriqués
Si votre ensemble de données comporte des objets imbriqués (à un ou plusieurs niveaux), dans certains cas, il est plus facile de les gérer en JSON au lieu de dénormaliser les données en colonnes ou en plusieurs tables.
-
Synchronisation avec des sources de données externes
Souvent, un système externe fournit des données au format JSON, il peut donc s'agir d'un magasin temporaire avant que les données ne soient ingérées dans d'autres parties du système. Par exemple, les transactions Stripe.
Chronologie de la prise en charge de JSON dans PostgreSQL
La prise en charge de JSON dans PostgreSQL a été introduite dans la version 9.2 et s'est régulièrement améliorée dans chaque version à venir.
-
Vague 1 :PostgreSQL 9.2 (2012) a ajouté la prise en charge du type de données JSON
La base de données JSON dans la version 9.2 était assez limitée (et probablement surestimée à ce stade) – essentiellement une chaîne glorifiée avec une validation JSON ajoutée. Il est utile de valider le JSON entrant et de le stocker dans la base de données. Plus de détails sont fournis ci-dessous.
-
Vague 2 :PostgreSQL 9.4 (2014) a ajouté la prise en charge du type de données JSONB
JSONB signifie « JSON Binary » ou « JSON better » selon la personne à qui vous demandez. C'est un format binaire décomposé pour stocker JSON. JSONB prend en charge l'indexation des données JSON et est très efficace pour analyser et interroger les données JSON. Dans la plupart des cas, lorsque vous travaillez avec JSON dans PostgreSQL, vous devez utiliser JSONB.
-
Wave 3 :PostgreSQL 12 (2019) a ajouté la prise en charge des requêtes SQL/JSON standard et JSONPATH
JSONPath apporte un puissant moteur de requête JSON à PostgreSQL.
Quand devriez-vous utiliser JSON ou JSONB ?
Dans la plupart des cas, JSONB est ce que vous devriez utiliser. Cependant, il existe des cas spécifiques où JSON fonctionne mieux :
- JSON préserve la mise en forme d'origine (c'est-à-dire les espaces blancs) et l'ordre des clés.
- JSON préserve les clés en double.
- JSON est plus rapide à ingérer que JSONB. Cependant, si vous effectuez un traitement supplémentaire, JSONB sera plus rapide.
Par exemple, si vous ingérez simplement des journaux JSON et que vous ne les interrogez d'aucune façon, alors JSON pourrait être une meilleure option pour vous. Pour les besoins de ce blog, lorsque nous ferons référence à la prise en charge de JSON dans PostgreSQL, nous ferons référence à JSONB à l'avenir.
Utilisation de JSONB dans PostgreSQL :comment stocker et indexer efficacement les données JSON dans PostgreSQLCliquez pour tweeterModèles et anti-modèles JSONB
Si PostgreSQL offre un excellent support pour JSONB, pourquoi avons-nous encore besoin de colonnes ? Pourquoi ne pas simplement créer une table avec un blob JSONB et se débarrasser de toutes les colonnes comme le schéma ci-dessous :
CREATE TABLE test(id int, data JSONB, PRIMARY KEY (id));
En fin de compte, les colonnes restent la technique la plus efficace pour travailler avec vos données. Le stockage JSONB présente certains inconvénients par rapport aux colonnes traditionnelles :
-
PostreSQL ne stocke pas les statistiques de colonne pour les colonnes JSONB
PostgreSQL maintient des statistiques sur les distributions des valeurs dans chaque colonne de la table - valeurs les plus courantes (MCV), entrées NULL, histogramme de distribution. Sur la base de ces données, le planificateur de requêtes PostgreSQL prend des décisions intelligentes sur le plan à utiliser pour la requête. À ce stade, PostgreSQL ne stocke aucune statistique pour les colonnes ou les clés JSONB. Cela peut parfois entraîner de mauvais choix, comme l'utilisation de jointures de boucles imbriquées par rapport à des jointures de hachage, etc. Un exemple plus détaillé de ceci est fourni dans cet article de blog - Quand éviter JSONB dans un schéma PostgreSQL.
-
Le stockage JSONB entraîne une plus grande empreinte de stockage
Le stockage JSONB ne déduplique pas les noms de clé dans le JSON. Cela peut entraîner une empreinte de stockage considérablement plus importante par rapport à MongoDB BSON sur WiredTiger ou au stockage en colonne traditionnel. J'ai effectué un test simple avec le modèle JSONB ci-dessous stockant environ 10 millions de lignes de données, et voici les résultats - À certains égards, cela ressemble au modèle de stockage MongoDB MMAPV1 où les clés de JSONB étaient stockées telles quelles sans aucune compression. Une solution à long terme consiste à déplacer les noms de clé vers un dictionnaire au niveau de la table et à faire référence à ce dictionnaire au lieu de stocker les noms de clé à plusieurs reprises. Jusque-là, la solution de contournement pourrait être d'utiliser des noms plus compacts (de style Unix) au lieu de noms plus descriptifs. Par exemple, si vous stockez des millions d'instances d'une clé particulière, il serait préférable, en termes de stockage, de la nommer "pb" au lieu de "publisherName".
Le moyen le plus efficace d'exploiter JSONB dans PostgreSQL est de combiner des colonnes et JSONB. Si une clé apparaît très fréquemment dans vos blobs JSONB, il est probablement préférable de la stocker sous forme de colonne. Utilisez JSONB comme "fourre-tout" pour gérer les parties variables de votre schéma tout en tirant parti des colonnes traditionnelles pour les champs plus stables.
Structures de données JSONB
JSONB et MongoDB BSON sont essentiellement des structures arborescentes, utilisant des nœuds à plusieurs niveaux pour stocker les données JSONB analysées. MongoDB BSON a une structure très similaire.
Source des images
JSONB &TOAST
Une autre considération importante pour le stockage est la façon dont JSONB interagit avec TOAST (The Oversize Attribute Storage Technique). Généralement, lorsque la taille de votre colonne dépasse le TOAST_TUPLE_THRESHOLD (2kb par défaut), PostgreSQL tentera de compresser les données et de tenir dans 2kb. Si cela ne fonctionne pas, les données sont déplacées vers un stockage hors ligne. C'est ce qu'ils appellent "toaster" les données. Lorsque les données sont récupérées, le processus inverse "deTOASTting" doit se produire. Vous pouvez également contrôler la stratégie de stockage TOAST :
- Étendu – Permet le stockage et la compression hors ligne (à l'aide de pglz). Il s'agit de l'option par défaut.
- Externe – Permet le stockage hors ligne, mais pas la compression.
Si vous rencontrez des retards dus à la compression ou à la décompression TOAST, une option consiste à définir de manière proactive le stockage de la colonne sur "ÉTENDU". Pour tous les détails, veuillez consulter cette doc PostgreSQL.
Opérateurs et fonctions JSONB
PostgreSQL fournit une variété d'opérateurs pour travailler sur JSONB. À partir de la documentation :
Opérateur | Description |
---|---|
-> | Obtenir l'élément de tableau JSON (indexé à partir de zéro, les entiers négatifs comptent à partir de la fin) |
-> | Obtenir le champ d'objet JSON par clé |
->> | Obtenir l'élément de tableau JSON sous forme de texte |
->> | Obtenir le champ d'objet JSON sous forme de texte |
#> | Obtenir l'objet JSON au chemin spécifié |
#>> | Obtenir l'objet JSON au chemin spécifié sous forme de texte |
@> | La valeur JSON de gauche contient-elle les bonnes entrées de chemin/valeur JSON au niveau supérieur ? |
<@ | Les entrées de chemin/valeur JSON de gauche sont-elles contenues au niveau supérieur dans la valeur JSON de droite ? |
? | Est-ce que la chaîne existe-t-il en tant que clé de niveau supérieur dans la valeur JSON ? |
?| | Effectuez l'une de ces chaînes de tableau existent en tant que clés de niveau supérieur ? |
?& | Faire toutes ces chaînes de tableau existent en tant que clés de niveau supérieur ? |
|| | Concaténer deux valeurs jsonb en une nouvelle valeur jsonb |
- | Supprimer la paire clé/valeur ou la chaîne élément de l'opérande de gauche. Les paires clé/valeur sont mises en correspondance en fonction de leur valeur clé. |
- | Supprimer plusieurs paires clé/valeur ou chaîne éléments de l'opérande de gauche. Les paires clé/valeur sont mises en correspondance en fonction de leur valeur clé. |
- | Supprimez l'élément de tableau avec l'index spécifié (les entiers négatifs comptent à partir de la fin). Génère une erreur si le conteneur de niveau supérieur n'est pas un tableau. |
#- | Supprimer le champ ou l'élément avec le chemin spécifié (pour les tableaux JSON, les entiers négatifs comptent à partir de la fin) |
@? | Le chemin JSON renvoie-t-il un élément pour la valeur JSON spécifiée ? |
@@ | Renvoie le résultat de la vérification du prédicat de chemin JSON pour la valeur JSON spécifiée. Seul le premier élément du résultat est pris en compte. Si le résultat n'est pas booléen, alors null est renvoyé. |
PostgreSQL fournit également une variété de fonctions de création et de fonctions de traitement pour travailler avec les données JSONB.
Index JSONB
JSONB fournit un large éventail d'options pour indexer vos données JSON. De manière générale, nous allons explorer 3 types d'index différents :GIN, BTREE et HASH. Tous les types d'index ne prennent pas en charge toutes les classes d'opérateurs. Une planification est donc nécessaire pour concevoir vos index en fonction du type d'opérateurs et de requêtes que vous prévoyez d'utiliser.
Indices GIN
GIN signifie « index inversés généralisés ». À partir de la documentation :
"GIN est conçu pour gérer les cas où les éléments à indexer sont des valeurs composites, et les requêtes à gérer par l'index doivent rechercher l'élément valeurs qui apparaissent dans les éléments composites. Par exemple, les éléments peuvent être des documents et les requêtes peuvent être des recherches de documents contenant des mots spécifiques. »
GIN prend en charge deux classes d'opérateurs :
- jsonb_ops (par défaut) – ?, ?|, ?&, @>, @@, @ ? [Indexer chaque clé et valeur dans l'élément JSONB]
- jsonb_pathops – @>, @@, @ ? [Indexer uniquement les valeurs de l'élément JSONB]
CREATE INDEX datagin ON books USING gin (data);
Opérateurs d'existence (?, ?|, ?&)
Ces opérateurs peuvent être utilisés pour vérifier l'existence de clés de niveau supérieur dans le JSONB. Créons un index GIN sur la colonne de données JSONB. Par exemple, recherchez tous les livres disponibles en braille. Le JSON ressemble à ceci :
"{"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)
Comme vous pouvez le voir dans la sortie d'explication, l'index GIN que nous avons créé est utilisé pour la recherche. Et si nous voulions trouver des livres en braille ou en couverture rigide ?
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'index GIN prend en charge les opérateurs « existence » uniquement sur les clés de « niveau supérieur ». Si la clé n'est pas au niveau supérieur, l'index ne sera pas utilisé. Il en résultera une analyse séquentielle :
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)
La façon de vérifier l'existence dans les documents imbriqués consiste à utiliser des "index d'expression". Créons un index sur 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)
Remarque :une alternative ici consiste à utiliser l'opérateur @> :
select * from books where data @> '{"tags":{"nk455671":{}}}'::jsonb;
Cependant, cela ne fonctionne que si la valeur est un objet. Ainsi, si vous ne savez pas si la valeur est un objet ou une valeur primitive, cela peut entraîner des résultats incorrects.
Opérateurs de chemin @>, <@
L'opérateur « chemin » peut être utilisé pour les requêtes à plusieurs niveaux de vos données JSONB. Utilisons-le comme le ? opérateur ci-dessus :
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)
Les opérateurs de chemin prennent en charge l'interrogation d'objets imbriqués ou d'objets de niveau supérieur :
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
Les requêtes peuvent également être à plusieurs niveaux :
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)
Classe d'opérateur "pathops" de l'index GIN
GIN prend également en charge une option "pathops" pour réduire la taille de l'index GIN. Lorsque vous utilisez l'option pathops, le seul support de l'opérateur est le « @> », vous devez donc être prudent avec vos requêtes. À partir de la documentation :
« La différence technique entre un index GIN jsonb_ops et jsonb_path_ops est que le premier crée des éléments d'index indépendants pour chaque clé et valeur dans les données, tandis que le second crée des éléments d'index uniquement pour chaque valeur dans les données"
Vous pouvez créer un index pathops GIN comme suit :
CREATE INDEX dataginpathops ON books USING gin (data jsonb_path_ops);
Sur mon petit ensemble de données de 1 million de livres, vous pouvez voir que l'indice pathops GIN est plus petit - vous devriez tester avec votre ensemble de données pour comprendre les économies :
public | dataginpathops | index | sgpostgres | books | 67 MB | public | datatagsgin | index | sgpostgres | books | 84 MB |
Relançons notre requête d'avant avec l'index 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)
Cependant, comme mentionné ci-dessus, l'option "pathops" ne prend pas en charge tous les scénarios pris en charge par la classe d'opérateur par défaut. Avec un index GIN "pathops", toutes ces requêtes ne peuvent pas tirer parti de l'index GIN. Pour résumer, vous avez un index plus petit mais il prend en charge un cas d'utilisation plus limité.
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
Index B-Tree
Les index B-tree sont le type d'index le plus courant dans les bases de données relationnelles. Cependant, si vous indexez une colonne JSONB entière avec un index B-tree, les seuls opérateurs utiles sont "=", <, <=,>,>=. Essentiellement, cela ne peut être utilisé que pour des comparaisons d'objets entiers, ce qui a un cas d'utilisation très limité.
Un scénario plus courant consiste à utiliser des "index d'expression" B-tree. Pour une introduction, reportez-vous ici - Index sur les expressions. Les index d'expression B-tree peuvent prendre en charge les opérateurs de comparaison courants '=', '<', '>', '>=', '<='. Comme vous vous en souvenez peut-être, les index GIN ne prennent pas en charge ces opérateurs. Considérons le cas où nous voulons récupérer tous les livres avec un data->criticrating> 4. Ainsi, vous construiriez une requête quelque chose comme ceci :
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.
Eh bien, cela ne fonctionne pas puisque l'opérateur « -> » renvoie un type JSONB. Nous devons donc utiliser quelque chose comme ceci :
demo=# select * from books where (data->'criticrating')::int4 > 4;
Si vous utilisez une version antérieure à PostgreSQL 11, cela devient plus moche. Vous devez d'abord interroger sous forme de texte, puis le convertir en entier :
demo=# select * from books where (data->'criticrating')::int4 > 4;
Pour les index d'expression, l'index doit correspondre exactement à l'expression de la requête. Ainsi, notre index ressemblerait à ceci :
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.
Index de hachage
Si vous n'êtes intéressé que par l'opérateur "=", alors les index Hash deviennent intéressants. Par exemple, considérons le cas où nous recherchons une étiquette particulière sur un livre. L'élément à indexer peut être un élément de niveau supérieur ou profondément imbriqué.
Ex. tags->éditeur =XlekfkLOtL
CREATE INDEX publisherhash ON books USING HASH ((data->'publisher'));
Les index de hachage ont également tendance à être plus petits que les index B-tree ou GIN. Bien sûr, cela dépend en fin de compte de votre ensemble de données.
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)
Mention spéciale :index trigrammes GIN
PostgreSQL prend en charge la correspondance de chaînes à l'aide d'index de trigrammes. Les index trigrammes fonctionnent en divisant le texte en trigrammes. 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;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 valeur.
- 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; @.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.
|