将 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,以便结果记录包含字段 f1
、f2
、f3
和 f4
,并扩展 JSON填写这些字段?
我知道 json_populate_record()
可以做到这一点,如果你事先知道 f3
和 f4
是字段名称的话。但我没有。我想使用第一行 metadata
中的键名作为所有其他行的模板。
换句话说:我希望查询的结果列是f1
、f2
+ 第一行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
一旦你知道 数, 结果列的 names 和 types ,它变得简单,就像你在问题中提到的那样。我建议您创建一个临时 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()
来简化安全查询字符串连接。
假设您有一个 Postgres table 形式:
f1 | f2 | metadata
-----+-------+-----------------------
a | 33 | {"f3": "d", "f4": "e"}
b | 20 | {"f3": "d", "f4": "g"}
metadata
列是非结构化 JSON 字段。我如何查询此 table,以便结果记录包含字段 f1
、f2
、f3
和 f4
,并扩展 JSON填写这些字段?
我知道 json_populate_record()
可以做到这一点,如果你事先知道 f3
和 f4
是字段名称的话。但我没有。我想使用第一行 metadata
中的键名作为所有其他行的模板。
换句话说:我希望查询的结果列是f1
、f2
+ 第一行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
一旦你知道 数, 结果列的 names 和 types ,它变得简单,就像你在问题中提到的那样。我建议您创建一个临时 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()
来简化安全查询字符串连接。