Postgres returns [null] 而不是 [] for array_agg of join table

Postgres returns [null] instead of [] for array_agg of join table

我正在 Postgres 中选择一些对象及其标签。架构相当简单,三个表:

对象 id

标签 id | object_id | tag_id

标签 id | tag

我正在加入这样的表格,使用 array_agg 将标签聚合到一个字段中:

SELECT objects.*,
    array_agg(tags.tag) AS tags,
FROM objects
LEFT JOIN taggings ON objects.id = taggings.object_id
LEFT JOIN tags ON tags.id = taggings.tag_id

然而,如果对象没有标签,Postgres returns this:

[ null ]

而不是一个空数组。 如果没有标签,我如何 return 一个空数组? 我已经仔细检查过我没有 returned 的空标签。

aggregate docs 说 "The coalesce function can be used to substitute zero or an empty array for null when necessary"。我试过 COALESCE(ARRAY_AGG(tags.tag)) as tags 但它仍然 return 是一个空数组。我试过使第二个参数有很多东西(例如COALESCE(ARRAY_AGG(tags.tag), ARRAY()),但它们都会导致语法错误。

文档说包含 NULL 的数组是 returned。如果你想把它转换成一个空数组,那么你需要做一些小魔术:

SELECT objects.id,
    CASE WHEN length((array_agg(tags.tag))[1]) > 0
    THEN array_agg(tags.tag) 
    ELSE ARRAY[]::text[] END AS tags
FROM objects
LEFT JOIN taggings ON objects.id = taggings.object_id
LEFT JOIN tags ON tags.id = taggings.tag_id
GROUP BY 1;

这假设标签是 text 类型(或其任何变体);根据需要修改演员表。

这里的技巧是 [NULL] 数组中的第一个(也是唯一一个)元素的长度为 0,因此如果任何数据是 return 从 tags 编辑的,您 return聚合,否则构造一个正确类型的空数组。

顺便说一句,文档中关于使用 coalesce() 的声明有点拙劣:意思是如果您不想 NULL 作为结果,您可以使用 coalesce() 将其转换为 0 或您选择的其他输出。但是您需要将其应用于 数组元素 而不是数组,在您的情况下,数组不会提供解决方案。

文档说,当您聚合零行时,您会得到一个空值,关于使用 COALESCE 的说明是针对这种特定情况。

这不适用于您的查询,因为 LEFT JOIN 的行为方式 - 当它找到 匹配行时,它 returns 一个行,用空值填充(一个空行的集合是一个包含一个空元素的数组)。

您可能会想在输出中盲目地将 [NULL] 替换为 [],但这样您就无法区分 没有标签的对象 标记的对象,其中 tags.tag 为 null。您的应用程序逻辑 and/or 完整性约束可能不允许第二种情况,但如果它确实设法潜入,那就更有理由不抑制空标记。

您可以通过检查连接条件另一端的字段是否为 null 来识别没有标签的对象(或者一般来说,告诉 LEFT JOIN 没有找到匹配项)。所以在你的情况下,只需替换

array_agg(tags.tag)

CASE
  WHEN taggings.object_id IS NULL
  THEN ARRAY[]::text[]
  ELSE array_agg(tags.tag)
END

另一个选项可能是 array_remove(..., NULL) (introduced in 9.3) 如果 tags.tagNOT NULL(否则您可能希望在数组中保留 NULL 值,但在那种情况下,由于 LEFT JOIN):

,您无法区分单个现有的 NULL 标签和 NULL 标签
SELECT objects.*,
     array_remove(array_agg(tags.tag), NULL) AS tags,
FROM objects
LEFT JOIN taggings ON objects.id = taggings.object_id
LEFT JOIN tags ON tags.id = taggings.tag_id

如果没有找到标签,则返回一个空数组。

从 9.4 开始,可以限制聚合函数调用只处理符合特定条件的行:array_agg(tags.tag) filter (where tags.tag is not null)

也许这个答案有点晚了,但我想与您分享另一种查询策略也是可能的:在单独的(公共)table 表达式中执行聚合。

WITH cte_tags AS (
  SELECT
    taggings.object_id,
    array_agg(tags.tag) AS tags
  FROM
    taggings
    INNER JOIN tags ON tags.id = taggings.tag_id
  GROUP BY
    taggings.object_id
)
SELECT
  objects.*,
  cte_tags.tags
FROM
  objects
  LEFT JOIN cte_tags ON cte_tags.object_id = objects.id

您现在将得到 NULL 而不是数组,而不是包含单个元素 NULL 的数组。

如果你真的想要一个空数组而不是结果中的 NULL,你可以使用 COALESCE 函数...:[=​​14=]

WITH cte_tags AS (
  SELECT
    taggings.object_id,
    array_agg(tags.tag) AS tags
  FROM
    taggings
    INNER JOIN tags ON tags.id = taggings.tag_id
  GROUP BY
    taggings.object_id
)
SELECT
  objects.*,
  COALESCE(cte_tags.tags, '{}') AS tags
FROM
  objects
  LEFT JOIN cte_tags ON cte_tags.object_id = objects.id

...或使用数组到数组的串联:

WITH cte_tags AS (
  SELECT
    taggings.object_id,
    array_agg(tags.tag) AS tags
  FROM
    taggings
    INNER JOIN tags ON tags.id = taggings.tag_id
  GROUP BY
    taggings.object_id
)
SELECT
  objects.*,
  cte_tags.tags || '{}' AS tags
FROM
  objects
  LEFT JOIN cte_tags ON cte_tags.object_id = objects.id

这个怎么样:

COALESCE(NULLIF(array_agg(tags.tag), '{NULL}'), '{}') AS tags,

似乎有效。

我换了

array_to_json(array_agg(col_name))

array_to_json(coalesce(array_agg(col_name), ARRAY[]::record[]))

所以我没有返回空 JSON 值,而是得到一个空的 JSON 数组