PostgreSQL Crosstab Query

154

Кто-нибудь знает, как создавать кросс-таблицы в 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

Возможно ли это?

  • 1
    У меня была немного другая структура, и я нашел этот пример немного сложным для понимания, поэтому я задокументировал свое мышление об этом stackoverflow.com/q/49051959/808723 . Может быть, это полезно для всех.
Теги:
pivot
case
crosstab

6 ответов

251
Лучший ответ

Установите дополнительный модуль 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);

Возвращает:

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

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

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

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

Возвращает:

 Section | Active | Inactive
---------+--------+----------
 A       |      1 |        2
 B       |      4 |        5
 C       |        |        7  -- !!
  • Обратите внимание на правильный результат для C

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

    'SELECT DISTINCT attribute FROM tbl ORDER BY 1'
    

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

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

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

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

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

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

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

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


\crosstabview в psql

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

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

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

Связанный ответ на dba.SE Daniel Vérité (автор функции psql):



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

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

    crosstab(text sql, int N)...

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

  • Неправильное кастинг и переименование.

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

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

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

  • 1
    Спасибо за предоставленный обновленный ответ. Я не знал об изменениях в функции crosstab .
  • 2
    +1, хорошая запись, спасибо, что заметили In practice the SQL query should always specify ORDER BY 1,2 to ensure that the input rows are properly ordered
Показать ещё 4 комментария
26

Вы можете использовать функцию 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);
  • 0
    В случае, если вы используете параметр в запросе кросс-таблицы, вы должны правильно его избежать. Пример: (сверху) говорят, что вам нужны только активные: 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 это также работает с? заполнитель.
17
SELECT section,
       SUM(CASE status WHEN 'Active' THEN count ELSE 0 END) AS active,
       SUM(CASE status WHEN 'Inactive' THEN count ELSE 0 END) AS inactive
FROM t
GROUP BY section
  • 1
    Может кто-нибудь объяснить, что функция кросс-таблицы в модуле tablefunc добавляет к этому ответу, что и делает работу под рукой, и, на мой взгляд, легче понять?
  • 3
    @ JohnBarça: Простой случай, подобный этому, можно легко решить с помощью операторов CASE. Однако, это становится громоздким очень быстро с большим количеством атрибутов и / или других типов данных, чем просто целые числа. Как отступление: эта форма использует агрегатную функцию sum() , было бы лучше использовать min() или max() и не использовать ELSE который также работает с text . Но это имеет несколько иные эффекты, чем corosstab() , который использует только «первое» значение для каждого атрибута. Не имеет значения, пока может быть только один. Наконец, производительность тоже актуальна. crosstab() написан на C и оптимизирован для этой задачи.
Показать ещё 6 комментариев
2

Решение с агрегацией 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
0

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

CREATE EXTENSION 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)
  • 0
    Этот ответ ничего не добавляет к уже существующим ответам.
0

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

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;

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню