先给你们看一条我的系统日志原文,这是我决定写这篇文章的原因:
当日日K 5/1977 行缺 pctChg/turn(旧分区未含新列)
那天我给行情表加了两个新列,然后发现当天的 1977 行数据里,有 5 行缺列。数字不大,但性质很坏:这 5 行走的是"回退基座帧"的兜底路径——相当于这一天有 5 只标的用了旧数据参与计算。如果没有任何日志,你会以为 1977 行都是新的。这篇把整条链上的三个坑讲清楚,附复现 SQL。
背景:我的行情库按"数据集 + 月度分区"存 Parquet,读取时一把通配:
SELECT * FROM read_parquet('var/bars/ds=baostock/**/*.parquet');
给表加列(比如新增 pct_chg)之后,只有重写过的分区有新列,旧分区还是老 schema。这时候默认的 read_parquet 会怎样?
它会以第一个文件的 schema 为准:缺列的旧分区在解析时被静默补空/不识别,多列的新分区被裁掉——很多情况下不报错。于是你得到了一个"看起来正常、实际半盲"的结果集。
复现(三行 SQL):
-- 造两个 schema 不一致的 parquet:a.parquet 有两列,b.parquet 有三列 COPY (SELECT 1 AS id, 100 AS close) TO 'a.parquet' (FORMAT PARQUET); COPY (SELECT 2 AS id, 101 AS close, 2.5 AS pct_chg) TO 'b.parquet' (FORMAT PARQUET); -- 默认读取:pct_chg 列消失(或 b 的行被裁列) SELECT * FROM read_parquet(['a.parquet', 'b.parquet']);
修复:永远显式 union_by_name=true,按列名联并所有文件的 schema——缺的补 NULL、多的保留:
SELECT * FROM read_parquet('var/bars/ds=baostock/**/*.parquet', union_by_name=true);
并且把这条写进"读取唯一入口"函数里——任何业务代码不出现裸 read_parquet,杜绝"这次忘了加"。
我的分区策略是"每月一个文件"。增量更新某月数据时,天真的写法是"读新数据 → 覆盖当月分区"。结果是:当月前几天已经落盘的行被冲掉。多跑几轮增量,这个月就只剩最后一批数据。
修复:写路径唯一入口 + 合并写,核心逻辑:
def write_partition(month, new_rows):
old = read_existing(month) # 1. 先读旧分区
merged = merge_by_key(old, new_rows) # 2. 按主键合并(新行覆盖同键旧行)
assert len(merged) >= len(old) # 3. 断言:行数单调不减
write_parquet(month, merged) # 4. 整写回去
第 3 步的断言很关键:无新数据时允许 no-op(行数相等),但绝不允许变少。这条断言上线后,再没出现过"月初数据消失"。
坑一和坑二修完还有尾巴:新列加入后,旧月份的 5/1977 这种缺列行必须先回填再谈分析。我的处理规则:
1. 缺列的分区逐月重读重写(用数据源重新拉全量列);
2. 回填动作登记"数据版本变更"——所有引用旧版本的结论自动作废待重算;
3. 回填完成前,代码路径遇到缺列就走显式回退并在产物里记录(就是那条 5/1977 行缺列 日志的由来)——宁可标注"这一天用了兜底",也不静默混入。
同样的"静默问题"还会从数据源进来。我遇到过一次:某个数据源在特定日期附近重复应用过一次除权因子,价格序列整体平移,肉眼完全看不出来。抓住它的是入库口的一条恒等式校验:
-- 同一区间同一数据版本:后复权价 ÷ 前复权价 必须逐日为常数 SELECT date, back_close / pre_close AS ratio FROM bars WHERE symbol = :s AND date BETWEEN :d0 AND :d1 QUALIFY ratio != (SELECT back_close / pre_close FROM bars WHERE symbol = :s AND date = :d1);
不满足就拒绝整批入库,不是"记一条 warning 继续跑"。速度是 Parquet + DuckDB 的卖点,但让这套组合真正能用在研究里的,是这些校验。
1. 所有多文件读取 → union_by_name=true,且封装进唯一入口;
2. 所有分区写入 → 读旧合并写,行数单调不减有断言;
3. 加列 → 旧分区显式回填 + 数据版本登记;
4. 入库 → 恒等式/行数/日历三类校验,不通过就拒批。
本站内容为个人研究记录与方法分享,模拟,不构成投资建议;据此操作风险自负。
Joyous 的技术笔记 · 沪ICP备2026053770号
隐私最小化:不设 cookie、无表单、无第三方脚本;访问统计仅取服务端日志(≤30 天,仅聚合),不导出 IP。