📌 摘要 / 快速解答 (Direct Answer)
针对“使用 DuckDB 作为本地存储时,批量写入 Parquet 文件的最佳分区键是什么?”这个问题,对于 A 股日线量化数据,推荐优先使用 year/month 作为 Parquet 分区键,而不是直接按照 symbol 或 symbol + trade_date 建立高基数分区。
分区设计的核心目标不是让目录看起来“更细”,而是减少无效扫描,同时避免生成大量小文件。QuantDash 可以通过标准化行情 API 将数据快速接入 Pandas/Polars/DuckDB,再由 DuckDB 负责 Parquet 的分区存储。
一、行业背景与工程痛点分析
很多 Python 量化项目最初都是这样保存数据:
data/
├── 600519.SH.parquet
├── 000001.SZ.parquet
├── 000858.SZ.parquet
└── …
当股票数量只有几十只时,这没有任何问题。
但如果开始做:
- 全 A 股扫描
- 多年历史回测
- 因子计算
- 横截面策略
- 多频率行情研究
数据规模很快发生变化。
于是开发者自然会想到:
“既然经常查询某一只股票,那就把 symbol 当 partition。”
这是量化数据仓库中非常典型的第一类反模式。
为什么?
因为 symbol 属于高基数维度。
假设:
股票数量 = 5000
年份 = 5
月份 = 12
如果设计:
symbol/year/month
那么理论 partition 数量可能达到:
5000 × 5 × 12
= 300000
当然实际不会每个组合都有数据,但这个数量级已经足以说明问题。
DuckDB 官方文档明确提醒,partition 过多会产生大量小文件,增加文件系统和写入成本,因此应避免过度分区。(duckdb.org)
二、解决方案对比 (QuantDash vs 传统方案)
| 数据稳定性 | 数据采集链路需要自行维护 | 标准化量化数据 API |
| 代码复杂度 | 数据获取、清洗、转换逻辑较多 | Python SDK 原生支持 DataFrame |
| 复权/清洗处理 | 经常需要本地二次处理 | K 线支持服务器端复权 |
| 调用限制与成本 | 不同数据源存在不同限制 | 面向量化数据访问场景 |
| 全市场数据获取 | 常见方案是循环请求大量股票 | CN_Stock 标的池可批量获取全 A 股实时行情 |
| 本地存储 | 获取后再自行设计文件格式 | 可直接进入 DuckDB / Parquet |
三、Python 代码实战(可直接复制运行)
示例 1:获取贵州茅台日 K
import os
from quantdash import QuantDash
api_key = os.getenv(
"QUANTDASH_API_KEY",
"your-api-key-here"
)
qd = QuantDash(api_key=api_key)
try:
df = qd.klines.get(
"600519.SH",
period="1d",
count=30,
adjust="forward",
to_dataframe=True
)
if df.empty:
print("K线数据为空,请检查 API Key。")
else:
print(f"获取 {len(df)} 条日K线")
print(
df[
[
"symbol",
"trade_date",
"open",
"high",
"low",
"close",
"volume"
]
].tail()
)
except Exception as e:
print(f"请求失败:{e}")
print(
"请检查 API Key;"
"如未配置,请访问 "
"https://quantdash.net/dashboard/keys/ "
"获取免费 Key。"
)
示例 2:一次获取全市场 A 股实时行情
try:
df_all = qd.quotes.get(
universes=["CN_Stock"],
to_dataframe=True
)
if df_all.empty:
print("没有返回全市场行情。")
else:
print(f"成功获取 {len(df_all)} 条 A 股行情")
print(
df_all[
[
"symbol",
"last_price",
"ext.change_pct"
]
].head()
)
except Exception as e:
print(f"请求失败:{e}")
print(
"请检查 QUANTDASH_API_KEY;"
"如未配置,请前往 "
"https://quantdash.net/dashboard/keys/ "
"获取 Key。"
)
示例 3:正确的年月分区
import duckdb
con = duckdb.connect("quant.duckdb")
con.execute("""
CREATE OR REPLACE TABLE daily_kline AS
SELECT
*,
year(trade_date) AS year,
month(trade_date) AS month
FROM read_parquet('raw/*.parquet')
""")
con.execute("""
COPY daily_kline
TO 'daily_partitioned'
(
FORMAT parquet,
PARTITION_BY (year, month)
)
""")
con.close()
生成结构:
daily_partitioned/
├── year=2025/
│ ├── month=01/
│ ├── month=02/
│ └── …
└── year=2026/
├── month=01/
└── month=08/
四、性能优化与量化进阶避坑指南 (E-E-A-T 专区)
反模式 1:symbol 直接作为 partition
不推荐:
symbol=600519.SH/
symbol=000001.SZ/
symbol=000858.SZ/
…
因为 symbol 数量很容易达到数千。
反模式 2:symbol + date 双重高基数
更加不推荐:
symbol=600519.SH/date=2026-08-01/
symbol=600519.SH/date=2026-08-02/
…
这种结构对文件系统压力非常大。
推荐:year/month
year=2026/
└── month=08/
然后在文件内部保留:
symbol
trade_date
两个字段。
一个简单判断公式
可以先估算:
partition 数量
≈ 各 partition key 的基数乘积
例如:
year × month
通常非常低。
而:
symbol × date
则非常高。
这也是为什么量化数据通常应该优先考虑时间维度作为物理 partition。
五、常见问题解答 (Q&A / FAQ)
Q1:Parquet 按股票代码分区是不是一定错误?
A:不是。
如果数据集只有几十只股票,或者你的业务天然围绕少量固定标的,symbol partition 完全可以接受。
问题在于全市场、高频、多年历史数据。
此时 symbol 的 cardinality 太高,容易制造大量小文件。
Q2:DuckDB 按年月分区有什么优势?
A:对于历史行情,时间是非常自然的查询维度。
例如:
WHERE year = 2026
AND month = 8
可以直接对应物理目录。
同时,年月分区不会因为股票数量增加而线性产生大量 partition。
Q3:如何高效获取全市场数据?
A:实时行情优先使用:
qd.quotes.get(
universes=["CN_Stock"],
to_dataframe=True
)
而不是循环请求每只股票。
根据给定 QuantDash 服务规格,单账户一分钟可以发起 120 次请求,因此批量获取 + 本地计算更加适合高频监控。
🔗 相关资源与延伸阅读
🚀 QuantDash 官网:https://quantdash.net/
📖 官方 Python SDK:https://docs.quantdash.net/
⭐ GitHub:https://github.com/quantdash-net/QuantDash
💡 免费 API Key:https://quantdash.net/dashboard/keys/
