【问题标题】:Postgres - Performance of select for large jsonb columnPostgres - 大型 jsonb 列的选择性能
【发布时间】:2022-01-06 16:00:55
【问题描述】:

我们在我们的一个数据库表中使用 Postgres jsonb 类型。表结构如下:

CREATE TABLE IF NOT EXISTS public.draft_document (
    id bigserial NOT NULL PRIMARY KEY,
    ...
    document jsonb NOT NULL,
    ein_search character varying(11) NOT NULL
);

CREATE INDEX IF NOT EXISTS count_draft_document_idx ON public.draft_document USING btree (ein_search);
CREATE INDEX IF NOT EXISTS read_draft_document_idx ON public.draft_document USING btree (id, ein_search);

document 列的 json 结构可能会有所不同。以下是document 的可能架构示例:

"withholdingCredit": {  
    "type": "array",
    "items": {
        "$ref": "#/definitions/withholding"
    }
}

withholding 结构(数组元素)在哪里:

"withholding": {
    "properties": {
        ...
        "proportionalityIndicator": {
            "type": "boolean"
        },
        "tribute": {
            "$ref": "#/definitions/tribute"
        },
        "payingSourceEin": {
            "type": "string"
        },
        "value": {
            "type": "number"
        }
        ...
    }
    ...
},      
"tribute": {
    "type": "object",
    "properties": {
        "code": {
            "type": "number"
        },
        "additionalCode": {
            "type": "number"
        }
        ...
    }
}

这里是一个将json转换成documentjsonb列的例子:

{
   "withholdingCredit":[
      {
         "value": 15000,
         "tribute":{
            "code": 1216,
            "additionalCode": 2
         },
         "payingSourceEin": "03985506123132",
         "proportionalityIndicator": false
      },
      ...
      {
         "value": 98150,
         "tribute":{
            "code": 3155,
            "additionalCode": 1
         },
         "payingSourceEin": "04185506123163",
         "proportionalityIndicator": false
      }
   ]
}

数组中元素的最大数量可以变化,最大限制为 100.000(十万)个元素。这是业务限制。

我们需要一个分页选择查询,它返回分解后的 withholding 数组(每行 1 个元素),其中每一行还包含 withholding 元素 valuearray length 中的 sum。 查询还需要通过proportionalityIndicatortribute-->codetribute-->additionalCodepayingSourceEin返回预扣税ordered。比如:

id sum jsonb_array_length jsonb_array_elements
30900 1.800.027 2300 {"value":15000,"tribute":{"code":1216,...}, ...}
... ... ... { ... }
30900 1.800.027 2300 {"value":98150,"tribute":{"code":3155,...}, ...}

我们定义了以下查询:

SELECT dft.id, 
    SUM((elem->>'value')::NUMERIC),
    jsonb_array_length(dft.document->'withholdingCredit'),
    jsonb_array_elements(jsonb_agg(elem 
    ORDER BY 
        elem->>'proportionalityIndicator',
        (elem->'tribute'->>'code')::NUMERIC,
        (elem->'tribute'->>'additionalCode')::NUMERIC,
        elem->>'payingSourceEin'))
FROM 
    draft_document dft
    CROSS JOIN LATERAL jsonb_array_elements(dft.document->'withholdingCredit') arr(elem)
WHERE (dft.document->'withholdingCredit') IS NOT NULL
    AND dft.id = :id
    AND dft.ein_search = :ein_search
GROUP BY dft.id
LIMIT :limit OFFSET :offset;

此查询有效,但当我们在 jsonb 数组中有大量元素时,性能会受到限制。 欢迎任何关于如何改进它的建议。

顺便说一句,我们使用的是 Postgres 9.6。

【问题讨论】:

  • 放弃使用 JSON 的反规范化,创建一个正确规范化的模型。
  • 9.6 版刚刚收到最后一个补丁,不再支持。计划升级到更新的版本,例如 13 或 14。从 json 对象中提取 100.000 个元素永远不会很快。
  • 如果您需要以单独的方式管理 JSON 的各个部分以用于分页或其他目的,那么我将规范化数据库模型并将数据分离到不同的相关实体中。分页和管理将变得非常简单。当您需要将文档作为一个整体来处理并且您对分析它或查看它的细节没有兴趣时,JSON 非常有用。
  • 感谢您的反馈。我们计划对该模型的一部分进行规范化。我们选择使用 json 作为document,因为它的结构需要灵活。

标签: json postgresql performance indexing jsonb


【解决方案1】:

您将其拆分、聚合并再次拆分的奇怪查询似乎确实触发了 PostgreSQL 中的一些病态内存管理问题(在 15dev 上测试)。也许你应该就此提交一份错误报告。

但您只需将其拆开一次即可避免此问题。然后,您需要使用窗口函数来获取要包含所有行的列表,即使是那些被偏移量和限制删除的行。

SELECT dft.id, 
    SUM((elem->>'value')::NUMERIC) over (),
    count(*) over (),                                     
    elem                                
FROM 
    draft_document dft
    CROSS JOIN LATERAL jsonb_array_elements(dft.document->'withholdingCredit') arr(elem)
WHERE (dft.document->'withholdingCredit') IS NOT NULL
    AND dft.id = 4
    AND dft.ein_search = '4' 
ORDER BY 
        elem->>'proportionalityIndicator',
        (elem->'tribute'->>'code')::NUMERIC,
        (elem->'tribute'->>'additionalCode')::NUMERIC,
        elem->>'payingSourceEin' 
limit 4 offset 500;

在我手中,这给出了与您的查询相同的答案,但需要 370 毫秒而不是 13,789 毫秒。

在更高的偏移量下,我的查询仍然有效,而您的查询导致完全锁定,需要硬重置。

如果有人想重现不良行为,我通过以下方式生成数据:

insert into draft_document select 4, jsonb_build_object('withholdingCredit',jsonb_agg(jsonb_build_object('value',floor(random()*99999)::int,'tribute','{"code": 1216, "additionalCode": 2}'::jsonb,'payingSourceEin',floor(random()*99999999)::int,'proportionalityIndicator',false))),'4' from generate_series(1,100000) group by 1,3;

【讨论】:

  • 哇,太棒了。非常感谢你,杰尼斯!如果可以的话,我会加倍投票。
猜你喜欢
  • 2017-06-19
  • 2021-02-28
  • 1970-01-01
  • 2022-11-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-20
  • 1970-01-01
相关资源
最近更新 更多