Postgres中聚合函数中的DISTINCT ON

54

针对我的问题,我们有一个模式,其中一张照片可以有多个标签和评论。因此,如果我想查询所有的评论和标签,它会将行数相乘。所以如果一张照片有2个标签和13个评论,那么对于这张照片,我会得到26行:

SELECT
        tag.name, 
        comment.comment_id
FROM
        photo
        LEFT OUTER JOIN comment ON comment.photo_id = photo.photo_id
        LEFT OUTER JOIN photo_tag ON photo_tag.photo_id = photo.photo_id
        LEFT OUTER JOIN tag ON photo_tag.tag_id = tag.tag_id

在此输入图像描述

对大多数情况都可以接受,但这意味着如果我使用GROUP BY然后使用json_agg(tag.*),我会得到第一个标签的13个副本和第二个标签的13个副本。

SELECT json_agg(tag.name) as tags
FROM
        photo
        LEFT OUTER JOIN comment ON comment.photo_id = photo.photo_id
        LEFT OUTER JOIN photo_tag ON photo_tag.photo_id = photo.photo_id
        LEFT OUTER JOIN tag ON photo_tag.tag_id = tag.tag_id
GROUP BY photo.photo_id

在此输入图像描述

相反,我想要一个仅包含“郊区”和“城市”的数组,就像这样:

 [
      {"tag_id":1,"name":"suburban"}, 
      {"tag_id":2,"name":"city"}
 ]

我可以使用json_agg(DISTINCT tag.name),但这只会生成标签名称的数组,而我想要整个行作为JSON。我想使用json_agg(DISTINCT ON(tag.name) tag.*),但显然这不是有效的SQL。

那么在Postgres中如何模拟在聚合函数中使用DISTINCT ON呢?


3
json_agg(DISTINCT tag.name) 对你应该管用,你试过了吗? - PinnyM
你能否删除与“Comments”的连接?在该表中没有选择任何内容。如果您需要保留它,因为您在“WHERE”子句中有一些内容,那么可能将每个“tag”字段添加到您的“GROUP BY”中,并且不使用“DISTINCT”。 - JNevill
@PinnyM 不行,因为json_agg只会生成标签名称的数组。我想要一个表示整行的对象数组(我的第二个查询只是一个示例,在实际情况中我想要json_agg(tag.*))。 - Migwell
@JNevill 不行,因为我还想选择 json_agg(comment.*) AS comments - Migwell
你能否请提供一下期望输出的具体样式?对我来说还不太清楚... - PinnyM
显示剩余5条评论
4个回答

50

我发现最简单的方法是在jsonb而不是json!)上使用DISTINCT。(jsonb_build_object创建jsonb对象)。

SELECT 
   JSON_AGG(
       DISTINCT jsonb_build_object('tag_id', photo_tag.tag_id, 
                                  'name', tag.name)) AS tags
FROM photo
    LEFT OUTER JOIN comment ON comment.photo_id = photo.photo_id
    LEFT OUTER JOIN photo_tag ON photo_tag.photo_id = photo.photo_id
    LEFT OUTER JOIN tag ON photo_tag.tag_id = tag.tag_id
GROUP BY photo.photo_id

2
不错。不过,使用jsonb是否存在比使用json更大的缺点? - kyw
3
@kyw,请查看此文章。文章一开始就讨论了使用jsonb相对于json的优缺点。对我而言,jsonb的优点非常不错 :) - Eugene Kovalev
@EugeneKovalev 有没有任何文档或链接可以解释 JSONB 对象中 DISTINCT 的行为?DISTINCT 对我来说似乎是在整个 JSONB 对象上计算不同的值,而不仅仅是 JSONB 对象中的任何键。虽然想知道它是如何工作的。 - Surya
1
使用jsonb_build_object()函数的DISTINCT效果非常好,简直太棒了。 - Mohammad Ashraful Islam
1
谢谢,伙计。比上面的答案容易多了。 - Alveona
显示剩余2条评论

31

每当你有一个中央表格,并想将它左连接到表A和表B中的多行,你就会遇到重复行的问题。如果不小心处理,它可能会特别影响聚合函数(如COUNTSUM)!因此我认为您需要分别构建每个照片的标签和评论,然后将它们连接在一起:

