订阅更新

Parquet、ORC、Avro选哪个?——列存、压缩、谓词下推怎么权衡?

数据湖手册 第 9 题 约 12 分钟读完

本文目录
  1. 先说结论
  2. 真实案例:用Avro导致查询慢5倍
  3. 列存 vs 行存:核心区别
  4. Parquet vs ORC vs Avro详细对比
  5. 谓词下推:为什么列存快?
  6. 什么时候用Avro?
  7. 面试高频追问
  8. 实战踩坑:用错格式导致ETL超时
  9. 总结

先说结论

Parquet:默认选择。列存格式,压缩率高,支持谓词下推,适合分析型查询(OLAP)。

ORC:Hive生态首选。列存格式,压缩率更高,支持ACID(Hive 3.0+),适合Hive数仓。

Avro:行存格式,Schema演化支持好,适合写入密集型场景(如Kafka Sink)和ETL中间层。

选择原则:分析型查询用Parquet/ORC,写入密集用Avro;Spark生态选Parquet,Hive生态选ORC。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

这道题面试官问得很多,因为存储格式选错了,性能会差很多。我在某出海硬件品牌遇到过'用Avro存储分析数据,查询比Parquet慢5倍'的问题。很多人只知道'Parquet是列存',但说不清楚'什么时候用Parquet、什么时候用ORC、什么时候用Avro、三者的区别是什么'。面试官想听的是你能说清楚原理和实战选择。

真实案例:用Avro导致查询慢5倍

2022年在某知名出海硬件品牌,我们接手了一个'用户画像'项目,前人用Avro格式存储用户画像数据(user_profile)。查询时发现性能很差:

1)查询'只选3列(user_id、age、gender)',但需要读取整个表(包括100列)

2)查询延迟高达5分钟,而同样数据的Parquet表只需要1分钟

3)存储空间:Avro 500GB vs Parquet 200GB(Parquet压缩率更高)

# 存储空间对比
du -sh user_profile_avro/    # 500GB
du -sh user_profile_parquet/ # 200GB
 
# 查询性能对比(只查3列)
spark.sql("SELECT user_id, age, gender FROM user_profile_avro").count()
# 耗时:5分钟
 
spark.sql("SELECT user_id, age, gender FROM user_profile_parquet").count()
# 耗时:1分钟

原因分析:Avro是行存格式(Row-oriented),每行的所有列存储在一起,即使只查3列,也要读取所有列的数据。Parquet是列存格式(Column-oriented),每列单独存储,只查3列就只读3列的数据。

解决方案:我们重新用Parquet格式存储数据,查询延迟从5分钟降到1分钟,存储空间从500GB降到200GB。

列存 vs 行存:核心区别

列存(Columnar Storage)和行存(Row Storage)的核心区别是'数据的物理存储方式':

行存(Avro、JSON、CSV):每行的所有列存储在一起,例如:[user_id=123, age=25, gender=M, country=US, ...],然后下一行。适合'写入密集'和'需要整行数据'的场景(如ETL、Kafka Sink)。

列存(Parquet、ORC):每列单独存储,例如:user_id列存储所有user_id值,age列存储所有age值,gender列存储所有gender值。适合'分析型查询'(OLAP),因为:1)只读需要的列;2)同类型数据压缩率高;3)支持谓词下推。

-- 示例:查询只选3列
SELECT user_id, age, gender FROM user_profile WHERE age > 25;
 
-- 行存:需要读取所有列(user_id、age、gender、country、...)
-- 即使只用3列,也要读100列
 
-- 列存:只读取3列(user_id、age、gender)
-- 其他97列不读,IO大幅减少

实战经验:我们在出海硬件品牌测试过:'查询只选10%的列'时,列存比行存快5-10倍;'查询所有列'时,列存和行存性能差不多。分析型查询(OLAP)通常只选10-30%的列,列存优势明显。

Parquet vs ORC vs Avro详细对比

维度ParquetORCAvro
存储格式列存(Columnar)列存(Columnar)行存(Row-oriented)
压缩率高(Snappy/Zstd)更高(Zstd)低(Snappy)
查询性能快(列存+谓词下推)更快(列存+更优压缩)慢(行存)
写入性能慢(需要构建列结构)慢(需要构建列结构)快(直接追加)
谓词下推支持(Min/Max统计)支持(Min/Max+Bloom Filter)不支持
Schema演化支持(向前兼容)支持(有限)支持(完美)
生态支持Spark、Impala首选Hive、Presto首选Kafka、Flume首选
ACID支持通过Delta LakeHive 3.0+支持不支持
我的选择Spark生态默认选Hive生态默认选写入密集/ETL中间层

谓词下推:为什么列存快?

谓词下推(Predicate Pushdown)是列存格式的核心优势。Parquet/ORC每个Row Group(约1万个行)存储'Min/Max统计信息',查询时可以'跳过不需要的Row Group'。

// 示例:查询WHERE age > 25
val df = spark.read.parquet("user_profile")
df.filter("age > 25").select("user_id", "age", "gender")
 
// Parquet的执行计划(谓词下推)
// 1) 读取Parquet文件的元数据(Min/Max统计信息)
// 2) 跳过age.Max <= 25的Row Group(不读数据)
// 3) 只读age.Max > 25的Row Group的数据
// 4) 只读3列(user_id、age、gender)
 
// 如果是Avro(行存),没有谓词下推,需要:
// 1) 读取所有列的所有数据
// 2) 在内存中过滤age > 25
// 3) 提取user_id、age、gender列

实战案例:我们在出海硬件品牌测试过:'查询WHERE dt='2024-03-19' AND age > 25',Parquet只需要扫描'1/365的分区'和'部分Row Group',而Avro需要扫描'所有数据'。查询延迟:Parquet 30秒 vs Avro 3分钟。

