我有一个具有唯一键/值对的PostgreSQL表,这些键/值对最初是JSON格式,但已被规范化并融化了:
key | value
-----------------------------
name | Bob
address.city | Vancouver
address.country | Canada
我需要将其转换为分层JSON:
{
"name": "Bob",
"address": {
"city": "Vancouver",
"country": "Canada"
}
}
有没有一种方法可以在SQL中轻松地做到这一点?
答案 0 :(得分:3)
jsonb_set()
几乎可以为您做所有事情,但是不幸的是,它只能创建丢失的叶子(即,丢失路径上的最后一个键),而不能创建整个丢失的分支。为了克服这个问题,这是它的修改版本,可以在任何缺失的级别上设置值:
create function jsonb_set_rec(jsonb, jsonb, text[])
returns jsonb
language sql
as $$
select case
when array_length($3, 1) > 1 and ($1 #> $3[:array_upper($3, 1) - 1]) is null
then jsonb_set_rec($1, jsonb_build_object($3[array_upper($3, 1)], $2), $3[:array_upper($3, 1) - 1])
else jsonb_set($1, $3, $2, true)
end
$$;
现在,您只需要对一个行逐个应用此功能,就从一个空的json对象:{}
开始。您可以使用recursive CTEs:
with recursive props as (
(select distinct on (grp)
pk, grp, jsonb_set_rec('{}', to_jsonb(value), string_to_array(key, '.')) json_object
from eav_tbl
order by grp, pk)
union all
(select distinct on (grp)
eav_tbl.pk, grp, jsonb_set_rec(json_object, to_jsonb(value), string_to_array(key, '.'))
from props
join eav_tbl using (grp)
where eav_tbl.pk > props.pk
order by grp, eav_tbl.pk)
)
select distinct on (grp)
grp, json_object
from props
order by grp, pk desc;
或,其自定义聚合定义为:
create aggregate jsonb_set_agg(jsonb, text[]) (
sfunc = jsonb_set_rec,
stype = jsonb,
initcond = '{}'
);
您的查询可能变得简单:
select grp, jsonb_set_agg(to_jsonb(value), string_to_array(key, '.'))
from eav_tbl
group by grp;
答案 1 :(得分:2)
目前没有准备使用的工具。该函数根据路径生成分层的json对象:
create or replace function jsonb_build_object_from_path(path text, value text)
returns jsonb language plpgsql as $$
declare
obj jsonb;
keys text[] := string_to_array(path, '.');
level int := cardinality(keys);
begin
obj := jsonb_build_object(keys[level], value);
while level > 1 loop
level := level- 1;
obj := jsonb_build_object(keys[level], obj);
end loop;
return obj;
end $$;
您还需要this answer.查询中描述的聚合函数jsonb_merge_agg(jsonb)
:
with my_table (path, value) as (
values
('name', 'Bob'),
('address.city', 'Vancouver'),
('address.country', 'Canada'),
('first.second.third', 'value')
)
select jsonb_merge_agg(jsonb_build_object_from_path(path, value))
from my_table;
提供此对象:
{
"name": "Bob",
"first":
{
"second":
{
"third": "value"
}
},
"address":
{
"city": "Vancouver",
"country": "Canada"
}
}
该函数无法识别json数组。
答案 2 :(得分:1)
尽管我认为应该有一种更简单的方法,但我真的无法想到更简单的方法。
我假设还有一些附加的列可用于将属于一个“人”的键组合在一起,在示例中,我使用了p_id
。
select p_id,
jsonb_object_agg(k, case level when 1 then v -> k else v end)
from (
select p_id,
elements[1] k,
jsonb_object_agg(case cardinality(elements) when 1 then ky else elements[2] end, value) v,
max(cardinality(elements)) as level
from (
select p_id,
"key" as ky,
string_to_array("key", '.') as elements, value
from kv
) t1
group by p_id, k
) t2
group by p_id;
最里面的查询只是将点表示法转换为数组,以便以后访问。
然后下一个级别根据“键”构建JSON对象。对于“单个级别”键,它仅使用键/值,对于其他键,它使用第二个元素+值,然后将属于它们的值进行聚合。
第二个查询级别返回以下内容:
p_id | k | v | level
-----+---------+--------------------------------------------+------
1 | address | {"city": "Vancouver", "country": "Canada"} | 2
1 | name | {"name": "Bob"} | 1
2 | address | {"city": "Munich", "country": "Germany"} | 2
2 | name | {"name": "John"} | 1
第二步完成的聚合为“单个元素”键留下了一个级别,这正是我们需要的级别。
如果未进行区分,则最终聚合将返回{"name": {"name": "Bob"}, "address": {"city": "Vancouver", "country": "Canada"}}
而不是通缉的{"name": "Bob", "address": {"city": "Vancouver", "country": "Canada"}}
。
表达式case level when 1 then v -> k else v end
本质上将{"name": "Bob"}
返回到"Bob"
。
因此,具有以下示例数据:
create table kv (p_id integer, "key" text, value text);
insert into kv
values
(1, 'name','Bob'),
(1, 'address.city','Vancouver'),
(1, 'address.country','Canada'),
(2, 'name','John'),
(2, 'address.city','Munich'),
(2, 'address.country','Germany');
然后查询返回:
p_id | jsonb_object_agg
-----+-----------------------------------------------------------------------
1 | {"name": "Bob", "address": {"city": "Vancouver", "country": "Canada"}}
2 | {"name": "John", "address": {"city": "Munich", "country": "Germany"}}
答案 3 :(得分:1)
create table kv (key text, value text);
insert into kv
values
('name','Bob'),
('address.city','Vancouver'),
('address.country','Canada'),
('name','John'),
('address.city','Munich'),
('address.country','Germany');
create view v_kv as select row_number() over() as nRec, key, value from kv;
create view v_datos as
select k1.nrec, k1.value as name, k2.value as address_city, k3.value as address_country
from v_kv k1 inner join v_kv k2 on (k1.nrec + 1 = k2.nrec)
inner join v_kv k3 on ((k1.nrec + 2= k3.nrec) and (k2.nrec + 1 = k3.nrec))
where mod(k1.nrec, 3) = 1;
select json_agg(json_build_object('name',name, 'address', json_build_object('city',address_city, 'country', address_country)))
from v_datos;