Hive 面试
Hive 面试
Hive 简介
【简单】什么是 Hive?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 概述
💎 关键结论
Hive 是构建在 Hadoop 之上的分布式数据仓库,把结构化数据文件映射成表,用类 SQL(HQL)做大规模离线分析,SQL 会被翻译成 MapReduce 作业提交到 Hadoop 运行。它上手门槛低、支持 UDF 扩展、元数据统一可被多引擎共享,但执行延迟高,只适合离线批处理,不适合实时场景。
⚡记忆卡片
- 口诀:Hadoop 上的 SQL 数仓,离线分析、高延迟、高吞吐
- 关键词:数据仓库 / HQL / MapReduce / HMS / UDF / 离线批处理
- 链路:HQL → 编译为 MapReduce → Hadoop 集群执行 → 结果返回
📖 核心知识
Apache Hive 是一种分布式、容错数据仓库,支持大规模分析。Hive Metastore(HMS)提供了一个元数据的中央存储库,可以轻松分析以做出明智的数据驱动决策,因此它是许多数据湖架构的关键组件。Hive 构建在 Apache Hadoop 之上,并通过 HDFS 支持在 S3、ADLS、GS 等上进行存储。Hive 允许用户使用 SQL 读取、写入和管理 PB 级数据。
Hive 可以将结构化的数据文件映射成表,并提供类 SQL 查询功能。用于查询的 SQL 语句会被转化为 MapReduce 作业,然后提交到 Hadoop 上运行。
特点:
- 简单、容易上手(提供了类似 SQL 的查询语言 HQL),使得精通 SQL 但是不了解 Java 编程的人也能很好地进行大数据分析;
- 灵活性高,可以自定义用户函数(UDF)和存储格式;
- 为超大的数据集设计的计算和存储能力,集群扩展容易;
- 统一的元数据管理,可与 Presto/Impala/SparkSQL 等共享数据;
- 执行延迟高,不适合做数据的实时处理,但适合做海量数据的离线处理。
🔀 发散问题
- Q:Hive 适合实时查询吗? → 不适合。Hive 面向离线批处理,SQL 会编译为作业执行,启动与调度开销导致延迟高;低延迟交互式查询应使用 Impala、Presto 等 MPP 引擎。
- Q:Hive 与传统关系型数据库的核心区别? → Hive 面向海量数据的离线分析,读时模式、扩展性强但延迟高;RDBMS 面向事务与低延迟点查,写时模式、规模受限。
【简单】什么是 HMS?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 元数据
💎 关键结论
HMS(Hive Metastore)是 Hive 表与分区元数据的中央存储库,元数据落在关系数据库中,通过元存储服务 API 对外提供访问。Hive、Spark、Impala、Presto 等引擎共享同一份元数据,因此 HMS 已成为数据湖生态的事实标准组件。
⚡记忆卡片
- 口诀:表结构存哪里,元数据问 HMS
- 关键词:Metastore / 元数据 / 关系数据库 / 服务 API / 多引擎共享
- 链路:Hive/Spark/Impala/Presto → Metastore API → 关系数据库中的元数据
📖 核心知识
Hive Metastore(HMS)是关系数据库中 Hive 表和分区元数据的中央存储库,它使用元存储服务 API 为客户端(包括 Hive、Impala 和 Spark)提供对此信息的访问。它已成为利用各种开源软件(如 Apache Spark 和 Presto)的数据湖的构建块。事实上,整个工具生态系统,无论是开源的还是其他的,都是围绕 Hive Metastore 构建的,下图说明了其中一些。

