去年帮一家中型电商做BI系统性能排查,一个回溯近三年用户行为数据的SQL跑了将近40分钟。拉出执行计划一看,底层数据全存成了CSV文本格式,每次查询都在做全表扫描。改写成Parquet之后,同样的查询跑进3分钟以内,不是优化了SQL,不是扩了集群,只是换了个存储格式。这就是我想在这篇文章里聊透的事:在BI回溯分析场景下,存储格式不是“存一下就行”的无关选项,而是决定查询能力上限的核心变量。
做了十多年数据平台,参与过几十个BI系统的底层架构设计,我越来越确信一件事:存储格式的选择,对回溯分析查询性能的影响可以达到10到100倍,远超过很多人对索引优化或SQL调优的预期。
这不是理论推导,而是我在多个项目里反复验证过的事实。用一个我自己的测试案例来说明。同一份数据,约5亿行、120个字段的电商订单明细,分别存成五种常见格式,在同一套Spark集群上做一次典型的BI回溯查询,按品类汇总过去12个月的销售额和毛利率。结果如下:
| 存储格式 | 查询耗时 | 存储体积 | 单次查询扫描数据量 |
|---|---|---|---|
| CSV(无压缩) | 2180秒 | 620 GB | 620 GB(全表) |
| CSV(Gzip压缩) | 1970秒 | 180 GB | 180 GB(仍须解压全量) |
| JSON(每行一条记录) | 2560秒 | 890 GB | 890 GB(全表) |
| Avro(行式存储) | 1680秒 | 155 GB | 约400 GB(无法做列裁剪) |
| ORC(列式存储,Zlib压缩) | 175秒 | 78 GB | 约22 GB(仅读取用到的5列) |
| Parquet(列式存储,Snappy压缩) | 152秒 | 92 GB | 约25 GB(同上,列裁剪+谓词下推) |
看这组数据,结论很清楚:列式存储格式(Parquet和ORC)在BI回溯分析场景中,把查询效率从“小时级”直接拉到了“分钟级”。而且,上表里的查询并不是特殊优化过的极端案例,就是业务上最常见的聚合类分析SQL。差距之所以这么大,根源在于一个核心机制。
BI回溯分析有一个被很多人忽视的特征:查询几乎从不读取全部字段,但几乎总要扫描大量行。聚合、分组、趋势分析,通常只涉及十几个字段中的三五列,但时间范围可能跨越数月甚至数年。这时候,行式存储必须把整行数据全部加载到内存,哪怕你只需要其中一列;而列式存储可以精准定位到目标列的数据块,物理上跳过所有不相关的列。根据我实际项目中的统计,列裁剪在回溯分析场景中可以减少80%到95%的磁盘I/O。而I/O恰恰是分布式查询中最慢的环节。

在深入聊格式之前,我必须先把“回溯分析”这四个字的真正含义讲清楚,因为太多人在这个问题上犯糊涂。
回溯分析在BI场景里特指这样一类操作:对已经沉淀下来的历史数据,以时间为锚点进行汇总、对比、趋势分析。比如“对比过去三年每个月的销售额”、“分析去年双十一前后一周的用户转化路径变化”、“找出近五年毛利率持续下滑的品类”。
这类查询有三个共同特征:
这和OLTP的事务查询(按主键查某一行的所有字段)完全不同。如果把后者的优化思路搬到前者身上,就像用跑车的设计理念去造卡车,材料再好,方向错了。

列式存储的设计哲学和回溯分析的需求几乎是天作之合。以Parquet为例,它的文件内部按照列来组织数据页,同一列的数据在磁盘上物理相邻。这意味着:
CSV和JSON这种行式文本格式最大的问题不是“慢”,而是“你想省也省不掉”。查询一句SELECT SUM(amount) FROM orders WHERE year=2024,行式存储必须把每一行的所有字段都解析一遍,即使amount只是200个字段中的一个。JSON更惨,每行都要做字符串解析、键名匹配,CPU开销远高于二进制格式。
Avro虽然是二进制行式格式,读取效率比文本格式高很多,但在回溯分析里仍然要吃“全列加载”的亏。在我的一次对比测试中,Avro的读取性能大约是CSV的1.8倍,但只有Parquet的十分之一,因为扫描量摆在那里,再怎么优化也绕不过去。