WITH tags AS (
  SELECT  photo.photo_id, json_agg(row_to_json(tag.*)) AS tags
  FROM    photo
  LEFT OUTER JOIN photo_tag on photo_tag.photo_id = photo.photo_id
  LEFT OUTER JOIN tag ON photo_tag.tag_id = tag.tag_id
  GROUP BY photo.photo_id
),
comments AS (
  SELECT  photo.photo_id, json_agg(row_to_json(comment.*)) AS comments
  FROM    photo
  LEFT OUTER JOIN comment ON comment.photo_id = photo.photo_id
  GROUP BY photo.photo_id
)
SELECT  COALESCE(tags.photo_id, comments.photo_id) AS photo_id,
        tags.tags,
        comments.comments
FROM    tags
FULL OUTER JOIN comments
ON      tags.photo_id = comments.photo_id

编辑: 如果你真的想在没有CTE的情况下将所有内容组合在一起,那么这似乎可以提供正确的结果:

SELECT  photo.photo_id,
        to_json(array_agg(DISTINCT tag.*)) AS tags,
        to_json(array_agg(DISTINCT comment.*)) AS comments
FROM    photo
LEFT OUTER JOIN comment ON comment.photo_id = photo.photo_id
LEFT OUTER JOIN photo_tag on photo_tag.photo_id = photo.photo_id
LEFT OUTER JOIN tag ON photo_tag.tag_id = tag.tag_id
GROUP BY photo.photo_id

你好,@paul,请问以下查询语句中是否有添加DISTINCT的方法:json_agg( json_build_object ( 'id', ca.id, 'name',ca.name, 'url',ca.url) ) as authors - Saurabh Chauhan
我认为你可以做到,但这有点棘手。似乎值得提出一个单独的问题! - Paul A Jungwirth

28
最便宜和最简单的“DISTINCT”操作是……不要在第一次“代理交叉连接”中增加行。先聚合,然后再连接。参见:

最适用于返回少量选定行

假设您实际上不想检索整个表,而只想一次检索一个或几个选定的照片,并带有聚合详细信息。那么LATERAL子查询是快速而优雅的解决方案:
SELECT *
FROM   photo p
CROSS  JOIN LATERAL (
   SELECT json_agg(c) AS comments
   FROM   comment c
   WHERE  photo_id = p.photo_id
   ) c1
CROSS  JOIN LATERAL (
   SELECT json_agg(t) AS tags
   FROM   photo_tag pt
   JOIN   tag       t USING (tag_id)
   WHERE  pt.photo_id = p.photo_id
   ) t
WHERE  p.photo_id = 2;  -- arbitrary selection

这将从评论和标签中返回整行,并分别聚合到JSON数组中。行不像您的尝试中那样重复,但它们只能在基本表中“区分”。

要在基本数据中另外折叠重复项,请参见下文。

注:

  • LATERALjson_agg()需要Postgres 9.3或更高版本。

  • json_agg(c)json_agg(c.*)的缩写。

  • 我们不需要进行LEFT JOIN,因为诸如json_agg()的聚合函数始终返回一行。

通常情况下,您只需要至少排除冗余的photo_id列的子集:

SELECT *
FROM   photo p
CROSS  JOIN LATERAL (
   SELECT json_agg(json_build_object('comment_id', comment_id
                                   , 'comment', comment)) AS comments
   FROM   comment
   WHERE  photo_id = p.photo_id
   ) c
CROSS  JOIN LATERAL (
   SELECT json_agg(t) AS tags
   FROM   photo_tag pt
   JOIN   tag       t USING (tag_id)
   WHERE  pt.photo_id = p.photo_id
   ) t
WHERE  p.photo_id = 2;

json_build_object() 是在Postgres 9.4中引入的。在旧版本中如果使用ROW构造函数就不会保留列名,因此变得繁琐。但有通用的解决方法:

此外,还可以自由选择JSON键名,不必坚持列名。

最适合返回所有或大多数行

SELECT p.*
     , COALESCE(c1.comments, '[]') AS comments
     , COALESCE(t.tags, '[]') AS tags
