3. 基于时间分片的归档目录规划

逐笔行情数据的特点是“量大、连续、按时间自然增长”。如果所有归档文件都堆在一个目录里,或者只按品种简单分文件夹,随着时间推移,单目录文件数会迅速膨胀,检索时既难以定位,也容易拖慢文件系统遍历。基于时间分片的归档目录规划,核心思路是:把时间作为第一维度的组织轴,用目录层级表达时间粒度,用文件命名表达分片边界,用元数据文件表达可检索属性。这样归档结构既能随交易日自然扩展,又能让“找某天某小时某品种的数据”变成一次确定性的路径拼接,而不是全盘扫描。

目录层级:以交易日为主轴,小时为可选细分

推荐的基础层级为“根目录 / 数据域 / 年 / 交易日 / 小时(可选)”。其中“数据域”用于区分不同来源或不同处理阶段,例如原始逐笔、清洗后逐笔、特征快照等;年目录用于控制单层子目录数量;交易日目录是归档的最小完整单元,因为逐笔行情天然以交易日为边界,跨日切分会产生大量边界处理逻辑。小时目录只在单日数据量特别大、或需要按小时增量落盘时启用,属于可选的二级分片。

一个可直接采用的目录结构示例如下(示例参数,非实测):

archive/
  tick_raw/                 # 数据域:原始逐笔
    2024/
      20240115/             # 交易日 YYYYMMDD
        09/                 # 小时 HH(可选)
          tick_raw_20240115_09_IF2401.csv.gz
          tick_raw_20240115_09_IF2401.meta.json
        10/
          tick_raw_20240115_10_IF2401.csv.gz
          tick_raw_20240115_10_IF2401.meta.json
      20240116/
        ...
  tick_clean/               # 数据域:清洗后逐笔
    2024/
      20240115/
        ...

这里有一个关键取舍:品种是否进入目录层级。如果品种数量少且固定,可以把品种放进文件名而不建目录,避免目录过深;如果品种多、且经常按品种批量检索,可以在交易日下再加一层品种目录。判断依据是“检索主路径”——若日常查询以“某天全市场”为主,品种放文件名;若以“某品种某段时间”为主,品种建目录。不要同时把品种和时间都做成多层目录,否则路径过深、维护成本上升。

文件命名规范:可解析、可排序、可去重

文件名应满足三个要求:机器可解析、字典序即时间序、包含分片边界信息。推荐格式为:

{数据域}_{交易日}_{小时}_{品种}_{分片序号}.{扩展名}

例如 tick_raw_20240115_09_IF2401_0001.csv.gz。其中交易日用 YYYYMMDD 保证字典序与时间序一致;小时用两位 HH;分片序号用四位补零,用于同一小时内因文件大小上限而切出的多个分片。扩展名保留 .csv.gz 这类“内容格式 + 压缩格式”的双后缀,便于下游按压缩方式选择读取器。命名中不要出现空格、中文和大小写混用,避免跨平台脚本解析差异。

命名规范一旦确定,应写入归档写入程序的常量配置,并由校验脚本在落盘前检查文件名是否匹配正则,例如 ^tick_raw_\d{8}_\d{2}_[A-Z0-9]+_\d{4}\.csv\.gz$,防止人工补文件时破坏规范。

元数据描述文件:让目录“自解释”

目录和文件名只能表达时间与品种,无法表达行数、时间范围、字段结构、校验值等检索必需信息。因此每个分片应配一个同名 .meta.json 元数据文件,与数据文件放在同一目录。建议字段包括:数据域、交易日、小时、品种、分片序号、首笔时间、末笔时间、记录行数、字段列表、压缩方式、文件字节数、校验值、生成程序版本。示例:

{
  "domain": "tick_raw",
  "trade_date": "20240115",
  "hour": "09",
  "symbol": "IF2401",
  "shard_seq": 1,
  "first_ts": "2024-01-15T09:00:00.120",
  "last_ts": "2024-01-15T09:59:59.870",
  "row_count": 182340,
  "columns": ["ts","symbol","last","volume","bid1","ask1"],
  "compression": "gzip",
  "bytes": 5242880,
  "checksum": "sha256:...",
  "writer_version": "archiver-1.2.0"
}

元数据的作用不只是“记录”,更是快速定位的索引来源。检索时先读元数据判断时间范围与行数,命中后再解压读取数据文件,可以避免大量无效解压。对于跨分片查询,可以先扫描目标交易日下所有 .meta.json,按 first_ts 与 last_ts 过滤出候选分片,再按 shard_seq 顺序读取,保证结果时间有序。

可扩展组织与快速定位的落地要点

第一,分片边界要稳定。小时分片按自然小时切分,跨小时的那一笔归属以“成交时间戳所在小时”为准,并在元数据中记录首末时间,避免边界重复或遗漏。第二,单文件大小设上限。例如单分片压缩后不超过 64MB(示例阈值),超过则在同一小时内递增 shard_seq,既控制单文件读取内存,又保持目录不膨胀。第三,写入采用“临时文件 + 原子重命名”,先写 .tmp 再改名为正式文件,防止归档过程中被检索程序读到半截文件。第四,预留扩展位:数据域目录、年目录、小时目录都是可增删的层级,新增数据域不影响既有路径;若未来需要按“半小时”或“自定义窗口”分片,只需在小时层下增加窗口标识,文件名同步增加窗口字段即可,不必重构整棵树。

时间分片归档目录:层级、命名与元数据定位 目录层级(时间为主轴) archive/ tick_raw/ tick_clean/ 2024/ 20240115/ 09/ tick_raw_20240115_09_IF2401_0001.csv.gz tick_raw_20240115_09_IF2401_0001.meta.json 10/ tick_raw_20240115_10_IF2401_0001.csv.gz 20240116/ 09/ … 文件命名拆解 tick_raw_20240115_09_IF2401_0001.csv.gz 数据域 交易日 小时 品种 分片序号 字典序 = 时间序;序号补零保证同小时分片有序。 双后缀 .csv.gz 同时表达内容格式与压缩方式。 检索定位:先元数据、后数据 目标:日期+品种 如 20240115 / IF2401 拼接目录路径 确定性定位 扫描 .meta.json 按首末时间过滤候选分片 first_ts / last_ts 按 shard_seq 顺序读取 保证结果时间有序 命中后再解压数据文件,避免无效解压与全盘扫描。 写入采用 .tmp + 原子重命名,防止读到半截文件。

最后需要强调,目录规划不是一次性设计,而是要与写入程序、校验脚本、检索接口三者对齐。写入端按规范生成路径与元数据,校验端定期检查“数据文件与元数据是否成对、命名是否合规、首末时间是否连续”,检索端只依赖目录约定与元数据字段,不依赖具体文件内容格式。做到这三点,归档结构就能在数据量持续增长时保持可扩展,并把“找数据”从模糊搜索变成可预期的路径定位。