本文总结了大数据处理中数据倾斜问题的本质与解决方案。
数据倾斜指某个Key数据量过大导致任务卡顿,常见于GROUP BY和JOIN操作。
核心解决思路是通过"加盐"将大Key打散到多个Reducer处理,对于聚合倾斜采用两阶段聚合(先局部聚合再全局合并);
对于JOIN倾斜则使用MapJoin或加盐扩容策略。
文中对比了Hive与Oracle处理COUNT(DISTINCT)的差异,并提供了Hive参数优化建议。
最后通过厨房比喻形象解释了Hadoop与Hive的关系,建议初学者先掌握Hive SQL再逐步深入底层原理。
数据倾斜处理方法总结
一、数据倾斜的本质
问题:某个Key的数据量过大(如大客户、空值、热卖商品),导致大部分数据涌入同一个Reducer,任务卡在99%。
为什么数据会倾斜?
假设我们要统计每个客户的交易总金额,SQL如下:
sql
SELECT 客户号, SUM(交易金额) FROM 交易流水表 GROUP BY 客户号;如果有一个"超级大客户"(比如某连锁企业),他一个人的交易流水占了全表的80%。那么Hive在做
GROUP BY时,所有属于这个客户的记录都会被分到同一个reducer处理------其他reducer很快跑完,但这个reducer要处理几亿条数据,导致整个任务卡死。
这就是数据倾斜。
解决思路 :把大Key的数据打散 到多个Reducer,分别处理后再合并结果。
先给
客户号加上一个随机前缀(比如0~N之间的随机数),把一个大客户的数据"人工拆散"到多个reducer里,每个reducer对自己拿到的部分数据先做一次局部聚合 。
把第一阶段生成的临时表作为输入,去掉随机前缀,还原回真正的
客户号,再对同一个客户的所有局部求和结果做一次最终求和。
二、倾斜发生的两种场景
| 场景 | 典型SQL | 解决方法 |
|---|---|---|
| 聚合倾斜 | GROUP BY 热点字段 |
两阶段聚合(加盐) |
| Join倾斜 | 大表 JOIN 大表 |
加盐 + 扩容 / MapJoin |
一个生活化的类比
想象你是一个快递站站长,要把全国所有寄往"北京市朝阳区"的包裹按小区统计数量。
不分阶段:你让一个员工把全部朝阳区的包裹一个个清点、按小区归类。这个员工会被堆积如山的包裹压垮。
两阶段聚合:
第一阶段:你先把包裹随机分成10堆,每堆由不同员工统计自己那堆里每个小区的包裹数量(局部统计)。
第二阶段:你再把10个员工的统计表合并,把同一个小区的数字加起来(全局统计)。
三、聚合倾斜解决方案
方案一:两阶段聚合(加盐)------ 推荐
核心思路:先给Key加随机前缀打散,局部聚合;再去掉前缀,全局聚合。
❌ 错误写法
sql
-- 这个写法不行!RAND()在GROUP BY里每行都重新计算,产生海量分组
SELECT
CONCAT(CAST(RAND()*100 AS INT), '_', 客户号) AS salted_key,
SUM(金额) AS partial_sum
FROM 交易表
GROUP BY CONCAT(CAST(RAND()*100 AS INT), '_', 客户号);
为什么错 :RAND()在GROUP BY中每行独立计算,导致每个随机值都不同,产生几亿个分组,反而更慢。
✅ 正确写法一:子查询固定随机值
sql
-- 先把随机数算好固定下来,再分组
SELECT
CONCAT(CAST(rn AS STRING), '_', 客户号) AS salted_key,
SUM(金额) AS partial_sum
FROM (
SELECT
客户号,
金额,
CAST(RAND() * 100 AS INT) AS rn -- 每行生成一次并固定
FROM 交易表
) t
GROUP BY CONCAT(CAST(rn AS STRING), '_', 客户号);
✅ 正确写法二:DISTRIBUTE BY打散(推荐,性能最好)
sql
-- 先用DISTRIBUTE BY物理打散,再分组聚合
SELECT
SUBSTRING_INDEX(salted_key, '_', 1) AS salt,
客户号,
SUM(金额) AS partial_sum
FROM (
SELECT
CONCAT(CAST(CEIL(RAND()*100) AS STRING), '_', 客户号) AS salted_key,
金额
FROM 交易表
DISTRIBUTE BY CAST(CEIL(RAND()*100) AS INT) -- 关键:随机打散到不同Reducer
) t
GROUP BY SUBSTRING_INDEX(salted_key, '_', 1), 客户号;
核心要点 :DISTRIBUTE BY决定了数据如何分配给Reducer,配合随机函数可以均匀打散。
四、Join倾斜解决方案
方案一:MapJoin(大表Join小表)
sql
-- 如果小表<25MB,强制Map端完成Join,不走Reduce
SELECT /*+ MAPJOIN(small_table) */
big.key, small.value
FROM big_table big
JOIN small_table small ON big.key = small.key;
方案二:大表Join大表且都倾斜(加盐 + 扩容)
sql
-- 对倾斜表加盐,对另一张表扩容
SELECT
/*+ MAPJOIN(expanded) */
a.key, SUM(amount)
FROM (
-- 大表A加盐
SELECT
CONCAT(CAST(CEIL(RAND()*100) AS STRING), '_', key) AS salted_key,
amount
FROM skewed_table
) a
JOIN (
-- 大表B扩容(每条记录复制100份,分别配上0-99的前缀)
SELECT
CONCAT(CAST(bucket AS STRING), '_', key) AS salted_key,
value
FROM normal_table
LATERAL VIEW explode(ARRAY(0,1,2,...,99)) t AS bucket
) b ON a.salted_key = b.salted_key
GROUP BY a.key;
五、Hive参数优化(一键开启)
sql
-- 开启自动倾斜优化(推荐优先尝试)
SET hive.groupby.skewindata = true; -- GROUP BY倾斜自动优化
SET hive.optimize.skewjoin = true; -- JOIN倾斜自动优化
SET hive.skewjoin.key = 1000000; -- 超过100万行视为倾斜Key
SET hive.map.aggr = true; -- 开启Map端预聚合
SET hive.groupby.mapaggr.checkinterval = 100000;
原理:Hive会自动将倾斜Key拆出来单独处理,等价于帮你做"两阶段聚合"。
什么时候不需要两阶段聚合?
如果Hive开启了Map端聚合 (
hive.map.aggr=true),并且数据倾斜不是很严重,可以先用这个参数试试,它会自动在Map端做部分聚合。只有当Map端聚合无法解决(例如热点key实在太大)时,才手动写两阶段聚合。
另外,如果使用Spark SQL 或Tez引擎,它们自带的优化器有时会自动处理这类倾斜,不一定需要手动改写。
但在银行传统的Hive数仓中,两阶段聚合仍然是开发人员必须掌握的技能。
场景:统计每个广告的独立点击用户数(某个广告被超级刷量)
问题SQL(导致数据倾斜)
sqlSELECT 广告位ID, COUNT(DISTINCT 用户ID) AS 独立用户数 FROM 点击日志表 GROUP BY 广告位ID;
问题:假设广告位ID='A001'的点击量占了全表90%。所有处理A001的数据都会涌入同一个Reducer去做COUNT(DISTINCT),导致任务卡死。
正确的写法:用
UNIQUE函数或放弃两阶段聚合
方法一:
COLLECT_SET+SIZE(替代COUNT(DISTINCT))
sqlSELECT SPLIT(salted_id, '_')[1] AS 广告位ID, SIZE(COLLECT_SET(用户ID)) AS 独立用户数 FROM ( SELECT CONCAT(CAST(FLOOR(RAND() * 10) AS STRING), '_', 广告位ID) AS salted_id, 用户ID FROM 点击日志表 ) t GROUP BY SPLIT(salted_id, '_')[1];原理:
先加盐打散
用
COLLECT_SET在每个salted_id分组内收集去重的用户ID(局部去重)用
SIZE统计集合大小
但这个方法在数据量极大时,
COLLECT_SET可能会爆内存,慎用。
方法二:两阶段其实不适用于
COUNT(DISTINCT)对于
COUNT(DISTINCT 用户ID)这种场景,两阶段聚合并不好用。更好的办法是换方案:
用
GROUP BY 用户ID, 广告位ID先做一次去重,然后再统计或者 用 RDBMS(如 Greenplum)的 HyperLogLog 近似去重
或者 放弃 Hive,用 Spark SQL 的
approx_count_distinct(近似去重,速度快)
sql-- 先用 GROUP BY 去重(把用户ID和广告位ID的组合做一次完全打散) SELECT 广告位ID, COUNT(1) AS 独立用户数 FROM ( SELECT DISTINCT 广告位ID, 用户ID FROM 点击日志表 DISTRIBUTE BY 广告位ID, 用户ID -- 打散分布 ) t GROUP BY 广告位ID;
两阶段聚合适合
SUM/COUNT,不适合COUNT(DISTINCT)。后者需要用GROUP BY先打散去重
六、Sqoop导入时的倾斜
问题
按主键切片时,如果主键分布不均匀(如ID集中在1-1000),某个Mapper会处理绝大多数数据。
解决方案
| 方法 | 示例 |
|---|---|
| 更换切片键 | --split-by UPDATE_TIME |
| 用Oracle函数造均匀列 | --split-by "ORA_HASH(CUST_ID, 8)" |
| 手动指定边界 | --boundary-query "SELECT 1, 10000000 FROM DUAL" |
| 关闭并行(小表) | --num-mappers 1 |
七、方法选择速查表
| 倾斜场景 | 推荐方案 | 一句话操作 |
|---|---|---|
| GROUP BY聚合 | 两阶段加盐 | 子查询先算随机数,再加盐分组 |
| GROUP BY聚合 | Hive参数 | SET hive.groupby.skewindata=true; |
| 大表Join小表 | MapJoin | /*+ MAPJOIN(小表) */ |
| 大表Join大表,都倾斜 | 加盐+扩容 | 一表加随机前缀,一表复制N份 |
| Sqoop导入倾斜 | 换切片键 | --split-by ORA_HASH(id, 8) |
八、最推荐的写法模板
sql
-- 通用两阶段聚合模板(处理GROUP BY倾斜)
-- Step 1: 子查询中固定随机盐
WITH salted AS (
SELECT
CONCAT(CAST(FLOOR(RAND() * 100) AS STRING), '_', key) AS salted_key,
value
FROM source_table
)
-- Step 2: 按盐分组做局部聚合
, partial_agg AS (
SELECT
salted_key,
SUM(value) AS partial_sum
FROM salted
GROUP BY salted_key
)
-- Step 3: 去盐做全局聚合
SELECT
SUBSTRING_INDEX(salted_key, '_', -1) AS key,
SUM(partial_sum) AS total_sum
FROM partial_agg
GROUP BY SUBSTRING_INDEX(salted_key, '_', -1);
九、一句话总结
数据倾斜 = 大Key集中 → 解决方法 = 打散(加盐)+ 合并(两阶段)
两个关键点:
-
RAND()不能在GROUP BY里直接用,要先用子查询固定随机值 -
优先试Hive参数
skewindata,不行再手写加盐SQL
Hive 的 COUNT(DISTINCT) 和Oracle的 COUNT(DISTINCT) 不一样吗,区别在哪
核心结论:COUNT(DISTINCT) 的 SQL 写法在两个数据库中是一样的,但它们底层的执行机制完全不同,导致在"大数据量下的性能"和"优化方法"上有天壤之别。
简单来说,Oracle 把它当做一个"精确计数"的指令,而 Hive 把它当做一个"可能引发数据倾斜"的危险信号。
核心区别对比
| 维度 | Oracle (传统数据库) | Hive (大数据数仓) |
|---|---|---|
| 设计定位 | 面向行的实时交易/查询系统(OLTP/OLAP) | 面向列的海量数据批处理分析系统(OLAP) |
| 执行逻辑 | 如果存在索引,Oracle 可以通过索引快速全扫进行优化,能比较好地完成计算 。 | Hive 会将这个操作翻译成一个 MapReduce 任务 ,所有数据必须经过唯一的 Reduce 阶段进行去重,极易产生单点瓶颈 。 |
| 数据倾斜风险 | 低。数据量相对小,引擎优化成熟。 | 极高。处理海量数据时,唯一的 Reducer 会成为性能瓶颈,任务极易超时或失败 。 |
| 性能表现 | 在千万、亿级数据量下,性能依然可接受。 | 在千万、亿级数据量下,性能极差,需要业务层进行优化规避 。 |
为什么会有这些区别?
1. 底层架构:冰箱 vs 管道
-
Oracle 像一台功能强大的智能冰箱 :它是整体设计的,内部有各种高效的算法和索引(比如 B-Tree 索引),找到不同的东西很快。在执行
COUNT(DISTINCT)时,如果能利用索引,计算效率会很高 。但如果数据量突破某个阈值,它也会"吃力"。 -
Hive 像一个分工明确的巨型食品加工厂 :它依赖 MapReduce 或 Tez 等分布式计算框架,擅长把任务拆成小份并行处理。然而,
COUNT(DISTINCT)这个指令天然要求"去重",这意味着必须在流水线的最后一步 ,把所有的原材料(数据)集中到一个工位上进行挑选和计数。在 Hive 里,这个"工位"就是一个 Reducer,无论你多大规模的数据,到最后都会涌进同一个 Reducer,这就是数据倾斜的根源 。
2. 优化思路:正面硬刚 vs 曲线救国
-
Oracle 的优化:靠的是"内力",比如创建更合适的索引、更新统计信息,让优化器自己选择最优路径 。
-
Hive 的"优化" :本质上是"想办法不去用
COUNT(DISTINCT)"。正如你之前学到的"两阶段聚合",就是通过GROUP BY将数据先打散、局部去重,再全局汇总,把一个 Reducer 的压力分摊到多个 Reducer 上去完成 。
总结
简单来说,在Oracle里,COUNT(DISTINCT) 是一个直接、高效的函数;在Hive里,它更像是一个需要你小心处理的"性能陷阱"。
明白了这个区别,你就能理解为什么在 Hive 大数据开发中,大家总是"谈 COUNT(DISTINCT) 色变",并且会花很多心思去用 GROUP BY 来改写了。
我想要的是你讲的银行的一天故事里"小钱修改后: sql -- 加盐打散 SELECT 客户号, SUM(交易金额) FROM ( SELECT IF(客户号 = 'HOT_CUSTOMER_001', CONCAT(客户号, '_', CAST(RAND()*100 AS INT)), 客户号) as salted_key, 交易金额 FROM dwd_transaction_di ) t GROUP BY salted_key; 老赵说:"思路对,但是HOT_CUSTOMER_001是写死的,不通用。应该先统计每个客户交易量,动态识别热点客户,再加盐。或者更简单------用distribute by + sort by配合bucket。我给你一个模板,你明天改一版。"
这个示例的SQL,优化后是怎么写的
老赵说的很对,你的写法有2个明显问题:
-
热点客户写死了 :只能处理
HOT_CUSTOMER_001这一个客户,其他大客户照样倾斜 -
加盐后group by只能拿到打散后的临时key,最后还要再聚合一次才能还原成真正的客户号
下面给你两种优化方案,一种是动态识别热点客户的通用写法,另一种是老赵提到的**DISTRIBUTE BY + SORT BY配合分桶**的写法(更适合大表关联场景)。
方案一:动态识别热点客户 + 两阶段聚合
核心思路:
-
先统计每个客户的交易量,标记出热点客户(比如交易量排名前10%或交易额超过阈值)
-
对热点客户加盐打散,对普通客户保持原样
-
先做局部聚合(带盐的key),再去盐做全局聚合
sql
-- ============================================================
-- 第一阶段:识别热点客户(交易额超过1亿的客户)
-- ===================================
WITH hot_customers AS (
SELECT 客户号
FROM dwd_transaction_di
GROUP BY 客户号
HAVING SUM(交易金额) > 100000000 -- 动态阈值,可根据业务调整
),
-- ============================================================
-- 第二阶段:加盐打散 + 局部聚合
-- ===================================
salted_agg AS (
SELECT
CASE
WHEN h.客户号 IS NOT NULL
THEN CONCAT(CAST(FLOOR(RAND() * 100) AS STRING), '_', t.客户号) -- 热点客户加盐
ELSE t.客户号 -- 普通客户不加盐
END AS salted_key,
SUM(t.交易金额) AS partial_sum
FROM dwd_transaction_di t
LEFT JOIN hot_customers h ON t.客户号 = h.客户号
GROUP BY
CASE
WHEN h.客户号 IS NOT NULL
THEN CONCAT(CAST(FLOOR(RAND() * 100) AS STRING), '_', t.客户号)
ELSE t.客户号
END
)
-- ============================================================
-- 第三阶段:去盐 + 全局聚合
-- ===================================
SELECT
SPLIT(salted_key, '_')[1] AS 客户号,
SUM(partial_sum) AS 交易总额
FROM salted_agg
GROUP BY SPLIT(salted_key, '_')[1];
优点 :通用,能自动识别所有大客户
缺点 :SQL比较长,需要扫描两次dwd_transaction_di(一次统计热点,一次做聚合)
方案二:DISTRIBUTE BY + SORT BY 配合分桶(老赵说的更简单的方法)
这个方法不需要加盐 ,核心思路是:用DISTRIBUTE BY强制把同一个客户的数据分到同一个reducer,但用SORT BY让每个reducer内部排序后分组聚合,避免单个reducer内存爆掉。配合分桶表效果更佳。
sql
-- ============================================================
-- 前提:dwd_transaction_di 已经是按客户号分桶的表(CLUSTERED BY 客户号 INTO 100 BUCKETS)
-- ===================================
SET hive.groupby.skewindata = true; -- 开启Hive自带的数据倾斜优化
SELECT 客户号, SUM(交易金额) AS 交易总额
FROM dwd_transaction_di
DISTRIBUTE BY 客户号 -- 按客户号分布数据
SORT BY 客户号 -- 每个reducer内按客户号排序
GROUP BY 客户号;
为什么这样能缓解倾斜?
普通 GROUP BY |
DISTRIBUTE BY + SORT BY |
|---|---|
| 所有数据自动按key hash到reducer | 人为控制分布逻辑,可以配合分桶 |
| 大客户数据全部进同一个reducer | 如果表已经是分桶表,数据本身已经打散存储,查询时每个桶独立聚合 |
| 只能等reducer慢慢跑 | 可以结合hive.groupby.skewindata=true让Hive自动拆分成两个MR任务(其实就是自动帮你做两阶段聚合) |
老赵会给你发的"模板"
老赵说的"给你一个模板",大概率是这样的:
sql
-- ============================================================
-- 数据倾斜通用处理模板
-- 适用场景:GROUP BY 聚合时某个key数据量过大
-- ===================================
SET hive.groupby.skewindata = true; -- 开启倾斜优化(Hive自动拆成两个MR)
SET hive.map.aggr = true; -- 开启Map端聚合
SET hive.groupby.mapaggr.checkinterval = 100000; -- Map端聚合的条数阈值
SELECT 客户号, SUM(交易金额) AS 交易总额
FROM dwd_transaction_di
GROUP BY 客户号;
hive.groupby.skewindata=true 的原理:
-
第一个MR:随机打散数据,做局部聚合(相当于你写的加盐阶段)
-
第二个MR:对局部聚合结果做全局聚合(相当于你去盐再聚合的阶段)
这就是Hive自动帮你做两阶段聚合。
大多数情况下,开启这个参数就够了,不需要手动写加盐SQL。
什么时候需要手动加盐?
| 场景 | 是否需要用 |
|---|---|
GROUP BY 聚合 + 开启 hive.groupby.skewindata=true |
❌ 不需要,Hive自动做 |
GROUP BY 聚合 + 参数不生效(某些Hive版本bug) |
✅ 需要 |
JOIN 时大表和大表关联,其中一个大表有热点key |
✅ 需要(加盐 + 扩容) |
COUNT(DISTINCT) |
✅ 需要(用GROUP BY替代或加盐) |
一句话总结
-
简单场景 :开
hive.groupby.skewindata=true,让Hive自动优化 -
复杂场景(JOIN倾斜、COUNT DISTINCT):手动加盐,分两阶段聚合
-
老赵推荐的方法 :
DISTRIBUTE BY + SORT BY配合分桶表,再加参数开关
Oracle 数据库加随机盐的示例
Oracle里"加随机盐"和Hive的思路一样,但写法完全不同------因为Oracle没有Hive那种内置的随机函数和字符串拼接的灵活语法,而且Oracle处理大数据倾斜的方式也不一样。
我先给你Oracle的写法,再解释为什么Oracle里很少这样写。
Oracle加随机盐的写法
场景:统计每个客户的交易总额,但有一个"超级大客户"数据量巨大,导致按客户号Group By时单个CPU核心跑死。
sql
-- ============================================================
-- Oracle 加盐写法(模拟Hive的两阶段聚合)
-- ===================================
-- 第一步:加盐打散 + 局部聚合
-- ===================================
WITH salted_agg AS (
SELECT
CASE
WHEN 客户号 = 'HOT_CUSTOMER_001'
THEN 客户号 || '_' || MOD(DBMS_RANDOM.VALUE(1, 100), 100) -- 热点客户加盐
ELSE 客户号
END AS salted_key,
SUM(交易金额) AS partial_sum
FROM 交易流水表
GROUP BY
CASE
WHEN 客户号 = 'HOT_CUSTOMER_001'
THEN 客户号 || '_' || MOD(DBMS_RANDOM.VALUE(1, 100), 100)
ELSE 客户号
END
)
-- ============================================================
-- 第二步:去盐 + 全局聚合
-- ===================================
SELECT
REGEXP_SUBSTR(salted_key, '^[^_]+') AS 客户号, -- 去掉盐后缀
SUM(partial_sum) AS 交易总额
FROM salted_agg
GROUP BY REGEXP_SUBSTR(salted_key, '^[^_]+');
Oracle的关键语法解释
| Hive写法 | Oracle写法 | 说明 |
|---|---|---|
CAST(RAND()*100 AS INT) |
MOD(DBMS_RANDOM.VALUE(1, 100), 100) |
Oracle生成0~99随机整数 |
CONCAT(客户号, '_', 随机数) |
`客户号 | |
IF(条件, 值1, 值2) |
CASE WHEN 条件 THEN 值1 ELSE 值2 END |
Oracle没有IF函数,用CASE |
SPLIT(salted_key, '_')[1] |
REGEXP_SUBSTR(salted_key, '^[^_]+') |
Oracle提取下划线前的部分 |
为什么Oracle里很少这样写?
核心原因:Oracle处理数据倾斜的能力比Hive强得多。
| 维度 | Hive | Oracle |
|---|---|---|
| 并行度控制 | 靠Reducer数量,写死了就改不了 | PARALLEL hint可以动态调整并行度 |
| 内存管理 | OOM就挂了 | 有PGA_AGGREGATE_TARGET自动管理 |
| 倾斜处理 | 手动加盐 | /*+ PARALLEL */ + 分区表 + 索引就能跑 |
| 数据量级 | TB/PB级,不加盐会死 | GB/TB级,加盐不是必须 |
Oracle推荐的做法是:
sql
-- 直接用并行查询,不用加盐
SELECT /*+ PARALLEL(8) */
客户号,
SUM(交易金额) AS 交易总额
FROM 交易流水表
GROUP BY 客户号;
给8个CPU并行跑,Oracle会自动把数据拆成8份,每个CPU处理一部分。
即使有热点客户,也只是这个CPU慢一点,不会像Hive那样整个任务卡死。
只有在数据量特别大、热点特别集中的极端情况下,Oracle才需要手动加盐。
而这种场景在Oracle的生产环境里很少见------因为如果数据真的大到这种程度,早就该迁移到Hadoop/Spark了。
总结
-
Oracle加盐语法 :用
CASE WHEN + DBMS_RANDOM + 字符串拼接实现 -
但Oracle里很少用加盐 :开
PARALLEL并行查询就够了 -
Hive里加盐是家常便饭:因为Hive的并行能力弱,不加盐热点key会炸
Hadoop里怎么处理数据倾斜
在Hadoop生态里处理数据倾斜,核心思路只有一个:把那个"撑死"的节点上的活儿,想办法匀给其他"饿着"的节点。
我们之前聊过的 "加盐" 就是最核心的手段,但Hadoop生态里不止这一招。我把这些方法分成了"必杀技"和"辅助技",你可以按顺序来排查。
🎯 核心三板斧:正面解决倾斜
这几招是专门用来"治本"的,直接对倾斜的数据下手。
1. 两阶段聚合(加盐法)
这是我们之前重点讨论过的。它的核心是把一个大的聚合任务,拆成"局部预聚合"和"全局最终聚合"两步。
-
适用场景 :
GROUP BY聚合操作,例如SELECT key, COUNT(*) FROM table GROUP BY key。 -
原理 :第一阶段给Key(如
hot_key)加上随机前缀(如0_hot_key),让它能分散到多个Reducer做预聚合;第二阶段去掉前缀,对预聚合的结果做最终汇总。 -
效果:能将一个大Reducer的压力,平摊到多个Reducer上。
2. 大表Join小表(MapJoin)
当一个大表和一个相对较小的表进行 JOIN 操作时,让Hadoop把小表直接"广播"到每个Map任务的内存里,在Map端就完成关联,完全跳过Shuffle和Reduce阶段。
-
适用场景 :小表 < 25MB 或 小表 < 1GB(取决于集群配置)。
-
Hive设置:
sqlSET hive.auto.convert.join = true; SET hive.mapjoin.smalltable.filesize = 25000000; -- 25MB -
效果 :彻底规避了由
JOIN引发的Shuffle倾斜,性能提升可达3-8倍。
3. 大表Join大表且都倾斜(加盐 + 扩容)
当两张表都很大且都存在热点Key时,单独对一个表加盐已经不够用了。需要采用一种巧妙的"加盐 + 扩容"组合技。
-
原理:
-
对倾斜表A加盐 :将热点Key(如
hot_key)加上0~N的随机前缀(如0_hot_key)。 -
对另一张表B扩容 :将另一张表对应的记录(
hot_key),通过LATERAL VIEW explode等方式,膨胀成N份(每份分别配上0~N的前缀,如0_hot_key)。 -
两表Join:此时,原本会集中在一个Key上的压力,因为加盐和扩容,会被打散成N份,由N个任务并行处理。
-
-
效果:两阶段Join,是解决双大表倾斜的经典方案。
🔧 辅助技:查漏补缺与性能调优
这些方法有时候不一定能根除倾斜,但能减轻症状,或者配合上面的方法使用。
| 方法 | 原理 | 使用建议 |
|---|---|---|
| 过滤脏数据 | 空值、无意义字符串(如 unknown)极容易造成倾斜,提前过滤掉或转为随机值。 |
养成习惯,在Join或Group前对Key进行清洗。 |
| 增加Reducer数量 | 通过 mapreduce.job.reduces 增加并行度,把大任务切成更小的块。 |
治标不治本,极端倾斜时效果有限。 |
| 开启Combiner | 在Map端执行一次预聚合,大大减少传到Reducer的数据量,尤其适合求和、计数操作。 | 强烈推荐,对减少网络IO和初步缓解压力非常有效。 |
| 自定义分区器 | 打破默认的哈希分区,比如根据业务逻辑把多个"重Key"分到不同的Reducer。 | 适合业务场景明确、热点Key较少的场景,实现灵活。 |
| 开启Skew Join | Hive的"傻瓜式"倾斜优化,会自动检测倾斜的Key并拆开处理,底层逻辑类似于"加盐"。 | 懒人首选,可以无脑开启试试看效果。 |
| 采样倾斜Key单独处理 | 用 GROUP BY key ORDER BY cnt 找出热点,然后对热点和非热点数据分别处理,最后 UNION ALL 合并。 |
适合业务场景明确、热点Key较少的场景。 |
💎 总结:处理流程
在工作中,你可以遵循下面的顺序来排查问题:
-
定位问题:通过YARN日志或SQL排查,找到是哪个Stage卡住了。
-
对症下药:
-
GROUP BY聚合 → 优先尝试 两阶段聚合(加盐)。 -
JOIN操作 → 先确认大小表,能用 MapJoin 就用 MapJoin;大表对大表则考虑 加盐+扩容。
-
-
参数兜底 :在代码里加上
SET hive.optimize.skewjoin=true;和SET hive.auto.convert.join=true;,让Hive自身也做一些优化尝试。 -
日常预防:养成用Combiner、过滤空值的写码习惯。
这些方法在Hive、Spark、MapReduce中原理相通。
Hadoop 和 Hive 的关系
Hadoop是厨房,Hive是点菜机。
一、它们到底是什么?
| 概念 | 一句话解释 | 生活化类比 |
|---|---|---|
| Hadoop | 一套存储和计算 海量数据的基础平台(生态圈) | 一个大型中央厨房。里面有冰箱(HDFS存数据)、灶台(YARN分配资源)、厨师(MapReduce/Spark炒菜)。 |
| Hive | 建立在Hadoop之上的数据仓库工具 ,让你能用 SQL 查询Hadoop里的数据 | 点菜机(或者点菜平板)。你不用去厨房亲自颠勺(写复杂的Java程序),只需要点击菜单(写SQL),它就会把需求翻译成任务,交给厨房去执行。 |
核心区别:
-
Hadoop 是一个完整的生态系统(有很多组件)。
-
Hive 只是这个系统里的一个工具 ,它的作用是让你用熟悉的 SQL 去操作Hadoop。
二、用一个完整故事讲清楚
场景:你要统计杭州所有超市的苹果销售总量。
1. 只有Hadoop(没有Hive)
你只能自己写Java程序:
java
// 伪代码示意
while (有下一行数据) {
if (行里有"苹果") {
map(超市名, 1); // 记录一次
}
}
// 然后再写Reduce代码去累加...
你需要自己处理:怎么拆分任务、怎么分配、某个机器挂了怎么办、最后怎么汇总......非常复杂。
2. 有了Hive(站在Hadoop之上)
你只需要写一行SQL:
sql
SELECT 超市名, COUNT(*) FROM 销售表 WHERE 商品='苹果' GROUP BY 超市名;
-
你:点菜机前下单的人,轻松写SQL。
-
Hive(点菜机):接收你的SQL,把它自动翻译成上面那段复杂的Java代码(MapReduce任务),然后交给厨房。
-
Hadoop(厨房):真正干活的地方。HDFS负责存储你那几TB的销售数据,YARN负责协调几百台机器同时计算,MapReduce负责实际执行"分组、计数"的动作。
三、你还可能听到的其他"Hive"概念
| 概念 | 关系 | 比喻(沿用厨房) |
|---|---|---|
| MapReduce | Hadoop的一种计算引擎(比较慢,像大锅炖菜)。 | 厨房里的一位老厨师(会做,但慢)。 |
| Spark | 另一种更快的计算引擎,可以替代MapReduce。 | 厨房里新请的年轻厨师(效率更高)。 |
| Tez | 另一种计算引擎,比MapReduce快,但没有Spark普及。 | 另一位专做流水席的厨师。 |
关键点:Hive可以配"后台厨师",默认用MapReduce,也可以换成Spark或Tez来做计算引擎,让SQL跑得更快。
无论用哪个厨师,你写SQL的方式基本不变。
四、一张终极关系图
sql
┌─────────────────────────────────────┐
│ 你的 SQL 语句 │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Hive (点菜机) │
│ 把你的SQL翻译成MapReduce任务 │
└─────────────────┬───────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Hadoop (中央厨房) │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ HDFS (冰箱) │ │ YARN (调度员) │ │ MapReduce(厨师)│ │
│ │ 存海量数据 │ │ 分配机器资源 │ │ 真正干活计算 │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────┘
五、你现在该怎么学?
| 阶段 | 学习重点 | 具体动作 |
|---|---|---|
| 入门 | Hive SQL(怎么用点菜机) | 学习建表、分区、函数、优化。不用担心Hadoop怎么实现的。 |
| 进阶 | Hive原理(点菜机怎么工作的) | 学习Hive如何把SQL转成MapReduce、数据倾斜怎么处理。 |
| 高阶 | Hadoop基础(了解厨房构造) | 学习HDFS的存储原理、YARN的资源调度。这会让你更懂如何优化Hive。 |
刚开始就把Hive当能处理超大数据量的MySQL来学,完全没问题。
学到后来,你可能会想试试不用Hive,直接在Hadoop上用Python写MapReduce。