Bloom Filter:ORC还支持Bloom Filter(布隆过滤器),可以进一步加速'等值查询'(如WHERE user_id='12345')。Parquet不支持Bloom Filter(Delta Lake通过Z-Ordering弥补)。

什么时候用Avro?

虽然Parquet/ORC在查询性能上有优势,但Avro在某些场景下更合适:

场景1:写入密集型场景——如Kafka Sink、实时流写入。Avro是行存,写入性能比Parquet高2-3倍(不需要构建列结构)。

// Kafka Sink用Avro格式写入
val kafkaDF = spark.readStream.format("kafka")...
kafkaDF.writeStream.format("avro")
  .option("path", "/data/user_events")
  .option("checkpointLocation", "/checkpoint/kafka")
  .start()
 
// 为什么不用Parquet?
// 1) Avro写入速度快2-3倍
// 2) Avro支持Schema演化(Kafka topic的Schema可能变化)
// 3) 写入后可以用Spark批处理任务转换为Parquet

场景2:ETL中间层——如ODS层(原始数据层)。ODS层需要'快速写入'和'保留原始Schema',Avro更合适。

场景3:Schema演化频繁——如'日志数据Schema经常变化'。Avro的Schema演化支持最完善(向前兼容、向后兼容)。

// Avro Schema示例(支持演化)
{
  "type": "record",
  "name": "UserEvent",
  "fields": [
    {"name": "user_id", "type": "string"},
    {"name": "age", "type": ["null", "int"], "default": null},  // 新增字段
    {"name": "event_type", "type": "string"}
  ]
}
 
// 旧数据读新Schema:age字段为null
// 新数据读旧Schema:忽略age字段

面试高频追问

追问一:Parquet和ORC怎么选?

原则:Spark生态选Parquet,Hive生态选ORC。Parquet是Spark的默认格式,优化得更好;ORC是Hive的默认格式,支持ACID(Hive 3.0+)。如果用Presto/Trino,两者都支持,ORC的压缩率略高。如果用Impala,只能用Parquet。

追问二:什么是谓词下推?为什么重要?

谓词下推是'将过滤条件推到数据源层执行',减少扫描数据量。Parquet/ORC的每个Row Group存储'Min/Max统计信息',查询时可以'跳过不需要的Row Group'。例如:查询WHERE age > 25,Parquet只读'age.Max > 25的Row Group',不读'age.Max <= 25的Row Group'。这是列存格式快的重要原因。

追问三:为什么Avro写入性能比Parquet好?

Avro是行存,写入时'直接追加'即可,不需要构建列结构。Parquet是列存,写入时需要'按列重组数据'(收集所有user_id、所有age、所有gender...),计算量大。实测:Avro写入速度比Parquet快2-3倍。

实战踩坑:用错格式导致ETL超时

2023年在某出海硬件品牌,我们遇到过一次'用错格式导致ETL超时'的问题。当时我们上线了一个'实时数仓'项目:

1)Kafka数据用Avro格式写入ODS层(写入快,没问题)

2)ODS层数据用Avro格式存储(问题来了!)

3)DWD层(明细层)从ODS层读取数据,用Spark SQL做ETL

运行后发现:DWD层的ETL任务经常超时(>2小时),而且'GC频繁'(Young GC每分钟几十次)。

// 问题代码:ODS层用Avro格式
val odsDF = spark.read.format("avro").load("/data/ods/user_events")
 
// ETL逻辑:复杂的过滤、Join、聚合
val dwdDF = odsDF
  .filter("event_time >= '2024-03-19'")
  .join(dimUser, "user_id")
  .groupBy("user_id", "event_type").count()
 
// 问题:Avro是行存,每次读取都要读所有列
// 即使只用3列,也要读100列,IO巨大

排查发现:ODS层用Avro格式存储,DWD层ETL需要'频繁扫描ODS层数据',每次都'读所有列',导致IO巨大。而且Avro的压缩率低(500GB vs Parquet 200GB),磁盘IO压力更大。

解决方案:我们做了两件事:1)ODS层保留Avro(写入快);2)增加一个'中间层(ODS_Parquet)',每天凌晨用批处理任务将ODS的Avro数据转换为Parquet格式。DWD层从ODS_Parquet读取数据。改后DWD层的ETL时间从2小时降到30分钟,GC频率大幅降低。

// 改进后:增加Parquet中间层
// 1) ODS层:Avro格式(快速写入)
kafkaDF.writeStream.format("avro").save("/data/ods/user_events")
 
// 2) 批处理任务:Avro -> Parquet(每天凌晨运行)
spark.read.format("avro").load("/data/ods/user_events")
  .write.format("parquet")
  .save("/data/ods_parquet/user_events")
 
// 3) DWD层:从Parquet读取(快速读取)
val odsDF = spark.read.format("parquet").load("/data/ods_parquet/user_events")
// ETL逻辑不变,但速度快4倍

总结

存储格式的选择直接影响性能。Parquet是Spark生态的默认选择,列存格式,压缩率高,支持谓词下推,适合分析型查询。ORC是Hive生态的默认选择,压缩率更高。Avro是行存格式,写入性能好,Schema演化支持完善,适合写入密集型场景(Kafka Sink、ETL中间层)。选择原则是:分析型查询用Parquet/ORC,写入密集用Avro;Spark生态选Parquet,Hive生态选ORC。面试时能说清楚'列存vs行存的区别、谓词下推的原理、实战选择经验',就很稳。

下一篇聊一个很多人困惑的问题:数据湖查询比Hive还慢?——谓词下推和向量化执行怎么配置?

你们生产环境用哪种存储格式?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器