4、列式存储与行式存储:Parquet与CSV在回放场景下的性能差异
做高频回放的朋友,十有八九都被I/O卡过脖子。
我记得刚入行那会儿,团队还在用CSV存Tick数据。每天收盘后跑一次回放,得等半小时。后来换了Parquet,同样的数据,同样的机器,跑进两分钟。你想想看,这差距有多大?
今天我们就来聊聊,为什么列式存储(Parquet)和行式存储(CSV)在回放场景下,性能能差出一个数量级。
先搞清楚:行式 vs 列式,到底存的姿势不一样
CSV是典型的行式存储。每一行是一条完整的记录,字段挨个排好。比如一条Tick数据:
timestamp,symbol,price,volume
09:30:01.000,IF2401,3850.0,10
09:30:01.050,IF2401,3850.2,5
09:30:01.100,IF2401,3850.4,8
Parquet是列式存储。它把同一列的数据放在一起。说白了,所有timestamp放一块,所有price放一块,所有volume放一块。
这两种姿势,决定了它们在回放场景下的命运完全不同。
回放场景的核心矛盾:你只需要一部分字段
做回放的时候,你真的需要所有字段吗?
我个人习惯,做策略回测时,通常只取price和volume。偶尔看看timestamp。其他字段?基本不碰。
CSV的问题就在这里。它不管你用不用,每次读取都得把整行数据全拉进来。你只想要price,它却把symbol、exchange、condition等一堆字段都塞给你。I/O带宽就这么被浪费了。
Parquet呢?它只读取你指定的列。你只要price和volume,它就只读这两列的数据块。其他列?碰都不碰。
我在项目中遇到过这样一个案例:一个Tick数据文件,CSV格式,1.2GB。回放时只取price和volume两列。CSV读取耗时47秒,Parquet只用了3.8秒。差了12倍。
为什么会这样?
因为CSV读了1.2GB的数据,而Parquet只读了大约200MB。剩下的1GB,全是你不用的字段。
压缩效率:列式存储的天然优势
这一点我特别想强调。很多人以为Parquet快只是因为列式读取,其实压缩也占了很大功劳。
同一列的数据,类型相同,数值范围接近。比如price这一列,全是浮点数,而且大部分在3850到3860之间。这种数据,压缩算法特别喜欢。
CSV呢?一行里混着字符串、整数、浮点数、时间戳。压缩算法面对这种混合数据,效率大打折扣。
我做过一个对比测试:
| 存储格式 | 原始大小 | 压缩后大小 | 压缩比 |
|---|---|---|---|
| CSV (gzip) | 1.2 GB | 380 MB | 3.2x |
| Parquet (snappy) | 1.2 GB | 180 MB | 6.7x |
| Parquet (zstd) | 1.2 GB | 120 MB | 10x |
你看,同样的原始数据,Parquet的压缩比是CSV的两到三倍。这意味着什么?意味着磁盘读取量更少,网络传输更快,内存占用更低。
核心结论:在高频回放场景下,Parquet相比CSV,I/O量减少80%-90%,回放速度提升5-15倍。
编码方式:Parquet的隐藏武器
除了压缩,Parquet还用了很多聪明的编码技巧。
比如行程编码(RLE)。Tick数据里,很多字段是重复的。比如symbol字段,连续几万条都是"IF2401"。Parquet会把它编码成"IF2401重复30000次",而不是存30000次字符串。
再比如字典编码。对于枚举值有限的字段,Parquet会建一个字典,然后用整数索引代替原始值。这在存储exchange、condition这类字段时特别有效。
CSV呢?老老实实存字符串。一个字都不少。
小技巧:如果你必须用CSV,可以试试把重复度高的字段单独抽出来,用字典映射。但说实话,不如直接上Parquet省心。
回放场景的另一个痛点:时间范围过滤
做回放时,我们经常只取某一段时间的数据。比如"只回放上午9:30到10:00的Tick"。
CSV的做法:从头读到尾,逐行判断时间戳是否在范围内。嗯,你想想看,如果数据文件有500万行,你得全部读完才能拿到那50万行。
Parquet的做法:利用统计信息(min/max),直接跳过不符合条件的数据块。如果某个数据块的时间戳范围是10:00到10:30,而你要的是9:30到10:00,这个块直接跳过,连读都不读。
我曾经优化过一个回放系统,加了Parquet的谓词下推(predicate pushdown)之后,时间过滤的效率提升了20倍。这不是夸张,是真的。
代码对比:同样的逻辑,不同的命运
我们来看一段实际的回放代码。假设我们要读取Tick数据,计算每分钟的均价。
CSV版本:
import pandas as pd
# 读全部数据,再过滤
df = pd.read_csv('tick_data.csv')
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df[(df['timestamp'] >= '2024-01-15 09:30:00') &
(df['timestamp'] <= '2024-01-15 10:00:00')]
# 计算均价
result = df.groupby(df['timestamp'].dt.minute)['price'].mean()
Parquet版本:
import pandas as pd
# 只读需要的列,带时间过滤
df = pd.read_parquet('tick_data.parquet',
columns=['timestamp', 'price'],
filters=[('timestamp', '>=', '2024-01-15 09:30:00'),
('timestamp', '<=', '2024-01-15 10:00:00')])
# 计算均价
result = df.groupby(df['timestamp'].dt.minute)['price'].mean()
代码量差不多,但背后的执行逻辑天差地别。
CSV版本:读取1.2GB数据 → 解析全部字段 → 过滤 → 计算。内存峰值1.5GB+。
Parquet版本:读取120MB数据(只读两列+过滤)→ 解析 → 计算。内存峰值不到200MB。
避坑指南:我曾经在项目中直接用pd.read_csv读全量Tick数据,结果内存爆了。后来改成chunksize分批读,但速度慢得让人崩溃。换了Parquet之后,这些问题都消失了。如果你还在用CSV做高频回放,我建议你尽快迁移。
什么时候CSV反而更好?
说了这么多Parquet的好处,但CSV也不是一无是处。
如果你需要频繁修改数据,或者经常用文本编辑器查看,CSV更方便。Parquet是二进制格式,改起来麻烦,查看也不直观。
另外,如果数据量很小(比如几千行),两种格式的差异可以忽略不计。没必要为了这点数据折腾格式转换。
但在高频交易的回放场景下,数据量动辄几GB甚至几十GB,Parquet的优势是碾压级的。
一张图看懂核心逻辑
下面这张图展示了行式存储和列式存储在回放场景下的核心差异:
这张图很直观。左边是行式存储,每次读取都得把整行数据全拉进来。右边是列式存储,只读取你需要的列。在高频回放场景下,这个差异就是性能的分水岭。
总结一下
Parquet在回放场景下的优势,说白了就三点:
- 列式读取:只读你需要的字段,I/O量大幅减少
- 高效压缩:同类数据压缩比高,磁盘占用和传输时间都降低
- 智能过滤:利用统计信息和谓词下推,跳过不必要的数据块
CSV的优势在于简单、可读、易修改。但如果你做的是高频回放,数据量大、字段多、需要频繁过滤,Parquet几乎是唯一正确的选择。
我个人建议:把CSV作为数据交换格式,把Parquet作为回放存储格式。两者结合,既保证了灵活性,又获得了性能。
一句话总结:高频回放场景下,Parquet比CSV快5-15倍,内存占用减少80%以上。如果你还在用CSV做回放,是时候考虑迁移了。
公众号:蓝海数据掘金营,微信deep3321