🔀 发散问题
- Q:为什么 Spark、Presto 也需要 HMS? → 它们复用 HMS 中统一的表结构与分区元数据,才能与 Hive 读同一份 HDFS 数据,避免各引擎各维护一套元数据。
- Q:HMS 挂掉会有什么影响? → 依赖元数据的查询与 DDL 全部不可用,因此生产上 HMS 通常部署多实例并用关系数据库做高可用。
Hive 存储
【简单】Hive 支持哪些数据类型?⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 数据类型
💎 关键结论
Hive 数据类型分两大类:基本类型覆盖整型、布尔、浮点、定点数、字符串、日期时间与二进制;复杂类型有 STRUCT、MAP、ARRAY 三种。要特别注意 TIMESTAMP 与 TIMESTAMP WITH LOCAL TIME ZONE 的时区行为差异。
⚡记忆卡片
- 口诀:四数值、三字符、两时间,外加 STRUCT/MAP/ARRAY
- 关键词:TINYINT~BIGINT / BOOLEAN / FLOAT/DOUBLE / DECIMAL / STRING/VARCHAR/CHAR / TIMESTAMP/DATE / BINARY / STRUCT/MAP/ARRAY
- 链路:基本类型(数值/布尔/字符串/时间/二进制)+ 复杂类型(STRUCT/MAP/ARRAY)→ 组成表列定义
📖 核心知识
Hive 表中的列支持以下基本数据类型:
| 大类 | 类型 |
|---|---|
| Integers(整型) | TINYINT—1 字节的有符号整数 SMALLINT—2 字节的有符号整数 INT—4 字节的有符号整数 BIGINT—8 字节的有符号整数 |
| Boolean(布尔型) | BOOLEAN—TRUE/FALSE |
| Floating point numbers(浮点型) | FLOAT— 单精度浮点型 DOUBLE—双精度浮点型 |
| Fixed point numbers(定点数) | DECIMAL—用户自定义精度定点数,比如 DECIMAL(7,2) |
| String types(字符串) | STRING—指定字符集的字符序列 VARCHAR—具有最大长度限制的字符序列 CHAR—固定长度的字符序列 |
| Date and time types(日期时间类型) | TIMESTAMP — 时间戳 TIMESTAMP WITH LOCAL TIME ZONE — 时间戳,纳秒精度 DATE—日期类型 |
| Binary types(二进制类型) | BINARY—字节序列 |
TIMESTAMP 和 TIMESTAMP WITH LOCAL TIME ZONE 的区别如下:
- TIMESTAMP WITH LOCAL TIME ZONE:用户提交时间给数据库时,会被转换成数据库所在的时区来保存。查询时则按照查询客户端的不同,转换为查询客户端所在时区的时间。
- TIMESTAMP :提交什么时间就保存什么时间,查询时也不做任何转换。
此外,Hive 还支持以下复杂类型:
| 类型 | 描述 | 示例 |
|---|---|---|
| STRUCT | 类似于对象,是字段的集合,字段的类型可以不同,可以使用 名称。字段名 方式进行访问 | STRUCT ('xiaoming', 12 , '2018-12-12') |
| MAP | 键值对的集合,可以使用 名称 [key] 的方式访问对应的值 | map('a', 1, 'b', 2) |
| ARRAY | 数组是一组具有相同类型和名称的变量的集合,可以使用 名称[index] 访问对应的值 | ARRAY('a', 'b', 'c', 'd') |
🔀 发散问题
- Q:TIMESTAMP 和 TIMESTAMP WITH LOCAL TIME ZONE 怎么选? → 需要跨时区展示用后者(存取自动按时区转换);要求存什么读什么、不做任何转换用 TIMESTAMP。
- Q:STRING、VARCHAR、CHAR 的区别? → STRING 不限长;VARCHAR 有最大长度限制;CHAR 是固定长度,不足补空格。数仓中一般直接用 STRING。
【简单】Hive 支持哪些存储格式?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 存储格式
💎 关键结论
Hive 的数据以文件形式存放在 HDFS 的库表目录下,支持 TextFile、SequenceFile、RCFile、ORC、Avro、Parquet 六种格式;其中 ORC 与 Parquet 综合性能突出,是生产首选。
⚡记忆卡片
- 口诀:文本默认、序列二进制、ORC/Parquet 列存强,分析场景选列存
- 关键词:TextFile / SequenceFile / RCFile / ORC / Avro / Parquet / STORED AS
- 链路:CREATE TABLE ... STORED AS 格式 → 数据以该格式落在 HDFS 表目录
📖 核心知识
Hive 会在 HDFS 为每个数据库上创建一个目录,数据库中的表是该目录的子目录,表中的数据会以文件的形式存储在对应的表目录下。Hive 支持以下几种文件存储格式:
| 格式 | 说明 |
|---|---|
| TextFile | 存储为纯文本文件。 这是 Hive 默认的文件存储格式。这种存储方式数据不做压缩,磁盘开销大,数据解析开销大。 |
| SequenceFile | SequenceFile 是 Hadoop API 提供的一种二进制文件,它将数据以<key,value>的形式序列化到文件中。这种二进制文件内部使用 Hadoop 的标准的 Writable 接口实现序列化和反序列化。它与 Hadoop API 中的 MapFile 是互相兼容的。Hive 中的 SequenceFile 继承自 Hadoop API 的 SequenceFile,不过它的 key 为空,使用 value 存放实际的值,这样是为了避免 MR 在运行 map 阶段进行额外的排序操作。 |
| RCFile | RCFile 文件格式是 FaceBook 开源的一种 Hive 的文件存储格式,首先将表分为几个行组,对每个行组内的数据按列存储,每一列的数据都是分开存储。 |
| ORC Files | ORC 是在一定程度上扩展了 RCFile,是对 RCFile 的优化。 |
| Avro Files | Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro 提供的机制使动态语言可以方便地处理 Avro 数据。 |
| Parquet | Parquet 是基于 Dremel 的数据模型和算法实现的,面向分析型业务的列式存储格式。它通过按列进行高效压缩和特殊的编码技术,从而在降低存储空间的同时提高了 IO 效率。 |
以上压缩格式中 ORC 和 Parquet 的综合性能突出,使用较为广泛,推荐使用这两种格式。
通常在创建表的时候使用 STORED AS 参数指定:
CREATE TABLE page_view(viewTime INT, userid BIGINT)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\001'
COLLECTION ITEMS TERMINATED BY '\002'
MAP KEYS TERMINATED BY '\003'
STORED AS SEQUENCEFILE;各个存储文件类型指定方式如下:
- STORED AS TEXTFILE
- STORED AS SEQUENCEFILE
- STORED AS ORC
- STORED AS PARQUET
- STORED AS AVRO
- STORED AS RCFILE
🔀 发散问题
- Q:TextFile 和 SequenceFile 的主要短板? → TextFile 不压缩、解析开销大;SequenceFile 是 Hadoop 专有二进制格式,跨生态兼容性差,两者都不适合现代分析场景。
- Q:为什么生产数仓基本只在 ORC 与 Parquet 之间二选一? → 两者都是带索引与高压缩的列存格式,支持列裁剪与谓词下推,其余格式在 IO 与压缩上差距明显。
【简单】Hive 中的内部表和外部表有什么区别?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 表管理
💎 关键结论
内部表(管理表)由 Hive 同时管理元数据与数据,DROP 时元数据和文件一起删;外部表用 EXTERNAL 修饰,DROP 只删元数据、HDFS 文件保留。ODS 贴源层用外部表保数据安全,DWD/DWS/ADS 加工层优先内部表统一生命周期。
⚡记忆卡片
- 口诀:内部表删表又删数,外部表只删元数据
- 关键词:管理表 / EXTERNAL / LOCATION / DROP 行为 / ODS 外部表 / warehouse 目录
- 链路:建表(默认内部表,EXTERNAL 建外部表)→ 导入数据(内部表移动,外部表只记位置)→ DROP(内部表删元数据+文件,外部表只删元数据)
📖 核心知识
内部表又叫做管理表 (Managed/Internal Table),创建表时不做任何指定,默认创建的就是内部表。想要创建外部表 (External Table),则需要使用 External 进行修饰。 内部表和外部表主要区别如下:
| 内部表 | 外部表 | |
|---|---|---|
| 数据存储位置 | 内部表数据存储的位置由 hive.metastore.warehouse.dir 参数指定,默认情况下表的数据存储在 HDFS 的 /user/hive/warehouse/数据库名。db/表名/ 目录下 | 外部表数据的存储位置创建表时由 Location 参数指定; |
| 导入数据 | 在导入数据到内部表,内部表将数据移动到自己的数据仓库目录下,数据的生命周期由 Hive 来进行管理 | 外部表不会将数据移动到自己的数据仓库目录下,只是在元数据中存储了数据的位置 |
| 删除表 | 删除元数据(metadata)和文件 | 只删除元数据(metadata) |
方案权衡:
- ODS 层(贴源层):选外部表。原始数据由上游采集任务管理,Hive 只读不删,误执行 DROP TABLE 不会冲掉 HDFS 数据,且方便重跑与多引擎(Spark/Presto)共享同一份数据。
- DWD/DWS/ADS 层(加工层与结果层):优先内部表,生命周期交给 Hive 统一管理,INSERT OVERWRITE 重跑时不会产生无人认领的脏目录;对需要跨引擎只读共享的结果表,再退回外部表。
🔬 扩展知识
扩展知识
- 【L3】Hive 3.x 中内部表/外部表的语义变化:Hive 3.x 引入了 managed 与 external 的细化区分,managed 表 DROP 时数据被删除;external 表 DROP 只删元数据。若建表时指定了
tblproperties("external.table.purge"="true"),DROP 外部表也会删数据。升级 Hive 3 后存量表的类型语义要用DESC FORMATTED逐一核对,避免删表行为与预期不符。 - 【L3】为什么数仓分层中 ODS 层几乎都用外部表:ODS 是贴源层,数据所有权在采集链路(Flume/DataX/Kafka Connect),Hive 只做注册。用外部表可以避免 ETL 任务误删原始数据,支持同一路径被多个引擎/多套表定义共享,重跑任务时直接覆盖分区文件即可,不需要重新导数。
- 【L3】TRUNCATE 和 DROP PARTITION 在两种表上的行为区别:TRUNCATE 仅支持内部表(外部表执行会报错);DROP PARTITION 对内部表会删除分区数据,对外部表默认只删元数据,需加 PURGE 或表级属性才会删文件。做分区级数据清理时要先确认表类型,否则会出现“删了元数据、文件还在”的脏分区。
🏭 实战场景
实战场景:ODS 日志表误建内部表,DROP 连带删除 3TB 原始数据
某团队把 ODS 日志表误建成内部表(建表时漏写 EXTERNAL),一次需求变更执行 DROP TABLE,连带删掉了 HDFS 上 3TB 原始数据,幸好 HDFS Trash 回收站未过期(默认保留 24 小时)才找回。事后规范:ODS 层建表必须带 EXTERNAL 关键字,并在 code review 中强制检查;内部表的 DROP 操作一律走审批。
⚠️ 常见误区
常见误区
- ❌ “外部表怎么操作都不会动数据” →
LOAD DATA INPATH对外部表同样是“移动”语义(文件会被挪到表 LOCATION 下),只有LOAD DATA LOCAL INPATH才是复制。LOAD 之后源目录文件消失就是这个误区。 - ❌ “建表写了 LOCATION 就是外部表” → 忘写
EXTERNAL只写了LOCATION,建出来的仍是内部表,DROP 照样删数据,需用DESC FORMATTED确认Table Type。
🔀 发散问题
- Q:实习生误 DROP 生产依赖表,如何确认损失范围并恢复? → 应急:先
DESC FORMATTED/HMS 审计日志确认表类型——外部表则 HDFS 数据还在原 LOCATION,按备份 DDL 重建并MSCK REPAIR TABLE秒级恢复;内部表则立即检查 HDFS Trash(默认保留 24 小时),用hdfs dfs -mv从回收站移回再重建元数据。根因通常是生产表建在测试库且未加 EXTERNAL;长期方案是生产表统一外部表 + Ranger 收回 DROP/TRUNCATE 权限 + 延长 Trash 保留 + HMS 元数据定期备份;代价是外部表 DROP 后残留文件需定期巡检清理,避免孤儿目录堆积。
【简单】什么是分区表?⭐⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 分区表
💎 关键结论
分区是 HDFS 上表目录的子目录,数据按分区存储。查询 WHERE 命中分区列时,分区裁剪直接读目标子目录而不扫全表,是 Hive 提升查询性能的最常用手段。用 PARTITIONED BY 建分区表,加载数据必须指定分区;动态分区需配套参数并控制分区数量上限。
⚡记忆卡片
- 口诀:分区即子目录,条件即裁剪,不扫全表性能升
- 关键词:PARTITIONED BY / 分区裁剪 / hive.optimize.ppd / 动态分区 / strict 模式 / 小文件
- 链路:PARTITIONED BY 建表 → LOAD/INSERT 指定分区 → HDFS 分区子目录 → WHERE 分区列裁剪只读目标目录
📖 核心知识
Hive 中的表对应为 HDFS 上的指定目录,在查询数据时候,默认会对全表进行扫描,这样时间和性能的消耗都非常大。
分区为 HDFS 上表目录的子目录,数据按照分区存储在子目录中。如果查询的 where 子句中包含分区条件,则直接从该分区去查找,而不是扫描整个表目录,合理的分区设计可以极大提高查询速度和性能。
分区表并非 Hive 独有的概念,实际上这个概念非常常见。通常,在管理大规模数据集的时候都需要进行分区,比如将日志文件按天进行分区,从而保证数据细粒度的划分,使得查询性能得到提升。比如,在我们常用的 Oracle 数据库中,当表中的数据量不断增大,查询数据的速度就会下降,这时也可以对表进行分区。表进行分区后,逻辑上表仍然是一张完整的表,只是将表中的数据存放到多个表空间(物理文件上),这样查询数据时,就不必要每次都扫描整张表,从而提升查询性能。
在 Hive 中可以使用 PARTITIONED BY 子句创建分区表。表可以包含一个或多个分区列,程序会为分区列中的每个不同值组合创建单独的数据目录。下面的我们创建一张雇员表作为测试:
CREATE EXTERNAL TABLE emp_partition(
empno INT,
ename STRING,
job STRING,
mgr INT,
hiredate TIMESTAMP,
sal DECIMAL(7,2),
comm DECIMAL(7,2)
)
PARTITIONED BY (deptno INT) -- 按照部门编号进行分区
ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"
LOCATION '/Hive/emp_partition';加载数据到分区表时候必须要指定数据所处的分区:
# 加载部门编号为 20 的数据到表中
LOAD DATA LOCAL INPATH "/usr/file/emp20.txt" OVERWRITE INTO TABLE emp_partition PARTITION (deptno=20)
# 加载部门编号为 30 的数据到表中
LOAD DATA LOCAL INPATH "/usr/file/emp30.txt" OVERWRITE INTO TABLE emp_partition PARTITION (deptno=30)这时候我们直接查看表目录,可以看到表目录下存在两个子目录,分别是 deptno=20 和 deptno=30, 这就是分区目录,分区目录下才是我们加载的数据文件。
# Hadoop fs -ls hdfs://hadoop001:8020/Hive/emp_partition/这时候当你的查询语句的 where 包含 deptno=20,则就去对应的分区目录下进行查找,而不用扫描全表。
分区裁剪(Partition Pruning):这是分区表性能收益的核心。查询谓词中的分区列能以常量形式下推时,编译期就只枚举命中的分区目录,其余分区完全不读。注意失效条件:分区列被函数包裹时无法裁剪,如 WHERE date_format(dt, 'yyyy-MM') = '2026-08' 会退化为全表扫描,应改写为 dt BETWEEN '2026-08-01' AND '2026-08-31'。hive.optimize.ppd=true(默认开启)控制谓词下推;strict 模式(hive.mapred.mode=strict)下分区表查询不带分区过滤会直接报错拦截。
动态分区参数:INSERT ... PARTITION (dt) 不写死分区值即为动态分区,必须配套以下参数,否则会直接报错:
hive.exec.dynamic.partition=true、hive.exec.dynamic.partition.mode=nonstrict(默认 strict,要求至少一个静态分区);hive.exec.max.dynamic.partitions.pernode=100(单节点上限)、hive.exec.max.dynamic.partitions=1000(全局上限),超限任务直接失败——这是防止一次写歪出几万个分区的保险丝。
小分区过多的代价:每个分区在 Metastore 中是一条记录、在 HDFS 上是一个目录。分区过碎(如按小时分区累积数年)会导致 SHOW PARTITIONS、DDL 和调度变慢,并产生海量小文件。
🔬 扩展知识
扩展知识
- 【L3】分区裁剪发生在哪个阶段:发生在编译期生成 Task 之前,优化器根据常量谓词枚举分区,物理计划里 TableScan 的
filterExpr与 partition pruning 信息会体现裁剪结果。用EXPLAIN查看 partition 信息,或用EXPLAIN DEPENDENCY确认实际读取的分区列表;如果谓词对分区列做了函数运算,裁剪会静默失效,这是最常见的“分区表反而更慢”的原因。 - 【L3】动态分区写入为什么容易产生小文件、怎么治理:每个 mapper/reducer 对每个分区各写一个文件,reducer 数 × 分区数 = 文件数。治理手段:写入侧控制 reducer 数;写入后定期执行
CONCATENATE(ORC 专属,合并 stripe 无需重排数据)或 INSERT OVERWRITE 自身分区重写合并;调度上把“合并小文件”做成下游前置节点。 - 【L4】分区列是逻辑列,不落在数据文件中,其值由目录名(如
dt=2026-08-08)承载,SELECT * 时 Hive 从路径回填。因此分区列必须出现在 PARTITIONED BY 子句且不能与普通列重名;查询时分区列的取值类型与目录字符串一致,隐式类型转换可能导致裁剪失效(如对字符串分区列传 int 常量)。
🏭 实战场景
实战场景:按小时分区累积 1.7 万个分区拖垮元数据操作
某行为日志表按小时分区,两年累积约 1.7 万个分区(24 × 730),SHOW PARTITIONS 要跑 10 分钟,调度系统拉元数据直接超时,夜间任务全线延迟。最终重建为按天分区、小时下沉为普通列(一天 24 个文件远比 24 个分区划算),才解决问题。
⚠️ 常见误区
常见误区
- ❌ “分区越多越好” → 每个分区在 Metastore 中是一条记录、在 HDFS 上是一个目录,分区过碎会让元数据膨胀,SHOW PARTITIONS、DDL 和调度变慢,并产生海量小文件。
- ❌ “WHERE 带了分区列条件就一定裁剪” → 分区列被函数包裹时无法裁剪,如
WHERE date_format(dt, 'yyyy-MM') = '2026-08'会退化为全表扫描,应改写为dt BETWEEN '2026-08-01' AND '2026-08-31'。 - ❌ “分区列取值随便传” → 分区列取值是目录字符串,对字符串分区列传 int 常量会触发隐式类型转换,裁剪可能失效。
🔀 发散问题
- Q:按天分区表近期分区查询劣化怎么排查? → 对慢分区
hdfs dfs -ls看文件数和大小——大概率是上游写入并行度失控,单分区出现上千个几 MB 的小文件,NameNode 寻址与 split 调度开销暴增。临时对目标分区 INSERT OVERWRITE 自身重写合并;长期固定写入并行度、分区落地后自动触发合并、监控加“单分区文件数 > 500 告警”。
【简单】什么是分桶表?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 分桶表
💎 关键结论
分桶表将指定列的值哈希散列后对桶数取余,把数据拆分到多个文件(桶)中,是比分区更细粒度的数据拆分方案。用 CLUSTERED BY 指定分桶列、INTO N BUCKETS 指定桶数;数据必须经 MapReduce 写入(LOAD 不会分桶),通常用 CTAS 或 INSERT 插入。
⚡记忆卡片
- 口诀:分区拆目录,分桶拆文件;哈希取模定桶位
- 关键词:CLUSTERED BY / SORTED BY / INTO N BUCKETS / 哈希取余 / hive.enforce.bucketing / CTAS
- 链路:CLUSTERED BY 指定分桶列 → 哈希散列对桶数取余 → 落入对应桶文件
📖 核心知识
分区提供了一个隔离数据和优化查询的可行方案,但是并非所有的数据集都可以形成合理的分区,分区的数量也不是越多越好,过多的分区条件可能会导致很多分区上没有数据。同时 Hive 会限制动态分区可以创建的最大分区数,用来避免过多分区文件对文件系统产生负担。鉴于以上原因,Hive 还提供了一种更加细粒度的数据拆分方案:分桶表 (bucket Table)。
分桶表会将指定列的值进行哈希散列,并对 bucket(桶数量)取余,然后存储到对应的 bucket(桶)中。
单从概念上理解分桶表可能会比较晦涩,其实和分区一样,分桶这个概念同样不是 Hive 独有的,对于 Java 开发人员而言,这可能是一个每天都会用到的概念,因为 Hive 中的分桶概念和 Java 数据结构中的 HashMap 的分桶概念是一致的。
当调用 HashMap 的 put() 方法存储数据时,程序会先对 key 值调用 hashCode() 方法计算出 hashcode,然后对数组长度取模计算出 index,最后将数据存储在数组 index 位置的链表上,链表达到一定阈值后会转换为红黑树 (JDK1.8+)。下图为 HashMap 的数据结构图:

图片引用自:HashMap vs. Hashtable
在 Hive 中,我们可以通过 CLUSTERED BY 指定分桶列,并通过 SORTED BY 指定桶中数据的排序参考列。下面为分桶表建表语句示例:
CREATE EXTERNAL TABLE emp_bucket(
empno INT,
ename STRING,
job STRING,
mgr INT,
hiredate TIMESTAMP,
sal DECIMAL(7,2),
comm DECIMAL(7,2),
deptno INT)
CLUSTERED BY(empno) SORTED BY(empno ASC) INTO 4 BUCKETS --按照员工编号散列到四个 bucket 中
ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"
LOCATION '/Hive/emp_bucket';这里直接使用 Load 语句向分桶表加载数据,数据时可以加载成功的,但是数据并不会分桶。
这是由于分桶的实质是对指定字段做了 hash 散列然后存放到对应文件中,这意味着向分桶表中插入数据是必然要通过 MapReduce,且 Reducer 的数量必须等于分桶的数量。由于以上原因,分桶表的数据通常只能使用 CTAS(CREATE TABLE AS SELECT) 方式插入,因为 CTAS 操作会触发 MapReduce。加载数据步骤如下:
(1)设置强制分桶
set hive.enforce.bucketing = true; --Hive 2.x 不需要这一步在 Hive 0.x and 1.x 版本,必须使用设置 hive.enforce.bucketing = true,表示强制分桶,允许程序根据表结构自动选择正确数量的 Reducer 和 cluster by column 来进行分桶。
(2)CTAS 导入数据
INSERT INTO TABLE emp_bucket SELECT * FROM emp; --这里的 emp 表就是一张普通的雇员表可以从执行日志看到 CTAS 触发 MapReduce 操作,且 Reducer 数量和建表时候指定 bucket 数量一致:

查看分桶文件
bucket(桶) 本质上就是表目录下的具体文件:

