数据库设计: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 快得多。

核心原则:

  • 写入用批量,别一条一条来
  • 查询走索引,别全表扫描
  • 数据量大就分区,别死磕单表
  • 定期 VACUUMANALYZE,保持统计信息新鲜

五、知识体系总览

下面这张图总结了本章的核心逻辑,你可以对照着梳理自己的设计思路:

做市系统数据库设计核心逻辑 数据库选型 SQLite(原型/测试) PostgreSQL(生产) 其他(不推荐) 表结构设计 写入优化 查询优化 订单表(orders) 成交表(trades) 行情快照表(ticker_snapshots) 外键约束、索引设计 批量写入(100-500条) COPY 命令导入 synchronous_commit = off WAL 日志控制 复合索引(symbol+time) 覆盖索引(INCLUDE) 分区表(按月) VACUUM / ANALYZE 目标:高吞吐、低延迟、数据一致

数据库设计没有银弹。选型、表结构、写入策略、查询优化,每个环节都要根据实际场景权衡。我个人的经验是:先按 PostgreSQL + 批量写入 + 复合索引这套组合拳打底,等数据量上来后再考虑分区和覆盖索引。别一开始就搞得太复杂,但也别偷懒到连索引都不建。

嗯,今天就聊到这儿。数据库这块儿坑不少,但只要你把基础打牢了,后面做策略回测、实盘监控都会顺手很多。


无相订单流研究社 微信Lucian808555