web-dev-qa-db-ja.com

postgresの集約関数でDISTINCT ON

私の問題については、1つの写真に多くのタグとコメントも含まれるスキーマがあります。そのため、すべてのコメントとタグを必要とするクエリがある場合、行を一緒に乗算します。したがって、1つの写真に2つのタグと13のコメントがある場合、その1つの写真に対して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個、2番目のタグのコピーが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_をシミュレートできますか?

26
Miguel

中央テーブルがあり、それをテーブル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
18

最も安価で簡単なDISTINCT操作は、最初に「プロキシ相互結合」で行を乗算しないことです。最初に集約、then join。見る:

選択されたいくつかの行を返すのに最適

仮定実際にはテーブル全体を取得するのではなく、一度に1つまたはいくつかの選択した写真だけを取得します。集約された詳細、最もエレガントでおそらく最速の方法は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配列に個別に集約されたcommentおよびtagからwhole rowsを返します。行は、あなたの試みのような乗算ではありませんが、ベーステーブルにあるのと同じくらい「異なる」だけです。

ベースデータ内の重複をさらに折りたたむには、以下を参照してください。

ノート:

  • LATERALおよびjson_agg()にはPostgres9.3以降が必要です。

  • json_agg(c)json_agg(c.*)の短縮形です。

  • json_agg()のような集約関数は常に行を返すため、_LEFT JOIN_は必要ありません。

通常、列のサブセットのみが必要です-- 少なくとも冗長な_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.4ROWコンストラクターは列名を保持しないため、以前のバージョンでは面倒でした。ただし、一般的な回避策があります。

また、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 *_を使用しなかったのは、削除された答えがあなたが望むことを示しているからです[〜#〜] null [〜#〜]の代わりに空のJSON配列タグ/コメントなし。

ベーステーブルの既存の重複も削除します

データ型jsonには等値演算子がないため、単にjson_agg(DISTINCT json_build_object(...))することはできません。見る:

さまざまな優れた方法があります。

_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;
_

_comments1_、_comments2_、_comments3_(冗長)およびtagsの4つの異なる手法を示します。

db <> fiddle here
古い SQL Fiddle Postgres 9.3にバックパッチ
古い SQL Fiddle Postgres 9.6の場合

15

コメントで述べたように、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
1
PinnyM