数据库设计:SQLite vs PostgreSQL选择、表结构设计、数据写入优化、查询优化
做市系统里,数据库这块儿经常被新手忽略。大家总觉得「先跑起来再说」,结果数据一多,查询慢得像蜗牛爬。我早期吃过这个亏,今天就把经验掰开揉碎讲清楚。
一、SQLite 还是 PostgreSQL?别纠结了
先给结论:做市系统用 PostgreSQL,别用 SQLite。为什么?
SQLite 是嵌入式数据库,说白了就是个文件。它适合单机、低并发的场景。但做市系统需要处理高频的订单流、实时行情,还要支持多个进程同时读写。SQLite 的锁机制会让你痛不欲生——写的时候读不了,读的时候写不了。
PostgreSQL 是真正的客户端-服务器架构。它支持 MVCC(多版本并发控制),读写互不阻塞。我在项目中遇到过,用 SQLite 时每秒 1000 笔订单写入就开始报错「database is locked」。换成 PostgreSQL 后,每秒 1 万笔写入都稳稳的。
核心对比:
| 特性 | SQLite | PostgreSQL |
|---|---|---|
| 并发写入 | 单写,其他阻塞 | 多写,MVCC 支持 |
| 数据完整性 | 弱,无 WAL 日志 | 强,WAL + 事务 |
| 扩展性 | 单机,无主从 | 主从、分片、流复制 |
| 适用场景 | 原型、测试、低并发 | 生产环境、高频交易 |
嗯,这里要注意:如果你只是做个人回测、本地实验,SQLite 够用。但一旦涉及实盘,哪怕资金再小,也请直接上 PostgreSQL。别问我怎么知道的——我曾经在实盘第一天就被 SQLite 坑了,订单数据丢了半小时。
二、表结构设计:别偷懒,分清楚
做市系统的核心数据无非三类:订单、成交、行情。我建议至少建三张表,别图省事塞到一张表里。
2.1 订单表(orders)
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
order_id VARCHAR(64) UNIQUE NOT NULL,
symbol VARCHAR(20) NOT NULL,
side VARCHAR(4) NOT NULL, -- 'buy' 或 'sell'
price NUMERIC(20,8) NOT NULL,
quantity NUMERIC(20,8) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_orders_symbol ON orders(symbol);
CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_created_at ON orders(created_at);
为什么用 BIGSERIAL?因为做市系统一天可能产生几十万笔订单,普通 SERIAL 会溢出。我见过有人用 INT,结果半年后 ID 用完了,整个系统崩了。
2.2 成交表(trades)
CREATE TABLE trades (
id BIGSERIAL PRIMARY KEY,
trade_id VARCHAR(64) UNIQUE NOT NULL,
order_id VARCHAR(64) NOT NULL REFERENCES orders(order_id),
symbol VARCHAR(20) NOT NULL,
price NUMERIC(20,8) NOT NULL,
quantity NUMERIC(20,8) NOT NULL,
fee NUMERIC(20,8) DEFAULT 0,
traded_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_trades_order_id ON trades(order_id);
CREATE INDEX idx_trades_traded_at ON trades(traded_at);
成交表要关联订单表,用外键约束。别觉得外键影响性能就删掉——数据一致性比那点性能重要得多。我早期做的一个系统,就是因为没加外键,成交记录关联到了不存在的订单,对账时花了三天才查出来。
2.3 行情快照表(ticker_snapshots)
CREATE TABLE ticker_snapshots (
id BIGSERIAL PRIMARY KEY,
symbol VARCHAR(20) NOT NULL,
bid NUMERIC(20,8) NOT NULL,
ask NUMERIC(20,8) NOT NULL,
last NUMERIC(20,8) NOT NULL,
volume NUMERIC(20,8) NOT NULL,
snapshot_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_ticker_symbol_time ON ticker_snapshots(symbol, snapshot_at DESC);
行情数据的特点是写入频繁、查询最近数据多。所以索引要按 (symbol, snapshot_at DESC) 建,这样查「某个币种的最新行情」就很快。
个人习惯:所有时间字段都用 TIMESTAMPTZ(带时区),别用 TIMESTAMP。做市系统可能跨交易所、跨时区,统一用 UTC 存储,展示时再转本地时间。否则夏令时切换时你会疯掉。
三、数据写入优化:别一条一条插
很多新手写数据是一条 INSERT 一条提交。这在做市系统里是灾难。你想想看,每秒几百笔成交,每条都单独提交,数据库光处理事务开销就占了大半。
正确的做法是批量写入:
-- 批量插入示例(一次插 100 条)
INSERT INTO trades (trade_id, order_id, symbol, price, quantity, fee, traded_at)
VALUES
('t1', 'o1', 'BTCUSDT', 50000, 0.1, 0.001, NOW()),
('t2', 'o2', 'BTCUSDT', 50001, 0.2, 0.002, NOW()),
-- ... 更多数据
('t100', 'o100', 'BTCUSDT', 50099, 0.15, 0.0015, NOW());
我建议每 100-500 条批量提交一次。太小了没效果,太大了内存扛不住。另外,记得用 COPY 命令做初始数据导入,比 INSERT 快 10 倍以上。
避坑指南:我曾经把批量大小设成 10000,结果 PostgreSQL 的 WAL 日志暴涨,磁盘直接写满。后来改成 500 一批,配合 synchronous_commit = off,写入速度提升了 3 倍,而且磁盘稳稳的。
四、查询优化:索引是王道,但不是万能
做市系统最常见的查询是:查某个币种的最新订单、查某段时间的成交记录。这些查询如果没走索引,全表扫描会慢到让你怀疑人生。
4.1 复合索引
比如查「BTCUSDT 最近 1 小时的成交」:
-- 慢查询(没索引)
SELECT * FROM trades
WHERE symbol = 'BTCUSDT'
AND traded_at > NOW() - INTERVAL '1 hour';
-- 建复合索引
CREATE INDEX idx_trades_symbol_time ON trades(symbol, traded_at DESC);
复合索引的字段顺序很重要。把区分度高的放前面,比如 symbol 只有几十种,但 traded_at 是连续的。所以 (symbol, traded_at) 比 (traded_at, symbol) 更高效。
4.2 覆盖索引
如果查询只需要部分字段,可以用覆盖索引避免回表:
-- 覆盖索引示例
CREATE INDEX idx_trades_covering ON trades(symbol, traded_at DESC)
INCLUDE (price, quantity, fee);
这样查询 SELECT price, quantity FROM trades WHERE symbol = 'BTCUSDT' 时,直接从索引拿数据,不用访问表。性能提升很明显。
4.3 分区表
当数据量超过 1 亿条时,单表索引也扛不住了。这时候考虑分区:
-- 按时间分区(每月一个分区)
CREATE TABLE trades (
id BIGSERIAL,
trade_id VARCHAR(64),
traded_at TIMESTAMPTZ NOT NULL
) PARTITION BY RANGE (traded_at);
CREATE TABLE trades_2024_01 PARTITION OF trades
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE trades_2024_02 PARTITION OF trades
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
分区后,查询只扫描对应分区,速度提升一个数量级。而且删除旧数据也方便——直接 DROP TABLE 分区,比 DELETE 快得多。
核心原则:
- 写入用批量,别一条一条来
- 查询走索引,别全表扫描
- 数据量大就分区,别死磕单表
- 定期
VACUUM和ANALYZE,保持统计信息新鲜
五、知识体系总览
下面这张图总结了本章的核心逻辑,你可以对照着梳理自己的设计思路:
数据库设计没有银弹。选型、表结构、写入策略、查询优化,每个环节都要根据实际场景权衡。我个人的经验是:先按 PostgreSQL + 批量写入 + 复合索引这套组合拳打底,等数据量上来后再考虑分区和覆盖索引。别一开始就搞得太复杂,但也别偷懒到连索引都不建。
嗯,今天就聊到这儿。数据库这块儿坑不少,但只要你把基础打牢了,后面做策略回测、实盘监控都会顺手很多。
无相订单流研究社 微信Lucian808555