Pour mon problème, nous avons un schéma selon lequel une photo a de nombreux tags et aussi de nombreux commentaires. Donc, si j'ai une requête où je veux tous les commentaires et balises, cela multipliera les lignes ensemble. Donc, si une photo a 2 balises et 13 commentaires, j'obtiens 26 lignes pour cette seule photo:
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
C'est bien pour la plupart des choses, mais cela signifie que si je GROUP BY
Puis json_agg(tag.*)
, j'obtiens 13 copies de la première balise et 13 copies de la deuxième balise.
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
Au lieu de cela, je veux un tableau qui ne soit que "suburbain" et "ville", comme ceci:
[
{"tag_id":1,"name":"suburban"},
{"tag_id":2,"name":"city"}
]
Je pourrais json_agg(DISTINCT tag.name)
, mais cela ne fera qu'un tableau de noms de balises, quand je veux que la ligne entière soit json. Je voudrais json_agg(DISTINCT ON(tag.name) tag.*)
, mais ce n'est pas du SQL valide apparemment.
Comment puis-je simuler DISTINCT ON
À l'intérieur d'une fonction d'agrégation dans Postgres?
Chaque fois que vous avez une table centrale et que vous souhaitez la joindre à gauche à plusieurs lignes du tableau A et également la joindre à gauche à de nombreuses lignes du tableau B, vous rencontrez ces problèmes de duplication des lignes. Il peut surtout supprimer les fonctions d'agrégat comme COUNT
et SUM
si vous n'y faites pas attention! Je pense donc que vous devez créer vos balises pour chaque photo et vos commentaires pour chaque photo séparément, puis les assembler:
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: Si vous voulez vraiment tout réunir sans CTE, cela ressemble à des résultats corrects:
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
L'opération DISTINCT
la moins chère et la plus simple est .. de ne pas multiplier les lignes dans une "jointure proxy" en premier lieu. Agréger d'abord, puis rejoindre. Voir:
En supposant vous ne voulez en fait pas récupérer la table entière, mais juste une ou quelques photos sélectionnées à la fois , avec des détails agrégés, le moyen le plus élégant et probablement le plus rapide est avec LATERAL
sous-requêtes :
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
Cela renvoie des lignes entières de comment
et tag
, agrégées séparément dans des tableaux JSON. Les lignes ne sont pas des multiplications comme dans votre tentative, mais elles sont aussi "distinctes" qu'elles le sont dans vos tables de base.
Pour plier en plus les doublons dans les données de base, voir ci-dessous.
Remarques:
LATERAL
et json_agg()
nécessitent Postgres 9.3 ou version ultérieure.
json_agg(c)
est l'abréviation de json_agg(c.*)
.
Nous n'avons pas besoin de LEFT JOIN
Car une fonction d'agrégation comme json_agg()
renvoie toujours une ligne.
Typiquement , vous ne voudriez qu'un sous-ensemble de colonnes - au moins excluant le redondant 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()
a été introduit avec Postgres 9.4 . Utilisé pour être encombrant dans les anciennes versions car un constructeur ROW
ne conserve pas les noms de colonne. Mais il existe des solutions de contournement génériques:
Permet également de choisir librement les noms de clés JSON, vous n'avez pas à vous en tenir aux noms de colonnes.
Pour renvoyer toutes les lignes, c'est plus efficace:
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);
Une fois que nous avons récupéré suffisamment de lignes, cela revient moins cher que les sous-requêtes LATERAL
. Fonctionne pour Postgres 9.3 + .
Notez la clause USING
dans la condition de jointure. De cette façon, nous pouvons facilement utiliser SELECT *
Dans la requête externe sans obtenir de colonnes en double pour photo_id
. Je n'ai pas utilisé SELECT *
Ici car votre réponse supprimée indique que vous voulez des tableaux JSON vides au lieu de [~ # ~] null [~ # ~] pour aucun tag/aucun commentaire.
Vous ne pouvez pas simplement json_agg(DISTINCT json_build_object(...))
car il n'y a pas d'opérateur d'égalité pour le type de données json
. Voir:
Il existe de meilleures façons:
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;
Démonstration de 4 techniques différentes dans comments1
, comments2
, comments3
(Redondant) et tags
.
db <> violon ici
Ancien SQL Fiddle backpatched to Postgres 9.3
Ancien SQL Fiddle pour Postgres 9.6
Comme indiqué dans les commentaires, json_agg ne sérialise pas une ligne en tant qu'objet, mais crée un tableau JSON des valeurs que vous lui transmettez. Tu auras besoin row_to_json
pour transformer votre ligne en objet JSON, puis json_agg
pour effectuer l'agrégation dans un tableau:
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