【问题标题】:How to pivot tables in MySQL如何在 MySQL 中进行数据透视表
【发布时间】:2023-03-23 21:15:02
【问题描述】:

我有一个关于如何在 MySQL 中旋转表的问题。 我有一个数据集,列如下:

ID   Name     job_title
1    Sam       Fireman
2    Tomas     Driver
3    Peter     Fireman
4    Lisa      Analyst
5    Marcus    Postman
6    Stephan   Analyst
7    Mary      Research Manager
8    Albert    Analyst
9    Chen      Driver
...etc...

我想生成一个这样的表:

Fireman  Driver   Analyst  Postman   Research Manager ...
Sam     Tomas     Lisa     Marcus     Mary
Peter   Chen      Stephan  (someone)  (someone)...
....etc...

因为,这只是数据集中的一个样本,所以我可能不知道数据集中有多少不同的职位。目标是在不同的职位列中列出每个人。

有什么方法可以做到吗?或者是否可以在 MySQL 中生成这样的表? 一位工程师告诉我,它可以通过创建视图来完成,但我不知道如何。看了几本书,还是一头雾水。

欢迎任何想法和 SQL 查询指南!

【问题讨论】:

  • 您发布的所需输出数据不是透视数据的示例,因为同一行中的值彼此没有关系(例如 Peter 与 Chen 无关) .
  • @Dai 感谢您的 cmets。我同意你的看法,但我怎样才能生成我在帖子中列出的表格。仅供讨论。我们可以通过使用创建视图语句来做到这一点吗?我真的很怀疑。
  • 如果您首先知道所有不同 job_title 值的值,则可以这样做,否则您将不得不使用动态 SQL,而您不能在 VIEW 中使用它。
  • 如果您的数据库支持ROW_NUMBER()FULL OUTER JOIN,那么压缩行是可能的,但是MySQL 目前都不支持,这使得它变得更加困难。不相关的 MySQL 8 确实添加了 ROW_NUMBER
  • 所以你问的是让每个表格列像job_titleName 每个都是一行值,对吗?您希望输出将特定列中的所有值转换为一行,对吗?在你说“我想生成一个像这样的表:”的地方,你似乎有第 1 行示例是名为 job_title 的列的值,对吧?

标签: mysql sql pivot-table


【解决方案1】:

有 3 件事需要考虑 1) 如何动态生成一堆 max(case when 2) 分配一些东西来分组 case when's by - 在这种情况下,我使用变量生成行号 3) 你的一些职位名称包含空格,我将其删除以生成列标题