在这一行待久了,我发现有些观念根深蒂固,每次做技术评审都得重新掰扯。以下是三个我在实际项目中反复遇到、每次都差点吵起来、最后用测试数据拍死的认知偏差。
这是最容易产生幻觉的方案。逻辑听起来没问题:CSV太大,压缩一下体积小了,查询不就快了吗?但压缩解决的是存储体积问题,不是数据扫描量问题。Gzip压缩的CSV文件在查询时仍然需要完整解压整个文件才能访问其中任何一列。解压后的数据量没变,而且解压本身还额外消耗CPU。
在我上面的测试里,CSV加Gzip压缩后存储体积从620GB降到了180GB,但查询耗时只从2180秒降到了1970秒,降幅不到10%。这210秒的缩减完全来自解压后数据在内存中传输时间的减少,而扫描量仍然是全量的。很多团队花大力气给历史数据做Gzip压缩,最后发现查询还是慢,问题就出在这里。
这句话前半句不完全对,后半句过于绝对。ORC确实是Hive生态里深度优化的格式,它在Hive上的谓词下推、向量化读取、轻量级索引等方面有独特的性能优势。但这不代表ORC在其他引擎上就不能用。实际上,Spark SQL对ORC的读取支持已经很成熟,而在某些特定场景(比如大量使用时间分区的表),ORC的内部分区索引甚至比Parquet的Row Group统计信息更高效。
我在一个项目里实测过,在Spark上对一个按日分区的ORC表做范围查询,利用ORC文件尾部的Stripe级统计信息,比同等条件下的Parquet快了约12%。两者差距不算巨大,但至少说明不能无脑地“Spark就选Parquet”。
这是我见过代价最高的一种“方便”。CSV在今天写的时候很方便,但等数据量涨上来之后,格式迁移的成本指数级上升。重写几TB的历史数据需要大量计算资源,迁移期间系统还可能出兼容性问题。我遇到过一个客户,两年间积累了约15TB的CSV日志数据,最后做格式迁移花了两周时间、烧掉了上万元的云计算费用,而这笔开销原本是可以在项目初期就用对格式来避免的。
格式选择应该是架构设计初期的决策,而不是留到后面的“优化项”。等到用户抱怨BI报表加载慢、老板拍桌子的时候再来改格式,你已经输在了起跑线上。
前面铺垫了这么多,现在进入核心的决策框架。我帮团队做存储格式选型的时候,有一套固定的思考路径,不是对着特性表打勾,而是从业务侧开始反向推导。
不是所有历史数据查询都是同一类。我习惯把回溯分析拆成三种子类型:
大多数BI场景中,前两种类型占比在80%以上,所以列式存储是毫无疑问的首选。但如果你发现自己绝大部分查询都是按某个唯一ID查单行详情,那就该考虑行式存储了,只不过这种情况在BI里极为罕见。
同样的Parquet文件,在不同引擎上的表现可以相差30%以上。这不是Parquet的问题,是引擎对该格式的实现程度不同。根据我自己的测试数据和社区反馈,给出以下适配参考:
| 查询引擎 | Parquet表现 | ORC表现 | 建议 |
|---|---|---|---|
| Spark SQL | 优异,向量化读取原生支持 | 优秀,但谓词下推在嵌套结构上稍弱 | 首选Parquet,除非表主要是扁平结构且频繁使用时间范围过滤 |
| Presto/Trino | 极优,社区对Parquet优化最深 | 良好,较新的版本已大幅提升 | 首选Parquet |
| Hive 3.x | 良好 | 极优,ACID事务表默认格式,Stripe级索引深度优化 | 首选ORC |
| ClickHouse | 不适用 | 不适用 | 使用自有MergeTree格式,可将Parquet/ORC作为外部表读取 |
| DuckDB | 极优,设计之初就深度绑定Parquet | 不支持 | 只能用Parquet |
这里要特别注意一个容易被忽视的细节:嵌套数据结构(Nested Types)的支持差异。如果你的数据里有数组、Map这种复杂字段,Parquet对嵌套列的谓词下推比ORC成熟得多。我在一个用户行为分析的案例里,因为大量使用了数组字段存储用户点击序列,从ORC切换到Parquet后,过滤查询快了40%,不是格式整体更优秀,而是在这个特定特性上Parquet的实现更完善。

