web-dev-qa-db-fra.com

Les index JSON postgres sont-ils suffisamment efficaces par rapport aux tables normalisées classiques?

Les versions actuelles de Postgresql ont introduit diverses fonctionnalités pour le contenu JSON, mais je suis inquiet si je devais vraiment les utiliser - je veux dire, il n'y a pas encore de "meilleure pratique" établie sur ce qui fonctionne et ce qui ne fonctionne pas, ou du moins je peux ' t le trouver.

J'ai un exemple spécifique - j'ai un tableau sur les objets qui, entre autres, contient une liste de noms alternatifs pour cet objet. Toutes ces données seront également incluses dans une colonne JSON à des fins de récupération. Par exemple (ignorer tous les autres champs non pertinents).

create table stuff (id serial primary key, data json);
insert into stuff(data) values('{"AltNames":["Name1","Name2","Name3"]}')

J'aurai besoin de quelques requêtes sous la forme "liste tous les objets dont l'un des altnames est 'foobar'." La taille de table attendue est de l'ordre de quelques millions d'enregistrements. Les requêtes JSON Postgres peuvent être utilisées pour cela, et elles peuvent également être indexées ( Index pour trouver un élément dans un tableau JSON , par exemple). Cependant, DEVRAIT-il être fait de cette façon ou s'agit-il d'une solution de contournement perverse qui n'est pas recommandée?

L'alternative classique, bien sûr, consiste à ajouter une table supplémentaire pour cette relation un-à-plusieurs, contenant le nom et une clé étrangère de la table principale; la performance de cela est bien comprise. Cependant, cela a ses propres inconvénients, car cela signifie soit la duplication des données entre cette table et JSON (avec un risque d'intégrité possible); ou en créant ces données de retour JSON dynamiquement à chaque demande, ce qui a sa propre pénalité de performance.

34
Peteris

J'aurai besoin de quelques requêtes sous la forme "liste tous les objets dont l'un des altnames est 'foobar'." La taille de table attendue est de l'ordre de quelques millions d'enregistrements. Les requêtes JSON Postgres peuvent être utilisées pour cela, et elles peuvent également être indexées (Index For Finding Element dans un tableau JSON, par exemple). Cependant, DEVRAIT-il être fait de cette façon ou s'agit-il d'une solution de contournement perverse qui n'est pas recommandée?

Cela peut être fait de cette façon, mais cela ne signifie pas que vous devriez le faire. Dans un certain sens, les meilleures pratiques sont déjà bien documentées (voir, par exemple, utiliser hstore vs utiliser XML vs utiliser EAV vs utiliser une table séparée) avec un nouveau type de données qui, à toutes fins utiles et pratiques (outre la validation et la syntaxe), n'est pas différent des options non structurées ou semi-structurées antérieures.

Autrement dit, c'est le même vieux cochon avec un nouveau maquillage.

JSON offre la possibilité d'utiliser index d'arbre de recherche inversés, de la même manière que hstore, les types de tableaux et les tsvectors. Ils fonctionnent bien, mais gardez à l'esprit qu'ils sont principalement conçus pour extraire des points dans un quartier (pensez aux types de géométrie) classés par distance, plutôt que pour extraire une liste de valeurs dans l'ordre lexicographique.

Pour illustrer, prenez les deux plans que la réponse de Roman décrit:

  • Celui qui fait un balayage d'index parcourt directement les pages du disque, récupérant les lignes dans l'ordre indiqué par l'index.
  • Celui qui fait un scan d'index bitmap commence par identifier chaque page de disque qui pourrait contenir une ligne, et les lit telles qu'elles apparaissent sur le disque, comme si c'était (et en fait, exactement comme) faire un balayage de séquence qui saute les zones inutiles.

Revenons à votre question: encombré et surdimensionné index d'arbre inversé améliorera en effet les performances de votre application si vous utilisez des tables Postgres en tant que magasins JSON géants. Mais ils ne sont pas non plus une solution miracle, et ils ne vous permettront pas d'obtenir une conception relationnelle appropriée en cas de goulots d'étranglement.

En fin de compte, le résultat n'est pas différent de ce que vous obtiendriez lorsque vous décidez d'utiliser hstore ou un EAV:

  1. S'il a besoin d'un index (c'est-à-dire qu'il apparaît fréquemment dans une clause where ou, plus important encore, dans une clause join), vous souhaiterez probablement les données dans un champ distinct.
  2. Si c'est principalement cosmétique, JSON/hstore/EAV/XML/tout ce qui vous fait dormir la nuit fonctionne bien.
25
Denis de Bernardy

Je dirais que ça vaut le coup d'essayer. J'ai créé un test (100000 enregistrements, ~ 10 éléments dans le tableau JSON) et vérifié comment cela fonctionne:

create table test1 (id serial primary key, data json);
create table test1_altnames (id int, name text);

create or replace function array_from_json(_j json)
returns text[] as
$func$
    select array_agg(x.elem::text)
    from json_array_elements(_j) as x(elem)
$func$
language sql immutable;

with cte as (
    select
        (random() * 100000)::int as grp, (random() * 1000000)::int as name
    from generate_series(1, 1000000)
), cte2 as (
    select
        array_agg(Name) as "AltNames"
    from cte
    group by grp
)
insert into test1 (data)
select row_to_json(t)
from cte2 as t

insert into test1_altnames (id, name)
select id, json_array_elements(data->'AltNames')::text
from test1

create index ix_test1 on test1 using gin(array_from_json(data->'AltNames'));
create index ix_test1_altnames on test1_altnames (name);

Requête JSON (ms sur ma machine):

select * from test1 where '{489147}' <@ array_from_json(data->'AltNames');

"Bitmap Heap Scan on test1  (cost=224.13..1551.41 rows=500 width=36)"
"  Recheck Cond: ('{489147}'::text[] <@ array_from_json((data -> 'AltNames'::text)))"
"  ->  Bitmap Index Scan on ix_test1  (cost=0.00..224.00 rows=500 width=0)"
"        Index Cond: ('{489147}'::text[] <@ array_from_json((data -> 'AltNames'::text)))"

Table de requête avec noms (15ms sur ma machine):

select * from test1 as t where t.id in (select tt.id from test1_altnames as tt where tt.name = '489147');

"Nested Loop  (cost=12.76..20.80 rows=2 width=36)"
"  ->  HashAggregate  (cost=12.46..12.47 rows=1 width=4)"
"        ->  Index Scan using ix_test1_altnames on test1_altnames tt  (cost=0.42..12.46 rows=2 width=4)"
"              Index Cond: (name = '489147'::text)"
"  ->  Index Scan using test1_pkey on test1 t  (cost=0.29..8.31 rows=1 width=36)"
"        Index Cond: (id = tt.id)"

Je dois également noter qu'il y a un certain coût à insérer/supprimer des lignes dans la table avec des noms (test1_altnames), c'est donc un peu plus compliqué que de simplement sélectionner des lignes. Personnellement, j'aime la solution avec JSON.

20
Roman Pekar