MySQL - 行到列

Posted

技术标签:

【中文标题】MySQL - 行到列【英文标题】:MySQL - Rows to Columns 【发布时间】:2022-01-15 19:57:00 【问题描述】:

我试图搜索帖子,但我只找到了 SQL Server/Access 的解决方案。我需要 mysql (5.X) 中的解决方案。

我有一个包含 3 列的表(称为历史记录):hostid、itemname、itemvalue。 如果我选择(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,您能否编辑问题以包含确切的查询? 见***.com/a/56670844/3900932 注意:@ako 的链接仅与 MariaDB 相关。 自动生成和运行枢轴:mysql.rjweb.org/doc.php/pivot 【参考方案1】:
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;

【讨论】:

为 'A','B','C' 创建三个不同的行 @Palani:不,它没有。见group by 谢谢,这对我有用!然而,几年后的一个仅供参考,我不得不使用MAX 而不是SUM,因为我的itemValue 是字符串,而不是数值。 如果 itemname 是动态的怎么办?【参考方案2】:

我把它变成Group By hostId 然后它只会显示第一行的值, 喜欢:

A   B  C
1  10
2      3

【讨论】:

【参考方案3】:

使用子查询

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 * 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-values(原因很明显)。


当我必须解决创建数据透视表的问题时,我使用三步过程(可选的第四步)来解决它:

    选择感兴趣的列,即y-valuesx-values 用额外的列扩展基表 -- 每个 x-value 列 对扩展表进行分组和聚合 -- 每个 y 值 一组 (可选)美化聚合表

让我们将这些步骤应用于您的问题,看看我们得到了什么:

第 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 值都有一行。) 好的,我们快到了!我们只需要摆脱那些丑陋的NULLs。

第 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,但也经常使用countmaxmax 经常用于构建已分布在多行中的单行“对象”) 为 y 值使用多列。此解决方案不仅限于对 y 值使用单列 - 只需将额外的列插入 group by 子句(不要忘记 select 它们)

已知限制:

此解决方案不允许数据透视表中有 n 列 - 扩展基表时需要手动添加每个数据透视列。所以对于 5 或​​ 10 个 x 值,这个解决方案很好。 100,不是很好。有一些使用存储过程生成查询的解决方案,但它们很难看且难以正确处理。当数据透视表需要有很多列时,我目前不知道解决此问题的好方法。

【讨论】:

很好的解释,谢谢。可以使用 IFNULL(sum(A), 0) AS A 将第 4 步合并到第 3 步中,为您提供相同的结果,但无需创建另一个表 这是枢轴最神奇的解决方案,但我只是好奇在构成 x 轴的列 itemname 中是否有多个值,就像这里我们只有三个值,即 A、B、C。如果这些值扩展到 A、B、C、D、E、AB、BC、AC、AD、H.....n。那么在这种情况下,解决方案是什么。 确实很好的解释。如果有大神能进一步详细说明,解决需要手动添加列的问题,那就太好了 @WhiteBig 请看一下日期——这个 *** 答案是在那篇博文之前 1.5 年写的。也许你应该让博客给我的功劳。 很好的答案!如果您的列类型是字符串,则必须使用 MAX() 而不是 SUM()【参考方案5】:

利用 Matt Fenwick 帮助我解决问题的想法(非常感谢),让我们将其减少到只有一个查询:

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

【讨论】:

【参考方案6】:

我从子查询中编辑Agung Sagita 的答案以加入。 我不确定这两种方式之间有多大区别,但仅供参考。

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'

【讨论】:

谢谢。这很好,很简单,但几乎可以工作。它为每个具有相同列和值的 hostid 返回重复的 3 行,所以我必须删除另外两个。【参考方案7】:

这不是您正在寻找的确切答案,但它是我在项目中需要的解决方案,希望这对某人有所帮助。这将列出以逗号分隔的 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

这个墓地有两个共同的名字,所以这些名字被列在不同的行中,由一个 id 但两个名字 id 连接,查询会产生类似这样的结果 CemeteryID     Cemetery_Name             纬度 1                   阿普尔顿,Sulpher Springs  35.4276242832293

【讨论】:

【参考方案8】:

我的解决方案:

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

它在提交的案例中产生预期的结果。

【讨论】:

【参考方案9】:

另一个选项,如果您有许多需要旋转的项目特别有用,是让 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 也许你可以帮助我。看看这个:***.com/questions/51832979/… 可以将'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' 这样的术语。 我们可以将其构建为视图吗?【参考方案10】:

我想出了一种方法,让我的报告使用简单的查询将行转换为列几乎是动态的。您可以查看并测试它online here。

查询的列数是固定的,但值是动态的并基于行的值。你可以构建它所以,我使用一个查询来构建表头,另一个来查看值:

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的结果:

http://rextester.com/ZSWKS28923

对于一个真实的使用示例,下面的这份报告以列的形式显示了船只/公共汽车的出发到达时间以及可视化时间表。您将看到最后一列未使用的一个附加列,而不会混淆可视化: ** 售票系统的在线售票和现场售票

【讨论】:

【参考方案11】:

我很抱歉这么说,也许我没有完全解决您的问题,但 PostgreSQL 比 MySQL 早 10 年,并且与 MySQL 相比非常先进,并且有很多方法可以轻松实现这一点。安装 PostgreSQL 并执行这个查询

CREATE EXTENSION tablefunc;

然后瞧!这里有大量文档:PostgreSQL: Documentation: 9.1: tablefunc 或这个查询

CREATE EXTENSION hstore;

然后再次瞧! PostgreSQL: Documentation: 9.0: hstore

【讨论】:

【参考方案12】:

如果你可以使用MariaDB,有一个非常简单的解决方案。

MariaDB-10.02 以来,添加了一个名为 CONNECT 的新存储引擎,它可以帮助我们将另一个查询或表的结果转换为数据透视表,就像您想要的那样: 你可以看看the docs。

首先install the connect storage engine。

现在我们表的数据透视表是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

More details here

【讨论】:

以上是关于MySQL - 行到列的主要内容,如果未能解决你的问题,请参考以下文章

在oracle SQL中将行移动到列和列到行

行到列的总和

调整多级行到列查询

使用 flex 实现四列到列列表的反馈 - 第 2 轮

sql server中的表行到列

Oracle 行到列的转换