LOGO 首页 OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 技术文档 其他文档  
 
网站管理员

SQL Server教程-列转行:UNPIVOT 从固定到动态

admin
2026年6月24日 9:53 本文热度 70

做数据分析的时候,你一定遇到过这种"反着来"的需求——数据库里存的是一行就是一个人,姓名、语文、数学、英语各占一列,清清楚楚。但 BI 报表工具要的是一行一条成绩记录,才能做分组、求和。

姓名
语文
数学
英语
张三
88
92
85
李四
76
88
91

​变成这样:

姓名
科目
成绩
张三
语文
88
张三
数学
92
张三
英语
85
李四
语文
76
...
...
...

这就是列转行,也叫"逆透视"(Unpivot)。

SQL Server 提供了两条路:

  • 静态 UNPIVOT
    :列名固定,直接写
  • 动态 UNPIVOT
    :列名不固定,用动态 SQL 自动拼

用一个完整的学生成绩案例,带你从头到尾搞定。


先建测试数据

CREATE TABLE ScoreTable (     StudentName NVARCHAR(20),     语文 INT,     数学 INT,     英语 INT );  INSERT INTO ScoreTable VALUES ('张三', 88, 92, 85), ('李四', 76, 88, 91), ('王五', 95, 87, 78); 

原始数据(一行一人):

StudentName
语文
数学
英语
张三
88
92
85
李四
76
88
91
王五
95
87
78

转换目标(一行一成绩):

StudentName
科目
成绩
张三
语文
88
张三
数学
92
张三
英语
85
李四
语文
76
...
...
...

方式一:静态 UNPIVOT

适用场景:列名固定,你提前知道要转哪几列。

SELECT StudentName, Subject, Score FROM ScoreTable UNPIVOT (     Score      -- 新生成的"值列"的列名     FOR Subject -- 新生成的"列名列"的列名     IN (语文, 数学, 英语) ) AS up; 

语法拆解

UNPIVOT (     值列名     FOR 列名列名     IN (要转的列1, 列2, ...) ) AS 别名 
  • Score
    :原来各列的值,聚合后放在这一列里
  • FOR Subject
    :原来各列的列名,放在这一列里
  • IN (语文, 数学, 英语)
    :指定哪些列要参与转置

执行结果正是我们要的格式 ✅


静态 UNPIVOT 的致命缺陷

列名是写死的。如果后期加了一列「物理」,或者列名本身就是动态的(比如每个月的销售额列),静态写法就废了——你得一直改 SQL。

这就是动态 UNPIVOT 的用武之地。


方式二:动态 UNPIVOT

核心思路:先查出所有要转的列名,自动拼出 IN (...) 里的内容,再用 sp_executesql 执行。

-- 第一步:获取所有科目列名,拼成 语文,数学,英语 格式 DECLARE @cols NVARCHAR(MAX); DECLARE @sql  NVARCHAR(MAX);  SELECT @cols = STRING_AGG(QUOTENAME(col), ',')               WITHIN GROUP (ORDER BY col) FROM (    -- 从系统表自动获取 ScoreTable 的所有数值列 SELECT @cols = STRING_AGG(QUOTENAME(COLUMN_NAME), ',')               WITHIN GROUP (ORDER BY ORDINAL_POSITION) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = 'ScoreTable'  AND COLUMN_NAME <> 'StudentName';  ) t;  -- 查看拼出来的列名(调试用) --  SELECT @cols;  --> [语文],[数学],[英语] 
 -- 第二步:拼接完整的 UNPIVOT SQL  SET @sql = N' SELECT StudentName, Subject, Score  FROM ScoreTable UNPIVOT ( Score  FOR Subject IN (' + @cols + N') ) AS up;';   -- 第三步:执行动态 SQL  EXEC sp_executesql @sql

这样即使以后加了「物理」列,SQL 也不用改,自动适配 ✅


兼容 SQL Server 2016 及以下(FOR XML PATH 拼列名)

如果你的环境不支持 STRING_AGG,用这段替代:

SELECT @cols = STUFF(     (         SELECT ',' + QUOTENAME(COLUMN_NAME)         FROM INFORMATION_SCHEMA.COLUMNS         WHERE TABLE_NAME = 'ScoreTable'           AND COLUMN_NAME <> 'StudentName'         ORDER BY ORDINAL_POSITION         FOR XML PATH('')     ),     1, 1, ''  -- 去掉开头多余的逗号 ); 

效果与 STRING_AGG 完全一样,只是写法更繁琐。


两种方式 + 手动方式对比

对比项
静态 UNPIVOT
动态 UNPIVOT
代码简洁度
✅ 最简单
⚠️ 需拼 SQL
列名灵活性
❌ 写死
✅ 自动适应
SQL Server 版本
2005+
2005+(STRING_AGG 需 2017+)
推荐指数
⭐⭐⭐(列固定时)
⭐⭐⭐⭐⭐(列动态时)

一个容易踩的坑

UNPIVOT 会自动过滤掉 NULL 值。

-- 王五的英语成绩是 NULL INSERT INTO ScoreTable VALUES ('赵六', 80, 90, NULL);  -- UNPIVOT 后,赵六只有 2 行记录(语文、数学),英语那行直接消失 SELECT StudentName, Subject, Score FROM ScoreTable UNPIVOT (     Score FOR Subject IN (语文, 数学, 英语) ) AS up; 

原因:UNPIVOT 的内部实现等价于把各列值拿出做 UNION ALL,然后过滤掉 NULL。这是 SQL Server 的既定行为,不是 Bug。

如果需要保留 NULL 行,改用 CROSS JOIN + CASE 手动方式,它不会过滤 NULL:

-- 保留 NULL 行的写法 SELECT StudentName, '语文' AS Subject, 语文 AS Score FROM ScoreTable UNION ALL SELECT StudentName, '数学' AS Subject, 数学 AS Score FROM ScoreTable UNION ALL SELECT StudentName, '英语' AS Subject, 英语 AS Score FROM ScoreTable; -- 赵六的英语行会保留,Score 为 NULL 

小结

列转行两步走:

  1. 列名固定
     → 直接用 UNPIVOT,IN 里面写死列名,简单省事
  2. 列名动态
     → 先用 STRING_AGG(或 FOR XML PATH)把列名拼成字符串,再动态执行


记住一个原则:UNPIVOT 会悄悄吃掉 NULL 行,如果业务上 NULL 也有意义,一定要用兜底方案。


该文章在 2026/6/24 9:53:56 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved  粤ICP备13012886号-1  粤公网安备44030602007207号