【问题标题】:How to use Postgres jsonb_path_query instead of select union如何使用 Postgres jsonb_path_query 而不是选择联合
【发布时间】:2022-01-01 09:13:30
【问题描述】:

db:Postgresql-14。这将是一个不常见的转换,我正在寻找可以做出的建议/改进,以便我可以学习/磨练我的 postgres/json 技能(并加快/优化这个非常慢的查询)。

我们从外部 api 接收可变大小/结构的 json 对象。

每个 json 对象都是一个调查响应。每个嵌套的“问题/答案”对象可以具有完全不同的结构。总共有大约 5 个已知结构。

响应对象存储在具有 jsonb_ops gin 索引的 jsonb 列中。

表格有大约 500,000 行。每行的 jsonb 列对象有大约 200 个嵌套值。

我们的目标是将所有嵌套的问题/答案响应提取到另一个 id,question,answer 表中。在目标表上,我们将使用 FTS 和 trigram 进行广泛的查询,并且旨在简化模式。这就是为什么我要提取到一个简单的表,而不是用 jsonb 查询做任何更奇特的事情。在那些我不需要的对象中也有很多元数据。所以我也希望通过归档原始表(它是 5GB + 索引)来节省一些空间。

具体来说,我很想学习一种更优雅的方式来遍历和提取​​ json 到目标表。

而且我一直无法找到将结果转换为实际 sql 文本而不是引用的 jsontext 的方法(通常我会使用 ->>、::text 或 jsonb 函数的 _text 版本)

这是一个非常简化的 json 对象版本,可以轻松运行它。

提前谢谢你!

create table test_survey_processing(
    id integer generated always as identity constraint test_survey_processing_pkey primary key,
    json_data jsonb
);
insert into test_survey_processing (json_data)
values ('{"survey_data": {"2": {"answer": "Option 1", "question": "radiobuttonquesiton"}, "3": {"options": {"10003": {"answer": "Option 1"}, "10004": {"answer": "Option 2"}}, "question": "checkboxquestion"}, "5": {"answer": "Column 2", "question": "Row 1"}, "6": {"answer": "Column 2", "question": "Row 2"}, "7": {"question": "checkboxGRIDquesiton", "subquestions": {"8": {"10007": {"answer": "Column 1", "question": "Row 1 : Column 1"}, "10008": {"answer": "Column 2", "question": "Row 1 : Column 2"}}, "9": {"10007": {"answer": "Column 1", "question": "Row 2 : Column 1"}, "10008": {"answer": "Column 2", "question": "Row 2 : Column 2"}}}}, "11": {"answer": "Option 1", "question": "Row 1"}, "12": {"answer": "Option 2", "question": "Row 2"}, "13": {"options": {"10011": {"answer": "Et molestias est opt", "option": "Option 1"}, "10012": {"answer": "Similique magnam min", "option": "Option 2"}}, "question": "textboxlist"}, "14": {"question": "textboxgridquesiton", "subquestions": {"15": {"10013": {"answer": "Qui error magna omni", "question": "Row 1 : Column 1"}, "10014": {"answer": "Est qui dolore dele", "question": "Row 1 : Column 2"}}, "16": {"10013": {"answer": "vident mol", "question": "Row 2 : Column 1"}, "10014": {"answer": "Consectetur dolor co", "question": "Row 2 : Column 2"}}}}, "17": {"question": "contactformquestion", "subquestions": {"18": {"answer": "Rafael", "question": "First Name"}, "19": {"answer": "Adams", "question": "Last Name"}}}, "33": {"question": "customgroupquestion", "subquestions": {"34": {"answer": "Sed magnam enim non", "question": "customgroupTEXTbox"}, "36": {"answer": "Option 2", "question": "customgroupradiobutton"}, "37": {"options": {"10021": {"answer": "Option 1", "option": "customgroupCHEC KBOX question : Option 1"}, "10022": {"answer": "Option 2", "option": "customgroupCHEC KBOX question : Option 2"}}, "question": "customgroupCHEC KBOX question"}}}, "38": {"question": "customTABLEquestion", "subquestions": {"10001": {"answer": "Option 1", "question": "customTABLEquestioncolumnRADIO"}, "10002": {"answer": "Option 2", "question": "customTABLEquestioncolumnRADIO"}, "10003": {"options": {"10029": {"answer": "OPTION1"}, "10030": {"answer": "OPTION2"}}, "question": "customTABLEquestioncolumnCHECKBOX"}, "10004": {"options": {"10029": {"answer": "OPTION1"}, "10030": {"answer": "OPTION2"}}, "question": "customTABLEquestioncolumnCHECKBOX"}, "10005": {"answer": "Aperiam itaque dolor", "question": "customTABLEquestioncolumnTEXTBOX"}, "10006": {"answer": "Hic qui numquam inci", "question": "customTABLEquestioncolumnTEXTBOX"}}}}}');
create index test_survey_processing_gin_index on test_survey_processing using gin (json_data);

