Динамическая альтернатива повороту с помощью CASE и GROUP BY

У меня есть таблица, которая выглядит так:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D

И я хочу, чтобы это выглядело так:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

У меня есть этот запрос, который делает это:

SELECT bar, 
   MAX(CASE WHEN abc."row" = 1 THEN feh ELSE NULL END) AS "val1",
   MAX(CASE WHEN abc."row" = 2 THEN feh ELSE NULL END) AS "val2",
   MAX(CASE WHEN abc."row" = 3 THEN feh ELSE NULL END) AS "val3"
FROM
(
  SELECT bar, feh, row_number() OVER (partition by bar) as row
  FROM "Foo"
 ) abc
GROUP BY bar

Это очень эффективный подход и становится громоздким, если есть много новых столбцов для создания. Мне было интересно, можно ли сделать инструкции CASE лучше сделать этот запрос более динамичным? Кроме того, мне бы хотелось увидеть другие подходы к этому.

Ответ 1

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

CREATE EXTENSION tablefunc;

Ответьте на вопрос

Очень базовое решение кросс-таблицы для вашего случая:

SELECT * FROM crosstab(
  'SELECT bar, 1 AS cat, feh
   FROM   tbl_org
   ORDER  BY bar, feh')
 AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

специальная трудность заключается в том, что в базовой таблице нет категории (cat). Для основной 1-параметрной формы мы можем просто предоставить фиктивный столбец с фиктивным значением, служащим в качестве категории. Значение все равно игнорируется.

Это один из <сильных > редких случаев, где второй параметр для функции crosstab() не нужен, потому что все NULL значения отображаются только в оборванных столбцах справа по определению этой проблемы. И порядок может быть определен значением.

Если бы у нас был столбец фактической категории с именами, определяющими порядок значений в результате, нам понадобилась 2-параметрическая форма crosstab(). Здесь я синтезирую столбец категории с помощью функции окна row_number(), чтобы основать crosstab() on:

SELECT * FROM crosstab(
   $$
   SELECT bar, val, feh
   FROM  (
      SELECT *, 'val' || row_number() OVER (PARTITION BY bar ORDER BY feh) AS val
      FROM tbl_org
      ) x
   ORDER BY 1, 2
   $$
 , $$VALUES ('val1'), ('val2'), ('val3')$$         -- more columns?
) AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?

Остальное в значительной степени зависит от мельницы. Найдите больше объяснений и ссылок в этих близких ответах.

Основы:
Прочитайте это, если вы не знакомы с функцией crosstab()!

Дополнительно:

Правильная настройка теста

Как вы должны предоставить тестовый пример для начала:

CREATE TEMP TABLE tbl_org (id int, feh int, bar text);
INSERT INTO tbl_org (id, feh, bar) VALUES
   (1, 10, 'A')
 , (2, 20, 'A')
 , (3,  3, 'B')
 , (4,  4, 'B')
 , (5,  5, 'C')
 , (6,  6, 'D')
 , (7,  7, 'D')
 , (8,  8, 'D');

Динамическая кросс-таблица?

Не очень динамично, но @Clodoaldo прокомментировал. PLGsql трудно достичь динамических типов возврата. Но вокруг есть способы - с некоторыми ограничениями.

Поэтому, чтобы не усложнять остальные, я демонстрирую с помощью более простого тестового примера:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

Вызов:

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2')
AS ct (row_name text, val1 int, val2 int, val3 int);

Возврат:

 row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8

Встроенная функция модуля tablefunc

Модуль tablefunc обеспечивает простую инфраструктуру для общих вызовов crosstab(), не предоставляя список определения столбцов. Ряд функций, записанных в C (обычно очень быстрый):

crosstabN()

crosstab1() - crosstab4() предварительно определены. Один второстепенный момент: они требуют и возвращают все text. Поэтому нам нужно указать наши значения integer. Но это упрощает вызов:

SELECT * FROM crosstab4('SELECT row_name, attrib, val::text  -- cast!
                         FROM tbl ORDER BY 1,2')

Результат:

 row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |

Пользовательская функция crosstab()

Для большего количества столбцов или других типов данных мы создаем собственный составной тип и функцию (один раз).
Тип:

CREATE TYPE tablefunc_crosstab_int_5 AS (
  row_name text, val1 int, val2 int, val3 int, val4 int, val5 int);

Функции:

CREATE OR REPLACE FUNCTION crosstab_int_5(text)
  RETURNS SETOF tablefunc_crosstab_int_5
AS '$libdir/tablefunc', 'crosstab' LANGUAGE c STABLE STRICT;

Вызов:

SELECT * FROM crosstab_int_5('SELECT row_name, attrib, val   -- no cast!
                              FROM tbl ORDER BY 1,2');

Результат:

 row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |

Одна полиморфная динамическая функция для всех

Это выходит за рамки того, что покрывается модулем tablefunc.
Чтобы сделать динамический тип возвращаемого значения, я использую полиморфный тип с техникой, подробно описанной в этом связанном ответе:

1-параметрическая форма:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L) t(%s)'
                , _qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

Перегрузка с этим вариантом для двухпараметрической формы:

CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L, %L) t(%s)'
                , _qry, _cat_qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;

pg_typeof(_rowtype)::text::regclass: существует определенный тип строки для каждого пользовательского составного типа, поэтому атрибуты (столбцы) перечислены в системном каталоге pg_attribute. Быстрая полоса для ее получения: введите зарегистрированный тип (regtype) в text и отбросьте этот text на regclass.

Создание композитных типов один раз:

Вам нужно определить один раз каждый возвращаемый тип, который вы собираетесь использовать:

CREATE TYPE tablefunc_crosstab_int_3 AS (
    row_name text, val1 int, val2 int, val3 int);

CREATE TYPE tablefunc_crosstab_int_4 AS (
    row_name text, val1 int, val2 int, val3 int, val4 int);

...

Для специальных вызовов вы также можете просто создать временную таблицу для того же (временного) эффекта:

CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);

