学习环境:Hive 3.x(Beeline + HiveServer2)
学习目标:掌握 Hive 查询的完整写法与执行顺序;能熟练使用字符串、日期、聚合、条件类函数;彻底搞懂行列转换与窗口函数(重点、难点、面试高频);了解自定义函数 UDF 的开发思路。
一、准备练习数据
本篇所有例子围绕一张员工表展开,先建表并准备数据:
sql
CREATE TABLE emp (
empno INT,
ename STRING,
dept STRING,
salary DECIMAL(10,2),
hiredate DATE
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t';
数据文件 emp.txt:
1001 张三 研发部 12000 2024-03-15
1002 李四 研发部 15000 2022-06-01
1003 王五 销售部 9000 2023-09-20
1004 赵六 销售部 11000 2021-12-10
1005 钱七 销售部 11000 2025-01-05
1006 孙八 人事部 10000 2023-04-18
二、查询语法结构与执行顺序
一条完整 HQL 的书写顺序:
sql
SELECT 字段
FROM 表
JOIN 表 ON 条件
WHERE 行级过滤
GROUP BY 分组字段
HAVING 分组后过滤
ORDER BY 排序字段
LIMIT 行数;
执行顺序与书写顺序不同,这是理解很多报错的关键:
FROM / JOIN → WHERE → GROUP BY → 聚合函数 → HAVING → SELECT → ORDER BY → LIMIT
所以 WHERE 中不能使用 SELECT 里起的聚合别名(此时还没算),而 HAVING 在聚合之后执行,可以过滤聚合结果。
几个与标准 SQL 的差异点:
- ORDER BY:全局排序,最终只用一个 Reducer,数据量大时极慢;
- SORT BY:每个 Reducer 内部局部排序,不保证全局有序;
- DISTRIBUTE BY :指定按某字段把行分到不同 Reducer;常与 SORT BY 搭配:
DISTRIBUTE BY dept SORT BY salary DESC表示同部门进同一 Reducer 并按薪资排序; - CLUSTER BY:当 DISTRIBUTE BY 和 SORT BY 字段相同且仅升序时,可简写为 CLUSTER BY。
sql
-- 各部门按薪资倒序取前几行的基础练习
SELECT dept, ename, salary
FROM emp
DISTRIBUTE BY dept SORT BY dept, salary DESC;
三、函数总览与查询方式
Hive 内置函数数量众多,不需要死记,先学会两条查看命令:
sql
SHOW FUNCTIONS; -- 列出所有函数
DESCRIBE FUNCTION concat; -- 查看某个函数说明
DESCRIBE FUNCTION EXTENDED concat; -- 查看更详细说明 + 使用示例
函数按使用方式分三大类,先建立框架:
| 分类 | 全称 | 特点 | 例子 |
|---|---|---|---|
| UDF | 一进一出 | 一行输入一行输出 | upper、substr |
| UDAF | 多进一出 | 多行聚合为一行 | count、sum、max |
| UDTF | 一进多出 | 一行输入展开成多行 | explode |
下面分类讲最常用的函数。
四、字符串函数(UDF)
| 函数 | 作用 |
|---|---|
| concat(str1,str2,...) | 拼接字符串(任一为 NULL 结果为 NULL) |
| concat_ws(sep,str1,str2,...) | 用分隔符拼接,自动跳过 NULL |
| lower/upper | 转小写/大写 |
| trim/ltrim/rtrim | 去空格 |
| substr(str,pos,len) | 截取子串,pos 从 1 开始 |
| length | 长度 |
| split(str,sep) | 按规则切分,返回 ARRAY |
| instr(str,sub) | 子串首次出现的位置 |
| replace / regexp_replace | 普通替换 / 正则替换 |
| regexp_extract | 正则提取 |
| get_json_object | 从 JSON 字符串取字段 |
举例:
sql
SELECT
concat(ename, '-', dept) AS 拼接,
concat_ws('_', ename, dept) AS 分隔拼接,
substr(ename, 1, 1) AS 姓氏,
split('2024-03-15', '-')[1] AS 年份,
regexp_replace('电话138abc000', '[^0-9]', '') AS 只留数字,
get_json_object('{"city":"北京","zip":"100000"}', '$.city') AS 城市;
输出关键列:张三-研发部、张、2024、138000、北京。
📸 截图位置 1:字符串函数综合查询结果。
五、日期函数(UDF)
| 函数 | 作用 |
|---|---|
| current_date / current_timestamp | 当前日期 / 时间戳 |
| to_date(ts) | 时间戳转日期 |
| date_format(date,fmt) | 格式化输出 |
| year/month/day | 取年/月/日 |
| datediff(d1,d2) | 相差天数(d1-d2) |
| date_add/date_sub | 加/减天数 |
| add_months | 加月 |
| last_day | 当月最后一天 |
| months_between | 相差月数 |
| unix_timestamp / from_unixtime | 与时间戳互转 |
举例------计算员工工龄(截至 2026-10-08):
sql
SELECT
ename,
hiredate,
datediff('2026-10-08', hiredate) AS 入职天数,
round(months_between('2026-10-08', hiredate), 1) AS 入职月数,
date_format(hiredate, 'yyyy/MM/dd') AS 格式化日期,
last_day(hiredate) AS 入职当月月末
FROM emp;
注意:日期函数对格式敏感,date_format 只识别标准格式;遇到 yyyyMMdd 这种紧凑字符串,要先用 from_unixtime(unix_timestamp('20240315','yyyyMMdd'),'yyyy-MM-dd') 转换。
📸 截图位置 2:工龄相关日期函数查询结果。
六、数学与聚合函数
数学函数常用:round(值,小数位)、ceil、floor、abs、rand()。
聚合函数(UDAF):
sql
SELECT
dept,
COUNT(*) AS 人数,
SUM(salary) AS 薪资总和,
ROUND(AVG(salary),2) AS 平均薪资,
MAX(salary) AS 最高薪资,
MIN(salary) AS 最低薪资
FROM emp
GROUP BY dept
HAVING COUNT(*) >= 2; -- 只保留人数不少于2的部门
两个去重要点:
COUNT(DISTINCT ename):统计去重后的人数;DISTINCT作用于多列时,是对多列组合去重:COUNT(DISTINCT dept, salary)。
七、条件函数与 CASE WHEN
| 函数 | 作用 |
|---|---|
| if(条件,值1,值2) | 条件为真取值1,否则值2 |
| case when ... then ... else ... end | 多分支判断 |
| nvl(a,b) | a 为 NULL 返回 b,否则返回 a |
| coalesce(a,b,c,...) | 返回第一个非 NULL 值 |
| nullif(a,b) | a=b 返回 NULL,否则返回 a |
给员工打薪资等级:
sql
SELECT ename, salary,
CASE
WHEN salary >= 15000 THEN '高级'
WHEN salary >= 10000 THEN '中级'
ELSE '初级'
END AS 等级
FROM emp;
空值处理在数仓中极其常见:coalesce(奖金, 补贴, 0) 表示优先取奖金,没有取补贴,再没有按 0 处理。NULL 参与运算结果恒为 NULL,聚合和拼接前要显式处理。
八、行列转换(重点)
这是实际报表中非常高频的需求,分两个方向。
8.1 行转列:collect_list / collect_set + concat_ws
需求:把同一部门的员工姓名合并到一行。
sql
SELECT
dept,
collect_list(ename) AS 名单数组, -- 保留重复、不去重
collect_set(ename) AS 去重数组, -- 去重
concat_ws(',', collect_list(ename)) AS 名单字符串
FROM emp
GROUP BY dept;
结果(销售部):
销售部 ["王五","赵六","钱七"] 王五,赵六,钱七
思路:collect_list 把多行聚合成一个数组(UDAF),concat_ws 再把数组拼成字符串。
8.2 列转行:explode + LATERAL VIEW
explode 是最常用的 UDTF,能把一个 ARRAY 或 MAP "炸"成多行。先准备一张电影标签表:
sql
CREATE TABLE movie_tags (movie STRING, tags ARRAY<STRING>);
INSERT INTO movie_tags
SELECT '流浪地球', array('科幻','灾难','国产');
直接 explode:
sql
SELECT explode(tags) AS tag FROM movie_tags;
-- 得到三行:科幻 / 灾难 / 国产
实际需求往往要保留原字段,这时必须配合 LATERAL VIEW(侧视图),把 explode 的结果与原表每行关联:
sql
SELECT movie, tag
FROM movie_tags
LATERAL VIEW explode(tags) t AS tag;
结果:
流浪地球 科幻
流浪地球 灾难
流浪地球 国产
如果原始字段是 科幻,灾难,国产 这样的字符串,先用 split 切成数组再炸:LATERAL VIEW explode(split(tags_str, ',')) t AS tag。
MAP 的 explode 会生成 key、value 两列:LATERAL VIEW explode(map字段) t AS k, v。
📸 截图位置 3:行转列 collect_list 与列转行 explode 的结果对比画面。
九、窗口函数(本篇核心,务必练熟)
9.1 为什么需要窗口函数
普通 GROUP BY 聚合后每组只剩一行,如果想"既看到每个员工自己的工资,又看到他所在部门的平均工资",单纯聚合做不到(要么明细、要么汇总)。窗口函数能在不合并行的前提下,基于一个"窗口(一批行)"计算聚合/排名值,并把结果附加到每一行。
语法:
sql
函数名() OVER (
PARTITION BY 字段 -- 按什么分组(窗口边界)
ORDER BY 字段 -- 窗口内排序
ROWS BETWEEN ... -- 可选:更精细的帧范围
)
9.2 排名类:row_number / rank / dense_rank
sql
SELECT ename, dept, salary,
row_number() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn,
rank() OVER (PARTITION BY dept ORDER BY salary DESC) AS rk,
dense_rank() OVER (PARTITION BY dept ORDER BY salary DESC) AS dr
FROM emp;
销售部两人并列 11000 时,三者区别(这是必考点):
| 函数 | 并列处理 | 序号是否跳号 | 销售部结果 |
|---|---|---|---|
| row_number | 不并列,强行 1、2、3 | --- | 1,2,3 |
| rank | 并列同名次 | 跳号 | 1,1,3 |
| dense_rank | 并列同名次 | 不跳号 | 1,1,2 |
经典应用:取每个部门薪资最高的人(TopN 分组取数):
sql
SELECT * FROM (
SELECT emp.*,
row_number() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM emp
) t
WHERE rn = 1;
9.3 聚合类窗口函数
sql
SELECT ename, dept, salary,
SUM(salary) OVER (PARTITION BY dept) AS 部门总薪资,
AVG(salary) OVER (PARTITION BY dept) AS 部门平均,
COUNT(*) OVER (PARTITION BY dept) AS 部门人数
FROM emp;
加上 ORDER BY 并定义帧,可以做累计求和:
sql
SELECT ename, hiredate, salary,
SUM(salary) OVER (ORDER BY hiredate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS 累计薪资
FROM emp;
帧的常用写法:UNBOUNDED PRECEDING(起点)、N PRECEDING/FOLLOWING(前后第 N 行)、CURRENT ROW(当前行)、UNBOUNDED FOLLOWING(终点)。当 OVER 中带 ORDER BY 时,默认帧就是"从起点到当前行",因此上面的累计可简写为 SUM(salary) OVER (ORDER BY hiredate)。
9.4 偏移类:lag / lead / first_value / last_value
| 函数 | 作用 |
|---|---|
| lag(字段,n,默认值) | 当前行往上第 n 行的值 |
| lead(字段,n,默认值) | 当前行往下第 n 行的值 |
| first_value | 窗口内第一行的值 |
| last_value | 窗口内最后一行的值 |
计算相邻两次入职的间隔天数:
sql
SELECT ename, hiredate,
lag(hiredate, 1) OVER (ORDER BY hiredate) AS 上一位入职日期,
datediff(hiredate, lag(hiredate, 1) OVER (ORDER BY hiredate)) AS 间隔天数
FROM emp;
9.5 分组汇总增强:GROUPING SETS / CUBE / ROLLUP
需要同时得到"按部门""按等级""按部门+等级"以及总计等多种维度的汇总时,用它们一条语句搞定,等价于多次 GROUP BY 的 UNION:
sql
SELECT dept,
COUNT(*)
FROM emp
GROUP BY dept
GROUPING SETS ( (dept), () ); -- 同时得到各部门明细 + 总计
WITH ROLLUP:按层级向上汇总(年→月→日 + 总计);WITH CUBE:所有维度组合的笛卡尔汇总,组合数最多,谨慎使用。
📸 截图位置 4 :row_number/rank/dense_rank 三者结果对比。
📸 截图位置 5 :TopN 取各部门薪资最高员工的结果。
📸 截图位置 6:累计求和与 lag 间隔计算结果。
十、自定义函数 UDF 入门
内置函数满足不了时,可以自己开发。Hive 中有三类自定义函数,与前面分类对应:
- UDF:一进一出,最常见;
- UDAF:自定义聚合,多进一出;
- UDTF:一进多出。
开发一个简单 UDF 的步骤(Hive 3.x 基于新版 GenericUDF API):
- 新建 Maven 工程,引入 hive-exec 依赖;
- 继承
GenericUDF,实现初始化(校验参数类型)和evaluate(核心逻辑)方法; - 打包成 jar,在 Beeline 中注册使用:
sql
ADD JAR /路径/my_udf.jar;
CREATE TEMPORARY FUNCTION my_upper AS 'com.example.MyUpperUDF';
SELECT my_upper(ename) FROM emp;
TEMPORARY 表示仅当前会话有效;需要长期共享可放到 HDFS 并使用 CREATE FUNCTION ... USING JAR 'hdfs://...' 注册为永久函数。能用内置函数解决就别重复造轮子,UDF 主要用于公司特有的业务规则(如自定义脱敏、内部编码转换)。
十一、第三天踩过的坑与对策
坑 1:窗口函数结果"不符合预期",累计值突然变化
原因:ORDER BY 的字段有重复值时,窗口默认按值(RANGE)而非按行(ROWS)划定,相同值会被并入同一帧。
对策 :需要严格按行累计时,显式写 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW。
坑 2:WHERE 里用窗口函数报错
原因:窗口函数在 WHERE 之后才执行。
对策 :把窗口函数放在子查询里,外层再用 WHERE 过滤(如 TopN 写法 WHERE rn=1)。
坑 3:explode 和其他字段一起查报错
现象:UDTF 不允许与普通字段并列直接 SELECT(旧版本限制)。
对策 :使用 LATERAL VIEW 关联,这也是标准写法。
坑 4:concat 拼接结果全是 NULL
原因:concat 只要有一个参数为 NULL,结果就是 NULL。
对策 :用 concat_ws(自动跳过 NULL)或先用 nvl/coalesce 把 NULL 处理成默认值。
坑 5:日期函数返回 NULL
原因:字符串不是标准日期格式,或格式串写反。
对策:先用 unix_timestamp + from_unixtime 归一化为标准格式,再参与日期计算。
坑 6:ORDER BY 全局排序非常慢
对策:海量数据只需要局部有序时用 SORT BY;确需全局排序且接受近似结果时配合 LIMIT,减少单 Reducer 压力。
十二、三天 Hive 学习总结
三天的完整路线回顾:
- 第一天(原理与环境):Hive 把 HQL 翻译成分布式任务;数据在 HDFS、元数据在 MySQL、计算靠 MR/Tez/Spark;完成了 MySQL Metastore 部署。
- 第二天(表与数据):基本/复杂类型、内部表与外部表、分区表与分桶表、SerDe、LOAD/INSERT 导入导出。
- 第三天(查询与函数):查询执行顺序;字符串/日期/条件函数;行列转换(collect_list、explode + LATERAL VIEW);窗口函数(排名、聚合累计、偏移);UDF 入门。
贯穿三天的核心思想:Hive 的价值是让你用声明式的 SQL 表达分析意图,把分布式执行的复杂度交给框架;而写好 Hive 的关键在于表结构设计(分区/分桶/存储格式)和函数运用(尤其是行列转换与窗口函数)。
后续进阶建议:
- 执行引擎切换为 Tez 或 Spark SQL,体会性能差异;
- 学习 Hive 调优:执行计划
EXPLAIN、谓词下推、MapJoin、数据倾斜(group by/join 倾斜)处理、小文件合并; - 学习 Hive 事务表与流式更新,理解它在 CDC 场景的能力边界;
- 与数仓分层(ODS/DWD/DWS/ADS)结合,把这三天的语法用到一套完整的数仓建模实战中。
语法只是起点,真正的分水岭在于"能不能设计出合理的分层模型,并写出在大数据量下依然跑得快的 HQL"。