MySQL - строки в столбцы

195

Я пытался искать сообщения, но нашел решения только для SQL Server / Access. Мне нужно решение в MySQL (5.X).

У меня есть таблица (называемая историей) с 3 столбцами: hostid, itemname, itemvalue.
Если я сделаю select ( select * from history), он вернет

   +--------+----------+-----------+
   | hostid | itemname | itemvalue |
   +--------+----------+-----------+
   |   1    |    A     |    10     |
   +--------+----------+-----------+
   |   1    |    B     |     3     |
   +--------+----------+-----------+
   |   2    |    A     |     9     |
   +--------+----------+-----------+
   |   2    |    c     |    40     |
   +--------+----------+-----------+

Как мне запросить базу данных, чтобы вернуть что-то вроде

   +--------+------+-----+-----+
   | hostid |   A  |  B  |  C  |
   +--------+------+-----+-----+
   |   1    |  10  |  3  |  0  |
   +--------+------+-----+-----+
   |   2    |   9  |  0  |  40 |
   +--------+------+-----+-----+
Боб Риверс
источник
@Rob, не могли бы вы отредактировать вопрос, чтобы включить точный запрос?
Johan
ПРИМЕЧАНИЕ: ссылка @ako актуальна только для MariaDB.
ToolmakerSteve,
Автоматическое создание и запуск pivot: mysql.rjweb.org/doc.php/pivot
Рик Джеймс

Ответы:

281

Я собираюсь добавить несколько более длинное и подробное объяснение шагов, которые необходимо предпринять для решения этой проблемы. Прошу прощения, если это слишком долго.


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

select * from history;

+--------+----------+-----------+
| hostid | itemname | itemvalue |
+--------+----------+-----------+
|      1 | A        |        10 |
|      1 | B        |         3 |
|      2 | A        |         9 |
|      2 | C        |        40 |
+--------+----------+-----------+

Это и будет нашей целью, красивой сводной таблицей :

select * from history_itemvalue_pivot;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 |    0 |
|      2 |    9 |    0 |   40 |
+--------+------+------+------+

Значения в history.hostidстолбце станут значениями y в сводной таблице. Значения в history.itemnameстолбце станут x-значениями (по очевидным причинам).


Когда мне нужно решить проблему создания сводной таблицы, я решаю ее, используя трехэтапный процесс (с необязательным четвертым шагом):

  1. выберите интересующие столбцы, то есть значения y и x
  2. расширить базовую таблицу дополнительными столбцами - по одному для каждого значения x
  3. group и агрегировать расширенную таблицу - по одной группе для каждого значения y
  4. (необязательно) преобразить агрегированную таблицу

Давайте применим эти шаги к вашей проблеме и посмотрим, что мы получим:

Шаг 1: выберите интересующие столбцы . В желаемом результате hostidпредоставляет значения y и itemnameпредоставляет значения x .

Шаг 2: расширьте базовую таблицу дополнительными столбцами . Обычно нам нужен один столбец на каждое значение x. Напомним, что наш столбец значений x itemname:

create view history_extended as (
  select
    history.*,
    case when itemname = "A" then itemvalue end as A,
    case when itemname = "B" then itemvalue end as B,
    case when itemname = "C" then itemvalue end as C
  from history
);

select * from history_extended;

+--------+----------+-----------+------+------+------+
| hostid | itemname | itemvalue | A    | B    | C    |
+--------+----------+-----------+------+------+------+
|      1 | A        |        10 |   10 | NULL | NULL |
|      1 | B        |         3 | NULL |    3 | NULL |
|      2 | A        |         9 |    9 | NULL | NULL |
|      2 | C        |        40 | NULL | NULL |   40 |
+--------+----------+-----------+------+------+------+

Обратите внимание, что мы не изменили количество строк - мы просто добавили дополнительные столбцы. Также обратите внимание на шаблон NULLs - строка с itemname = "A"ненулевым значением для нового столбца Aи нулевыми значениями для других новых столбцов.

Шаг 3: сгруппируйте и объедините расширенную таблицу . Нам нужно group by hostid, поскольку он предоставляет значения y:

create view history_itemvalue_pivot as (
  select
    hostid,
    sum(A) as A,
    sum(B) as B,
    sum(C) as C
  from history_extended
  group by hostid
);

