¿Cómo genero una CROSS JOIN pivotada donde la definición de tabla resultante es desconocida?


17

Dadas dos tablas con un recuento de filas indefinido con un nombre y un valor, ¿cómo mostraría CROSS JOINuna función pivotada sobre sus valores?

CREATE TEMP TABLE foo AS
SELECT x::text AS name, x::int
FROM generate_series(1,10) AS t(x);

CREATE TEMP TABLE bar AS
SELECT x::text AS name, x::int
FROM generate_series(1,5) AS t(x);

Por ejemplo, si esa función fuera la multiplicación, ¿cómo generaría una tabla (de multiplicación) como la siguiente?

Tabla de multiplicación común de 1..12

Todas esas (arg1,arg2,result)filas se pueden generar con

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x AS result
FROM foo
CROSS JOIN bar; 

Entonces, esto es solo una cuestión de presentación, me gustaría que esto también funcione con un nombre personalizado , un nombre que no es simplemente el argumento CASTeditado en texto, sino que se establece en la tabla,

CREATE TEMP TABLE foo AS
SELECT chr(x+64) AS name, x::int
FROM generate_series(1,10) AS t(x);

CREATE TEMP TABLE bar AS
SELECT chr(x+72) AS name, x::int
FROM generate_series(1,5) AS t(x);

Creo que esto sería fácilmente posible con un CROSSTAB capaz de un tipo de retorno dinámico.

SELECT * FROM crosstab(
  '
    SELECT foo.x AS arg1, bar.x AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  ', 'SELECT DISTINCT name FROM bar'
) AS **MAGIC**

Pero, sin el **MAGIC**, me sale

