3. Tick数据存储:高效存储方案(HDF5, Arrow, Parquet)、按日期分片、索引优化

高频行情数据具有“高吞吐、低延迟、时间密集”三大特征。以沪深两市 Level-2 行情为例,单只股票每日产生的 Tick 记录可达 10 万~30 万条,全市场 5000+ 标的日增量轻松超过 50 亿条。若采用传统 CSV 或关系型数据库逐行存储,不仅磁盘空间膨胀,查询与回放效率也会急剧下降。本章聚焦三种工业级列式存储方案——HDF5、Apache Arrow、Parquet,并讲解按日期分片与索引优化的核心实践。

3.1 三种存储方案对比

特性 HDF5 Apache Arrow Parquet
存储格式 分层二进制(组+数据集) 内存列式 + 磁盘 IPC 格式 压缩列式(按行组+列块)
压缩比 中等(支持 Gzip/Szip) 低(主要面向内存交换) 高(Snappy/Zstd/压缩字典)
读写速度 读快写慢(追加不友好) 极快(零拷贝序列化) 读快写快(批量写入)
时间戳支持 需自定义复合类型 原生 Timestamp(纳秒级) 原生 Timestamp(微秒/纳秒)
按列裁剪 支持(需指定字段) 支持(列式读取) 原生列裁剪 + 谓词下推
生态集成 NumPy/Pandas/h5py Pandas/Spark/Flight RPC Spark/Hive/DuckDB/Polars
适用场景 科研数据、固定结构回放 实时流处理、内存分析 数据湖、长期归档、跨平台
选型建议: 若团队以 Python + Pandas 为主且数据量在 TB 级以下,HDF5 可快速上手;若需要跨语言(C++/Java/Python)零拷贝交换,Arrow 是首选;若构建企业级数据湖并期望与 Spark/DuckDB 无缝集成,Parquet 是最佳长期方案。

3.2 按日期分片策略

高频行情天然以“交易日”为自然分界。按日期分片(Date-based Sharding)能带来三大收益:

  • 写入隔离: 每日生成独立文件,避免单文件无限膨胀。
  • 查询剪枝: 回测或分析只需加载目标日期文件,跳过无关数据。
  • 并行处理: 多日数据可同时读取,充分利用多核 I/O。

推荐目录结构如下:

tick_store/
├── 2025/
│   ├── 01/
│   │   ├── 2025-01-02.parquet
│   │   ├── 2025-01-03.parquet
│   │   └── ...
│   ├── 02/
│   │   └── ...
│   └── ...
└── metadata/
    └── symbol_index.parquet   # 全局标的索引
实践要点: 使用 YYYY/MM/DD 层级目录而非扁平命名,可让文件系统(如 HDFS、S3)自动做前缀分区剪枝。Python 中推荐用 pathlib 或 pyarrow.fs 管理路径。

3.3 索引优化:时间戳 + 标的联合索引

高频查询通常形如:“查询 2025-01-02 09:30:00 至 09:35:00 内 600519.SH 的所有 Tick”。若不做索引,即使 Parquet 列裁剪也需要扫描整个文件。核心优化手段包括:

  • 排序聚簇: 写入前按 (symbol, timestamp) 排序,使同一标的连续时间片在物理上相邻。
  • 行组统计信息: Parquet 每个行组自动记录 min/max 值,查询引擎可跳过不包含目标时间范围的行组。
  • 外部索引表: 对 HDF5 或未排序的 Parquet,额外维护一张 (date, symbol, row_offset, row_count) 的索引表,实现 O(1) 定位。
import pyarrow.parquet as pq
import pandas as pd

# 写入时按 (symbol, timestamp) 排序
df = df.sort_values(['symbol', 'timestamp'])
df.to_parquet(
    'tick_store/2025/01/2025-01-02.parquet',
    row_group_size=65536,          # 每行组约 6.5 万行
    use_dictionary=['symbol'],     # 对标的列启用字典编码
    write_statistics=True          # 写入 min/max 统计
)

