PostgreSQL unnest () con número de elemento

89

Cuando tengo una columna con valores separados, puedo usar la unnest()función:

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
...

¿Cómo puedo incluir números de elementos? Es decir:

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

Quiero la posición original de cada elemento en la cadena de origen. Lo he intentado con funciones de ventana ( row_number(), rank()etc.) pero siempre obtengo 1. ¿Quizás porque están en la misma fila de la tabla de origen?

Sé que es un mal diseño de mesa. No es mío, solo intento arreglarlo.

BartekR
fuente

Respuestas:

183

Postgres 9.4 o posterior

Úselo WITH ORDINALITYpara funciones de devolución de conjuntos:

Cuando una función en la FROMcláusula tiene el sufijo WITH ORDINALITY, bigintse agrega una columna a la salida que comienza desde 1 y se incrementa en 1 para cada fila de la salida de la función. Esto es más útil en el caso de establecer funciones de devolución como unnest().

En combinación con la LATERALfunción en la página 9.3+ , y de acuerdo con este hilo en pgsql-hackers , la consulta anterior ahora se puede escribir como:

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 TRUEconserva todas las filas de la tabla de la izquierda, incluso si la expresión de la tabla de la derecha no devuelve filas. Si eso no le preocupa, puede usar esta forma equivalente y menos detallada con un implícito CROSS JOIN LATERAL:

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

O más simple si se basa en una matriz real ( arrsiendo una columna de matriz):

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

O incluso, con una sintaxis mínima:

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

aes automáticamente alias de tabla y columna. El nombre predeterminado de la columna de ordinalidad agregada es ordinality. Pero es mejor (más seguro, más limpio) agregar alias de columna explícitos y columnas de calificación de tabla.

Postgres 8.4 - 9.3

Con row_number() OVER (PARTITION BY id ORDER BY elem)obtiene números según el orden de clasificación, no el número ordinal de la posición ordinal original en la cadena.

Simplemente puede omitir ORDER BY:

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

Si bien esto normalmente funciona y nunca lo he visto fallar en consultas simples, PostgreSQL no afirma nada con respecto al orden de las filas sin ORDER BY. Sucede que funciona debido a un detalle de implementación.

Para garantizar números ordinales de elementos en la cadena separada por espacios en blanco :

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;

O más simple si se basa en una matriz real :

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

Respuesta relacionada en dba.SE:

Postgres 8.1 - 8.4

Ninguna de estas características están disponibles, sin embargo: RETURNS TABLE, generate_subscripts(), unnest(), array_length(). Pero esto funciona:

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';

Tenga en cuenta, en particular, que el índice de la matriz puede diferir de las posiciones ordinales de los elementos. Considere esta demostración con una función extendida :

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

Comparar:

Erwin Brandstetter
fuente
10
Esta respuesta es una de las respuestas más completas en SO, con respecto a PostgreSQL. Gracias Erwin.
Alexandros
¿Podemos adaptar la función unnest2 a continuación a un retorno de tabla real (no filas falsas), en las nuevas versiones de pg?
Peter Krauss
@ erwin-brandstetter, ¿podría explicar por qué / si WITH ORDINALITYes preferible generate_subscripts()? Me parece que generate_subscripts()es mejor, ya que muestra la ubicación real del elemento en la matriz. Esto es útil, por ejemplo, al actualizar la matriz ... ¿debería usar en su WITH ORDINALITYlugar?
losthorse
1
@losthorse: Lo describiría así: WITH ORDINALITYes la solución general para obtener números de fila para cualquier función de retorno establecida en una consulta SQL. Es la forma más rápida y confiable y también funciona perfectamente para arreglos de 1 dimensión, basados ​​en 1 (el valor predeterminado para arreglos de Postgres, considere esto ). Si trabaja con cualquier otro tipo de matrices (la mayoría de la gente no lo hace), y realmente necesita preservar / trabajar con los subíndices originales, entonces este generate_subscripts()es el camino a seguir. Pero unnest()aplana todo para empezar ...
Erwin Brandstetter
1
@ z0r_ El manual: 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

Tratar:

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

fuente
6

Utilice funciones generadoras de subíndices .
http://www.postgresql.org/docs/current/static/functions-srf.html#FUNCTIONS-SRF-SUBSCRIPTS

Por ejemplo:

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
;

Más simple:

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
;
YujiSoftware
fuente
3

Si el orden de los elementos no es importante, puede

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
fuente
0

unnest2() como ejercicio

Las versiones anteriores antes de pg v8.4 necesitan un archivo unnest(). Podemos adaptar esta vieja función para devolver elementos con un índice:

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
fuente
2
Esto no funcionaría antes de la página v8.4, porque todavía no lo hay RETURNS TABLE. Agregué un capítulo a mi respuesta sobre una solución.
Erwin Brandstetter
1
@ErwinBrandstetter, tus respuestas son muy didácticas y estás puliendo un texto de hace 4 años (!) ... ¿Estás escribiendo un libro de PostgreSQL usando tus textos SO? :-)
Peter Krauss
Hola a todos, es una Wiki, podéis editar (!) ... Pero ok, corregí a setof record.
Peter Krauss