写这篇文章只因见过太多人在 Hive 函数上栽跟头。明明离线数仓跑数、数据清洗、特征加工都离不开 Hive 内置函数,但很多同学的用法都是从别人代码里扒的,能跑就行,不知道为什么,也不知道哪些函数能做得更高效。这篇“hive函数大全”不打算做成一个看了就忘的API列表,而是把函数按实用场景重新组装,让做数据开发、数据分析的人真正用得上。
我这几年在数据开发一线打转,经手的离线任务从几十张表做到上千张表,函数积累下来无非那些东西,但同样的函数在不同人手里,跑出来的效率和正确率差距很大。所以这篇会在讲清每个函数的同时,把里面最容易踩的坑一并说透。看这篇文章的人,不管是刚开始学 Hive 的入门者,还是已经在写各种复杂报表的开发人员,都有可以直接拿走的参考价值。
1. 先立一个整体认知:Hive 函数与其分类逻辑
1.1 按用途分类才是最快记忆方式
Hive 的函数数量真的很多,光内置的就有一两百个,靠死记硬背不现实。我在实际工作中把常用函数归成了几大类:数值函数、字符串函数、日期时间函数、条件函数、聚合函数、表生成函数,还有一类是窗口分析函数。每一类解决一类问题,遇到需求先判断属于哪一类,再去细翻具体的函数,效率高得多。
我这里所说的分类不追求官方定义,而是贴近数据开发的真实工作流。比如正则相关的函数归到字符串处理里,开窗函数单独当作一类,因为它们在复杂统计场景中实在太常用了。这个分类方式支撑了我写绝大多数数仓查询和 ETL 加工脚本,照着这个思路去看其他函数文档,也更容易建立记忆锚点。
1.2 不同业务场景该找哪一类函数
数据开发中最常见的需求无非是三类:清洗加工、指标统计、结构转化。清洗加工最依赖字符串函数和日期函数;指标统计靠聚合函数和窗口函数;结构转化,比如一行变多行、多行变一行,需要表生成函数和 collect 类函数。把它们对应到场景里,比孤立记函数有用得多。
比如用户标签表需要把多个标签合并成一个字段,优先想到 concat_ws;要算用户首单时间与最近一次活跃日期间隔,离不开 datediff;要对每个用户的订单按金额排序取前 N 条,那必须用窗口函数。这种对应关系越熟练,写脚本就越快,代码出错率也越低。
2. 数值函数:从四舍五入到高效随机抽样
2.1 round、floor、ceil 在日常跑数里的分工
数值函数看着简单,但用错是常有的事。round 做四舍五入,floor 向下取整,ceil 向上取整。很多人在计算金额人均值的时候直接 round(avg(amount), 2),没有问题,但如果后续还要对金额做汇总,最好保留原来的高精度,展示层再统一做四舍五入,不然累计误差会在多层计算中被放大。
另外 Hive 在做除法时,整型与整型相除默认返回整型。比如 select 5/2 结果不是 2.5 而是 2。这是个非常典型的坑,特别是写指标口径的时候,稍不留神就丢了小数位。我习惯在涉及除法时给其中一个操作数做类型转换,比如写成 5/2.0,或者用 cast(5 as double)/2。
2.2 rand 函数在抽样和分桶里的正确打开方式
rand() 返回 0 到 1 之间的随机数,它的价值不只是取随机值,更常用于数据抽样和分桶。比如从用户表中抽 1% 的用户,可以 select * from user where rand() < 0.01。但这里要注意,rand() 不带种子的话每次执行结果都不同,调试的时候如果你想复现同一份抽样结果,要给 rand(1) 这种固定种子。
分桶场景里有个更优的做法:用 hash 函数配合取余,比如 abs(hash(user_id)) % 100。这样保证同一用户的分配结果永远稳定,后续不管重跑几次任务,只要 user_id 不变,分桶结果就不变。这对线上做 A/B 实验或离线特征分组非常关键。
2.3 数值溢出与精度问题的处理建议
数值函数还有个隐患是数据溢出。当对超大金额做求和时,int 类型很容易溢出,所以金额字段至少要用 bigint 或 decimal。我的习惯是金额都用 decimal(38, 2),其中 38 是总位数,2 代表小数位。两个 decimal 相乘一定要考虑结果位数,不然会报数据溢出错误。
3. 字符串函数:数据清洗加工的核心武器
3.1 concat_ws 拼接字段细节
concat_ws 是我用得非常多的函数。它跟 concat 不一样,可以指定分隔符,且能跳过 null。比如拼一个用户完整地址:concat_ws('-', province, city, district),中间三个字段如果有 null,不会出现“xx--”这种空位片段。而 concat 遇到 null 整个结果直接变 null,这是两类函数最大的区别。
实际工作中我经常用 concat_ws 来做字段合并,比如把订单的商品ID和数量拼成“123:2,456:3”这种紧凑格式,这样下游解析时按约定符号拆开就行。按约定符号存多值字段在数据湖仓里很常见,concat_ws 是最省事的产生源之一。
3.2 substr、split 解析字符串的固定搭配
对字符串做截取和拆分,substr 和 split 是配合最紧密的一对。substr(str, start, len) 注意起始位置是从 1 开始的,不是 0,写错这个就能差一位。split(str, regex) 返回数组,尤其要注意第二个参数是正则表达式,普通字符没问题,特殊字符要转义,比如按点拆分要用 split(str, '\\.')。
我常说看到什么格式的数据先想清楚定界符是什么再去 split,不然很容易把字符串拆得乱七八糟。比如日期「2024-05-20 13:14:15」要取日期部分,substr 比 split 更轻量,直接 substr(dt, 1, 10) 就行。
3.3 正则三兄弟:regexp_extract、regexp_replace、regexp
正则函数在 Hive 里超级有用。regexp_extract(str, regex, idx) 用于提取,idx 表示取第几个分组。regexp_replace(str, regex, rep) 用于替换,regexp 可以判断字符串是否匹配。三者的区别可以用一句话概括:一个负责取,一个负责改,一个负责判。
举例来说,日志里的 UA 字段往往很长,我只取其中的浏览器版本:regexp_extract(ua, 'Chrome/([0-9.]+)', 1)。这里正则写不好会经常匹配不到,我踩过多次坑后总结了个经验:先在一小份数据上反复验证正则,确认提取结果正确后再放到大批量任务里跑,不要直接全量跑完才发现语法匹配错。
3.4 补齐、去空格、大小写转换这些细节别轻视
trim 函数去空格看起来没什么好说的,但实际数据里常见的是中文空格和制表符,trim 不一定能处理,需要用 regexp_replace 替换掉。lpad 和 rpad 也很实用,比如订单号要求固定 10 位,长度不足时用 lpad 补零:lpad(order_id, 10, '0')。
大小写转换 upper/lower 主要用在维度值规整。我把用户注册渠道字段统一转成小写再分组,就能避免“App”和“app”被当成两个渠道。这些看起来很小的事,处理不规范会直接导致统计口径不一致。
4. 日期时间函数:跑数前先对齐时间口径
4.1 unix_timestamp 与 from_unixtime 的互转套路
日期函数的坑比想象的更多。Hive 的时间戳通常有两种形态:一种是 unix 秒级时间戳,一种是标准格式的字符串日期。两者互转靠 unix_timestamp 和 from_unixtime 两个函数。比如 from_unixtime(1716451200, 'yyyy-MM-dd HH:mm:ss') 能转成可读时间,unix_timestamp('2024-05-23', 'yyyy-MM-dd') 则把日期转回时间戳。
这里强调一点:Hive 默认时区是服务器时区,如果你所在业务使用的是北京时间,部署集群时区没配好的话,from_unixtime 转出来会差 8 小时。我曾经因为时区问题在凌晨数据上看到前一天的日期,排查了很久才发现是 cluster 时区设置成了 UTC。
4.2 常用日期加减与格式化的正确姿势
date_add、date_sub 做日期加减,datediff 算两个日期之间天数,date_format 做任意格式转换。这些函数具体到 date_add(‘2024-05-01’, 7) 返回 '2024-05-08',而 datediff('2024-05-10', '2024-05-01') 返回 9。在按天统计指标的时候,经常要用 date_sub(current_date, 1) 表示昨天。
计算过程要注意 date_format 的格式串大小写敏感,比如月份的 MM 和分钟 mm 如果用反了,出来的数完全对不上。我习惯把格式串写成一个常量先行验证一次,避免在动辄几千万条数据的任务里重复返工。
4.3 一个自然月指标统计的典型场景拆解
做月度报表时,我们经常拿本月第一天和当前日期做区间。掌握两个日期就可以:每月第一天用 date_format(current_date, 'yyyy-MM-01') 或 trunc(current_date, 'MM'),当前日期直接用 current_date。有了这两个边界值,再配合 datediff 算本月已过天数,就能算各种节奏指标。
这种写法比硬编码日期更稳健。以前有些人喜欢在代码里写死“2024-05-01”,到下个月就失效了,必须手动改。正确做法是用 date_add(last_day(add_months(current_date, -1)), 1) 自动算上个月最后一天加一天,得到本月第一天。代码写得越抽象,维护成本越低。
5. 条件函数:让数据按照业务规则做分支
5.1 case when 和 if 的使用策略
条件逻辑是数据处理里最普通的刚需。case when 可以做多条件分支,比如按订单金额分档:case when amount >= 1000 then '高价值' when amount >= 500 then '中价值' else '一般' end。if 函数适合简单的二元判断,比如 if(sex = '1', '男', '女'),嵌套超过两层就看不清了,这种情况我更推荐 case when。
我发现很多人写 case when 不写 else,导致不匹配任何条件的记录直接变成 null。下游指标计算时,null 参与统计很容易被 count 或 sum 忽略,结果少一截。我现在写 case when 基本必带 else,除非业务上确实允许缺失才特意不写。
5.2 coalesce、nvl 与 null 值处理的三种境界
null 值处理函数看起来简单,其实很有讲究。nvl(a, b) 只在 a 为 null 时返回 b,coalesce(a, b, c, ...) 则多个候选值里返回第一个非 null 的。还有一个容易被忽略的事实:字符串空字符串''跟 null 是不同的,''不参与 coalesce 的替换,要清理数据得把''也转成 null。
比如一个大宽表里手机号字段存在''和 null 两种形态,直接用 coalesce 手机号字段判断用户是否有手机号,会把空字符串当成有,造成误判。我做稳健做法是先把''转成 null,再统一做逻辑判断。这算是数据开发里一个细节决定成败的典型案例。
6. 聚合函数:日报指标的核心支撑
6.1 常用五件套:sum、count、avg、max、min
聚合函数本身听着简单,但用起来有很多细节。sum 可以数值求和,也可以对布尔判断做求和;count 有几种不同写法的差异:count(1) 统计行数,count(col) 统计某列非 null 的行数,count(distinct col) 统计去重后的数量。很多人写 count(distinct user_id),如果数据量特别大,这个操作会非常重,能替代时要尽量替代。
avg 会自动忽略 null,但如果全部是 null,avg 结果也是 null。有时业务希望 null 按 0 处理,就要用 avg(coalesce(amount, 0))。max/min 在日期和字符串上也能用,比如取最近一次登录时间用 max(login_time) 就行,不一定要转成时间戳来比大小。
6.2 collect_list 与 collect_set:实现行转列的利器
聚合不只是算数统计,collect_list 和 collect_set 能把多行的值收集成一个数组。collect_list 保留重复值,collect_set 去重。因为分组内顺序通常不稳定,需要保证顺序时可以用 sort_array 包一层再收集。常见的场景是生成用户标签数组:select user_id, collect_set(tag) from user_tag group by user_id。
这里有个容易踩的坑:collect_list 收集的结果中包含 null 值时,数组里会出现 null。我在做聚合后的数组去重时通常先过滤掉 null 再 collect,避免最终结果出现莫名的空元素。这个坑在生成用户行为序列时特别容易踩到,一下就把下游模型的特征喂错了。
6.3 sum 与 case when 结合做多条件人数统计
用聚合函数和条件函数组合,能完成非常复杂的统计需求。比如同时统计不同支付状态下的订单量,可以用 count(if(status = 'paid', 1, null)) 这种写法。if 不满足条件时返回 null,count 自动忽略 null,这样一步就统计出支付订单数,不需要多个子查询做连接。
同理,sum(case when type = 'new_user' then amount else 0 end) 可以按用户类型分流求和。这种方法我几乎天天用,是写日报、周报的高级效率工具。同时要留意一点:用 if 或 case when 生成的分组结果记得起一个语义明确的别名,否则后续解析非常痛苦。
7. 表生成函数:把一行数据展开成多行
7.1 explode 与 lateral view 的经典搭配
explode 能把数组或 map 展开成多行。比如 select explode(split(‘a,b,c’, ‘,’)) 会生成三行。但是直接 select explode 会限制其他字段的同时查询,必须配合 lateral view 才能同时保留外层字段,这是这个函数最经典的用法。比如商品表里有一个 category_list 数组,要统计每个品类下的商品数:select product_id, cate from product lateral view explode(category_list) t as cate。
lateral view 生成的新表别名和列别名一定要写清楚,我见过很多人漏写别名直接被 Hive 报错。还有 explode 的结果如果为空数组,那一行直接不显示,如果你希望保留原始行,可以用 lateral view outer,这是更多细微但实用的细节差异。
7.2 posexplode 获取数组下标
posexplode 是 explode 的进阶版,额外返回一个位置下标。这在处理有序序列时非常重要,比如一个用户的行为序列数组,展开后要保留步骤序号,就能直接用 posexplode 一次拿到两个值:select behavior, pos from user_log lateral view posexplode(behavior_list) t as pos, behavior。有了 pos 就能做“第几步做了什么事”这种分析,比单纯展开强很多。
有的场景需要把逗号分隔字符串变成两列,一行变两行,也可以用 posexplode 配合 case when 实现。这个函数算是表生成函数里性价比最高的一个,掌握之后就很少再需要写复杂的自定义 UDTF。
7.3 多列同时 explode 的一个大坑
当同一行里有多个数组需要同时展开时,不能直接写两个 lateral view explode,当然可以写两个,但结果会变成笛卡尔积,行数爆增。比如一个用户同时有浏览列表和购买列表,两个数组同时展开,每对组合都会生成一行。如果你只想要相同位置的元素一一对应,需要想办法取下标关联。
我之前处理过用户行为路径,两个长度不同的列表同时展开,结果行数级数膨胀,直接拖垮了整个任务。后来解法是把两个列表都拆成带序号的子查询,再用序号关联,才避免了笛卡尔积。这里提醒大家,凡是遇到多列一对一展开,先想清楚是否会产生数据放大的问题。
8. 窗口函数:排序、分组与去重的进阶操作
8.1 row_number、rank、dense_rank 的差异
窗口函数是写复杂报表绕不开的能力。row_number() 不管值是否相同,都按顺序编号,不会重复;rank() 会留出重复位置,比如并列第一后,下一个是第三名;dense_rank() 不会留空位,并列第一后下一个是第二。这三个在分组取数时语义非常关键,选错会直接影响结果行数。
排序需求看似简单,实际业务里常常要在分组内取 TopN。比如每个品类取销售额前三的商品:select * from (select *, row_number() over (partition by cate order by sales desc) rn from sku) t where rn <= 3。这个写法是离线取 TopN 的标准动作,所有做数据分析的人都应该形成肌肉记忆。
8.2 sum over 开窗做累计值
窗口不只是在排序时用,sum(col) over(partition by ... order by ...) 能算累计值。比如统计每天累计成交额,就能用 sum(amount) over(order by dt) 实现,不需要去用自连接。这也是报表开发中非常高频的需求。这里要记住 order by 在开窗中决定了窗口的范围,如果不写 order by,默认是全部分区参与聚合,写错了结果就完全不对了。
累计值通常还和去重需求一起出现,比如用户每日留存。用窗口函数算当日新增用户、当日活跃用户的累计曲线,非常方便。配上一个合理的分区粒度,比如按天分区、按月粒度算累计,就能一次性拿到整个自然月的完整趋势。
8.3 窗口函数在数据去重中的应用
除了取数,窗口函数也是去重利器。面对同一用户多笔重复记录时,常用 row_number() over(partition by user_id order by create_time desc) rn,然后取 rn=1,保留最新记录。这个模式在增量同步、状态表回溯中非常常用,可以替代复杂的取最大时间子查询关联,逻辑还更好读。
这里要提醒:row_number 对数据的顺序非常敏感。如果你表的粒度已经去重了,或者时间字段有大量重复,row_number 的结果可能不稳定。稳妥做法是再追加一个唯一字段进排序条件,比如把时间相同的情况按 id 再排一次,确保结果唯一且稳定。
9. 高频函数速查:一份随时捞出来用的表
下面这张表可以说是全文精华的浓缩,把这些函数按场景整理在一起,适合日常写 HiveQL 时直接检索。我更推荐把下面的内容做成自己的笔记,每次写脚本前扫一遍,比临时翻文档更有效。
| 场景分类 | 推荐函数 | 典型用途 |
|---|---|---|
| 数值处理 | round | 四舍五入 |
| 数值处理 | ceil, floor | 向上/向下取整 |
| 数值处理 | rand | 抽样与分桶 |
| 字符串处理 | concat_ws | 用分隔符拼接字段 |
| 字符串处理 | substr, split | 截取、拆分 |
| 字符串处理 | regexp_extract | 正则提取 |
| 日期时间 | from_unixtime | 时间戳转日期 |
| 日期时间 | date_format | 日期格式转换 |
| 日期时间 | datediff | 日期差 |
| 条件逻辑 | case when | 多分支判断 |
| 条件逻辑 | coalesce | 取第一个非 null |
| 聚合统计 | sum, count, avg | 常规指标统计 |
| 聚合统计 | collect_list, collect_set | 行转数组 |
| 行转列 | explode, lateral view | 数组展开为多行 |
| 行转列 | posexplode | 数组展开并带序号 |
| 窗口分析 | row_number() over | 分组取 TopN |
| 窗口分析 | sum() over | 累计统计 |
10. 高频报错与自查方法:常见的坑都在这里
10.1 null 值参加计算的隐性错误
Hive 里 null 参与任何算术运算,结果都是 null。比如金额 amount 字段允许为空,sum(amount) 会把 null 跳过,但如果做 amount * 0.9 就变成 null 了,下游一聚合就少了很多数据。这点是最容易被忽视的数据质量黑洞。我处理这类问题几乎成了惯例:凡是可空字段要做运算,先 coalesce 给默认值,明确业务语义里 0 和 null 分别代表什么。
还有一个场景是 left join 后右表字段大量为 null,直接用这些字段判断会得到错误结论。我之前排查过一个新增用户指标,就是因为状态字段为 null,用 is not null 判断漏掉了大量实际用户。后来改用 nvl(status, 'unknown') 统一归到未知类,指标才恢复正常。
10.2 类型转换问题导致的数据异常
Hive 里隐式类型转换规则和常见数据库不完全一样。字符串和整型比较时,如果字符串不是合法数字,会发生异常。比如把订单号字段当字符串和数字 123 比较,可能匹配不到任何数据。更稳妥的做法是显式 cast,把类型控制权抓在自己手里,别依赖 Hive 自动转。
我在早期写关联查询时,两边关联键类型不一致,join 完结果比预期少了几十万行,一查是左边 string 右边 int,Hive 转换后很多值匹配不上。解决方式是统一用 cast(uid as string) 或 cast(uid as bigint),让两边类型对齐。这是离线任务里最隐蔽也最致命的一类问题。
10.3 函数嵌套导致的可维护性陷阱
函数能嵌套固然好,但嵌套层数过多会让人难以阅读和排错。比如一个很长的字符串处理脚本,一眼望去全是一层层括号。我的习惯是分步操作,先用一个子查询或临时表把中间结果算出来,再做下一步加工。这样调试的时候能准确定位是哪一层逻辑出了问题,不用把整个表达式拽进各种调试工具里慢慢试。
别人接手你的代码时,也会更愿意维护这种分步写法。数据开发不只看运行结果,代码可读性直接关系到长期维护成本。嵌套超过三层就该考虑拆解,这是我给自己定的硬规矩。
最后分享一点我的使用体会
做 Hive 开发这几年,我深刻体会到函数这东西不是背出来的,是用出来的。遇到一个新需求,先想想它在数据形态上做了什么变换,再去对应地找函数,比死记硬背强得多。我也建议你把自己最常用的函数和踩过的坑整理成一个速查笔记,日积月累,写脚本的速度和质量会有肉眼可见的提升。每个任务跑完不是结束,把过程内化成素材才是长期效率的来源。