很多团队在搭建数据仓库时,都会纠结同一个问题:底层数据文件到底该用行式存储还是列式存储?这个问题在技术社区里的讨论热度一直很高,但大部分资料只会给出一句“OLTP用行存,OLAP用列存”。这句话本身没有错,可真到项目里,你会发现事情远没有这么简单。

存储格式的选择不仅影响查询速度,还会影响存储成本、写入链路、数据更新方式,甚至决定后续能不能平滑迁移。今天这篇文章就围绕数据仓库的行式存储与列式存储来聊一聊,它们各自的运行机制是什么,在什么场景下更适合哪一种,以及实际选型时应该怎么判断。
先搞清楚两种存储格式到底差在哪
行式存储的逻辑理解起来很直接。一张用户表,每条记录的所有字段在磁盘上按顺序连续摆放,读取一条数据时,一次IO就能拿到这一行的全部信息。而列式存储恰好相反,它把表按列拆开,同一列的数据放在连续的区域里,不同列各自独立存储。
# 用户表 user(id, name, age)
# 行式存储的磁盘布局
1, 张三, 28
2, 李四, 33
3, 王五, 25
# 列式存储的磁盘布局
id: 1, 2, 3
name: 张三, 李四, 王五
age: 28, 33, 25
这段示意虽然简单,但足够说明两者在IO层面的差异。如果查询只需要统计用户的年龄分布,列式存储可以只读取age这一列的数据,行式存储则必须把每条记录整体读出来,才能从中提取age字段。这个差异在大规模扫描时会被无限放大。
为什么数据仓库大多数场景偏向列存
数据仓库的分析查询往往有这几个特征:一次读取大量行、只关心少数列、对数据进行聚合计算。列式存储的结构天然契合这些特征。
这带来的好处是环环相扣的。第一层是IO量的下降,只扫描必要列,意味着从磁盘读入的数据量远远小于整行读取。第二层是压缩率,同一列的数据类型一致,压缩算法能发挥更大作用。比如一个性别列只有两个枚举值,字典编码之后可以压缩到很小。第三层是扫描效率,列存块上往往还会维护min/max索引,在扫描前就能跳过无效数据块,这种能力在行式存储里需要额外建索引才能实现,而且成本更高。
还有一个容易被忽略的点:列式存储更容易配合向量化执行。因为同一列的数据在内存中也紧凑排列,CPU可以一条指令处理多个数据,这对聚合类查询的加速效果非常明显。
行式存储的价值没有被取代
但如果说列存全面优于行存,那就太绝对了。在线事务处理场景中,行式存储依然是更合理的选择。比如最常见的订单表、账户表,业务请求通常按主键查出完整记录,然后进行更新。行存一次IO就能获取一行所有字段,而且更新时只需要修改对应行所在的数据页,开销可控。列存则完全不同,一次点查往往要串联多个列文件,更新某一行还要重写所有相关列块,代价非常高。
此外,成熟的数据库事务机制,比如MVCC、索引结构与行级锁,都和行式存储的结合非常紧密。将这些能力迁移到列存上,不是一种存储格式就能解决的问题。
列存不是万能药,这些场景容易踩坑
很多团队以为列式存储是数据分析的银弹,结果上线后发现一些低频查询反而比原来慢了。最典型的例子是点查需求。假设你在一张千亿行的行为日志表上做列存,日常聚合查询确实很快,但业务方突然提出要按用户ID查最近一条日志,要求秒级返回。这时候列存就会变得吃力,因为需要跨越多个列文件,分别读取用户ID、日志时间、日志内容等列的数据,再对它们做关联和排序。
这个例子在很多互联网公司都发生过。日志表换成Parquet之后,聚合任务性能提升明显,但为了支撑“用户详情”实时查询,只能在旁边保留一份行式存储的副本。后来实在觉得维护两套数据成本太高,索性把这类点查需求交给了专用的KV系统。
另一个典型问题来自湖仓架构中的更新场景。Parquet本身是只读文件格式,不支持单行更新,但业务上经常需要修正数据,比如更正一条错误订单。很多团队使用Hudi或Iceberg这类表格式来解决更新问题,但底层Copy-on-Write策略实际上会产生大量重写开销。小批量更新可能需要重写MB甚至GB级别的Parquet文件,如果更新频繁,这个代价会直接影响数据摄入管线的实时性。
三个常见误区:格式、引擎和分层
误区一:列存一定比行存快
列存快是有条件的。它适合大范围扫描,但在高频点查和小数据量查询上不一定有优势。点查的延迟往往取决于列文件数量和元数据开销,而不是数据量。所以在一个以点查为主的系统里强制使用列存,可能会得到相反的结果。
误区二:存储格式与查询引擎能力混淆
同一份ORC或Parquet文件,不同的查询引擎表现差异很大。有的引擎没有充分发挥列存能力,依然按行解码,性能提升有限;有的引擎做了min/max索引下推和向量化执行,才能真正体现列存的威力。如果只把数据“改成列存”而查询引擎跟不上,很难看到理想效果。
误区三:期望全仓库用同一种存储格式
一个完整的数据仓库通常有明细层、汇总层、维度表、应用层,不同层的数据访问模式不同。明细层以扫描聚合为主,适合列存;维表经常按主键查询,行式存储往往更顺手。很多团队在设计之初就要求所有表统一格式,这其实是不必要的限制。
数据仓库里是不是只能二选一?
答案是没必要二选一。现在的技术栈足够灵活,完全可以在不同的数据分层中采用不同的存储格式,甚至在同一套数仓系统中混合使用。
比如在Hive中,明细表可以创建为ORC或Parquet列式存储,维表仍然使用行式文本或轻量的行式格式。在ClickHouse中,MergeTree引擎是列式存储,但如果某个表需要高频点查,可以通过物化视图或外部字典来模拟行存能力。更成熟的方案是引入湖仓架构,让Parquet作为列存底座,同时利用Hudi/Iceberg提供行级更新能力,兼顾两点。
-- 明细层事件表:选择ORC列式存储,适合分析扫描
CREATE TABLE dwd_event_log (
user_id STRING,
event_type STRING,
event_time TIMESTAMP,
page_url STRING
) STORED AS ORC;
-- 维度表用户表:选择行存储,适合按主键快速查询
CREATE TABLE dim_user (
user_id STRING PRIMARY KEY,
user_name STRING,
reg_time TIMESTAMP
) STORED AS TEXT;
这个建表示例实际没那么简单,需要根据数据量和查询模式验证。但它说明了一个思路:按访问模式决定存储格式,不必全线统一。
行式存储与列式存储的对比
这里汇总一下两种格式的关键差异,方便在方案评审时快速对照。
| 对比维度 | 行式存储 | 列式存储 |
|---|---|---|
| 数据布局 | 一行记录连续存放 | 一列数据连续存放 |
| 典型代表 | MySQL InnoDB、PostgreSQL、Hive TEXT/SequenceFile | Parquet、ORC、ClickHouse MergeTree |
| 查询优势 | 主键点查、按行更新/删除 | 大规模扫描、聚合计算、只读部分列 |
| 写入模式 | 事务性小批量写入友好 | 批量导入友好,小批量更新代价高 |
| 压缩效率 | 同页数据字段类型混杂,压缩受限 | 同列数据同构,压缩率高 |
| 适用位置 | 维表、ODS中需要频繁变更的数据、在线业务库 | 明细层、汇总层、日志和事件分析等OLAP场景 |
表格里的“典型代表”并不是绝对划分,像ClickHouse也支持ReplacingMergeTree这种近似更新的能力,但总体判断可以拿这些维度作为起点。
落地时该怎么判断?先回答四个问题
与其套用“什么引擎用什么格式”的固定模板,不如回到自己的数据和工作负载。以下几个问题可以帮助你做判断:
- 查询是大范围扫描还是主键点查?如果分析报表中90%以上的查询都需要扫描大分区,列存是更稳妥的选择;如果核心场景是频繁的按ID查详情,行存会更顺手。
- 数据以追加写入为主,还是经常更新?日志、埋点、事件流天然适合列存;订单、账户等状态频繁变化的数据,至少要在在线链路保留行存。
- 能不能接受写放大和重写成本?列存的小批量写入和更新往往需要重写数据块,如果数据链路对实时性要求高,这个代价需要提前评估。
- 数仓分层是否有不同的访问模式?与其统一格式,不如让明细层和汇总层用列存,让维表和热点小表用行存,必要时通过索引或缓存补足。
如果现有系统已经在行式存储上跑了好几年,也不用急着推翻重来。比较稳妥的做法是先在离线分析链路中引入列存副本,用真实查询和真实数据量进行比较,重点观察IO量、压缩率、查询延迟和资源消耗的变化。确认收益后再评估迁移成本,不要因为别人说“列存好”就盲目切换。
写在最后:别让“之争”干扰工程判断
行式存储与列式存储并不是完全对立的关系。它们各自解决的问题不同,适用的场景也不同。数据仓库作为一个以分析为核心的系统,列式存储确实是主流选择,但这不意味着行式存储就失去了价值。真正重要的不是跟风选择某个时髦的存储格式,而是理清自己的查询模式、写入链路和数据生命周期。
技术选型是一个权衡过程,不存在一个放之四海而皆准的答案。多花一点时间去梳理业务需求,大多数团队都能找到适合的组合方案。
原创文章,作者:fudengji,如若转载,请注明出处:https://fudengji.cn/article/882/