将JSONB展开为以第一行为模板的列

时间:2015-10-01 20:15:30

标签: sql json postgresql dynamic-sql jsonb

我们假设你有一张表格的Postgres表:

 f1  | f2    |   metadata   
-----+-------+-----------------------
 a   | 33    | {"f3": "d", "f4": "e"}
 b   | 20    | {"f3": "d", "f4": "g"}

metadata列是非结构化JSON字段。如何查询此表,以便结果记录包含字段f1f2f3f4,并扩展JSON以填充这些字段?

我知道json_populate_record()如果您事先知道f3f4是字段名称,则可以执行此操作。但我不是。我想使用第一行metadata中的键名作为所有其他行的模板。

换句话说:我希望查询的结果列为f1f2 + 第一行的JSON数据的所有键。任何其他不符合第一行中的键的键都应该被删除。

1 个答案:

答案 0 :(得分:2)

没有自然的“第一”排。你需要定义“第一”。 假设ORDER BY f1, f2的第一行。

如果您不知道预期列的数量和数据类型,则 无法在单个SQL语句中完成。 SQL要求知道返回类型,至少在调用时。但是有两种方法可以用两种方式来做。

第0步

这是一个适合的测试设置:

CREATE TABLE tbl (f1 text, f2 int, metadata jsonb);
INSERT INTO tbl VALUES
  ('a', 33, '{"f3": "d", "f4": "e"}')  -- "first" row
, ('b', 20, '{"f3": "d", "f4": "g"}')  -- same keys
, ('c', 40, '{"f7": "x", "f4": "o"}')  -- one matching key
, ('d', 50, '{"f3": "o", "f9": "x", "f123": "z"}')  -- one match, two miss
, ('e', 60, '{"x": "d", "y": "g"}')    -- no match
, ('f', 33, '{"f3": 1, "f4": false}'); -- numeric and boolean

第1步

知道 数字 名称 类型 的结果列,它变得简单,就像你在问题中提到的那样。我建议你创建一个临时表来提供jsonb_populate_record()的行类型:

BEGIN;
CREATE TEMP TABLE tmp(f3 text, f4 text) ON COMMIT DROP;

SELECT f1, f2, meta.*
FROM   tbl, jsonb_populate_record(NULL::tmp, metadata) meta;
ROLLBACK;  -- or: COMMIT;

ON COMMIT DROP会自动删除事务结束时的表。你可能想要也可能不想要那样。如果这样做,请对两个命令使用单个事务。

临时表仅在同一会话中可见,因此与多个执行相同操作的事务没有命名冲突。

如果您没有这些信息,那就更复杂了。

第2步

您可以使用DO命令和动态SQL执行相同的操作:

DO
$do$
BEGIN
EXECUTE 'CREATE TEMP TABLE tmp(f3 text, f4 text) ON COMMIT DROP';
END
$do$;

第3步

由于我们实际上并不知道输出列的数量和名称,因此我们从第一行中提取该信息。假设:

  • 所有列的数据类型均为text
  • “第一”行在jsonb列中至少有一个键。
  • 现有列名“f1”和“f2”不会作为JSON列中的键重复。 (Postgres允许输出列中的重复名称,但有些客户端存在问题 - 而且相当混乱。)

DO
$do$
BEGIN

EXECUTE (
   SELECT (SELECT 'CREATE TEMP TABLE tmp('
                || string_agg(quote_ident(k), ' text, ')  -- f3 text, f4 
                || ' text) ON COMMIT DROP'
           FROM   jsonb_object_keys(metadata) k)
   FROM   tbl
   ORDER  BY f1, f2
   LIMIT  1
   );

END
$do$;

SELECT f1, f2, meta.*
FROM   tbl, jsonb_populate_record(NULL::tmp, metadata) meta;

VOILÀ。

请务必使用quote_ident()或类似内容正确转义密钥名称。

如果事先知道列名......
addressing your comment),你可以简单地说:

SELECT f1, f2, metadata->>'f3', metadata->>'f4'
FROM   tbl;

对于宽行,jsonb_populate_record()更方便。您仍然可以使用动态解决方案,或者保留表或类型并使用它。

替代

如果您的第二个命令可以 依赖 ,那么您也可以动态构建简单语句并在另一个调用中执行该命令:

SELECT (SELECT 'SELECT f1, f2, metadata->>'
             || string_agg(format('%1$L AS %1$I', k), ', metadata->>')
             || ' FROM tbl'
        FROM   jsonb_object_keys(metadata) k)
FROM   tbl
ORDER  BY f1, f2
LIMIT  1;

以文本形式返回上述简单查询。执行它作为第二个命令...执行可能会快一点,但它需要两个往返服务器,而第一个解决方案可以只用一个...你决定。

在此处使用format()来简化安全查询字符串连接。