格式选择不应该是一刀切的决策。历史数据有不同的“温度”,没必要用同一种方式存储所有阶段的数据。我在一个物流行业客户的项目中设计过一套分层存储策略,上线后年存储成本降低了约40%,同时高频查询的P95响应时间还缩短了:
这个分层策略的核心逻辑是:格式不只要匹配查询模式,还要匹配数据的生命周期阶段。新数据的查询模式通常是探索性的、多变的,需要完整的明细字段;老旧数据则偏向固定的汇总报表,可以提前聚合。

Parquet和ORC选哪个,是我被问得最多的一个问题。先说整体结论:在绝大多数BI回溯分析场景中,两者的性能差距在10%以内,选择任何一个都能获得量级上的提升。纠结选哪个而迟迟不做决定,比选错一个代价大得多。
但既然做技术决策,有些细节点还是值得说清楚的。
Parquet以Row Group为基本读写单元,每个Row Group有独立的列块和统计信息。ORC以Stripe为基本单元,同时还有文件级、Stripe级和行组级三级统计信息。
这个设计差异在实际查询中会体现为:

如果非要我给出一个明确的建议:
如果以上都不适用,随便选一个,把精力用在优化分区策略和统计信息维护上,比纠结格式更有价值。
2023年年底,我接手了一个物流云仓客户的BI性能优化项目。情况是这样的:
我做的第一件事不是调SQL,而是把最近三个月的数据先转成Parquet格式,在同样的集群上跑同样的查询。结果耗时从38分钟骤降到4分20秒。再进一步优化了分区策略(从按周改按日),查询稳定在2分半以内。
这个案例里还有一个小细节值得单独拿出来说:日志JSON里有大量冗余的嵌套键名,比如每条记录都完整重复了“latitude”“longitude”“timestamp”“status”这些键名字符串。8亿次重复,这个开销非常可观。转换成Parquet后,Schema只在文件头存一次,数据部分只存值,仅这一项就节省了约35%的存储空间和相应的解析时间。

性能是格式选择最直观的维度,但不是唯一维度。另外三个容易被忽视的影响,我在多个项目中深有体会。
业务变化是常态,今天订单表有80个字段,半年后可能变成120个。格式对Schema演化的支持能力,决定了你的数据管道是持续健壮还是逐渐腐烂。
CSV和JSON在这方面是最差的。CSV根本没有Schema的概念,列名存在文件头,多一个少一个全靠约定,一旦不同时期的数据文件列不一致,下游查询直接报错。JSON稍好,因为键值对可以灵活增减,但读取引擎需要自己做Schema推断和合并,效率低且容易出错。
Avro和Parquet都原生支持Schema演化,但实现方式不同:

