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

📌 摘要 / 快速解答 (Direct Answer)

针对“使用 DuckDB 作为本地存储时,批量写入 Parquet 文件的最佳分区键是什么?”这个问题,对于 A 股日线量化数据,推荐优先使用 year/month 作为 Parquet 分区键,而不是直接按照 symbolsymbol + 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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值