MySQL - Filas a columnas

188

Intenté buscar publicaciones, pero solo encontré soluciones para SQL Server / Access. Necesito una solución en MySQL (5.X).

Tengo una tabla (llamada historial) con 3 columnas: hostid, itemname, itemvalue.
Si hago un select ( select * from history), volverá

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

¿Cómo consulto la base de datos para devolver algo como

   +--------+------+-----+-----+
   | hostid |   A  |  B  |  C  |
   +--------+------+-----+-----+
   |   1    |  10  |  3  |  0  |
   +--------+------+-----+-----+
   |   2    |   9  |  0  |  40 |
   +--------+------+-----+-----+
Bob Rivers
fuente
@Rob, ¿puede editar la pregunta para incluir la consulta exacta?
Johan
NOTA: el enlace de @ako solo es relevante para MariaDB.
ToolmakerSteve
Generación automática y ejecución de un pivote: mysql.rjweb.org/doc.php/pivot
Rick James

Respuestas:

276

Voy a agregar una explicación un poco más larga y más detallada de los pasos a seguir para resolver este problema. Pido disculpas si es demasiado largo.


Comenzaré con la base que has dado y la usaré para definir un par de términos que usaré para el resto de esta publicación. Esta será la tabla base :

select * from history;

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

Este será nuestro objetivo, la bonita tabla dinámica :

select * from history_itemvalue_pivot;

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

Los valores en la history.hostidcolumna se convertirán en valores y en la tabla dinámica. Los valores en la history.itemnamecolumna se convertirán en valores x (por razones obvias).


Cuando tengo que resolver el problema de crear una tabla dinámica, lo abordo mediante un proceso de tres pasos (con un cuarto paso opcional):

  1. seleccionar las columnas de interés, es decir , valores y y valores x
  2. extienda la tabla base con columnas adicionales, una para cada valor de x
  3. agrupe y agregue la tabla extendida: un grupo para cada valor de y
  4. (opcional) prettify la tabla agregada

Apliquemos estos pasos a su problema y veamos qué obtenemos:

Paso 1: seleccione columnas de interés . En el resultado deseado, hostidproporciona los valores y y itemnameproporciona los valores x .

Paso 2: extienda la tabla base con columnas adicionales . Por lo general, necesitamos una columna por valor de x. Recuerde que nuestra columna de valor x es 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 |
+--------+----------+-----------+------+------+------+

Tenga en cuenta que no cambiamos el número de filas, solo agregamos columnas adicionales. También tenga en cuenta el patrón de NULLs: una fila con itemname = "A"tiene un valor no nulo para la nueva columna Ay valores nulos para las otras columnas nuevas.

Paso 3: agrupe y agregue la tabla extendida . Necesitamos group by hostid, ya que proporciona los 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 |
+--------+------+------+------+

(Tenga en cuenta que ahora tenemos una fila por valor de y). Bien, ¡ya casi llegamos! Solo tenemos que deshacernos de esos feos NULL.

Paso 4: prettify . Simplemente vamos a reemplazar cualquier valor nulo con ceros para que el conjunto de resultados sea más agradable de ver:

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

Y hemos terminado: hemos creado una tabla dinámica agradable y bonita con MySQL.


Consideraciones al aplicar este procedimiento:

  • qué valor usar en las columnas adicionales. Yo usé itemvalueen este ejemplo
  • qué valor "neutral" usar en las columnas adicionales. Solía NULL, pero también podría ser 0o "", dependiendo de tu situación exacta
  • qué función agregada usar al agrupar. Solía sum, pero county maxtambién se utilizan a menudo ( maxse utiliza a menudo en la construcción de una sola hilera "objetos" que habían sido repartidos en muchas filas)
  • usando múltiples columnas para valores de y. Esta solución no se limita al uso de una sola columna para los valores y, simplemente conecte las columnas adicionales a la group bycláusula (y no se olvide de selectellas)

Limitaciones conocidas:

  • Esta solución no permite n columnas en la tabla dinámica: cada columna dinámica debe agregarse manualmente al extender la tabla base. Entonces, para 5 o 10 valores x, esta solución es buena. Por 100, no tan agradable. Hay algunas soluciones con procedimientos almacenados que generan una consulta, pero son feas y difíciles de corregir. Actualmente no conozco una buena manera de resolver este problema cuando la tabla dinámica necesita tener muchas columnas.
Matt Fenwick
fuente
25
+1 Esta es, con mucho, la mejor / más clara explicación de tablas dinámicas / tablas cruzadas en MySQL que he visto
cameron.bracken
66
Excelente explicación, gracias. El paso 4 podría fusionarse con el paso 3 utilizando IFNULL (suma (A), 0) AS A, que le da el mismo resultado pero sin la necesidad de crear otra tabla
nealio82
1
Fue la solución más sorprendente para pivote, pero tengo curiosidad por saber si en la columna nombre del elemento que forma el eje x tiene múltiples valores, como aquí solo tenemos tres valores, es decir, A, B, C. Si estos valores se extienden a A, B, C, D, E, AB, BC, AC, AD, H ..... n. entonces en este caso cuál sería la solución.
Deepesh
1
Esta debería ser la respuesta aceptada aquí. Es mucho más detallado, útil y explica cómo entenderlo en lugar de simplemente vincularlo a un artículo como el actualmente aceptado
EdgeCaseBerg
2
@WhiteBig, por favor, eche un vistazo a las fechas: esta respuesta de StackOverflow se escribió 1.5 años antes de la publicación del blog. Quizás deberías pedirle al blog que me acredite.
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
fuente
Crea tres filas diferentes, para 'A', 'B', 'C'
Palani
1
@Palani: No, no lo hace. Ver group by.
ruakh
¡Gracias, esto funcionó para mí! Sin embargo, solo un FYI con un par de años de retraso, tuve que usar en MAXlugar de SUMporque mis itemValueson cadenas, no valores numéricos.
Merricat
33

