web-dev-qa-db-fra.com

Comment dégrouper et regrouper des éléments d'un tableau JSON?

Étant donné la table band, avec une colonne json contenant un tableau:

id | people
---+-------------
1  | ['John', 'Thomas']
2  | ['John', 'James']
3  | ['James', 'George']

Comment lister le nombre de bandes dont chaque nom fait partie?
sortie souhaitée:

name   | count
-------+------------
John   | 2
James  | 2
Thomas | 1
George | 1
8
Bax

Le type de données de la colonne people est json, tout comme le résultat de json_array_elements(people). Et il n'y a pas d'opérateur d'égalité (=) Pour le type de données json. Vous ne pouvez donc pas non plus exécuter GROUP BY Dessus. Plus:

jsonb possède un opérateur d'égalité, donc la "solution de contournement" dans votre réponse consiste à transtyper en jsonb et à utiliser l'équivalent jsonb_array_elements(). Le casting ajoute du coût:

jsonb_array_elements(people::jsonb)

Depuis Postgres 9.4, nous avons également json_array_elements_text(json) renvoyant les éléments du tableau sous la forme text. En relation:

Donc:

SELECT p.name, count(*) AS c
FROM   band b, json_array_elements_text(b.people) p(name)
GROUP  BY p.name;

Il semble plus pratique d'obtenir des noms sous la forme d'objets text au lieu de jsonb (entre guillemets dans la représentation textuelle) et votre "sortie souhaitée" indique que vous voulez/avez besoin text dans le résultat pour commencer.

GROUP BY Sur text les données sont également moins chères que sur jsonb, donc cette solution alternative devrait être plus rapide pour deux raisons. (Testez avec EXPLAIN (ANALYZE, TIMING OFF).)

Pour mémoire, il n'y avait rien de mal à votre réponse d'origine . La virgule (,) Est tout aussi "correcte" que CROSS JOIN LATERAL. Le fait d'avoir été défini plus tôt dans SQL standard ne le rend pas inférieur. Voir:

Il n'est pas non plus plus portable pour les autres SGBDR, et puisque jsonb_array_elements() ou json_array_elements_text() ne sont pas portables pour les autres SGBDR pour commencer, ce n'est pas non plus pertinent. La courte requête ne devient pas plus claire avec CROSS JOIN LATERAL IMO, mais le dernier bit est juste mon opinion personnelle.

J'ai utilisé l'alias de table et de colonne plus explicite p(name) et la référence qualifiée de table p.name Pour se défendre contre les noms en double possibles. name est un tel mot commun, il peut également apparaître comme nom de colonne dans la table sous-jacente band, auquel cas il se résoudrait silencieusement en band.name. La forme simple json_array_elements_text(people) name attache uniquement un alias table, le nom de la colonne est toujours value, tel que renvoyé par la fonction. Mais name résout sa seule colonne value lorsqu'il est utilisé dans la liste SELECT. Il arrive à fonctionner comme prévu. Mais un vrai nom de colonne name (si band.name Devait exister) se lierait en premier. Bien que cela ne morde pas dans l'exemple donné, il peut s'agir d'un une arme à pied chargée dans d'autres cas.

N'utilisez pas le "nom" générique comme identifiant pour commencer. Peut-être que c'était juste pour le cas de test simple.


Si la colonne people peut contenir autre chose qu'un simple tableau JSON, l'une ou l'autre requête déclencherait une exception. Si vous ne pouvez pas garantir l'intégrité des données, vous voudrez peut-être vous défendre avec json_typeof() :

SELECT p.name, count(*) AS c
FROM   band b, json_array_elements_text(b.people) p(name)
WHERE  json_typeof(b.people) = 'array'
GROUP  BY 1; -- optional short syntax since you seem to prefer short syntax

Exclut les lignes violées de la requête.

En relation:

6

Basé sur @ ypercubeᵀᴹ commentaire avec lequel je me suis retrouvé:

SELECT name, count(*) as c
FROM band 
CROSS JOIN LATERAL jsonb_array_elements(people::jsonb) as name
GROUP BY name;

Juste utilisé jsonb_array_elements au lieu de unnest.

4
Bax