一、多行转多列( Pivot / 透视转换)
假设原始表stu_score是这样:
| stu_id | subject | score |
|---|---|---|
| 1 | math | 90 |
| 1 | english | 80 |
| 1 | chinese | 70 |
| 2 | math | 88 |
| 2 | english | 95 |
现在希望变成:
| stu_id | math_score | english_score | chinese_score |
|---|---|---|---|
| 1 | 90 | 80 | 70 |
| 2 | 88 | 95 | NULL |
这就是 行转列。
本质是:
原来
subject的不同取值是“行里的值”,现在把这些值变成“列名”。多行明细数据变成一行宽表数据先按主键分组,再用条件判断把不同类别的数据筛到不同列里,最后用聚合函数把多行压成一行。
1.1 模板
| |
Hive 中行转列通常使用 GROUP BY 配合条件聚合实现。
核心写法是 MAX/SUM/COUNT(CASE WHEN ... THEN ... ELSE ... END)。
如果每个分组下某个类别只有一条记录,一般用
MAX或MIN;如果需要累计金额或次数,就用
SUM;如果需要统计条件次数,可以用
SUM(CASE WHEN 条件 THEN 1 ELSE 0 END)。
例如按用户把不同事件类型转成行为指标列,可以写 SUM(CASE WHEN event_type='click' THEN 1 ELSE 0 END) AS click_cnt。
如果类别是动态的,Hive 本身不适合直接做完全动态 Pivot,通常通过脚本动态拼 SQL,或者用 map 类型保存动态键值。
1.2 原理
1、Hive 中最常见的行转列写法
| |
1) 为什么需要 GROUP BY?
因为行转列通常是:同一个 user_id 的多行合并成一行
| user_id | subject | score |
|---|---|---|
| 1 | math | 90 |
| 1 | english | 80 |
| 1 | chinese | 70 |
要变成同一个 user_id 的一行:
| user_id | math_score | english_score | chinese_score |
|---|---|---|---|
| 1 | 90 | 80 | 70 |
所以必须:GROUP BY user_id
2)为什么需要 CASE WHEN / IF?
因为你要判断:
如果 subject = ‘math’,那这一行的 score 就放到 math_score
如果 subject = ’english’,那这一行的 score 就放到 english_score
如果 subject = ‘chinese’,那这一行的 score 就放到 chinese_score
所以必须:CASE WHEN subject = 'math' THEN score END
1.3 题目
题目1_模板
将一个用户一天内多次消费转为列显示
| user_id | category | amount |
|---|---|---|
| 1 | food | 20 |
| 1 | food | 30 |
| 1 | game | 100 |
即
| user_id | food_amount | game_amount |
|---|---|---|
| 1 | 50 | 100 |
| |
题目2_加列
原表 user_event:
| user_id | event_type | amount |
|---|---|---|
| 1 | view | NULL |
| 1 | click | NULL |
| 1 | buy | 100 |
| 1 | buy | 200 |
| 2 | view | NULL |
| 2 | click | NULL |
要求输出每个用户每种行为的次数和金额:
| user_id | view_cnt | click_cnt | buy_cnt | buy_amount |
|---|---|---|---|---|
| 1 | 1 | 1 | 2 | 300 |
| 2 | 1 | 1 | 0 | 0 |
| |
题目3_汇总统计
原表 orders:
| user_id | category | order_id | amount |
|---|---|---|---|
| 1 | food | o1 | 20 |
| 1 | food | o2 | 30 |
| 1 | game | o3 | 100 |
| 2 | food | o4 | 50 |
| 2 | travel | o5 | 500 |
要求统计每个用户在不同品类下的订单数和消费金额:
| user_id | food_order_cnt | food_amount | game_order_cnt | game_amount | travel_order_cnt | travel_amount |
|---|---|---|---|---|---|---|
| 1 | 2 | 50 | 1 | 100 | 0 | 0 |
| 2 | 1 | 50 | 0 | 0 | 1 | 500 |
| |
题目4_合并统计(不汇总)
假设原始表stu_score是这样:系统记录出现问题,同一个科目出现了两个分数score
| stu_id | subject | score |
|---|---|---|
| 1 | math | 90 |
| 1 | english | 80 |
| 1 | math | 70 |
| 2 | math | 88 |
| 2 | english | 95 |
现在希望变成:将相同科目的多个分数合并
| stu_id | math_score | english_score |
|---|---|---|
| 1 | 90,70 | 80 |
| 2 | 88 | 95 |
思路:行转列 + 同一分组同一科目的多个值合并成一个字符串。也就是说:
不再用 MAX(score) 取一个分数,而是用 collect_list(score) 把多个分数收集起来,再用concat_ws(',', ...) 拼成 “90,70” 这种字符串
| |
concat_ws 拼接字符串数组,所以最好显式写:cast(score AS string)
collect_list 收集出来的顺序在分布式执行中不一定绝对稳定。如果你想去重,用 collect_set
如果像保证顺序,可以加一个sort_arra,但如果转成字符串排序,会按字符串字典序排序,字符串排序可能不是你想要的数值顺序
| |
写法2:先聚合再行转列
| |
| 对比项 | 写法一:一步到位 | 写法二:先聚合再行转列 |
|---|---|---|
| 聚合次数 | 通常 1 次 | 通常 2 次 |
| Shuffle 次数 | 通常较少 | 可能更多 |
| SQL 长度 | 较短 | 较长 |
| 小数据性能 | 可能更好 | 可能略慢 |
| 大数据重复多 | 不一定好 | 通常更稳 |
| 科目很多 | CASE WHEN 很多,较重 | 更清晰 |
| 内存压力 | 一个 stu_id 下维护多个 list | 按 stu_id, subject 拆分,通常更稳 |
| 可维护性 | 一般 | 更好 |
| 数仓生产推荐 | 一般 | 推荐 |
题目5_动态行转列思路
前面的例子都有一个特点:统计列的种类是提前知道的,比如math、english、chinese ,这种叫 静态行转列
但是如果类别很多,而且不固定,不可能手写几百个
方案1:报表层的字段一般是确定的,不会无限动态变化,通常只取 Top N 类别
方案2:可以不强行转成很多列,而是转成一个 map 字段。用
MAP保存动态键值。例如:user_id tag score 1 tag_a 10 1 tag_b 20 2 tag_c 30 变成:
user_id tag_score_map 1 {“tag_a”:10,“tag_b”:20} 2 {“tag_c”:30} 1 2 3 4 5 6 7 8 9SELECT user_id, map_from_entries( collect_list( named_struct('key', tag, 'value', score) ) ) AS tag_score_map FROM user_tag_score GROUP BY user_id;方案3:用脚本动态生成 SQL
先查出所有类别;
用 Python / Shell / 调度系统生成 SQL;
动态拼接
CASE WHEN;提交 Hive SQL 执行。
二、多列转多行(Unpivot / 反透视转换)
假设原始表 student_score_wide
| user_id | math_score | english_score | chinese_score |
|---|---|---|---|
| 1 | 90 | 80 | 70 |
| 2 | 88 | 95 | NULL |
转成
| user_id | subject | score |
|---|---|---|
| 1 | math | 90 |
| 1 | english | 80 |
| 1 | chinese | 70 |
| 2 | math | 88 |
| 2 | english | 95 |
现在希望变就是 列转行,跟上面讲的恰好相反
本质是:
一行宽表数据拆成多行明细数据
2.1 模板
Hive 中常见有三种写法:
| 写法 | 特点 | 推荐程度 |
|---|---|---|
UNION ALL | 最容易理解,适合新手 | 高 |
LATERAL VIEW + explode | 更像 Hive 风格,适合数组/map 拆行 | 高 |
stack | 专门用于列转行,写法简洁 | 高 |
2.2 原理
| |
所以列转行的关键是:
- 列名转成行值
- 列值转成统一字段值
- 一行拆多行
Hive 中列转行通常有三种做法:UNION ALL、LATERAL VIEW stack、LATERAL VIEW explode。
最常用的是 stack,它可以把一行中的多组列值拆成多行。
如果列很多但逻辑固定,用
stack更简洁;如果要处理数组、map、struct,通常用
explode;如果是新手或兼容性要求高,也可以用
UNION ALL。
方法1:UNION ALL
手动将所有非主键的列一一拆除,现在有3种枚举值,则写三个SELECT 拆解 english_score,最后将结果UNION ALL (UNION会做去重,可能额外引入开销,而且有时会错误去掉你想保留的重复行)
如果需要排序,可以再套一层SELECT
| |
方法2:LATERAL VIEW stack
stack 行级构造,把一行数据拆成 n 行,每行输出一组 key/value(强类型)。
每一“行”字段数一致
n 为key/value对的个数
stack 会在编译阶段统一列类型,数据类型必须完全一致,通常需要统一成 string(Hive 会尝试统一类型,失败就直接抛异常ClassCastException: LazyInteger cannot be cast to IntWritable)
基本语法
| |
示例:
| stu_id | math_score | english_score | chinese_score |
|---|---|---|---|
| 1 | 90 | 80 | 70 |
| 2 | 88 | 95 | NULL |
这里的key是’math’,value是score,string+int 无法安全转换,触发 SerDe 层类型不匹配,所以是需要显式将score这个int转为string
因为 stack()会把 一行变成多行,属于“行生成函数”,Hive 规定这类函数必须通过 lateral view使用。这里的 stack 和 explode 类似,都是 UDTF,都会把一行拆成多行,都必须配合 lateral view
| |
表示一行拆成 3 行:
| stu_id | subject | score |
|---|---|---|
| 1 | math | 90 |
| 1 | english | 80 |
| 1 | chinese | 70 |
| 2 | math | 88 |
| 2 | english | 95 |
| 2 | chinese | NULL |
方法3:LATERAL VIEW explode
先把多个列构造成一个数组:
| |
然后 explode 数组,把数组中的每个元素拆成一行。
| |
执行过程对比
UNION ALL:多分支、多扫描、最后 Union
| |
stack:单扫描、直接展开、不需要 Union
| |
explode:单扫描、先构造复杂类型、再展开
| |
| 对比点 | UNION ALL | stack | explode(array(named_struct)) |
|---|---|---|---|
| 扫描原表 | 可能多次 | 通常一次 | 通常一次 |
| 核心算子 | Union Operator | UDTFOperator + Lateral View | UDTFOperator + Lateral View |
| 是否构造复杂对象 | 否 | 否/较少 | 是,构造 array/struct |
| SQL 可读性 | 新手最好懂 | 简洁 | 稍复杂 |
| 大表性能 | 通常较差 | 通常较好 | 通常较好 |
| 列很多时 | SQL 很长,计划复杂 | 更适合 | 可用但对象构造更重 |
| 处理复杂类型 | 不适合 | 一般 | 最适合 |
| 推荐场景 | 学习、少量列、兼容性 | 固定列转行首选 | array/map/struct 拆行 |