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

时间:2015-06-29 06:16:26

标签: postgresql left-join database-normalization

我在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但它仍然返回一个null数组。我试过让第二个参数很多(例如COALESCE(ARRAY_AGG(tags.tag), ARRAY()),但它们都会导致语法错误。

7 个答案:

答案 0 :(得分:26)

如果array_remove(..., NULL)tags.tag,则另一个选项可能是NOT NULLintroduced in 9.3)(否则您可能希望在数组中保留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

如果未找到任何标记,则返回空数组。

答案 1 :(得分:13)

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

答案 2 :(得分:10)

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

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

您可能想要在输出中盲目地将[NULL]替换为[],但是您将无法在没有标记的对象标记之间进行区分tags.tag为空的对象。您的应用程序逻辑和/或完整性约束可能不允许第二种情况,但如果它确实设法潜入,则更有理由不抑制空标记。

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

array_agg(tags.tag)

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

答案 3 :(得分:3)

我发现这样做了:

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

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

不确定这可能不适用于较旧的Postgres版本,但我在ver中使用它。 9.6它似乎比以前提供的CASE WHEN x IS NULL... GROUP BY...解决方案更有效,更简单。

答案 4 :(得分: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或您选择的其他输出。但您需要将其应用于数组元素而不是数组,在您的情况下,这不会提供解决方案。

答案 5 :(得分: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

答案 6 :(得分:0)

那呢:

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

似乎可以正常工作。