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

问题描述 投票:38回答:6

我正在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说“必要时可使用合并函数将零或空数组替换为null”。我尝试了COALESCE(ARRAY_AGG(tags.tag)) as tags,但它仍然返回带有null的数组。我尝试使第二个参数变多(例如COALESCE(ARRAY_AGG(tags.tag), ARRAY()),但它们都会导致语法错误。

postgresql left-join database-normalization
6个回答
32
投票

[如果array_remove(..., NULL)introduced in 9.3,则另一个选项可能是tags.tagNOT NULL)(否则,您可能希望将NULL值保留在数组中,但是在这种情况下,您无法区分单个值现有的NULL标签和由于NULL而产生的LEFT JOIN标签:

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

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


15
投票

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


13
投票

文档说,当您聚合零行时,您将得到一个空值,并且有关使用COALESCE的注释正在解决此特定情况。

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

您可能会尝试在输出中将[NULL]盲目替换为[],但是随后您失去了区分没有标签的对象带标签的对象,其中tags.tag为空的对象的能力。 。您的应用程序逻辑和/或完整性约束可能不允许第二种情况,但这更多的原因是,如果确实成功潜入,则不禁止显示空标签。

您可以通过检查联接条件另一侧的字段是否为空来标识没有标签的对象(或通常来说,确定LEFT JOIN何时未找到匹配项)。因此,就您而言,只需替换

array_agg(tags.tag)

with

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

3
投票

我发现这可以做到:

COALESCE(ARRAY_AGG(tags.tag), ARRAY[]::TEXT[])

...假设tags.tag是文本类型。

[不确定这是否可能在较旧的Postgres版本中不起作用,但我正在使用它。 9.6,它似乎比以前提供的CASE WHEN x IS NULL... GROUP BY...解决方案有效且省事。


1
投票

文档说返回包含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()将其转换为0或您选择的其他输出。但是您需要将其应用于array elements而不是数组,在您的情况下,这将无法提供解决方案。


0
投票

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

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
© www.soinside.com 2019 - 2024. All rights reserved.