Как создать поворотное CROSS JOIN, где определение таблицы в результате неизвестно?

18

Учитывая две таблицы с неопределенным количеством строк с именем и значением, как бы я отобразил функцию поворота CROSS JOINнад их значениями.

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

Например, если бы эта функция была умножением, как бы я сгенерировал (умножение) таблицу, как показано ниже,

Общая таблица умножения 1..12

Все эти (arg1,arg2,result)строки могут быть сгенерированы с

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

Так что это только вопрос представления, я хотел бы, чтобы это также работало с произвольным именем - именем, которое не просто является аргументом CASTдля текста, но установлено в таблице,

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

Я думаю, что это было бы легко выполнимо с CROSSTAB, способным к динамическому типу возврата.

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

Но, без **MAGIC**, я получаю

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

Для справки, используя приведенные выше примеры с именами , это нечто большее , как то , что tablefunc«s crosstab()хочет.

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

Но теперь мы вернулись к предположениям о содержании и размере barтаблицы в нашем примере. Так что если,

  1. Таблицы имеют неопределенную длину,
  2. Тогда перекрестное соединение представляет куб неопределенного измерения (из-за выше),
  3. Названия катагорий (на языке кросс-таблицы) приведены в таблице.

Что мы можем сделать лучше всего в PostgreSQL без «списка определений столбцов» для создания такого рода презентации?

Эван Кэрролл
источник
1
Будут ли результаты JSON хорошим подходом? Будет ли ARRAY хорошим подспорьем? Таким образом, определение «выходной таблицы» будет уже известно (и исправлено). Вы вкладываете гибкость в JSON или ARRAY. Я предполагаю, что это будет зависеть от множества инструментов, используемых впоследствии для обработки информации.
Joanolo
Я бы предпочел, чтобы это было так же, как и выше, если это возможно.
Эван Кэрролл

Ответы:

12

Простой случай, статический SQL

Нединамическое решение с crosstab()для простого случая:

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

Я заказываю полученные столбцы foo.name, а не foo.x. И то и другое сортируется параллельно, но это простая установка. Выберите правильный порядок сортировки для вашего случая. Фактическое значение второго столбца не имеет значения в этом запросе (форма с 1 параметром crosstab()).

Нам даже не нужно crosstab()с 2 параметрами, потому что по определению отсутствуют пропущенные значения. Видеть:

(Вы исправили перекрестный запрос в вопросе, заменив fooс barв более поздней редакции. Это также фиксирует запрос, но продолжает работать с именами из foo.)

Неизвестный тип возвращаемого значения, динамический SQL

Имена и типы столбцов не могут быть динамическими. SQL требует знать число, имена и типы результирующих столбцов во время вызова. Либо с помощью явного объявления, либо из информации в системных каталогах (вот что происходит SELECT * FROM tbl: Postgres ищет определение зарегистрированной таблицы.)

Вы хотите, чтобы Postgres извлекал результирующие столбцы из данных в пользовательской таблице. Не произойдет.

Так или иначе, вам нужно две поездки туда и обратно на сервер. Либо вы создаете курсор, а затем проходите через него. Или вы создаете временную таблицу, а затем выбираете из нее. Или вы регистрируете тип и используете его в вызове.

Или вы просто генерируете запрос за один шаг и выполняете его в следующем:

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;

Это генерирует запрос выше, динамически. Выполните это на следующем шаге.

Я использую Dollar-quotes ( $$), чтобы упростить обработку вложенных кавычек. Видеть:

quote_ident() важно избежать в противном случае недопустимых имен столбцов (и, возможно, защитить от внедрения SQL).

Связанный:

Эрвин Брандштеттер
источник
Я заметил, что выполнение запроса, который вы назвали «Неизвестный тип возврата, динамический SQL», на самом деле просто возвращает строку, представляющую другой запрос, а затем вы говорите «выполнить его на следующем шаге». Означает ли это, что было бы трудно, например, создать материализованный взгляд на это?
Колин Д.
@ColinD: не сложно, но просто невозможно. Вы можете создать MV из сгенерированного SQL с известным типом возврата. Но вы не можете иметь MV с неизвестным типом возврата.
Эрвин Брандштеттер,
11

Что мы можем сделать лучше всего в PostgreSQL без «списка определений столбцов» для создания такого рода презентации?

Если вы представляете это как проблему презентации, вы можете рассмотреть возможность представления после запроса.

Более новые версии psql(9.6) идут с \crosstabviewпоказом результата в представлении кросс-таблицы без поддержки SQL (поскольку SQL не может произвести это напрямую, как упоминалось в ответе @ Эрвина: SQL требует знать число, имена и типы результирующих столбцов во время вызова )

Например, ваш первый запрос дает:

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)

Второй пример с именами столбцов ASCII дает:

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)

См. Руководство по psql и https://wiki.postgresql.org/wiki/Crosstabview для получения дополнительной информации.

Даниэль Верите
источник
1
Это действительно чертовски круто.
Эван Кэрролл
1
Самый элегантный обходной путь.
Эрвин Брандстеттер
1

Это не окончательное решение

Это мой лучший подход до сих пор. Еще нужно конвертировать окончательный массив в столбцы.

Сначала я получил декартово произведение обеих таблиц:

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

Но я добавил номер строки, чтобы идентифицировать каждую строку первой таблицы.

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

Тогда я получу результат в этом формате:

[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 |
+---+---------------------+ 

Преобразование его в строку, разделенную запятыми:

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

(Просто попробуйте позже: http://rextester.com/NBCYXA2183 )

McNets
источник