将 JSON 扩展为以第一行为模板的列

Expand JSONB into columns with first row as template

假设您有一个 Postgres table 形式:

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

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

我知道 json_populate_record() 可以做到这一点,如果你事先知道 f3f4 是字段名称的话。但我没有。我想使用第一行 metadata 中的键名作为所有其他行的模板。

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

没有自然的 "first" 行。您需要定义 "first"。 假设第一行 ORDER BY f1, f2.

如果您不知道预期的列数和数据类型,则此 不能 在单个 SQL 语句中完成。 SQL 要求知道 return 类型,至少在调用时是这样。但是有多种方法可以用两个语句来完成。

第 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

一旦你知道 , 结果列的 namestypes ,它变得简单,就像你在问题中提到的那样。我建议您创建一个临时 table 来为 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 在交易结束时自动删除 table。你可能想要也可能不想要那个。如果这样做,请对两个命令使用单个事务。

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

如果您没有此信息,它会变得更加复杂。

第 2 步

您可以使用 DO 命令和动态 SQL:

执行相同的操作
DO
$do$
BEGIN
EXECUTE 'CREATE TEMP TABLE tmp(f3 text, f4 text) ON COMMIT DROP';
END
$do$;

步骤 3

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

  • 所有列的数据类型都是 text
  • "first" 行在 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;

瞧瞧。

请务必使用 quote_ident() 或类似名称正确转义键名。

如果列名事先已知...
(),您可以简单地:

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

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

备选

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

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;

Returns 以上简单查询为文本。将其作为第二个命令执行...执行速度可能会快一点,但它需要 两次 到服务器的往返行程,而第一个解决方案只需要一次...您决定.

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