Подтвердить что ты не робот

DISTINCT ON в совокупной функции в postgres

Для моей проблемы у нас есть схема, в которой на одной фотографии есть много тегов, а также много комментариев. Поэтому, если у меня есть запрос, где я хочу все комментарии и теги, он будет умножать строки вместе. Поэтому, если у одной фотографии есть 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, по-видимому.

Как я могу моделировать DISTINCT ON внутри агрегатной функции в Postgres?

4b9b3361

Ответ 1

Всякий раз, когда у вас есть центральная таблица и вы хотите присоединиться к ней во многие строки таблицы A, а также соединить ее со многими строками в таблице B, вы сталкиваетесь с этими проблемами дублирования строк. Это может особенно сбросить функции агрегации, такие как COUNT и SUM, если вы не будете осторожны! Поэтому я думаю, что вам нужно отдельно создавать свои метки для каждой фотографии и комментарии для каждой фотографии, а затем объединить их:

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

EDIT: Если вы действительно хотите объединить все вместе без 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

Ответ 2

Самая дешевая и простая операция 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

Это возвращает целые строки из comment и tag, сгруппированные в массивы JSON отдельно. Строки не являются умножениями, как в вашей попытке, но они настолько же "различны", как и в ваших базовых таблицах.

Чтобы дополнительно сложить дубликаты в базе данных, см. Ниже.

Заметки:

  • LATERAL и json_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 подзапросов. Работает на 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;

Демонстрация 4 различных методов в comments1, comments2, comments3 (избыточно) и tags.

дБ <> скрипка здесь
Старая скрипта SQL, исправленная в Postgres 9.3
Старая SQL скрипка для Postgres 9.6

Ответ 3

Как указано в комментариях, 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