Ответ 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. Он отлично решил мой прецедент, и я считаю, что он также будет служить многим другим.