欢迎光临
我们一直在努力

DuckDB 写 Parquet 最佳分区键怎么选?量化数据千万别把 symbol 当成 partition

📌 摘要 / 快速解答 (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 传统方案)

对比维度传统/竞品方案 (Yahoo/Tushare/AkShare/自建爬虫)QuantDash 解决方案
数据稳定性 数据采集链路需要自行维护 标准化量化数据 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/

赞(0)
未经允许不得转载:171主机测评 » DuckDB 写 Parquet 最佳分区键怎么选?量化数据千万别把 symbol 当成 partition
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址