行转列和列转行,一篇讲透
总结
| 方向 | 通用方法 | Hive/Spark方法 | 注意事项 |
|---|---|---|---|
| ------ | ---------- | ---------------- | ---------- |
| 行转列 | CASE WHEN + GROUP BY | pivot() | MAX/MIN/SUM都行 |
| 列转行 | UNION ALL | LATERAL VIEW EXPLODE | 列多时UNION ALL很冗长 |
| 动态行转列 | 代码拼SQL | DataFrame.pivot() | 纯SQL无法动态确定列 |
下一篇 聊用户留存率怎么算,这道题把SQL能力和业务理解结合在一起考,是中高难度的面试常客。
你在工作中行转列用的最多的场景是什么?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
行转列和列转行是面试高频题,也是实际工作中天天遇到的需求。很多人每次写都要现查语法,今天我帮你把这两个方向的写法一次性理清楚,下次直接闭眼写。
先搞清楚:什么是行转列,什么是列转行
一句话总结:
行转列(Pivot): 多行变一行,把某列的值"展开"成多个列
列转行(Unpivot): 一行变多行,把多个列"压扁"成一列
举个例子你就懂了。
原始数据——学生成绩表(一行一个科目):
| name | subject | score |
|--------|---------|-------|
| 小明 | 语文 | 85 |
| 小明 | 数学 | 92 |
| 小明 | 英语 | 78 |
| 小红 | 语文 | 90 |
| 小红 | 数学 | 88 |
| 小红 | 英语 | 95 |
行转列后(每人一行,科目变成列):
| name | 语文 | 数学 | 英语 |
|------|------|------|------|
| 小明 | 85 | 92 | 78 |
| 小红 | 90 | 88 | 95 |
列转行就是反过来,从第二种格式变回第一种。
行转列:CASE WHEN 大法
这是最通用的方法,几乎所有SQL引擎都支持:
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 student_score
GROUP BY name
为什么要套MAX?
因为GROUP BY之后,每个name对应多行数据。CASE WHEN会把不满足条件的行变成NULL,MAX的作用是从一堆NULL和一个有效值里把有效值"捞出来"。这里用MIN、SUM都行,效果一样(因为每个分组里只有一个非NULL值)。
Hive/Spark里的简写:
如果你用Hive或Spark,可以用 str_to_map 或 collect_list + map 的方式动态处理,但面试中直接写CASE WHEN最稳。
列转行:UNION ALL 拆开
把刚才行转列的结果变回去:
-- 假设宽表叫 score_wide(name, 语文, 数学, 英语)
SELECT name, '语文' AS subject, 语文 AS score FROM score_wide
UNION ALL
SELECT name, '数学' AS subject, 数学 AS score FROM score_wide
UNION ALL
SELECT name, '英语' AS subject, 英语 AS score FROM score_wide
简单粗暴,每个UNION ALL对应一个列。缺点是列多了要写很多UNION ALL。
Hive/Spark的优雅写法——LATERAL VIEW + EXPLODE:
SELECT
name,
subject,
score
FROM score_wide
LATERAL VIEW EXPLODE(
MAP('语文', 语文, '数学', 数学, '英语', 英语)
) t AS subject, score
先用MAP把多列打包成键值对,再EXPLODE炸开。一行代码搞定,而且不用写N个UNION ALL。
实战场景:我工作中怎么用的
行转列在实际业务中极其常见。我在做广告数据分析的时候,经常遇到这种需求:
原始数据是按天、按渠道、按指标存的"长表":
| dt | channel | metric | value |
|------------|----------|------------|-------|
| 2025-01-01 | Facebook | spend | 5000 |
| 2025-01-01 | Facebook | impressions| 80000 |
| 2025-01-01 | Facebook | clicks | 2400 |
| 2025-01-01 | TikTok | spend | 3000 |
| 2025-01-01 | TikTok | impressions| 120000|
| 2025-01-01 | TikTok | clicks | 5000 |
业务方要看的报表是这样的"宽表":
| dt | channel | spend | impressions | clicks | CTR |
|------------|----------|-------|-------------|--------|-------|
| 2025-01-01 | Facebook | 5000 | 80000 | 2400 | 3.00% |
| 2025-01-01 | TikTok | 3000 | 120000 | 5000 | 4.17% |
SQL:
SELECT
dt,
channel,
MAX(CASE WHEN metric = 'spend' THEN value END) AS spend,
MAX(CASE WHEN metric = 'impressions' THEN value END) AS impressions,
MAX(CASE WHEN metric = 'clicks' THEN value END) AS clicks,
ROUND(
MAX(CASE WHEN metric = 'clicks' THEN value END) * 100.0 /
NULLIF(MAX(CASE WHEN metric = 'impressions' THEN value END), 0),
2
) AS ctr
FROM ad_metrics
GROUP BY dt, channel
注意那个 NULLIF(..., 0) —— 防止除以0报错。这种细节面试官一看就知道你是真干过活的。
面试追问:动态行转列怎么做?
面试官可能会问:如果不知道有多少个科目(或者科目会动态增加),怎么做行转列?
纯SQL很难做到动态行转列,因为SQL的列必须在编译期确定。实际工作中一般有两种方案:
方案1:拼SQL
先查出所有不同的科目值,然后用代码(Python / Shell)动态拼接CASE WHEN语句。这是最常见的做法。
subjects = spark.sql("SELECT DISTINCT subject FROM student_score").collect()
case_stmts = ", ".join([
f"MAX(CASE WHEN subject = '{s.subject}' THEN score END) AS `{s.subject}`"
for s in subjects
])
spark.sql(f"SELECT name, {case_stmts} FROM student_score GROUP BY name")
方案2:用Hive的grouping sets或Spark的pivot
Spark DataFrame有原生的pivot方法:
df.groupBy("name").pivot("subject").agg(max("score"))
面试的时候把这两种方案都说出来,表明你知道纯SQL的局限性,也知道工程上怎么解决,这就很全面了。