MySQL - Linhas para colunas

188

Tentei pesquisar postagens, mas só encontrei soluções para o SQL Server / Access. Eu preciso de uma solução no MySQL (5.X).

Eu tenho uma tabela (chamada história) com 3 colunas: hostid, itemname, itemvalue.
Se eu fizer um select ( select * from history), ele retornará

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

Como faço para consultar o banco de dados para retornar algo como

   +--------+------+-----+-----+
   | hostid |   A  |  B  |  C  |
   +--------+------+-----+-----+
   |   1    |  10  |  3  |  0  |
   +--------+------+-----+-----+
   |   2    |   9  |  0  |  40 |
   +--------+------+-----+-----+
Bob Rivers
fonte
@ Rob, você pode editar a pergunta para incluir a consulta exata?
28411 Johan
NOTA: O link do @ako é relevante apenas para o MariaDB.
ToolmakerSteve
Geração automática e execução de um pivô: mysql.rjweb.org/doc.php/pivot
Rick James

Respostas:

276

Vou adicionar uma explicação um pouco mais longa e detalhada dos passos a serem tomados para resolver esse problema. Peço desculpas se for muito longo.


Começarei com a base que você forneceu e a utilizará para definir alguns termos que utilizarei para o restante deste post. Esta será a tabela base :

select * from history;

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

Este será o nosso objetivo, a bonita tabela dinâmica :

select * from history_itemvalue_pivot;

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

Os valores na history.hostidcoluna se tornarão valores y na tabela dinâmica. Os valores na history.itemnamecoluna se tornarão valores x (por razões óbvias).


Quando tenho que resolver o problema de criar uma tabela dinâmica, resolvo-a usando um processo de três etapas (com uma quarta etapa opcional):

  1. seleccionar as colunas de interesse, ou seja, valores de y e x-valores
  2. estenda a tabela base com colunas extras - uma para cada valor x
  3. agrupar e agregar a tabela estendida - um grupo para cada valor y
  4. (opcional) pré-definir a tabela agregada

Vamos aplicar estas etapas ao seu problema e ver o que temos:

Etapa 1: selecione colunas de interesse . No resultado desejado, hostidfornece os valores de y e itemnamefornece os valores de x .

Etapa 2: estenda a tabela base com colunas extras . Normalmente, precisamos de uma coluna por valor x. Lembre-se de que nossa coluna de valor 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 |
+--------+----------+-----------+------+------+------+

Observe que não alteramos o número de linhas - apenas adicionamos colunas extras. Observe também o padrão de NULLs - uma linha com itemname = "A"um valor não nulo para a nova coluna Ae valores nulos para as outras novas colunas.

Etapa 3: agrupe e agregue a tabela estendida . Precisamos group by hostid, pois fornece os valores 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 |
+--------+------+------+------+

(Observe que agora temos uma linha por valor y). Ok, estamos quase lá! Nós só precisamos nos livrar daqueles feios NULL.

Etapa 4: prettify . Vamos substituir todos os valores nulos por zeros, para que o conjunto de resultados seja melhor:

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 |
+--------+------+------+------+

E pronto - nós construímos uma tabela dinâmica bonita e bonita usando o MySQL.


Considerações ao aplicar este procedimento:

  • qual valor usar nas colunas extras. Eu usei itemvalueneste exemplo
  • qual valor "neutro" usar nas colunas extras. Eu usei NULL, mas também pode ser 0ou "", dependendo da sua situação exata
  • qual função agregada usar ao agrupar. Eu costumava sum, mas counte maxtambém são usados frequentemente (max é frequentemente utilizado na construção de uma linha "objetos" que tinham sido espalhados por várias linhas)
  • usando várias colunas para valores y. Essa solução não se limita ao uso de uma única coluna para os valores y - basta conectar as colunas extras à group bycláusula (e não se esqueça selectdelas)

Limitações conhecidas:

  • essa solução não permite n colunas na tabela dinâmica - cada coluna dinâmica precisa ser adicionada manualmente ao estender a tabela base. Portanto, para 5 ou 10 valores x, esta solução é boa. Por 100, não é tão legal. Existem algumas soluções com procedimentos armazenados que geram uma consulta, mas são feias e difíceis de corrigir. Atualmente, não conheço uma boa maneira de resolver esse problema quando a tabela dinâmica precisa ter muitas colunas.
Matt Fenwick
fonte
25
+1 Este é de longe o melhor e mais explicação / clara de tabelas dinâmicas / abas transversais no MySQL Eu vi
cameron.bracken
6
Excelente explicação, obrigado. Passo 4 podem ser fundidos em etapa 3 usando IFNULL (soma (A), 0) como um, dando-lhe o mesmo resultado, mas sem a necessidade de criação de mais uma mesa
nealio82
1
Foi a solução mais incrível para o pivô, mas estou curioso para saber se na coluna itemname, que forma o eixo x, tem vários valores, como aqui só temos três valores, como A, B, C. Se esses valores se estenderem para A, B, C, D, E, AB, BC, CA, AD, H ..... n. então, nesse caso, qual seria a solução.
Deepesh
1
essa deve ser realmente a resposta aceita aqui. É muito mais detalhado, útil e explica como compreendê-lo ao invés de apenas links para alguns artigos como o que actualmente aceite
EdgeCaseBerg
2
@WhiteBig, dê uma olhada nas datas - esta resposta do StackOverflow foi escrita 1,5 anos antes da publicação no blog. Talvez você devesse pedir ao blog que me creditasse.
Matt Fenwick
55
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
fonte
Cria três linhas diferentes, para 'A', 'B', 'C'
Palani 9/12
1
@Palani: Não, não. Veja group by.
Ruakh
Obrigado, isso funcionou para mim! No entanto, apenas um FYI com alguns anos de atraso, eu tive que usar em MAXvez de SUMporque itemValuesão strings, não valores numéricos.
Merricat 17/06
33

