1. 从一道面试题聊起:为什么行转列、列转行总被考
先抛个场景。假设你面前坐着一位面试官,他让你当场写一句 SQL,把下面这种“一行一条记录”的销售表:
| 月份 | 商品 | 销售额 |
|---|---|---|
| 1月 | 手机 | 100 |
| 1月 | 电脑 | 200 |
| 2月 | 手机 | 150 |
| 2月 | 电脑 | 180 |
变成“每个商品一行、不同月份作为列”的报表:
| 商品 | 1月 | 2月 |
|---|---|---|
| 手机 | 100 | 150 |
| 电脑 | 200 | 180 |
如果你能脱口而出“用 CASE WHEN 配合 GROUP BY”或者“用 PIVOT / crosstab / GROUP BY + FILTER”,并且能立刻说出不同数据库的写法差异,那这道题基本就稳了。如果只是背过“行转列是 CASE WHEN、列转行是 UNION ALL”,但一追问“为什么要这么写”“性能上有什么坑”“面试官还会怎么变式问”,就开始支支吾吾,那这道题大概率会成为整场的分水岭。
我过去几年里既被问过,也帮别人模拟过这类问题,发现一个规律:行转列和列转行,从来不是单纯考一个语法点——它背后牵出的是一整套数据建模思维、SQL 执行顺序的理解,以及你对“宽表”和“长表”这两种数据组织方式本质的把握。就算你平时用不上 PIVOT 这类关键字,理解它的等价手工写法,也能帮你写出更稳、更可维护的查询。
这篇内容就围绕这个经典题目,把行转列、列转行背后的原理、各数据库方言的写法差异、常见变式题、性能优化思路和踩坑记录一次讲透。适合正准备面试的人,也适合那些工作中天天被报表需求折磨、想彻底搞懂这类转换的开发者和数据分析师。
2. 先搞清楚底层模型:长表、宽表与数据透视的本质
2.1 数据库里存得最多的是“长表”
大多数 OLTP(日常事务处理)系统里,数据天然是以“一行一个事实”的方式存储的。比如订单表里每个订单一行、每个明细一行;交易流水里每笔交易一行。这种形态在数据分析里叫“长表”(Long Format),也叫堆叠表(Stacked Data)。
长表的特点是:同一个逻辑实体(比如某个商品)的信息分散在多行里,区分这些行的关键往往是一个“类别或属性字段”(比如月份、渠道、地区)。它的优点是:新增一类数据不需要改表结构,直接 insert 新行就行,插入和更新非常方便,也符合关系数据库“行是基本单位”的设计哲学。
2.2 但报表和看板偏偏需要“宽表”
到了分析和报表阶段,情况就反过来了。业务方往往想看到的形态是“一个商品一行,列是不同月份、不同渠道、不同地区”。这种形状在数据分析里叫“宽表”(Wide Format),也叫透视表(Pivot Table)。
宽表的优势是:人眼扫行就能对比一个商品在不同月份的表现;BI 工具做折线图、柱状图时,通常也更喜欢宽表,因为列名可以做维度,一行就是一条完整序列。Excel 里的数据透视表,本质上就是帮你把长表转成宽表的过程,只不过它发生在 Excel 内存里,不需要你写 SQL。
2.3 行转列、列转行,实际上就是这两种形态的互转
行转列,就是把“类别字段”的不同取值,转成结果集里不同的“列”。这个过程在数据库里通常伴随一个聚合操作,因为原来多行数据可能要落到同一行、同一组里,必须决定每个格子里放什么值——是最大值、最小值、求和、计数,还是直接取唯一值。
列转行,就是把原本横向躺在多个列里的信息,重新“竖起来”——每列转成一行记录,同时加一列说明“原来的列名叫什么”。这个过程在数据库中通常是做“表内容的横向扩展”的反向操作,不需要聚合(严格来说,UNION ALL 会产生多行,但不会对原始数据做归并),所以它的写法核心是“拆列成行 + 附加维度标记”。
这两类操作,在数据仓库 ETL、报表开发、BI 数据集市、考试面试题里无处不在。理解了本质,你就不会背一堆语法,而是看到需求就能判断“该把哪个字段变成列、哪个字段变成行、要不要聚合、用哪个聚合函数”。
3. 行转列:四种主流写法与层层递进
3.1 先明确需求,别急着敲 SQL
面试最容易翻车的地方不是语法,而是“需求都没确认清楚就开始写”。一个标准的行转列需求,至少要回答四个问题:
- 哪个字段要变成列?根据什么取值生成新列名?
- 哪些字段要放在同一行里?也就是 GROUP BY 谁?
- 一个格子里如果有多条记录,用什么聚合函数?
- 列数是固定的,还是动态的?
前三个问题会在写 SQL 时直接决定你的语法结构。第四个问题决定了你是用静态 SQL 还是动态 SQL。很多人在家背好了 CASE WHEN,一到现场发现“月份可能不止 12 个”,就懵了——这类动态列场景,在面试中通常会作为加分变式出现,后面我会单独讲。
3.2 写法一:CASE WHEN + GROUP BY(最通用、最笔试友好)
这是绝大多数面试官期待你写的方案,也是所有数据库都通用的写法。核心思路是:先把类别字段的每个取值,用 CASE WHEN 判断并投放到对应列上,然后对主键字段做 GROUP BY 聚合,最后对每组用 MAX/SUM/AVG 等聚合函数收敛。
用最开头的销售表举例,把“月份”变成列,SQL 是:
SELECT 商品, MAX(CASE WHEN 月份 = '1月' THEN 销售额 END) AS "1月", MAX(CASE WHEN 月份 = '2月' THEN 销售额 END) AS "2月" FROM 销售明细 GROUP BY 商品;这里用 MAX 而不是 SUM,很多人不理解。原因在于:如果同一商品同一月份只有一行数据,MAX 和 SUM 都得到同一个值;但一旦存在重复记录,MAX 会把多行的值收敛为最大值,SUM 会把它们累加。在“每个格子里只允许一列一值”的前提下,取 MAX 是安全的聚合策略,因为其他值被丢弃了,不会造成重复求和。面试时你可以补一句“如果业务上同一商品同一月份可能多行且需要累加,我会改成 SUM”,这句话能体现你真的懂聚合语义,而不是背模板。
用 CASE WHEN 的好处是零依赖、任何数据库都能跑、脑子清楚不容易错。缺点是列数一多,SQL 文本会变得很长,维护起来令人崩溃。但这在面试中反而是加分项,因为你还能继续给出下面的写法。
3.3 写法二:PIVOT 表操作符(SQL Server 专用)
如果你是面试 SQL Server 方向,面试官大概率会想看到 PIVOT。PIVOT 的底层执行逻辑其实和 CASE WHEN + GROUP BY 是一回事,只是语法上把“要变成列的字段”和“要聚合的值”显式声明出来。
拿上面的数据,SQL Server 写法是:
SELECT 商品, [1月], [2月] FROM 销售明细 PIVOT ( MAX(销售额) FOR 月份 IN ([1月], [2月]) ) AS pvt;需要注意几个关键点:
- FOR 子句式里写的列值,必须与表中实际存储的值完全一致,因为 PIVOT 是拿这个值直接做列名的。
- PIVOT 会自动对除了“FOR 指定列”和“聚合列”之外的所有列做隐式 GROUP BY。所以前面 SELECT 里只能包含参与分组的列(比如商品),如果表里有其他字段(比如地区、渠道),PIVOT 会把它们也悄悄当作分组列,导致结果行数比预期多。这是 PIVOT 最常见的隐性坑,我在面试中经常提醒候选人注意这一点。
- PIVOT 的 IN 列表通常用字面量写死,遇到动态列需要拼接动态 SQL,比较麻烦。
SQL Server 里还有一个配套的 UNPIVOT 做列转行,后面会一起讲。
3.4 写法三:crosstab 函数(PostgreSQL 扩展)
如果你面的是 PostgreSQL 方向,而且数据库恰好装了 tablefunc 扩展,可以用 crosstab。它最初是为做统计报表设计的,性能和写法都相当好用:
CREATE EXTENSION IF NOT EXISTS tablefunc; SELECT * FROM crosstab( 'SELECT 商品, 月份, 销售额 FROM 销售明细 ORDER BY 商品, 月份', 'SELECT DISTINCT 月份 FROM 销售明细 ORDER BY 月份' ) AS ct(商品 text, "1月" int, "2月" int);第一个参数是“产生数据源”的 SQL,第二个参数是“生成列名”的 SQL,AS 子句里需要按列名顺序声明结果列。crosstab 的要求比 PIVOT 更严格:
- 查询结果必须是三列:行头字段、列头字段、值字段。
- 数据源 SQL 必须按行头字段排序,否则扩展函数会直接报错,这是初用者最容易踩的坑。
- 如果某个行列交叉处没有数据,crosstab 会填入 NULL,但要注意,它要求“每个行头对应的列头顺序稳定”,所以第二个参数的排序与第一个参数的排序要保持一致。
由于 crosstab 是 PostgreSQL 专有功能,平时不常用的话,面试可能不会主动考到,但你能说出来“还有 crosstab 这种方案”,给人的印象会是“对这个数据库生态有实际了解”,而不是只会背 CASE WHEN。
3.5 写法四:GROUP BY + FILTER(PostgreSQL 和部分数据库的特色写法)
除了 crosstab,PostgreSQL 还有一种非常优雅的写法,利用 FILTER 子句直接对聚合函数做条件过滤:
SELECT 商品, MAX(销售额) FILTER (WHERE 月份 = '1月') AS "1月", MAX(销售额) FILTER (WHERE 月份 = '2月') AS "2月" FROM 销售明细 GROUP BY 商品;这种写法在语义上比 CASE WHEN 更紧凑,更重要的是它保留了“聚合函数 + 过滤条件”的结构,写复杂统计(比如同时算不同条件下的 COUNT、SUM)时清晰度远超一串 CASE WHEN。FILTER 在 PostgreSQL、SQLite 较新版本里都支持,但 MySQL 和 SQL Server 不支持。面试时如果提到它,一定要说明适用数据库,否则会显得你混用方言。
3.6 横向对比:什么时候用哪种
| 写法 | 适用数据库 | 优缺点 | 面试推荐度 |
|---|---|---|---|
| CASE WHEN + GROUP BY | 所有 | 零依赖、易理解;列多时代码冗长 | 必写 |
| PIVOT | SQL Server | 语法简洁;多列聚合后隐式分组易出错 | 加分 |
| crosstab | PostgreSQL | 性能好;强制三列且需排序,坑多 | 加分 |
| GROUP BY + FILTER | PostgreSQL / SQLite | 语义清晰、易扩展;方言受限 | 提一句即可 |
你会发现核心决策点永远是:你的数据库是什么、列是否动态、团队维护习惯如何。面试中不是选最新奇的写法,而是先写最稳的 CASE WHEN,再顺着追问展示你对其他方言的了解,这才是最优策略。
4. 列转行:UNION ALL 是根,方言只是壳
4.1 需求形态和两种拆解思路
列转行的需求,典型场景是:某个系统里的配置表、评分表、指标表,把它们设计成了“一行里同时放着 1 月、2 月、3 月三列”的宽表,现在 BI 工具要抖成“按月一行”的长表,才能方便做 group by 月份统计。
比如一张成绩表:
| 学生 | 语文成绩 | 数学成绩 | 英语成绩 |
|---|---|---|---|
| A同学 | 80 | 90 | 70 |
| B同学 | 60 | 85 | 95 |
要变成:
| 学生 | 科目 | 成绩 |
|---|---|---|
| A同学 | 语文 | 80 |
| A同学 | 数学 | 90 |
| A同学 | 英语 | 70 |
| B同学 | 语文 | 60 |
这里有两种实现思路。第一种是“一行变三行,直接拆”:把每一列单独 select 出来,各自附带一个“科目名”的常量标记,然后通过 UNION ALL 合并。第二种是“先变后拆”:用一些数据库特有的横向展开功能,比如 SQL Server 的 UNPIVOT、PostgreSQL 的 LATERAL + VALUES、MySQL 的 UNION ALL 或者 CROSS JOIN + CASE。核心逻辑都是把“原来作为列的维度”转换成“新生成的一列值”。
4.2 方案一:UNION ALL 硬拆(最通用)
每个需要转成行的列,都单独写一段 SELECT:
SELECT 学生, '语文' AS 科目, 语文成绩 AS 成绩 FROM 成绩表 UNION ALL SELECT 学生, '数学' AS 科目, 数学成绩 AS 成绩 FROM 成绩表 UNION ALL SELECT 学生, '英语' AS 科目, 英语成绩 AS 成绩 FROM 成绩表;这个写法在 MySQL、Oracle、SQL Server、PostgreSQL 里都能跑,也是面试中首先要给的方案。为什么用 UNION ALL 而不是 UNION?因为这三段数据在逻辑上不可能重复,用 UNION 反而会让数据库多做一次去重运算,白白浪费性能;而且如果某个学生恰好两科分数相同,UNION 会直接把两行合并成一行,数据就丢了。这个细节面试官经常会追问。
这种硬拆方式的缺点是:如果列有几十个,你要写几十段 SELECT,SQL 长到爆炸,而且新增一列就要改一次 SQL。优点是:它是最容易让面试官看懂的方案,也是讲清楚“列转行本质”的最佳载体,所以作为第一答案准没错。
4.3 方案二:SQL Server 的 UNPIVOT
SQL Server 提供了对称的 UNPIVOT:
SELECT 学生, 科目, 成绩 FROM 成绩表 UNPIVOT ( 成绩 FOR 科目 IN (语文成绩, 数学成绩, 英语成绩) ) AS ut;IN 后面的每个列名会被 UNPIVOT 自动转成“科目”这一列的值,即“语文成绩”这个字符串,而对应单元格的值进入“成绩”列。需要注意:UNPIVOT 默认会过滤掉值为 NULL 的行,如果业务上需要把 NULL 也保留成一行,你需要在源表里先把 NULL 替换成默认值,比如 ISNULL(语文成绩, 0)。这个细节,不实际操作过是很容易被忽略的,面试能说出来会很加分。
另外,UNPIVOT 的输出列类型是所有源列类型的公共类型,如果不同列类型不一致(比如一个 int 一个 decimal),需要提前统一转换,否则会报错。
4.4 方案三:PostgreSQL 的 LATERAL + VALUES
PostgreSQL 里可以用 LATERAL 子查询配合 VALUES 构造一个“临时表”来完成列转行,写法非常灵活:
SELECT t.学生, v.科目, v.成绩 FROM 成绩表 t CROSS JOIN LATERAL ( VALUES ('语文', t.语文成绩), ('数学', t.数学成绩), ('英语', t.英语成绩) ) AS v(科目, 成绩);这段 SQL 的意思是:对每一行原数据,构造一个只有“科目、成绩”两列的虚拟表,然后与原表展开连接,天然完成了“一行变三行”。它比 UNION ALL 简洁,也不像 UNPIVOT 那样会丢掉 NULL(NULL 会保留在结果里),是 PostgreSQL 实战中比较推荐的写法。
4.5 方案四:MySQL 的多种曲线写法
MySQL 中除了 UNION ALL 硬拆,还可以使用 CASE WHEN + 笛卡尔积的方式:
SELECT t.学生, CASE c.col WHEN '语文成绩' THEN '语文' WHEN '数学成绩' THEN '数学' WHEN '英语成绩' THEN '英语' END AS 科目, CASE c.col WHEN '语文成绩' THEN t.语文成绩 WHEN '数学成绩' THEN t.数学成绩 WHEN '英语成绩' THEN t.英语成绩 END AS 成绩 FROM 成绩表 t CROSS JOIN ( SELECT '语文成绩' AS col UNION ALL SELECT '数学成绩' UNION ALL SELECT '英语成绩' ) c;这种方式看起来绕,但在 MySQL 没有 UNPIVOT 的情况下,也能实现“动态列名常量展开”的效果。面试时如果对方问“MySQL 列转行除了 UNION ALL 还有别的写法吗”,能写出这段,说明你对 SQL 执行过程的理解不浅,因为这里用到了 CROSS JOIN 的“行数放大”能力。不过日常开发中,UNION ALL 已经足够清晰高效,不一定非要炫技。
4.6 列转行的注意点:类型统一、NULL 处理、列数稳定性
做列转行最容易出 bug 的地方,基本集中在三处:
第一,类型统一。原来多个列的类型可能都是数值,但混合起来进入同一列时,数据库会做隐式转换,如果不统一,可能会截断或报错。稳妥做法是先用 CAST 把所有列转成同一个目标类型。
第二,NULL 语义。UNPIVOT 会丢 NULL,CASE WHEN 方案默认不会丢;如果业务要求“缺失值也要保留一行以便统计”,要显式处理。
第三,列数稳定性。列转行生成的列名是常量,如果宽表新增了列,你的 SQL 没有同步更新,报表就会悄悄少数据。所以在 ETL 场景里,更推荐用数据库元数据动态生成列转行语句,或者干脆在数仓建模阶段避免产出过多的宽表——这个问题后面第五节会展开。
5. 变式与实战:加总、去重、动态列、性能优化
5.1 变式一:不是 MAX 而是 SUM,行转列遇上重复数据
面试官看了你写 MAX(CASE WHEN ...) 之后,通常会追问:“如果同一个商品在同一个月份有多张订单,你怎么把销售额求和后转成一列?”
这时你要立刻意识到:聚合函数的选择不是语法问题,是业务语义问题。如果需求是“当月累计销售额”,就用 SUM:
SELECT 商品, SUM(CASE WHEN 月份 = '1月' THEN 销售额 ELSE 0 END) AS "1月", SUM(CASE WHEN 月份 = '2月' THEN 销售额 ELSE 0 END) AS "2月" FROM 销售明细 GROUP BY 商品;注意我把 CASE WHEN 的 ELSE 改成了 0 而不是省略。原因是:SUM 对 NULL 是忽略的,但是 NULL 参与 CASE 判断时,整个表达式为 NULL,SUM 会自动跳过;写成 ELSE 0 后,求和结果会更符合“当月无数据算 0”的业务直觉,也在某些 BI 工具里避免显示 NULL。不过严格讲,如果某商品 1 月确实没记录,上面这段 SQL 的“1月”列也会是 0,而不是 NULL——这两种语义有细微差别,你要按业务需求取舍。
如果是“每个商品每月最多一条记录,但不确定是否有重复”,用 MAX 更安全;如果是“订单明细表,想求月度销量”,用 SUM。我在面试中会这样拆解,候选人只要能把这一层讲清楚,基本证明他是真做过报表的。
5.2 变式二:多列同时行转列(比如同时转销售额和销量)
实际报表里,一个行转列需求往往不只是转一列值,而是要把“销售额”“销量”两个指标同时转成列。比如:
SELECT 商品, MAX(CASE WHEN 月份 = '1月' THEN 销售额 END) AS "1月销售额", MAX(CASE WHEN 月份 = '1月' THEN 销量 END) AS "1月销量", MAX(CASE WHEN 月份 = '2月' THEN 销售额 END) AS "2月销售额", MAX(CASE WHEN 月份 = '2月' THEN 销量 END) AS "2月销量" FROM 销售明细 GROUP BY 商品;这种写法虽然列数翻倍,但在逻辑上依然清晰。如果用 PIVOT,SQL Server 只允许一个聚合列,所以要多列指标时通常还得回退到 CASE WHEN,或者用两次 PIVOT 再 JOIN——这也是我实际项目里更喜欢 CASE WHEN 的原因之一,它支持任意多列同时最后进行任意聚合。
5.3 变式三:列数不确定怎么办?动态 SQL 是面试加分项
面试官最容易出的“杀招”是:“如果月份不止 1、2 月,可能有 12 个月,甚至未来还会增加,你的 SQL 怎么写?”
这种问题,标准的 CASE WHEN 字面量写法天然失效,因为你不可能把未来的列名也 enum 出来。这时正确答案是“动态生成 SQL”,核心思路分三步:
- 用 SQL 查出所有需要转成列的去重值列表(比如 SELECT DISTINCT 月份 FROM 表 WHERE 条件)。
- 在程序里或者存储过程里,根据这些值拼出 CASE WHEN 片段或 PIVOT IN 列表。
- 将拼好的完整 SQL 提交执行。
在 SQL Server 里常见做法是用 FOR XML PATH 拼接字符串;在 MySQL 里可以用 GROUP_CONCAT;在 PostgreSQL 里可以用 string_agg。举一个 PostgreSQL 拼接列名片段的最简示意:
SELECT string_agg( format('MAX(CASE WHEN 月份 = %L THEN 销售额 END) AS %I', 月份, 月份), ',' ) FROM (SELECT DISTINCT 月份 FROM 销售明细 ORDER BY 月份) t;然后把这段拼进外层查询。面试时不需要把完整动态 SQL 写得完美,但要把“查出 distinct 值”、“拼 SQL”、“执行 SQL”这个链路说清楚。更进一步的进阶点是:这个方法虽然灵活,但存在 SQL 注入风险和缓存失效问题,实际生产环境建议用白名单校验或直接改用报表工具内置的动态列功能——能把“灵活性与风险”这对矛盾讲出来,面试官通常会对你另眼相看。
5.4 变式四:转置之后再计算(行转列 + 窗口函数连用)
有时面试官会把行转列和窗口函数组合起来考,比如“统计每个商品的月度销售额,并计算月度销售额的累计值和环比增长率”。
这种题通常先做行转列,然后对转置后的宽表做窗口计算,或者在长表阶段直接做窗口计算再转置。核心考点不是语法,而是你能不能识别出“该在哪个阶段计算”。我一般建议:先在长表阶段把 window function 算完,再行转列,因为长表阶段做 partition by 商品 order by 月份 的累计求和是最自然的;如果先转成宽表,反而要跨列计算,SQL 会非常别扭。
举个例子,先在长表算累计销售额,再行转列:
WITH base AS ( SELECT 商品, 月份, 销售额, SUM(销售额) OVER (PARTITION BY 商品 ORDER BY 月份) AS 累计销售额 FROM 销售明细 ) SELECT 商品, MAX(CASE WHEN 月份 = '1月' THEN 累计销售额 END) AS "1月累计", MAX(CASE WHEN 月份 = '2月' THEN 累计销售额 END) AS "2月累计" FROM base GROUP BY 商品;这里要注意:窗口函数 OVER 的计算发生在 GROUP BY 之前,所以如果先对明细做 SUM(销售额) 的聚合,再在此基础上做窗口累加,写法会变成 GROUP BY 之后用聚合值做窗口,SQL 会多一层子查询。面试时能主动指出“窗口函数的执行顺序在聚合前后有差异”,绝对是稳定加分项。
5.5 性能优化:行转列/列转行在数据量大时怎么不翻车
很多面试者能写出正确 SQL,但被问到“如果这张表有 1 亿行,你上面的写法还扛得住吗”就会卡住。这块其实是实际工作中最值钱的部分,我把常见优化手段列一下:
第一,尽早过滤。无论是源表还是子查询,先加上时间、地区、类型等过滤条件,缩小数据量,再做行转列或列转行。转置本身相当于一次“行数放大或缩小”的过程,数据量越小开户越快。
第二,控制分组粒度。行转列的本质是 GROUP BY,分组字段越多,分组数越多,每个分组越小,聚合开销越低。但如果你的分组字段中混入了没必要的列(比如莫名其妙的备注字段),会导致分组行数暴增,聚合压力飙升,结果还容易出现重复行。检查 PIVOT 或 CASE WHEN 时,一定只保留真正需要的分组列。
第三,列转行时避免扫描多次。UNION ALL 硬拆方案里,每段 SELECT 都会扫描一次原始表,如果源表很大、列又多,I/O 会成倍增长。解决办法是用 CROSS JOIN + CASE(或 LATERAL + VALUES)这种“一次扫描多行展开”的方式,让数据库只读一遍数据。
第四,索引设计。行转列时,GROUP BY 后面如果经常以某些字段组合分组,可以考虑建复合索引,比如 (商品, 月份)。列转行场景下,原始宽表的主键列尽量靠前,能让“横向展开”时的访问路径更集中。
第五,如果只是报表展示,不一定非得在数据库里转。可以把数据拉到报表工具(比如 BI 工具里的透视表、Excel 的数据透视表)里做可视化转置,数据库主要负责过滤和聚合,这能省下大量数据库 CPU。我说这句话是想提醒你:SQL 里能写不代表就应该写在 SQL 里,架构上的取舍在面试中也很重要。
6. 面试实战:被我记录下来的一些典型问答实录
这一节记录几段我在模拟面试中真实遇到的问答片段,供大家参考。
片段一:为什么要用 MAX 而不是直接 SELECT?
面试官:你写了 MAX(CASE WHEN ...),但没有 ELSE,如果那个月没有数据,这个格子是不是 NULL?为什么你还用 MAX?
候选人:是的,NULL 在 CASE WHEN 没有 ELSE 分支时会出现。MAX 的主要作用不是为了处理 NULL,而是把同一分组里多行 CASE 的结果收敛成一个值。因为同一商品同一月份可能对应多行,如果不套 MAX,GROUP BY 后每个分组可能冒出多行,语法上就错了。
面试官点评:能说到“收敛成一值”这个层面,说明不是背的。
片段二:UNION 还是 UNION ALL?
面试官:你写的列转行用了 UNION ALL,能解释为什么不用 UNION 吗?
候选人:UNION 会做去重,但这个场景里每段 SELECT 分别来自不同列,逻辑上不可能重复;用 UNION 反而增加排序去重开销,更关键的是如果某个学生语数外三科中两科同分,UNION 会错误地把两行合并成一行,造成数据丢失。
面试官点评:这个答案很完整,把性能和语义两个层面都踩到了。
片段三:动态列你打算怎么实现?
面试官:假设这张表每月自动新增一列,你的行转列 SQL 能不用改代码就适配吗?
候选人:静态 SQL 做不到。我会改成两层:先用一个 SQL 查出所有月份,程序端拼出完整的聚合表达式,再提交执行。拼 SQL 的时候要注意列名转义,防止 SQL 注入;生产环境如果拼接频率很高,可能影响数据库缓存,我会考虑换成报表工具的动态列或者物化视图方案。
面试官点评:如果你能把投影风险也讲出来,这个回答基本能等同于工作了两三年的人的思考水平。
这些片段并不是让你背答案,而是想展示一个判断标准:面试官在听到你“额外说出的那几句话”时,才是真正在评估你的经验值。语法本身只值 40 分,剩下的 60 分全在语义判断、边界处理和性能意识上。
另外,我自己在设计模拟面试题时,有一条经验:如果面试者能在 15 分钟内把一道行转列从“基础写法”聊到“动态列 + 性能取舍”,同时还能反问我“业务上到底希望 NULL 还是 0”,那我基本不会再怀疑他写复杂 SQL 的能力。反过来,如果只给出一个答案就停住,我会默认他对这一块的理解只有一个“面”而不是一个“体”。
7. 面试前 30 分钟速记:一张清单照抄
如果你马上要去面试,来不及通读全文,直接看这节就够了。
| 问题 | 标准答案要点 |
|---|---|
| 行转列的本质 | 把某个类别字段的不同取值变成结果集的列,并伴随聚合 |
| 列转行的本质 | 把宽表的多列拆成多行,并增加一列标记原列名 |
| 最通用的行转列写法 | CASE WHEN + GROUP BY + MAX/SUM/AVG |
| SQL Server 专用写法 | PIVOT / UNPIVOT |
| PostgreSQL 专用写法 | crosstab / FILTER / LATERAL + VALUES |
| 行转列时第二个易错点 | 多分组列导致结果行数比预期多 |
| 列转行时第一个易错点 | 用 UNION 还是 UNION ALL,别丢数据 |
| 列转行时第一个易错点 | NULL 会不会被丢弃(UNPIVOT 默认丢弃) |
| 动态列怎么做 | 查 DISTINCT 值 → 拼 SQL → 执行 |
| 性能上的首要原则 | 先过滤、后转置;列转行尽量一次扫描 |
| 面试加分表达 | 先确认需求语义,再挑写法,最后补充性能和动态列方案 |
还有三条口语化“定海神针”,你可以背下来,在任何答案后面跟一句都会提升整体表现:
- “这块如果数据量大会涉及扫描放大,我会优先考虑在数仓层提前完成转置,而不是在报表查询时临时转。”
- “动态列方案虽然灵活,但有注入风险和缓存问题,生产环境需要做白名单或改用报表工具。”
- “我写 SQL 前会先确认业务定义:这里到底是要 MAX 还是 SUM,NULL 还是 0,这决定了查询的正确性。”
这第三条看起来像是废话,但恰恰是很多候选人做不到的。面试官不是考你背了多少函数,而是想看你面对真实业务需求时,是否具备“提出问题再动手”的成熟度。
8. 我踩过的坑:三个真实教训帮你省下半天排查时间
最后分享三个我在实际业务中真实遇到过的坑,每一个都直接对应到这篇文章某个章节,希望你别重复交学费。
坑一:行转列后“多出来”很多行,排查半天发现是隐式分组
有一回做渠道销售报表,用 SQL Server 的 PIVOT 写了一个月度转化表。源表里本来只有商品、月份、渠道、销售额四个字段,我以为 PIVOT 后按商品分组就行,结果跑出来每个商品多了好几行。查了半天才意识到,PIVOT 会把除了 FOR 和聚合值之外的所有字段都塞进隐式分组,渠道也被当成了一组,所以才“多维度”地形成了组合行。从那以后,我使用 PIVOT 前一定会先确认“除了目标和值列以外,到底还有哪些列”,尤其旁边还有一堆无关紧要的备注字段时。
坑二:列转行后数据翻倍,因为 CROSS JOIN 悄悄做了行数放大
有一次用 MySQL 的 CROSS JOIN + CASE 方案做科目宽表转长表。刚开始觉得写法挺飘逸,结果发现输出行数比预期多了一倍。原因是我在 CROSS JOIN 临时表里写列时,没有先做去重——每行原记录都被“科目列”枚举三次,但我的 WHERE 条件没控制好,导致部分科目映射重复,数据直接翻倍。从那以后,我在做列转行时严格遵循“先拆列、再过滤、再聚合”的顺序,并且会先算一下“总行数 = 原始行数 × 列数”是否符合预期,再做后续处理。
坑三:动态 SQL 拼好的列名没有转义,一格特殊字符直接让报表崩掉
那是我刚接触动态行转列时的事。某个列值里带着“&”字符,我在拼接 SELECT 片段时直接把它当成了列名,结果数据库执行报错。复盘之后,我开始对列名一律做转义处理,比如在 PostgreSQL 里用 quote_ident 或 format 的 %I,在 SQL Server 里用 QUOTENAME。这个坑几乎不会出现在面试里,但实际开发中特别容易遇到。如果你在面试中能主动提一句“动态 SQL 列名要做转义”,面试官会认为你不仅懂原理,还真正写过生产等级的代码。
这三个坑有一个共性:都不是语法不会写,而是对“数据库底层行为”的预期不足。行转列、列转行表面上是“形状变换”,本质上是在跟数据库的“分组机制、扫描方式、元数据转义”博弈。写之前多问自己一句:“数据库会怎么执行这条 SQL?”胜过背十种写法。
我自己的习惯是,但凡遇到行转列、列转行这类需求,先花两分钟想清楚业务语义,再选一种当前库最稳的写法,最后跑一下数据行数验证,确认“行数变化符合预期”。这套流程帮我挡下了不少隐藏 bug。如果你现在正要准备面试,强烈建议你拿一个真实业务表,把长表转宽表、宽表转长表各做一遍,再试一次动态列版本,整个过程不会超过一个下午,但你对这类问题的理解会比其他候选人高出一个级别。