-- the query I'm using (it works, but it is unmanageably slow)

-- EXPLAIN (ANALYZE, VERBOSE, BUFFERS, FORMAT JSON)
select level1.value['question'] question, level1.value['answer'] as answer ,tgsr.json_data['survey_data']
from test_survey_processing tgsr,
     jsonb_each(tgsr.json_data['survey_data']::jsonb) level1
-- where survey_id = 6633968 and id = 4
union
select level1.value['question'] question, jsonb_path_query(level1.value, '$.answer')::jsonb as answer ,tgsr.json_data['survey_data']
from test_survey_processing tgsr,
     jsonb_each(tgsr.json_data['survey_data']::jsonb) level1
-- where survey_id = 6633968 and id = 4
union
select level1.value['question'] question, jsonb_path_query(level1.value, '$.options.*.answer')::jsonb as answer ,tgsr.json_data['survey_data']
from test_survey_processing tgsr,
     jsonb_each(tgsr.json_data['survey_data']::jsonb) level1
-- where survey_id = 6633968 and id = 4
union
select level1.value['question'] question, jsonb_path_query(level1.value, '$.subquestions.*.*.answer')::jsonb as answer ,tgsr.json_data['survey_data']
from test_survey_processing tgsr,
     jsonb_each(tgsr.json_data['survey_data']::jsonb) level1
-- where survey_id = 6633968 and id = 4

完善并获得我需要的结果后的后续编辑

这是我最终运行的查询。处理和插入 3400 万条记录耗时 11 分钟。这很好,因为它是一次操作。

关于我所做的更改的一些 cmets

-我使用 -> 和 ->> 而不是 [下标],因为我读到即使在 pg14 中,下标也不使用索引(不确定这在 FROM 中是否重要)
-"to_json(...) #>> '{}'" 是我基于此将 json 字符串转换为不带引号的字符串的方式:stack overflow answer

create table respondent_questions_answers as
select tgsr.id,tgsr.survey_id,level1.value ->> 'question' question, '' as sub_question,
       to_json(jsonb_path_query(level1.value, '$.answer')) #>> '{}' as answer 
from test_survey_processing tgsr, jsonb_each(tgsr.json -> 'survey_data') level1
union
select tgsr.id,tgsr.survey_id,level1.value ->> 'question' question,
       to_json(jsonb_path_query(level1.value, '$.options.*.option')) #>> '{}' as sub_question,
       to_json(jsonb_path_query(level1.value, '$.options.*.answer')) #>> '{}' as answer
from test_survey_processing tgsr, jsonb_each(tgsr.json -> 'survey_data') level1 
union
select tgsr.id,tgsr.survey_id,level1.value ->> 'question' question,
       to_json(jsonb_path_query(level1.value, '$.subquestions.*.*.question')) #>> '{}' as sub_question,
       to_json(jsonb_path_query(level1.value, '$.subquestions.*.*.answer')) #>> '{}' as answer
from test_survey_processing tgsr, jsonb_each(tgsr.json -> 'survey_data') level1
union
select tgsr.id,tgsr.survey_id,level1.value ->> 'question' question,
       to_json(jsonb_path_query(level1.value, '$.subquestions.*.question')) #>> '{}' as sub_question,
       to_json(jsonb_path_query(level1.value, '$.subquestions.*.answer')) #>> '{}' as answer
