1. 为什么文件存储格式值得单独拎出来讲
很多人学Hive的时候,注意力都放在SQL语法、分区表、分桶表这些"看得见"的东西上,觉得文件格式无非就是建表时写一句STORED AS ORC或者STORED AS TEXTFILE的事。我刚接触数仓那会儿也是这么想的,直到有一次帮同事排查一个查询慢的问题——同样的数据量、同样的SQL逻辑,两张表跑出来的耗时差了将近八倍,最后定位下来就是存储格式选错了。从那以后我才真正意识到,文件存储格式在Hive里绝对不是可有可无的装饰项,它直接决定了数据在磁盘上怎么躺、查询时怎么读、压缩比能到多少、能不能用向量化执行。
先把这件事的本质说清楚。Hive本身不存储数据,它只是把HDFS上的文件"翻译"成一张张表。你建表时指定的存储格式,实际上是在告诉Hive:这些文件里的字节该怎么解析成行和列。不同的格式,解析方式完全不同,带来的读写性能、存储成本、Schema演化能力也天差地别。所以选格式这件事,本质上是在"写入成本、读取成本、存储成本、灵活性"这四个维度之间做权衡,没有哪个格式是万能的。
这篇文章我会把Hive里常见的几种文件格式——TextFile、SequenceFile、RCFile、ORC、Parquet、Avro——挨个拆开讲,重点说清楚它们各自的底层结构、适用场景,以及我在实际项目里踩过的坑。如果你正在做数仓选型,或者被某个查询性能问题卡住了,这篇应该能帮你少走点弯路。适合有一定Hive基础、想深入理解存储层的人看,纯新手也能看懂,因为我会尽量用生活化的类比来解释。
2. 行存储与列存储:理解一切格式差异的起点
2.1 用"书架摆书"理解行存和列存
要搞懂各种格式的差异,必须先理解行存储和列存储这两个概念。我习惯用书架来类比:假设你有一排书架,每本书代表一行数据,书里的每一页代表一个字段。
行存储就像把每本书完整地放在书架上,你要查某本书的第3页,得先把整本书抽出来翻到第3页。优点是拿一整本书很方便,缺点是只想看某一页时,其他页都是累赘。TextFile、SequenceFile、Avro都是典型的行存储。
列存储则是把每本书的第1页抽出来放一起、第2页抽出来放一起,按页归档。你要查所有书的第3页,直接去第3页那一摞拿就行,不用碰其他页。缺点是如果你想完整读一本书,得跑好几个地方把页凑齐。ORC、Parquet、RCFile属于列存储。
这个差异在数仓场景里影响巨大。因为数仓的查询有个典型特征:宽表多、字段多,但每次查询往往只用到少数几个字段。比如一张用户行为宽表有80个字段,你写个报表可能只查user_id、event_time、event_type这三个。行存储模式下,Hive得把80个字段全读出来再丢掉77个;列存储模式下,它只读那3列对应的数据块,I/O量直接降到几十分之一。这就是为什么列存储在OLAP场景里几乎是标配。
2.2 列存储为什么天然适合压缩
列存储还有个隐藏优势:同一列的数据类型相同、取值往往有规律,压缩效率远高于行存储。举个例子,一列"省份"字段,行存储里它和用户ID、时间戳、金额混在一起,压缩算法很难找到规律;列存储里这一列全是"广东""浙江""江苏"这样的重复值,用字典编码或者游程编码能压到原来的几十分之一。
我在一个项目里做过对比:同样一份10亿行的日志数据,TextFile格式占HDFS空间约420GB,换成ORC加ZLIB压缩后只有38GB左右,压缩比超过10:1。这不只是省磁盘的事——数据量小了,扫描时的I/O自然就少,查询速度跟着上去。所以选格式的时候,压缩能力必须纳入考量。
2.3 一个容易被忽略的点:Schema演化
除了读写性能,格式对Schema变化的支持程度也不一样。业务是活的,表结构经常要加字段、改字段类型。有些格式(比如Avro、Parquet)对Schema演化支持得很好,加个字段老数据照样能读;有些格式(比如早期的RCFile)改起来就很别扭。这一点在做长期数仓规划时特别重要,后面讲具体格式时我会展开。
3. TextFile:最朴素也最容易踩坑的默认格式
3.1 TextFile到底是怎么存数据的
TextFile是Hive的默认存储格式,也是最好理解的:数据就是纯文本,一行一条记录,字段之间用分隔符隔开(默认是\001,也就是Ctrl+A)。你建表时不写STORED AS,默认就是它。
CREATE TABLE user_log_text ( user_id STRING, event_time STRING, event_type STRING ) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t' STORED AS TEXTFILE;这种格式的好处是人眼可读、通用性极强。你用hdfs dfs -cat就能直接看数据,用任何工具都能处理,导入导出毫无障碍。做数据交换、临时中转、跟外部系统对接时,TextFile是最省心的选择。
3.2 TextFile的性能陷阱
但TextFile的缺点同样明显,而且很多坑是新手意识不到的。
第一,它不支持块级压缩,只能整文件压缩。也就是说,如果用一个大的gzip文件存TextFile数据,Hive没法把它切分给多个Map任务并行处理,只能一个任务从头读到尾。数据量一大,这个任务就成了瓶颈。我见过一个同事用gzip压缩的TextFile表,1TB数据只起了1个Map,跑了四个多小时。解决办法是用支持切分的压缩格式(如bzip2),或者干脆换列存格式。
第二,它没有Schema信息。TextFile里只有裸数据,字段类型、列名全靠建表语句"约定"。一旦建表语句和数据对不上,读出来就是一堆null或者错位,而且Hive不会报错,非常隐蔽。
第三,解析开销大。每次查询都要逐行按分隔符切分、做类型转换,CPU消耗比二进制格式高不少。字段越多、类型越复杂,这个开销越明显。
第四,空值处理有坑。TextFile里\N表示null,但如果你的数据里本来就有字符串\N,就会混淆。这个坑我在处理用户填写的备注字段时踩过,后来统一改成用其他格式存。
3.3 TextFile的合理使用场景
说了这么多缺点,不是让你完全弃用它。我的经验是,TextFile适合这几类场景:数据量不大(几十GB以内)的中间表、需要人工查看或外部系统对接的落地表、以及ETL流程最源头的原始数据接入层。真正参与高频查询的核心表,还是尽早换成列存格式。
提示:如果非要用TextFile存大表,压缩格式优先选bzip2(可切分),别用gzip。或者在建表时配合
hive.exec.compress.output和合适的压缩编码,至少省点空间。
4. SequenceFile与RCFile:两个过渡性选手
4.1 SequenceFile:为Hadoop而生的二进制行存
SequenceFile是Hadoop生态里比较早的一种二进制格式,本质上是key-value对的序列。它支持块级压缩和记录级压缩,能切分,比TextFile在性能上有明显提升。在Hive早期,它常被用作中间结果的存储格式,因为读写比TextFile快,又不像列存那样复杂。
但SequenceFile的问题在于:它还是行存储,宽表查询时该读的冗余数据一点没少。而且它的Schema支持也很弱,跨语言兼容性差。现在新项目基本不会选它了,更多是维护老系统时会遇到。如果你接手了一个用SequenceFile的存量表,知道它是行存、支持压缩和切分就够了,迁移时优先考虑ORC或Parquet。
4.2 RCFile:Hive列存的第一次尝试
RCFile(Record Columnar File)是Hive团队早期为了弥补行存缺陷搞出来的列式格式。它的结构是"先水平切分成Row Group,再在Group内部按列存"。这样既保留了行存"一行数据在同一个Group里"的局部性,又获得了列存"按列读取"的好处。
RCFile在当年是个不小的进步,但它有几个硬伤:不支持Schema演化(加字段很麻烦)、压缩效率不如后来的ORC、对嵌套数据类型的支持有限。所以现在它基本被ORC取代了。我个人的建议是,除非你在维护一个历史遗留系统,否则新表不要选RCFile,直接上ORC。
不过理解RCFile的结构对理解ORC很有帮助,因为ORC可以看作是RCFile的全面升级版——同样的"先分行组再分列"思路,但在索引、压缩、Schema演化上做了大量增强。
5. ORC:Hive亲儿子的深度拆解
5.1 ORC的文件结构长什么样
ORC(Optimized Row Columnar)是Hive社区专门为Hive优化设计的列式格式,可以说是"亲儿子"。它的文件结构从大到小是这样的:整个文件被切成若干个Stripe(默认64MB或256MB),每个Stripe内部按列存储,每列的数据又分成多个Stream(比如PRESENT流存null标记、DATA流存实际值、LENGTH流存长度)。
每个Stripe的头部有Index Data,记录了每列的最小值、最大值、以及一些统计信息。查询时Hive先读Index,判断这个Stripe里有没有符合条件的数据,没有就整块跳过。这个机制叫谓词下推(Predicate Pushdown),是ORC快的重要原因之一。
文件尾部有File Footer,存了整个文件的Schema、行数、每列的统计信息;再往后是PostScript,记录了压缩算法、Footer长度等元信息。读文件时先从尾部读PostScript,再定位到Footer,最后按需读Stripe。
5.2 ORC为什么快:三个关键机制
第一,列裁剪。你只查3列,ORC就只读这3列的Stream,其他列碰都不碰。这是列存的通用优势,但ORC实现得比较彻底。
第二,谓词下推加Stripe跳过。假设你查WHERE dt='2024-01-01',ORC读Index发现某个Stripe的dt列最小值是2024-01-02,直接跳过这个Stripe。数据按分区、按排序键组织得好时,这个跳过率能到90%以上。
第三,向量化读取。ORC支持一次读一批行(默认1024行)而不是一行一行读,配合Hive的向量化执行引擎,CPU利用率大幅提升。这个特性在Hive 0.13之后默认开启,效果非常明显。
5.3 ORC的压缩选择与实测数据
ORC支持多种压缩编码:NONE、SNAPPY、ZLIB、LZO。我的实测经验是这样的:
| 压缩方式 | 压缩比 | 解压速度 | 适用场景 |
|---|---|---|---|
| NONE | 1:1 | 最快 | 对CPU极度敏感、磁盘充裕 |
| SNAPPY | 约3:1 | 快 | 平衡之选,最常用 |
| ZLIB | 约8:1 | 较慢 | 存储紧张、查询不频繁 |
| LZO | 约3:1 | 快 | 需要可切分且快解压 |
大部分场景我推荐SNAPPY,压缩比和解压速度平衡得最好。如果这张表是冷数据、查询频率低,可以上ZLIB省空间。建表时这样写:
CREATE TABLE user_log_orc ( user_id STRING, event_time STRING, event_type STRING ) STORED AS ORC TBLPROPERTIES ("orc.compress"="SNAPPY");5.4 ORC的Schema演化与ACID支持
ORC对加字段的支持很好:在表末尾加列,老数据读出来新列为null,不需要重写历史文件。这个特性在做数仓迭代时太重要了,省去了大量数据回刷的工作。
另外,ORC是Hive支持ACID事务(增删改)的底层格式。如果你需要做行级更新、merge操作,表必须是ORC格式。这一点在做维度表缓慢变化(SCD)时经常用到。
注意:ORC虽然强大,但它的跨平台兼容性不如Parquet。如果你的数据还要给Spark、Presto、Flink等多个引擎读,Parquet的通用性更好。纯Hive生态内,ORC是最优解。
6. Parquet:跨引擎时代的通用选择
6.1 Parquet的设计哲学
Parquet和ORC几乎是同时期出现的列式格式,但它的定位更"中立"——它不绑定任何计算引擎,目标是成为Hadoop生态的通用列存标准。它的结构和ORC类似:行组(Row Group)、列块(Column Chunk)、页(Page),也支持谓词下推和列裁剪。
Parquet最大的特点是嵌套数据结构支持得非常好。它用一套叫Dremel的算法(就是Google那篇著名论文里的方案)来处理嵌套的数组、结构体。如果你的数据里有复杂的嵌套字段,比如JSON里套数组再套对象,Parquet处理起来比ORC更自然。
6.2 ORC和Parquet到底怎么选
这是被问得最多的问题。我的经验总结成一张表:
| 维度 | ORC | Parquet |
|---|---|---|
| 主要生态 | Hive原生 | 跨引擎通用 |
| 压缩效率 | 略优 | 略逊 |
| 嵌套数据 | 支持一般 | 支持优秀 |
| 向量化 | Hive内成熟 | Spark内成熟 |
| Schema演化 | 加列方便 | 加列方便 |
| ACID | Hive支持 | Hive支持较弱 |
结论很直接:纯Hive数仓选ORC,多引擎混用选Parquet。我现在的项目是Hive加Spark混跑,统一用Parquet,省得两边格式不一致出问题。如果确定只在Hive里用,ORC的压缩和查询性能会更好一点。
6.3 Parquet的踩坑记录
Parquet有个坑我印象很深:小文件问题。Parquet的Row Group默认128MB,如果写入时每个文件都很小,会产生大量小文件,NameNode压力大,查询时也要起很多Map任务。解决办法是写入前做一次合并,或者用INSERT ... SELECT时控制reduce数量。
另一个坑是Schema不一致导致的读取失败。Parquet文件里存了Schema,如果建表语句和文件Schema对不上(比如字段顺序变了、类型不兼容),读的时候会报错。所以用Parquet时,建表语句要和写入时的Schema严格对齐,别指望它像TextFile那样"随便读"。
7. Avro:行存里的Schema演化之王
7.1 Avro的独特定位
Avro是行存储格式,但它在Schema管理上做到了极致。它的数据文件里内嵌了完整的Schema(用JSON描述),读数据时先读Schema再读数据,所以永远不会有Schema对不上的问题。这个特性让它在数据交换、消息队列、流式场景里特别受欢迎。
Avro支持非常完善的Schema演化规则:加字段给默认值、删字段、改字段类型(在兼容范围内),老数据和新Schema之间能自动转换。这一点比Parquet和ORC都强。
7.2 Avro在Hive里的适用场景
在Hive数仓里,Avro用得不算多,因为它是行存,查询性能不如列存。但有几类场景它很合适:数据接入层(原始数据格式多变,用Avro能扛住Schema变化)、需要长期归档且Schema可能演化的表、和Kafka等消息系统对接的落地表。
我有个项目用Avro存用户画像的原始埋点,因为埋点字段经常加,用Avro后加字段完全不用回刷历史数据,省了大量运维成本。查询时如果嫌慢,可以再建一张ORC的派生表专门供分析用。
8. 格式选型的决策框架与实战建议
8.1 一张决策表搞定选型
把前面讲的综合起来,我整理了一个选型决策表,你可以直接对照自己的场景:
| 场景 | 推荐格式 | 理由 |
|---|---|---|
| 原始数据接入、格式多变 | Avro / TextFile | Schema演化强、通用 |
| 核心分析宽表、纯Hive | ORC + SNAPPY | 压缩好、查询快 |
| 多引擎混用(Hive+Spark) | Parquet + SNAPPY | 跨引擎兼容 |
| 需要ACID事务 | ORC | Hive事务支持最好 |
| 临时中间表、小数据量 | TextFile | 简单、可读 |
| 冷数据归档 | ORC + ZLIB | 压缩比最高 |
8.2 我踩过的三个真实坑
坑一:默认格式的惯性。很多人建表不写STORED AS,默认就是TextFile,然后一直用下去。我接手过一个项目,核心报表表全是TextFile,查询慢得离谱,换成ORC后平均查询时间从3分钟降到20秒。所以建核心表时,第一件事就是显式指定格式。
坑二:压缩格式和存储格式混淆。有人以为STORED AS ORC就自动压缩了,其实ORC默认压缩是ZLIB,但如果你不显式配orc.compress,不同版本行为可能不一致。稳妥做法是建表时明确写上压缩方式。
坑三:小文件不合并。不管什么格式,小文件多了都慢。ORC和Parquet尤其明显,因为每个文件都有Footer和元数据开销。我一般会在ETL最后加一步合并,或者用hive.merge.mapfiles和hive.merge.mapredfiles让Hive自动合并小文件。
8.3 格式转换的实操方法
实际工作中经常需要把TextFile表转成ORC表,标准做法是:
-- 建ORC目标表 CREATE TABLE user_log_orc ( user_id STRING, event_time STRING, event_type STRING ) STORED AS ORC TBLPROPERTIES ("orc.compress"="SNAPPY"); -- 从TextFile表导入 INSERT INTO TABLE user_log_orc SELECT user_id, event_time, event_type FROM user_log_text;如果数据量特别大,建议按分区分批导,避免单个任务跑太久。导入完成后对比一下两边的存储占用和查询耗时,你会对格式的威力有更直观的感受。
提示:转换前先确认目标表的字段类型和源表一致,尤其是时间字段,TextFile里常存成字符串,ORC表里最好转成timestamp或date类型,查询时能利用统计信息做下推。
9. 关于格式选择,我个人的几条经验
做了这么多年数仓,关于文件格式我最大的体会是:格式选择不是技术炫技,而是成本核算。选ORC还是Parquet,选SNAPPY还是ZLIB,背后都是"省了多少磁盘、快了多少查询、多花了多少CPU"的账。没有绝对的对错,只有适不适合当前场景。
我的习惯是,新项目一开始就用ORC或Parquet打底,别等数据量大了再迁移,迁移成本远高于一开始就选对。接入层可以用TextFile或Avro保持灵活,但核心分析层一定要用列存。另外,格式选型要和分区策略、分桶策略一起考虑,三者配合好了,查询性能会有质的提升。
最后分享一个小技巧:如果你不确定某个格式的实际效果,别光看文档,拿自己的一小部分真实数据做个对比测试。建两张结构相同的表,一张TextFile一张ORC,导入同样的数据,跑几个典型查询,对比耗时和存储占用。这个测试花不了半小时,但得到的数据比任何文档都有说服力。我自己就是这么一步步摸清各种格式脾气的,希望这套方法对你也有用。