查询 json / jsonb 列超慢。我可以使用索引吗?

Query on json / jsonb column super slow. Can I use an index?

我正在尝试加快查询存储在 PostgreSQL 数据库中的一些 json 数据。我继承了一个查询名为 data 的 PostgreSQL table 的应用程序,其中有一个名为 value 的字段,其中值是类型为 jsonb.[=16= 的 json 的 blob ]

它大约有 300 行,但需要 12 秒才能从 5 个 json 元素中 select 此数据。 json blob 有点大,但如果有帮助的话,我需要的数据都在 json 嵌套的顶层。

我尝试添加 CREATE INDEX idx_tbl_data ON data USING gin (value); 的索引,但这没有帮助。我应该使用其他索引吗?长期愿景是重新编写应用程序以将数据移出 json 但由于应用程序其他部分的复杂性,这至少需要 30-40 个工作日的工作,所以我正在寻找看看我能否在短期内加快速度。

不确定它是否有帮助,但构成此结果集的基础数据并不经常更改。 json blob 中更靠下的数据经常发生变化。

SELECT
  value::json ->> 'name' AS name,
  value::json ->> 'mnemonic' AS mnemonic,
  value::json ->> 'urlName' AS "urlName",
  value::json ->> 'countryCode' AS "countryCode",
  value::json #>>'{team}' AS team
FROM
  data;

就像 a_horse 已经建议的那样(您也提到了自己),正确的解决方法 是将这些属性提取到单独的列中,在某种程度上规范您的设计。

索引有帮助吗?

遗憾的是,没有(截至 Postgres 14)。

理论上可以工作。由于您的值是 big,Postgres 可以在 index-only 扫描中获取仅包含一些小属性的表达式索引,即使在检索所有行时(否则它会忽略索引)。

The manual:

However, PostgreSQL's planner is currently not very smart about such cases. It considers a query to be potentially executable by index-only scan only when all columns needed by the query are available from the index.

因此您必须将 value 本身包含在索引中,即使只是作为 INCLUDE 列 - 完全破坏了整个想法。不行。

你可能在短期内仍然可以做一些事情。两个关键引语:

I am looking to see if I can make this faster in short term

The json blobs are a bit large

数据类型

从查询中删除对 json 的强制转换。每次施放都会增加无意义的成本。

压缩

一个主要的成本因素是压缩。 Postgres 必须 "de-toast" the whole large column, just to extract some small attributes. Since Postgres 14, you can switch the compression algorithm (if support is enabled in your version!). The default is dictated by the config setting default_toast_compression,默认设置为 pglz。目前唯一可用的替代方案是 lz4。您可以按列设置。任何时候。

LZ4 (lz4) 速度相当快,但压缩通常要少一些。速度大约翻倍,但存储空间增加大约 10%(视情况而定!)。如果性能不是问题,最好坚持使用默认 LZ 算法的更强压缩 (pglz)。将来可能会有更多的压缩算法可供选择。

实施:

ALTER TABLE data
  ALTER COLUMN value SET COMPRESSION lz4;

为列设置新的 COMPRESSION 不会自动 re-compress。 Postgres 会记住压缩方法,并且只有 re-compresses 如果它无论如何都被强制 un-compress。您可能想要强制 re-compression 现有值。您可以查看:

SELECT pg_column_compression(value) FROM data LIMIT 10;

相关博客post:

GENERATED

虽然坚持使用损坏的设计,但您可以添加一些(小!)生成的列来覆盖您的查询:

ALTER TABLE data
  ADD COLUMN name text GENERATED ALWAYS AS (value::json ->> 'name') STORED
, ADD COLUMN mnemonic text GENERATED ALWAYS AS (value::json ->> 'mnemonic') STORED
...

然后只针对那些生成的列,根本不涉及大 value

SELECT name, mnemonic, ... FROM data;

这将绕过主要的性能问题。

参见:

  • Computed / calculated / virtual / derived columns in PostgreSQL

但是,您提到:

It's the data that is further down in the json blob that often changes.

value 的每次更改都会在生成的列上强制执行 re-check,从而增加写入成本。

我也有类似的性能问题,不幸的是我不是 运行 PostgreSQL 12+,所以不能使用 generated 列。

你有两个解决方案:

1.) 您的 table 应该对每一列进行硬编码,并且解析器应该插入到这些列中。 (理想解)

2.) 对查询进行materialized view 并向该视图的主键列添加索引。然后,您可以使用 refresh materialized view concurrently 刷新此视图,而不会对另一个 user/application 查询视图时的性能产生任何影响。