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


但在银行传统的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。

相关推荐
Gl�ria1 个月前
Hadoop MapReduce / Hive 场景:ReduceTask 热点 Key 排查
运维·hadoop·数据倾斜
智码看视界2 个月前
Spark 3.5 AQE 调优:10 个生产环境案例让作业提速 3-10 倍
大数据·spark·性能调优·aqe·sparksql·数据倾斜·etl优化
Thanks_ks3 个月前
深入理解一致性 Hash 算法:从理论到虚拟节点的工程实践
负载均衡·缓存雪崩·go语言·一致性hash·分布式系统·数据倾斜·虚拟节点
南修子7 个月前
【Flink 30天】Day22-23 FlinkSQL 性能优化:Mini-Batch + 两阶段聚合 + TOP-N + 完整配置
性能优化·flinksql·数据倾斜·mini-batch·两阶段聚合
佐伊238 个月前
SQL优化剧场:当Hive/MaxCompute遇上数据倾斜的十二种剧情
hive·sql优化·maxcompute·数据倾斜
励志成为糕手1 年前
Spark Shuffle:分布式计算的数据重分布艺术
大数据·分布式·spark·性能调优·数据倾斜
涤生大数据1 年前
从MR迁移到Spark3:数据倾斜与膨胀问题的实战优化
数据库·数据仓库·spark·mapreduce·大数据开发·数据倾斜·spark3
linweidong1 年前
深入剖析 Spark Shuffle 机制:从原理到实战优化
大数据·分布式·spark·spark sql·数据开发·shuffle·数据倾斜
济南java开发,求内推1 年前
关于hbaseRegion和hbaseRowKey的一些处理
负载均衡·hbase·数据倾斜·rowkey·热点数据