十三、Hive 行列转换

作者:IvanCodes

日期:2025年5月19日

专栏:Hive教程

在Hive中,数据的形态转换是数据清洗、分析和报表制作中的核心环节。行列转换尤为关键,它能将数据从一种组织形式变为另一种,以适应不同的业务洞察需求。本笔记将深入探讨Hive中实现行转列和列转行的多种技巧,包括聚合函数、集合操作以及强大的UDTF。

一、行转列

行转列的目标是将某一列的不同值(类别)提升为结果集中的新列名,并将与之对应的数值或信息填充到这些新列中。

1. CASE WHEN 结合聚合函数 (最灵活、最通用)

这是实现行转列的基石方法,适应性强。

场景示例
student_scores 表:

student_name subject score
Alice Math 90
Alice English 85
Bob Math 95
Bob Physics 88

期望输出

student_name Math_score English_score Physics_score
Alice 90 85 NULL
Bob 95 NULL 88

Hive SQL 实现:

sql 复制代码
SELECT
student_name,
MAX(CASE WHEN subject = 'Math' THEN score ELSE NULL END) AS Math_score,
MAX(CASE WHEN subject = 'English' THEN score ELSE NULL END) AS English_score,
MAX(CASE WHEN subject = 'Physics' THEN score ELSE NULL END) AS Physics_score
FROM
student_scores
GROUP BY
student_name;

核心逻辑: 为每个要成为新列的subject值创建一个 CASE WHEN 表达式,并配合聚合函数 (如 MAX, SUM, AVG) 在 GROUP BY 子句定义的粒度上提取值。

2. COLLECT_LIST / COLLECT_SET (聚合为数组/集合)

这两个函数本身不直接完成典型的行转列(即每个类别一个独立列),而是将属于同一分组的多行数据的某一列值聚合成一个数组 (COLLECT_LIST 保留重复,COLLECT_SET 去重) 或集合。这通常是一个中间步骤,后续可能需要UDF或其他处理来进一步转换。

场景示例:获取每个学生所有科目的成绩列表。
Hive SQL 实现:

sql 复制代码
SELECT
student_name,
COLLECT_LIST(score) AS all_scores_list, -- 所有成绩,可能重复
COLLECT_SET(subject) AS distinct_subjects_set -- 所有不重复的科目
FROM
student_scores
GROUP BY
student_name;

输出可能形式:

student_name all_scores_list distinct_subjects_set
Alice [90, 85] ["Math", "English"]
Bob [95, 88] ["Math", "Physics"]

进阶用法: 可以结合 CONCAT_WS 将数组元素连接成字符串,或者使用 MAP 数据类型和 STR_TO_MAP 来更灵活地组织数据,但这已偏离严格的"列名固定"的行转列。

二、列转行

列转行是将一行中的多个列值"铺开"成多行,每行通常包含一个标识列、一个原列名(或其代表的类别)和原列的值。

1. UNION ALL (基础且通用)

对于固定且数量不多的列,UNION ALL 非常直观。

场景示例
monthly_sales 表:

product_id sales_jan sales_feb sales_mar
P001 100 120 110

期望输出

product_id month sales
P001 Jan 100
P001 Feb 120
P001 Mar 110

Hive SQL 实现:

sql 复制代码
SELECT product_id, 'Jan' AS month, sales_jan AS sales FROM monthly_sales
UNION ALL
SELECT product_id, 'Feb' AS month, sales_feb AS sales FROM monthly_sales
UNION ALL
SELECT product_id, 'Mar' AS month, sales_mar AS sales FROM monthly_sales;

2. LATERAL VIEW EXPLODE 配合 MAPARRAY (更灵活、可扩展)

当列多或需要动态处理时,此方法更为推荐。

Hive SQL 实现 (使用 MAP):

sql 复制代码
SELECT
t1.product_id,
t2.month_name,
t2.sales_value
FROM
monthly_sales t1
LATERAL VIEW EXPLODE(MAP(
'Jan', sales_jan,
'Feb', sales_feb,
'Mar', sales_mar
)) t2 AS month_name, sales_value;

核心逻辑: 先将要转换的列构建成一个 MAP (键为新类别,值为原列值),然后使用 EXPLODE 将其"炸开"成多行。LATERAL VIEW 使得我们能够在 SELECT 中引用 EXPLODE 生成的新列。

Hive SQL 实现 (使用 ARRAY OF STRUCTS):

