Skip to content
Go back

SQL行转列——CASE WHEN + MAX + GROUP BY的通用范式

SQL 行转列:CASE WHEN + MAX + GROUP BY

一句话结论(30s)

行转列用 CASE WHEN + MAX + GROUP BY 三段式:CASE WHEN 造条件列、GROUP BY 把多行压成一行、MAX 忽略 NULL 挑出唯一有效值——因为 MySQL/PostgreSQL 没有 Oracle/SQL Server 的 PIVOT 关键字,这三件套是唯一所有关系库都能跑的标准解法。

核心原理(2min)

底层深入(5-10min)

问题

成绩表:
name  | subject | score
------|---------|------
张三  | 语文    | 90
张三  | 数学    | 85
张三  | 英语    | 92

期望输出:
name  | 语文 | 数学 | 英语
------|------|------|-----
张三  | 90   | 85   | 92

通用解法

SELECT name,
  MAX(CASE WHEN subject = '语文' THEN score END) AS 语文,
  MAX(CASE WHEN subject = '数学' THEN score END) AS 数学,
  MAX(CASE WHEN subject = '英语' THEN score END) AS 英语
FROM scores
GROUP BY name;

💭 思考:为什么 GROUP BY 的键必须选 name(行身份),而不是 subjectscore?——先想清楚「最终每一行代表什么」:行转列要的是「一个人一行」,所以分组键就是「让每个人聚合到唯一一行」的那个维度,也就是 name。如果误用 GROUP BY subject,结果会变成「每科一行」,横向展开的方向就反了;score 则是要被「取值」的度量值,根本不是分组维度。先定对分组键,CASE WHEN 才能安心去「造列」。

为什么需要 MAX?

GROUP BY 把张三的三行合并为一行。对于 subject='语文' 这个条件,三行中只有一行满足(返回 90),另外两行返回 NULL。MAX(90, NULL, NULL) = 90——MAX 用来从”一个有值 + N 个 NULL”中挑出唯一非 NULL 值

用 MIN 也行(效果一样),用 SUM 也行(只有一行满足所以 sum = 该值)。MAX/MIN 的作用只是”忽略 NULL”。

💭 思考:为什么 CASE WHEN 里故意不写 ELSE(让它默认返回 NULL),而不是写成 ELSE 0?——因为整套「挑出唯一非 NULL 值」的机制,依赖聚合函数「忽略 NULL」这个能力。若写成 ELSE 0,未命中行就填 0 而非 NULL,这时 MAX 仍蒙对(MAX(90,0,0)=90),MIN 却立刻出错(MIN(90,0,0)=0,取到 0),SUM 也只剩「恰好一行非零」才勉强成立。所以「省略 ELSE → NULL」不是偷懒,而是刻意把「空位」标成 NULL,让 MAX/MIN/SUM 三种写法都成立——这也顺带解释了为何三者效果完全等价:每个条件列最多一个非 NULL,max/min/sum 都等于那个唯一的真实值。

💡 思考穿插:为什么行转列必须配一个聚合函数,而不是直接 CASE WHEN 完就 SELECT? 因为 GROUP BY 会把同一个 name 的多行压成一行,此时每个条件列面对的是「一组值」——只有命中那一行有真实 score,其余全是 NULL。直接取这一列,SQL 不知道「90 还是 NULL」该选哪个(同一分组内多行值冲突);必须用一个「忽略 NULL」的聚合函数,把这组值里的唯一非 NULL 值挑出来。MAX/MIN/SUM 在这里不是「取最大/最小/求和」,而是「过滤 NULL 取值」。

为什么不用 PIVOT 关键字?

Oracle、SQL Server 有 PIVOT 关键字,MySQL/PostgreSQL 没有。CASE WHEN + GROUP BY + MAX所有关系型数据库都可用的 SQL 标准语法

💡 思考穿插:为什么这套三段式比 PIVOT 关键字更值得掌握? 因为 PIVOT 是数据库私有语法、换库就失效,而 CASE WHEN + GROUP BY + 聚合是 SQL 标准、所有关系库通吃。更重要的是,理解了底层「GROUP BY 压行 + 聚合函数挑非 NULL 值」的原理,你写的就不是死记的模板,而是能自由扩展条件列数、甚至由程序动态拼接 CASE WHEN 的通用范式。

列转行(UNPIVOT)

SELECT name, '语文' AS subject, 语文 AS score FROM pivoted
UNION ALL
SELECT name, '数学' AS subject, 数学 AS score FROM pivoted
UNION ALL
SELECT name, '英语' AS subject, 英语 AS score FROM pivoted;

就是反向操作——把一行扩展为三行。用 UNION ALL 而非 UNION(保留重复行)。

💡 思考穿插:为什么列转行要用 UNION ALL 而不是 UNION? 因为 UNION 会去重——如果两个学生的某科成绩恰好相同,去重会把合法的重复行吞掉;UNION ALL 保留所有行,而且省掉去重的排序/哈希开销、更快。行转列是「聚合」,列转行是「展开」,两个方向都不该丢数据,所以一个用聚合函数收敛、一个用 UNION ALL 全量展开。

章末提问

追问 1:行转列为什么一定要配 MAX(或其他聚合函数)?

结论先行:因为 GROUP BY 压行后,每个条件列是「一个有值 + 多个 NULL」,必须用忽略 NULL 的聚合函数挑出唯一非 NULL 值。因为 不聚合的话,同一分组内某一列出现多行值,SQL 无法确定取哪个,直接 SELECT 会报错或语义错误;聚合函数(MAX/MIN/SUM)在这里的作用不是「求值」,而是「过滤 NULL 后取唯一值」。

追问 2:MAX、MIN、SUM 在行转列里有什么差别?为什么效果一样?

结论先行:三者效果等价,作用都是「忽略 NULL 取唯一非 NULL 值」。因为 每个条件列在同一分组里最多只有一个非 NULL(一行只能对应一个 subject),所以聚合函数面对的是「一个值 + N 个 NULL」,取 max/min/sum 都得到同一个值,选哪个只看个人习惯。

追问 3:为什么不用 PIVOT 关键字?CASE WHEN 方案的优势是什么?

结论先行:因为 PIVOT 是 Oracle/SQL Server 的私有语法,MySQL/PostgreSQL 不支持;CASE WHEN + GROUP BY + 聚合是 SQL 标准。因为 标准方案可移植、且底层原理清晰,遇到列数不确定的动态行转列时,也能通过程序循环拼接 CASE WHEN 来扩展,这是固定语法的 PIVOT 做不到的。


Share this post on:

Previous Post
RocketMQ 面试回答——架构、可靠消息、高可用与死信队列
Next Post
Redis 面试回答——数据结构、持久化、高可用与缓存设计