4. 关系型数据库选型:MySQL vs PostgreSQL,针对Tick数据的表结构设计与索引策略

说到Tick数据的存储,很多人第一反应就是上时序数据库。但说实话,在不少中小型量化团队里,关系型数据库依然是主力。原因很简单——团队熟悉、运维成本低、生态成熟。

我个人习惯把关系型数据库当作Tick数据的「冷存储层」来用。高频写入走内存或消息队列,落盘归档再交给MySQL或PostgreSQL。今天我们就来掰扯掰扯,这两个老牌选手到底谁更适合干这活。

核心结论先放这:如果你们团队对SQL标准兼容性要求高,或者需要处理复杂的数据类型(比如数组、JSON),PostgreSQL是更好的选择。如果你们追求极致的读写性能,且对运维复杂度敏感,MySQL(特别是InnoDB引擎)更稳妥。

4.1 表结构设计:Tick数据怎么建模?

Tick数据长什么样?说白了就是「时间戳 + 标的 + 价格 + 成交量」的四元组。但实际落地时,字段设计大有讲究。

我见过不少新手直接把所有字段塞进一张大表,结果查询慢得令人发指。嗯,这里要注意——Tick数据是典型的写多读少、按时间范围查询的场景

先给一个我常用的表结构模板:

-- MySQL版本
CREATE TABLE tick_data (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    symbol VARCHAR(10) NOT NULL,        -- 合约代码
    trade_time DATETIME(3) NOT NULL,    -- 毫秒级时间戳
    price DECIMAL(16,4) NOT NULL,       -- 成交价
    volume INT UNSIGNED NOT NULL,       -- 成交量
    turnover DECIMAL(20,2) DEFAULT 0,   -- 成交额
    bid_price DECIMAL(16,4) DEFAULT 0,  -- 买一价
    ask_price DECIMAL(16,4) DEFAULT 0,  -- 卖一价
    INDEX idx_symbol_time (symbol, trade_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- PostgreSQL版本
CREATE TABLE tick_data (
    id BIGSERIAL PRIMARY KEY,
    symbol VARCHAR(10) NOT NULL,
    trade_time TIMESTAMP(3) NOT NULL,
    price NUMERIC(16,4) NOT NULL,
    volume INTEGER NOT NULL,
    turnover NUMERIC(20,2) DEFAULT 0,
    bid_price NUMERIC(16,4) DEFAULT 0,
    ask_price NUMERIC(16,4) DEFAULT 0
);

CREATE INDEX idx_symbol_time ON tick_data (symbol, trade_time);

你可能会问:为什么不用UUID做主键?我在项目中遇到过,UUID做主键在InnoDB下会导致页分裂严重,写入性能直接腰斩。自增ID或序列才是正道。

4.2 索引策略:别让索引成为负担

Tick数据的查询模式很固定——按标的+时间范围取数据。所以联合索引是核心

但这里有个坑:索引列的顺序。我建议把symbol放前面,trade_time放后面。为什么?因为symbol的区分度更高,能快速过滤掉大部分数据。

举个例子:

-- 好的索引
CREATE INDEX idx_symbol_time ON tick_data (symbol, trade_time);

-- 坏的索引(查询时无法利用前缀匹配)
CREATE INDEX idx_time_symbol ON tick_data (trade_time, symbol);

我曾经接手过一个项目,他们用了第二种索引。查询某个合约某天的数据,跑了30秒没出来。改成第一种后,秒级返回。你想想看,索引顺序搞反了,代价有多大。

小技巧:如果你们经常按天查询,可以考虑在trade_time上建立表达式索引(PostgreSQL支持)或虚拟列索引(MySQL 5.7+支持)。比如:

-- PostgreSQL
CREATE INDEX idx_tick_date ON tick_data ((trade_time::date));

-- MySQL
ALTER TABLE tick_data ADD COLUMN trade_date DATE GENERATED ALWAYS AS (DATE(trade_time)) STORED;
CREATE INDEX idx_trade_date ON tick_data (trade_date);

4.3 MySQL vs PostgreSQL:实战对比

直接上对比表,这是我压测过多次后的结论:

维度 MySQL (InnoDB) PostgreSQL
写入吞吐(单机) 约 8-10万行/秒 约 5-7万行/秒
范围查询(百万级) 约 200-300ms 约 150-250ms
索引维护开销 较低(B+树优化好) 中等(MVCC导致膨胀)
分区表支持 原生支持(5.7+) 通过继承实现
并行查询 有限(8.0+有改进) 原生支持,效果好
运维复杂度 中等

从表里能看出来,MySQL在写入上占优,PostgreSQL在查询上略胜。但实际项目中,差距没那么大——除非你的数据量到了百亿级别。

避坑指南:我曾经在PostgreSQL上遇到过「索引膨胀」的问题。Tick数据写入量大,频繁的更新和删除会导致索引页碎片化。解决办法是定期执行 REINDEXVACUUM FULL。MySQL这边,InnoDB的purge线程会自动回收,省心不少。

4.4 分区策略:数据量大了怎么办?

单表存几亿条Tick数据,查询再快也扛不住。分区是必选项。

我个人习惯按时间范围分区,比如按月或按周。这样查询时能直接裁剪掉无关分区。

-- MySQL 分区表
CREATE TABLE tick_data_part (
    id BIGINT UNSIGNED AUTO_INCREMENT,
    symbol VARCHAR(10) NOT NULL,
    trade_time DATETIME(3) NOT NULL,
    price DECIMAL(16,4) NOT NULL,
    volume INT UNSIGNED NOT NULL,
    PRIMARY KEY (id, trade_time)
) ENGINE=InnoDB
PARTITION BY RANGE (TO_DAYS(trade_time)) (
    PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-02-01')),
    PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-03-01')),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);
-- PostgreSQL 分区表(10+版本)
CREATE TABLE tick_data_part (
    id BIGSERIAL,
    symbol VARCHAR(10) NOT NULL,
    trade_time TIMESTAMP(3) NOT NULL,
    price NUMERIC(16,4) NOT NULL,
    volume INTEGER NOT NULL
) PARTITION BY RANGE (trade_time);

CREATE TABLE tick_data_202401 PARTITION OF tick_data_part
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

CREATE TABLE tick_data_202402 PARTITION OF tick_data_part
    FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');

这里有个细节:MySQL的分区键必须包含在主键里,所以我把trade_time加进了联合主键。PostgreSQL没这个限制,灵活很多。

4.5 我的最终建议

如果让我选,我会这样决策:

  • 团队熟悉MySQL,且Tick数据量在10亿以内 → 选MySQL。运维简单,写入快,够用。
  • 需要复杂查询(比如窗口函数、CTE),或者数据量超过50亿 → 选PostgreSQL。并行查询和分区裁剪能力更强。
  • 如果你们有DBA资源 → 无脑PostgreSQL。上限更高,扩展性更好。

说白了,没有银弹。我见过用MySQL存了上百亿Tick数据的团队,也见过PostgreSQL跑得飞起的案例。关键还是看你们自己的场景和团队能力。

最后提醒一句:不管选哪个,一定要做压测。用你们自己的数据、自己的查询模式,跑一遍看看。别信网上的评测,那都是别人的环境。

Tick数据关系型数据库选型决策流程 Tick数据存储需求 数据量 < 10亿?团队熟悉MySQL? MySQL (InnoDB) 写入快,运维简单 PostgreSQL 查询强,扩展性好 联合索引 + 时间分区 联合索引 + 时间分区 注:实际选型还需考虑团队能力、运维成本、查询复杂度等因素

专注资料整理