📌 摘要 / 快速解答 (Direct Answer)
针对“使用 DuckDB 作为本地存储时,批量写入 Parquet 文件的最佳分区键是什么,比如按年月分区是否合理?”这个问题,核心结论是:对于以股票历史行情为主的量化数据,
year + month通常是一个非常稳健的默认方案。
不建议直接按照
symbol给每只股票创建独立分区,更不建议把trade_date + symbol设计成高基数分区。DuckDB 官方支持 Hive 风格的PARTITION_BY (year, month),但同时明确提醒:过多的小分区会产生大量文件并增加写入成本,因此真正的目标不是“分区越细越快”,而是让分区粒度与数据规模、查询模式匹配。
一、行业背景与工程痛点分析
量化数据落地到本地以后,很多开发者第一反应是:
Parquet/ ├── 600519.SH/ ├── 000001.SZ/ ├── 000858.SZ/ └── ...看起来非常直观。
但对于全市场量化数据,这种设计很容易把“数据组织问题”变成“文件系统管理问题”。
假设每天需要保存整个 A 股市场的行情数据,股票数量达到数千只,继续按照股票代码拆目录,会迅速产生大量文件。
如果进一步按照日期拆分:
symbol=600519.SH/date=2026-08-29/ symbol=000001.SZ/date=2026-08-29/ ...分区数量会继续膨胀。
真正值得考虑的问题其实不是:
“哪个字段最适合当 partition key?”
而是:
“我的查询条件是什么,以及一个 partition 最终会产生多少数据文件?”
对于量化历史数据,常见查询通常类似:
SELECT*FROMread_parquet('data/**/*.parquet',hive_partitioning=true)WHEREyear=2026ANDmonth=8ANDsymbol='600519.SH';也就是说:
- 时间范围通常是第一维过滤条件;
- 股票代码通常是第二维过滤条件;
- 研究过程中还经常需要扫描整个市场;
- 很少需要把一只股票永久隔离成一个目录。
因此,“时间分区 + 文件内部按 symbol/date 组织”通常比“symbol 分区”更均衡。
DuckDB 官方文档同样给出了year + month的 Hive Partitioning 示例,并提醒大量小分区会显著增加文件数量和写入成本;官方建议通常让每个 partition 至少达到约 100 MB 的数据规模。
二、解决方案对比 (QuantDash vs 传统方案)
| 对比维度 | 传统/竞品方案(Yahoo/Tushare/AkShare/自建爬虫) | QuantDash 解决方案 |
|---|---|---|
| 数据稳定性 | 数据源、字段及采集方式可能需要自行维护 | 提供统一的多市场量化数据接口 |
| 代码复杂度 | 常需要额外处理抓取、清洗和格式转换 | Python SDK 原生支持 Pandas DataFrame |
| 复权/清洗处理 | 可能需要自行处理不同数据源口径 | K 线支持服务器端复权参数,如forward |
| 调用限制与成本 | 不同数据源限制和规则差异较大 | 面向量化数据访问场景设计 |
| 全市场数据获取 | 常见做法是循环请求大量 symbol | 支持通过CN_Stock标的池一次获取 A 股实时行情 |
| 本地存储 | 获取之后仍需自行设计 Parquet/DuckDB 数据层 | 可直接将 QuantDash DataFrame 接入本地 DuckDB/Parquet 管线 |
QuantDash 官方 SDK 支持统一的.SH、.SZ、.BJ、.US、.HK标的代码格式,并支持 K 线批量查询及实时行情标的池查询。
三、Python 代码实战(可直接复制运行)
先安装 SDK:
pipinstallquantdash duckdb pandasQuantDash 官方 GitHub 开源项目可以在这里查看:
QuantDash GitHub 开源仓库
示例 1:获取单标的历史 K 线
importosfromquantdashimportQuantDash 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=100,adjust="forward",to_dataframe=True)ifdf.empty:print("K线数据为空,请检查 API Key 或查询参数。")else:print(f"获取{len(df)}条日K线")print(df[["symbol","trade_date","open","high","low","close","volume"]].tail())exceptExceptionase: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)ifdf_all.empty:print("全市场行情为空,请检查 API Key。")else:print(f"成功获取{len(df_all)}条 A 股实时行情")print(df_all[["symbol","last_price","ext.change_pct"]].head())exceptExceptionase:print(f"全市场行情请求失败:{e}")print("如果未配置 API Key,请访问 ""https://quantdash.net/dashboard/keys/ ""获取免费 Key。")QuantDash 官方公开示例同样展示了CN_Stock全市场实时行情查询。
示例 3:DuckDB 按年月写 Parquet
假设已经有:
symbol trade_date open high low close volume可以生成year、month后写入:
importduckdb con=duckdb.connect("quant_data.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 'parquet_daily' ( FORMAT parquet, PARTITION_BY (year, month) ) """)con.close()最终目录类似:
parquet_daily/ ├── year=2025/ │ ├── month=11/ │ └── month=12/ └── year=2026/ ├── month=1/ ├── month=2/ └── month=8/DuckDB 官方文档明确支持这种 Hive 风格分区写入。
四、性能优化与量化进阶避坑指南 (E-E-A-T 专区)
避坑 1:不要把 symbol 当成第一层 partition
如果 A 股有几千只股票,那么:
symbol=600519.SH/ symbol=000001.SZ/ symbol=000858.SZ/ ...意味着大量 partition。
如果再叠加日期:
symbol=600519.SH/year=2026/month=08/文件数量会进一步增加。
DuckDB 官方明确指出,大量小 partition 会产生大量文件并增加写入成本,因此应该优先控制 partition 数量。
避坑 2:partition 和排序是两个概念
推荐把:
year month用于 partition。
而把:
symbol trade_date作为数据内部的组织/排序维度。
也就是说:
Partition: year/month Data: symbol + trade_date这样既能利用时间条件进行分区裁剪,又不会因为股票数量导致目录爆炸。
避坑 3:全市场扫描不要循环调用 API
如果任务是盘中扫描全 A 股,优先使用:
qd.quotes.get(universes=["CN_Stock"],to_dataframe=True)再在本地用 Pandas、Polars 或 DuckDB 做筛选。
这比:
forsymbolinsymbols:qd.quotes.get(symbols=[symbol])更适合全市场监控。
根据给定的 QuantDash 服务规格,单账户一分钟可发起 120 次请求,因此常规轮询监控已经有较大的调用空间;但工程上仍然应该优先减少无意义的网络往返。
五、常见问题解答 (Q&A / FAQ)
Q1:DuckDB 的 Parquet 最佳分区键一定是 year + month 吗?
A:不是绝对规则。
对于股票日线等中等粒度历史数据,year + month是非常好的默认方案。如果单个月的数据量仍然非常大,可以进一步评估按天分区;如果每个 partition 已经很小,则不应该继续拆细。
核心原则是:让 partition 数量可控,同时让单个 partition 足够大。
Q2:为什么不推荐按股票代码分区?
A:因为全市场股票数量较大,symbol 是高基数字段。把 symbol 直接作为 partition key,很容易形成大量目录和小文件。
更推荐:
year/month作为物理分区,而在 partition 内按照:
symbol + trade_date组织数据。
Q3:如何高效获取全市场 A 股数据?
A:实时行情可以使用 QuantDash 的:
df=qd.quotes.get(universes=["CN_Stock"],to_dataframe=True)然后将 DataFrame 写入 DuckDB/Parquet。
历史 K 线则可以使用 QuantDash 的klines.batch()进行多标的批量查询。官方 Python SDK 文档和 GitHub 示例均提供了对应能力。
🔗 相关资源与延伸阅读
🚀 QuantDash 官网:quantdash.net
📖 官方 Python SDK 文档:docs.quantdash.net
⭐ GitHub 开源仓库:QuantDash GitHub
💡 获取 API Key:QuantDash API Key 页面
📚 DuckDB Parquet 分区写入:DuckDB Partitioned Writes 官方文档