在 MySQL 中取消透视多个列

Posted

技术标签:

【中文标题】在 MySQL 中取消透视多个列【英文标题】:Unpivot Multiple Columns in MySQL 【发布时间】:2014-08-14 23:52:48 【问题描述】:

我有一些非规范化数据,我正试图对其列进行反透视,希望大家能帮助我找出最好的方法。我已经使用多个联合语句完成了这项工作,但我希望做的是创建一个动态查询,随着更多列添加到表中,它可以一遍又一遍地执行此操作。我的数据看起来像这样:(数字列一直到 50)

| Code  | Desc  | Code_0 | Desc_0| Period|    1    |    2   |    3    |    4     |  
|-------|-------|--------|-------|-------|---------|--------|---------|----------|
| NULL  | NULL  |  NULL  |  NULL |  Date |29-Nov-13|6-Dec-13|13-Dec-13| 20-Dec-13|  
|CTR07  |Risk   |  P1    | Phase1|  P    |   0.2   |  0.4   |   0.6   |    1.1   |         
|CTR07  |Risk   |  P1    | Phase1|  F    |   0.2   |  0.4   |   0.6   |    1.1   |          
|CTR07  |Risk   |  P1    | Phase1|  A    |   0.2   |  0.4   |   0.6   |    1.1   |
|CTR08  |Oper   |  P1    | Phase1|  P    |   0.6   |  0.6   |   0.9   |    2.7   |
|CTR08  |Oper   |  P1    | Phase1|  F    |   0.6   |  0.6   |   0.9   |    2.7   |
|CTR08  |Oper   |  P1    | Phase1|  A    |   0.6   |  0.6   |   0.9   |    2.7   |

列标题是最上面的行。正如您在查看数据时所看到的,有一些奇怪的事情需要处理。

日期字段开始之前的前四个 NULL 列是一个问题。具有数字标题 (1-50) 的每一列代表一周。这样做的问题是,每周不仅有日期字段,而且同一列中还有该周的百分比值。我想让它向下旋转,所以它看起来像这样:

| Code  | Desc  |Code_0 |Desc_0 | Period| Date    |Percent|  
|-------|-------|-------|-------|-------|---------|-------|
|CTR07  | Risk  |  P1   | Phase |   P   | 11/29/13| 0.2   |   
|CTR07  | Risk  |  P1   | Phase1|   F   | 11/29/13| 0.2   |
|CTR07  | Risk  |  P1   | Phase1|   A   | 11/29/13| 0.2   |
|CTR08  | Oper. |  P1   | Phase1|   P   | 11/29/13| 0.6   |

每周的日期在它自己的列中,百分比按各自的日期分组。

由不同的代码、描述、CODE_0、期间和日期键入。我想将日期与数字列中的百分比分开,然后将数字列放入按日期连接的自己的列中。正如我之前所说,我已经使用 UNION 语句静态地完成了它,但是我想编写某种查询来随着表的扩展而动态地完成它。任何帮助将不胜感激。如果您需要任何其他信息,请告诉我,这是我在 *** 上的第一个问题,我有两个不错的屏幕截图向您展示,但我在这次交流中还不到 10 岁。仅适用于科幻和奇幻。我知道,对吧?

我在 union 中用于静态创建底部表的代码:

select `Code`, `Desc`, `Code_0`, `Desc_0`, `Period`, (select STR_TO_DATE(`1`, '%d%b%y') from combined_complete where `1` = '29Nov13') as `Date`, `1` as `Percent`
from combined_complete
where period <> 'Date'
union
select `Code`, `Desc`, `Code_0`, `Desc_0`, `Period`, (select STR_TO_DATE(`2`, '%d%b%y') from combined_complete where `2` = '06Dec13') as `Date`, `2`
from combined_complete
where period <> 'Date'
union
select `Code`, `Desc`, `Code_0`, `Desc_0`, `Period`, (select STR_TO_DATE(`3`, '%d%b%y') from combined_complete where `3` = '13Dec13') as `Date`, `3`
from combined_complete
where period <> 'Date'
union
select `Code`, `Desc`, `Code_0`, `Desc_0`, `Period`, (select STR_TO_DATE(`4`, '%d%b%y') from combined_complete where `4` = '20Dec13') as `Date`, `4`
from combined_complete
where period <> 'Date'

