作者: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
配合 MAP
或 ARRAY
(更灵活、可扩展)
当列多或需要动态处理时,此方法更为推荐。
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_WS
和 SPLIT
结合 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_WS
和 SPLIT
进行严格的列转行不如 MAP
/ARRAY
+EXPLODE
或 STACK
简洁和高效。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 |
- 行转列 (CASE WHEN): 将
device_logs
表转换为每行显示 device_id 和 log_date,列为 CPU_Usage, Memory_Usage, DiskIO_Rate。 - 行转列 (COLLECT_LIST/CONCAT_WS): 从
employee_skills
表,为每个员工生成一列all_skills_string
,其中包含该员工的主要技能和次要技能,用逗号分隔 (例如: "Java,Python")。如果次要技能为空,则只显示主要技能。 - 行转列 (更复杂的CASE WHEN): 基于
device_logs
表,统计每个 device_id 在每个 log_date 的 CPU 平均使用率和 Memory 最大使用率。输出列:device_id, log_date, Avg_CPU, Max_Memory。 - 列转行 (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。 - 列转行 (LATERAL VIEW EXPLODE MAP): 使用
LATERAL VIEW EXPLODE(MAP(...))
将employee_skills
表的 skill_primary 和 skill_secondary 列转换为长表。输出列:emp_name, skill_category ('PrimarySkill' 或 'SecondarySkill'), skill. - 列转行 (STACK): 使用
STACK
函数将employee_skills
表中的 years_exp_primary 和 years_exp_secondary 转换为长表。输出列:emp_name, experience_type ('PrimaryExp' 或 'SecondaryExp'), years. - 行转列 (动态类别处理思路 - 先聚合再处理): 从
device_logs
表中,统计每个device_id
下所有metric_type
的metric_value
的总和,结果存储在一个Map<String, Double>中。输出列:device_id, metrics_sum_map。 (提示:STR_TO_MAP
可能与COLLECT_LIST
ofCONCAT_WS
结合) - 列转行 (ARRAY of STRUCTS): 将
employee_skills
表的主要技能和次要技能及其经验年限,使用ARRAY
ofSTRUCTS
配合LATERAL VIEW EXPLODE
转换为长表。输出列:emp_name, dept, skill_info (STRUCT包含skill_name和years_exp)。 - 行转列 (带条件的字符串聚合): 从
device_logs
表,为每个 device_id 和 log_date,将所有 CPU 使用率大于0.7的metric_value
用逗号连接成一个字符串,列名为high_cpu_values
。 - 列转行 (综合应用): 假设
employee_skills
表有额外的project_A_role
和project_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;