我在前面那个物流案例的测试数据里已经展示过压缩率的差异:同一份数据,JSON占890GB,Parquet占92GB,差了接近10倍。在云环境下,每TB每月的存储费用大约200到400元(不同云厂商和存储层级有差异),如果是PB级数据,格式选择带来的存储成本差异可以达到每年数十万元。
但成本不仅看存储。查询扫描的数据量决定了计算资源的消耗。每次查询CSV要扫描全量620GB,而Parquet只需要扫描25GB,中间差了25倍的I/O和CPU消耗。在按扫描量计费的查询服务中(比如一些云数仓),这个差距直接反映在账单上。
CSV和JSON最大的优势是可读性好,出了问题可以直接用文本编辑器打开看。这个优势在开发调试阶段有实际价值,但在生产环境中,这种需求出现的频率远低于你的想象。而且,现在的主流查询引擎和IDE都提供了对Parquet和ORC文件的直接浏览和采样功能,不需要人工去翻文件。
为了那0.1%的“可能需要直接看原始文件”的场景,牺牲99.9%场景下的查询性能,这笔账怎么算都是亏的。
说了这么多结论,回到一个很现实的问题:很多团队的历史数据已经存成了CSV或者JSON,数据量还不小,怎么处理?我有两套经过实战验证的迁移方案。
适用于数据量在5TB以下、可以接受几小时的停机窗口、团队有足够计算资源的场景。操作步骤:
这个方案的优点是干净利落,缺点是迁移期间写入需要暂停或者做双写。我一般建议选择业务低峰期(比如凌晨2点到6点)执行,把影响降到最低。
适用于数据量大、不能接受停机、新旧格式需要并行跑一段时间的场景。操作思路:
这个方案的优点是平滑、风险低,缺点是引擎需要同时兼容两种格式,查询视图时会有一点额外开销。但对于TB级以上数据量,这是更务实的选择。

最后,我把前面所有讨论浓缩成一套实用的决策流程。下次你在做技术选型的时候,可以逐条过一遍:
写到这里,我想强调一个比任何技术细节都重要的观点:在BI系统的数据架构中,存储格式的选择具有“路径依赖”效应。你今天拍板的格式,会成为未来两年所有查询、所有ETL、所有数据产品的底层约束。改一个格式容易,改一个大团队、几十张表、几百个下游应用对格式的依赖很难。所以,在这个决策上花两个小时认真做调研和测试,可能是你今年最划算的时间投资。
如果你的团队现在正在被BI查询慢的问题困扰,我的建议是:别急着扩集群、别急着加索引、别急着做缓存。先去看一眼你的历史数据底层存在了什么格式里。如果答案是CSV或者JSON,那你已经找到了一个几乎零成本、收益巨大的优化方向。

