将扁平化的键/值表转换为PostgreSQL中的分层JSON

时间:2019-02-15 16:47:07

标签: json postgresql

我有一个具有唯一键/值对的PostgreSQL表,这些键/值对最初是JSON格式,但已被规范化并融化了:

key             | value
-----------------------------
name            | Bob
address.city    | Vancouver
address.country | Canada

我需要将其转换为分层JSON:

{
"name": "Bob",
"address": {
    "city": "Vancouver",
    "country": "Canada"
    }
}

有没有一种方法可以在SQL中轻松地做到这一点?

4 个答案:

答案 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;

https://rextester.com/TULNU73750

答案 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"}} 

在线示例:https://rextester.com/SJOTCD7977

答案 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;