【问题讨论】:

老实说,我不会尝试使用“纯”SQL 来执行此操作...我宁愿使用另一种编程语言(python、java 等)来读取数据并将其存储在新表 无论如何不要使用UNION请使用UNION ALL,这里不需要联合本身,并且比union all 【参考方案1】:

对于这个建议,我创建了一个简单的 50 行表,名为 TransPoser,在 mysql 或您的数据库中可能已经有一个整数表可用,但您想要类似的东西,将您的数字 1 到 N列编号。

然后,使用该表,交叉连接到您的非规范化表(我称之为 BadTable),但将其限制在第一行。然后使用一组 case 表达式,我们将pivot 那些日期字符串放入一列。如果需要,我们可以转换为适当的日期(我会建议它,但没有包括它)。

这个小转置随后用作主查询中的派生表。

主查询忽略第一行,但也使用交叉连接将所有原始行强制为 50 行(或本示例中的 4 行)。然后将此笛卡尔积加入到上面讨论的派生表中以提供日期。然后是另一组 case 表达式,将百分比转换为一列,与日期和各种代码对齐。

示例结果(来自示例数据),手动添加空行:

| N |  CODE | DESC | CODE_0 | DESC_0 |   THEDATE | PERCENTAGE |
|---|-------|------|--------|--------|-----------|------------|
| 1 | CTR07 | Risk |     P1 | Phase1 | 29-Nov-13 |        0.2 |
| 1 | CTR07 | Risk |     P1 | Phase1 | 29-Nov-13 |        0.2 |
| 1 | CTR07 | Risk |     P1 | Phase1 | 29-Nov-13 |        0.2 |
| 1 | CTR08 | Oper |     P1 | Phase1 | 29-Nov-13 |        0.6 |
| 1 | CTR08 | Oper |     P1 | Phase1 | 29-Nov-13 |        0.6 |
| 1 | CTR08 | Oper |     P1 | Phase1 | 29-Nov-13 |        0.6 |

| 2 | CTR07 | Risk |     P1 | Phase1 |  6-Dec-13 |        0.4 |
| 2 | CTR07 | Risk |     P1 | Phase1 |  6-Dec-13 |        0.4 |
| 2 | CTR07 | Risk |     P1 | Phase1 |  6-Dec-13 |        0.4 |
| 2 | CTR08 | Oper |     P1 | Phase1 |  6-Dec-13 |        0.6 |
| 2 | CTR08 | Oper |     P1 | Phase1 |  6-Dec-13 |        0.6 |
| 2 | CTR08 | Oper |     P1 | Phase1 |  6-Dec-13 |        0.6 |

| 3 | CTR07 | Risk |     P1 | Phase1 | 13-Dec-13 |        0.6 |
| 3 | CTR07 | Risk |     P1 | Phase1 | 13-Dec-13 |        0.6 |
| 3 | CTR07 | Risk |     P1 | Phase1 | 13-Dec-13 |        0.6 |
| 3 | CTR08 | Oper |     P1 | Phase1 | 13-Dec-13 |        0.9 |
| 3 | CTR08 | Oper |     P1 | Phase1 | 13-Dec-13 |        0.9 |
| 3 | CTR08 | Oper |     P1 | Phase1 | 13-Dec-13 |        0.9 |

| 4 | CTR07 | Risk |     P1 | Phase1 | 20-Dec-13 |        1.1 |
| 4 | CTR07 | Risk |     P1 | Phase1 | 20-Dec-13 |        1.1 |
| 4 | CTR07 | Risk |     P1 | Phase1 | 20-Dec-13 |        1.1 |
| 4 | CTR08 | Oper |     P1 | Phase1 | 20-Dec-13 |        2.7 |
| 4 | CTR08 | Oper |     P1 | Phase1 | 20-Dec-13 |        2.7 |
| 4 | CTR08 | Oper |     P1 | Phase1 | 20-Dec-13 |        2.7 |

查询:

select
       n.n
     , b.Code
     , b.Desc
     , b.Code_0
     , b.Desc_0
     , T.theDate
     , case
            when n.n =  1 then `1`
            when n.n =  2 then `2`
            when n.n =  3 then `3`
            when n.n =  4 then `4`
         /* when n.n =  5 then `5` */
         /* when n.n = 50 then `50`*/
       end as Percentage
