web-dev-qa-db-fra.com

Comment transformer un tableau JSON en tableau Postgres?

J'ai une colonne data de type json qui contient des documents JSON comme ceci:

{
    "name": "foo",
    "tags": ["foo", "bar"]
}

Je voudrais transformer le tableau tags imbriqué en une chaîne concaténée ('foo, bar'). Cela serait facilement possible avec la fonction array_to_string() en théorie. Cependant, cette fonction n'accepte pas l'entrée json. Je me demande donc comment transformer ce tableau JSON en tableau Postgres (tapez text[])?

77
Christoph

Postgres 9.4 ou plus récent

De toute évidence inspiré par cet article , Postgres 9.4 a ajouté la ou les fonctions manquantes:
Merci à Laurence Rowe pour le patch et à Andrew Dunstan pour son engagement!

Pour délier le tableau JSON. Utilisez ensuite array_agg() ou un constructeur ARRAY pour construire un Postgres array à partir de celui-ci. Ou string_agg() pour construire une text chaîne.

Agréger les éléments non imbriqués par ligne dans une sous-requête LATERAL ou corrélée. Ensuite l'ordre d'origine est conservé et nous n'avons pas besoin de ORDER BY, GROUP BY Ni même d'une clé unique dans la requête externe. Voir:

Remplacez 'json' par 'jsonb' pour jsonb dans tout le code SQL suivant.

SELECT t.tbl_id, d.list
FROM   tbl t
CROSS  JOIN LATERAL (
   SELECT string_agg(d.elem::text, ', ') AS list
   FROM   json_array_elements_text(t.data->'tags') AS d(elem)
   ) d;

Syntaxe courte:

SELECT t.tbl_id, d.list
FROM   tbl t, LATERAL (
   SELECT string_agg(value::text, ', ') AS list
   FROM   json_array_elements_text(t.data->'tags')  -- col name default: "value"
   ) d;

En relation:

Constructeur ARRAY dans la sous-requête corrélée:

SELECT tbl_id, ARRAY(SELECT json_array_elements_text(t.data->'tags')) AS txt_arr
FROM   tbl t;

En relation:

Différence subtile : null les éléments sont conservés dans les réels tableaux. Cela n'est pas possible dans les requêtes ci-dessus produisant une chaîne text, qui ne peut pas contenir de valeurs null. La représentation vraie est un tableau.

Enveloppeur de fonction

Pour une utilisation répétée, pour simplifier encore plus, encapsulez la logique dans une fonction:

CREATE OR REPLACE FUNCTION json_arr2text_arr(_js json)
  RETURNS text[] LANGUAGE sql IMMUTABLE PARALLEL SAFE AS
'SELECT ARRAY(SELECT json_array_elements_text(_js))';

