1
votes

Convertir une table clé / valeur aplatie en JSON hiérarchique dans PostgreSQL

J'ai une table PostgreSQL avec des paires clé / valeur uniques, qui étaient à l'origine au format JSON, mais qui ont été normalisées et fusionnées:

{
"name": "Bob",
"address": {
    "city": "Vancouver",
    "country": "Canada"
    }
}

Je dois transformer cela en un JSON hiérarchique :

key             | value
-----------------------------
name            | Bob
address.city    | Vancouver
address.country | Canada

Y a-t-il un moyen de faire cela facilement dans SQL?


0 commentaires

4 Réponses :


1
votes

Je ne peux pas vraiment penser à quelque chose de plus simple, même si je pense qu'il devrait y avoir un moyen plus simple.

Je suppose qu'il y a une colonne supplémentaire qui peut être utilisée pour rassembler les clés appartenant à une "personne", J'ai utilisé p_id pour cela dans mon exemple.

p_id | jsonb_object_agg                                                      
-----+-----------------------------------------------------------------------
   1 | {"name": "Bob", "address": {"city": "Vancouver", "country": "Canada"}}
   2 | {"name": "John", "address": {"city": "Munich", "country": "Germany"}} 

La requête la plus interne convertit simplement la notation par points en un tableau pour un accès plus facile plus tard.

Le niveau suivant construit ensuite des objets JSON en fonction de la "clé". Pour les clés "à un seul niveau", il utilise juste la clé / valeur, pour les autres, il utilise le deuxième élément + la valeur et agrège ensuite ceux qui appartiennent ensemble.

Le deuxième niveau de requête renvoie ce qui suit:

create table kv (p_id integer, "key" text, value text);
insert into kv
values
(1, 'name','Bob'),
(1, 'address.city','Vancouver'),
(1, 'address.country','Canada'),
(2, 'name','John'),
(2, 'address.city','Munich'),
(2, 'address.country','Germany');

L'agrégation effectuée à la deuxième étape, laisse un niveau de trop pour les clés "élément unique" , et c'est pour cela que nous avons besoin de niveau.

Si cette distinction n'était pas faite, l'agrégation finale renverrait {"name": {"name": "Bob"}, "address": {"city": "Vancouver" , "country": "Canada"}} au lieu du recherché: {"name": "Bob", "address": {"city": "Vancouver", "country": "Canada "}} .

L'expression case level when 1 then v -> k else v end tourne essentiellement {" name ":" Bob "} retour à "Bob".


Donc, avec les exemples de données suivants:

p_id | k       | v                                          | level
-----+---------+--------------------------------------------+------
   1 | address | {"city": "Vancouver", "country": "Canada"} |     2
   1 | name    | {"name": "Bob"}                            |     1
   2 | address | {"city": "Munich", "country": "Germany"}   |     2
   2 | name    | {"name": "John"}                           |     1

puis la requête renvoie:

select p_id, 
       jsonb_object_agg(k, case level when 1 then v -> k else v end) 
from (
  select p_id, 
         elements[1] k, 
         jsonb_object_agg(case cardinality(elements) when 1 then ky else elements[2] end, value) v, 
         max(cardinality(elements)) as level
  from (       
    select p_id, 
           "key" as ky, 
           string_to_array("key", '.') as elements, value
    from kv 
  ) t1
  group by p_id, k
) t2
group by p_id;

Exemple en ligne: https: // rextester. com / SJOTCD7977


3 commentaires

Merci a_horse_with_no_name. C'est la meilleure solution qui ne nécessite aucune création de nouvelles fonctions. Malheureusement, cela ne fonctionne que sur les deux niveaux. S'il existe plus de 2 niveaux, la clé de 2e niveau acquiert la valeur de l'un des niveaux inférieurs.


@maciej: la réponse d'arcadio peut être écrite sans créer les vues. Utilisez simplement des expressions de table courantes à la place avec v_kv as (...), v_datos as (...) select ...


oui, mais vous devez encore coder en dur les éléments (nom, adresse, etc.) Je ne sais pas comment cela pourrait être généralisé.



