PostgreSQL 递归行到 JSONB 映射

PostgreSQL recursive rows to JSONB map

这个问题最好用一个例子来解释。所以,如果你在 PostgreSQL 中有 2 tables categoryevent 如下: -

create table category (
    id integer primary key,
    type varchar(255),
    label varchar (255),
    parent_id integer
);
insert into category (id, type,           label,    parent_id) 
              values (1,  'organisation', 'Google', null),
                     (2,  'product',      'Gmail',  1),
                     (3,  'organisation', 'Apple',  null),
                     (4,  'product',      'iPhone', 3),
                     (5,  'product',      'Mac',    3);

create table event (
    id integer primary key,
    name varchar (255),
    category_id integer
);
insert into event (id, name, category_id) 
           values (1,  'add', 4),
                  (2,  'delete', 5),
                  (3,  'update', 2); 

如您所见,category table 非常动态,可以定义类别层次结构。

我想要实现的是选择 event table 的条目并将其与类别合并,但将其展平为 JSON 结构。我可以使用以下查询来说明:-

select e.*, 
       jsonb_build_object( 
           c1.type, c1.label,
           c2.type, c2.label
       ) as categories
  from event e
  left join category c2 on c2.id = e.category_id
  left join category c1 on c1.id = c2.parent_id

这将 return: -

+----+--------+-------------+------------------------------------------------+
| id | name   | category_id | categories                                     |
+----+--------+-------------+------------------------------------------------+
| 1  | add    | 4           | {"organisation": "Apple", "product": "iPhone"} |
| 2  | delete | 5           | {"organisation": "Apple", "product": "Mac"}    |
| 3  | update | 2           | {"organisation": "Google", "product": "Gmail"} |
+----+--------+-------------+------------------------------------------------+

但是,此方法仅在 event.category_id 列引用恰好具有 1 个父级(2 个级别)的子类别时才有效。我真正要寻找的是生成 categories,无论 (a) 它是否没有父类别(即 1 级类别)或 (b) 是否有超过 1 个父类别(例如 3 级) .例如,如果我将以下行添加到 eventcategory table 中:-

insert into category (id, type,           label,    parent_id) 
              values (6,  'module',       'Mobile', 5),    /* has 2 parents  */
                     (7,  'organisation', 'AirBNB', null); /* has no parents */

insert into event (id, name, category_id) 
           values (4,  'event1', 6),
                  (5,  'event2', 7);

... 和 运行 上面的查询将 return: -

ERROR: argument 1: key must not be null
SQL state: 

我的直觉是递归 CTE 可以解决这个问题。


更新 1

 create or replace function category_array(category_parent_id int) returns setof jsonb as $$
     select case
         when count(x) > 0 then
              jsonb_agg(f.x) || jsonb_build_object (
                  c.type, c.label
              )
         else jsonb_build_object (
                  c.type, c.label
              )
      end as category_pair
 from category c
 left join category_array (c.parent_id) as f(x) on true
where c.id = category_parent_id
group by c.id, c.type, c.label;

$$ language sql;

... 并使用此 SQL ...

调用
select *, 
       category_array(category_id) 
  from event;

...将return以下...

+----+--------+-------------+--------------------------------------------------------------------------+
| id | name   | category_id | categories                                                               |
+----+--------+-------------+--------------------------------------------------------------------------+
| 1  | add    | 4           |  [{"organisation": "Apple"}, {"product": "iPhone"}]                      |
| 2  | delete | 5           |  [{"organisation": "Apple"}, {"product": "Mac"}]                         |
| 3  | update | 2           |  [{"organisation": "Google"}, {"product": "Gmail"}]                      |
| 4  | event1 | 6           |  [[{"organisation": "Apple"}, {"product": "Mac"}], {"module": "Mobile"}] |
| 5  | event2 | 7           |  {"organisation": "AirBNB"}                                              |
+----+--------+-------------+--------------------------------------------------------------------------+

非常接近,但还没有完全实现!

使用concatenation operator ||构建累积jsonb对象:

with recursive events as (
    select 
        e.id, e.name, e.category_id as parent_id,
        jsonb_build_object(c.type, c.label) as categories
    from event e
    left join category c on c.id = e.category_id
union all
    select 
        e.id, e.name, c.parent_id,
        categories || jsonb_build_object(c.type, c.label)
    from events e
    join category c on c.id = e.parent_id
)

select id, name, categories 
from events
where parent_id is null
order by id;

请注意,查询不受循环依赖保护,因此您需要确保 table 中的所有路径都以空值结尾。

DbFiddle.

上测试查询

备选方案:

create or replace function get_categories(int) 
returns jsonb language sql as $$
    select case
        when parent_id is null then
            jsonb_build_object (type, label)
        else 
            jsonb_build_object (type, label) || get_categories(parent_id)
    end as categories
    from category
    where id = 
$$;

select id, name, get_categories(category_id)
from event
order by id;

DbFiddle.