sql 复制代码
SELECT
t1.product_id,
t2.month_struct.month_name AS month,
t2.month_struct.sales_value AS sales
FROM
monthly_sales t1
LATERAL VIEW EXPLODE(ARRAY(
STRUCT('Jan' AS month_name, sales_jan AS sales_value),
STRUCT('Feb' AS month_name, sales_feb AS sales_value),
STRUCT('Mar' AS month_name, sales_mar AS sales_value)
)) t2 AS month_struct;

说明: 使用 ARRAY of STRUCTS 可以更好地组织每一对 (类别名, 值),特别是当"值"本身也是复合结构时。

3. Hive STACK UDTF (简洁高效)

Hive 内置的 STACK 函数专门为列转行设计,语法简洁。

Hive SQL 实现:

sql 复制代码
SELECT
product_id,
month_data.month_name,
month_data.sales_value
FROM
monthly_sales
LATERAL VIEW STACK(3, -- 要生成的行数 (等于要转换的列的组数)
'Jan', sales_jan,
'Feb', sales_feb,
'Mar', sales_mar
) month_data AS month_name, sales_value;

STACK(N, c1_key, c1_val, c2_key, c2_val, ..., cN_key, cN_val): N 指定输出的行数,后面的参数成对提供新类别名和对应的列值。

4. CONCAT_WSSPLIT 结合 LATERAL VIEW EXPLODE (字符串拼接与拆分)

这种方法略显迂回,但有时在特定场景下或处理已拼接的字符串数据时有用。

场景: 先将多个列用特定分隔符拼接成一个字符串,然后拆分。
Hive SQL (概念性,不直接用于上述monthly_sales场景,因其不是最优):

假设要转换 col_A, col_B

sql 复制代码
-- 步骤1: (可选) 如果数据已经是拼接好的字符串列,则跳过
-- SELECT id, CONCAT_WS(',', col_A, col_B) AS combined_string FROM some_table;

-- 步骤2: 拆分 (假设combined_string列存在)
SELECT
id,
exploded_value
FROM
(SELECT id, CONCAT_WS(':', 'col_A_name', col_A_val, ',', 'col_B_name', col_B_val) AS combined_string_with_names FROM ...) -- 构造一个包含名称和值的字符串
LATERAL VIEW EXPLODE(SPLIT(combined_string_with_names, ',')) t1 AS single_pair_string
LATERAL VIEW EXPLODE(SPLIT(single_pair_string, ':')) t2 AS item; -- 这一步还需要更复杂的逻辑来分离name和value,通常不这么做

注意: 直接使用 CONCAT_WSSPLIT 进行严格的列转行不如 MAP/ARRAY+EXPLODESTACK 简洁和高效。CONCAT_WS 更常用于将多列数据或数组元素合并成一个展示性的字符串。

三、练习题

通用数据表:
employee_skills 表:

emp_name dept skill_primary skill_secondary years_exp_primary years_exp_secondary
John IT Java Python 5 2
Jane Sales Communication Negotiation 7 4
Mike IT Python SQL 3 3
Sara Marketing SEO Content Creation 4 NULL

device_logs 表:

device_id log_date metric_type metric_value
D01 2023-01-01 CPU 0.75
D01 2023-01-01 Memory 0.60
D01 2023-01-02 CPU 0.80
D02 2023-01-01 CPU 0.50
D02 2023-01-01 DiskIO 120.5

  1. 行转列 (CASE WHEN): 将 device_logs 表转换为每行显示 device_id 和 log_date,列为 CPU_Usage, Memory_Usage, DiskIO_Rate。
  2. 行转列 (COLLECT_LIST/CONCAT_WS): 从 employee_skills 表,为每个员工生成一列 all_skills_string,其中包含该员工的主要技能和次要技能,用逗号分隔 (例如: "Java,Python")。如果次要技能为空,则只显示主要技能。
  3. 行转列 (更复杂的CASE WHEN): 基于 device_logs 表,统计每个 device_id 在每个 log_date 的 CPU 平均使用率和 Memory 最大使用率。输出列:device_id, log_date, Avg_CPU, Max_Memory。
  4. 列转行 (UNION ALL): 将 employee_skills 表中的 skill_primary 和 skill_secondary 以及它们对应的 years_exp_primary 和 years_exp_secondary 转换为长表。输出列:emp_name, dept, skill_type ('Primary' 或 'Secondary'), skill_name, years_experience。
  5. 列转行 (LATERAL VIEW EXPLODE MAP): 使用 LATERAL VIEW EXPLODE(MAP(...))employee_skills 表的 skill_primary 和 skill_secondary 列转换为长表。输出列:emp_name, skill_category ('PrimarySkill' 或 'SecondarySkill'), skill.
  6. 列转行 (STACK): 使用 STACK 函数将 employee_skills 表中的 years_exp_primary 和 years_exp_secondary 转换为长表。输出列:emp_name, experience_type ('PrimaryExp' 或 'SecondaryExp'), years.
  7. 行转列 (动态类别处理思路 - 先聚合再处理): 从 device_logs 表中,统计每个 device_id 下所有 metric_typemetric_value 的总和,结果存储在一个Map<String, Double>中。输出列:device_id, metrics_sum_map。 (提示: STR_TO_MAP 可能与 COLLECT_LIST of CONCAT_WS 结合)
  8. 列转行 (ARRAY of STRUCTS): 将 employee_skills 表的主要技能和次要技能及其经验年限,使用 ARRAY of STRUCTS 配合 LATERAL VIEW EXPLODE 转换为长表。输出列:emp_name, dept, skill_info (STRUCT包含skill_name和years_exp)。
  9. 行转列 (带条件的字符串聚合): 从 device_logs 表,为每个 device_id 和 log_date,将所有 CPU 使用率大于0.7的 metric_value 用逗号连接成一个字符串,列名为 high_cpu_values
  10. 列转行 (综合应用): 假设 employee_skills 表有额外的 project_A_roleproject_B_role 两列。使用最合适的方法将 skill_primary, skill_secondary, project_A_role, project_B_role 都转换为长表,包含 emp_name, category ('Skill' 或 'ProjectRole'), description (对应的值)。

四、练习题答案

答案1:

sql 复制代码
SELECT
device_id,
log_date,
MAX(CASE WHEN metric_type = 'CPU' THEN metric_value ELSE NULL END) AS CPU_Usage,
MAX(CASE WHEN metric_type = 'Memory' THEN metric_value ELSE NULL END) AS Memory_Usage,
MAX(CASE WHEN metric_type = 'DiskIO' THEN metric_value ELSE NULL END) AS DiskIO_Rate
FROM
device_logs
GROUP BY
device_id, log_date;

答案2:

sql 复制代码
SELECT
emp_name,
dept,
CONCAT_WS(',', skill_primary, IF(skill_secondary IS NOT NULL AND skill_secondary != '', skill_secondary, NULL)) AS all_skills_string --  更严谨的IF处理空或空字符串
FROM
employee_skills;
-- 或者更简单的,如果次要技能为NULL,CONCAT_WS会自动忽略它(但空字符串不会)
-- SELECT emp_name, dept, CONCAT_WS(',', skill_primary, skill_secondary) AS all_skills_string FROM employee_skills;

答案3:

sql 复制代码
SELECT
device_id,
log_date,
AVG(CASE WHEN metric_type = 'CPU' THEN metric_value ELSE NULL END) AS Avg_CPU,
MAX(CASE WHEN metric_type = 'Memory' THEN metric_value ELSE NULL END) AS Max_Memory
FROM
device_logs
GROUP BY
device_id, log_date;

答案4:

sql 复制代码
SELECT emp_name, dept, 'Primary' AS skill_type, skill_primary AS skill_name, years_exp_primary AS years_experience FROM employee_skills
UNION ALL
SELECT emp_name, dept, 'Secondary' AS skill_type, skill_secondary AS skill_name, years_exp_secondary AS years_experience FROM employee_skills WHERE skill_secondary IS NOT NULL;

答案5:

sql 复制代码
SELECT
t1.emp_name,
t2.skill_category,
t2.skill
FROM
employee_skills t1
LATERAL VIEW EXPLODE(MAP(
'PrimarySkill', skill_primary,
'SecondarySkill', skill_secondary
)) t2 AS skill_category, skill
WHERE t2.skill IS NOT NULL;

答案6:

sql 复制代码
SELECT
emp_name,
exp_data.experience_type,
exp_data.years
FROM
employee_skills
LATERAL VIEW STACK(2,
'PrimaryExp', years_exp_primary,
'SecondaryExp', years_exp_secondary
) exp_data AS experience_type, years
WHERE exp_data.years IS NOT NULL;

答案7:

sql 复制代码
SELECT
device_id,
STR_TO_MAP(CONCAT_WS(',', COLLECT_LIST(CONCAT_WS(':', metric_type, CAST(SUM(metric_value) AS STRING))))) AS metrics_sum_map
FROM
device_logs
GROUP BY
device_id, metric_type -- 先按metric_type聚合得到SUM
GROUP BY device_id; -- 再按device_id聚合得到MAP
-- 上述写法比较复杂且可能不是最优,更常见的是直接聚合到MAP:
-- SELECT device_id, MAP_FROM_ENTRIES(COLLECT_LIST(STRUCT(metric_type, SUM(metric_value)))) AS metrics_sum_map
-- FROM device_logs GROUP BY device_id, metric_type GROUP BY device_id; -- Hive版本支持MAP_FROM_ENTRIES
-- 如果Hive版本不支持MAP_FROM_ENTRIES,通常聚合到数组,然后外部处理或UDF

更正和简化第7题思路 (直接用Hive内建功能构建Map,假设目标是每个metric_type对应其总和):

sql 复制代码
SELECT
device_id,
MAP_AGG(metric_type, sum_metric_value) AS metrics_sum_map
FROM (
SELECT device_id, metric_type, SUM(metric_value) as sum_metric_value
FROM device_logs
GROUP BY device_id, metric_type
) sub
GROUP BY device_id;
-- 注意: MAP_AGG 是 Hive 2.2.0+ 的功能. 如果版本较低,思路会更复杂,如上面注释的collect_list of structs.

答案8:

sql 复制代码
SELECT
t1.emp_name,
t1.dept,
t2.skill_info.skill_name AS skill_name,
t2.skill_info.years_exp AS years_experience
FROM
employee_skills t1
LATERAL VIEW EXPLODE(ARRAY(
STRUCT(skill_primary AS skill_name, years_exp_primary AS years_exp),
STRUCT(skill_secondary AS skill_name, years_exp_secondary AS years_exp)
)) t2 AS skill_info
WHERE t2.skill_info.skill_name IS NOT NULL;

答案9:

sql 复制代码
SELECT
device_id,
log_date,
CONCAT_WS(',', COLLECT_LIST(CASE WHEN metric_type = 'CPU' AND metric_value > 0.7 THEN CAST(metric_value AS STRING) ELSE NULL END)) AS high_cpu_values
FROM
device_logs
GROUP BY
device_id, log_date;

(如果想确保没有符合条件时不输出空字符串而是NULL,可以再包一层IF)

sql 复制代码
SELECT
device_id,
log_date,
IF(SIZE(COLLECT_LIST(CASE WHEN metric_type = 'CPU' AND metric_value > 0.7 THEN CAST(metric_value AS STRING) ELSE NULL END)) > 0,
CONCAT_WS(',', COLLECT_LIST(CASE WHEN metric_type = 'CPU' AND metric_value > 0.7 THEN CAST(metric_value AS STRING) ELSE NULL END)),
NULL) AS high_cpu_values
FROM
device_logs
GROUP BY
device_id, log_date;

答案10:

sql 复制代码
SELECT
emp_name,
items.category,
items.description
FROM
employee_skills
LATERAL VIEW STACK(4, -- 假设project_A_role 和 project_B_role 列存在
'Skill', skill_primary,
'Skill', skill_secondary,
'ProjectRole', project_A_role,
'ProjectRole', project_B_role
) items AS category, description
WHERE items.description IS NOT NULL;
相关推荐
小飞敲代码2 小时前
【Hadoop 实战】Yarn 模式上传 HDFS 卡顿时 “No Route to Host“ 错误深度解析与解决方案
大数据·linux·运维·服务器·hadoop·分布式·hdfs
你的坚持终将美好,4 小时前
谷歌es插件elasticsearch-head.crx
大数据·elasticsearch·搜索引擎
小白的码BUG之路4 小时前
Elasticsearch-kibana索引操作
大数据·数据库·elasticsearch
QX_hao4 小时前
【Hadoop】--HA高可用搭建--3.2.2
大数据·hadoop·分布式
dianzouyou1234565 小时前
钉钉报销与金蝶付款单系统对接技术揭秘
大数据·数据库·钉钉
Ultipa5 小时前
云计算与大数据进阶 | 26、解锁云架构核心:深度解析可扩展数据库的5大策略与挑战(上)
大数据·数据库·云计算
资讯分享周6 小时前
让机房更可靠,让工程更简单!奥兰德智能微模块又添新军
大数据
线条16 小时前
深入理解 Hadoop 核心组件 Yarn:架构、配置与实战
大数据·hadoop·架构
深蓝易网7 小时前
打破传统仓库管理困局:WMS如何重构出入库全流程
大数据·运维·人工智能·重构·数据分析·制造