ERROR:  a column definition list is required for functions returning "record"
LINE 1: SELECT * FROM crosstab(

A modo de referencia, utilizando los ejemplos anteriores, con nombres que esto es algo más parecido a lo que tablefunc's crosstab()necesidades.

SELECT * FROM crosstab(
  '
    SELECT foo.x AS arg1, bar.x AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  '
) AS t(row int, i int, j int, k int, l int, m int);

Pero, ahora volvemos a hacer suposiciones sobre el contenido y el tamaño de la bartabla en nuestro ejemplo. Así que si,

  1. Las tablas son de longitud indefinida,
  2. Entonces la unión cruzada representa un cubo de dimensión indefinida (debido a lo anterior),
  3. Los nombres de categoría (lenguaje de tabla cruzada) están en la tabla

¿Qué es lo mejor que podemos hacer en PostgreSQL sin una "lista de definición de columnas" para generar ese tipo de presentación?


1
¿Serían los resultados JSON un buen enfoque? ¿Sería un ARRAY un buen enfoque? De esta manera, la definición de la "tabla de salida" ya sería conocida (y fijada). Pones la flexibilidad dentro de JSON o ARRAY. Supongo que dependerá mucho de las herramientas utilizadas después para procesar la información.
joanolo

Preferiría que fuera igual que lo anterior, si es posible.
Evan Carroll

Respuestas:


11

Caso simple, SQL estático

La solución no dinámica con crosstab()para el caso simple:

SELECT * FROM crosstab(
  'SELECT b.x, f.name, f.x * b.x AS prod
   FROM   foo f, bar b
   ORDER  BY 1, 2'
   ) AS ct (x int, "A" int, "B" int, "C" int, "D" int, "E" int
                 , "F" int, "G" int, "H" int, "I" int, "J" int);

Ordeno las columnas resultantes por foo.name, no foo.x. Ambos están ordenados en paralelo, pero esa es solo la configuración simple. Elija el orden de clasificación adecuado para su caso. El valor real de la segunda columna es irrelevante en esta consulta (forma de 1 parámetro crosstab()).

Ni siquiera necesitamos crosstab() 2 parámetros porque, por definición, no faltan valores. Ver:

(Arreglaste la consulta de tabla cruzada en la pregunta reemplazándola fooporbar una edición posterior. Esto también corrige la consulta, pero sigue trabajando con nombres de foo).

Tipo de retorno desconocido, SQL dinámico

Los nombres y tipos de columna no pueden ser dinámicos. SQL exige saber el número, los nombres y los tipos de columnas resultantes en el momento de la llamada. Ya sea por declaración explícita o por información en los catálogos del sistema (Eso es lo que sucede con SELECT * FROM tbl: Postgres busca la definición de tabla registrada).

Desea que Postgres derive las columnas resultantes de los datos en una tabla de usuario. No va a pasar.

De una forma u otra, necesita dos viajes de ida y vuelta al servidor. O creas un cursor y luego lo recorres. O puede crear una tabla temporal y luego seleccionarla. O registra un tipo y lo usa en la llamada.

O simplemente genera la consulta en un paso y la ejecuta en el siguiente:

SELECT $$SELECT * FROM crosstab(
  'SELECT b.x, f.name, f.x * b.x AS prod
   FROM   foo f, bar b
   ORDER  BY 1, 2'
   ) AS ct (x int, $$
 || string_agg(quote_ident(name), ' int, ' ORDER BY name) || ' int)'
FROM   foo;

Esto genera la consulta anterior, dinámicamente. Ejecútelo en el siguiente paso.

Estoy usando dollar-quotes ( $$) para simplificar el manejo de las cotizaciones anidadas. Ver:

quote_ident() es esencial para escapar de nombres de columna ilegales (y posiblemente defenderse contra la inyección de SQL).

Relacionado:


Noté que ejecutar la consulta que llamó "Tipo de retorno desconocido, SQL dinámico" en realidad solo devuelve una cadena que representa otra consulta, y luego dice "ejecutarla en el siguiente paso". ¿Significa esto que sería difícil, por ejemplo, crear una vista materializada a partir de esto?
Colin D

@ColinD: No es difícil, pero simplemente imposible. Puede crear un MV a partir del SQL generado con un tipo de retorno conocido. Pero no puede tener un MV con un tipo de retorno desconocido.
Erwin Brandstetter

10

¿Qué es lo mejor que podemos hacer en PostgreSQL sin una "lista de definición de columnas" para generar ese tipo de presentación?

Si enmarca esto como un problema de presentación, podría considerar una función de presentación posterior a la consulta.

psqlVienen versiones más nuevas de (9.6) \crosstabview, que muestran un resultado en la representación de tabla cruzada sin soporte de SQL (ya que SQL no puede producir esto directamente, como se menciona en la respuesta de @ Erwin: SQL exige saber el número, los nombres y los tipos de columnas resultantes en el momento de la llamada )

Por ejemplo, su primera consulta da:

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x AS result
FROM foo
CROSS JOIN bar
\crosstabview

 arg1 | 1  | 2  | 3  | 4  | 5  
------+----+----+----+----+----
 1    |  1 |  2 |  3 |  4 |  5
 2    |  2 |  4 |  6 |  8 | 10
 3    |  3 |  6 |  9 | 12 | 15
 4    |  4 |  8 | 12 | 16 | 20
 5    |  5 | 10 | 15 | 20 | 25
 6    |  6 | 12 | 18 | 24 | 30
 7    |  7 | 14 | 21 | 28 | 35
 8    |  8 | 16 | 24 | 32 | 40
 9    |  9 | 18 | 27 | 36 | 45
 10   | 10 | 20 | 30 | 40 | 50
(10 rows)

El segundo ejemplo con nombres de columna ASCII da:

SELECT foo.name AS arg1, bar.name AS arg2, foo.x*bar.x
    FROM foo
    CROSS JOIN bar
  \crosstabview

 arg1 | I  | J  | K  | L  | M  
------+----+----+----+----+----
 A    |  1 |  2 |  3 |  4 |  5
 B    |  2 |  4 |  6 |  8 | 10
 C    |  3 |  6 |  9 | 12 | 15
 D    |  4 |  8 | 12 | 16 | 20
 E    |  5 | 10 | 15 | 20 | 25
 F    |  6 | 12 | 18 | 24 | 30
 G    |  7 | 14 | 21 | 28 | 35
 H    |  8 | 16 | 24 | 32 | 40
 I    |  9 | 18 | 27 | 36 | 45
 J    | 10 | 20 | 30 | 40 | 50
(10 rows)

Consulte el manual de psql y https://wiki.postgresql.org/wiki/Crosstabview para obtener más información.


1
Esto es realmente genial.
Evan Carroll

1
La solución más elegante.
Erwin Brandstetter

1

Esta no es una solución definitiva

Este es mi mejor enfoque hasta ahora. Todavía necesita convertir la matriz final en columnas.

Primero tengo el producto cartesiano de ambas tablas:

select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
       ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
 from bar
     cross join foo
 order by bar.name, foo.name

Pero, agregué un número de fila solo para identificar cada fila de la primera tabla.

((row_number() over ()) - 1) / (select count(*)::integer from foo)

Luego he compilado el resultado en este formato:

[Row name] [Array of values]


select col_name, values
from
(
select '' as col_name, array_agg(name) as values from foo
UNION
select fy.name as col_name,
    (select array_agg(t.val) as values
    from  
        (select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
              ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
        from bar
           cross join foo
        order by bar.name, foo.name) t
    where t.row = fy.row)
from
    (select name, (row_number() over(order by name)) - 1 as row from bar) fy
) a
order by col_name;

+---+---------------------+
|   |      ABCDEFGHIJ     |
+---+---------------------+
| I |     12345678910     |
+---+---------------------+
| J |   2468101214161820  |
+---+---------------------+
| K |  36912151821242730  |
+---+---------------------+
| L |  481216202428323640 |
+---+---------------------+
| M | 5101520253035404550 |
+---+---------------------+ 

Convirtiéndolo en una cadena delimitada por comas:

select col_name, values
from
(
select '' as col_name, array_to_string(array_agg(name),',') as values from foo
UNION
select fy.name as col_name,
    (select array_to_string(array_agg(t.val),',') as values
    from  
        (select foo.name xname, bar.name yname, (foo.x * bar.x)::text as val,
              ((row_number() over ()) - 1) / (select count(*)::integer from foo) as row
        from bar
           cross join foo
        order by bar.name, foo.name) t
    where t.row = fy.row)
from
    (select name, (row_number() over(order by name)) - 1 as row from bar) fy
) a
order by col_name;


+---+------------------------------+
|   | A,B,C,D,E,F,G,H,I,J          |
+---+------------------------------+
| I | 1,2,3,4,5,6,7,8,9,10         |
+---+------------------------------+
| J | 2,4,6,8,10,12,14,16,18,20    |
+---+------------------------------+
| K | 3,6,9,12,15,18,21,24,27,30   |
+---+------------------------------+
| L | 4,8,12,16,20,24,28,32,36,40  |
+---+------------------------------+
| M | 5,10,15,20,25,30,35,40,45,50 |
+---+------------------------------+

(Solo para probarlo más tarde: http://rextester.com/NBCYXA2183 )


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.