Parquet Variant × DuckDB:列存半结构化数据的正确打开方式(下)
Parquet Variant × DuckDB:列存半结构化数据的正确打开方式
本文为上篇《Parquet Variant × DuckDB:列存半结构化数据的正确打开方式》的下篇,继续讨论实战建议、生态对比与总结。
实战建议:双层存储
如果你打算在生产中使用 Variant,推荐"双层存储"模式。
1原始层(Variant) 提取层(STRUCT)
2┌──────────────┐ ┌────────────────┐
3│ raw_events │ │ core_events │
4│ ├─ id │ ───→ │ ├─ id │
5│ └─ payload │ (ETL) │ ├─ event │
6│ (VARIANT) │ │ ├─ user_id │
7└──────────────┘ │ ├─ timestamp │
8 │ └─ payload │ ← 保留原始
9 │ (VARIANT) │
10 └────────────────┘
1-- 原始层:直接接收动态 schema 数据
2CREATE TABLE raw_events AS
3SELECT * FROM read_parquet('logs/*.parquet');
4
5-- 提取层:高频字段物化为静态列
6CREATE TABLE core_events AS
7SELECT
8 id,
9 payload.event::VARCHAR AS event,
10 payload.user::INTEGER AS user_id,
11 payload.ts::TIMESTAMP AS timestamp,
12 payload -- 保留完整 Variant 以便未来提取新字段
13FROM raw_events;
14
15-- 或者用 GENERATED 列
16CREATE TABLE events_v2 (
17 id INTEGER,
18 payload VARIANT,
19 event VARCHAR GENERATED ALWAYS AS (payload.event::VARCHAR),
20 user_id INTEGER GENERATED ALWAYS AS (payload.user::INTEGER)
21);
这么做的好处是:原始层享受 Variant 的写入灵活性和 schema 弹性,提取层享受 STRUCT 的极致查询性能。
生态支持
Variant 的跨引擎互操作已经形成共识:
| 引擎/平台 | 支持状态 |
|---|---|
| Apache Spark 4.0 | ✅ 读写 + shredding |
| Snowflake | ✅ 原生,原生 |
| DuckDB v1.5+ | ✅ 全链路 |
| Delta Lake | ✅ 官方协议 |
| Apache Iceberg v3 | ✅ 扩展类型 |
| parquet-java | ✅ 参考实现 1.16.0 |
| Arrow C++ / cuDF / Polars | ❌ 待支持 |
DuckDB 可以直接读取 Snowflake 产生的 shredded Variant Parquet,v1.5.3+ 可读 Iceberg V3 的 Variant 列。这意味着一份 Variant 数据可以在 Snowflake、Spark、DuckDB 之间自由流转。
总结
Parquet Variant 不是银弹,但对于处理动态 schema 数据的团队来说,它是目前最正确的方向。
它在理念上解决了 JSON 字符串的四个根本问题:
- 查询时不需要重新 parse
- 类型信息在写入时就保留
- 字段名不重复存储
- 支持列式优化(shredding、剪枝、下推)
DuckDB 的集成速度很快——从首个 PR 到发布只用了 8 个月。v1.5 阶段的实现"可用但偏早期",v1.6 的原生 Variant 类型和性能优化值得期待。
如果你面对的是日志分析、API 数据湖、事件流处理这类动态 schema 场景,现在就可以用"双层存储"模式上车。高频查询走提取列,灵活查询走原始 Variant。两全其美。
格式层面最好的半结构化类型,引擎层面最快的 OLAP 分析引擎——Variant × DuckDB 是 2026 年数据工程领域最具影响力的组合之一。不是因为它完美,而是因为它十年来第一次给出了一个"正确"的答案。