数据倾斜(Hadoop、Hive、Oracle、Sqoop)处理方法总结

本文总结了大数据处理中数据倾斜问题的本质与解决方案。


数据倾斜指某个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 SQLTez引擎,它们自带的优化器有时会自动处理这类倾斜,不一定需要手动改写。


但在银行传统的Hive数仓中,两阶段聚合仍然是开发人员必须掌握的技能。

场景:统计每个广告的独立点击用户数(某个广告被超级刷量)

问题SQL(导致数据倾斜)

sql 复制代码
SELECT 
    广告位ID,
    COUNT(DISTINCT 用户ID) AS 独立用户数
FROM 点击日志表
GROUP BY 广告位ID;
复制代码
问题:假设广告位ID='A001'的点击量占了全表90%。所有处理A001的数据都会涌入同一个Reducer去做COUNT(DISTINCT),导致任务卡死。

正确的写法:用 UNIQUE 函数或放弃两阶段聚合


方法一:COLLECT_SET + SIZE(替代 COUNT(DISTINCT)

sql 复制代码
SELECT 
    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) 这种场景,两阶段聚合并不好用

更好的办法是换方案:

  1. GROUP BY 用户ID, 广告位ID 先做一次去重,然后再统计

  2. 或者 用 RDBMS(如 Greenplum)的 HyperLogLog 近似去重

  3. 或者 放弃 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集中 → 解决方法 = 打散(加盐)+ 合并(两阶段)

两个关键点

  1. RAND()不能在GROUP BY里直接用,要先用子查询固定随机值

  2. 优先试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个明显问题:

  1. 热点客户写死了 :只能处理HOT_CUSTOMER_001这一个客户,其他大客户照样倾斜

  2. 加盐后group by只能拿到打散后的临时key,最后还要再聚合一次才能还原成真正的客户号

下面给你两种优化方案,一种是动态识别热点客户的通用写法,另一种是老赵提到的**DISTRIBUTE BY + SORT BY配合分桶**的写法(更适合大表关联场景)。


方案一:动态识别热点客户 + 两阶段聚合

核心思路:

  1. 先统计每个客户的交易量,标记出热点客户(比如交易量排名前10%或交易额超过阈值)

  2. 对热点客户加盐打散,对普通客户保持原样

  3. 先做局部聚合(带盐的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设置

    sql 复制代码
    SET hive.auto.convert.join = true;
    SET hive.mapjoin.smalltable.filesize = 25000000; -- 25MB
  • 效果 :彻底规避了由 JOIN 引发的Shuffle倾斜,性能提升可达3-8倍。

3. 大表Join大表且都倾斜(加盐 + 扩容)

当两张表都很大且都存在热点Key时,单独对一个表加盐已经不够用了。需要采用一种巧妙的"加盐 + 扩容"组合技。

  • 原理

    1. 对倾斜表A加盐 :将热点Key(如 hot_key)加上0~N的随机前缀(如 0_hot_key)。

    2. 对另一张表B扩容 :将另一张表对应的记录(hot_key),通过 LATERAL VIEW explode 等方式,膨胀成N份(每份分别配上0~N的前缀,如 0_hot_key)。

    3. 两表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较少的场景。

💎 总结:处理流程

在工作中,你可以遵循下面的顺序来排查问题:

  1. 定位问题:通过YARN日志或SQL排查,找到是哪个Stage卡住了。

  2. 对症下药

    • GROUP BY 聚合 → 优先尝试 两阶段聚合(加盐)

    • JOIN 操作 → 先确认大小表,能用 MapJoin 就用 MapJoin;大表对大表则考虑 加盐+扩容

  3. 参数兜底 :在代码里加上 SET hive.optimize.skewjoin=true;SET hive.auto.convert.join=true;,让Hive自身也做一些优化尝试。

  4. 日常预防:养成用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。

相关推荐
南修子3 个月前
【Flink 30天】Day22-23 FlinkSQL 性能优化:Mini-Batch + 两阶段聚合 + TOP-N + 完整配置
性能优化·flinksql·数据倾斜·mini-batch·两阶段聚合
佐伊234 个月前
SQL优化剧场:当Hive/MaxCompute遇上数据倾斜的十二种剧情
hive·sql优化·maxcompute·数据倾斜
励志成为糕手8 个月前
Spark Shuffle:分布式计算的数据重分布艺术
大数据·分布式·spark·性能调优·数据倾斜
涤生大数据8 个月前
从MR迁移到Spark3:数据倾斜与膨胀问题的实战优化
数据库·数据仓库·spark·mapreduce·大数据开发·数据倾斜·spark3
linweidong10 个月前
深入剖析 Spark Shuffle 机制:从原理到实战优化
大数据·分布式·spark·spark sql·数据开发·shuffle·数据倾斜
济南java开发,求内推1 年前
关于hbaseRegion和hbaseRowKey的一些处理
负载均衡·hbase·数据倾斜·rowkey·热点数据
gegeyanxin1 年前
Flink checkpoint问题排查指南
flink·数据倾斜·checkpoint失败
青云交1 年前
大数据新视界 -- Hive 数据倾斜实战案例分析(2 - 16 - 6)
大数据·hive·数据分析·解决方案·数据倾斜·电商平台·实战案例·sql 优化
青云交2 年前
大数据新视界 --大数据大厂之基于 MapReduce 的大数据并行计算实践
大数据·搜索引擎·性能优化·mapreduce·并行计算·数据倾斜·电商数据