用于比较 JSONB 值的 PostgreSQL 索引
Posted
技术标签:
【中文标题】用于比较 JSONB 值的 PostgreSQL 索引【英文标题】:PostgreSQL index for comparison of JSONB values 【发布时间】:2020-11-30 17:18:49 【问题描述】:我们正在 PostgreSQL 12/13 上试验 JSONB,看看它是否比一堆扩展表(EAV,我猜)更适合可定制的扩展属性,到目前为止,我对结果印象深刻,尽管使用 GIN索引比一开始看起来更棘手。
实验表很简单:
create TABLE jtest (
id SERIAL PRIMARY KEY,
text text,
ext jsonb
);
CREATE INDEX jtest_ext_gin_idx ON jtest USING gin (ext);
我正在使用(更大版本的)这个巨大的块(仅引用 db-fiddle)插入一些不同的数据:
DO 'BEGIN
FOR r IN 1..100000 LOOP
IF r % 10 <= 3 THEN
-- some entries have no extension
INSERT INTO jtest (text, ext) VALUES (''json-'' || LPAD(r::text, 10, ''0''), NULL);
ELSEIF r % 10 = 7 THEN
-- let''s add some numbers and wannabe "dates"
INSERT INTO jtest (text, ext)
VALUES (''json-'' || LPAD(r::text, 10, ''0''), ('''' ||
''"hired": "'' || current_date - width_bucket(random(), 0, 1, 1000) || ''",'' ||
''"rating": '' || width_bucket(random(), 0, 1, 10) || '''')::jsonb);
ELSE
INSERT INTO jtest (text, ext)
VALUES (''json-'' || LPAD(r::text, 10, ''0''), (''"email": "user'' || r || ''@mycompany.com", "other-key-'' || r || ''": "other-value-'' || r || ''"'')::jsonb);
END IF;
END LOOP;
END';
各种精确值匹配操作很容易,GIN 非常适合这些操作。但我们也需要
示例查询是:
select * from jtest
where ext->>'hired' >= '2020-06-01' -- not using function index on its own
但是如果我添加语义上无用的并且索引开始:
select * from jtest
where ext->>'hired' >= '2020-06-01'
and ext?'hired';
这是fiddle example。
问题 #1:我可以在我们的应用程序中实现查询解释器以使其正常工作,但这是预期的行为吗? PG不知道用>=
的时候左边确实不为空吗?
我还在(ext->>'hired')
- fiddle here 上尝试了功能索引:
CREATE INDEX jtest_ext_hired1_idx ON jtest ((ext->>'hired'));
CREATE INDEX jtest_ext_hired2_idx ON jtest ((ext->>'hired')) WHERE ext ? 'hired';
第二个索引比第一个小很多,我不确定第一个是什么好。
问题 #2:当我使用 ext->>'hired' >= '2020-06-01'
执行查询时,它使用小提琴中的第一个 - 但在我的 15M 行测试中没有(只有 18k 行返回)。所以这是第一个混淆 - 我不想在小提琴上重新创建的内部测试(它会执行太久)应该更具体 - 但无论出于何种原因使用顺序扫描。为什么它在更大的表上使用顺序扫描?
答案 #2:在运行ANALYZE
之后,它确实做到了,并且变得很快。因为这不是最重要的问题,所以我在这里直接回答。
最后,不是问题,额外的AND ext ? 'hired'
它使用jtest_ext_hired2_idx
索引就好了(在小提琴和我更大的表中)。
问题#3:相当笼统,这甚至是正确的方法吗?如果我希望对 JSONB 中的值使用比较和 LIKE 操作,我可以用额外的功能索引来覆盖它吗?对于我们的案例来说,它似乎仍然比添加自定义列或连接扩展表更灵活,但它不能在未来咬我们吗?
【问题讨论】:
附注:dbfiddle.uk 适用于美元报价。 是的,我后来发现了一个,但已经转换了我的,我更喜欢 db-fiddle 或 sqlfiddle 的 UX。 dbfiddle.uk 插入所有行的速度也慢了很多倍。如果其他小提琴手理解美元报价,那肯定会很好。 :-( 顺便说一句:您实际上不需要 DO 块来生成测试数据:db-fiddle.com/f/2mRXT4wGjM2ZSftjgKyZce/14 哇,这是一个整洁的。我不知道如何使用来自generate_series
的r
,非常感谢。 :-)
【参考方案1】:
As documented in the manual GIN 索引仅支持以下运算符:?
、?&
、?|
、@>
、@?
、@@
。因此,通过添加(看似无用的)ext?'hired'
条件,您可以使优化器使用 GIN 索引(而不是功能索引)。
为了索引雇用日期,我将创建一个函数,将值提取为适当的日期。您不能使用索引表达式中的强制转换来做到这一点,因为强制转换不是不可变的。但是我们知道 yyyy-mm-dd 的强制转换确实是不可变的,所以创建一个标记为不可变的函数并没有错。
create function hire_date(p_input jsonb)
returns date
as
$$
select (p_input ->> 'hired')::date;
$$
language sql
strict
immutable;
那么你可以使用:
CREATE INDEX jtest_ext_hired1_idx ON jtest ( (hire_date(ext)) );
并且在where子句中使用该函数时直接使用该索引:
select *
from jtest
where hire_date(ext) >= '2020-06-01';
当然,如果键 'hire_date'
实际上不包含正确的 DATE 值,这将失败(但由于无法更新索引,在插入过程中它会失败)。
索引 LIKE 表达式通常很棘手,但如果您只留下锚定搜索字符串 (like 'foo%'
),则可以使用常规 b-tree 索引:
create index jtest_email on jtest ( (ext ->> 'email') varchar_pattern_ops);
要使用右锚搜索字符串 (like '%foo%'
) 索引 LIKE 表达式,您需要一个三元组索引。
【讨论】:
我接受这个答案,但经过更多思考后,我正在考虑为大多数查询依赖额外的ext?'attr'
,并仅为有需要的人添加专门的索引,例如用于高百分比行的属性。我认为拥有数十到数百个专用索引将是有害的(空间、插入/更新性能)。我们可能有许多不同的属性,也许不针对任何可能的查询进行优化是可以的 - 可以从 UI 创建。
GIN 索引不仅支持?
运算符。但他们都只测试平等。我认为您也无法获得“通用”索引来支持范围(或 LIKE)查询。尽管支持 JSON 路径运算符 @@
,但它似乎除了表达式内的相等条件外没有用于其他任何事情以上是关于用于比较 JSONB 值的 PostgreSQL 索引的主要内容,如果未能解决你的问题,请参考以下文章
用于嵌套 jsonb 的雄辩的 Where 子句。 PostgreSQL
如何为 postgresql 中的唯一(不包括顺序)JSONB 列创建约束
如何逃脱? (问号)运算符在 Rails 中查询 Postgresql JSONB 类型