用于比较 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不知道用&gt;=的时候左边确实不为空吗?

我还在(ext-&gt;&gt;'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-&gt;&gt;'hired' &gt;= '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_seriesr,非常感谢。 :-) 【参考方案1】:

As documented in the manual GIN 索引仅支持以下运算符:??&amp;?|@&gt;@?@@。因此,通过添加(看似无用的)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 索引的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL比较两个jsonb对象

用于嵌套 jsonb 的雄辩的 Where 子句。 PostgreSQL

如何为 postgresql 中的唯一(不包括顺序)JSONB 列创建约束

如何逃脱? (问号)运算符在 Rails 中查询 Postgresql JSONB 类型

为啥我在postgresql的json数据中查询,速度会比mysql慢很多

如何比较 Laravel 中 JSONB 列的字段?