FROM   photo p
LEFT   JOIN (
   SELECT photo_id
        , json_agg(json_build_object('comment_id', comment_id
                                   , 'comment', comment)) AS comments
   FROM   comment c
   GROUP  BY 1
   ) c1 USING (photo_id)
LEFT  JOIN LATERAL (
   SELECT photo_id , json_agg(t) AS tags
   FROM   photo_tag pt
   JOIN   tag       t USING (tag_id)
   GROUP  BY 1
   ) t USING (photo_id);

一旦我们检索到足够的行,这比 LATERAL 子查询更便宜。适用于Postgres9.3+

请注意连接条件中的 USING 子句。这样,我们就可以方便地在外部查询中使用 SELECT * 而不会为 photo_id 获取重复列。我没有在这里使用 SELECT * ,因为您删除的答案表明您希望对于没有标签/评论的情况,使用空JSON数组而不是NULL

还要从基本表中删除现有的重复项

你不能只使用 json_agg(DISTINCT json_build_object(...)) ,因为数据类型 json 没有相等运算符。参见:

有各种更好的方法:

SELECT *
FROM   photo p
CROSS  JOIN LATERAL (
   SELECT json_agg(to_json(c1.comment)) AS comments1
        , json_agg(json_build_object('comment', c1.comment)) AS comments2
        , json_agg(to_json(c1)) AS comments3
   FROM  (
      SELECT DISTINCT c.comment  -- folding dupes here
      FROM   comment c
      WHERE  c.photo_id = p.photo_id
   -- ORDER  BY comment --  any particular order?
      ) c1
   ) c2
CROSS  JOIN LATERAL (
   SELECT jsonb_agg(DISTINCT t) AS tags  -- demonstrating jsonb_agg
   FROM   photo_tag pt
   JOIN   tag       t USING (tag_id)
   WHERE  pt.photo_id = p.photo_id
   ) t
WHERE  p.photo_id = 2;

展示了4种不同的技术,分别是comments1comments2comments3(冗余)和tagsdb<>fiddle 在这里
旧版:sqlfiddle 适用于 Postgres 9.3;sqlfiddle 适用于 Postgres 9.6

使用json_build_object(distinct...)会非常棒。 - victor n.
你好,@Erwin,请问以下查询语句中是否有添加DISTINCT的方法:- json_agg( json_build_object ( 'id', ca.id, 'name',ca.name, 'url',ca.url) ) as authors - Saurabh Chauhan
@SaurabhChauhan:我添加了一个章节来解决这个问题。如果还有不清楚的地方,请提出详细的问题 - Erwin Brandstetter
1
已经使用SQL工作了二十多年,但总有新的东西要学习... :o - mmey

5

如评论所述,json_agg不会将一行序列化为对象,而是构建一个包含您传递给它的值的JSON数组。您需要使用row_to_json将行转换为JSON对象,然后使用json_agg将其聚合到数组中:

SELECT json_agg(DISTINCT row_to_json(comment)) as tags
FROM
    photo
    LEFT OUTER JOIN comment ON comment.photo_id = photo.photo_id
    LEFT OUTER JOIN photo_tag ON photo_tag.photo_id = photo.photo_id
    LEFT OUTER JOIN tag ON photo_tag.tag_id = tag.tag_id
GROUP BY photo.photo_id

实际上,json_agg会将行隐式地转换为JSON。看一下我刚刚创建的这个例子:http://pastebin.com/8jVSNstd。但你查询的问题是因为没有JSON比较运算符,它无法找到不同的行,因为它们都是JSON。 - Migwell
2
如果我复制你的完整代码并运行它,我会得到“ERROR: could not identify an equality operator for type json”的错误信息,这就是我所指的。 - Migwell
@Miguel 嗯,你试过在行上调用DISTINCT吗,就像这样:json_agg(DISTINCT tag)?如果这不起作用,你可能需要使用子查询来完成这个任务。 - PinnyM
2
这不起作用,因为行隐式转换为JSON,并且您会收到相同的错误。如果您知道如何使用子查询来执行此操作,我很想知道,因为我想不到任何方法。 - Migwell

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接