1
votes
select json_agg(json_build_object('name',name, 'address', json_build_object('city',address_city, 'country', address_country)))
    from  v_datos;

1 commentaires

Merci Arcadio, cela est utile pour un cas simple où la structure des données est connue. Malheureusement, mes données sont plus complexes et imprévisibles.



2
votes

Il n'y a pas d'outils prêts à l'emploi pour cela. La fonction génère un objet json hiérarchique basé sur un chemin:

{
    "name": "Bob",
    "first":
    {
        "second":
        {
            "third": "value"
        }
    },
    "address":
    {
        "city": "Vancouver",
        "country": "Canada"
    }
}

Vous avez également besoin de la fonction d'agrégation jsonb_merge_agg (jsonb) décrite dans cette réponse. La requête:

with my_table (path, value) as (
values
    ('name', 'Bob'),
    ('address.city', 'Vancouver'),
    ('address.country', 'Canada'),
    ('first.second.third', 'value')
)

select jsonb_merge_agg(jsonb_build_object_from_path(path, value))
from my_table;

donne cet objet:

create or replace function jsonb_build_object_from_path(path text, value text)
returns jsonb language plpgsql as $$
declare
    obj jsonb;
    keys text[] := string_to_array(path, '.');
    level int := cardinality(keys);
begin
    obj := jsonb_build_object(keys[level], value);
    while level > 1 loop
        level := level- 1;
        obj := jsonb_build_object(keys[level], obj);
    end loop;
    return obj;
end $$;


1 commentaires

Merci beaucoup pour cette excellente solution @klin! J'aimerais vraiment pouvoir accepter deux solutions, car cela fait aussi très bien le travail. J'ai choisi l'autre car j'ai pu facilement le modifier pour créer également des tableaux JSON, sans approfondir les fonctions génériques.



3
votes

jsonb_set () presque fait tout pour vous, mais malheureusement, il ne peut créer que des feuilles manquantes (c'est-à-dire les dernières clés manquantes sur un chemin), mais pas des branches manquantes entières. Pour surmonter cela, voici une version modifiée de celui-ci, qui peut définir des valeurs sur tous les niveaux manquants:

select   grp, jsonb_set_agg(to_jsonb(value), string_to_array(key, '.'))
from     eav_tbl
group by grp;

Il ne vous reste plus qu'à appliquer cette fonction une par une à vos lignes, en commençant par avec un objet json vide: {} . Vous pouvez le faire avec les CTE récursifs :

XXX

Ou , avec un agrégat personnalisé défini comme:

create aggregate jsonb_set_agg(jsonb, text[]) (
  sfunc    = jsonb_set_rec,
  stype    = jsonb,
  initcond = '{}'
);

votre requête pourrait devenir aussi simple que:

with recursive props as (
   (select   distinct on (grp)
             pk, grp, jsonb_set_rec('{}', to_jsonb(value), string_to_array(key, '.')) json_object
    from     eav_tbl
    order by grp, pk)
  union all
   (select   distinct on (grp)
             eav_tbl.pk, grp, jsonb_set_rec(json_object, to_jsonb(value), string_to_array(key, '.'))
    from     props
    join     eav_tbl using (grp)
    where    eav_tbl.pk > props.pk
    order by grp, eav_tbl.pk)
)
select   distinct on (grp)
         grp, json_object
from     props
order by grp, pk desc;

https://rextester.com/TULNU73750


1 commentaires

Wow, merci pour cette solution vraiment élégante! Au cas où cela serait avantageux pour les autres - dans ma table actuelle, j'avais des valeurs qui étaient des listes stockées sous forme de texte comme "['un', 'deux', 'trois']". Pour que cette solution fonctionne aussi pour cela, j'ai remplacé les instances de to_jsonb (valeur) par CASE substring (value, 1, 1) WHEN '[' THEN to_jsonb (pylist_to_array (value)) ELSE to_jsonb (valeur) END , où pylist_to_array est une fonction personnalisée qui convertit ce type de TEXTE en TEXT [].