4. 回测数据表设计原则:表结构设计基础

量化回测系统对数据的要求极高:既要存储海量的历史行情快照,又要支持毫秒级的策略查询与聚合分析。表结构设计的好坏,直接决定了回测引擎的计算效率与数据维护成本。本节从范式化与反范式化的权衡、时间序列数据表的设计要点、分区表与索引策略三个维度展开。

4.1 范式化与反范式化的权衡

在回测数据库中,范式化(Normalization)旨在消除数据冗余,保证一致性;反范式化(Denormalization)则通过冗余字段减少 JOIN 操作,提升查询性能。两者并非对立,而是根据业务场景进行混合设计。

维度 范式化设计 反范式化设计
存储空间 低,无冗余 高,存在重复字段
写入性能 快,单表写入 较慢,需更新多表或冗余字段
查询性能 慢,需多表 JOIN 快,单表即可返回
数据一致性 高,更新一处即可 低,需维护冗余字段同步
典型场景 证券基础信息、因子定义 K 线快照、逐笔成交明细
设计建议:在回测库中,将“静态数据”(如股票代码、行业分类)严格范式化;将“动态高频数据”(如 tick 级行情、分钟 K 线)适度反范式化,将时间戳、证券代码、开盘价、收盘价等字段直接存储在同一张宽表中,避免回测时频繁 JOIN。

4.2 时间序列数据表设计要点

回测数据本质上是时间序列(Time Series),其表结构设计需遵循以下核心原则:

  • 时间戳为主键第一列:将 trade_date 或 event_time 作为复合主键的第一列,便于按时间范围扫描。
  • 证券代码作为分区键:将 symbol 或 instrument_id 作为第二主键列,支持单标的快速检索。
  • 避免可变长度字段:时间序列数据量极大,应使用固定长度数值类型(如 DECIMAL(20,8) 或 FLOAT),避免 VARCHAR 带来的存储与索引开销。
  • 包含数据质量标记:增加 quality_flag 字段(如 TINYINT),标记数据是否经过清洗、复权或异常剔除。
-- 推荐的时间序列 K 线表结构(反范式化宽表)
CREATE TABLE daily_kline (
    trade_date      DATE        NOT NULL,
    symbol          VARCHAR(10) NOT NULL,
    open_price      DECIMAL(20,8),
    high_price      DECIMAL(20,8),
    low_price       DECIMAL(20,8),
    close_price     DECIMAL(20,8),
    volume          BIGINT,
    amount          DECIMAL(20,2),
    adjust_factor   DECIMAL(10,6),
    quality_flag    TINYINT     DEFAULT 1,
    PRIMARY KEY (trade_date, symbol)
) ENGINE=InnoDB
PARTITION BY RANGE (YEAR(trade_date)) (
    PARTITION p2019 VALUES LESS THAN (2020),
    PARTITION p2020 VALUES LESS THAN (2021),
    PARTITION p2021 VALUES LESS THAN (2022),
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);
避坑提示:不要将时间戳存储为 VARCHAR 或 BIGINT 毫秒数。使用 DATE 或 DATETIME(3) 类型,既能利用数据库内置的时间函数,又能让分区裁剪和索引下推更高效。

4.3 分区表与索引策略

回测数据表动辄数亿行,合理的分区与索引策略是查询性能的基石。下图展示了分区表在回测查询中的工作流程:

分区表查询流程示意图 分区表查询流程:分区裁剪与索引下推 SQL 查询 分区裁剪(Partition Pruning) 分区列表:p2019 | p2020 | p2021 | p2022 | p2023 | p_future 仅扫描 p2022、p2023(WHERE trade_date BETWEEN '2022-01-01' AND '2023-12-31') 索引下推(Index Condition Pushdown) 返回结果集 分区裁剪减少扫描数据量,索引下推在存储引擎层过滤无效行,两者结合可提升 10~100 倍查询性能

4.3.1 分区策略选择

分区类型 适用场景 示例
RANGE 分区 按时间范围(年、月) PARTITION BY RANGE (YEAR(trade_date))
LIST 分区 按证券代码或交易所 PARTITION BY LIST (exchange_id)
HASH 分区 数据均匀分布,无自然范围 PARTITION BY HASH (symbol_id) PARTITIONS 16
KEY 分区 类似 HASH,但由 MySQL 内部哈希 PARTITION BY KEY (symbol) PARTITIONS 8
最佳实践:对于日频 K 线数据,推荐使用 RANGE 分区 按年划分,配合 复合主键 (trade_date, symbol)。对于 tick 级数据,建议按月分区,并额外建立 symbol + trade_date 的二级索引。

4.3.2 索引设计要点

索引不是越多越好,回测场景下应遵循以下原则:

  • 主键索引即聚簇索引:将最频繁的查询条件(时间 + 标的)作为复合主键,避免额外回表。
  • 覆盖索引优先:如果查询只需要 close_price 和 volume,可以建立 (trade_date, symbol, close_price, volume) 的复合索引,实现索引覆盖。
  • 避免冗余索引:MySQL 会自动为 PRIMARY KEY 建立索引,不要重复建立以 trade_date 开头的单列索引。
  • 监控索引使用率:定期使用 SHOW INDEX FROM table_name 和 EXPLAIN 分析查询计划,删除长期未使用的索引。
-- 为 tick 数据表设计索引(覆盖索引示例)
ALTER TABLE tick_data ADD INDEX idx_symbol_date_cover
    (symbol, trade_date, last_price, volume, bid_price, ask_price);

-- 查询时可直接从索引返回,无需回表
EXPLAIN SELECT last_price, volume
FROM tick_data
WHERE symbol = '000300.SH'
  AND trade_date BETWEEN '2023-06-01' AND '2023-06-30';
-- Extra 列显示:Using index

4.4 范式化与反范式化的混合架构

在实际回测平台中,通常采用分层混合架构:

回测数据分层混合架构 回测数据分层混合架构 基础信息层 范式化设计 证券代码、行业分类、因子定义 清洗层 轻度反范式化 复权因子、异常标记、数据质量 分析层 反范式化宽表 K 线快照、tick 数据、因子值 回测引擎直接读取分析层宽表,避免 JOIN;数据清洗与基础信息通过 ETL 定期从清洗层和基础信息层更新 ETL 清洗 宽表构建

这种分层架构的优势在于:

  • 基础信息层严格遵循第三范式,保证证券代码、行业分类等静态数据的一致性。
  • 清洗层引入少量冗余(如复权因子、数据质量标记),降低 ETL 复杂度。
  • 分析层采用反范式化宽表,将回测所需的所有字段(开盘价、收盘价、成交量、复权因子等)存储在同一行,回测引擎只需一次顺序扫描即可完成策略计算。
性能红线:分析层宽表不要超过 30~40 个字段。过多的列会导致行存储膨胀,降低缓冲池命中率。如果因子数量超过 50 个,建议将因子列拆分到独立的“因子宽表”中,通过 trade_date + symbol 进行关联。