我之前一直用CSV存历史数据,每次跑一个季度销售额汇总要等十几分钟,运维同事推荐我转成Parquet,说能快十倍。但我试了一下,好像没他们说的那么夸张?请问列式存储到底是怎么加速回溯分析的,实际能快多少?有没有具体的测试数据?
我踩过这个坑。两年前我们团队负责一个电商BI平台,历史订单数据每天以TB级增长,底层全是CSV加Gzip压缩。每次产品经理要求按品类、地区、时间切片回溯做销售漏斗,后台Spark任务就跑半小时以上,集群CPU飙满,下游看板超时崩溃。
后来我主导做了存储格式迁移,把超过30天的冷数据全部转成Parquet列式格式,配合分区和谓词下推。实测:同样一个查询(统计过去90天、20个维度、5个度量的交叉表),CSV+Gzip耗时22分18秒,Parquet仅用了1分47秒,提速12.5倍。
还不止于此,列式存储的核心优势在于“只读需要的列”。CSV虽然压缩了,但引擎必须把整行解压才能知道哪些列有用,每行垃圾字段占70%以上;Parquet则按列独立编码和压缩,读哪列解压哪列,I/O量直接降到原来的1/10。
另外,Parquet保留每列的min/max/字典等统计信息,查询引擎能做更聪明的谓词下推,比如过滤日期区间,它直接跳过不包含该日期的数据块(Block Skipping),相当于免费多了一层索引。这个机制是行式格式(CSV、JSON、Avro)完全不具备的。
当然,如果你数据量很小(几百MB以内)、字段很少、且查询模式全是全表扫描,那列式优势不明显。但对回溯分析这种频繁扫描海量历史数据的场景,列式存储几乎是唯一合理选择。我的建议是:如果你还在用CSV/JSON做超过1TB的BI分析,立刻换Parquet或ORC,性能提升立竿见影。
网上搜了一圈,有人说Parquet生态广,有人说ORC压缩率更高、Hive优化好。我们公司数据平台主要用Spark做ETL,Presto做即席查询,Hive用得少。请问这种情况选哪个格式更合适?有没有实际迁移踩坑的经验?
这个问题我纠结过整整两周。我们当时在自建数据湖,Spark 2.4 + Presto 0.239,存储层是HDFS。我先分别做了压力测试:用TPC-DS 100GB数据集,分别转成Parquet和ORC(snappy压缩),跑10个典型聚合查询。
结果:Presto上Parquet平均快18%,Spark上两者基本持平,但Parquet在嵌套结构支持上更好(我们有很多JSON嵌套字段需要反嵌套分析)。所以最终选了Parquet。但这不是唯一答案。
如果你重度依赖Hive(比如Hive on Tez/Spark),ORC优势非常明显:Hive 3.x对ORC有专有优化,比如向量化执行、惰性解压、统计信息更丰富。我们后来接入了一个业务线,他们用Hive做日调度,ORC比Parquet快了近30%(单次ETL从40分钟降到28分钟)。
还有一个关键点:Schema变更。ORC和Parquet都支持Schema演化,但实现细节不同。ORC在Hive里可以灵活增删列,而Parquet在Spark里如果升级了元数据,旧文件需要对位读取,容易出兼容问题。我的建议是:如果你的查询引擎以Spark+Presto为主,优先Parquet;
如果Hive是核心,优先ORC;如果两者混用,建议统一用Parquet并做好版本管理。另外,压缩算法选Snappy还是Zstd?回溯分析场景下,Zstd压缩率比Snappy高25%~40%,但解压稍慢,CPU不是瓶颈的话推荐Zstd。
我们实测:Zstd压缩的Parquet文件比Snappy小35%,查询时间基本一致,存储成本显著下降。
老板让我控制数据平台成本,但技术人员都说换Parquet能省钱。我不太理解:格式不是免费的嘛,压缩后文件大小差不多,怎么会省钱?能具体算一笔账吗?比如我们公司每天新增500GB日志,存半年,用CSV vs Parquet,每年云上费用差多少?
这个问题我算过,算完我们CTO直接批了迁移预算。先给公式:云上数据平台成本 = 存储成本 + 计算成本 + 网络带宽成本。存储格式影响三者:1)存储成本:Parquet列式压缩率通常比CSV Gzip高2~3倍(因为同类数据编码更高效)。
我们的历史数据(含几十个字符串枚举列),CSV Gzip压缩比约3:1,Parquet Snappy约7:1,Parquet Zstd约10:1。以每天500GB原始数据(CSV),转化为Parquet Zstd后仅50GB。
半年(180天)存储量:CSV Gzip约 500GB/3 * 180 ≈ 30TB,Parquet Zstd约 500GB/10 * 180 ≈ 9TB。
按对象存储0.02元/GB/月(仅供参考),CSV: 30TB * 0.02 * 12 ≈ 7200元/年,Parquet: 9TB * 0.02 * 12 ≈ 2160元/年,存储直接省5000+。2)计算成本更重要。回溯分析作业跑Spark,按核时计费。
我们的典型查询:扫描30TB数据用CSV,需启动100个worker跑20分钟,约33个核时;换成9TB Parquet(列裁剪后实际扫描可能仅1TB),同样查询只需5分钟,不到10个核时。按0.5元/核时算,单次查询成本从16.5元降到5元。
每天跑20个类似回溯作业,一年(250个工作日)节省 (16.5-5)*20*250 = 57,500元。加上存储节省,一年轻松省6万+。数据量更大、查询更频繁,这个数字会指数级增长。3)网络带宽:如果数据跨区域复制,Parquet变小后传输费用也降低。
所以结论:存储格式不是免费的选择,它是成本结构的关键变量。很多团队只盯着EC2实例和存储单价,却忽略了“浪费的I/O和计算时间”才是最大的隐性成本。换格式相当于每年白捡一台MacBook Pro。
我们公司的历史数据增长太快了,全存Parquet明细太贵,删掉又怕业务需要回溯两年数据。有没有办法根据不同时间范围用不同的存储格式和数据组织形式?比如热数据保留原始粒度,冷数据只存聚合,或者换一种更便宜的格式?这样会影响查询速度吗?
这就是我上一家公司正在做的混合存储策略,效果非常棒。我们的方案:三个月内的热数据存在高性能的Parquet(Zstd压缩),按天分区,保留全字段明细。三个月到一年的温数据,转成按周聚合的ORC宽表(保留常用维度和度量,丢掉低频JSON字段)。
一年以上的冷数据,进一步聚合为月级别的Parquet,甚至直接用Gzip压缩的CSV转存到成本极低的冷存储(比如AWS S3 Glacier)。查询时,BI前端加一个“时间范围”筛选器,后端路由引擎自动判断:查最近三个月走热存;查一年内走温存(聚合表);
查更早则走冷存(可能需要全量扫描,但数据量已经很小)。实测一次跨两年回溯(“2022-2024品类销售趋势”),纯热存方案扫描200TB,耗时40分钟;混合方案只扫描热存12TB + 温存3TB + 冷存0.5TB,耗时12分钟。存储成本从每季度15万元降到5万元。
当然,混合存储有代价:需要额外维护ETL流程(不同格式、不同粒度的数据对齐),并且不支持所有维度的随意下钻(因为聚合表已丢失细节)。但是,绝大多数业务分析只看核心维度和趋势,70%的场景都可以满足。对于需要精确到单条明细的极少数需求,可以额外提供“拉取原始数据”的异步接口。
所以,我的建议是:不要幻想用一个格式打天下。数据应该根据访问频度和查询模式分层存储,每层选择最适合的格式和粒度。这样既能兼顾查询速度,又把成本控制在合理范围。
如果你对自动化路由感兴趣,可以研究Apache Iceberg或Delta Lake的Time Travel和分区演化,它们原生支持这种策略,比手动维护省心得多。


