Hive:让 SQL 党也能玩转大数据
大数据系列第 7 篇:不会写 Java/Scala?没关系,用 SQL 也能处理海量数据。来看看 Hive 是怎么做到的。
一个真实的需求场景
假设你是公司的数据分析师,老板让你统计一下:过去一个月,每个省份的用户平均消费金额是多少?
如果你的数据存在 MySQL 里,SQL 大概长这样:
SELECTprovince,AVG(amount)asavg_amountFROMordersWHEREcreated_at>='2024-01-01'GROUPBYprovince;简单吧?几行 SQL 搞定。
但问题是——你们的订单数据有 100 亿条,存在 HDFS 上。
这时候你懵了:HDFS 上的数据怎么查?写 MapReduce?那得写几十行 Java 代码,还得理解 InputFormat、OutputFormat、Mapper、Reducer……
数据分析师表示:我只是想写个 SQL 啊!
这就是 Hive 诞生的原因。
Hive 是什么?
Hive 是 Facebook 开源的一个数据仓库工具,它的口号是:“给 Hadoop 提供类 SQL 的查询能力”。
简单说,Hive 让你可以用类似 SQL 的语法(叫 HiveQL)来操作 HDFS 上的数据,底层自动帮你转成 MapReduce/Spark/Tez 作业来执行。
┌─────────────────────────────────────────────────────────────────┐ │ Hive 的定位 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 你写的: │ │ SELECT province, COUNT(*) FROM users GROUP BY province; │ │ │ │ ↓ Hive 自动转换 │ │ │ │ 底层执行: │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ MapReduce / Spark / Tez │ │ │ │ 读取 HDFS 数据 → 执行计算 → 输出结果 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ 你不需要关心: │ │ • 数据怎么从 HDFS 读出来 │ │ • 怎么写 Mapper 和 Reducer │ │ • 任务怎么调度到集群上 │ │ │ │ 你只需要关心: │ │ • 表结构是什么 │ │ • SQL 怎么写 │ │ │ └─────────────────────────────────────────────────────────────────┘注意:Hive 不是数据库,它不做存储,也不做实时查询。它只是一个"翻译器",把你的 SQL 翻译成分布式计算作业,数据还是存在 HDFS 上,计算还是靠 MapReduce/Spark。
Hive 的架构:怎么把 SQL 变成分布式作业?
┌─────────────────────────────────────────────────────────────────┐ │ Hive 架构(简化版) │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 你 │ │ │ 写 HiveQL │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Hive 驱动(Driver) │ │ │ │ │ │ │ │ 1. 解析器(Parser):把 SQL 解析成抽象语法树(AST) │ │ │ │ ↓ │ │ │ │ 2. 编译器(Compiler):把 AST 转成逻辑执行计划 │ │ │ │ ↓ │ │ │ │ 3. 优化器(Optimizer):优化执行计划(谓词下推等) │ │ │ │ ↓ │ │ │ │ 4. 执行器(Executor):生成物理执行计划 │ │ │ │ ↓ │ │ │ │ 5. 提交到计算引擎(MapReduce/Spark/Tez) │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ │ 查询元数据 │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Metastore(元数据存储) │ │ │ │ │ │ │ │ • 表名、列名、列类型 │ │ │ │ • 表存在 HDFS 的哪个路径 │ │ │ │ • 数据格式(Text、ORC、Parquet) │ │ │ │ • 分区信息 │ │ │ │ │ │ │ │ 通常用 MySQL/PostgreSQL 存储 │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘Metastore:Hive 的"字典"
Metastore 是 Hive 的元数据管理中心,存储了:
- 有哪些数据库、哪些表
- 每个表有哪些列,什么类型
- 表的数据存在 HDFS 的哪个目录
- 表用了什么文件格式、什么分隔符
- 表有没有分区,分区字段是什么
Metastore 通常用 MySQL 或 PostgreSQL 存储。为什么不用 HDFS?因为元数据需要频繁读写、支持事务,关系型数据库更适合。
执行引擎的演进
Hive 底层支持多种执行引擎:
| 引擎 | 特点 | 现状 |
|---|---|---|
| MapReduce | Hive 最早的执行引擎,稳定但慢 | 已基本淘汰 |
| Tez | Hortonworks 推出的 DAG 执行引擎,比 MR 快 | 少数场景在用 |
| Spark | 目前主流,内存计算,性能最好 | 推荐使用 |
-- 设置执行引擎为 SparkSEThive.execution.engine=spark;-- 或者 TezSEThive.execution.engine=tez;HiveQL 与标准 SQL 的差异
HiveQL 大部分语法和 MySQL 类似,但有一些差异和扩展:
1. 创建表:要指定存储格式和分隔符
-- 创建内部表(数据由 Hive 管理,删除表时数据也删除)CREATETABLEusers(idINT,name STRING,ageINT,province STRING)ROWFORMAT DELIMITEDFIELDSTERMINATEDBY'\t'-- 字段用制表符分隔STOREDASTEXTFILE;-- 存储格式为文本文件-- 创建外部表(数据不由 Hive 管理,删除表只删元数据)CREATEEXTERNALTABLEorders(order_id STRING,user_idINT,amountDECIMAL(10,2),created_atTIMESTAMP)ROWFORMAT DELIMITEDFIELDSTERMINATEDBY','STOREDASTEXTFILE LOCATION'/data/orders';-- 数据已经存在这个 HDFS 路径内部表 vs 外部表:
- 内部表:Hive 全权管理,删表时数据和元数据一起删
- 外部表:Hive 只管理元数据,数据由外部程序生成,删表不影响数据
生产环境建议用外部表,避免误删数据。
2. 分区:避免全表扫描的神器
如果你的表有几十亿条数据,每次查询都扫描全表,那得等到天荒地老。
Hive 的解决方案是分区(Partition):
-- 按日期分区CREATETABLElogs(user_id STRING,actionSTRING,url STRING)PARTITIONEDBY(dt STRING)-- 分区字段:日期STOREDASTEXTFILE;-- 插入数据时指定分区INSERTINTOlogsPARTITION(dt='2024-01-01')VALUES('u001','click','/home');-- 查询时指定分区,只扫描对应目录SELECT*FROMlogsWHEREdt='2024-01-01';-- Hive 只读 /data/logs/dt=2024-01-01/ 这个目录,其他目录不碰HDFS 上的目录结构: /data/logs/ ├── dt=2024-01-01/ │ └── part-00001.txt ├── dt=2024-01-02/ │ └── part-00001.txt ├── dt=2024-01-03/ │ └── part-00001.txt └── ... 按 dt 分区后,查询 WHERE dt='2024-01-01' 只读一个目录 而不是扫描所有数据!分区是 Hive 性能优化的第一要义。常见的分区字段:日期、省份、业务线。
3. 分桶:进一步优化 Join 性能
如果两个表经常按某个字段 Join,可以把它们按同一个字段分桶(Bucket):
-- 按 user_id 分桶,分 16 个桶CREATETABLEuser_orders(order_id STRING,user_id STRING,amountDECIMAL(10,2))CLUSTEREDBY(user_id)INTO16BUCKETS STOREDASORC;分桶后,相同user_id的数据一定在同一个桶里。两个分桶表 Join 时,可以直接按桶配对,避免全局 Shuffle,大幅提升性能。
4. 存储格式:选对格式,性能翻倍
Hive 支持多种存储格式,选对格式很重要:
| 格式 | 特点 | 适用场景 |
|---|---|---|
| TEXTFILE | 纯文本,人类可读,但无压缩 | 测试、调试 |
| SEQUENCEFILE | 二进制键值对,可压缩 | 已较少使用 |
| ORC | 列式存储,高压缩,支持谓词下推 | 推荐 |
| Parquet | 列式存储,跨语言兼容性好 | 推荐 |
-- 创建 ORC 格式的表(推荐)CREATETABLEusers_orc(idINT,name STRING,ageINT)STOREDASORC TBLPROPERTIES('orc.compress'='ZLIB');-- 使用 ZLIB 压缩ORC 和 Parquet 是列式存储格式,相比行式存储(TEXTFILE)有巨大优势:
- 查询时只读需要的列,不读整行
- 压缩率高,节省存储空间
- 支持谓词下推,跳过不满足条件的数据块
Hive 的执行流程:你的 SQL 到底经历了什么?
咱们以一个简单的查询为例,看看 Hive 内部是怎么执行的:
SELECTprovince,COUNT(*)ascntFROMusersWHEREage>18GROUPBYprovinceORDERBYcntDESCLIMIT10;┌─────────────────────────────────────────────────────────────────┐ │ Hive SQL 执行流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ Step 1: 解析(Parser) │ │ SQL → 抽象语法树(AST) │ │ │ │ Step 2: 语义分析(Semantic Analysis) │ │ • 从 Metastore 查询表结构 │ │ • 检查列名、类型是否正确 │ │ • 检查表是否存在、是否有权限 │ │ │ │ Step 3: 逻辑计划生成(Logical Plan) │ │ • 生成逻辑操作树: │ │ Scan(users) → Filter(age > 18) → GroupBy(province) │ │ → Aggregate(COUNT) → Sort(cnt DESC) → Limit(10) │ │ │ │ Step 4: 优化(Optimization) │ │ • 谓词下推:把 Filter 尽可能下推到数据源层 │ │ • 列裁剪:只读取需要的列(province, age) │ │ • 分区裁剪:如果 users 按 province 分区,只读需要的分区 │ │ │ │ Step 5: 物理计划生成(Physical Plan) │ │ • 根据执行引擎(MapReduce/Spark/Tez)生成物理操作 │ │ │ │ Step 6: 执行(Execution) │ │ • 提交作业到集群 │ │ • 监控执行进度 │ │ • 返回结果 │ │ │ └─────────────────────────────────────────────────────────────────┘Hive 的坑:这些坑我替你踩过了
坑 1:小文件问题
Hive 表如果产生大量小文件(比如每个文件几 MB),会导致:
- MapReduce/Spark 启动大量 Task,Task 启动开销比处理数据还大
- NameNode 内存压力(HDFS 小文件问题)
解决方案:
-- 插入数据时合并小文件INSERTOVERWRITETABLEtarget_tableSELECT*FROMsource_table;-- 或者设置合并参数SEThive.merge.mapfiles=true;SEThive.merge.mapredfiles=true;SEThive.merge.size.per.task=256000000;-- 合并后每个文件 256MB坑 2:数据倾斜
某个 Key 的数据特别多(比如province='广东'占了 80% 的数据),导致某个 Reduce Task 特别慢,拖垮整个作业。
正常情况: Reduce 1: 100万条 ┃ Reduce 2: 100万条 ┃ Reduce 3: 100万条 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 数据倾斜: Reduce 1: 800万条 ┃ Reduce 2: 50万条 ┃ Reduce 3: 50万条 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ↑ 这个 Reduce 跑了 2 小时,其他的 10 分钟就跑完了解决方案:
- 开启 Hive 的 Skew Join 优化
- 给热点 Key 加随机前缀,分散到多个 Reduce
- 用 Spark 引擎,Spark 对数据倾斜的处理更好
坑 3:动态分区插入慢
-- 动态分区:不指定具体分区值,Hive 自动根据数据生成分区INSERTINTOTABLElogsPARTITION(dt)SELECTuser_id,action,dtFROMraw_logs;如果分区数特别多(比如几万个),动态分区插入会非常慢。
解决方案:
- 限制动态分区数量:
SET hive.exec.max.dynamic.partitions=1000 - 分批插入,不要一次插入太多分区
Hive vs Spark SQL:选哪个?
| 维度 | Hive | Spark SQL |
|---|---|---|
| 定位 | 数据仓库工具 | Spark 的 SQL 模块 |
| 执行引擎 | 可插拔(MR/Tez/Spark) | 固定 Spark |
| 元数据 | 独立的 Metastore | 可用 Hive Metastore |
| 延迟 | 分钟/小时级 | 秒/分钟级 |
| 交互式查询 | 慢(Hive CLI 体验差) | 快(Spark Shell) |
| UDF 开发 | Java | Scala/Java/Python |
| 生态 | 成熟,企业广泛部署 | Spark 生态内统一 |
实际使用中,两者经常一起用:
- Hive 负责离线数仓的 ETL(数据清洗、分层建模)
- Spark SQL 负责交互式查询和复杂分析
- Spark SQL 可以直接读 Hive 表(
spark.sql("SELECT * FROM hive_table"))
小结
今天咱们聊了 Hive:
- 定位:给 Hadoop 提供类 SQL 的查询能力,不是数据库,是"翻译器"
- 架构:Driver 解析优化 SQL,Metastore 管理元数据,底层交给计算引擎执行
- 核心概念:内部表/外部表、分区(避免全表扫描)、分桶(优化 Join)、存储格式(ORC/Parquet)
- 执行流程:解析 → 语义分析 → 逻辑计划 → 优化 → 物理计划 → 执行
- 常见坑:小文件、数据倾斜、动态分区慢
- 与 Spark SQL 的关系:Hive 管数仓 ETL,Spark SQL 管交互查询,经常配合使用
Hive 最大的价值在于:它降低了大数据的入门门槛。你不需要会写 Java,不需要理解 MapReduce,只要会写 SQL,就能处理海量数据。这也是大数据技术能够普及的重要原因之一。
你用过 Hive 吗?有没有被数据倾斜折磨过?或者有没有写过特别长的 Hive SQL?欢迎聊聊~