← 返回文章列表 数据工程

用 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数据存储性能
下一篇

波动率目标法的三个陷阱,以及怎么把它调稳