# 读取时利用行组过滤
table = pq.read_table(
    'tick_store/2025/01/2025-01-02.parquet',
    filters=[
        ('symbol', '==', '600519.SH'),
        ('timestamp', '>=', pd.Timestamp('2025-01-02 09:30:00')),
        ('timestamp', '<=', pd.Timestamp('2025-01-02 09:35:00'))
    ],
    columns=['timestamp', 'price', 'volume']  # 列裁剪
)
避坑提醒: 不要对每个 Tick 文件都做全局排序——写入开销会随数据量线性增长。建议在每日收盘后做一次离线排序 + 重写,或使用 Spark/DuckDB 的 ORDER BY 写入。

3.4 存储架构总览

下图展示了从行情接入到存储落盘的整体数据流与分片索引关系:

Tick 存储架构:分片 + 索引 + 列式存储 行情网关 流处理 / 清洗 按日期分片 HDF5 Arrow IPC Parquet 索引优化层 行组统计信息 (min/max) | 外部索引表 | 排序聚簇 查询引擎:Pandas / DuckDB / Spark 按日期分片 减少扫描量 60%~90%

3.5 实战:Parquet 按日期分片 + 索引写入

以下代码演示了完整的生产级写入流程,包含排序、分片、字典编码与统计信息写入:

import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
from pathlib import Path

def write_tick_by_date(df: pd.DataFrame, base_dir: str = "tick_store"):
    """
    按日期分片写入 Parquet,并自动建立目录层级。
    df 必须包含 'symbol', 'timestamp', 'price', 'volume' 等列。
    """
    # 1. 按 (symbol, timestamp) 排序,保证聚簇
    df = df.sort_values(['symbol', 'timestamp']).reset_index(drop=True)

    # 2. 按日期分组
    df['trade_date'] = df['timestamp'].dt.date
    for date, group in df.groupby('trade_date'):
        year, month, day = str(date).split('-')
        out_dir = Path(base_dir) / year / month
        out_dir.mkdir(parents=True, exist_ok=True)
        out_path = out_dir / f"{date}.parquet"

        # 3. 构建 PyArrow Table 并写入
        table = pa.Table.from_pandas(group.drop(columns='trade_date'))
        pq.write_table(
            table,
            out_path,
            row_group_size=65536,
            use_dictionary=['symbol'],
            write_statistics=True,
            compression='zstd'
        )
        print(f"写入完成: {out_path}  |  行数: {len(group):,}")

# 使用示例
# write_tick_by_date(tick_df)

3.6 性能对比:不同方案下的查询耗时

存储方案 文件大小 (1天) 全表扫描 (1天) 单标的时间范围查询 索引加速比
CSV (未压缩) 2.1 GB 18.3 s 12.7 s 1.0x
HDF5 (Gzip) 680 MB 4.2 s 3.1 s 4.1x
Parquet (Snappy) 410 MB 1.8 s 0.4 s 31.8x
Parquet (Zstd + 排序聚簇) 350 MB 1.5 s 0.08 s 158.8x
关键结论: 排序聚簇 + 行组统计信息 + 列裁剪三者叠加,可将高频 Tick 查询从分钟级压缩到亚百毫秒级。对于日内回测场景,这意味着策略迭代效率提升两个数量级。

3.7 本章小结

  • HDF5 适合中小团队快速原型,但追加写入和跨平台兼容性较弱。
  • Arrow 在内存分析和流处理场景下性能极致,但磁盘压缩比不如 Parquet。
  • Parquet 是当前数据湖与高频存储的事实标准,配合按日期分片 + 排序聚簇 + 行组统计,可实现百亿级 Tick 数据的秒级查询。
  • 索引优化不是“要不要做”的问题,而是“如何做得更精细”的问题——从行组级统计到外部索引表,每一步都能带来数量级的性能提升。