3. 数据模型设计(上):星型模型与雪花模型选择、事实表设计、维度表设计

做量化回测,数据模型是地基。地基没打好,后面策略再牛也跑不稳。今天咱们聊聊数据模型设计的上半部分——星型模型和雪花模型怎么选,事实表和维度表怎么设计。

3.1 星型模型 vs 雪花模型:我选哪个?

先说说这两种模型。星型模型,说白了就是一张大事实表,周围围着几张维度表。雪花模型呢,维度表还能再分,像雪花一样展开。

我个人习惯,90% 的场景用星型模型。为什么?

  • 查询快:星型模型表少,JOIN 也少。你想想看,回测时一秒可能要查几万条数据,多一个 JOIN 就多几毫秒,累积起来很可观。
  • 好理解:新同事来了,看星型模型一眼就懂。雪花模型层级多,容易绕晕。
  • 维护简单:维度表就那几张,改起来方便。

那什么时候用雪花模型?我在项目中遇到过一种情况——行业分类维度。行业分类经常变,今天申万一级,明天中信二级。如果用星型模型,每次改行业分类都得动事实表。雪花模型把行业分类拆成独立的表,改起来就轻松多了。

核心原则:查询性能优先用星型,维度变化频繁用雪花。

嗯,这里要注意:别为了“规范”而用雪花模型。量化回测的核心是快,不是数据库范式。我曾经见过一个团队,把时间维度拆成年、月、日三张表,结果查询时 JOIN 了五张表,回测速度慢得让人崩溃。

3.2 事实表设计:日线、分钟线、Tick 级

事实表是数据仓库的核心。量化回测里,事实表就是行情数据。咱们按粒度分三种:日线、分钟线、Tick 级。

3.2.1 日线事实表

日线最简单,也最常用。我建议这样设计:

CREATE TABLE fact_daily (
    trade_date    DATE NOT NULL,      -- 交易日期
    stock_code    VARCHAR(10) NOT NULL, -- 股票代码
    open_price    DECIMAL(10,2),       -- 开盘价
    high_price    DECIMAL(10,2),       -- 最高价
    low_price     DECIMAL(10,2),       -- 最低价
    close_price   DECIMAL(10,2),       -- 收盘价
    volume        BIGINT,              -- 成交量
    amount        DECIMAL(16,2),       -- 成交额
    adj_factor    DECIMAL(10,6),       -- 复权因子
    PRIMARY KEY (trade_date, stock_code)
);

注意看,主键是 (trade_date, stock_code)。为什么?因为同一只股票在同一天只有一条日线数据。这个组合唯一确定一条记录。

小技巧:复权因子一定要存。我见过有人只存复权价,不复权价丢了。结果回测时想算真实收益率,还得重新找数据,麻烦得很。

3.2.2 分钟线事实表

分钟线比日线复杂一些。数据量大了几十倍。设计时要注意:

CREATE TABLE fact_minute (
    trade_date    DATE NOT NULL,
    stock_code    VARCHAR(10) NOT NULL,
    minute_time   TIME NOT NULL,       -- 分钟时间,如 09:31:00
    open_price    DECIMAL(10,2),
    high_price    DECIMAL(10,2),
    low_price     DECIMAL(10,2),
    close_price   DECIMAL(10,2),
    volume        BIGINT,
    amount        DECIMAL(16,2),
    trades_count  INT,                 -- 成交笔数
    PRIMARY KEY (trade_date, stock_code, minute_time)
);

这里有个坑:分钟线的开盘价不是 09:30:00 那一分钟的开盘价。09:30:00 是集合竞价的结果,真正的连续竞价从 09:30:00 开始。所以 09:31:00 这一分钟的开盘价,才是 09:30:00 到 09:31:00 这60秒内的第一笔成交价。我曾经在这个问题上栽过跟头,回测结果和实盘对不上,查了两天才发现是分钟线定义搞错了。

3.2.3 Tick 级事实表

Tick 级数据,就是每一笔成交记录。数据量巨大,一天可能几千万条。设计时要考虑性能:

CREATE TABLE fact_tick (
    trade_date    DATE NOT NULL,
    stock_code    VARCHAR(10) NOT NULL,
    trade_time    TIMESTAMP NOT NULL,   -- 精确到毫秒
    trade_price   DECIMAL(10,2),
    trade_volume  INT,
    trade_amount  DECIMAL(16,2),
    trade_type    CHAR(1),              -- 'B' 买盘, 'S' 卖盘
    PRIMARY KEY (trade_date, stock_code, trade_time)
);

