PostgreSQL unnest () avec numéro d'élément

89

Lorsque j'ai une colonne avec des valeurs séparées, je peux utiliser la unnest()fonction:

myTable
id | elements
---+------------
1  |ab,cd,efg,hi
2  |jk,lm,no,pq
3  |rstuv,wxyz

select id, unnest(string_to_array(elements, ',')) AS elem
from myTable

id | elem
---+-----
1  | ab
1  | cd
1  | efg
1  | hi
2  | jk
...

Comment puis-je inclure des numéros d'élément? C'est à dire:

id | elem | nr
---+------+---
1  | ab   | 1
1  | cd   | 2
1  | efg  | 3
1  | hi   | 4
2  | jk   | 1
...

Je veux la position d'origine de chaque élément dans la chaîne source. J'ai essayé avec les fonctions de fenêtre ( row_number(), rank()etc.) mais je reçois toujours 1. Peut-être parce qu'ils sont dans la même ligne de la table source?

Je sais que c'est une mauvaise conception de table. Ce n'est pas à moi, j'essaye juste de le réparer.

BartekR
la source

Réponses:

183

Postgres 9.4 ou version ultérieure

Utilisation WITH ORDINALITYpour les fonctions de retour de set:

Lorsqu'une fonction de la FROMclause est suffixée par WITH ORDINALITY, une bigintcolonne est ajoutée à la sortie qui commence à 1 et s'incrémente de 1 pour chaque ligne de la sortie de la fonction. Ceci est très utile dans le cas de set renvoyant des fonctions telles que unnest().

En combinaison avec la LATERALfonctionnalité de pg 9.3+ , et selon ce fil sur pgsql-hackers , la requête ci-dessus peut maintenant être écrite comme:

SELECT t.id, a.elem, a.nr
FROM   tbl AS t
LEFT   JOIN LATERAL unnest(string_to_array(t.elements, ','))
                    WITH ORDINALITY AS a(elem, nr) ON TRUE;

LEFT JOIN ... ON TRUEconserve toutes les lignes de la table de gauche, même si l'expression de table de droite ne renvoie aucune ligne. Si cela ne vous concerne pas, vous pouvez utiliser cette forme autrement équivalente, moins verbeuse avec un implicite CROSS JOIN LATERAL:

SELECT t.id, a.elem, a.nr
FROM   tbl t, unnest(string_to_array(t.elements, ',')) WITH ORDINALITY a(elem, nr);

Ou plus simple si basé sur un tableau réel ( arrétant une colonne de tableau):

SELECT t.id, a.elem, a.nr
FROM   tbl t, unnest(t.arr) WITH ORDINALITY a(elem, nr);

Ou même, avec une syntaxe minimale:

SELECT id, a, ordinality
FROM   tbl, unnest(arr) WITH ORDINALITY a;

aest automatiquement l' alias de table et de colonne. Le nom par défaut de la colonne d'ordinalité ajoutée est ordinality. Mais il est préférable (plus sûr, plus propre) d'ajouter des alias de colonne explicites et des colonnes de qualification de table.

Postgres 8.4 - 9.3

Avec, row_number() OVER (PARTITION BY id ORDER BY elem)vous obtenez des nombres en fonction de l'ordre de tri, pas du nombre ordinal de la position ordinale d'origine dans la chaîne.

Vous pouvez simplement omettre ORDER BY:

SELECT *, row_number() OVER (PARTITION by id) AS nr
FROM  (SELECT id, regexp_split_to_table(elements, ',') AS elem FROM tbl) t;

Bien que cela fonctionne normalement et que je ne l'ai jamais vu échouer dans des requêtes simples, PostgreSQL n'affirme rien concernant l'ordre des lignes sans ORDER BY. Cela arrive à fonctionner en raison d'un détail de mise en œuvre.

Pour garantir le nombre ordinal d'éléments dans la chaîne séparée par des blancs :

SELECT id, arr[nr] AS elem, nr
FROM  (
   SELECT *, generate_subscripts(arr, 1) AS nr
   FROM  (SELECT id, string_to_array(elements, ' ') AS arr FROM tbl) t
   ) sub;

Ou plus simple si basé sur un tableau réel :

SELECT id, arr[nr] AS elem, nr
FROM  (SELECT *, generate_subscripts(arr, 1) AS nr FROM tbl) t;

Réponse connexe sur dba.SE:

Postgres 8.1 - 8.4

Aucune de ces caractéristiques sont disponibles, mais: RETURNS TABLE, generate_subscripts(), unnest(), array_length(). Mais cela fonctionne:

CREATE FUNCTION f_unnest_ord(anyarray, OUT val anyelement, OUT ordinality integer)
  RETURNS SETOF record
  LANGUAGE sql IMMUTABLE AS
'SELECT $1[i], i - array_lower($1,1) + 1
 FROM   generate_series(array_lower($1,1), array_upper($1,1)) i';

Notez en particulier que l'index du tableau peut différer des positions ordinales des éléments. Considérez cette démo avec une fonction étendue :

CREATE FUNCTION f_unnest_ord_idx(anyarray, OUT val anyelement, OUT ordinality int, OUT idx int)
  RETURNS SETOF record
  LANGUAGE sql IMMUTABLE AS
