Como gerar um CROSS JOIN dinâmico, onde a definição da tabela resultante é desconhecida?

17

Dadas duas tabelas com uma contagem de linhas indefinidas com um nome e um valor, como eu exibiria CROSS JOINuma função dinâmica sobre seus 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 exemplo, se essa função fosse multiplicação, como eu geraria uma tabela (multiplicação) como a abaixo,

Tabela de multiplicação comum de 1..12

Todas essas (arg1,arg2,result)linhas podem ser geradas com

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

Portanto, isso é apenas uma questão de apresentação. Gostaria que ele também trabalhasse com um nome personalizado - um nome que não é apenas o argumento CASTeditado no texto, mas definido na tabela,

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

Eu acho que isso seria facilmente possível com um CROSSTAB capaz de um 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**

Mas, sem o **MAGIC**, eu recebo

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

Para referência, usando os exemplos acima com nomes isso é algo mais parecido com o que tablefunc's crosstab()necessidades.

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

Mas agora voltamos a fazer suposições sobre o conteúdo e o tamanho da bartabela em nosso exemplo. Então se,

  1. As tabelas são de comprimento indefinido,
  2. Em seguida, a junção cruzada representa um cubo de dimensão indefinida (por causa do acima),
  3. Os nomes de categorias (linguagem de tabulação cruzada) estão na tabela

Qual é o melhor que podemos fazer no PostgreSQL sem uma "lista de definições de colunas" para gerar esse tipo de apresentação?

Evan Carroll
fonte
11
Os resultados do JSON seriam uma boa abordagem? Uma ARRAY seria uma boa caipira? Dessa forma, a definição da "tabela de saída" já seria conhecida (e corrigida). Você coloca a flexibilidade dentro do JSON ou do ARRAY. Eu acho que vai depender de muitas das ferramentas usadas depois para processar as informações.
Joanolo 27/12/16
Eu preferiria que fosse exatamente como o descrito acima, se possível.
Evan Carroll

Respostas:

11

Caso simples, SQL estático

A solução não dinâmicacrosstab() para o caso simples:

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

Eu ordeno as colunas resultantes por foo.name, não foo.x. Os dois são classificados em paralelo, mas essa é apenas a configuração simples. Escolha a ordem de classificação correta para o seu caso. O valor real da segunda coluna é irrelevante nesta consulta (forma de 1 parâmetro de crosstab()).

Nem precisamos de crosstab()dois parâmetros porque não há valores ausentes por definição. Vejo:

(Você corrigiu a consulta de tabela de referência cruzada na pergunta substituindo foopor barem uma edição posterior. Isso também corrige a consulta, mas continua trabalhando com nomes de foo.)

Tipo de retorno desconhecido, SQL dinâmico

Os nomes e tipos de colunas não podem ser dinâmicos. O SQL exige saber número, nomes e tipos de colunas resultantes no momento da chamada. Por declaração explícita ou por informações nos catálogos do sistema (é o que acontece com SELECT * FROM tbl: O Postgres consulta a definição da tabela registrada.)

Você deseja que o Postgres obtenha colunas resultantes dos dados em uma tabela de usuário. Não vai acontecer.

De uma forma ou de outra, você precisa de duas viagens de ida e volta ao servidor. Ou você cria um cursor e depois o percorre. Ou você cria uma tabela temporária e seleciona nela. Ou você registra um tipo e o usa na chamada.

Ou você simplesmente gera a consulta em uma etapa e a executa na próxima:

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;

Isso gera a consulta acima dinamicamente. Execute-o na próxima etapa.

Estou usando dollar-quote ( $$) para manter o manuseio de aspas aninhadas simples. Vejo:

quote_ident() é essencial para evitar nomes de colunas ilegais (e possivelmente defender-se da injeção de SQL).

Palavras-chave:

Erwin Brandstetter
fonte
Percebi que a execução da consulta que você chamou de "Tipo de retorno desconhecido, SQL dinâmico" na verdade apenas retorna uma string que representa outra consulta e, em seguida, você diz "execute-a na próxima etapa". Isso significa que seria difícil, por exemplo, criar uma visão materializada disso?
Colin D
@ CololinD: Não é difícil, mas é impossível. Você pode criar uma MV a partir do SQL gerado com o tipo de retorno conhecido. Mas você não pode ter uma MV com tipo de retorno desconhecido.
Erwin Brandstetter
10

Qual é o melhor que podemos fazer no PostgreSQL sem uma "lista de definições de colunas" para gerar esse tipo de apresentação?

Se você enquadrar isso como um problema de apresentação, considere um recurso de apresentação pós-consulta.

As versões mais recentes do psql(9.6) são fornecidas \crosstabview, mostrando um resultado na representação de tabela de referência cruzada sem suporte ao SQL (já que o SQL não pode produzi-lo diretamente, como mencionado na resposta de @ Erwin: SQL exige saber número, nomes e tipos de colunas resultantes no momento da chamada )

Por exemplo, sua primeira consulta fornece:

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)

O segundo exemplo com nomes de coluna ASCII fornece:

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)

Veja o manual do psql e https://wiki.postgresql.org/wiki/Crosstabview para mais.

Daniel Vérité
fonte
11
Isso é muito legal.
Evan Carroll
11
A solução mais elegante.
Erwin Brandstetter
1

Esta não é uma solução definitiva

Esta é a minha melhor abordagem até agora. Ainda é necessário converter a matriz final em colunas.

Primeiro, eu tenho o produto cartesiano de ambas as tabelas:

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

Mas adicionei um número de linha apenas para identificar todas as linhas da primeira tabela.

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

Então eu comprei o resultado neste 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 |
+---+---------------------+ 

Convertendo-o em string 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 |
+---+------------------------------+

(Apenas para tentar mais tarde: http://rextester.com/NBCYXA2183 )

McNets
fonte