from BadTable as B
cross join (select N from TransPoser where N < 5) as N
inner join (
            /* transpose just the date row */
            /* join back vis the number given to each row */
            select
                    n.n
                  , case
                        when n.n =  1 then `1`
                        when n.n =  2 then `2`
                        when n.n =  3 then `3`
                        when n.n =  4 then `4`
                     /* when n.n =  5 then `5` */
                     /* when n.n = 50 then `50`*/
                   end as theDate
            from BadTable as B
            cross join (select N from TransPoser where N < 5) as N
            where b.code is null
            and b.Period = 'Date'
           ) as T on N.N = T.N
where b.code is NOT null
and b.Period <> 'Date'
order by
       n.n
     , b.code
;

以上内容见this SQLFIDDLE

由于一个问题恕我直言,期望一个完全准备好的可执行交付物确实是不公平的 - 它是“延长友谊”。但是将上述查询变成动态查询并不难。这有点“乏味”,因为语法有点棘手。我对 MySQL 不是很有经验,但我会这样做:

set @numcols := 4;
set @casevar := '';

set @casevar := (
                  select 
                  group_concat(@casevar
                                       ,'when n.n =  '
                                       , n.n
                                       ,' then `'
                                       , n.n
                                       ,'`'
                                      SEPARATOR ' ')
                  from TransPoser as n
                  where n.n <= @numcols
                 )
;


set @sqlvar := concat(
          'SELECT n.n , b.Code , b.Desc , b.Code_0 , b.Desc_0 , T.theDate , CASE '
        , @casevar
        , ' END AS Percentage FROM BadTable AS B CROSS JOIN (SELECT N FROM  TransPoser WHERE N <='
        , @numcols
        , ') AS N INNER JOIN ( SELECT n.n , CASE '
        , @casevar                                                                                                       
        , ' END AS theDate FROM BadTable AS B CROSS JOIN (SELECT N FROM  TransPoser WHERE N <='
        , @numcols
        , ') AS N WHERE b.code IS NULL '
        , ' AND b.Period = ''Date'' ) AS T ON N.N = T.N WHERE b.code IS NOT NULL AND b.Period <> ''Date'' ORDER BY n.n , b.code ' 
        );

PREPARE stmt FROM @sqlvar;
EXECUTE stmt;

Demo of the dynamic approach

【讨论】:

这是一个很好的答案,但它仍然将我限制为 50 行。有没有办法使这种动态化,以便如果来自源的列超过 50 列,它也会取消旋转其他列?这真的很接近我正在寻找的东西。这只是动态问题。 重新阅读您的问题,然后“(数值列一直到 50)”如果您需要超过 50 的动态,为什么将 50 设为固定数字? 重点是,是的,它可以动态化,但我不是来为您编写整个解决方案的。我提供的要求取消透视的技术。你没写过“动态sql”吗? 我已经为你提供了一个动态版本。 我真的很感激。我在问题中确实提到过我之前使用联合语句完成了非动态版本,这就是我正在寻找动态解决方案的原因。你的回答绝对是最好的,所以我对你的出色表现给予充分肯定。再次感谢您为我写出来,我很了解 SQL,但是动态或 PL/SQL 是我没有太多经验的东西。【参考方案2】:

一个粗略的方法是:

    通过LIKE-Pattern 或ORDINAL_POSITION &gt; 5 从表的INFORMATION_SCHEMA.COLUMNS 检索列名 为每一列执行prepared statement,将前五个列和数字列插入到新表中 在插入过程中,还要通过 NULL 值子选择日期列的值

【讨论】:

你有一些代码示例来执行上述操作吗?我知道你所暗示的背后的理论,我只是很难想象它是如何结合在一起的。提前致谢。

以上是关于在 MySQL 中取消透视多个列的主要内容,如果未能解决你的问题,请参考以下文章

取消透视多个列

Oracle 11g:取消透视多个列并包含列名

取消透视到 Oracle 中的多个列

将日期列取消透视到 Oracle 中复杂查询的单个列

尝试在 Oracle SQL 中取消透视列时出错

取消透视多个变量 Pandas Dataframe