【Python行情API量化工程实战 M01】数据存哪里不卡?Python股票存储方案 CSV-SQLite-MySQL 对比与选型
本文是「Python 行情 API 量化工程实战」系列第 1 篇(工程化模块)。做量化的第一步往往是把行情数据拉下来------但拉下来塞哪儿?扔进内存下次启动就没了,写成 CSV 文件又慢又占盘。这篇把量化项目最常见的三种本地存储方案放在同一台机器、同一份数据上跑了一次真实基准,看完你就能根据自己的场景挑对方案。
本文你将得到什么
- 三种存储方案在同一台机器同一份数据下的写入 / 全量读 / 条件查询耗时对比
- CSV / SQLite / MySQL 选型决策树(10 万行以下?百万行?千万行?多人协作?)
- SQLite 的一行代码增量更新写法:用
UNIQUE(code, trade_dt)避免重复写 - 三种方案的 5 个常见坑(编码、并发、列类型、磁盘 IO、备份策略)
- 一个可直接套用的「拉数 → 落库」最小工程骨架(零第三方 SDK,纯 HTTP)
一、数据从哪来(最小依赖)
本文不依赖任何第三方行情 SDK------所有数据通过一个官方演示证书直接 HTTP GET 获取(返回的是预先设置的演示数据,需替换为自己的实际证书),零安装、跨语言。你只需要:
bash
pip install requests # 仅此一个依赖,标准库 csv / sqlite3 自带
运行环境:
- Python 3.9+(标准库即可,无需 pandas)
- requests 2.x
- SQLite 3.x(Python 标准库自带,无需安装)
- MySQL(可选,需自行部署;本文 DDL 部分给参考,未连库实测)
python
import requests
# 演示证书(官方演示证书,返回的是预先设置的演示数据,需要替换为自己的实际证书)
TOKEN = "TEST-API-TOKEN-MOMA-836089C22111"
BASE = "https://api.momaapi.com"
本文数据用演示证书限量拉取(每接口返回近期样本),共 10 只样本股各 50 条日 K,合计 500 行 × 11 列。付费证书可拉取全量历史,量级到百万行时结论会更明显(见第四节)。
二、三种方案的真实基准
先别急着选方案,拿数据说话。我准备了 10 只股票各 50 条日 K(共 500 行),分别用 CSV、SQLite 写入磁盘,再分别测"全量读回内存"和"按 code 过滤"两个高频操作的耗时。所有数字均为本机实测。
python
import time, sqlite3, os, csv
import requests
TOKEN = "TEST-API-TOKEN-MOMA-836089C22111"
BASE = "https://api.momaapi.com"
# 拉样本(演示证书,最小化代码,无重试------存储演示用)
r = requests.get(f"{BASE}/hslt/list/{TOKEN}", timeout=30)
stock_list = r.json()[:10]
rows_all = []
for s in stock_list:
url = f"{BASE}/hsstock/history/{s['dm']}/d/n/{TOKEN}?st=20240101&et=20260630"
for x in requests.get(url, timeout=40).json():
x["code"] = s["dm"]; x["name"] = s["mc"]; rows_all.append(x)
print(f"样本: {len(rows_all)} 行 x {len(rows_all[0])} 列")
真实输出:
text
样本: 500 行 x 11 列
2.1 写入耗时
python
CSV, SQL = "kline.csv", "kline.sqlite"
def save_csv(rows):
t0 = time.time()
with open(CSV, "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
return (time.time() - t0) * 1000
def save_sqlite(rows):
t0 = time.time()
conn = sqlite3.connect(SQL)
cols = list(rows[0].keys())
conn.execute(f"CREATE TABLE IF NOT EXISTS kline({','.join(cols)})")
q = ",".join("?" * len(cols))
conn.executemany(f"INSERT INTO kline VALUES ({q})", [tuple(r.values()) for r in rows])
conn.commit(); conn.close()
return (time.time() - t0) * 1000
真实输出(本机实测):
text
CSV 写入: 19.6 ms 文件 40.6 KB
SQLite 写入: 132.9 ms 文件 56.0 KB
小数据量下 CSV 反而更快------因为 SQLite 要建表、开事务,有固定开销。这个开销是一次性摊销的:当数据量从 500 行涨到 500 万行,CSV 的纯文本解析成本会线性放大,而 SQLite 的优势会逐步显现(见第四节扩展建议)。
2.2 全量读回内存
python
def load_csv():
t0 = time.time()
with open(CSV, encoding="utf-8-sig") as f:
list(csv.DictReader(f))
return (time.time() - t0) * 1000
def load_sqlite():
t0 = time.time()
conn = sqlite3.connect(SQL); list(conn.execute("SELECT * FROM kline")); conn.close()
return (time.time() - t0) * 1000
真实输出:
text
CSV 全量读: 47.9 ms
SQLite 全量读: 127.2 ms
差距不大------500 行的数据量(40 KB)太小,磁盘 IO 主导,SQLite 的索引优势还没发挥。
2.3 条件查询(最常被忽视的差距)
python
# CSV 方案:只能把全表读进内存再用 Python 过滤
with open(CSV, encoding="utf-8-sig") as f:
hits = [x for x in csv.DictReader(f) if x["code"] == "000001.SZ"]
# SQLite 方案:库内过滤,只返回匹配行
conn = sqlite3.connect(SQL)
rows = list(conn.execute("SELECT * FROM kline WHERE code=?", ("000001.SZ",)))
conn.close()
真实输出:
text
CSV 读全表再过滤: 56.1 ms(含 IO,内存过滤)
SQLite 索引过滤: 49.2 ms(库内过滤)
在本机 500 行样本下两者差距很小(约 1.1 倍)。但注意一个本质区别:CSV 必须每次把全表读进内存再过滤,而 SQLite 只在磁盘上检索匹配行。当表单边到几百万行,CSV 方案的内存占用会线性爆炸,SQLite 仍能稳定返回------这是工程化项目里 SQLite 的核心价值。
三、选型决策树:你的项目该选谁
| 场景 | 推荐 | 理由 |
|---|---|---|
| 临时分析 / 单次回测(<1 万行) | CSV | 零依赖、Excel 可直接打开、人工检查方便 |
| 单机小项目(10 万 ~ 100 万行) | SQLite 【推荐】 | 单文件、零配置、并发安全、查询快 |
| 多人协作 / 数据量 > 1 亿行 | MySQL / PostgreSQL | 真正的服务进程,多用户权限与备份机制 |
| 多端同步 / Web 后端读取 | MySQL | 跨进程访问,SQLite 写锁会成瓶颈 |
| 回测历史归档 | CSV + Parquet | Parquet 列存压缩,1 亿行可压到 1 GB 以内 |
经验阈值:
- 数据量 < 1 GB → SQLite 完全够用
- 数据量 1~50 GB → MySQL 单机能扛
- 数据量 > 50 GB → MySQL 分库分表 + Parquet 归档
四、SQLite 的 4 个工程化技巧
SQLite 是单文件 DB,看起来"玩具",但合理使用完全可以扛住个人量化项目。几个关键技巧:
4.1 启用索引(让条件查询真正快起来)
sql
CREATE INDEX idx_code ON kline(code);
CREATE UNIQUE INDEX uniq_code_dt ON kline(code, trade_dt);
UNIQUE(code, trade_dt) 是增量更新的基础------同一天不会写两行。
4.2 增量更新(不重复写)
配合上面的唯一索引,写之前先 INSERT OR IGNORE,已存在的行自动跳过:
python
conn = sqlite3.connect(SQL)
conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS uniq_code_dt ON kline(code, trade_dt)")
q = ",".join("?" * len(cols))
conn.executemany(f"INSERT OR IGNORE INTO kline VALUES ({q})", new_rows)
conn.commit(); conn.close()
4.3 编码与并发
- 编码 :CSV 写盘用
encoding="utf-8-sig"(带 BOM),Excel 打开中文不乱码;读盘也用utf-8-sig。 - 并发 :SQLite 写是库级锁,多进程同时写会
database is locked。解决:单写多读、或改用 MySQL。CSV 同理,多进程写同一文件要自己加锁。
4.4 备份策略
- CSV:直接复制文件即可,但大文件复制慢。
- SQLite:可用
VACUUM INTO 'backup.db'在线热备,不阻塞读。 - MySQL:用
mysqldump或主从复制。
五、从 CSV 平滑迁移到 MySQL(最小 DDL)
当项目长大到需要 MySQL,SQLite 的表结构几乎可以直接平移。最小 DDL 模板(含主键/唯一索引):
sql
CREATE TABLE kline (
t DATE,
o DECIMAL(10,3),
h DECIMAL(10,3),
l DECIMAL(10,3),
c DECIMAL(10,3),
v BIGINT,
a BIGINT,
pc DECIMAL(10,3),
code VARCHAR(16),
name VARCHAR(64),
PRIMARY KEY (code, t),
UNIQUE KEY uk_code_t (code, t)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
迁移只需把 executemany 的 SQLite 连接换成 MySQL 连接(如 pymysql),字段一一对应即可。
六、小结
存储方案没有"最好",只有"最合适"。本机实测的结论很朴素:小数据量下 CSV 零开销最省事;量级上到百万行,SQLite 的库内过滤和增量更新会变成刚需;再往上才是 MySQL 的舞台。 先用 CSV 把流程跑通,等数据真把你卡住了再升级------这是量化工程里最省心的节奏。
注意:本文为技术分享,所有接口调用均使用官方演示证书返回的演示数据(需替换为自己的实际证书) 魔码证书申请