Comment transformer un tableau json en tableau postgres?


69

J'ai une colonne dataqui contient un jsondocument à peu près comme ceci:

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

Je voudrais transformer le tagstableau imbriqué en une chaîne concaténée ( foo, bar). Ce serait facilement possible avec la array_to_string()fonction en théorie. Cependant, cette fonction n'agit pas sur les jsontableaux. Je me demande donc comment transformer ce jsontableau en Postgres array?


Est- json_extract_path_text(your_column, 'tags') ce que vous cherchez?
a_horse_with_no_name

1
@a_horse_with_no_name: le problème restant: les éléments de tableau sont toujours cités pour le format JSON. Le texte n'est pas correctement extrait ...
Erwin Brandstetter

Réponses:


94

Postgres 9.4 ou plus récent

Visiblement inspiré par ce billet , Postgres 9.4 a ajouté la (les) fonction (s) manquante (s):
Merci à Laurence Rowe pour le correctif et Andrew Dunstan pour son engagement!

Pour annuler le tableau JSON. Ensuite, utilisez array_agg()ou un constructeur ARRAY pour construire un tableau Postgres à partir de celui-ci. Ou string_agg()pour construire une text chaîne .

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

Remplacez "json" par "jsonb" jsonbdans 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;

Apparenté, relié, connexe:

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

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

Apparenté, relié, connexe:

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

Fonction wrapper

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 AS
'SELECT ARRAY(SELECT json_array_elements_text(_js))';

Faites-en une fonction SQL , afin qu'il puisse être intégré dans des requêtes plus volumineuses.
Faites-le IMMUTABLE(parce que c'est le cas) pour éviter les évaluations répétées dans les requêtes plus volumineuses et autorisez-le dans les expressions d'index.

Appel:

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

db <> fiddle ici


Postgres 9.3 ou plus ancien

Utilisez la fonction json_array_elements(). Mais nous en tirons une double chaîne .

Requête alternative avec agrégation dans la requête externe. CROSS JOINsupprime les lignes avec des tableaux manquants ou vides. Peut aussi être utile pour traiter 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;

Le constructeur ARRAY, toujours avec les chaînes citées:

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

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

Le pauvre homme sans scrupule 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érer 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;

ARRAY constructeur:

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

Fiddle SQL original (obsolète) .
db <> fiddle ici.

Apparenté, relié, connexe:

Notes (obsolète depuis la page 9.4)

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


Bon post comme toujours, mais avec votre connaissance des internes, pourquoi le casting de array-> jsonb n’est-il pas là? Je peux comprendre de ne pas implémenter l'autre distribution parce que le tableau SQL est plus fortement typé. Est-ce simplement parce que PostgreSQL est opposé à la génération automatique de code à transtyper (int [], bigint [], text []), etc.
Evan Carroll

3
@ Evan: vous l'utiliseriez to_jsonb()pour la conversion tableau-> jsonb.
Erwin Brandstetter le

Est-ce que SELECT ARRAY(SELECT json_array_elements_text(_js))cela garantit vraiment que l'ordre du tableau est préservé? L'optimiseur n'est-il pas autorisé à modifier théoriquement l'ordre des lignes sortant de json_array_elements_text?
Felix Geisendörfer

@Felix: il n'y a pas de garantie formelle dans le standard SQL. (Là encore, les fonctions de retour définies ne sont même pas autorisées dans la liste SELECT en SQL standard.) mais il existe une assertion informelle dans le manuel Postgres. Voir: dba.stackexchange.com/a/185862/3684 Pour être explicite - au prix d'une pénalité de performance mineure - voir: dba.stackexchange.com/a/27287/3684 . Personnellement, je suis sûr à 100% que cette expression particulière fonctionne comme prévu dans toutes les versions présentes et futures de Postgres depuis la version 9.4.
Erwin Brandstetter

@ ErwinBrandstetter merci beaucoup pour cette confirmation! Je suis actuellement en train de chercher un article qui résume les garanties formelles et informelles de commande fournies par PostgreSQL. Vos réponses ont été incroyablement utiles! Si vous souhaitez lire l'article, faites-le moi savoir, mais ne vous inquiétez pas si ce n'est pas le cas. Je suis extrêmement reconnaissant pour vos contributions à StackOverflow et j'ai beaucoup appris de vous au fil des ans!
Felix Geisendörfer

16

PG 9.4+

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

CREATE OR REPLACE FUNCTION jsonb_array_to_text_array(
  p_input jsonb
) RETURNS TEXT[] AS $BODY$

DECLARE v_output text[];

BEGIN

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

  RETURN v_output;

END;

$BODY$
LANGUAGE plpgsql VOLATILE;

Alors fais juste:

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

J'ai ajouté des expressions plus rapides à ma réponse et une fonction plus simple. Cela peut être considérablement moins cher.
Erwin Brandstetter

4
Cette fonction doit être du SQL pur pour que l'optimiseur puisse y jeter un coup d'œil. Pas besoin d'utiliser pgplsql ici.
Divisez

8

Cette question a été posée sur les listes de diffusion de PostgreSQL et j’ai imaginé cette méthode sophistiquée de conversion de 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 consiste à utiliser cet opérateur pour extraire tous les champs un par un. Mais pour les grands tableaux, cela est probablement plus lent, car il faut analyser la chaîne JSON entière pour chaque élément du tableau, ce qui entraîne une complexité de 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
 

1

J'ai testé quelques options. Voici ma requête préférée. Supposons que nous ayons une table contenant les champs 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 n'importe où et plus vite que les autres, mais a l'air craquant.)

Tout d'abord, json array est converti en texte, puis nous changeons les crochets en parenthèses. Enfin, le texte est converti en tableau du 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
SELECT TRANSLATE('{"name": "foo", "tags": ["foo", "bar"]}'::jsonb::text, '[]','{}')::INT[]; ERROR: malformed array literal: "{"name": "foo", "tags": {"foo", "bar"}}"Je pense que vous devez ajouter une explication sur la façon dont cela est censé fonctionner.
dezso

La question est de savoir comment transformer un tableau JSON (!) En un tableau pg. Supposons que j'ai la table contenant les colonnes id et jsonb. La colonne JSONb contient un tableau JSON. Alors
FiscalCliff

TRANSLATE (jsonb :: jsonb :: text, '[]', '{}') :: INT [] convertit le tableau json en tableau pg.
FiscalCliff

SELECT translate('["foo", "bar"]'::jsonb::text, '[]','{}')::INT[]; ERROR: invalid input syntax for integer: "foo"Ce n'est pas si à l'
abri des

Envisagez d'utiliser du texte [] pour ces tableaux
FiscalCliff

0

Ces quelques fonctions, tirées des réponses à cette question , sont ce que j'utilise et 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’a un peu troublé la tête, en ce sens que si vous essayez de lancer un tableau vide à partir de json/ jsonbsans, vous n’obtiendrez rien en retour. tableau vide ( {}) comme on peut s'y attendre. Je suis certain qu'il y a une optimisation pour eux, mais ils sont laissés en l'état pour simplifier l'explication du concept.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.