Или используйте тип существующей таблицы, представления или материализованного представления, если они доступны.

Вызов

Использование вышеперечисленных типов строк:

1-параметрическая форма (без пропущенных значений):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1,2'
 , NULL::tablefunc_crosstab_int_3);

2-параметрическая форма (некоторые значения могут отсутствовать):

SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1'
 , $$VALUES ('val1'), ('val2'), ('val3')$$
 , NULL::tablefunc_crosstab_int_3);

Эта одна функция работает для всех типов возврата, а структура crosstabN(), предоставляемая модулем tablefunc, нуждается в отдельной функции для каждого.
Если вы назвали свои типы последовательно, как показано выше, вам нужно заменить только жирный номер. Чтобы найти максимальное количество категорий в базовой таблице:

SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;

Это примерно так же динамично, как и для отдельных столбцов. Такие массивы, как продемонстрированные @Clocoaldo или простое текстовое представление, или результат, заключенный в тип документа типа json или hstore, может работать для любого количества категорий динамически.

Отказ от ответственности:
Это всегда потенциально опасно, когда пользовательский ввод преобразуется в код. Убедитесь, что это не может использоваться для SQL-инъекции. Не принимайте вход от ненадежных пользователей (напрямую).

Вызов оригинального вопроса:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);

Ответ 2

Хотя это старый вопрос, я хотел бы добавить еще одно решение, которое стало возможным благодаря недавним улучшениям в PostgreSQL. Это решение достигает той же цели - вернуть структурированный результат из динамического набора данных без использования функции кросс-таблицы вообще.. Другими словами, это хороший пример повторного изучения непреднамеренных и неявных предположений, которые предотвращают нам от открытия новых решений старых проблем.;)

Чтобы проиллюстрировать, вы попросили метод переноса данных со следующей структурой:

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D

в этот формат:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

Традиционное решение - это умный (и невероятно осведомленный) подход к созданию динамических кросс-табличных запросов, которые объясняются в изысканной детализации в ответе Эрвина Брандстретера.

Однако, если ваш конкретный вариант использования достаточно гибкий, чтобы принять немного другой формат результата, тогда возможно другое решение, которое красиво обрабатывает динамические точки. Этот метод, о котором я узнал здесь

использует функцию PostgreSQL new jsonb_object_agg для создания поворотных данных "на лету" в виде объекта JSON.

Я буду использовать мистер Brandstetter "более простой тестовый пример", чтобы проиллюстрировать:

CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);

Используя функцию jsonb_object_agg, мы можем создать необходимый поворотный результирующий набор с этой красотой:

SELECT
  row_name AS bar,
  json_object_agg(attrib, val) AS data
FROM tbl
GROUP BY row_name
ORDER BY row_name;

Какие выходы:

 bar |                  data                  
-----+----------------------------------------
 A   | { "val1" : 10, "val2" : 20 }
 B   | { "val1" : 3, "val2" : 4 }
 C   | { "val1" : 5 }
 D   | { "val3" : 8, "val1" : 6, "val2" : 7 }

Как вы можете видеть, эта функция работает путем создания пар ключ/значение в объекте JSON из столбцов attrib и value в образцах данных, все сгруппированы по row_name.

Хотя этот набор результатов, очевидно, выглядит по-другому, я считаю, что он действительно удовлетворит многие (если не большинство) случаи использования в реальном мире, особенно те, где данные требуют динамически сгенерированного стержня или где результирующие данные потребляются родительским приложением (например, необходимо переформатировать для передачи в HTTP-ответе).

Преимущества такого подхода:

  • Чистый синтаксис.. Я думаю, что все согласятся с тем, что синтаксис этого подхода намного понятнее и понятнее, чем даже самые основные примеры кросс-таблицы.

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

  • Обработка больших количеств столбцов.. Поскольку скользящие данные сохраняются как один столбец jsonb, вы не столкнетесь с префиксным столбцом PostgreSQL (я полагаю, это число, равное 1 600 столбцам). По-прежнему существует предел, но я считаю, что это то же самое, что и для текстовых полей: 1 ГБ на созданный объект JSON (пожалуйста, поправьте меня, если я ошибаюсь). Это много пар ключ/значение!

  • Упрощенная обработка данных.. Я считаю, что создание данных JSON в БД упростит (и, вероятно, ускорит) процесс преобразования данных в родительских приложениях. (Вы заметите, что целочисленные данные в нашем примере тестового теста были правильно сохранены как таковые в результирующих объектах JSON. PostgreSQL обрабатывает это путем автоматического преобразования его внутренних типов данных в JSON в соответствии со спецификацией JSON.) Это эффективно устранит необходимость вручную передавать данные, переданные родительским приложениям: все они могут быть делегированы в собственный JSON-анализатор приложения.

