Postgres为连接表的array_agg返回[null]而不是[]

Posted

技术标签:

【中文标题】Postgres为连接表的array_agg返回[null]而不是[]【英文标题】:Postgres returns [null] instead of [] for array_agg of join table 【发布时间】:2015-09-15 12:27:12 【问题描述】:

我正在 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 会返回:

[ null ]

而不是一个空数组。 没有标签时如何返回空数组?我已经仔细检查过我没有返回空标签。

aggregate docs 表示“必要时,可使用 coalesce 函数将零或空数组替换为 null”。我试过COALESCE(ARRAY_AGG(tags.tag)) as tags,但它仍然返回一个空数组。我已经尝试将第二个参数设置为很多东西(例如COALESCE(ARRAY_AGG(tags.tag), ARRAY()),但它们都会导致语法错误。

【问题讨论】:

恕我直言聚合应该返回空数组,不知道为什么他们决定返回null。可能是有原因的,但是返回数组的东西不应该返回null 【参考方案1】:

我换了

array_to_json(array_agg(col_name))

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

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

【讨论】:

【参考方案2】:

这个呢:

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

似乎有效。

【讨论】:

【参考方案3】:

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

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 函数...:

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

【讨论】:

【参考方案4】:

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

【讨论】:

如果所有tags.tag 都为空,则返回null,而不是空数组。有没有办法默认返回一个空数组? coalesce(array_agg(tags.tag) filter (where tags.tag is not null), '')【参考方案5】:

如果tags.tagNOT NULL,另一个选项可能是array_remove(..., NULL) (introduced in 9.3)(否则您可能希望将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

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

【讨论】:

我选择了这个答案,也许对其他人不公平,因为它涉及的查询修改少得多,而且我不关心这里的空标签情况。 这也是我使用的答案,但对于那些希望了解更多“为什么”的人,请参阅下面 Patrick 的回答以及聚合函数文档 postgresql.org/docs/9.5/static/functions-aggregate.html 有什么方法可以将它与 jsob_agg 一起使用? 由于NULL标签很可能失效,我也觉得这个答案很好。【参考方案6】:

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

这不适用于您的查询,因为 LEFT JOIN 的行为方式 - 当它找到 个匹配的行时,它返回 一个 行,并用空值填充(并且一个空行的聚合是一个具有一个空元素的数组)。

您可能想在输出中盲目地将[NULL] 替换为[],但随后您将无法区分没有标签的对象带有@987654325 的标签对象@ 为空。您的应用程序逻辑和/或完整性约束可能不允许第二种情况,但如果它确实设法潜入,那就更有理由不抑制空标签。

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

array_agg(tags.tag)

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

【讨论】:

我认为这是一个更好的解释和答案,除了我注意到它需要将taggings.object_id 添加到GROUP BY 子句中以避免语法错误ERROR: 42803: column "taggings.object_id" must appear in the GROUP BY clause or be used in an aggregate function - 添加这个子句会改变最终结果吗? @user9645:假设原始查询有一个GROUP BY objects.id(这是避免同样的错误所必需的),将其更改为GROUP BY objects.id, taggings.object_id不会影响分组(JOIN条件确保给定的objects.id 值永远不会与多个不同的taggings.object_id 值相关联。 尼克 - 谢谢,我是这么认为的,但并不积极。【参考方案7】:

文档说返回了一个包含NULL 的数组。如果要将其转换为空数组,则需要做一些小魔术:

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,因此如果从 tags 返回任何数据,则返回聚合,否则构造右边的空数组输入。

顺便说一句,文档中关于使用coalesce() 的声明有点糟糕:意思是如果你不希望NULL 作为结果,你可以使用coalesce() 把它变成@987654330 @ 或您选择的其他输出。但是您需要将其应用于 数组元素 而不是数组,在您的情况下,它不会提供解决方案。

【讨论】:

是的,你实际上想要coalescenullif 的反面,如果问题真的像它出现的那样。 length(NULL)NULL,而不是 0length(NULL) > 0 也是NULL,恰好属于ELSE 的情况。但length('') > 0 也是如此,我认为这不是理想的行为。 postgresql.org/docs/9.5/static/functions-aggregate.html 有点隐藏在文档中,但相关文字是“需要注意的是,除了计数,这些函数在没有选择行时返回空值。特别是总和no rows 返回 null,而不是预期的零,并且当没有输入行时,array_agg 返回 null 而不是空数组。"

以上是关于Postgres为连接表的array_agg返回[null]而不是[]的主要内容,如果未能解决你的问题,请参考以下文章

postgres array_agg 错误:无法累积不同维度的数组

如何删除使用 array_agg postgres 函数生成的重复项

单个查询中的多个 array_agg() 调用

如何优化大表的 Postgresql ARRAY_AGG 查询?

我如何联接两个以上的postgres表,并在postgres中将其作为nestedjson获得结果

来自多个表的 Postgres/netezza 多重连接