用 Parquet 存分钟线:分区、压缩与读取速度实测
按日期分区还是按标的分区,zstd 还是 snappy,一份 3 亿行分钟线上的实测对比。
先想清楚查询模式
分区方式没有普适答案,只有和查询模式匹配不匹配。研究场景通常是「取全市场某一段日期的数据」,那就按日期分区;单标的回测和实盘信号计算是「取某只票的全部历史」,那就按标的分区。选错了,读取速度会差一个数量级。
import pyarrow as pa import pyarrow.parquet as pq tbl = pa.Table.from_pandas(df, preserve_index=False) # 研究场景:按日期分区,配合谓词下推读取任意区间 pq.write_to_dataset( tbl, root_path='bars/minute', partition_cols=['date'], compression='zstd', compression_level=3, row_group_size=200_000, )
3 亿行上的实测
同一份 2019 到 2025 的全市场分钟线,约 3.1 亿行 8 个字段,在同一台机器(NVMe SSD、16 核)上的对比:
38 GB
无压缩
11 GB
snappy
7.4 GB
zstd-3
6.1 GB
zstd-9
读取一整年全市场数据:snappy 用 42 秒,zstd-3 用 39 秒,zstd-9 用 51 秒。zstd-3 在体积和速度上都优于 snappy,是这份数据上的最优选择;zstd-9 多花的解压时间已经抵消了 IO 的节省。
row_group_size 别用默认值
默认的 row group 往往过大,谓词下推时会读进大量无关行。分钟线按 10 万到 30 万行一组比较合适,既能有效跳过又不会让元数据膨胀。
读取时才是关键
写的时候做对了分区,读的时候必须把过滤条件传下去,否则前面的功夫全白费。用 filters 参数让 Arrow 在文件层面跳过整个分区,而不是先全读进内存再筛。
df = pq.read_table(
'bars/minute',
columns=['code', 'ts', 'close', 'volume'],
filters=[('date', '>=', '2024-01-01'), ('date', '<', '2024-04-01')],
).to_pandas()
# 只取需要的列同样重要:8 列全读比 4 列慢将近一倍
下一篇讲怎么在这套存储上加一层增量更新,让每日盘后的数据落库控制在一分钟以内。
Parquet数据存储性能