select * from history_itemvalue_pivot;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 | NULL |
|      2 |    9 | NULL |   40 |
+--------+------+------+------+

(Обратите внимание, что теперь у нас есть одна строка для каждого значения y.) Хорошо, мы почти у цели! Нам просто нужно избавиться от этих уродливых NULL.

Шаг 4: украсить . Мы просто собираемся заменить любые нулевые значения нулями, чтобы на набор результатов было удобнее смотреть:

create view history_itemvalue_pivot_pretty as (
  select 
    hostid, 
    coalesce(A, 0) as A, 
    coalesce(B, 0) as B, 
    coalesce(C, 0) as C 
  from history_itemvalue_pivot 
);

select * from history_itemvalue_pivot_pretty;

+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 |    0 |
|      2 |    9 |    0 |   40 |
+--------+------+------+------+

И мы закончили - мы создали красивую сводную таблицу с использованием MySQL.


Соображения при применении этой процедуры:

  • какое значение использовать в дополнительных столбцах. Я использовал itemvalueв этом примере
  • какое «нейтральное» значение использовать в дополнительных столбцах. Я использовал NULL, но это также может быть 0или "", в зависимости от конкретной ситуации
  • какую агрегатную функцию использовать при группировке. Я использовал sum, но countи maxтакже часто используются ( maxчасто используется при создании одной строки «объекты» , которые были распространены во многих рядах)
  • использование нескольких столбцов для значений y. Это решение не ограничивается использованием одного столбца для значений y - просто вставьте дополнительные столбцы в group byпредложение (и не забудьте selectих).

Известные ограничения:

  • это решение не допускает n столбцов в сводной таблице - каждый сводный столбец необходимо добавлять вручную при расширении базовой таблицы. Так что для 5 или 10 значений x это решение подходит. На 100 не очень приятно. Есть некоторые решения с хранимыми процедурами, генерирующими запрос, но они уродливы, и их трудно понять. В настоящее время я не знаю хорошего способа решить эту проблему, когда в сводной таблице должно быть много столбцов.
Мэтт Фенвик
источник
25
+1 Это, безусловно, лучшее / наиболее ясное объяснение сводных таблиц / кросс-таблиц в MySQL, которое я видел
cameron.bracken
6
Отличное объяснение, спасибо. Шаг 4 можно объединить с шагом 3, используя IFNULL (sum (A), 0) AS A, что даст вам тот же результат, но без необходимости создания еще одной таблицы
nealio82
1
Это было самое удивительное решение для поворота, но мне просто любопытно, если в имени элемента столбца, который формирует ось x, есть несколько значений, например, здесь у нас есть только три значения, то есть A, B, C. Если эти значения расширяются до A, B, C, D, E, AB, BC, AC, AD, H ..... n. то в этом случае какое было бы решение.
Deepesh
1
это действительно должен быть принятый ответ здесь. Он более подробный, полезный и объясняет, как это понимать, а не просто ссылку на какую-то статью, подобную принятой в настоящее время
EdgeCaseBerg
2
@WhiteBig, пожалуйста, взгляните на даты - этот ответ на StackOverflow был написан за 1,5 года до этого сообщения в блоге. Возможно, вам стоит вместо этого попросить блог поверить мне.
Мэтт Фенвик,
59
SELECT 
    hostid, 
    sum( if( itemname = 'A', itemvalue, 0 ) ) AS A,  
    sum( if( itemname = 'B', itemvalue, 0 ) ) AS B, 
    sum( if( itemname = 'C', itemvalue, 0 ) ) AS C 
FROM 
    bob 
GROUP BY 
    hostid;
Shantanuo
источник
Создает три разных ряда для «A», «B», «C»
Палани
1
@Palani: Нет, это не так. Смотрите group by.
ruakh
Спасибо, у меня это сработало! Однако, к вашему сведению, на пару лет позже мне пришлось использовать MAXвместо, SUMпотому что мои itemValue- это строки, а не числовые значения.
Меррикат,
35

Другой вариант, особенно полезный, если у вас есть много элементов, которые нужно развернуть, - это позволить mysql построить запрос за вас:

SELECT
  GROUP_CONCAT(DISTINCT
    CONCAT(
      'ifnull(SUM(case when itemname = ''',
      itemname,
      ''' then itemvalue end),0) AS `',
      itemname, '`'
    )
  ) INTO @sql
FROM
  history;
SET @sql = CONCAT('SELECT hostid, ', @sql, ' 
                  FROM history 
                   GROUP BY hostid');

PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

FIDDLE Добавлены дополнительные значения, чтобы увидеть, как он работает

GROUP_CONCAT имеет значение по умолчанию 1000, поэтому, если у вас действительно большой запрос, измените этот параметр перед его запуском

SET SESSION group_concat_max_len = 1000000;

Контрольная работа:

DROP TABLE IF EXISTS history;
CREATE TABLE history
(hostid INT,
itemname VARCHAR(5),
itemvalue INT);

INSERT INTO history VALUES(1,'A',10),(1,'B',3),(2,'A',9),
(2,'C',40),(2,'D',5),
(3,'A',14),(3,'B',67),(3,'D',8);

  hostid    A     B     C      D
    1     10      3     0      0
    2     9       0    40      5
    3     14     67     0      8
Михай
источник
@Mihai Может ты мне поможешь. Посмотрите на это: stackoverflow.com/questions/51832979/…
Success Man
Можно упростить 'ifnull(SUM(case when itemname = ''',с помощью ''' then itemvalue end),0) AS ', `до 'SUM(case when itemname = '''с ''' then itemvalue else 0 end) AS ',. Это выводит такие термины, как SUM(case when itemname = 'A' then itemvalue else 0 end) AS 'A'.
ToolmakerSteve,
25

Воспользовавшись идеей Мэтта Фенвика, которая помогла мне решить проблему (большое спасибо), давайте сведем ее к одному запросу:

select
    history.*,
    coalesce(sum(case when itemname = "A" then itemvalue end), 0) as A,
    coalesce(sum(case when itemname = "B" then itemvalue end), 0) as B,
    coalesce(sum(case when itemname = "C" then itemvalue end), 0) as C
from history
group by hostid
болтун
источник
14

Я редактирую ответ Агунг Сагиты из подзапроса, чтобы присоединиться. Я не уверен, насколько велика разница между этими двумя способами, но просто для другой справки.

SELECT  hostid, T2.VALUE AS A, T3.VALUE AS B, T4.VALUE AS C
FROM TableTest AS T1
LEFT JOIN TableTest T2 ON T2.hostid=T1.hostid AND T2.ITEMNAME='A'
LEFT JOIN TableTest T3 ON T3.hostid=T1.hostid AND T3.ITEMNAME='B'
LEFT JOIN TableTest T4 ON T4.hostid=T1.hostid AND T4.ITEMNAME='C'
хулиганство
источник
2
Возможно, это могло быть более быстрое решение.
jave.web 07
Я так не думаю. потому что левое соединение имеет свою задержку!
Abadis 03
10

использовать подзапрос

SELECT  hostid, 
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='A' AND hostid = t1.hostid) AS A,
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='B' AND hostid = t1.hostid) AS B,
    (SELECT VALUE FROM TableTest WHERE ITEMNAME='C' AND hostid = t1.hostid) AS C
FROM TableTest AS T1
GROUP BY hostid

но это будет проблемой, если подзапрос даст больше, чем строку, используйте дополнительную агрегатную функцию в подзапросе

Агунг Сагита
источник
4

Мое решение:

select h.hostid, sum(ifnull(h.A,0)) as A, sum(ifnull(h.B,0)) as B, sum(ifnull(h.C,0)) as  C from (
select
hostid,
case when itemName = 'A' then itemvalue end as A,
case when itemName = 'B' then itemvalue end as B,
case when itemName = 'C' then itemvalue end as C
  from history 
) h group by hostid

Это дает ожидаемые результаты в представленном случае.

Андре Вебер
источник
4

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

Количество столбцов запроса фиксировано, но значения являются динамическими и основаны на значениях строк. Вы можете его построить. Итак, я использую один запрос для создания заголовка таблицы, а другой - для просмотра значений:

SELECT distinct concat('<th>',itemname,'</th>') as column_name_table_header FROM history order by 1;

SELECT
     hostid
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue else '' end) as col1
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue else '' end) as col2
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue else '' end) as col3
    ,(case when itemname = (select distinct itemname from history a order by 1 limit 3,1) then itemvalue else '' end) as col4
FROM history order by 1;

Вы также можете резюмировать это:

SELECT
     hostid
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 0,1) then itemvalue end) as A
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 1,1) then itemvalue end) as B
    ,sum(case when itemname = (select distinct itemname from history a order by 1 limit 2,1) then itemvalue end) as C
FROM history group by hostid order by 1;
+--------+------+------+------+
| hostid | A    | B    | C    |
+--------+------+------+------+
|      1 |   10 |    3 | NULL |
|      2 |    9 | NULL |   40 |
+--------+------+------+------+

Результаты RexTester :

Результаты RexTester

http://rextester.com/ZSWKS28923

В качестве одного из реальных примеров использования этот отчет ниже показывает в столбцах часы отправления и прибытия лодки / автобуса с визуальным расписанием. Вы увидите один дополнительный столбец, который не используется в последнем столбце, чтобы не запутать визуализацию: онлайн-система venda de passagens и конечный потребитель и контроль над фротой - xsl tecnologia - xsl.com.br ** Система продажи билетов для продажи билетов онлайн и предварительная

lynx_74
источник
3

Я сделаю это, Group By hostIdтогда он покажет только первую строку со значениями,
например:

A   B  C
1  10
2      3
арпит
источник
3

Если бы вы могли использовать MariaDB, есть очень простое решение.

Начиная с MariaDB-10.02 был добавлен новый механизм хранения под названием CONNECT, который может помочь нам преобразовать результаты другого запроса или таблицы в сводную таблицу, как вы хотите: вы можете просмотреть документацию .

Прежде всего установите механизм подключения хранилища .

Теперь сводный столбец нашей таблицы есть, itemnameи данные для каждого элемента расположены в itemvalueстолбце, поэтому мы можем получить сводную таблицу результатов, используя этот запрос:

create table pivot_table
engine=connect table_type=pivot tabname=history
option_list='PivotCol=itemname,FncCol=itemvalue';

Теперь мы можем выбрать то, что хотим, из pivot_table:

select * from pivot_table

Подробнее здесь

ако
источник
1

Это не точный ответ, который вы ищете, но это было решение, которое мне нужно в моем проекте, и надеюсь, что это кому-то поможет. Будет перечислено от 1 до n элементов строки, разделенных запятыми. Group_Concat делает это возможным в MySQL.

select
cemetery.cemetery_id as "Cemetery_ID",
GROUP_CONCAT(distinct(names.name)) as "Cemetery_Name",
cemetery.latitude as Latitude,
cemetery.longitude as Longitude,
c.Contact_Info,
d.Direction_Type,
d.Directions

    from cemetery
    left join cemetery_names on cemetery.cemetery_id = cemetery_names.cemetery_id 
    left join names on cemetery_names.name_id = names.name_id 
    left join cemetery_contact on cemetery.cemetery_id = cemetery_contact.cemetery_id 

    left join 
    (
        select 
            cemetery_contact.cemetery_id as cID,
            group_concat(contacts.name, char(32), phone.number) as Contact_Info

                from cemetery_contact
                left join contacts on cemetery_contact.contact_id = contacts.contact_id 
                left join phone on cemetery_contact.contact_id = phone.contact_id 

            group by cID
    )
    as c on c.cID = cemetery.cemetery_id


    left join
    (
        select 
            cemetery_id as dID, 
            group_concat(direction_type.direction_type) as Direction_Type,
            group_concat(directions.value , char(13), char(9)) as Directions

                from directions
                left join direction_type on directions.type = direction_type.direction_type_id

            group by dID


    )
    as d on d.dID  = cemetery.cemetery_id

group by Cemetery_ID

У этого кладбища два общих имени, поэтому имена перечислены в разных строках, связанных одним идентификатором, но двумя идентификаторами имен, и запрос дает что-то вроде этого

    CemeteryID Cemetery_Name Latitude
    1 Appleton, Sulpher Springs 35.4276242832293

Джеймс Хамфри
источник
-2

Мне жаль это говорить, и, возможно, я не решаю вашу проблему в точности, но PostgreSQL на 10 лет старше MySQL и чрезвычайно продвинут по сравнению с MySQL, и есть много способов легко этого добиться. Установите PostgreSQL и выполните этот запрос

CREATE EXTENSION tablefunc;

тогда вуаля! А вот обширная документация: PostgreSQL: Документация: 9.1: tablefunc или этот запрос

CREATE EXTENSION hstore;

потом снова вуаля! PostgreSQL: Документация: 9.0: hstore

гдаркан
источник