set @sql = 
            (select concat('select ', gc,            ' from 
             (select name,job_title,
                if (job_title <> @p, @rn:=1 ,@rn:=@rn+1) rn,
                @p:=job_title p
                from t
                cross join (select @rn:=0,@p:=null) r
                order by job_title
              ) s group by rn;') from
            (select 
                group_concat('max(case when job_title = ', char(39),job_title ,char(39),' then name else char(32) end ) as ',replace(job_title,char(32),'')) gc
                from
                (
                select distinct job_title from t
                ) s
                ) t
             )
;           

生成此 sql 代码

select max(case when job_title = 'Fireman' then name else char(32) end ) as Fireman,
        max(case when job_title = 'Driver' then name else char(32) end ) as Driver,
        max(case when job_title = 'Analyst' then name else char(32) end ) as Analyst,
        max(case when job_title = 'Postman' then name else char(32) end ) as Postman,
        max(case when job_title = 'Research Manager' then name else char(32) end ) as ResearchManager
         from 
             (select name,job_title,
                if (job_title <> @p, @rn:=1 ,@rn:=@rn+1) rn,
                @p:=job_title p
                from t
                cross join (select @rn:=0,@p:=null) r
                order by job_title
              ) s group by rn;

哪些可以提交动态sql

prepare sqlstmt from @sql;
execute sqlstmt;
deallocate prepare sqlstmt;

结果

+---------+--------+---------+---------+-----------------+
| Fireman | Driver | Analyst | Postman | ResearchManager |
+---------+--------+---------+---------+-----------------+
| Sam     | Tomas  | Lisa    | Marcus  | Mary            |
| Peter   | Chen   | Stephan |         |                 |
|         |        | Albert  |         |                 |
+---------+--------+---------+---------+-----------------+
3 rows in set (0.00 sec)

【讨论】:

    【解决方案2】:

    我在 HackerRank 上也遇到过这个问题。虽然我认为group_concat 的答案非常好,并且通常用于早期版本的 MySql 的此类关键情况,但我发现concatgroup_concat 可能难以阅读和理解。

    如果您的 MySql 版本支持 窗口函数,那么您可以使用临时表来解决这个问题,因为 MySql 不支持外连接。您需要为每个数据透视列创建一个单独的临时表,以避免 Window function is not allowed in window specification 错误:

    use test;
    drop table if exists occupations;
    create table if not exists occupations  (
        name varchar(50)
        ,occupation varchar(50)
    );
    insert into occupations (name, occupation) select 'Samantha', 'Doctor'
        union all select 'Julia', 'Actor'
        union all select 'Maria', 'Actor'
        union all select 'Meera', 'Singer'
        union all select 'Ashley', 'Professor'
        union all select 'Kelly', 'Professor'
        union all select 'Christeen', 'Professor'
    ;
    -- the way to approach this in mysql is to create a temp table with ordinals.
    -- then upsert with four queries using row_number()
    -- nb full join not supported. let's try temp table
    drop table if exists doctors;
    create temporary table doctors
    (
        name varchar(50)
        ,occupation varchar(50)
        ,ordinal int
    );
    insert into doctors
        select 
            name
            ,occupation
            ,row_number() over (partition by occupation order by name) as ordinal
        from occupations
        where occupation = 'Doctor'
    ;
    drop table if exists actors;
    create temporary table actors
    (
        name varchar(50)
        ,occupation varchar(50)
        ,ordinal int
    );
    insert into actors
        select 
            name
            ,occupation
            ,row_number() over (partition by occupation order by name) as ordinal
        from occupations
        where occupation = 'Actor'
    ;
    drop table if exists professors;
    create temporary table professors
    (
        name varchar(50)
        ,occupation varchar(50)
        ,ordinal int
    );
    insert into professors
        select 
            name
            ,occupation
            ,row_number() over (partition by occupation order by name) as ordinal
        from occupations
        where occupation = 'Professor'
    ;
    drop table if exists singers;
    create temporary table singers
    (
        name varchar(50)
        ,occupation varchar(50)
        ,ordinal int
    );
    insert into singers
        select 
            name
            ,occupation
            ,row_number() over (partition by occupation order by name) as ordinal
        from occupations
        where occupation = 'Singer'
    ;
    
    -- upsert: update if not exists
    drop table if exists results;
    create temporary table results
    (
        singer varchar(50)
        ,actor varchar(50)
        ,doctor varchar(50)
        ,professor varchar(50)
        ,ordinal int primary key
    );
    insert into results (singer, ordinal) 
        select name, ordinal from singers
    on duplicate key update singer = name
    ;
    insert into results (actor, ordinal) 
        select name, ordinal from actors
    on duplicate key update actor = name
    ;
    insert into results (doctor, ordinal) 
        select name, ordinal from doctors
    on duplicate key update doctor = name
    ;
    insert into results (professor, ordinal) 
        select name, ordinal from professors
    on duplicate key update professor = name
    ;
    select singer, actor, doctor, professor from results;
    

    附言。我不得不不同意早期的 cmets:这是一个支点。我们将行投影到列中,行是职业和序数的投影。

    【讨论】:

      【解决方案3】:

      最好从结果开始,尽量映射到原表。基本上结果表的每一行都应该在原始表中的同一组中。并且CTE表分组和排名窗口函数按名称创建分组顺序。

      with grouping as (
      select
      Name,
      job_title,
      rank() over (partition by job_title order by name) as rnk
      from jobs
      )
      
      select
      group_concat(if(g.job_title = 'Fireman', g.Name, NULL)) as 'Fireman',
      group_concat(if(g.job_title = 'Driver',g.Name, NULL)) as 'Driver',
      group_concat(if(g.job_title = 'Analyst', g.Name, NULL)) as 'Analyst',
      group_concat(if(g.job_title = 'Research Manager', g.Name, NULL)) as 'Research Manager'
      from grouping g
      group by g.rnk
      order by g.rnk
      

      【讨论】:

        【解决方案4】:

        您发布的所需输出数据不是透视数据的示例,因为同一行中的值彼此之间没有关系,听起来您只是想要一个每个人的紧凑表示-细胞基础。这使它成为一个视图级别的问题,不应该在 SQL 中执行,而应该在您的视图级别中执行(可能是 PHP 网页,因为您使用的是 MySQL)。

        您的输出数据是面向列的,而不是面向行的,但是 HTML 表格(以及用于其他平台的大多数数据网格组件,如 WinForms、Java 和 WPF)是面向行的,因此您需要考虑如何去做它。

        假设您以 HTML 为目标,并考虑了所需的面向行与面向列的转换,请试试这个(伪代码)

        define type DBResultRow {
            id: int,
            name: string,
            job_title: string
        }
        
        let rows : List<DBResultRow> = // get rows from your view, no changes to SQL required
        
        let jobTitles : List<String>
        let jobTitleMap : Map<String,Int32>
        let outputTable : List<List<String>>
        
        foreach( person: DBResultRow in rows )
        {
            let columnIdx = jobTitleMap[ person.job_title ];
            if( !columnIdx )
            {
                jobTitles.Add( person.job_title );
                columnIdx = jobTitles.Count - 1;
                jobTitleMap[ person.job_title, columnIdx ];
            }
        
            outputTable[ columnIdx ].Add( person.name );
        }
        
        let longestColumnLength = outputTable.Select( col => col.Count ).Max();
        

        然后渲染成 HTML:

        <table>
            <thead>
                <tr>
        foreach( jobTitle: String in jobTitles )
        {
                    <th><%= jobTitle #></th>
        }
                </tr>
            </thead>
            <tbody>
        for( row = 0; row < longestColumnLength; row++ )
        {
                <tr>
            for( col = 0; col < jobTitles.Count; col++ )
            {
                if( row > outputTable[ col ].Count )
                {
                    <td></td>
                }
                else
                {
                    <td><%= outputTable[ col ][ row ] %></td>
                }
            }
                </tr>
        }
            </tbody>
        </table>
        

        【讨论】:

        • 同一行中的井值可以反映集合中给定结果的出现。无论如何,我同意最好的解决方案是使用某种应用程序级代码
        【解决方案5】:

        看JSON服务(JSON_OBJECTAGG,JSON_OBJECT),在java中可以用盆对象映射(Jackson)解析。

        select xyz, JSON_OBJECTAGG( a, b) as pivit_point
        from ... group by xyz;
        

        【讨论】:

          猜你喜欢
          • 2022-08-18
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-08-28
          • 1970-01-01
          • 2020-11-03
          相关资源
          最近更新 更多