據(jù)庫選型指南:從文件到時序數(shù)據(jù)庫的實戰(zhàn)對比)
1. 先想清楚你的數(shù)據(jù)到底要存什么、怎么用個人量化研究最怕的不是模型不靈而是數(shù)據(jù)沒管好。模型可以換策略可以調(diào)但數(shù)據(jù)一旦亂了或者查詢慢到跑一次回測要等半天整個研究流程就卡住了。所以選數(shù)據(jù)庫不是看哪個技術(shù)最火而是先想清楚你的數(shù)據(jù)長什么樣、你打算怎么用它。我見過很多新手一上來就問“用 MySQL 還是 PostgreSQL”這其實問錯了。對于個人研究者你的數(shù)據(jù)場景通常很明確時間序列數(shù)據(jù)是絕對核心。你的股票K線、期貨tick、因子值、信號都是帶時間戳的。除此之外你還需要存一些維度數(shù)據(jù)比如股票代碼列表、行業(yè)分類、財務(wù)指標快照。最后你的研究中間結(jié)果比如每次回測的凈值曲線、績效指標也需要有個地方放。所以選擇的核心矛盾是時間序列的寫入和查詢效率與維度數(shù)據(jù)的關(guān)聯(lián)查詢便利性之間的權(quán)衡。一個只存時間序列的專用數(shù)據(jù)庫查詢速度可能飛快但你想把行情數(shù)據(jù)和財務(wù)數(shù)據(jù)關(guān)聯(lián)起來做分析可能就得自己寫一堆代碼拼接很麻煩。一個通用的關(guān)系型數(shù)據(jù)庫關(guān)聯(lián)查詢很方便但面對每天幾百萬條的tick數(shù)據(jù)性能可能成為瓶頸。我的建議是先別急著定方案花十分鐘把你的數(shù)據(jù)需求列清楚數(shù)據(jù)量級是日頻、分鐘級還是tick級未來一年大概有多少條記錄查詢模式是經(jīng)常按股票代碼查一段時間的歷史行情還是需要做復(fù)雜的多表關(guān)聯(lián)比如找出市盈率低于行業(yè)平均且最近有放量上漲的股票分析工具你主要用 Python 的 Pandas 做分析還是用 SQL 直接跑你的回測框架對數(shù)據(jù)接口有什么偏好硬件環(huán)境數(shù)據(jù)是放在你自己的電腦上還是云服務(wù)器你的機器內(nèi)存、磁盤特別是SSD有多大想清楚這些我們再來看看有哪些選項以及它們分別適合什么樣的“個人研究者”。2. 從最簡單到最專業(yè)四種主流方案的實戰(zhàn)對比市面上方案很多但個人研究者沒必要全都折騰一遍。我根據(jù)復(fù)雜度和適用場景把它們歸為四類。你可以直接對號入座。2.1 方案一文件 Pandas (HDF5/Parquet/Feather)適合誰剛剛?cè)腴T數(shù)據(jù)量不大比如只研究A股日頻數(shù)據(jù)幾年下來也就幾十萬條追求極簡啟動不想維護任何數(shù)據(jù)庫服務(wù)的研究者。核心能力這不是數(shù)據(jù)庫而是序列化文件格式。你的整個數(shù)據(jù)庫就是一個或幾個文件。用 Pandas 讀寫在內(nèi)存里操作思路最直接。HDF5(.h5): 通過pandas.HDFStore使用。可以存儲帶類型的 DataFrame支持分塊讀取查詢速度不錯。但文件內(nèi)部結(jié)構(gòu)復(fù)雜一旦損壞可能難以修復(fù)且跨語言支持一般。Parquet(.parquet): Apache 生態(tài)的列式存儲格式。最大優(yōu)點是壓縮比高節(jié)省磁盤空間而且被 Spark、DuckDB 等很多新工具原生支持。用pandas.read_parquet讀取很方便。Feather(.feather): 設(shè)計目標就是快速讀寫在 Pandas 和 R 之間交換數(shù)據(jù)。它幾乎就是內(nèi)存數(shù)據(jù)的直接鏡像所以讀寫速度極快但壓縮率不如 Parquet。怎么用import pandas as pd import numpy as np # 假設(shè)你有一個日頻行情 DataFrame: df_daily # 保存 df_daily.to_parquet(stock_daily.parquet) # 節(jié)省空間 # 或 df_daily.to_feather(stock_daily.feather) # 追求讀寫速度 # 讀取可以只讀部分列 df pd.read_parquet(stock_daily.parquet, columns[code, date, close]) # 按日期和代碼篩選 (在內(nèi)存中) filtered df[(df[date] 2023-01-01) (df[code] 000001.SZ)]避坑點全部數(shù)據(jù)加載到內(nèi)存這是最大限制。如果你的數(shù)據(jù)量超過內(nèi)存就會卡死。Parquet 雖支持分塊讀取但用 Pandas 處理大文件依然不便。并發(fā)訪問差文件一般不支持多進程同時寫入回測時如果想并行計算并寫結(jié)果需要小心處理鎖或拆分文件。復(fù)雜查詢靠手動所有關(guān)聯(lián)、聚合、篩選邏輯都需要你用 Pandas 代碼寫出來不如 SQL 直觀。結(jié)論入門首選快速驗證想法。當你的數(shù)據(jù)量在幾個GB以內(nèi)且分析模式固定時用 Parquet 或 Feather 能讓你最快跑起來。一旦數(shù)據(jù)增長或查詢變復(fù)雜就要考慮升級。2.2 方案二SQLite適合誰需要關(guān)系型數(shù)據(jù)庫的便利性比如用 SQL 做復(fù)雜關(guān)聯(lián)查詢但又不想安裝和配置 MySQL/PostgreSQL 這類獨立服務(wù)的研究者。數(shù)據(jù)量在幾十GB級別以下通常都能勝任。核心能力SQLite 是一個庫不是一個服務(wù)器。你的整個數(shù)據(jù)庫就是一個.db文件。它支持標準的 SQL具備事務(wù)、索引等核心功能但無需管理服務(wù)進程零配置。怎么用import sqlite3 import pandas as pd # 連接數(shù)據(jù)庫文件不存在會自動創(chuàng)建 conn sqlite3.connect(quant_research.db) # 將Pandas DataFrame寫入表 df_daily.to_sql(stock_daily, conn, if_existsreplace, indexFalse) # 用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()性能關(guān)鍵一定要建索引對于時間序列查詢在(code, date)上建立復(fù)合索引速度提升是數(shù)量級的。CREATE INDEX idx_daily_code_date ON stock_daily (code, date);避坑點寫入并發(fā)弱SQLite 在某一時刻只允許一個寫入操作。如果你的回測是并行任務(wù)同時寫入結(jié)果可能會遇到“database is locked”錯誤。解決方案是讓每個子進程寫入獨立的臨時文件最后合并。內(nèi)存模式你可以用:memory:創(chuàng)建純內(nèi)存數(shù)據(jù)庫速度極快適合中間計算但程序關(guān)閉數(shù)據(jù)就消失記得持久化。數(shù)據(jù)類型寬松SQLite 數(shù)據(jù)類型比較靈活有時可能導(dǎo)致類型推斷錯誤在創(chuàng)建表時最好顯式定義字段類型。結(jié)論個人研究的“瑞士軍刀”。在數(shù)據(jù)量未達到TB級且你需要頻繁使用SQL做關(guān)聯(lián)分析時SQLite 是平衡便利與能力的絕佳選擇。它的.db文件也方便備份和遷移。2.3 方案三DuckDB適合誰處理的數(shù)據(jù)量超過了 Pandas 內(nèi)存限制比如幾十GB需要進行復(fù)雜的交互式分析或連接多個大型數(shù)據(jù)集但又覺得部署傳統(tǒng)數(shù)倉如ClickHouse太重的個人研究者。核心能力這是一個進程內(nèi)分析型數(shù)據(jù)庫。它像 SQLite 一樣以庫的形式嵌入你的應(yīng)用但引擎是為分析型查詢OLAP設(shè)計的擅長處理海量數(shù)據(jù)的聚合、連接操作。它可以直接讀寫 Parquet/CSV 文件無需先“導(dǎo)入”數(shù)據(jù)。怎么用import duckdb # 連接內(nèi)存或文件 conn duckdb.connect(quant.duckdb) # 或 :memory: # 1. 直接查詢Parquet文件無需導(dǎo)入 query SELECT code, date, volume FROM stock_daily.parquet WHERE date 2023-01-01 AND volume 10000000 df conn.execute(query).df() # 直接返回DataFrame # 2. 也可以創(chuàng)建表并持久化 conn.execute(CREATE TABLE daily AS SELECT * FROM stock_daily.parquet) # 3. 執(zhí)行復(fù)雜關(guān)聯(lián)查詢即使數(shù)據(jù)在多個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()性能關(guān)鍵DuckDB 會自動并行化查詢以利用多核CPU。對于超大數(shù)據(jù)集確保你的機器有足夠內(nèi)存或者使用它的溢出到磁盤的功能。避坑點不是事務(wù)型數(shù)據(jù)庫雖然支持事務(wù)但它的強項是分析不適合高頻率、小事務(wù)的寫入場景比如實時交易記錄。更適合存儲清洗后的歷史數(shù)據(jù)和回測結(jié)果。社區(qū)和工具生態(tài)相比 MySQL/PostgreSQL其管理工具和客戶端支持較少但作為嵌入式庫這通常不是問題。仍在快速發(fā)展雖然核心很穩(wěn)定但一些高級功能可能還在演進中。結(jié)論個人量化分析的“性能加速器”。當你受限于 Pandas 內(nèi)存又厭倦了 SQLite 處理大數(shù)據(jù)連接時的緩慢DuckDB 幾乎是無縫升級的最佳選擇。特別是它能直接查詢 Parquet 文件讓數(shù)據(jù)管理流程變得極其簡潔。2.4 方案四時序數(shù)據(jù)庫 (InfluxDB, TimescaleDB)適合誰數(shù)據(jù)源是超高頻率的時序數(shù)據(jù)如每秒數(shù)千條的tick數(shù)據(jù)、分鐘級傳感器數(shù)據(jù)并且查詢模式幾乎全是基于時間范圍的聚合和篩選的專業(yè)個人研究者或小型團隊。核心能力為時間序列數(shù)據(jù)優(yōu)化。寫入速度極快壓縮效率高專門針對“按時間范圍查詢某指標”這類操作做了索引和存儲優(yōu)化。InfluxDB專門的時序數(shù)據(jù)庫數(shù)據(jù)模型圍繞“指標(measurement)、標簽(tags)、字段(fields)、時間戳”設(shè)計。它的查詢語言是 Flux 或 InfluxQL和 SQL 思路不同需要學(xué)習(xí)。TimescaleDB基于 PostgreSQL 的插件。這意味著你可以在享受 PostgreSQL 全部功能復(fù)雜的SQL、事務(wù)、GIS等的同時獲得針對時序數(shù)據(jù)的超表hypertable和自動分區(qū)管理。這對需要關(guān)聯(lián)時序數(shù)據(jù)和其他關(guān)系數(shù)據(jù)的場景特別友好。怎么用 (以 TimescaleDB 為例) 首先你需要一個運行的 PostgreSQL并安裝 TimescaleDB 擴展。-- 創(chuàng)建超表自動按時間分區(qū) CREATE TABLE stock_ticks ( time TIMESTAMPTZ NOT NULL, code TEXT NOT NULL, price DECIMAL, volume BIGINT ); SELECT create_hypertable(stock_ticks, time); -- 插入數(shù)據(jù)和PostgreSQL完全一樣 INSERT INTO stock_ticks VALUES (NOW(), 000001.SZ, 14.25, 10000); -- 查詢最近10分鐘某只股票的平均價格底層會自動掃描相關(guān)分區(qū)效率高 SELECT code, AVG(price) FROM stock_ticks WHERE time NOW() - INTERVAL 10 minutes AND code 000001.SZ GROUP BY code;避坑點復(fù)雜度高需要安裝和維護一個數(shù)據(jù)庫服務(wù)比前面三種方案都重。適用場景專一如果你的數(shù)據(jù)不是典型的高頻時序數(shù)據(jù)或者你需要大量非時序的復(fù)雜關(guān)聯(lián)查詢那時序數(shù)據(jù)庫的優(yōu)勢可能不明顯反而引入了不必要的復(fù)雜度。學(xué)習(xí)成本尤其是 InfluxDB需要學(xué)習(xí)其特定的數(shù)據(jù)模型和查詢語言。結(jié)論高頻數(shù)據(jù)專家的選擇。對于絕大多數(shù)個人研究者日頻、分鐘級數(shù)據(jù)用前三種方案足以應(yīng)對。只有當你真的被 tick 數(shù)據(jù)淹沒并且查詢都是時間窗口聚合時才值得引入專門的時序數(shù)據(jù)庫。TimescaleDB 因為兼容 SQL是更平滑的入門選擇。3. 從選擇到落地我的配置與操作清單光知道方案不夠還得知道怎么把它用起來。下面是我根據(jù)常見場景整理的配置和操作順序。3.1 環(huán)境準備與依賴安裝無論選哪個Python 環(huán)境是基礎(chǔ)。建議使用conda或venv創(chuàng)建獨立環(huán)境。通用基礎(chǔ)# 創(chuàng)建環(huán)境 conda create -n quant_db python3.10 conda activate quant_db # 核心數(shù)據(jù)分析庫 pip install pandas numpy按方案安裝文件方案pip install pyarrow fastparquet(用于 Parquet) 或pip install pyarrow feather-format(用于 Feather)。SQLitePython 標準庫自帶sqlite3無需額外安裝。DuckDBpip install duckdbTimescaleDB需要先安裝 PostgreSQL 服務(wù)器并加載 TimescaleDB 擴展。本地開發(fā)可以用 Docker 快速啟動docker run -d --name timescaledb -p 5432:5432 -e POSTGRES_PASSWORDpassword timescale/timescaledb:latest-pg16然后安裝 Python 驅(qū)動pip install psycopg2-binary或asyncpg。3.2 數(shù)據(jù)入庫標準流程以 SQLite/DuckDB 為例不要一次性把所有數(shù)據(jù)塞進去。遵循這個流程可以避免后期混亂。設(shè)計表結(jié)構(gòu)時間序列表至少包含timestamp/date(主鍵或索引的一部分)、symbol/code、以及各種價格/因子字段。明確時間字段的精度和時區(qū)。維度表如股票信息表 (code,name,industry,list_date)。code設(shè)為主鍵。結(jié)果表回測結(jié)果表 (strategy_id,run_date,nav,sharpe,max_drawdown)。編寫數(shù)據(jù)清洗與入庫腳本import pandas as pd import sqlite3 from pathlib import Path def init_database(db_pathquant.db): conn sqlite3.connect(db_path) # 創(chuàng)建日頻行情表 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) ) ) # 創(chuàng)建股票信息表 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文件更新日線數(shù)據(jù) df pd.read_csv(csv_file_path) # 確保列名和數(shù)據(jù)類型匹配 df[date] pd.to_datetime(df[date]).dt.date # 使用 pandas 的 to_sql 方法if_existsappend df.to_sql(daily_bar, conn, if_existsappend, indexFalse) print(fUpdated {len(df)} records.) if __name__ __main__: conn init_database() # 假設(shè)你的數(shù)據(jù)文件在 data/ 目錄下 for csv_file in Path(./data).glob(*.csv): update_daily_data(conn, csv_file) # 創(chuàng)建索引應(yīng)在數(shù)據(jù)插入后創(chuàng)建效率更高 conn.execute(CREATE INDEX IF NOT EXISTS idx_daily_code_date ON daily_bar (code, date)) conn.commit() conn.close()建立索引這是影響查詢速度最關(guān)鍵的一步。對于時間序列表必須在(code, date)上建立復(fù)合索引。如果經(jīng)常按日期范圍查全市場可以單獨為date建索引。3.3 查詢模式與性能優(yōu)化不同的研究問題對應(yīng)不同的查詢寫法。場景A獲取單只股票歷史行情-- 高效因為命中 (code, date) 索引 SELECT * FROM daily_bar WHERE code 000001.SZ ORDER BY date;場景B獲取特定日期全市場數(shù)據(jù)-- 為 date 單獨建索引會更快 SELECT * FROM daily_bar WHERE date 2023-12-01;場景C關(guān)聯(lián)查詢股票行情行業(yè)信息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復(fù)雜聚合計算計算行業(yè)日度平均收益率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這種涉及大表關(guān)聯(lián)和聚合的查詢優(yōu)勢明顯。性能排查如果查詢變慢第一反應(yīng)不是換數(shù)據(jù)庫而是檢查索引用EXPLAIN QUERY PLAN(SQLite) 或EXPLAIN(DuckDB/PostgreSQL) 查看查詢計劃確認是否利用了索引。檢查數(shù)據(jù)量是否已經(jīng)增長到超出預(yù)期考慮按年份分表或分區(qū)。檢查查詢語句是否無意中導(dǎo)致了全表掃描比如對索引列使用了函數(shù)WHERE YEAR(date)2023。4. 長期維護與升級路徑研究不是一錘子買賣數(shù)據(jù)庫方案也需要能跟著你的需求成長。4.1 日常維護清單定期備份尤其是 SQLite 的.db文件或 DuckDB 的數(shù)據(jù)庫文件直接復(fù)制即可。對于文件方案整個數(shù)據(jù)目錄也要備份??梢钥紤]用腳本自動備份到網(wǎng)盤或其他硬盤。日志記錄在數(shù)據(jù)更新腳本中加入日志記錄每次更新的時間、數(shù)據(jù)源、行數(shù)便于出錯時追溯。版本控制你的數(shù)據(jù)庫 Schema 定義腳本CREATE TABLE語句、數(shù)據(jù)清洗腳本都應(yīng)該用 Git 管理起來。存儲監(jiān)控留意磁盤空間。特別是高頻數(shù)據(jù)增長很快。設(shè)置警報或定期清理過期數(shù)據(jù)如只保留最近3年的tick數(shù)據(jù)。4.2 何時需要考慮升級用著用著覺得難受了可能就是升級的信號查詢慢到無法忍受在正確使用索引后簡單查詢?nèi)孕枰獢?shù)秒且數(shù)據(jù)量仍在快速增長。內(nèi)存不足使用文件或 SQLite 時Pandas 經(jīng)常因內(nèi)存不足崩潰。需要更復(fù)雜的分析需要頻繁進行窗口函數(shù)、遞歸查詢等高級 SQL 操作而當前數(shù)據(jù)庫支持不好。并發(fā)需求需要多個回測任務(wù)同時寫入結(jié)果當前方案鎖沖突嚴重。平滑升級路徑從 文件 升級到 SQLite/DuckDB這是最自然的路徑。寫一個遷移腳本將 Parquet 文件讀入然后用.to_sql()或 DuckDB 的CREATE TABLE ... AS SELECT * FROM file.parquet導(dǎo)入。從 SQLite 升級到 DuckDBDuckDB 可以直接連接并查詢 SQLite 數(shù)據(jù)庫文件遷移成本極低。從 SQLite/DuckDB 升級到 TimescaleDB/PostgreSQL這一步稍重需要使用pgloader或自定義 ETL 腳本進行數(shù)據(jù)遷移。但換來的是更強大的功能和更好的并發(fā)支持。4.3 最后的建議從簡單開始逐步演進不要一開始就追求最“專業(yè)”最“強大”的方案。過度設(shè)計是個人項目最大的殺手。我的建議始終是第零步用 CSV 或 Parquet 文件把數(shù)據(jù)整理好用 Pandas 跑通你的第一個策略回測。這是最快的驗證。第一步當數(shù)據(jù)多了查詢復(fù)雜了馬上切換到SQLite。享受 SQL 的便利它能支撐你很長一段時間。第二步當 SQLite 在處理大數(shù)據(jù)關(guān)聯(lián)查詢時開始力不從心無縫切換到DuckDB。幾乎不用改代碼性能立竿見影。第三步只有當你的研究確實深入到高頻領(lǐng)域或者需要構(gòu)建一個多用戶、高并發(fā)的回測服務(wù)時才去考慮TimescaleDB或更專業(yè)的方案。記住工具是為你服務(wù)的。最合適的方案是那個能讓你最少分心在數(shù)據(jù)管理上最多精力集中在策略研究上的方案。從今天起選一個最簡單的先把數(shù)據(jù)規(guī)整地存起來讓研究流程跑起來這才是最重要的一步。