与经典规范化表相比,postgres JSON索引是否足够高效?

时间:2013-09-14 08:09:13

标签: json postgresql postgresql-9.3

目前的Postgresql版本已经为JSON内容引入了各种功能,但我担心的是我是否真的应该使用它们 - 我的意思是,还没有“最佳实践”建立在什么有效,什么没有,或至少我找不到了。

我有一个具体的例子 - 我有一个关于对象的表,除其他外,它包含该对象的备用名称列表。所有这些数据也将包含在JSON列中以供检索。例如(跳过所有其他不相关的字段)。

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

我需要一些查询形式“列出其中一个altnames为'foobar'的所有对象。”预期的表大小大约为几百万条记录。 可以使用Postgres JSON查询,也可以将其编入索引(例如Index for finding an element in a JSON array)。但是,它应该以这种方式完成,还是不推荐使用反常的解决方法?

当然,经典的替代方法是为该一对多关系添加一个附加表,其中包含主表的名称和外键;这种表现很好理解。但是,这有其自身的缺点,因为它意味着该表与JSON之间的数据重复(可能存在完整性风险);或者在每次请求时动态创建JSON返回数据,这会对其产生性能损失。

2 个答案:

答案 0 :(得分:25)

  

我需要一些查询形式“列出其中一个altnames为'foobar'的所有对象。”预期的表大小大约为几百万条记录。可以使用Postgres JSON查询,也可以将其编入索引(例如,在JSON数组中查找元素索引)。但是,它应该以这种方式完成,还是不推荐使用反常的解决方法?

可以以这种方式完成,但这并不意味着你应该这样做。从某种意义上说,最佳实践已经很好地记录了(参见例如使用hstore vs使用XML与使用EAV而不是使用单独的表)和新的数据类型,对于所有意图和实际目的(除了验证和语法),没有什么不同来自先前的非结构化或半结构化选项。

换句话说,这是与新化妆相同的老猪。

JSON提供了使用反向搜索树索引的能力,与hstore,数组类型和tsvector相同。它们工作正常,但请记住,它们主要用于提取按距离排序的邻域中的点(思考几何类型),而不是按字典顺序提取值列表。

为了说明这一点,请采用Roman的答案概述的两个计划:

  • 执行索引扫描的人直接浏览磁盘页,按索引指示的顺序检索行。
  • 执行位图索引扫描的操作首先要识别可能包含行的每个磁盘页,然后将它们显示在磁盘上,就好像它一样(事实上,就像)进行跳过无用区域的序列扫描。

回到你的问题:如果你使用Postgres表作为巨型JSON商店,那么杂乱和超大的倒置树索引确实会提高你的应用程序的性能。但它们也不是银弹,在处理瓶颈时它们不会让你达到适当的关系设计。

最后,底线与决定使用hstore或EAV时的底线没有什么不同:

  1. 如果需要索引(即它经常出现在where子句中,或者更重要的是,出现在join子句中),您可能希望将数据放在一个单独的字段中。
  2. 如果它主要是装饰性的,那么JSON / hstore / EAV / XML /无论什么让你在晚上睡觉都能正常工作。

答案 1 :(得分:19)

我说它值得一试。我创建了一些测试(100000条记录,JSON数组中的10个元素)并检查它是如何工作的:

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);

在我的机器上查询JSON( 30ms ):

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

查询表名称(我的机器上 15ms ):

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

另外我必须注意,在名称(test1_altnames)的表中插入/删除行会有一些成本,所以它比仅选择行要复杂一些。我个人喜欢用JSON解决方案。