在postgres中的聚合函数中进行DISTINCT ON

时间:2015-05-06 13:04:00

标签: sql json postgresql distinct aggregate-functions

对于我的问题,我们有一个架构,其中一张照片有很多标签和许多评论。因此,如果我有一个查询,我想要所有的注释和标记,它会将行相乘。因此,如果一张照片有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

enter image description here

这对大多数事情都很好,但这意味着如果我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

enter image description here

相反,我想要一个只有'郊区'和'城市'的数组,如下所示:

 [
      {"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

4 个答案:

答案 0 :(得分:18)

每当你有一个中心表并希望将它左连接到表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

答案 1 :(得分:14)

最便宜和最简单的DISTINCT操作是..不要在"代理交叉连接中乘以行#34;首先。首先聚合,然后加入。参见:

最适合返回少数选定的行

假设 您实际上并不想要检索整个表格,而是一次只选择一张或几张精选的照片,并且汇总细节,最优雅可能最快的方法是 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

这将从commenttag返回整行,分别汇总到JSON数组中。行不是你尝试中的倍数,但它们只是"不同的"因为它们在你的基表中。

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

注意:

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

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

  • 我们不需要LEFT JOIN,因为像json_agg()这样的聚合函数总是返回一行。

通常,您只需要子集列 - 至少,不包括多余的photo_id:< / p>

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;
Postgres 9.4 引入了

json_build_object()。以前在旧版本中比较麻烦,因为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子查询便宜。适用于Postgres 9.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;

comments1comments2comments3(冗余)和tags中演示4种不同的技巧。

db&lt;&gt;小提琴here
SQL Fiddle回到Postgres 9.3 Postgres 9.6的旧SQL Fiddle

答案 2 :(得分:1)

如注释中所述,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

答案 3 :(得分:0)

我发现的最简单的方法是在DISTINCT上使用jsonb不是json!)。 (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