警告:Tick 表不要用自增主键。用 (trade_date, stock_code, trade_time) 做复合主键,查询时能直接走索引,快很多。我见过有人用自增 ID,结果查某只股票某天的数据,全表扫描,慢得没法用。

3.3 维度表设计:股票、时间、行业

维度表是事实表的“字典”。设计得好,查询时少走弯路。

3.3.1 股票维度表

股票信息会变,比如改名、退市、增发。我建议用缓慢变化维度(SCD)类型2

CREATE TABLE dim_stock (
    stock_id      INT AUTO_INCREMENT PRIMARY KEY,
    stock_code    VARCHAR(10) NOT NULL,
    stock_name    VARCHAR(50),
    ipo_date      DATE,
    delist_date   DATE,
    exchange      VARCHAR(10),          -- 'SH', 'SZ'
    status        VARCHAR(10),          -- '上市', '退市', '暂停'
    valid_from    DATE NOT NULL,        -- 生效日期
    valid_to      DATE,                 -- 失效日期,NULL 表示当前有效
    is_current    BOOLEAN DEFAULT TRUE  -- 是否当前版本
);

举个例子:某股票从“XX科技”改名为“XX集团”。在维度表里,会新增一条记录,旧记录的 valid_to 设为改名日期,新记录的 valid_from 设为改名日期。这样,回测时用 valid_from <= trade_date AND (valid_to IS NULL OR valid_to > trade_date) 就能查到当时的正确名称。

3.3.2 时间维度表

时间维度表,很多人觉得没必要。但我建议一定要建。为什么?

  • 可以预计算交易日、节假日、季度末等标记
  • 查询时直接 JOIN,不用在 SQL 里写复杂的日期函数
  • 回测时判断“是否是交易日”特别方便
CREATE TABLE dim_time (
    date_key      DATE PRIMARY KEY,
    year          INT,
    month         INT,
    day           INT,
    week_day      INT,                  -- 1=周一, 7=周日
    is_trade_day  BOOLEAN,              -- 是否交易日
    is_month_end  BOOLEAN,              -- 是否月末
    is_quarter_end BOOLEAN,             -- 是否季末
    is_year_end   BOOLEAN               -- 是否年末
);

嗯,这里要注意:交易日数据要提前维护好。我每年年初会把全年的交易日导入这张表,包括春节、国庆等调休日。回测时直接查 is_trade_day = TRUE,省心省力。

3.3.3 行业维度表

行业分类是个麻烦事。不同机构分类不同,同一机构也会调整。我建议用雪花模型处理:

-- 行业分类表
CREATE TABLE dim_industry (
    industry_id   INT PRIMARY KEY,
    industry_name VARCHAR(50),
    level         INT,                  -- 1=一级, 2=二级
    parent_id     INT                   -- 父级分类ID
);

-- 股票-行业关联表
CREATE TABLE dim_stock_industry (
    stock_code    VARCHAR(10),
    industry_id   INT,
    valid_from    DATE,
    valid_to      DATE,
    PRIMARY KEY (stock_code, industry_id, valid_from)
);

这样设计的好处是:行业分类变了,只改 dim_industry 表;股票所属行业变了,只改 dim_stock_industry 表。事实表完全不受影响。

总结一下:事实表存“发生了什么”,维度表存“是什么”。两者分开,查询灵活,维护方便。

好了,数据模型设计的上半部分就聊到这里。星型模型和雪花模型的选择,说白了就是性能和灵活性的权衡。事实表设计要关注粒度和主键,维度表设计要关注变化和关联。把这些基础打牢了,后面的数据加载和查询优化才能事半功倍。

量化回测数据模型设计(上)知识体系 星型模型 vs 雪花模型 性能优先 vs 灵活优先 事实表设计 日线 · 分钟线 · Tick级 维度表设计 股票·时间·行业 星型模型 雪花模型 日线 分钟线 Tick级 股票维度 时间维度 行业维度 核心原则 1. 查询性能优先用星型,维度变化频繁用雪花 2. 事实表粒度决定查询精度,主键设计影响查询性能 3. 维度表用SCD类型2处理变化,时间维度表预计算交易日

专注资料整理