与经典规范化表相比,postgres JSON 索引是不是足够高效?
Posted
技术标签:
【中文标题】与经典规范化表相比,postgres JSON 索引是不是足够高效?【英文标题】:Are postgres JSON indexes efficient enough compared with classic normalized tables?与经典规范化表相比,postgres JSON 索引是否足够高效? 【发布时间】:2013-09-18 22:21:38 【问题描述】:当前的 Postgresql 版本已经为 JSON 内容引入了各种功能,但我担心我是否真的应该使用它们 - 我的意思是,还没有建立关于哪些有效和哪些无效的“最佳实践”,或者至少没找到。
我有一个具体的例子——我有一个关于对象的表,其中包含该对象的备用名称列表。所有这些数据也将包含在 JSON 列中以供检索。例如(跳过所有其他不相关的字段)。
create table stuff (id serial primary key, data json);
insert into stuff(data) values('"AltNames":["Name1","Name2","Name3"]')
我需要一些查询,格式为“列出所有替代名称之一为 'foobar' 的对象”。预期的表大小约为几百万条记录。 可以使用 Postgres JSON 查询,也可以对其进行索引(例如Index for finding an element in a JSON array)。但是,应该这样做还是不建议这样做的不正当的解决方法?
当然,经典的替代方法是为该一对多关系添加一个附加表,其中包含主表的名称和外键;它的表现是很好理解的。但是,这有其自身的缺点,因为这意味着该表和 JSON 之间的数据重复(可能存在完整性风险);或者在每次请求时动态创建 JSON 返回数据,这有其自身的性能损失。
【问题讨论】:
密切相关:***.com/questions/18404055/… 如果你只想存储一个数组,为什么不使用text[]
而不是JSON?
thebuild.com/presentations/pg-as-nosql-pgday-fosdem-2013.pdf
【参考方案1】:
我需要一些查询,格式为“列出所有替代名称之一为 'foobar' 的对象”。预期的表大小约为几百万条记录。可以使用 Postgres JSON 查询,也可以对其进行索引(例如,在 JSON 数组中查找元素的索引)。但是,应该这样做还是不建议这样做的不正当的解决方法?
可以这样做,但这并不意味着你应该这样做。从某种意义上说,最佳实践已经得到了很好的记录(例如,参见使用 hstore 与使用 XML 与使用 EAV 与使用单独的表)和一个新的数据类型,对于所有意图和实际目的(除了验证和语法),没有什么不同来自之前的非结构化或半结构化选项。
换个说法,就是一头换了新妆的老猪。
JSON 提供了使用反向搜索树索引的能力,就像 hstore、数组类型和 tsvector 一样。它们工作正常,但请记住,它们主要用于提取按距离排序的邻域中的点(想想几何类型),而不是按字典顺序提取值列表。
为了说明,以 Roman 的回答概述的两个计划:
索引扫描直接遍历磁盘页面,按索引指示的顺序检索行。 位图索引扫描首先识别可能包含一行的每个磁盘页面,并在它们出现在磁盘上时读取它们,就好像它是一样(实际上,就像) 执行跳过无用区域的序列扫描。回到您的问题:如果您将 Postgres 表用作巨型 JSON 存储,那么杂乱和过大的倒排树索引确实会提高您的应用程序的性能。但它们也不是灵丹妙药,在处理瓶颈时它们不会让您达到正确的关系设计。
归根结底,这与您决定使用 hstore 或 EAV 时得到的结果没有什么不同:
-
如果它需要一个索引(即它经常出现在 where 子句中,或者更重要的是出现在 join 子句中),您可能希望将数据放在单独的字段中。
如果它主要是装饰性的,那么 JSON/hstore/EAV/XML/whatever-makes-you-sleep-at-night 可以正常工作。
【讨论】:
+1 丹尼斯,好一个。我必须在 PostgreSQL 上为此测试 hstore 和 xml。【参考方案2】:我觉得值得一试。我创建了一些测试(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 的解决方案。
【讨论】:
在我的机器上使用 PostgreSQL 9.4 查询要快得多:两个查询大约 1 毫秒:D 1ms、15ms或30ms的测量值无关紧要,因为它们受一级CPU缓存、二级、RAM延迟、交换、系统负载、HDD/SSD/SATA/SATA2/SATA3的影响很大.......等等。您总是必须在大量数据上并且可能多次测量这些事情。以上是关于与经典规范化表相比,postgres JSON 索引是不是足够高效?的主要内容,如果未能解决你的问题,请参考以下文章