在 Postgres 中收集递归 JSON 密钥

Posted

技术标签:

【中文标题】在 Postgres 中收集递归 JSON 密钥【英文标题】:Collect Recursive JSON Keys In Postgres 【发布时间】:2015-07-19 21:15:05 【问题描述】:

我有 JSON 文档以 JSON 数据类型 (Postgres 9.3) 存储在 Postgres 中,我需要递归地收集树中的键名。

例如,给定这个 JSON 树


 "files": 
  "folder": 
   "file1": 
    "property": "blah"
   ,
   "file2": 
    "property": "blah"
   ,
   "file3": 
    "property": "blah"
   ,
   "file4": 
    "property": "blah"
   
 
,
"software": 
  "apt": 
    "package1": 
        "version": 1.2
    ,
    "package2": 
        "version": 1.2
    ,
    "package3": 
        "version": 1.2
    ,
    "package4": 
        "version": 1.2
    
  
 

我想提取 [file1,file2,file3,file3,package1,package2,package3,package4] 之类的内容

基本上只是我可以用于文本搜索索引的键列表。

我知道我可以使用类似的方法获取最外层对象的键列表

SELECT DISTINCT(json_object_keys(data))

而且我知道可以使用类似

的方法递归地爬过树
WITH RECURSIVE data()

但我无法将两者放在一起。

谁能帮忙?

【问题讨论】:

【参考方案1】:

诀窍是在正确的位置使用json_typeof 添加一些最终条件测试。

如果您不关心对象键顺序,也应该使用jsonb

这是我的工作环境:

CREATE TABLE test (
  id  SERIAL PRIMARY KEY,
  doc JSON
);

INSERT INTO test (doc) VALUES ('
 "files": 
  "folder": 
   "file1": 
    "property": "blah"
   ,
   "file2": 
    "property": "blah"
   ,
   "file3": 
    "property": "blah"
   ,
   "file4": 
    "property": "blah",
    "prop" : 
      "clap": "clap"
    
   
 
,
"software": 
  "apt": 
    "package1": 
        "version": 1.2
    ,
    "package2": 
        "version": 1.2
    ,
    "package3": 
        "version": 1.2
    ,
    "package4": 
        "version": 1.2
    
  
 
');

当第二个查询没有返回任何行时,递归停止。这是通过将一个空对象传递给json_each 来完成的。

 WITH RECURSIVE doc_key_and_value_recursive(key, value) AS (
  SELECT
    t.key,
    t.value
  FROM test, json_each(test.doc) AS t

  UNION ALL

  SELECT
    t.key,
    t.value
  FROM doc_key_and_value_recursive,
    json_each(CASE 
      WHEN json_typeof(doc_key_and_value_recursive.value) <> 'object' THEN '' :: JSON
      ELSE doc_key_and_value_recursive.value
    END) AS t
)
SELECT *
FROM doc_key_and_value_recursive
WHERE json_typeof(doc_key_and_value_recursive.value) <> 'object';

【讨论】:

哦,伙计.. 你刚刚救了我几个小时的挫败感。我整个下午都在用头撞桌子,试图让它工作,你把它钉在了一起。该查询还允许我通过对 where 子句的简单更改来提取属性或键值。非常感谢! 这个解决方案是正确的。感谢那!关于如何迭代数组中对象中的键的任何建议?如"stuff": ["thing": "a", "thing": "b"] 你应该可以加入一些json_array_elements() :) 知道了。我更改了第二个 SELECT t.key, t.value 以包含一个案例声明:SELECT t.key, CASE WHEN json_typeof(t.value) = 'array' THEN json_array_elements(t.value) ELSE t.value END AS value ... 假设我没有任何数组混合它,我可以使用这个导出密钥的路径吗?即结果是一个点分隔的关键路径,例如parent.child.grandchild【参考方案2】:

一个更简洁的版本,您可以使用它进行测试:

WITH RECURSIVE reports (key, value) AS (
  SELECT
    NULL as key,
    '"k1": "k2": "v1", "k3": "k4": "v2", "k5": "v3"'::JSONB as value

UNION ALL

   SELECT
    jsonb_object_keys(value)as key,
    value->jsonb_object_keys(value) as value
   FROM
    reports
   WHERE
    jsonb_typeof(value) = 'object'
)

SELECT
    *
FROM
    reports;

这将返回一个列表,然后您需要将其与 distinct 分组。

【讨论】:

Simon 上面的函数不太通用:'"A":[[["C":"B"]]],"X":"Y"'::JSONB 的结果不同,因为数组中有对象。【参考方案3】:

我为此编写了一个函数:

CREATE OR REPLACE FUNCTION public.jsonb_keys_recursive(_value jsonb)
 RETURNS TABLE(key text)
 LANGUAGE sql
AS $function$
WITH RECURSIVE _tree (key, value) AS (
  SELECT
    NULL   AS key,
    _value AS value
  UNION ALL
  (WITH typed_values AS (SELECT jsonb_typeof(value) as typeof, value FROM _tree)
   SELECT v.*
     FROM typed_values, LATERAL jsonb_each(value) v
     WHERE typeof = 'object'
   UNION ALL
   SELECT NULL, element
     FROM typed_values, LATERAL jsonb_array_elements(value) element
     WHERE typeof = 'array'
  )
)
SELECT DISTINCT key
  FROM _tree
  WHERE key IS NOT NULL
$function$;

举个例子,试试:

SELECT jsonb_keys_recursive('"A":[[["C":"B"]]],"X":"Y"');

请注意,其他两个答案在数组内的对象中找不到键,我的解决方案可以。 (这个问题根本没有给出任何数组的例子,所以在数组中查找键可能不是最初的提问者需要的,但这是我需要的。)

【讨论】:

【参考方案4】:

@Simon 上面的回答很棒,但是对于我构建 JSON 对象差异的类似情况,我希望有 JSONpath 形式的键路径,不仅是姓氏,还包括数组索引和值。

所以,例如"A":[[["C":"B", "D":"E"]]],"X":"Y", "F": "G": "H",我不仅需要键XDGCFA,还需要每个路径上的值,例如.A[0][0][0].C = ' B'。

还有一些小的改进,例如:

    提供值的数据类型 自行提供价值,无需额外引号

我希望它对某人也有帮助:

WITH RECURSIVE _tree (key, value, type) AS (

    SELECT
        NULL as key
        ,'"A":[[["C":"B", "D":"E"]]],"X":"Y", "F": "G": "H"'::jsonb as value
        ,'object'
        UNION ALL
    (
        WITH typed_values AS (
            SELECT key, jsonb_typeof(value) as typeof, value FROM _tree
        )
        SELECT CONCAT(tv.key, '.', v.key), v.value, jsonb_typeof(v.value)
        FROM typed_values as tv, LATERAL jsonb_each(value) v
        WHERE typeof = 'object'
            UNION ALL
        SELECT CONCAT(tv.key, '[', n-1, ']'), element.val, jsonb_typeof(element.val)
        FROM typed_values as tv, LATERAL jsonb_array_elements(value) WITH ORDINALITY as element (val, n)
        WHERE typeof = 'array'
    )
)
SELECT DISTINCT key, value #>> '' as value, type
FROM _tree
WHERE key IS NOT NULL
ORDER BY key

Dbfiddle to run.

【讨论】:

以上是关于在 Postgres 中收集递归 JSON 密钥的主要内容,如果未能解决你的问题,请参考以下文章

使用 row_to_json 的 Postgres 递归查询

Postgres:在递归合并函数中删除 jsonb 键

如何在 Postgres 中选择一列 json 对象,以便返回的行是 json 数组?

在 postgres 表中计算不为空的 json 键-> 值

使用 jq 从 JSON 文件中递归获取键名

将 json 作为 celery 任务的一部分插入 postgres