PostgreSQL Crosstab Query

197

Кто-нибудь знает, как создавать кросс-таблицы запросов в PostgreSQL?
Например, у меня есть следующая таблица:

Section    Status    Count
A          Active    1
A          Inactive  2
B          Active    4
B          Inactive  5

Я бы хотел, чтобы запрос возвращал следующую кросс-таблицу:

Section    Active    Inactive
A          1         2
B          4         5

Это возможно?

Schone
источник
1
У меня была немного другая структура, и мне показалось, что этот пример немного сложен для понимания, поэтому я задокументировал свой способ мышления об этом stackoverflow.com/q/49051959/808723 . Может быть, это полезно для всех.
GameScripting

Ответы:

318

Установите дополнительный модуль tablefunc один раз для каждой базы данных, что обеспечивает функцию crosstab(). Начиная с Postgres 9.1 вы можете использовать CREATE EXTENSIONдля этого:

CREATE EXTENSION IF NOT EXISTS tablefunc;

Улучшенный тестовый пример

CREATE TABLE tbl (
   section   text
 , status    text
 , ct        integer  -- "count" is a reserved word in standard SQL
);

INSERT INTO tbl VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                    , ('C', 'Inactive', 7);  -- ('C', 'Active') is missing

Простая форма - не подходит для отсутствующих атрибутов

crosstab(text)с 1 входным параметром:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- needs to be "ORDER BY 1,2" here
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Возвращает:

Раздел | Активный | Неактивный
--------- + -------- + ----------
 A | 1 | 2
 Б | 4 | 5
 C |      7 | - !!
  • Нет необходимости в кастинге и переименовании.
  • Обратите внимание на неверный результат для C: значение 7заполнено для первого столбца. Иногда такое поведение желательно, но не для этого варианта использования.
  • Простая форма также ограничена ровно тремя столбцами в предоставленном входном запросе: row_name , category , value . Нет места для дополнительных столбцов, как в альтернативе с двумя параметрами ниже.

Безопасная форма

crosstab(text, text)с 2 входными параметрами:

SELECT *
FROM   crosstab(
   'SELECT section, status, ct
    FROM   tbl
    ORDER  BY 1,2'  -- could also just be "ORDER BY 1" here

  , $$VALUES ('Active'::text), ('Inactive')$$
   ) AS ct ("Section" text, "Active" int, "Inactive" int);

Возвращает:

Раздел | Активный | Неактивный
--------- + -------- + ----------
 A | 1 | 2
 Б | 4 | 5
 C | |        7   - !!
  • Обратите внимание на правильный результат для C.

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

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'

    Это в руководстве.

    Так как в любом случае вам необходимо прописать все столбцы в списке определений столбцов (кроме предопределенных вариантов), как правило, более эффективно предоставить короткий список в выражении, как показано:crosstabN()VALUES

    $$VALUES ('Active'::text), ('Inactive')$$)

    Или (не в руководстве):

    $$SELECT unnest('{Active,Inactive}'::text[])$$  -- short syntax for long lists
  • Я использовал цитирование доллара, чтобы сделать его проще.

  • Вы даже можете выводить столбцы с разными типами данных при crosstab(text, text)условии, что текстовое представление столбца значения является допустимым вводом для целевого типа. Таким образом , вы можете иметь атрибуты различных видов и продукции text, date, и numericт.д. для соответствующих атрибутов. В конце главы crosstab(text, text)руководства приведен пример кода .

дБ <> скрипка здесь

Расширенные примеры


\crosstabview в PSQL

Postgres 9.6 добавил эту мета-команду к своему интерактивному терминалу psql по умолчанию . Вы можете запустить запрос, который вы будете использовать в качестве первого crosstab()параметра, и передать его \crosstabview(немедленно или на следующем шаге). Подобно:

db=> SELECT section, status, ct FROM tbl \crosstabview

Результат, аналогичный приведенному выше, но это функция представления исключительно на стороне клиента . Входные строки обрабатываются немного по-разному, поэтому ORDER BYне требуется. Подробности \crosstabviewв руководстве. Внизу этой страницы есть больше примеров кода.