from test_survey_processing tgsr, jsonb_each(tgsr.json -> 'survey_data') level1;

接受以下答案作为解决方案后的最终编辑

感谢@Edouard H. 的回答以及对如何正确使用 jsonb_path_query 的更好理解,我能够消除所有 UNION SELECT,发现一些丢失的值,并消除对 to_json hack 的需要。尽管 CROSS JOIN LATERAL 隐含在 json 函数中,但最好包含 JOIN 而不是逗号,因为它们的绑定更紧密,更易于阅读。下面是我使用的最终查询。

SELECT concat_ws(' ',
    qu.value::jsonb->>'question'
,   an.answer::jsonb->>'question'
,   an.answer::jsonb->>'option') AS question
,   an.answer::jsonb->>'answer' AS answer
--      , tgsr.json_data->>'survey_data'
FROM test_survey_processing tgsr
         CROSS JOIN LATERAL jsonb_each(tgsr.json_data->'survey_data') AS qu
         CROSS JOIN LATERAL jsonb_path_query(qu.value::jsonb, '$.** ? (exists(@.answer))') AS an(answer)

【问题讨论】:

  • 有多慢?如果它要返回 4 亿行,你期望它有多快?请显示`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)`的输出`
  • 从总体上看,10 分钟或 1 小时的处理时间并不重要。我更好奇是否有更优化的方法来实现相同的结果。随着我在 postgres 中对 json 的使用每天都在增加,我的学习也在增加。

标签: sql json postgresql jsonb jsonpath


【解决方案1】:

第一个想法:将 UNION 的 4 个查询替换为 1 个唯一查询。

第二个想法:第一个查询中的语句level1.value['answer'] as answer 听起来像第二个查询中的语句jsonb_path_query(level1.value, '$.answer')::jsonb as answer。我认为两个查询都返回相同的行集,并且两个查询之间的UNION 删除了重复的行。

第三个思路:在FROM子句中使用jsonb_path_query函数代替SELECT子句,使用CROSS JOIN LATERAL以便逐步分解jsonb数据:

SELECT qu.question->>'question' AS question
     , an.answer->>'answer' AS answer
     , tgsr.json_data->>'survey_data'
  FROM test_survey_processing tgsr
 CROSS JOIN LATERAL jsonb_each(tgsr.json_data->'survey_data') AS qu(question)
 CROSS JOIN LATERAL jsonb_path_query(qu.question, '$.** ? (exists(@.answer))') AS an(answer)

--其中survey_id = 6633968,id = 4

【讨论】:

  • 感谢您的反馈。 - 据我所知,我需要联合,因为我正在遍历 4 个不同结构化 json 对象的所有值。 - 很好,我错过了我以某种方式复制了它。 - FROM 中包含的 json 函数是隐含的“横向”,所以没有必要把它写出来(AFAIK)——对于#3,我无法让它工作。 [42883] 错误:函数 jsonb_path_query(record, unknown) 不存在 提示:没有函数与给定名称和参数类型匹配。您可能需要添加显式类型转换。
  • 对于#3,我已经更新了查询,希望这次能正常工作,没有错误。关于 UNION,我仍然不明白您为什么需要它以及“4 个不同的结构化 json 对象”是什么意思?它们是同一个表的不同列,还是来自不同的表?
  • 我必须对您写的内容进行一些编辑才能使其正常工作,但最重要的是,您引导我找到了更好的解决方案。你是对的,我对 jsonb_path_query 缺乏了解意味着我正在拼凑工会。为了回答您的问题,我需要将几个不同键的值连接到一列。作为奖励,我发现了一些未在我的原始查询中捕获值的情况。我用我使用的最终解决方案编辑了原始帖子。再次感谢。
猜你喜欢
  • 2020-03-05
  • 1970-01-01
  • 2017-06-25
  • 1970-01-01
  • 2012-08-25
  • 2016-11-02
  • 1970-01-01
  • 1970-01-01
  • 2021-11-19
相关资源
最近更新 更多