'SELECT $1[i], i - array_lower($1,1) + 1, i
 FROM   generate_series(array_lower($1,1), array_upper($1,1)) i';

SELECT id, arr, (rec).*
FROM  (
   SELECT *, f_unnest_ord_idx(arr) AS rec
   FROM  (VALUES (1, '{a,b,c}'::text[])  --  short for: '[1:3]={a,b,c}'
               , (2, '[5:7]={a,b,c}')
               , (3, '[-9:-7]={a,b,c}')
      ) t(id, arr)
   ) sub;

 id |       arr       | val | ordinality | idx
----+-----------------+-----+------------+-----
  1 | {a,b,c}         | a   |          1 |   1
  1 | {a,b,c}         | b   |          2 |   2
  1 | {a,b,c}         | c   |          3 |   3
  2 | [5:7]={a,b,c}   | a   |          1 |   5
  2 | [5:7]={a,b,c}   | b   |          2 |   6
  2 | [5:7]={a,b,c}   | c   |          3 |   7
  3 | [-9:-7]={a,b,c} | a   |          1 |  -9
  3 | [-9:-7]={a,b,c} | b   |          2 |  -8
  3 | [-9:-7]={a,b,c} | c   |          3 |  -7

Comparer:

Erwin Brandstetter
la source
10
Cette réponse est l'une des réponses les plus complètes en SO, concernant PostgreSQL. Merci Erwin.
Alexandros
Peut-on adapter la fonction unnest2 ci-dessous à un vrai retour de table (pas de fausses lignes), dans les nouvelles versions de pg?
Peter Krauss
@ erwin-brandstetter, pourriez-vous expliquer pourquoi / si WITH ORDINALITYest préféré generate_subscripts()? Il me semble que generate_subscripts()c'est mieux car il montre l'emplacement réel de l'élément dans le tableau. Ceci est utile, par exemple, lors de la mise à jour du tableau ... devrais-je utiliser à la WITH ORDINALITYplace?
losthorse
1
@losthorse: Je voudrais le décrire comme ceci: WITH ORDINALITYest la solution générale pour obtenir les numéros de ligne pour toute fonction de retour d'ensemble dans une requête SQL. C'est le moyen le plus rapide et le plus fiable et il fonctionne également parfaitement pour les tableaux 1 dimenstional, 1-based (par défaut pour les tableaux Postgres, considérez ceci ). Si vous travaillez avec un autre type de tableaux (la plupart des gens ne le font pas) et que vous devez en fait conserver / travailler avec les indices d'origine, alors generate_subscripts()c'est la voie à suivre. Mais unnest()aplatit tout pour commencer ...
Erwin Brandstetter
1
@ z0r_ Le manuel: Table functions appearing in FROM can also be preceded by the key word LATERAL, but for functions the key word is optional; the function's arguments can contain references to columns provided by preceding FROM items in any case.
Erwin Brandstetter
9

Essayer:

select v.*, row_number() over (partition by id order by elem) rn from
(select
    id,
    unnest(string_to_array(elements, ',')) AS elem
 from myTable) v

la source
6

Utilisez les fonctions de génération d'indices .
http://www.postgresql.org/docs/current/static/functions-srf.html#FUNCTIONS-SRF-SUBSCRIPTS

Par exemple:

SELECT 
  id
  , elements[i] AS elem
  , i AS nr
FROM
  ( SELECT 
      id
      , elements
      , generate_subscripts(elements, 1) AS i
    FROM
      ( SELECT
          id
          , string_to_array(elements, ',') AS elements
        FROM
          myTable
      ) AS foo
  ) bar
;

Plus simplement:

SELECT
  id
  , unnest(elements) AS elem
  , generate_subscripts(elements, 1) AS nr
FROM
  ( SELECT
      id
      , string_to_array(elements, ',') AS elements
    FROM
      myTable
  ) AS foo
;
Logiciel Yuji
la source
3

Si l'ordre des éléments n'est pas important, vous pouvez

select 
  id, elem, row_number() over (partition by id) as nr
from (
  select
      id,
      unnest(string_to_array(elements, ',')) AS elem
  from myTable
) a
Florin Ghita
la source
0

unnest2() comme exercice

Les anciennes versions antérieures à pg v8.4 nécessitent un fichier unnest(). Nous pouvons adapter cette ancienne fonction pour renvoyer des éléments avec un index:

CREATE FUNCTION unnest2(anyarray)
  RETURNS setof record  AS
$BODY$
  SELECT $1[i], i
  FROM   generate_series(array_lower($1,1),
                         array_upper($1,1)) i;
$BODY$ LANGUAGE sql IMMUTABLE;
Peter Krauss
la source
2
Cela ne fonctionnerait pas avant pg v8.4, car il n'y en a pas RETURNS TABLEencore. J'ai ajouté un chapitre à ma réponse sur une solution.
Erwin Brandstetter
1
@ErwinBrandstetter, vos réponses sont très didactiques, et vous peaufinez un texte d'il y a 4 ans (!) ... Vous écrivez un livre PostgreSQL en utilisant vos textes SO? :-)
Peter Krauss
Salut à tous, c'est un Wiki, vous pouvez éditer (!) ... Mais ok, j'ai corrigé setof record.
Peter Krauss