Соответствующий ответ на dba.SE Даниэля Верите (автор функции psql):



Ранее принят ответ устарел.

  • Вариант функции crosstab(text, integer)устарел. Второй integerпараметр игнорируется. Я цитирую текущее руководство :

    crosstab(text sql, int N) ...

    Устаревшая версия crosstab(text). Параметр Nтеперь игнорируется, поскольку количество столбцов значений всегда определяется вызывающим запросом.

  • Ненужное литье и переименование.

  • Сбой, если строка не имеет всех атрибутов. См. Безопасный вариант с двумя входными параметрами выше для правильной обработки отсутствующих атрибутов.

  • ORDER BYтребуется в однопараметрической форме crosstab(). Руководство:

    На практике SQL-запрос должен всегда указывать, ORDER BY 1,2чтобы гарантировать, что входные строки правильно упорядочены

Эрвин Брандштеттер
источник
3
+1, хорошая рецензия, спасибо, что заметилиIn practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered
ChristopheD
У меня проблемы с использованием $$ VALUES .. $$. Вместо этого я использовал 'VALUES (' '<attr>' ':: <type>), ..'
Marco Fantasia
Можем ли мы указать привязку параметров в кросс-таблице? Я получаю эту ошибку => не могу определить тип данных параметра $ 2
Ashish
1
Можно ли установить значение по умолчанию для столбца в запросе кросс-таблицы?
Ашиш
2
@Ashish: Пожалуйста, начните новый вопрос. Комментарии не место. Вы всегда можете связаться с этим для контекста.
Эрвин Брандштеттер
30

Вы можете использовать crosstab()функцию дополнительного модуля tablefunc - которую вы должны установить один раз для каждой базы данных. Начиная с PostgreSQL 9.1 вы можете использовать CREATE EXTENSIONдля этого:

CREATE EXTENSION tablefunc;

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

CREATE TABLE t (Section CHAR(1), Status VARCHAR(10), Count integer);

INSERT INTO t VALUES ('A', 'Active',   1);
INSERT INTO t VALUES ('A', 'Inactive', 2);
INSERT INTO t VALUES ('B', 'Active',   4);
INSERT INTO t VALUES ('B', 'Inactive', 5);

SELECT row_name AS Section,
       category_1::integer AS Active,
       category_2::integer AS Inactive
FROM crosstab('select section::text, status, count::text from t',2)
            AS ct (row_name text, category_1 text, category_2 text);
Иеремия Пешка
источник
В случае, если вы используете параметр в запросе кросс-таблицы, вы должны правильно его избежать. Пример: (сверху) говорят, что вам нужны только активные: SELECT ... FROM кросс-таблицы ('select section :: text, status, count :: text from t, где status =' 'active' '', 2) AS. .. (обратите внимание на двойные кавычки). Если параметр передается пользователем во время выполнения (например, в качестве параметра функции), вы можете сказать: SELECT ... FROM crosstab ('select section :: text, status, count :: text from t, где status =' ' '|| par_active ||' '' ', 2) AS ... (тройные кавычки здесь!). В BIRT это также работает с? заполнитель.
Вим Верхаверт
26
SELECT section,
       SUM(CASE status WHEN 'Active' THEN count ELSE 0 END) AS active, --here you pivot each status value as a separate column explicitly
       SUM(CASE status WHEN 'Inactive' THEN count ELSE 0 END) AS inactive --here you pivot each status  value as a separate column explicitly

FROM t
GROUP BY section
araqnid
источник
1
Может кто-нибудь объяснить, что функция кросс-таблицы в модуле tablefunc добавляет к этому ответу, что и делает работу под рукой, и, на мой взгляд, легче понять?
Джон Пауэлл
4
@ JohnBarça: Простой случай, подобный этому, можно легко решить с помощью операторов CASE. Однако это становится громоздким очень быстро с большим количеством атрибутов и / или других типов данных, чем просто целые числа. Как отступление: эта форма использует агрегатную функцию sum(), было бы лучше использовать min()или max()нет, и ELSEэто также работает text. Но это имеет несколько иной эффект, чем тот corosstab(), который использует только «первое» значение для каждого атрибута. Не имеет значения, пока может быть только один. Наконец, производительность тоже актуальна. crosstab()написан на C и оптимизирован для задачи.
Эрвин Брандстеттер
Это не работает для меня, для postgresql. Я получаю ошибкуERROR: 42803: aggregate function calls may not be nested
Одри
1
@ Оудри, вы не используете тот же SQL?
2
Подумайте о том, чтобы добавить объяснение против всего лишь блока кода
Даниэль Л. ВанДенБош
10

