在单个查询中从一对多关系数据构建 JSON 对象?

ant*_*awn 9 postgresql join array json many-to-many

我有一个带有如下表的 PostgreSQL 9.5.3 DB:

container
    id: uuid (pk)
    ... other data

thing
    id: uuid (pk)
    ... other data

container_thing
    container_id: uuid (fk)
    thing_id: uuid (fk)
    primary key (container_id, thing_id)
Run Code Online (Sandbox Code Playgroud)

Acontainer可以指向任意数量的things(没有重复),并且 athing可以被任意数量的containers指向。

可能有大量的容器和东西(取决于我们有多少客户)。每个容器中可能只有 1 到 10 个东西。我们一次最多只能查询大约 20 个容器。一个容器可以是空的,我需要取回一个空数组。

我需要构建代表容器的 json 对象,如下所示:

{
    "id": "d7e1bc6b-b659-432d-b346-29f3a530bfa9",
    ... other data
    "thingIds": [
        "4e3ad81b-f2b5-4220-8e0e-e9d53c80a214",
        "f26f49e5-76b4-4363-9ffe-9654ba0b0f0d"
    ]
}
Run Code Online (Sandbox Code Playgroud)

这工作正常,但我通过使用两个查询来做到这一点:

select * from "container" where "id" in (<list of container ids>)
select * from "container_thing" where "container_id" in (<list of container ids>)
Run Code Online (Sandbox Code Playgroud)

然后我按程序为每个容器构建“thingIds”数组。

后来我找到了一个具有相关子查询解决方案,目前对我来说效果很好。

select *, array(select thing_id from container_thing where container_id = c.id) as "thingIds"
from container c;
Run Code Online (Sandbox Code Playgroud)

有没有更好的方法来做到这一点,也许以某种方式使用连接?
似乎它们总是生成一组行,这意味着为指向的container每个行复制数据thing

Erw*_*ter 15

如果容器可以是空的,则当前接受的解决方案对您不起作用。它必须是一个外部连接才能保留没有匹配的行 - 以获得与您在小提琴中使用的相关子查询等效的结果:

select *, array(select thing_id from container_thing where container_id = container.id) as "thingIds"
from container
Run Code Online (Sandbox Code Playgroud)

1.

SELECT to_json(sub) AS container_with_things
FROM  (
   SELECT c.*, json_agg(thing_id) AS "thingIds"
   FROM   container c
   LEFT   JOIN container_thing ct ON  ct.container_id = c.id
   WHERE  c.id IN (<list of container ids>)
   GROUP  BY c.id
   ) sub;
Run Code Online (Sandbox Code Playgroud)

2.

每个容器有多行(您提到了 20 行),通常加入之前聚合速度更快:

SELECT to_json(sub) AS container_with_things
FROM  (
   SELECT c.*, ct."thingIds"
   FROM   container c
   LEFT   JOIN (
      SELECT container_id AS id, json_agg(thing_id) AS "thingIds"
      FROM   container_thing
      WHERE  container_id IN (<list of container ids>) -- repeat condition
      GROUP  BY 1
      ) ct USING (id)
   WHERE  c.id IN (<list of container ids>)
   ) sub;
Run Code Online (Sandbox Code Playgroud)

3.

或者,您可以将找到的ARRAY 构造函数LEFT JOIN LATERAL

SELECT to_json(sub) AS container_with_things
FROM  (
   SELECT c.*, ct."thingIds"
   FROM   container c
   LEFT   JOIN LATERAL (
      SELECT ARRAY (
         SELECT thing_id
         FROM   container_thing
         WHERE  container_id = c.id
         -- ORDER  BY thing_id  -- optional order for deterministic results
         ) AS "thingIds"
      ) ct ON true
   WHERE  c.id IN (<list of container ids>)
   ) sub;
Run Code Online (Sandbox Code Playgroud)

可能会更快,但。

SQL小提琴。(扩展@a_horse 的小提琴。)

请注意,空容器的结果在上述三个查询中略有不同:

  1. "thingIds":[null]
  2. "thingIds":null
  3. "thingIds":[]

4.

在 Postgres 9.5(因为您正在使用它)中,您还可以使用 jsonb它的功能和一个更少的子查询:

SELECT jsonb_set(to_jsonb(c), '{thingIds}', "thingIds") AS container_with_things
FROM   container c
LEFT   JOIN (
   SELECT container_id AS id, jsonb_agg(thing_id) AS "thingIds"
   FROM   container_thing
   WHERE  container_id IN (<list of container ids>) -- repeat condition
   GROUP  BY 1
   ) ct USING (id)
WHERE  c.id IN (<list of container ids>);
Run Code Online (Sandbox Code Playgroud)

或者:

SELECT to_jsonb(c) || jsonb_build_object('thingIds', "thingIds") AS container_with_things
FROM   ... 
Run Code Online (Sandbox Code Playgroud)