Hive 学习第三天:HQL 查询精讲与内置函数实战

学习环境: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):

  1. 新建 Maven 工程,引入 hive-exec 依赖;
  2. 继承 GenericUDF,实现初始化(校验参数类型)和 evaluate(核心逻辑)方法;
  3. 打包成 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 的关键在于表结构设计(分区/分桶/存储格式)和函数运用(尤其是行列转换与窗口函数)。

后续进阶建议:

  1. 执行引擎切换为 Tez 或 Spark SQL,体会性能差异;
  2. 学习 Hive 调优:执行计划 EXPLAIN、谓词下推、MapJoin、数据倾斜(group by/join 倾斜)处理、小文件合并;
  3. 学习 Hive 事务表与流式更新,理解它在 CDC 场景的能力边界;
  4. 与数仓分层(ODS/DWD/DWS/ADS)结合,把这三天的语法用到一套完整的数仓建模实战中。

语法只是起点,真正的分水岭在于"能不能设计出合理的分层模型,并写出在大数据量下依然跑得快的 HQL"。

相关推荐
JWASX1 小时前
go 学习 - prometheus 指标学习
学习·golang·prometheus
小弥儿1 小时前
GitHub今日热榜 | 2026-10-08:逆向工程 MCP 登顶
学习·开源·github
代码山河2 小时前
JDK、JRE、JVM的区别:一文讲清楚Java运行环境
java·学习·架构·教程·面向对象·项目
fj800j2 小时前
首助记账本新增「标签管理」功能:跨分类归集项目账目,一个账本管完所有事
数据结构·经验分享·笔记·学习·分布式账本
JWASX2 小时前
Java 转 go 学习 - dubbo-go(2)
学习·golang
陈年老古董2 小时前
Hive DML 语言学习笔记:数据加载、插入、导出与导入
hive·笔记·学习·mysql
kkkkkkkkkk_Z2 小时前
新手自学嵌入式 | 学习日记:ARM内核启动代码、时钟系统(PLL/Prescaler/PFD)与IMX6ULL时钟配置
arm开发·学习
clorinda2 小时前
Hive 窗口函数详解:让数据在分组中完成排名、累计和跨行计算
数据仓库·hive·hadoop
丁希希哇2 小时前
强化学习与偏好学习基础:PPO,DPO,GRPO
人工智能·学习·机器学习·大语言模型