订阅更新

行转列和列转行,一篇讲透

SQL手册 第 3 题 约 7 分钟读完

本文目录
  1. 总结
  2. 先搞清楚:什么是行转列,什么是列转行
  3. 行转列:CASE WHEN 大法
  4. 列转行:UNION ALL 拆开
  5. 实战场景:我工作中怎么用的
  6. 面试追问:动态行转列怎么做?

总结

方向通用方法Hive/Spark方法注意事项
------------------------------------------
行转列CASE WHEN + GROUP BYpivot()MAX/MIN/SUM都行
列转行UNION ALLLATERAL VIEW EXPLODE列多时UNION ALL很冗长
动态行转列代码拼SQLDataFrame.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的局限性,也知道工程上怎么解决,这就很全面了。

刷题进度保存在当前浏览器