Outra opção, especialmente útil se você tiver muitos itens que você precisa dinamizar, é permitir que o mysql construa a consulta para você:

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 Adicionado alguns valores extras para vê-lo funcionando

GROUP_CONCAT tem um valor padrão de 1000, portanto, se você tiver uma consulta muito grande, altere esse parâmetro antes de executá-lo

SET SESSION group_concat_max_len = 1000000;

Teste:

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
fonte
@ Mihai Talvez você possa me ajudar. Veja isto: stackoverflow.com/questions/51832979/…
Success Man
Pode simplificar 'ifnull(SUM(case when itemname = ''',com ''' then itemvalue end),0) AS ', `para 'SUM(case when itemname = '''com ''' then itemvalue else 0 end) AS ',. Isso gera termos como SUM(case when itemname = 'A' then itemvalue else 0 end) AS 'A'.
ToolmakerSteve
24

Aproveitando a ideia de Matt Fenwick que me ajudou a resolver o problema (muito obrigado), vamos reduzi-lo para apenas uma consulta:

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
andarilho
fonte
14

Eu edito a resposta de Agung Sagita da subconsulta para participar. Eu não tenho certeza sobre quanta diferença entre este caminho 2, mas apenas para outra referência.

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'
haudoing
fonte
2
Possivelmente, essa poderia ser uma solução mais rápida.
jave.web
Acho que não. porque a junção esquerda tem sua própria latência!
Abadis 3/01/19
10

usar subconsulta

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

mas será um problema se a subconsulta resultante mais de uma linha usar mais funções agregadas na subconsulta

Agung Sagita
fonte
4

Minha solução:

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

Produz os resultados esperados no caso enviado.

André Wéber
fonte
3

Eu faço isso para que Group By hostIdele mostre apenas a primeira linha com valores,
como:

A   B  C
1  10
2      3
arpit
fonte
3

Eu descobri uma maneira de tornar meus relatórios convertendo linhas em colunas quase dinâmicos usando consultas simples. Você pode ver e testá-lo online aqui .

O número de colunas da consulta é fixo, mas os valores são dinâmicos e baseados nos valores das linhas. Você pode construí-lo. Então, eu uso uma consulta para criar o cabeçalho da tabela e outra para ver os valores:

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;

Você pode resumir também:

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 |
+--------+------+------+------+

Resultados do RexTester :

Resultados do RexTester

http://rextester.com/ZSWKS28923

Para um exemplo real de uso, este relatório mostra em colunas as horas de partida das chegadas de barco / ônibus com uma programação visual. Você verá uma coluna adicional não usada na última coluna sem confundir a visualização: sistema de venda de passagens on-line e consumidor final e controle de frota - xsl tecnologia - xsl.com.br ** sistema de venda de ingressos on-line e presencial

lynx_74
fonte
3

Se você pudesse usar o MariaDB, há uma solução muito, muito fácil.

Desde o MariaDB-10.02 , foi adicionado um novo mecanismo de armazenamento chamado CONNECT que pode nos ajudar a converter os resultados de outra consulta ou tabela em uma tabela dinâmica, exatamente como você deseja: Você pode conferir os documentos .

Antes de tudo, instale o mecanismo de armazenamento de conexão .

Agora, a coluna dinâmica de nossa tabela está itemnamee os dados de cada item estão localizados na itemvaluecoluna, para que possamos ter a tabela dinâmica de resultados usando esta consulta:

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

Agora podemos selecionar o que queremos entre pivot_table:

select * from pivot_table

Mais detalhes aqui

ako
fonte
1

Esta não é a resposta exata que você está procurando, mas era uma solução que eu precisava no meu projeto e espero que isso ajude alguém. Isso listará 1 a n itens de linha separados por vírgulas. O Group_Concat torna isso possível no 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

Este cemitério tem dois nomes comuns, portanto os nomes são listados em linhas diferentes conectadas por um único ID, mas dois IDs de nome e a consulta produz algo como este

    CemeteryID Cemetery_Name Latitude
    1 Appleton, Sulpher Springs 35.4276242832293

James Humphrey
fonte
-2

Lamento dizer isso e talvez eu não esteja resolvendo seu problema exatamente, mas o PostgreSQL é 10 anos mais antigo que o MySQL e é extremamente avançado em comparação com o MySQL e há muitas maneiras de conseguir isso facilmente. Instale o PostgreSQL e execute esta consulta

CREATE EXTENSION tablefunc;

então voila! E aqui está uma extensa documentação: PostgreSQL: Documentation: 9.1: tablefunc ou esta consulta

CREATE EXTENSION hstore;

então novamente voila! PostgreSQL: Documentação: 9.0: hstore

gdarcan
fonte