1. 先想清楚你的数据到底要存什么、怎么用
个人量化研究,最怕的不是模型不灵,而是数据没管好。模型可以换,策略可以调,但数据一旦乱了,或者查询慢到跑一次回测要等半天,整个研究流程就卡住了。所以,选数据库不是看哪个技术最火,而是先想清楚你的数据长什么样、你打算怎么用它。
我见过很多新手一上来就问“用 MySQL 还是 PostgreSQL”,这其实问错了。对于个人研究者,你的数据场景通常很明确:时间序列数据是绝对核心。你的股票K线、期货tick、因子值、信号,都是带时间戳的。除此之外,你还需要存一些维度数据,比如股票代码列表、行业分类、财务指标快照。最后,你的研究中间结果,比如每次回测的净值曲线、绩效指标,也需要有个地方放。
所以,选择的核心矛盾是:时间序列的写入和查询效率,与维度数据的关联查询便利性之间的权衡。一个只存时间序列的专用数据库,查询速度可能飞快,但你想把行情数据和财务数据关联起来做分析,可能就得自己写一堆代码拼接,很麻烦。一个通用的关系型数据库,关联查询很方便,但面对每天几百万条的tick数据,性能可能成为瓶颈。
我的建议是,先别急着定方案,花十分钟把你的数据需求列清楚:
- 数据量级:是日频、分钟级,还是tick级?未来一年大概有多少条记录?
- 查询模式:是经常按股票代码查一段时间的历史行情,还是需要做复杂的多表关联(比如,找出市盈率低于行业平均且最近有放量上涨的股票)?
- 分析工具:你主要用 Python 的 Pandas 做分析,还是用 SQL 直接跑?你的回测框架对数据接口有什么偏好?
- 硬件环境:数据是放在你自己的电脑上,还是云服务器?你的机器内存、磁盘(特别是SSD)有多大?
想清楚这些,我们再来看看有哪些选项,以及它们分别适合什么样的“个人研究者”。
2. 从最简单到最专业:四种主流方案的实战对比
市面上方案很多,但个人研究者没必要全都折腾一遍。我根据复杂度和适用场景,把它们归为四类。你可以直接对号入座。
2.1 方案一:文件 + Pandas (HDF5/Parquet/Feather)
适合谁:刚刚入门,数据量不大(比如,只研究A股日频数据,几年下来也就几十万条),追求极简启动,不想维护任何数据库服务的研究者。
核心能力:这不是数据库,而是序列化文件格式。你的整个数据库就是一个或几个文件。用 Pandas 读写,在内存里操作,思路最直接。
- HDF5(
.h5): 通过pandas.HDFStore使用。可以存储带类型的 DataFrame,支持分块读取,查询速度不错。但文件内部结构复杂,一旦损坏可能难以修复,且跨语言支持一般。 - Parquet(
.parquet): Apache 生态的列式存储格式。最大优点是压缩比高,节省磁盘空间,而且被 Spark、DuckDB 等很多新工具原生支持。用pandas.read_parquet读取很方便。 - Feather(
.feather): 设计目标就是快速读写,在 Pandas 和 R 之间交换数据。它几乎就是内存数据的直接镜像,所以读写速度极快,但压缩率不如 Parquet。
怎么用:
import pandas as pd import numpy as np # 假设你有一个日频行情 DataFrame: df_daily # 保存 df_daily.to_parquet('stock_daily.parquet') # 节省空间 # 或 df_daily.to_feather('stock_daily.feather') # 追求读写速度 # 读取(可以只读部分列) df = pd.read_parquet('stock_daily.parquet', columns=['code', 'date', 'close']) # 按日期和代码筛选 (在内存中) filtered = df[(df['date'] >= '2023-01-01') & (df['code'] == '000001.SZ')]避坑点:
- 全部数据加载到内存:这是最大限制。如果你的数据量超过内存,就会卡死。Parquet 虽支持分块读取,但用 Pandas 处理大文件依然不便。
- 并发访问差:文件一般不支持多进程同时写入,回测时如果想并行计算并写结果,需要小心处理锁或拆分文件。
- 复杂查询靠手动:所有关联、聚合、筛选逻辑,都需要你用 Pandas 代码写出来,不如 SQL 直观。
结论:入门首选,快速验证想法。当你的数据量在几个GB以内,且分析模式固定时,用 Parquet 或 Feather 能让你最快跑起来。一旦数据增长或查询变复杂,就要考虑升级。
2.2 方案二:SQLite
适合谁:需要关系型数据库的便利性(比如用 SQL 做复杂关联查询),但又不想安装和配置 MySQL/PostgreSQL 这类独立服务的研究者。数据量在几十GB级别以下通常都能胜任。
核心能力:SQLite 是一个库,不是一个服务器。你的整个数据库就是一个.db文件。它支持标准的 SQL,具备事务、索引等核心功能,但无需管理服务进程,零配置。
怎么用:
import sqlite3 import pandas as pd # 连接数据库(文件不存在会自动创建) conn = sqlite3.connect('quant_research.db') # 将Pandas DataFrame写入表 df_daily.to_sql('stock_daily', conn, if_exists='replace', index=False) # 用SQL直接查询 sql = """ SELECT a.date, a.close, b.industry FROM stock_daily a JOIN stock_info b ON a.code = b.code WHERE a.date BETWEEN '2023-01-01' AND '2023-03-31' AND b.industry = '银行' """ df_result = pd.read_sql_query(sql, conn) conn.close()性能关键:一定要建索引!对于时间序列查询,在(code, date)上建立复合索引,速度提升是数量级的。
CREATE INDEX idx_daily_code_date ON stock_daily (code, date);避坑点:
- 写入并发弱:SQLite 在某一时刻只允许一个写入操作。如果你的回测是并行任务同时写入结果,可能会遇到“database is locked”错误。解决方案是让每个子进程写入独立的临时文件,最后合并。
- 内存模式:你可以用
:memory:创建纯内存数据库,速度极快,适合中间计算,但程序关闭数据就消失,记得持久化。 - 数据类型宽松:SQLite 数据类型比较灵活,有时可能导致类型推断错误,在创建表时最好显式定义字段类型。
结论:个人研究的“瑞士军刀”。在数据量未达到TB级,且你需要频繁使用SQL做关联分析时,SQLite 是平衡便利与能力的绝佳选择。它的.db文件也方便备份和迁移。
2.3 方案三:DuckDB
适合谁:处理的数据量超过了 Pandas 内存限制(比如几十GB),需要进行复杂的交互式分析或连接多个大型数据集,但又觉得部署传统数仓(如ClickHouse)太重的个人研究者。
核心能力:这是一个进程内分析型数据库。它像 SQLite 一样以库的形式嵌入你的应用,但引擎是为分析型查询(OLAP)设计的,擅长处理海量数据的聚合、连接操作。它可以直接读写 Parquet/CSV 文件,无需先“导入”数据。
怎么用:
import duckdb # 连接(内存或文件) conn = duckdb.connect('quant.duckdb') # 或 ':memory:' # 1. 直接查询Parquet文件!无需导入 query = """ SELECT code, date, volume FROM 'stock_daily.parquet' WHERE date > '2023-01-01' AND volume > 10000000 """ df = conn.execute(query).df() # 直接返回DataFrame # 2. 也可以创建表并持久化 conn.execute("CREATE TABLE daily AS SELECT * FROM 'stock_daily.parquet'") # 3. 执行复杂关联查询(即使数据在多个Parquet文件里) complex_sql = """ SELECT d.code, d.date, d.close, f.pe_ratio FROM 'daily/*.parquet' d JOIN 'financials.parquet' f ON d.code = f.code AND d.date = f.report_date WHERE f.pe_ratio < 15 """ result = conn.execute(complex_sql).df()性能关键:DuckDB 会自动并行化查询以利用多核CPU。对于超大数据集,确保你的机器有足够内存,或者使用它的溢出到磁盘的功能。
避坑点:
- 不是事务型数据库:虽然支持事务,但它的强项是分析,不适合高频率、小事务的写入场景(比如实时交易记录)。更适合存储清洗后的历史数据和回测结果。
- 社区和工具生态:相比 MySQL/PostgreSQL,其管理工具和客户端支持较少,但作为嵌入式库,这通常不是问题。
- 仍在快速发展:虽然核心很稳定,但一些高级功能可能还在演进中。
结论:个人量化分析的“性能加速器”。当你受限于 Pandas 内存,又厌倦了 SQLite 处理大数据连接时的缓慢,DuckDB 几乎是无缝升级的最佳选择。特别是它能直接查询 Parquet 文件,让数据管理流程变得极其简洁。
2.4 方案四:时序数据库 (InfluxDB, TimescaleDB)
适合谁:数据源是超高频率的时序数据(如每秒数千条的tick数据、分钟级传感器数据),并且查询模式几乎全是基于时间范围的聚合和筛选的专业个人研究者或小型团队。
核心能力:为时间序列数据优化。写入速度极快,压缩效率高,专门针对“按时间范围查询某指标”这类操作做了索引和存储优化。
- InfluxDB:专门的时序数据库,数据模型围绕“指标(measurement)、标签(tags)、字段(fields)、时间戳”设计。它的查询语言是 Flux 或 InfluxQL,和 SQL 思路不同,需要学习。
- TimescaleDB:基于 PostgreSQL 的插件。这意味着你可以在享受 PostgreSQL 全部功能(复杂的SQL、事务、GIS等)的同时,获得针对时序数据的超表(hypertable)和自动分区管理。这对需要关联时序数据和其他关系数据的场景特别友好。
怎么用 (以 TimescaleDB 为例): 首先,你需要一个运行的 PostgreSQL,并安装 TimescaleDB 扩展。
-- 创建超表,自动按时间分区 CREATE TABLE stock_ticks ( time TIMESTAMPTZ NOT NULL, code TEXT NOT NULL, price DECIMAL, volume BIGINT ); SELECT create_hypertable('stock_ticks', 'time'); -- 插入数据(和PostgreSQL完全一样) INSERT INTO stock_ticks VALUES (NOW(), '000001.SZ', 14.25, 10000); -- 查询最近10分钟某只股票的平均价格(底层会自动扫描相关分区,效率高) SELECT code, AVG(price) FROM stock_ticks WHERE time > NOW() - INTERVAL '10 minutes' AND code = '000001.SZ' GROUP BY code;避坑点:
- 复杂度高:需要安装和维护一个数据库服务,比前面三种方案都重。
- 适用场景专一:如果你的数据不是典型的高频时序数据,或者你需要大量非时序的复杂关联查询,那时序数据库的优势可能不明显,反而引入了不必要的复杂度。
- 学习成本:尤其是 InfluxDB,需要学习其特定的数据模型和查询语言。
结论:高频数据专家的选择。对于绝大多数个人研究者,日频、分钟级数据用前三种方案足以应对。只有当你真的被 tick 数据淹没,并且查询都是时间窗口聚合时,才值得引入专门的时序数据库。TimescaleDB 因为兼容 SQL,是更平滑的入门选择。
3. 从选择到落地:我的配置与操作清单
光知道方案不够,还得知道怎么把它用起来。下面是我根据常见场景整理的配置和操作顺序。
3.1 环境准备与依赖安装
无论选哪个,Python 环境是基础。建议使用conda或venv创建独立环境。
通用基础:
# 创建环境 conda create -n quant_db python=3.10 conda activate quant_db # 核心数据分析库 pip install pandas numpy按方案安装:
- 文件方案:
pip install pyarrow fastparquet(用于 Parquet) 或pip install pyarrow feather-format(用于 Feather)。 - SQLite:Python 标准库自带
sqlite3,无需额外安装。 - DuckDB:
pip install duckdb - TimescaleDB:需要先安装 PostgreSQL 服务器并加载 TimescaleDB 扩展。本地开发可以用 Docker 快速启动:
然后安装 Python 驱动:docker run -d --name timescaledb -p 5432:5432 -e POSTGRES_PASSWORD=password timescale/timescaledb:latest-pg16pip install psycopg2-binary或asyncpg。
- 文件方案:
3.2 数据入库标准流程(以 SQLite/DuckDB 为例)
不要一次性把所有数据塞进去。遵循这个流程,可以避免后期混乱。
设计表结构:
- 时间序列表:至少包含
timestamp/date(主键或索引的一部分)、symbol/code、以及各种价格/因子字段。明确时间字段的精度和时区。 - 维度表:如股票信息表 (
code,name,industry,list_date)。code设为主键。 - 结果表:回测结果表 (
strategy_id,run_date,nav,sharpe,max_drawdown)。
- 时间序列表:至少包含
编写数据清洗与入库脚本:
import pandas as pd import sqlite3 from pathlib import Path def init_database(db_path='quant.db'): conn = sqlite3.connect(db_path) # 创建日频行情表 conn.execute(""" CREATE TABLE IF NOT EXISTS daily_bar ( code TEXT, date DATE, open REAL, high REAL, low REAL, close REAL, volume INTEGER, PRIMARY KEY (code, date) ) """) # 创建股票信息表 conn.execute(""" CREATE TABLE IF NOT EXISTS stock_info ( code TEXT PRIMARY KEY, name TEXT, industry TEXT ) """) conn.commit() return conn def update_daily_data(conn, csv_file_path): """从CSV文件更新日线数据""" df = pd.read_csv(csv_file_path) # 确保列名和数据类型匹配 df['date'] = pd.to_datetime(df['date']).dt.date # 使用 pandas 的 to_sql 方法,if_exists='append' df.to_sql('daily_bar', conn, if_exists='append', index=False) print(f"Updated {len(df)} records.") if __name__ == '__main__': conn = init_database() # 假设你的数据文件在 data/ 目录下 for csv_file in Path('./data').glob('*.csv'): update_daily_data(conn, csv_file) # 创建索引(应在数据插入后创建,效率更高) conn.execute("CREATE INDEX IF NOT EXISTS idx_daily_code_date ON daily_bar (code, date)") conn.commit() conn.close()建立索引:这是影响查询速度最关键的一步。对于时间序列表,必须在
(code, date)上建立复合索引。如果经常按日期范围查全市场,可以单独为date建索引。
3.3 查询模式与性能优化
不同的研究问题,对应不同的查询写法。
场景A:获取单只股票历史行情
-- 高效,因为命中 (code, date) 索引 SELECT * FROM daily_bar WHERE code = '000001.SZ' ORDER BY date;场景B:获取特定日期全市场数据
-- 为 date 单独建索引会更快 SELECT * FROM daily_bar WHERE date = '2023-12-01';场景C:关联查询(股票行情+行业信息)
SELECT d.*, s.industry FROM daily_bar d JOIN stock_info s ON d.code = s.code WHERE d.date BETWEEN '2023-01-01' AND '2023-03-31' AND s.industry = '电子'确保
stock_info.code有主键索引。场景D:复杂聚合计算(计算行业日度平均收益率)
SELECT s.industry, d.date, AVG((d.close - d.open) / d.open) AS avg_daily_return FROM daily_bar d JOIN stock_info s ON d.code = s.code WHERE d.date >= '2023-01-01' GROUP BY s.industry, d.date ORDER BY s.industry, d.date;对于 DuckDB,这种涉及大表关联和聚合的查询优势明显。
性能排查:如果查询变慢,第一反应不是换数据库,而是:
- 检查索引:用
EXPLAIN QUERY PLAN(SQLite) 或EXPLAIN(DuckDB/PostgreSQL) 查看查询计划,确认是否利用了索引。 - 检查数据量:是否已经增长到超出预期?考虑按年份分表或分区。
- 检查查询语句:是否无意中导致了全表扫描?比如对索引列使用了函数
WHERE YEAR(date)=2023。
4. 长期维护与升级路径
研究不是一锤子买卖,数据库方案也需要能跟着你的需求成长。
4.1 日常维护清单
- 定期备份:尤其是 SQLite 的
.db文件或 DuckDB 的数据库文件,直接复制即可。对于文件方案,整个数据目录也要备份。可以考虑用脚本自动备份到网盘或其他硬盘。 - 日志记录:在数据更新脚本中加入日志,记录每次更新的时间、数据源、行数,便于出错时追溯。
- 版本控制:你的数据库 Schema 定义脚本(
CREATE TABLE语句)、数据清洗脚本,都应该用 Git 管理起来。 - 存储监控:留意磁盘空间。特别是高频数据,增长很快。设置警报或定期清理过期数据(如只保留最近3年的tick数据)。
4.2 何时需要考虑升级?
用着用着觉得难受了,可能就是升级的信号:
- 查询慢到无法忍受:在正确使用索引后,简单查询仍需要数秒,且数据量仍在快速增长。
- 内存不足:使用文件或 SQLite 时,Pandas 经常因内存不足崩溃。
- 需要更复杂的分析:需要频繁进行窗口函数、递归查询等高级 SQL 操作,而当前数据库支持不好。
- 并发需求:需要多个回测任务同时写入结果,当前方案锁冲突严重。
平滑升级路径:
- 从 文件 升级到 SQLite/DuckDB:这是最自然的路径。写一个迁移脚本,将 Parquet 文件读入,然后用
.to_sql()或 DuckDB 的CREATE TABLE ... AS SELECT * FROM 'file.parquet'导入。 - 从 SQLite 升级到 DuckDB:DuckDB 可以直接连接并查询 SQLite 数据库文件,迁移成本极低。
- 从 SQLite/DuckDB 升级到 TimescaleDB/PostgreSQL:这一步稍重,需要使用
pgloader或自定义 ETL 脚本进行数据迁移。但换来的是更强大的功能和更好的并发支持。
4.3 最后的建议:从简单开始,逐步演进
不要一开始就追求最“专业”最“强大”的方案。过度设计是个人项目最大的杀手。
我的建议始终是:
- 第零步:用 CSV 或 Parquet 文件把数据整理好,用 Pandas 跑通你的第一个策略回测。这是最快的验证。
- 第一步:当数据多了,查询复杂了,马上切换到SQLite。享受 SQL 的便利,它能支撑你很长一段时间。
- 第二步:当 SQLite 在处理大数据关联查询时开始力不从心,无缝切换到DuckDB。几乎不用改代码,性能立竿见影。
- 第三步:只有当你的研究确实深入到高频领域,或者需要构建一个多用户、高并发的回测服务时,才去考虑TimescaleDB或更专业的方案。
记住,工具是为你服务的。最合适的方案,是那个能让你最少分心在数据管理上,最多精力集中在策略研究上的方案。从今天起,选一个最简单的,先把数据规整地存起来,让研究流程跑起来,这才是最重要的一步。