🔬 扩展知识
扩展知识
- 【L3】分桶与 Java HashMap 的分桶原理一致:调用 HashMap 的 put() 方法时,程序先对 key 调用 hashCode() 计算哈希值,再对数组长度取模得到 index,将数据存到对应位置;Hive 分桶同样是“哈希散列 + 取余落桶”。
- 【L3】分桶表的典型用途:在无法形成合理分区的场景提供细粒度拆分;配合分桶键 JOIN 优化(桶数对齐时可做桶级 map join);Hive 事务表强制要求分桶。
🔀 发散问题
- Q:为什么 LOAD 语句加载到分桶表的数据不会分桶? → 分桶的实质是对指定字段做 hash 散列后写入对应文件,必须经过 MapReduce 且 Reducer 数等于桶数;LOAD 只是文件移动不触发计算,因此分桶表数据通常用 CTAS 或 INSERT 写入。
- Q:分区和分桶如何选择? → 时间、地区等枚举明确的维度用分区(目录级、可裁剪);高基数列或需要采样、JOIN 优化、事务时用分桶(文件级);两者也可组合,见本文档『分区和分桶可以一起使用吗?』。
【简单】分区和分桶可以一起使用吗?⭐
🎯 目标等级:L2 | ⏱ 建议用时:5 min | 🏷 标签:Hive / 分区 + 分桶
💎 关键结论
可以。分区与分桶的本质都是将数据按不同粒度拆分,使查询不必扫描全表;两者组合可同时保证多粒度上的合理拆分:PARTITIONED BY 指定分区,CLUSTERED BY 指定分桶。
⚡记忆卡片
- 口诀:分区定目录,分桶定文件,组合使用粒度更细
- 关键词:PARTITIONED BY + CLUSTERED BY / 两级拆分 / 写入指定分区
- 链路:PARTITIONED BY 建分区目录 → CLUSTERED BY INTO N BUCKETS 分区内拆桶文件 → INSERT 写入时指定分区
📖 核心知识
分区表和分桶表的本质都是将数据按照不同粒度进行拆分,从而使得在查询时候不必扫描全表,只需要扫描对应的分区或分桶,从而提升查询效率。两者可以结合起来使用,从而保证表数据在不同粒度上都能得到合理的拆分。下面是 Hive 官方给出的示例:
CREATE TABLE page_view_bucketed(
viewTime INT,
userid BIGINT,
page_url STRING,
referrer_url STRING,
ip STRING )
PARTITIONED BY(dt STRING)
CLUSTERED BY(userid) SORTED BY(viewTime) INTO 32 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\001'
COLLECTION ITEMS TERMINATED BY '\002'
MAP KEYS TERMINATED BY '\003'
STORED AS SEQUENCEFILE;此时导入数据时需要指定分区:
INSERT OVERWRITE page_view_bucketed
PARTITION (dt='2009-02-25')
SELECT * FROM page_view WHERE dt='2009-02-25';🔀 发散问题
- Q:分区 + 分桶组合的收益是什么? → 分区先做目录级裁剪减少扫描范围,分区内再由桶文件提供更细粒度的拆分与并行度,两层叠加让大表查询与 JOIN 更高效。
- Q:组合使用时写入要注意什么? → 写入必须指定分区(静态或动态),分桶由计算引擎按 CLUSTERED BY 自动完成,Reducer 数量对齐桶数。
【中等】ORC 与 Parquet 列式存储的原理是什么?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:Hive / 列式存储
💎 关键结论
列存用“只读所需列 + 同列高压缩 + 索引跳过”三招把分析查询的 IO 打到最低。ORC 按 Stripe + Row Group 组织、带行组统计与 Bloom Filter,与 Hive 生态融合深、支持 ACID;Parquet 按 Row Group → Column Chunk → Page 三层组织,生态中立、嵌套结构友好。纯 Hive 数仓选 ORC,多引擎混读选 Parquet。
⚡记忆卡片
- 口诀:列存只读所需列,同列压缩索引跳;ORC 贴 Hive,Parquet 跨引擎
- 关键词:Stripe / Row Group / Page / 列裁剪 / 谓词下推 / Bloom Filter / 字典编码 / ACID
- 链路:只读所需列 → 同列高压缩 → 文件内统计索引跳过无关块
📖 核心知识
为什么列存:分析型查询通常只读少数列,行存必须读取整行,列存将同一列的数据连续存放,只读需要的列,大幅减少 IO;且同列数据类型相同、取值相似,压缩率高(可达行存的 1/5~1/10)。
ORC(Optimized Row Columnar):
- 数据按 Stripe(默认 250MB)组织,每个 Stripe 内按列存储(Stream),是并行读取的最小单元。
- 每 10000 行一个 Row Group,记录 min/max/count 等统计信息(行组索引),配合谓词下推可跳过不匹配的行组。
- 文件尾部 Footer 记录每列的类型、行数、统计值,支持快速裁剪;还支持 Bloom Filter 加速等值过滤。
Parquet:
- 文件按 Row Group → Column Chunk → Page 三层组织,Page 是压缩和编码(字典编码、RLE、位打包)的基本单元。
- 基于 Google Dremel 的嵌套模型,用 repetition level / definition level 支持复杂嵌套结构。
- 生态中立,Spark、Impala、Flink、Presto 等引擎均原生支持。
选型:ORC 与 Hive 生态融合更深(支持 ACID、轻量索引);Parquet 生态兼容性更广。两者均支持谓词下推与列裁剪,是生产首选。
一句话总结:列存用“只读所需列 + 同列高压缩 + 索引跳过”三招,把分析查询的 IO 成本打到最低。
ORC vs Parquet 选型权衡:
| 维度 | ORC | Parquet |
|---|---|---|
| ACID 事务 | Hive 原生支持(事务表必须 ORC + 分桶) | 不支持 |
| 轻量索引 | Stripe 统计 + Row Group 索引 + Bloom Filter,更细 | Page 级统计,依赖引擎实现 |
| 生态 | Hive 系绑定深;Impala 对 ORC 支持长期较弱 | Spark/Impala/Flink/Presto 全生态中立 |
| 嵌套结构 | 支持一般 | Dremel 模型(rep/def level)对深层嵌套更友好 |
| 默认块粒度 | Stripe 默认 250MB | Row Group 默认 128MB |
选型结论:纯 Hive 数仓、需要 ACID 更新(CDC 入仓)选 ORC;多引擎混读、数据要出 Hive 生态(Spark/Iceberg/Hudi)选 Parquet。压缩率两者同量级(实测互有胜负,通常相差 10% 以内),不构成决定性因素。
🔬 扩展知识
扩展知识
- 【L3】为什么 ORC 的 ACID 必须依赖分桶表:Hive 事务的锁粒度、delta 合并与 compaction 调度都按 bucket 文件组织,UPDATE/DELETE 只需重写受影响分桶内的文件,读时 merge 也按桶对齐 base 与 delta;不分桶则无法定位“受影响的数据块”,只能整分区重写,成本不可接受,所以事务表强制 CLUSTERED BY。
- 【L3】列存为什么不适合高频点查和频繁更新:列存一次 IO 读一列的连续块,单行点查也要加载整个 stripe/row group 的相关 page,放大严重;更新则需要重写整列数据块,ORC ACID 也是靠 delta + 读时合并绕开原地修改。高频点查请用 HBase/Redis,频繁更新请评估 Kudu 或 Iceberg。
- 【L4】Stripe 和 Row Group 的大小权衡:块越大顺序读吞吐越高、footer 索引越省,但单块内过滤跳过的粒度变粗,且失败重算成本更高;块太小则索引膨胀、列裁剪收益下降。生产上 ORC Stripe 保持默认 250MB 即可,Parquet Row Group 常对齐 HDFS Block(128MB)减少跨块读。
🏭 实战场景
实战场景:跟风全量 Parquet,文件比隔壁 ORC 表大 30%
某团队跟风全量采用 Parquet,上线后发现同等数据量下文件比隔壁 ORC 表大 30%。排查后定位根因:表里 70% 的列是高基数字符串(user_id 为 UUID),字典编码失效后 Parquet 退回普通编码,而 ORC 的 RLE + ZLIB 组合在低基数列上压缩更狠。最终对高基数明细列关闭 Parquet 字典编码、对需要 ACID 的表迁回 ORC,存储空间才回落正常。
⚠️ 常见误区
常见误区
- ❌ “列存索引一定生效” → ORC 的 min/max 统计要发挥作用,前提是文件内数据按谓词列大致有序;上游乱序写入时每个 Row Group 的 min/max 都覆盖全值域,索引退化为全扫描——同样的 SQL,有序表能跳过 95% 的行组,乱序表一个都跳不过。
- ❌ “Parquet 字典编码总是省空间” → Page 内 distinct 值过多时字典膨胀,触发 fallback 反而多一次编码开销;uuid、trace_id 这类高基数列应显式关闭字典编码。
🔀 发散问题
- Q:一张 ORC 表等值谓词查询扫描了 80% 的文件怎么排查? → 用
EXPLAIN确认谓词下推是否生效(看 TableScan 的 filterExpr),对比文件数与单文件大小——慢表大概率是几千个几 MB 的小文件且未按谓词列有序。修复:写入侧合并小文件并按高频过滤列排序写入、对等值过滤列开启orc.bloom.filter.columns、历史分区定期 CONCATENATE;代价是排序写入增加 shuffle 成本,Bloom Filter 增加约 1%-5% 存储开销且只对等值过滤有效。
Hive 索引
【中等】Hive 的索引是如何工作的?⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 索引
💎 关键结论
Hive 索引(0.7.0 引入)会在指定列上生成一张索引表,记录索引列的值、对应的 HDFS 文件路径及值在文件中的偏移量;带谓词的查询先到索引表定位目标数据块,避免全表扫描。索引通过 CREATE/SHOW/DROP/ALTER INDEX 管理。
⚡记忆卡片
- 口诀:索引表 = 值 + 文件路径 + 偏移量,查询先查索引再读数据
- 关键词:CREATE INDEX / 索引表 / _bucketname / _offsets / REBUILD / SHOW FORMATTED INDEX
- 链路:CREATE INDEX 生成索引表 → 查询谓词命中索引列 → 索引表查文件路径与偏移量 → 只读目标数据块
📖 核心知识
Hive 在 0.7.0 引入了索引的功能,索引的设计目标是提高表某些列的查询速度。如果没有索引,带有谓词的查询(如'WHERE table1.column = 10')会加载整个表或分区并处理所有行。但是如果 column 存在索引,则只需要加载和处理文件的一部分。
在指定列上建立索引,会产生一张索引表(表结构如下),里面的字段包括:索引列的值、该值对应的 HDFS 文件路径、该值在文件中的偏移量。在查询涉及到索引字段时,首先到索引表查找索引列值对应的 HDFS 文件路径及偏移量,这样就避免了全表扫描。
+--------------+----------------+----------+--+
| col_name | data_type | comment |
+--------------+----------------+----------+--+
| empno | int | 建立索引的列 |
| _bucketname | string | HDFS 文件路径 |
| _offsets | array<bigint> | 偏移量 |
+--------------+----------------+----------+--+创建索引:
CREATE INDEX index_name --索引名称
ON TABLE base_table_name (col_name, ...) --建立索引的列
AS index_type --索引类型
[WITH DEFERRED REBUILD] --重建索引
[IDXPROPERTIES (property_name=property_value, ...)] --索引额外属性
[IN TABLE index_table_name] --索引表的名字
[
[ ROW FORMAT ...] STORED AS ...
| STORED BY ...
] --索引表行分隔符 、 存储格式
[LOCATION hdfs_path] --索引表存储位置
[TBLPROPERTIES (...)] --索引表表属性
[COMMENT "index comment"]; --索引注释查看索引:
--显示表上所有列的索引
SHOW FORMATTED INDEX ON table_name;删除索引:
删除索引会删除对应的索引表。
DROP INDEX [IF EXISTS] index_name ON table_name;如果存在索引的表被删除了,其对应的索引和索引表都会被删除。如果被索引表的某个分区被删除了,那么分区对应的分区索引也会被删除。
重建索引:
ALTER INDEX index_name ON table_name [PARTITION partition_spec] REBUILD;重建索引。如果指定了 PARTITION,则仅重建该分区的索引。
🔬 扩展知识
扩展知识
- 【L3】索引表与基表的生命周期联动:删除索引会删除对应的索引表;基表被删除时其索引和索引表一并删除;基表某个分区被删除时,该分区对应的分区索引也会被删除。
- 【L3】重建成本:数据新增或删除后需
ALTER INDEX ... REBUILD重新执行作业生成索引数据;指定PARTITION可仅重建单个分区的索引。
🔀 发散问题
- Q:Hive 索引为什么最终被弃用? → 索引表无法自动 rebuild、维护成本高,Hive 从 3.0 开始移除索引功能,详见本文档『Hive 索引有什么缺陷?』。
- Q:索引表与列存格式的轻量索引有何区别? → ORC/Parquet 内置文件级统计与索引(min/max、Bloom Filter),写入时自动生成、无需单独维护,已取代传统索引表成为主流方案。
【中等】Hive 索引有什么缺陷?⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 索引
💎 关键结论
索引表最大的缺陷是无法自动 rebuild:表数据新增或删除后必须手动重建索引。正因维护成本高,Hive 从 3.0 开始移除索引功能,由支持自动重写的物化视图和列存格式的选择性扫描替代。
⚡记忆卡片
- 口诀:索引不自动重建,维护成本高,3.0 直接移除
- 关键词:手动 rebuild / 3.0 移除 / 物化视图 / 列存选择性扫描
- 链路:数据变更 → 手动 REBUILD → 维护成本高 → Hive 3.0 移除,物化视图 + 列存轻量索引替代
📖 核心知识
索引表最主要的一个缺陷在于:索引表无法自动 rebuild,这也就意味着如果表中有数据新增或删除,则必须手动 rebuild,重新执行 MapReduce 作业,生成索引表数据。
同时按照 官方文档 的说明,Hive 会从 3.0 开始移除索引功能,主要基于以下两个原因:
- 具有自动重写的物化视图 (Materialized View) 可以产生与索引相似的效果(Hive 2.3.0 增加了对物化视图的支持,在 3.0 之后正式引入)。
- 使用列式存储文件格式(Parquet,ORC)进行存储时,这些格式支持选择性扫描,可以跳过不需要的文件或块。
ORC 内置的索引功能可以参阅这篇文章:Hive 性能优化之 ORC 索引–Row Group Index vs Bloom Filter Index
🔬 扩展知识
扩展知识
- 【L3】移除原因一:具有自动重写的物化视图(Materialized View)可以产生与索引相似的效果(Hive 2.3.0 增加了对物化视图的支持,在 3.0 之后正式引入)。
- 【L3】移除原因二:使用列式存储文件格式(Parquet、ORC)时,这些格式支持选择性扫描,可以跳过不需要的文件或块。
- 【L4】替代方案:用 ORC 的 Row Group Index/Bloom Filter 或 Parquet 的统计信息实现跳过扫描,写入时自动生成、无需维护独立索引表。
🔀 发散问题
- Q:没有索引时如何加速谓词查询? → 选列存(ORC/Parquet)+ 谓词下推跳过无关文件与块;对固定聚合查询用物化视图自动重写。
- Q:升级 Hive 3.0 前存量索引怎么办? → 3.0 移除索引功能,升级前需清理存量索引,并评估物化视图或列存索引的替代方案。
【中等】Hive 有哪些高频窗口函数?如何使用?⭐⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:Hive / 窗口函数
💎 关键结论
窗口函数语法为 函数() OVER (PARTITION BY ... ORDER BY ... [ROWS BETWEEN ...]),在保留明细行的前提下对分组内数据做计算。高频三类:排名函数 row_number/rank/dense_rank/ntile,偏移函数 lag/lead/first_value/last_value,聚合窗口 sum/avg/count/max/min。分组 TopN 用 row_number,环比用 lag。
⚡记忆卡片
- 口诀:排名 row_number,环比 lag/lead,TopN 外层 rn<=N
- 关键词:ROW_NUMBER / RANK / DENSE_RANK / NTILE / LAG/LEAD / ROWS BETWEEN / 滑动窗口
- 链路:OVER(PARTITION BY 分组 ORDER BY 排序 [ROWS BETWEEN 帧]) → 保留明细行附加计算列
📖 核心知识
窗口函数语法为 函数() OVER (PARTITION BY ... ORDER BY ... [ROWS BETWEEN ...]),在保留明细行的前提下对分组内数据做计算。
排名函数:
| 函数 | 行为 | 示例(分数 90,90,80) |
|---|---|---|
ROW_NUMBER() | 连续不重复编号 | 1,2,3 |
RANK() | 并列后跳号 | 1,1,3 |
DENSE_RANK() | 并列不跳号 | 1,1,2 |
NTILE(n) | 均分为 n 组 | 按组分桶 |
偏移函数:
LAG(col, n, default)- 取当前行前 n 行的值;LEAD(col, n, default)- 取后 n 行的值。常用于计算环比/同比。FIRST_VALUE()/LAST_VALUE()- 取窗口内首/末行的值。
聚合窗口:SUM/AVG/COUNT/MAX/MIN() OVER (...),配合 ROWS BETWEEN ... PRECEDING AND ... FOLLOWING 可做滑动窗口(如近 7 日累计)。
典型应用:
-- 每个部门工资 Top 3
SELECT * FROM (
SELECT empno, deptno, sal,
ROW_NUMBER() OVER (PARTITION BY deptno ORDER BY sal DESC) AS rn
FROM emp) t WHERE rn <= 3;可运行完整示例(salary 取值:90,90,80,用于辨析 rank 三者差异):
-- 1. 三个排名函数对并列值的差异
SELECT empno, sal,
ROW_NUMBER() OVER (ORDER BY sal DESC) AS rn, -- 1,2,3(并列也不重复)
RANK() OVER (ORDER BY sal DESC) AS rk, -- 1,1,3(并列后跳号)
DENSE_RANK() OVER (ORDER BY sal DESC) AS dr -- 1,1,2(并列不跳号)
FROM emp;
-- 2. lag/lead 计算工资环比涨幅(上一行/下一行对比)
SELECT empno, sal,
LAG(sal, 1, 0) OVER (ORDER BY hiredate) AS prev_sal,
LEAD(sal, 1, 0) OVER (ORDER BY hiredate) AS next_sal
FROM emp;
-- 3. sum over 滑动窗口:按入职序的累计工资与近 3 人滚动均值
SELECT empno, sal,
SUM(sal) OVER (ORDER BY hiredate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS cum_sal,
AVG(sal) OVER (ORDER BY hiredate
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS avg3_sal
FROM emp;方案权衡:分组 TopN 用 row_number(并列时结果确定、行数可控);需要保留并列名次用 dense_rank;连续分段(如分位数分档)用 NTILE。环比计算用 lag/lead 比自关联(self join)少一次 shuffle,数据量大时快一个量级。
失效场景:
- 窗口帧默认是 RANGE 不是 ROWS:
SUM(sal) OVER (ORDER BY hiredate)不写 ROWS 子句时默认RANGE UNBOUNDED PRECEDING,ORDER BY 字段有重复值时并列行会算出相同的累计值,导致"累计金额"出现平台期,必须显式ROWS BETWEEN ... PRECEDING AND CURRENT ROW。 - PARTITION BY 键含 NULL:所有 NULL 行被划进同一个分区参与计算,产生意外的超大分组。
- ORDER BY 字段含 NULL:升序时 NULL 默认排在最前,LAG/LEAD 与 ROW_NUMBER 结果与业务预期不符。
踩坑案例:生产上计算"用户复购间隔",LAG 结果大面积为 NULL。排查发现上游日志表约 12% 的订单 time 字段缺失,NULL 在 ORDER BY 升序中全部排在最前,LAG 取到的是 NULL 行而非上一笔真实订单。修复:窗口计算前先过滤或 COALESCE 补默认时间,并在接入层加非空校验,指标当天恢复正常。
一句话总结:窗口函数是 Hive SQL 的灵魂,排名用 row_number,环比用 lag,分组 TopN 是面试必考题。
🔬 扩展知识
扩展知识
- 【L3】窗口函数不会改变结果集行数:它只在原明细行上附加一列计算结果,行数与输入完全一致,与 GROUP BY 的压缩聚合互为对照;真正炸开行数的是 LATERAL VIEW explode。因此 TopN 取数必须外层包一层子查询用
WHERE rn <= N裁剪,不能指望窗口函数自己减少行数。 - 【L3】PARTITION BY 键基数极高时的表现:每个分组只有极少行时窗口计算退化为逐行计算,性能没问题但语义易踩坑(LAG 永远取不到值);反过来若某分区键对应数据量极大(典型如 NULL 聚成一个超大分区),单 task 处理该分区会形成窗口计算倾斜,需先过滤异常键或先聚合到合适粒度再开窗。
- 【L4】ROWS 与 RANGE 窗口帧的语义差异:ROWS 按物理行偏移计数(
2 PRECEDING就是往前数 2 行);RANGE 按 ORDER BY 值的逻辑区间匹配,并列值会被一起纳入。默认帧是 RANGE UNBOUNDED PRECEDING,所以 ORDER BY 有重复值时累计类指标必须显式指定 ROWS 帧,否则同值行结果相同,报表出现“平台期”假象。
🏭 实战场景
实战场景:LAG 结果大面积为 NULL,根因是上游 12% 时间字段缺失
生产上计算“用户复购间隔”,LAG 结果大面积为 NULL。排查发现上游日志表约 12% 的订单 time 字段缺失,NULL 在 ORDER BY 升序中全部排在最前,LAG 取到的是 NULL 行而非上一笔真实订单。修复:窗口计算前先过滤或 COALESCE 补默认时间,并在接入层加非空校验,指标当天恢复正常。
⚠️ 常见误区
常见误区
- ❌ “不写 ROWS 子句就是按行累计” → 窗口帧默认是 RANGE 不是 ROWS:
SUM(sal) OVER (ORDER BY hiredate)默认RANGE UNBOUNDED PRECEDING,ORDER BY 字段有重复值时并列行会算出相同的累计值,必须显式ROWS BETWEEN ... PRECEDING AND CURRENT ROW。 - ❌ “PARTITION BY 键含 NULL 无所谓” → 所有 NULL 行会被划进同一个分区参与计算,产生意外的超大分组。
- ❌ “ORDER BY 里的 NULL 不影响结果” → 升序时 NULL 默认排在最前,LAG/LEAD 与 ROW_NUMBER 结果会与业务预期不符。
🔀 发散问题
- Q:如何统计每个用户连续登录的最大天数? → 先 DISTINCT 去掉一天多条,再用“日期 - 序号”恒等式(连续日期相减得到相同 grp)识别连续段,两层 GROUP BY 得到最长连续段:
SELECT user_id, MAX(cont_days) FROM (SELECT user_id, grp, COUNT(*) AS cont_days FROM (SELECT user_id, login_date, DATE_SUB(login_date, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date)) AS grp FROM (SELECT DISTINCT user_id, login_date FROM user_login) t1) t2 GROUP BY user_id, grp) t3 GROUP BY user_id。陷阱:不去重会导致 ROW_NUMBER 序号错乱、连续天数被低估;login_date 若为带时间的 timestamp 需先 to_date 归一。
【中等】Hive 如何实现行转列和列转行?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 行列转换
💎 关键结论
行转列(多行合一行)用 collect_set/collect_list + concat_ws,条件透视用 CASE WHEN + SUM;列转行(一行拆多行)用 explode + LATERAL VIEW,需要保留空数组或 NULL 原行时用 LATERAL VIEW OUTER explode。两者互为逆操作。
⚡记忆卡片
- 口诀:行转列 collect 加 concat_ws,列转行 explode 配 LATERAL VIEW
- 关键词:collect_set / collect_list / concat_ws / explode / LATERAL VIEW / OUTER / CASE WHEN + SUM
- 链路:行转列 GROUP BY + collect → concat_ws 拼串;列转行 split → explode 展开 → LATERAL VIEW 侧向连接
📖 核心知识
行转列(多行聚合为一行多列):
collect_set(col)- 聚合为去重数组;collect_list(col)- 聚合为不去重数组。concat_ws(sep, array)- 用分隔符将数组拼接为字符串。
-- 将多个城市的行合并为一行逗号分隔的字符串
SELECT id, concat_ws(',', collect_set(city)) AS cities
FROM user_city GROUP BY id;条件行列转换可配合 IF/CASE WHEN + SUM 实现(如将性别行转为男/女两列计数)。
列转行(一行拆为多行):
explode(array)- UDTF,将数组/Map 展开为多行。LATERAL VIEW- 将 explode 的结果与原表每行做侧向连接,保留原列。
-- 将逗号分隔的城市字符串拆成多行
SELECT id, city
FROM user_city
LATERAL VIEW explode(split(cities, ',')) t AS city;一句话总结:行转列靠 collect + concat_ws,列转行靠 explode + LATERAL VIEW,二者互为逆操作。
方案权衡:
- 行转列:collect_set/collect_list + concat_ws 适合"明细合并为分隔串"(如用户标签拼接);CASE WHEN + SUM 适合"枚举值转固定列"的聚合透视(如按性别计数)。前者输出仍是单列字符串,下游需再解析;后者直接产出结构化多列,但枚举值多时会列爆炸。
- 列转行:explode + LATERAL VIEW 是标准方案;需要保留展开前原行(数组为空或 NULL 的行)时,必须用
LATERAL VIEW OUTER explode,否则这些行会被静默丢弃。
失效场景:
collect_set默认过滤 NULL 且不保证顺序,需要顺序输出必须套sort_array;explode遇到空数组/NULL 直接丢弃原行,丢行事故多源于此;- CASE WHEN 透视在枚举值成百上千时产生几百列且每次新增取值都要改 SQL,此时应改用 collect 拼串方案。
🔬 扩展知识
扩展知识
- 【L3】collect_set/collect_list 在倾斜场景下的风险:热点 key(如某大 V 用户关联百万条记录)会让单个 reducer 内存暴涨甚至 OOM,需先过滤异常 key 或对 key 加盐两阶段聚合;若只需要 TopN 标签,应先用窗口函数截断再 collect,避免把超大数组拖进内存。
- 【L3】多个 LATERAL VIEW 叠加的语义:逐行做笛卡尔式展开——一行原文先被第一个 explode 拆成 m 行,每行再被第二个拆成 n 行,最终 m × n 行。标签、城市、渠道三个数组同时展开时行数相乘,务必先评估基数,必要时拆成多个任务分别展开。
- 【L4】posexplode 相比 explode 多了什么:同时输出元素下标 pos 和值,用于需要保留原始顺序/位置的场景(如还原数组下标、按位置对齐两个数组),下游可利用 pos 做精确匹配,避免再用 row_number 重新编号。
🏭 实战场景
实战场景:collect_set 无序引发告警风暴 + explode 丢行 8%
某用户标签拼接任务每次跑出的标签顺序都不一致,下游按字符串做等值比对导致告警风暴。根因是 collect_set 无序,修复为 concat_ws(',', sort_array(collect_set(tag))) 后结果稳定。另一次列转行后日报用户数比前一天少了 8%,排查发现 explode 把 cities 为 NULL 和空串的用户全部丢掉了,改用 LATERAL VIEW OUTER explode 才恢复行数对齐。
⚠️ 常见误区
常见误区
- ❌ “collect_set 输出顺序与输入一致” → collect_set 去重且不保证顺序,需要稳定顺序必须套
sort_array。 - ❌ “explode 只会拆行不会丢行” → explode 遇到空数组/NULL 会直接丢弃原行,需要保留这些行时必须用
LATERAL VIEW OUTER explode。 - ❌ “CASE WHEN 透视万能” → 枚举值成百上千时会产生几百列且新增取值就要改 SQL,此时应改用 collect 拼串方案。
🔀 发散问题
- Q:用户画像表 tags 数组列转行后单 reducer OOM 怎么处理? → 对 tags 数组长度 > 1000 的异常用户先过滤(画像统计场景通常可接受);根因常是 explode 后按 tag GROUP BY 时热门标签占全量三成导致单 reducer 承载数亿行,长期开
hive.groupby.skewindata=true两阶段聚合兜底或对热门标签加盐;代价是截断标签会丢长尾画像精度,两阶段聚合常规任务耗时增加约 20%。
Hive 架构
【中等】Hive SQL 如何执行的?⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 执行流程
💎 关键结论
一条 HQL 从提交到执行要经过语法解析(AST)、语义解析(QueryBlock)、生成与优化逻辑执行计划(OperatorTree)、生成与优化物理执行计划六步,分为编译期与运行期两个阶段,最终交给 MapReduce/Tez/Spark 执行。
⚡记忆卡片
- 口诀:词法语法出 AST,语义解析 QueryBlock,逻辑计划 OperatorTree,物理计划交引擎
- 关键词:Antlr / AST / QueryBlock / OperatorTree / CBO / EXPLAIN
- 链路:SQL → AST → QueryBlock → OperatorTree(逻辑计划)→ 逻辑优化 → 物理计划(Task)→ 物理优化 → 执行引擎
📖 核心知识
Hive 在执行一条 HQL 的时候,会经过以下步骤:
- 语法解析:Antlr 定义 SQL 的语法规则,完成 SQL 词法,语法解析,将 SQL 转化为抽象 语法树 AST Tree;
- 语义解析:遍历 AST Tree,抽象出查询的基本组成单元 QueryBlock;
- 生成逻辑执行计划:遍历 QueryBlock,翻译为执行操作树 OperatorTree;
- 优化逻辑执行计划:逻辑层优化器进行 OperatorTree 变换,合并不必要的 ReduceSinkOperator,减少 shuffle 数据量;
- 生成物理执行计划:遍历 OperatorTree,翻译为 MapReduce 任务;
- 优化物理执行计划:物理层优化器进行 MapReduce 任务的变换,生成最终的执行计划。
关于 Hive SQL 的详细执行流程可以参考美团技术团队的文章:Hive SQL 的编译过程
两阶段拆解(编译期 + 运行期):
- 编译期:Antlr 词法/语法解析生成 AST → 遍历 AST 提炼 QueryBlock → 翻译为 OperatorTree(逻辑计划)→ 逻辑优化(谓词下推、列裁剪、Join 重排序、合并 ReduceSinkOperator)。
- 运行期:OperatorTree 转 Task(MapReduce/Tez/Spark)→ 物理优化器结合 CBO(
hive.cbo.enable=true,默认开启)与表/列统计信息做代价估算,自动决定 Join 顺序与 Join 方式 → 提交执行引擎。
调试与验证手段:EXPLAIN 查看 Stage 数、Join 类型(Map Join 会标注 mapjoin)、TableScan 的过滤与分区裁剪信息;EXPLAIN EXTENDED 看更多元数据细节。小数据量调试可设 hive.exec.mode.local.auto=true,让查询跳过 YARN 排队在本地运行。
踩坑案例:某报表任务平时 20 分钟,某天突然跑了 3 小时。EXPLAIN 一看,整个作业只有 1 个 Reducer——SQL 里用了 COUNT(DISTINCT user_id) 全局去重,单 Reducer 扛全量数据。改写为两阶段:先 GROUP BY user_id 去重(并行),再外层 COUNT(*),任务回到 15 分钟。这是执行计划视角最经典的收益案例。
失效场景:CBO 依赖统计信息,表没做过 ANALYZE TABLE ... COMPUTE STATISTICS 或统计过期时,CBO 退化为基于规则(RBO),Join 顺序可能完全反掉;另外 count distinct 在 MR 引擎下全局去重只落 1 个 Reducer,属于计划层面的结构性瓶颈,参数调优救不了,只能改写 SQL。
🔬 扩展知识
扩展知识
- 【L3】explain 输出中最值得在性能排查时关注的三个关键点:Stage 数量变化(多一个 Stage 意味着多一次 shuffle 与落盘)、Join 算子类型(Map Join 退化成 Sort Merge Join 通常是小表变大超过了
hive.mapjoin.smalltable.filesize默认 25MB 阈值)、TableScan 的 partition/filterExpr(分区裁剪和谓词下推是否生效)。对比“昨天快、今天慢”两天的执行计划差异,往往一眼定位。 - 【L3】谓词下推在什么条件下不生效:谓词作用在被函数/表达式包裹的列上(如
WHERE substr(name,1,3)='abc')无法下推到存储层;TextFile 没有统计信息,下推收益为零,只有 ORC/Parquet 这类带索引的格式才能跳过 stripe/row group;跨表的 OR 条件、NOT 包裹的复杂谓词也可能放弃下推。用hive.optimize.ppd控制开关,用 explain 验证。 - 【L4】Join 条件写在 ON 还是 WHERE 的影响:内连接下两者语义等价,优化器都会尝试下推过滤以减少 shuffle;但 LEFT JOIN 下右表条件放 WHERE 会把 NULL 行过滤掉,实质退化为 INNER JOIN(结果错误),放 ON 才保持左表全量。这既是语义陷阱也是计划陷阱:谓词位置决定了它能否在 join 之前生效。
🏭 实战场景
实战场景:COUNT(DISTINCT) 全局去重导致单 Reducer,3 小时改回 15 分钟
某报表任务平时 20 分钟,某天突然跑了 3 小时。EXPLAIN 一看,整个作业只有 1 个 Reducer——SQL 里用了 COUNT(DISTINCT user_id) 全局去重,单 Reducer 扛全量数据。改写为两阶段:先 GROUP BY user_id 去重(并行),再外层 COUNT(*),任务回到 15 分钟。这是执行计划视角最经典的收益案例。
⚠️ 常见误区
常见误区
- ❌ “CBO 不需要统计信息也能选对计划” → CBO 依赖表/列统计信息做代价估算,没做过
ANALYZE TABLE ... COMPUTE STATISTICS或统计过期时会退化为基于规则(RBO),Join 顺序可能完全反掉。 - ❌ “count distinct 慢可以靠参数调优救” → MR 引擎下全局
count distinct只落 1 个 Reducer,是计划层面的结构性瓶颈,参数救不了,只能改写为两阶段去重或 sum case when。
🔀 发散问题
- Q:同一段 SQL 昨天 30 分钟、今天 4 小时,如何排查? → 对比两天的执行计划(
EXPLAIN)与 YARN 资源视图:Stage 数是否变多、是否出现单 Reducer 长尾、集群是否资源抢占。典型根因三类:数据量突增、上游分布变化产生倾斜 key(如大量 NULL)、统计信息过期导致 CBO 选错 Join 顺序;用 explain 对比 + 源表采样逐一验证,长期接入执行计划基线告警与自动 ANALYZE。
【中等】Hive 的执行引擎有哪些?有什么区别?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:10 min | 🏷 标签:Hive / 执行引擎
💎 关键结论
Hive 本身只是“SQL 转执行计划”的翻译器,实际计算交给执行引擎,通过 hive.execution.engine 指定。MapReduce 稳定但阶段间反复落盘;Tez 用 DAG 合并多阶段、配合 LLAP 支持秒级交互查询,是 Hive 3.x 默认引擎;Spark 以内存计算加速但需单独部署。生产上 Hive 3 + Tez 是主流组合。
⚡记忆卡片
- 口诀:MR 稳但慢,Tez DAG 快,Spark 内存算,生产 Hive 3 配 Tez
- 关键词:hive.execution.engine / MapReduce / Tez / LLAP / Spark / DAG
- 链路:Hive 编译 SQL 生成执行计划 → hive.execution.engine 选择引擎 → MR/Tez/Spark 实际执行
📖 核心知识
Hive 本身只是“SQL 转执行计划”的翻译器,实际计算交给执行引擎,通过 hive.execution.engine 指定:
| 维度 | MapReduce | Tez | Spark |
|---|---|---|---|
| 执行模型 | 每个阶段一个 MR 作业 | DAG 一次执行多阶段 | 内存 DAG |
| 中间数据 | 每阶段落盘 HDFS | 管道化传输,尽量不落盘 | 内存缓存 |
| 启动开销 | 高(每阶段启动 JVM) | 低(容器复用) | 低 |
| 交互式查询 | 不支持 | 支持(配合 LLAP) | 较好 |
- MapReduce - 早期默认引擎,胜在稳定可靠,但复杂 SQL 会被拆成多个 MR 作业,阶段间反复落盘,延迟高。
- Tez - 将多个 MR 阶段合并为一个 DAG 作业,减少落盘和启动开销,配合 LLAP(Live Long And Process)提供常驻服务和数据缓存,支持秒级交互查询。Hive 3.x 的默认引擎。
- Spark - Hive on Spark,利用 Spark 内存计算加速,需单独部署 Spark,维护成本略高。
一句话总结:MR 赢在稳定,Tez 赢在 DAG 与交互,Spark 赢在内存——生产上 Hive 3 + Tez 是主流组合。
🔬 扩展知识
扩展知识
- 【L3】Tez 提速的本质:将多个 MR 阶段合并为一个 DAG 作业,中间数据管道化传输尽量不落盘,容器复用降低启动开销;配合 LLAP(Live Long And Process)提供常驻服务与数据缓存,才实现秒级交互查询。
- 【L3】Hive on Spark 的代价:利用 Spark 内存计算加速,但需单独部署并维护一套 Spark 集群,与数仓既有组件版本兼容成本较高,因此实践中占比低于 Tez。
🔀 发散问题
- Q:什么场景还会继续用 MapReduce 引擎? → 看重稳定可靠、对延迟不敏感的存量批处理任务;但复杂 SQL 会被拆成多个 MR 作业、阶段间反复落盘,新任务不建议再用。
- Q:如何确认当前任务用的哪个引擎? → 看会话或集群的
hive.execution.engine参数,也可从执行日志的作业类型(MR/Tez/Spark)确认。
【困难】Hive 支持事务吗?ACID 是如何实现的?⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:15 min | 🏷 标签:Hive / 事务
💎 关键结论
Hive 从 0.13 开始支持 ACID 事务(INSERT/UPDATE/DELETE),但必须满足 ORC 格式 + 分桶表。实现机制是 MVCC + 基线增量:表目录由 base + 若干 delta 组成,查询时 merge 合并,compaction 定期压实 delta。它只适合低频小批量变更(如 CDC 入仓),不是 OLTP 的替代品。
⚡记忆卡片
- 口诀:ORC + 分桶才能事务,base + delta 做 MVCC,compaction 来压实
- 关键词:ACID / ORC + 分桶 / base/delta / MVCC / TxnManager / compaction / ACID 2.0
- 链路:首次写入生成 base → INSERT/UPDATE/DELETE 生成 delta → 查询时 merge 合并 → minor/major compaction 压实
📖 核心知识
Hive 从 0.13 开始支持 ACID 事务(INSERT/UPDATE/DELETE),但必须满足:ORC 格式 + 分桶表 + 事务专用 SerDe/IO 接口。
实现机制(MVCC + 基线增量):
- 每个事务表目录由 base 目录 + 若干 delta 目录组成。首次写入生成 base,后续 INSERT 生成 delta 目录,UPDATE/DELETE 也会生成 delta(记录变更)。
- 查询时通过 Merge 将 base 与可见 delta 合并成最终视图,实现快照隔离(读不阻塞写)。
- 事务由 TxnManager 管理,锁信息和事务状态存储在 HiveMetastore 中,支持表级/分区级锁。
- Compaction(压实) - minor compaction 将多个 delta 合并为一个,major compaction 将 base 与 delta 合并为新的 base。Hive 2.x 的 UPDATE/DELETE 需全量重写文件,3.0(ACID 2.0)改为只重写受影响的 Stripe,大幅提升更新性能。
局限性:不支持跨表分布式事务、不支持 SELECT FOR UPDATE、事务表读写性能低于普通表,适合低频小批量变更(如 CDC 入仓),不适合高频 OLTP 式写入。
一句话总结:Hive ACID 用“base + delta + compaction”的 MVCC 模型实现,能支撑数仓级小批量更新,但绝非 OLTP 数据库的替代品。
🔬 扩展知识
扩展知识
- 【L3】ACID 为什么必须依赖分桶:Hive 事务的锁粒度、delta 合并与 compaction 调度都按 bucket 文件组织,UPDATE/DELETE 只需重写受影响分桶内的文件,读时 merge 也按桶对齐 base 与 delta;不分桶则无法定位“受影响的数据块”,只能整分区重写,成本不可接受,所以事务表强制 CLUSTERED BY。
- 【L3】ACID 2.0(Hive 3.0)的改进:2.x 的 UPDATE/DELETE 需全量重写文件,3.0 改为只重写受影响的 Stripe,大幅提升更新性能。
- 【L4】局限性边界:不支持跨表分布式事务、不支持 SELECT FOR UPDATE、事务表读写性能低于普通表,适合低频小批量变更(如 CDC 入仓),不适合高频 OLTP 式写入。
🔀 发散问题
- Q:CDC 增量数据入仓怎么用 Hive 事务? → 将 CDC 的 INSERT/UPDATE/DELETE 映射为事务表的对应语句写入 delta,由 compaction 后台压实;注意控制批次频率,避免 delta 过多拖慢读时 merge。
- Q:需要高频更新时该选什么? → Hive 事务不是为高频 OLTP 设计的,高频点查/更新应评估 HBase、Kudu 或 Iceberg/Hudi 等湖仓格式。
Hive 调优
【困难】Hive 中如何定位和调优数据倾斜?⭐⭐⭐⭐⭐
🎯 目标等级:L2 | ⏱ 建议用时:20 min | 🏷 标签:Hive / 调优
💎 关键结论
倾斜调优先定位再优化:看任务卡 99%、EXPLAIN 锁定 Stage、采样统计 Top 热 key、检查 NULL/空串占比。手段优先级:小表用 MapJoin、热 key 加盐或 skewjoin、脏数据先过滤、参数兜底;count distinct 改两阶段去重或 sum case when。
⚡记忆卡片
- 口诀:小表 MapJoin、热 key 加盐、脏数据过滤、参数兜底
- 关键词:MapJoin / hive.optimize.skewjoin / Salting / 过滤 NULL key / sum case when / hive.groupby.skewindata
- 链路:定位(卡 99%/EXPLAIN/采样 Top key)→ 分类(join/聚合/count distinct)→ 选手段(MapJoin→过滤脏数据→加盐→参数兜底)
📖 核心知识
定位手段(先找到倾斜点再谈优化):
- 任务长期卡在 99%,YARN 页面显示个别 Reducer 处理数据量远超其他(如平均 200MB、个别 40GB),其余 task 早已完成。
EXPLAIN看执行计划,确认倾斜发生在 Join、Group By 还是 Count Distinct 环节,锁定具体 Stage。- 看运行日志中该 Reducer 的 key 分布;或直接对源表采样统计:
SELECT join_key, COUNT(*) FROM big_table WHERE dt='...' GROUP BY join_key ORDER BY 2 DESC LIMIT 100,找出 Top 热 key。 - 检查异常 key:
SELECT COUNT(*) FROM big_table WHERE join_key IS NULL OR join_key = ''——生产中大量倾斜的根因是 NULL/空串脏数据而非业务热点。 - count distinct 检查:全局
COUNT(DISTINCT x)在 MR 引擎下只落 1 个 Reducer,属于结构性单点,与数据分布无关。
SQL 层优化手段(含代价对比):
- MapJoin:
hive.auto.convert.join=true+hive.mapjoin.smalltable.filesize(默认 25MB),小表广播到所有 mapper,join 在 map 端完成、无 shuffle。适用边界:一侧表能装进内存;小表超阈值自动转换失效,join 退回 shuffle。代价:占用 mapper 堆内存,多张小表同时广播会挤爆内存。 - 倾斜 Join:
hive.optimize.skewjoin=true+hive.skewjoin.key(默认 100000),运行时把超过阈值的倾斜 key 拆出单独 join 后与普通数据 UNION。适用边界:join 倾斜且无法 mapjoin;代价是拆分 Stage 逻辑复杂,小表 join 场景无效。 - Salting 加盐两阶段聚合:group by 倾斜时给热 key 拼接随机后缀(如
CONCAT(key, '_', CAST(RAND()*10 AS INT)))先做局部聚合,再去盐全局聚合。适用边界:聚合类倾斜(sum/count/group by);代价是多一个 Stage、SQL 复杂度上升。注意:加盐破坏行级明细,join 后需要 row_number 等明细计算时不能直接用。 - 过滤异常 key:join 前把 NULL/空串 key 过滤掉或赋随机值打散(
COALESCE(key, CONCAT('null_', RAND()))),单独处理后再 UNION。适用边界:脏数据型倾斜(最常见,修复最快)。 - sum case when 替代 count distinct:
COUNT(DISTINCT user_id)改写为两阶段(先 group by user_id,再 count(*))或SUM(CASE WHEN ... THEN 1 ELSE 0 END),把单 Reducer 瓶颈打散为并行聚合。
MapReduce 层的 Combiner、自定义 Partitioner 等引擎级手段属于 Hadoop 计算框架范畴,此处不展开,参见 Hadoop 相关文档。
参数级兜底:hive.map.aggr=true(Map 端预聚合,默认开启);hive.groupby.skewindata=true(Group By 倾斜自动两阶段:第一阶段随机分发局部聚合,第二阶段按 key 全局聚合)。
一句话总结:Hive 倾斜调优的口诀是——小表用 MapJoin、热 key 先打散、脏数据先过滤、参数再兜底。
🔬 扩展知识
扩展知识
- 【L3】倾斜 join 用 mapjoin 还是 salting 的决策:先看 join 另一侧表的大小——能装进内存(默认阈值 25MB,可调大但受 mapper 堆限制)直接 mapjoin,从根源消灭 shuffle,代价最低;装不下再看热 key 成因——NULL/空串等脏数据直接过滤或打散最划算;真实业务热点 key(如头部大 V)才上 salting 两阶段或 skewjoin 参数兜底。决策顺序:mapjoin → 过滤脏数据 → 加盐/skewjoin。
- 【L3】为什么 sum case when 能替代 count distinct:count distinct 全局去重只能把所有数据 shuffle 到 1 个 Reducer 去重计数,单点瓶颈;sum case when 是普通聚合,按 group key 并行分发到多个 Reducer,天然无单点。典型改写:
SUM(CASE WHEN gender='M' THEN 1 ELSE 0 END)分性别计数,或先GROUP BY user_id去重再COUNT(*),把去重压力打散。 - 【L4】salting 为什么不能用于需要保留明细的场景:加盐把同一个 key 的数据人为拆到多个 Reducer 做局部聚合,聚合结果(sum/count)可以再合并,但行级明细(如 join 后按 user_id 做 row_number 排序取 TopN)在拆散后已失去全局视图,排序结果必然错误。此类场景应先解决 join 倾斜(mapjoin/过滤脏数据)保住明细,再开窗计算。
🏭 实战场景
实战场景:NULL key 占 60% 导致任务从 3 小时跑到 8 小时
每日用户行为大表 join 商品表的任务稳定跑 3 小时,某天突然跑到 8 小时仍未结束。看日志发现某 Reducer 处理 40GB 而其余只有 200MB;采样 key 分布发现 user_id IS NULL 占当天数据 60%——上游新接入渠道把 user_id 写成了 NULL。止血:NULL 行单独过滤后与主链路 UNION,任务回到 25 分钟;长期:接入层加非空校验 + 数据质量监控。
⚠️ 常见误区
常见误区
- ❌ “倾斜就是调参数的事” → 生产中大量倾斜的根因是 NULL/空串脏数据而非业务热点,先采样定位、过滤脏数据往往比任何参数都见效;参数只是兜底。
- ❌ “salting 可以随便用” → 加盐破坏行级明细,join 后还需要 row_number 等明细计算时不能直接用,应先保明细再开窗。
- ❌ “count distinct 慢是数据分布不均” → 全局
COUNT(DISTINCT x)在 MR 引擎下只落 1 个 Reducer,是结构性单点,与数据分布无关,只能改写 SQL。
🔀 发散问题
- Q:20 亿行大表 join 突然从 3 小时跑到 8 小时,排查路径? → 应急不盲目 kill,先看 YARN 确认是否卡在单个 Reducer,SLA 告急时用昨日分区兜底;定位用
EXPLAIN看倾斜在 join 还是聚合,对当天分区采样 group by join_key 看 Top 100 分布,同时统计 NULL/空串占比;若是脏数据则过滤后单独 UNION,若是真实热 key 则 salting 或开 skewjoin;长期接入层非空校验 + 大小表 join 统一评估 mapjoin,手段按“代价从小到大”依次尝试。