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 表。事实表完全不受影响。
总结一下:事实表存“发生了什么”,维度表存“是什么”。两者分开,查询灵活,维护方便。
好了,数据模型设计的上半部分就聊到这里。星型模型和雪花模型的选择,说白了就是性能和灵活性的权衡。事实表设计要关注粒度和主键,维度表设计要关注变化和关联。把这些基础打牢了,后面的数据加载和查询优化才能事半功倍。