读者评论
作为数据工程师,太有共鸣了。我们之前一个物流项目,历史运单数据全存CSV,回溯查询动不动半小时。后来我按文章思路换成Parquet+Snappy压缩,同样5亿行数据,查询直接降到2分钟。文中那句‘列裁剪减少80%-95% I/O’一点不夸张,实测扫描量从800G掉到30G。这个案例应该作为新人必修课。
我是BI分析师,之前总被业务问‘为啥报表加载这么慢’,自己只能模糊说‘数据量大’。看了这篇文章才明白,根本原因是底层存的是CSV,每个聚合查询都在全表扫描。现在我去找数仓同事要求改格式,有了具体的性能数据做支撑,沟通顺畅多了。
团队一直纠结选Parquet还是ORC,文章给出了非常务实的决策框架:先看查询模式,再看引擎适配度。我们主要用Presto,按建议选了Parquet,测试下来确实比原来用Avro快了6倍。这个‘三级思考路径’很实用,比网上那些直接说‘XX格式最好’的帖子靠谱太多。
之前为了‘方便’,在一套小系统里用CSV存了两年数据,等数据涨到10TB想迁移时才发现成本巨大:转换耗时、兼容性问题、业务中断风险。文章说的‘留到后面再换是代价最高的方便’简直扎心。现在新项目第一天就定好列式存储,少走弯路就是省钱。