数据模型设计基础:行情数据的实体关系

行情数据库的设计,说白了就是搞清楚「谁产生了什么数据,数据长什么样」。我做了这么多年金融IT,见过太多人一上来就建表,结果数据量一上来就崩了。今天咱们聊聊最核心的部分——实体关系与主键设计。

核心实体:股票、合约、交易所

行情数据离不开三个基本实体。你想想看,一条行情记录,必须知道「在哪个交易所」、「哪个品种」、「什么时间」产生的。这三者缺一不可。

实体 核心属性 说明
交易所 交易所代码、名称、时区 比如SSE、SZSE、CME
股票/合约 标的ID、名称、类型、所属交易所 股票、期货、期权都算
行情快照 时间戳、开盘价、收盘价、成交量等 这是核心事实表

关键点:交易所和标的之间是多对一关系。一个交易所下有多个标的,但一个标的只属于一个交易所。这个关系在建模时一定要明确。

我在项目中遇到过有人把交易所代码直接塞进行情表里,结果每次查询都要做字符串匹配,性能惨不忍睹。后来我建议把交易所ID作为外键,用整数关联,查询速度直接提升了3倍。

实体关系图

下面这张图展示了三个核心实体之间的关系。我习惯用这种简洁的方式来表达,比文字直观多了。

交易所 exchange_id (PK) exchange_code, name 股票/合约 instrument_id (PK) symbol, name, type 行情快照 instrument_id (复合PK) timestamp (复合PK) 1 N 1 N 实体关系:交易所 1:N 股票/合约 1:N 行情快照

复合主键设计:时间戳 + 标的ID

行情表的主键怎么选?我个人的习惯是坚决不用自增ID做主键。为什么?因为行情数据的查询模式决定了——你永远是按「某个标的在某个时间范围」来查的。

推荐方案:使用 (instrument_id, timestamp) 作为复合主键。

这样做的好处很明显:

  • 查询效率高:按标的+时间范围查询时,直接走主键索引,不需要回表
  • 数据唯一性:同一个标的在同一时刻只能有一条记录,天然防重复
  • 存储紧凑:InnoDB的聚簇索引会把数据按主键顺序存储,范围扫描非常快

注意:时间戳的精度要统一。我见过有人用秒级时间戳,有人用毫秒级,结果关联查询时对不上。建议统一使用毫秒级Unix时间戳,或者直接用DATETIME(3)。

实战中的避坑指南

我曾经在一个期货行情项目中,一开始用了自增ID做主键,然后在instrument_id和timestamp上分别建了索引。结果呢?每天几千万条数据写入,索引维护开销巨大,查询还经常要回表。后来改成复合主键,写入速度提升了40%,查询延迟从200ms降到了5ms。

嗯,这里要注意另一个坑:复合主键的字段顺序。我建议把instrument_id放在前面,timestamp放在后面。因为大部分查询是先限定标的,再限定时间范围。如果你反过来,查询效率会大打折扣。

小技巧:如果数据量特别大(比如每天上亿条),可以考虑按时间分区。比如按月分区,每个分区内再用复合主键。这样历史数据归档和清理都方便。

代码示例:建表语句

下面是一个实际项目中用的建表语句,你可以参考一下:

CREATE TABLE market_snapshot (
    instrument_id   INT UNSIGNED    NOT NULL COMMENT '标的ID',
    trade_date      DATE            NOT NULL COMMENT '交易日期',
    timestamp_ms    BIGINT UNSIGNED NOT NULL COMMENT '毫秒级时间戳',
    open_price      DECIMAL(16,4)   NOT NULL COMMENT '开盘价',
    high_price      DECIMAL(16,4)   NOT NULL COMMENT '最高价',
    low_price       DECIMAL(16,4)   NOT NULL COMMENT '最低价',
    close_price     DECIMAL(16,4)   NOT NULL COMMENT '收盘价',
    volume          BIGINT UNSIGNED NOT NULL COMMENT '成交量',
    amount          DECIMAL(20,4)   NOT NULL COMMENT '成交额',
    PRIMARY KEY (instrument_id, trade_date, timestamp_ms),
    INDEX idx_trade_date (trade_date)
) ENGINE=InnoDB
  PARTITION BY RANGE (YEAR(trade_date))
  SUBPARTITION BY HASH (MONTH(trade_date))
  SUBPARTITIONS 12;

这个设计里,我把trade_date单独拎出来做分区键,同时作为复合主键的一部分。这样既保证了按标的+时间的高效查询,又方便做数据生命周期管理。

你想想看,如果不用分区,一张表里堆上几年的数据,光索引就比数据还大。分区后,查询只扫描对应分区的数据,性能完全不是一个量级。

好了,关于实体关系和复合主键设计,今天就聊到这儿。这些经验都是我在实际项目中一点点踩坑踩出来的,希望能帮你少走弯路。


专注资料整理