Faites-en une fonction SQL , afin qu'elle puisse être insérée dans les requêtes plus importantes.
Faites-le IMMUTABLE (car il l'est) pour éviter une évaluation répétée dans les requêtes plus volumineuses et autorisez-le dans les expressions d'index.
Faites en sorte que PARALLEL SAFE (Dans Postgres 9.6 ou version ultérieure!) Ne fasse pas obstacle au parallélisme. Voir:

Appel:

SELECT tbl_id, json_arr2text_arr(data->'tags')
FROM   tbl;

db <> violon ici


Postgres 9.3 ou plus

Utilisez la fonction json_array_elements() . Mais nous obtenons des chaînes entre guillemets doubles .

Requête alternative avec agrégation dans la requête externe. CROSS JOIN Supprime les lignes avec des tableaux manquants ou vides. Peut également être utile pour le traitement des éléments. Nous avons besoin d'une clé unique pour agréger:

SELECT t.tbl_id, string_agg(d.elem::text, ', ') AS list
FROM   tbl t
CROSS  JOIN LATERAL json_array_elements(t.data->'tags') AS d(elem)
GROUP  BY t.tbl_id;

Constructeur ARRAY, toujours avec des chaînes entre guillemets:

SELECT tbl_id, ARRAY(SELECT json_array_elements(t.data->'tags')) AS quoted_txt_arr
FROM   tbl t;

Notez que null est converti en la valeur de texte "null", contrairement à ci-dessus. Incorrect, à proprement parler et potentiellement ambigu.

Unquoting du pauvre avec trim():

SELECT t.tbl_id, string_agg(trim(d.elem::text, '"'), ', ') AS list
FROM   tbl t, json_array_elements(t.data->'tags') d(elem)
GROUP  BY 1;

Récupérez une seule ligne de tbl:

SELECT string_agg(trim(d.elem::text, '"'), ', ') AS list
FROM   tbl t, json_array_elements(t.data->'tags') d(elem)
WHERE  t.tbl_id = 1;

Les chaînes forment une sous-requête corrélée:

SELECT tbl_id, (SELECT string_agg(trim(value::text, '"'), ', ')
                FROM   json_array_elements(t.data->'tags')) AS list
FROM   tbl t;

Constructeur ARRAY:

SELECT tbl_id, ARRAY(SELECT trim(value::text, '"')
                     FROM   json_array_elements(t.data->'tags')) AS txt_arr
FROM   tbl t;

Original (obsolète) SQL Fiddle .
db <> violon ici.

En relation:

Notes (obsolète depuis pg 9.4)

Nous aurions besoin d'un json_array_elements_text(json), le jumeau de json_array_elements(json) pour renvoyer les valeurs appropriées de text à partir d'un tableau JSON. Mais cela semble manquer dans le fourni l'arsenal des fonctions JSON . Ou une autre fonction pour extraire une valeur text d'une valeur scalaire JSON. Il me semble que je manque celui-là aussi.
J'ai donc improvisé avec trim(), mais que échouera pour les cas non triviaux ...

99
Erwin Brandstetter

PG 9.4 +

La réponse acceptée est certainement ce dont vous avez besoin, mais pour des raisons de simplicité, voici une aide que j'utilise pour cela:

CREATE OR REPLACE FUNCTION jsonb_array_to_text_array(p_input jsonb)
 RETURNS text[]
 LANGUAGE sql
 IMMUTABLE
AS $function$

SELECT array_agg(ary)::text[] FROM jsonb_array_elements_text(p_input) AS ary;

$function$;

Ensuite, faites simplement:

SELECT jsonb_array_to_text_array('["a", "b", "c"]'::jsonb);

Mis à jour le 23/02/2020 en réponse aux commentaires: Les commentaires sont corrects que cela pourrait être plus efficace. Au moment où j'ai posté, aucune solution modulaire n'était proposée, j'en ai donc proposé une sérieusement, si elle n'était pas optimale. Depuis lors, Erwin a mis à jour sa réponse avec une fonction simple et efficace, donc je n'ai jamais mis à jour la mienne. Mise à jour maintenant car il y a encore de l'attention à venir à cette réponse

Encore une mise à jour, car cela m'a juste mord: La fonction ci-dessus renverra null s'il n'y a pas de valeurs. Cela peut ne pas être souhaitable selon votre situation. Voici une fonction qui renvoie un tableau vide si la valeur n'est pas null, mais renvoie toujours null si l'entrée est null.

CREATE OR REPLACE FUNCTION jsonb_array_to_text_array_strict(p_input jsonb)
 RETURNS text[]
 LANGUAGE sql
 IMMUTABLE
AS $function$

SELECT 
  CASE 
    WHEN p_input IS null 
    THEN null 
    ELSE coalesce(ary_out, ARRAY[]::text[]) 
  END
FROM (
  SELECT array_agg(ary)::text[] AS ary_out
  FROM jsonb_array_elements_text(p_input) AS ary
) AS extracted;

$function$
;
17
andrew.carpenter

Cette question a été posée sur les listes de diffusion PostgreSQL et j'ai trouvé cette façon hackeuse de convertir du texte JSON en type de texte PostgreSQL via l'opérateur d'extraction de champ JSON:

CREATE FUNCTION json_text(json) RETURNS text IMMUTABLE LANGUAGE sql
AS $$ SELECT ('['||$1||']')::json->>0 $$;

db=# select json_text(json_array_elements('["hello",1.3,"\u2603"]'));
 json_text 
-----------
 hello
 1.3
 ☃

Fondamentalement, il convertit la valeur en un tableau à un seul élément, puis demande le premier élément.

Une autre approche serait d'utiliser cet opérateur pour extraire tous les champs un par un. Mais pour les grands tableaux, cela est probablement plus lent, car il doit analyser toute la chaîne JSON pour chaque élément du tableau, ce qui entraîne une complexité O (n ^ 2).

CREATE FUNCTION json_array_elements_text(json) RETURNS SETOF text IMMUTABLE LANGUAGE sql
AS $$ SELECT $1->>i FROM generate_series(0, json_array_length($1)-1) AS i $$;

db=# select json_array_elements_text('["hello",1.3,"\u2603"]');
 json_array_elements_text 
--------------------------
 hello
 1.3
 ☃
8
intgr

J'ai testé quelques options. Voici ma requête préférée. Supposons que nous ayons une table contenant le champ id et json. Le champ json contient un tableau que nous voulons transformer en tableau pg.

SELECT * 
FROM   test 
WHERE  TRANSLATE(jsonb::jsonb::text, '[]','{}')::INT[] 
       && ARRAY[1,2,3];

Il fonctionne partout et plus rapidement que les autres, mais il a l'air béquille)

Tout d'abord, le tableau json est converti en texte, puis nous changeons simplement les crochets en parenthèses. Enfin, le texte est converti en tableau de type requis.

SELECT TRANSLATE('[1]'::jsonb::text, '[]','{}')::INT[];

et si vous préférez les tableaux de texte []

SELECT TRANSLATE('[1]'::jsonb::text, '[]','{}')::TEXT[];
2
FiscalCliff

Ces quelques fonctions, tirées des réponses à cette question , sont ce que j'utilise et elles fonctionnent très bien

CREATE OR REPLACE FUNCTION json_array_casttext(json) RETURNS text[] AS $f$
    SELECT array_agg(x) || ARRAY[]::text[] FROM json_array_elements_text($1) t(x);
$f$ LANGUAGE sql IMMUTABLE;

CREATE OR REPLACE FUNCTION jsonb_array_casttext(jsonb) RETURNS text[] AS $f$
    SELECT array_agg(x) || ARRAY[]::text[] FROM jsonb_array_elements_text($1) t(x);
$f$ LANGUAGE sql IMMUTABLE;

CREATE OR REPLACE FUNCTION json_array_castint(json) RETURNS int[] AS $f$
    SELECT array_agg(x)::int[] || ARRAY[]::int[] FROM json_array_elements_text($1) t(x);
$f$ LANGUAGE sql IMMUTABLE;

CREATE OR REPLACE FUNCTION jsonb_array_castint(jsonb) RETURNS int[] AS $f$
    SELECT array_agg(x)::int[] || ARRAY[]::int[] FROM jsonb_array_elements_text($1) t(x);
$f$ LANGUAGE sql IMMUTABLE;

Dans chacun d'eux, en concaténant avec un tableau vide, ils gèrent un cas qui m'avait un peu secoué le cerveau, en ce sens que si vous essayez de caster un tableau vide à partir de json/jsonb sans cela, vous ne recevrez rien en retour, au lieu d'un tableau vide ({}) comme vous vous en doutez. Je suis certain qu'il y a une optimisation pour eux, mais ils sont laissés tels quels pour simplifier l'explication du concept.

0
Joel B