Решение с агрегацией JSON:

CREATE TEMP TABLE t (
  section   text
, status    text
, ct        integer  -- don't use "count" as column name.
);

INSERT INTO t VALUES 
  ('A', 'Active', 1), ('A', 'Inactive', 2)
, ('B', 'Active', 4), ('B', 'Inactive', 5)
                   , ('C', 'Inactive', 7); 


SELECT section,
       (obj ->> 'Active')::int AS active,
       (obj ->> 'Inactive')::int AS inactive
FROM (SELECT section, json_object_agg(status,ct) AS obj
      FROM t
      GROUP BY section
     )X
Милош
источник
Спасибо, это помогло мне с связанной проблемой.
JeffCharter
1

Извините, это не завершено, потому что я не могу проверить это здесь, но это может увести вас в правильном направлении. Я перевожу что-то, что я использую, что делает аналогичный запрос:

select mt.section, mt1.count as Active, mt2.count as Inactive
from mytable mt
left join (select section, count from mytable where status='Active')mt1
on mt.section = mt1.section
left join (select section, count from mytable where status='Inactive')mt2
on mt.section = mt2.section
group by mt.section,
         mt1.count,
         mt2.count
order by mt.section asc;

Код, из которого я работаю:

select m.typeID, m1.highBid, m2.lowAsk, m1.highBid - m2.lowAsk as diff, 100*(m1.highBid - m2.lowAsk)/m2.lowAsk as diffPercent
from mktTrades m
   left join (select typeID,MAX(price) as highBid from mktTrades where bid=1 group by typeID)m1
   on m.typeID = m1.typeID
   left join (select typeID,MIN(price) as lowAsk  from mktTrades where bid=0 group by typeID)m2
   on m1.typeID = m2.typeID
group by m.typeID, 
         m1.highBid, 
         m2.lowAsk
order by diffPercent desc;

который возвращает typeID, ставку с наивысшей ценой и наименьшую запрашиваемую цену, а также разницу между ними (положительная разница будет означать, что что-то можно купить за меньшую цену, чем можно продать).

LanceH
источник
1
Вы пропускаете предложение from, иначе это правильно. Планы объяснения в моей системе сильно отличаются - стоимость функции кросс-таблицы составляет 22,5, в то время как подход LEFT JOIN примерно в 4 раза дороже, а стоимость - 91,38. Он также производит вдвое больше физических чтений и выполняет хеш-соединения - что может быть довольно дорого по сравнению с другими типами соединений.
Иеремия Пешка
Спасибо, Иеремия, это приятно знать. Я проголосовал против другого ответа, но ваш комментарий стоит сохранить, поэтому я не буду удалять этот.
LanceH
-1

Crosstabфункция доступна под tablefuncрасширением. Вам нужно будет создать это расширение один раз для базы данных.

СОЗДАТЬ ПРОДЛЕНИЕ tablefunc;

Вы можете использовать приведенный ниже код для создания сводной таблицы с использованием кросс-таблицы:

create table test_Crosstab( section text,
<br/>status text,
<br/>count numeric)

<br/>insert into test_Crosstab values ( 'A','Active',1)
                <br/>,( 'A','Inactive',2)
                <br/>,( 'B','Active',4)
                <br/>,( 'B','Inactive',5)

select * from crosstab(
<br/>'select section
    <br/>,status
    <br/>,count
    <br/>from test_crosstab'
    <br/>)as ctab ("Section" text,"Active" numeric,"Inactive" numeric)
Лекшми Куруп
источник
1
Этот ответ ничего не добавляет к ранее существующим ответам.
Эрвин Брандштеттер