Otra opción, especialmente útil si tiene muchos elementos que necesita pivotar es dejar que mysql construya la consulta por usted:

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 Se agregaron algunos valores adicionales para verlo funcionar

GROUP_CONCAT tiene un valor predeterminado de 1000, por lo que si tiene una consulta realmente grande, cambie este parámetro antes de ejecutarlo

SET SESSION group_concat_max_len = 1000000;

Prueba:

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
fuente
@Mihai Quizás puedas ayudarme. Mira esto: stackoverflow.com/questions/51832979/…
Success Man
Se puede simplificar 'ifnull(SUM(case when itemname = ''',con ''' then itemvalue end),0) AS ', `a 'SUM(case when itemname = '''con ''' then itemvalue else 0 end) AS ',. Esto genera términos como SUM(case when itemname = 'A' then itemvalue else 0 end) AS 'A'.
ToolmakerSteve
24

Aprovechando la idea de Matt Fenwick que me ayudó a resolver el problema (muchas gracias), reduzcamoslo a una sola 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
jalber
fuente
14

Edito la respuesta de Agung Sagita de subconsulta para unirme. No estoy seguro de cuánta diferencia entre esta 2 vías, pero solo para otra referencia.

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
fuente
2
Posiblemente, esta podría ser una solución más rápida.
jave.web
No lo creo. ¡porque left join tiene su propia latencia!
Abadis
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

pero será un problema si la subconsulta que resulta en más de una fila, utiliza una función agregada adicional en la subconsulta

Agung Sagita
fuente
4

Mi solución :

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

Produce los resultados esperados en el caso presentado.

André Wéber
fuente
3

Hago eso y Group By hostIdluego mostrará solo la primera fila con valores,
como:

A   B  C
1  10
2      3
arpit
fuente
3

Descubro una forma de hacer que mis informes conviertan filas en columnas casi dinámicas mediante consultas simples. Puedes verlo y probarlo en línea aquí .

El número de columnas de consulta es fijo, pero los valores son dinámicos y se basan en valores de filas. Puede compilarlo Entonces, utilizo una consulta para compilar el encabezado de la tabla y otra para ver los 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;

También puedes resumirlo:

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 de RexTester :

Resultados de RexTester

http://rextester.com/ZSWKS28923

Para un ejemplo real de uso, este informe muestra en columnas las horas de salida y arribo de un bote / autobús con un horario visual. Verá una columna adicional que no se utilizó en la última columna sin confundir la visualización: sistema venda de passns en línea e consumidor final y control de frota - xsl tecnologia - xsl.com.br ** sistema de venta de boletos para vender boletos en línea y presenciales

lynx_74
fuente
3

Si pudieras usar MariaDB, hay una solución muy fácil.

Desde MariaDB-10.02 se ha agregado un nuevo motor de almacenamiento llamado CONNECT que puede ayudarnos a convertir los resultados de otra consulta o tabla en una tabla dinámica, tal como lo desea: puede echar un vistazo a los documentos .

En primer lugar, instale el motor de almacenamiento Connect .

Ahora la columna dinámica de nuestra tabla es itemnamey los datos de cada elemento se encuentran en la itemvaluecolumna, por lo que podemos obtener la tabla dinámica de resultados utilizando esta consulta:

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

Ahora podemos seleccionar lo que queremos de pivot_table:

select * from pivot_table

Más detalles aquí

ako
fuente
1

Esta no es la respuesta exacta que está buscando, pero fue una solución que necesitaba en mi proyecto y espero que esto ayude a alguien. Esto enumerará de 1 a n elementos de fila separados por comas. Group_Concat lo hace posible en 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 cementerio tiene dos nombres comunes, por lo que los nombres se enumeran en diferentes filas conectadas por una sola identificación pero dos identificadores de nombre y la consulta produce algo como esto

    CemeteryID Cemetery_Name Latitude
    1 Appleton, Sulpher Springs 35.4276242832293

James Humphrey
fuente
-2

Lamento decir esto y tal vez no estoy resolviendo su problema exactamente, pero PostgreSQL es 10 años mayor que MySQL y es extremadamente avanzado en comparación con MySQL y hay muchas maneras de lograrlo fácilmente. Instala PostgreSQL y ejecuta esta consulta

CREATE EXTENSION tablefunc;

entonces voila! Y aquí hay una extensa documentación: PostgreSQL: Documentación: 9.1: tablefunc o esta consulta

CREATE EXTENSION hstore;

entonces otra vez voila! PostgreSQL: Documentación: 9.0: hstore

gdarcan
fuente