web-dev-qa-db-fra.com

Postgres renvoie [null] au lieu de [] pour array_agg de la table de jointure

Je sélectionne des objets et leurs tags dans Postgres. Le schéma est assez simple, trois tables:

objetsid

taggingsid | object_id | tag_id

tagsid | tag

Je rejoins les tables comme ceci, en utilisant array_agg pour regrouper les balises dans un seul champ:

SELECT objects.*,
    array_agg(tags.tag) AS tags,
FROM objects
LEFT JOIN taggings ON objects.id = taggings.object_id
LEFT JOIN tags ON tags.id = taggings.tag_id

Cependant, si l'objet n'a pas de balises, Postgres renvoie ceci:

[ null ]

au lieu d'un tableau vide. Comment puis-je retourner un tableau vide lorsqu'il n'y a pas de balises? J'ai vérifié deux fois que je n'ai pas de balise null retournée.

Le agrégat docs dire "La fonction coalesce peut être utilisée pour substituer zéro ou un tableau vide à NULL si nécessaire". J'ai essayé COALESCE(ARRAY_AGG(tags.tag)) as tags mais il retourne toujours un tableau avec null. J'ai essayé de créer de nombreuses choses dans le deuxième paramètre (comme COALESCE(ARRAY_AGG(tags.tag), ARRAY()), mais elles entraînent toutes des erreurs de syntaxe.

28
Andy Ray

Une autre option pourrait être array_remove(..., NULL) ( introduite dans 9.3 ) si tags.tag est NOT NULL (sinon, vous voudrez peut-être conserver les valeurs NULL dans le tableau, mais dans ce cas, vous ne pouvez pas faire la distinction entre une balise NULL existante et une variable NULL. tag en raison du LEFT JOIN):

SELECT objects.*,
     array_remove(array_agg(tags.tag), NULL) AS tags,
FROM objects
LEFT JOIN taggings ON objects.id = taggings.object_id
LEFT JOIN tags ON tags.id = taggings.tag_id

Si aucune balise n'est trouvée, un tableau vide est renvoyé.

23
Thomas Perl

La documentation indique que, lorsque vous n’ajoutez aucune ligne, vous obtenez une valeur null et la remarque sur l’utilisation de COALESCE aborde ce cas particulier.

Cela ne s'applique pas à votre requête, en raison de la manière dont un LEFT JOIN se comporte - quand il trouve zéro lignes correspondantes, il retourne une ligne, remplie de valeurs null (et l'agrégat d'une ligne nulle est un tableau avec un élément nul).

Vous pourriez être tenté de remplacer aveuglément [NULL] par [] dans la sortie, mais vous perdrez alors la possibilité de distinguer les objets sans balises et objets marqués où tags.tag est nul . La logique de votre application et/ou les contraintes d’intégrité ne permettent peut-être pas ce second cas, mais c’est une raison de plus de ne pas supprimer une balise null si elle parvient à se faufiler.

Vous pouvez identifier un objet sans balises (ou, en général, indiquer quand un LEFT JOIN n'a trouvé aucune correspondance) en vérifiant si le champ situé de l'autre côté de la condition de jointure est null. Donc, dans votre cas, il suffit de remplacer

array_agg(tags.tag)

avec

CASE
  WHEN taggings.object_id IS NULL
  THEN ARRAY[]::text[]
  ELSE array_agg(tags.tag)
END
9
Nick Barnes

Depuis la version 9.4, il est possible de restreindre un appel de fonction d'agrégation pour ne traiter que les lignes correspondant à un certain critère: array_agg(tags.tag) filter (where tags.tag is not null)

8
Alexey Bashtanov

J'ai découvert que cela va le faire:

COALESCE(ARRAY_AGG(tags.tag), ARRAY[]::TEXT[])

... en supposant que tags.tag est un type de texte.

Je ne sais pas si cela ne fonctionnerait peut-être pas dans les anciennes versions de Postgres, mais je l’utilise en version 9.6 et il semble fonctionner et être moins encombrant que la solution CASE WHEN x IS NULL... GROUP BY... fournie précédemment.

2
user9645

La documentation indique qu'un tableau contenant NULL est renvoyé. Si vous voulez convertir cela en un tableau vide, alors vous devez faire quelques magies mineures:

SELECT objects.id,
    CASE WHEN length((array_agg(tags.tag))[1]) > 0
    THEN array_agg(tags.tag) 
    ELSE ARRAY[]::text[] END AS tags
FROM objects
LEFT JOIN taggings ON objects.id = taggings.object_id
LEFT JOIN tags ON tags.id = taggings.tag_id
GROUP BY 1;

Cela suppose que les balises sont de type text (ou l’une de ses variantes); modifiez le casting si nécessaire.

L'astuce ici est que le premier (et unique) élément d'un tableau [NULL] a une longueur de 0. Par conséquent, si des données sont renvoyées à partir de tags, vous renvoyez l'agrégat, sinon créez un tableau vide du type approprié.

Incidemment, la documentation relative à l'utilisation de coalesce() est un peu dégoûtante: si vous ne voulez pas de NULL, vous pouvez utiliser coalesce() pour le transformer en 0 ou en une autre sortie de votre choix. Mais vous devez appliquer cela aux éléments array au lieu du tableau, qui, dans votre cas, ne fournirait pas de solution.

1
Patrick