Различия (и возможные недостатки):

  • Все выглядит иначе. Нельзя отрицать, что результаты этого подхода выглядят иначе. Объект JSON не так хорош, как набор результатов кросс-таблицы; однако различия являются чисто косметическими. Такая же информация создается и в формате, который, вероятно, более дружелюбен для потребления родительскими приложениями.

  • Отсутствующие ключи. Недостающие значения в подходе кросс-таблицы заполняются нулями, в то время как объекты JSON просто пропускают соответствующие ключи. Вам придется решать за себя, если это приемлемый компромисс для вашего прецедента. Мне кажется, что любая попытка решить эту проблему в PostgreSQL значительно усложнит процесс и, вероятно, потребует некоторой интроспекции в виде дополнительных запросов.

  • Порядок клавиш не сохраняется.Я не знаю, может ли это быть рассмотрено в PostgreSQL, но эта проблема в основном косметическая, так как любые родительские приложения либо вряд ли будут полагаться на порядок клавиш, либо иметь возможность определять правильный порядок клавиш другими способами. В худшем случае, вероятно, потребуется только запрос на добавление базы данных.

Заключение

Мне очень любопытно слышать мнения других (особенно @ErwinBrandstetter's) об этом подходе, особенно в том, что касается производительности. Когда я обнаружил этот подход в блоге Эндрю Бендера, это было похоже на попадание в сторону головы. Какой прекрасный способ сделать новый подход к сложной проблеме в PostrgeSQL. Он отлично решил мой прецедент, и я считаю, что он также будет служить многим другим.

Ответ 3

В вашем случае я думаю, что массив хорош. SQL Fiddle

select
    bar,
    feh || array_fill(null::int, array[c - array_length(feh, 1)]) feh
from
    (
        select bar, array_agg(feh) feh
        from foo
        group by bar
    ) s
    cross join (
        select count(*)::int c
        from foo
        group by bar
        order by c desc limit 1
    ) c(c)
;
 bar |      feh      
-----+---------------
 A   | {10,20,NULL}
 B   | {3,4,NULL}
 C   | {5,NULL,NULL}
 D   | {6,7,8}

Ответ 4

Мне жаль возвращаться в прошлое, но решение "Динамическая кросс-таблица" возвращает ошибочную таблицу результатов. Таким образом, значения valN ошибочно "выравниваются влево", и они не соответствуют именам столбцов. Когда входная таблица имеет "отверстия" в значениях, например. "C" имеет val1 и val3, но не val2. Это вызывает ошибку: значение val3 будет располагаться в столбце val2 (т.е. Следующий свободный столбец) в финальной таблице.

CREATE TEMP TABLE tbl (текст строки, текст атрибута, val int); INSERT INTO tbl (row_name, attrib, val) VALUES ('C', 'val1', 5) ('C', 'val3', 7);

SELECT * FROM crosstab ('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2') AS ct (string_name text, val1 int, val2 int, val3 int); ROW_NAME | знач1 | val2 | val3  C | 5 | 7 |

Чтобы вернуть правильные ячейки с "отверстиями" в правом столбце, запрос кросс-таблицы требует 2-го SELECT в кросс-таблице, что-то вроде этого "кросс-таблицы" ( "SELECT row_name, attrib, val FROM tbl ORDER BY 1,2", 'выберите отдельное имя_выбора из порядка tbl на 1') "

Ответ 5

Это для полного ответа @Damian. Я уже предлагал подход JSON в других ответах до 9.6 удобной функции json_object_agg. Это просто требует больше работы с предыдущим набором инструментов.

Два из приведенных возможных недостатков на самом деле нет. При необходимости случайный порядок ключей трижды корректируется. Отсутствующие ключи, если необходимо, принимают почти тривиальное количество кода, которое необходимо устранить:

select
    row_name as bar,
    json_object_agg(attrib, val order by attrib) as data
from
    tbl
    right join
    (
        (select distinct row_name from tbl) a
        cross join
        (select distinct attrib from tbl) b
    ) c using (row_name, attrib)
group by row_name
order by row_name
;
 bar |                     data                     
-----+----------------------------------------------
 a   | { "val1" : 10, "val2" : 20, "val3" : null }
 b   | { "val1" : 3, "val2" : 4, "val3" : null }
 c   | { "val1" : 5, "val2" : null, "val3" : null }
 d   | { "val1" : 6, "val2" : 7, "val3" : 8 }

Для конечного пользователя запроса, который понимает JSON, нет недостатков. Единственное, что он не может быть использован в качестве источника таблицы.