概述
2026 年 3 月,DuckDB 发布了 v1.5 ‘Variegata’(以新西兰天堂鸭命名),这是自 v1.0 以来最具变革意义的大版本。最新版本 v1.5.3 已修复多项关键 bug。

一、数据结构进化:VARIANT + GEOMETRY
1.1 VARIANT 类型:一行存下整个 JSON 宇宙
v1.5 终于实现了完整的 VARIANT 列类型,支持非结构化数据的原生存储与查询。这意味着你可以把嵌套 JSON、API 返回的复杂结构直接塞进表里,用 SQL 直接提取字段——不再需要 Spark 或 Flink 做预处理。
实战场景:电商用户行为分析
-- 创建含 VARIANT 列的表
CREATE TABLE user_events (
event_id VARCHAR,
user_id BIGINT,
event_time TIMESTAMP,
payload VARIANT -- 整个事件载荷直接存进去
);
-- 模拟写入嵌套 JSON(实际中来自日志文件)
COPY user_events FROM 'events.json' (FORMAT JSON);
-- 直接从 VARIANT 中提取嵌套字段
SELECT
user_id,
payload:->>'action' AS action,
payload:->'metadata:->'device' AS device_info,
payload:->'metadata:->'location:->'lat' AS lat
FROM user_events
WHERE payload:->>'action' = 'purchase';
Python 侧调用示例:
import duckdb
con = duckdb.ATTACH ("events.duckdb")
# 直接读取 JSON 到 VARIANT 列
con.execute("""
CREATE TABLE raw_events AS
SELECT
id, user_id, timestamp,
CAST(json AS VARIANT) AS payload
FROM read_json_auto('events/*.json')
""")
# 查询嵌套字段做收入分析
result = con.execute("""
SELECT
DATE_TRUNC('day', timestamp) AS day,
COUNT(DISTINCT user_id) AS buyers,
SUM(payload:->'amount'::DOUBLE) AS revenue
FROM raw_events
WHERE payload:->>'action' = 'purchase'
GROUP BY 1
""").fetchdf()
print(result)
1.2 GEOMETRY 类型:空间数据分析开箱即用
v1.5 完成了 GEOMETRY 类型的全面重构,支持坐标系(CRS)、WKB 转换和过滤下推。物流、房地产、零售选址等行业可以直接用 SQL 做空间分析。
import duckdb
con = duckdb.ATTACH (":memory:")
# 创建空间数据表
con.execute("""
CREATE TABLE stores (
store_id INT,
store_name VARCHAR,
location GEOMETRY
)
""")
# 插入地理坐标(WKT 格式)
con.execute("""
INSERT INTO stores VALUES
(1, '北京旗舰店', 'POINT(116.4074 39.9042)'),
(2, '上海店', 'POINT(121.4737 31.2304)'),
(3, '深圳店', 'POINT(114.0579 22.3193)')
""")
# 计算各店到北京旗舰店的距离(使用投影坐标系)
result = con.execute("""
SELECT
s.store_name,
ST_Distance(
s.location,
ST_Transform(
ST_SetSRID(ST_Point(116.4074, 39.9042), 4326),
3857
)
) / 1000 AS distance_km
FROM stores s
WHERE s.store_name != '北京旗舰店'
ORDER BY distance_km
""").fetchdf()
print(result)
二、架构能力提升:read_duckdb + 并发读写
2.1 read_duckdb:跨 .duckdb 文件查询的革命
v1.5 引入了 read_duckdb() 函数,可以直接扫描和查询 .duckdb 数据库文件——不需要先加载,直接作为表函数使用。
实战场景:多租户 SaaS 聚合报表
import duckdb
con = duckdb.ATTACH ()
# 直接扫描多个客户的 .duckdb 文件
# 相当于把 100 个独立数据库变成一个数据湖
result = con.execute("""
SELECT
client_name,
COUNT(*) AS total_orders,
SUM(revenue) AS total_revenue
FROM read_duckdb('clients/*.duckdb') AS t(client_name VARCHAR, revenue DOUBLE)
GROUP BY client_name
ORDER BY total_revenue DESC
""").fetchdf()
print(result)
更强大的用法——利用晚期物化(late materialization)和过滤下推:
# 只读取需要的列和过滤条件,性能接近直接打开数据库
result = con.execute("""
SELECT order_id, revenue, order_date
FROM read_duckdb('clients/acme.duckdb') AS t
WHERE revenue > 10000
AND order_date >= '2026-01-01'
LIMIT 100
""").fetchdf()
2.2 并发写入 + 并发读取:从"单机只读"到"可写入的分析引擎"
v1.5 最重要的架构改进之一是允许在 checkpoint 期间同时执行并发读取、插入、删除。这意味着 DuckDB 不再是只能分析的"只读引擎",而是一个真正的可写入分析数据库。
import duckdb
import threading
import time
con = duckdb.ATTACH ("analytics.duckdb")
# 创建目标表
con.execute("""
CREATE TABLE if not exists daily_metrics (
metric_date DATE,
metric_name VARCHAR,
metric_value DOUBLE,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# 写入线程 1:持续追加数据
def writer(thread_id):
for i in range(100):
con.execute(f""""
INSERT INTO daily_metrics (metric_date, metric_name, metric_value)
VALUES (CURRENT_DATE, 'metric_{thread_id}', {i * 1.5})
""")
time.sleep(0.01)
print(f"Writer {thread_id} done")"
# 查询线程 1:实时读取
def reader():
for i in range(50):
result = con.execute("SELECT COUNT(*) FROM daily_metrics").fetchone()
print(f"Reader sees {result[0]} rows")"
time.sleep(0.05)
# 启动并发
threads = [
threading.Thread(target=writer, args=(1,)),
threading.Thread(target=writer, args=(2,)),
threading.Thread(target=reader),
]
for t in threads:
t.start()
for t in threads:
t.join()
# 最终查询
print(con.execute(
"SELECT metric_date, COUNT(*), SUM(metric_value) FROM daily_metrics GROUP BY 1"
).fetchdf())
三、与传统工具对比
| 能力维度 | v1.5 之前 | v1.5 ‘Variegata’ |
|---|---|---|
| 非结构化数据 | 需要 JSON 字符串 + 手动解析 | VARIANT 原生存储 + 一键提取 |
| 跨库查询 | 需要 UNION ALL 手动合并 | read_duckdb() 一行函数搞定 |
| 写入能力 | 单线程写入 | 并发写入 + 并发读取 |
| 空间分析 | 需要 PostGIS 外部依赖 | GEOMETRY 内置,SQL 直接查询 |
| 适用场景 | 纯离线分析 | 实时写入 + 分析一体化 |
四、优化建议:让你的数据产品快 10 倍
4.1 使用 Parquet 存储 + eager aggregate 下推
import duckdb
con = duckdb.ATTACH ("warehouse.duckdb")
# COPY 到 Parquet 时自动利用 eager aggregation 优化
con.execute("""
COPY (
SELECT
DATE_TRUNC('month', order_date) AS month,
category,
SUM(amount) AS total_sales,
COUNT(*) AS order_count
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY 1, 2
) TO 'sales_monthly.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)
""")
# 后续查询直接从 Parquet 读取,利用列级统计信息
result = con.execute("""
SELECT * FROM read_parquet('sales_monthly.parquet')
WHERE month >= '2025-06-01'
AND category = '电子产品'
""").fetchdf()
4.2 使用索引 + VACUUM 提升频繁查询表性能
con.execute("""
-- 为高频查询条件建索引
CREATE INDEX idx_orders_date ON orders(order_date);
CREATE INDEX idx_orders_user ON orders(user_id);
-- 定期 VACUUM 清理已删除的行(v1.5 支持带索引的 VACUUM)
VACUUM (FULL);
""")
五、实战项目:用 v1.5 打造"SaaS 销售分析看板"
把上述功能组合起来,我们可以快速搭建一个面向中小企业的数据产品:
架构设计:
- 客户通过 API 提交订单数据(JSON 格式)→ 存入 VARIANT 列
- 每个客户的 .duckdb 文件物理隔离 → 满足合规要求
- 后台通过
read_duckdb()定期聚合全平台数据 → 生成运营报表 - 实时写入最新订单 → 并发写入 + 并发读取
- 用 GEOMETRY 存储门店位置 → 自动生成销售热力图
核心代码骨架:
import duckdb
import json
from datetime import datetime
class SaaSSalesAnalyzer:
def __init__(self, client_id: str):
self.con = duckdb.ATTACH (f"clients/{client_id}.duckdb")"
self._init_schema()
def _init_schema(self):
self.con.execute("""
CREATE TABLE IF NOT EXISTS orders (
order_id VARCHAR PRIMARY KEY,
customer_name VARCHAR,
amount DOUBLE,
store_location GEOMETRY,
created_at TIMESTAMP,
metadata VARIANT
)
""")
def ingest_order(self, order_json: dict):
"""解析 JSON 并写入,VARIANT 直接存储扩展字段"""
self.con.execute("""
INSERT INTO orders VALUES (?, ?, ?, ?, ?, ?)
""", [
order_json['order_id'],
order_json['customer_name'],
order_json['amount'],
f"POINT({order_json['lat']} {order_json['lon']})" if 'lat' in order_json else None,"
datetime.fromisoformat(order_json['created_at']),
json.dumps(order_json.get('extra', {})) # 存入 VARIANT
])
# 使用:从多个客户端文件聚合报表
con = duckdb.ATTACH ()
report = con.execute("""
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS order_count,
SUM(amount) AS revenue,
COUNT(DISTINCT customer_name) AS customers
FROM read_duckdb('clients/*.duckdb')
GROUP BY 1
ORDER BY 1
""").fetchdf()
print(report)
这个产品可以直接卖给中小零售企业,月费 1000-5000 元/客户。
六、总结
- 数据结构(VARIANT + GEOMETRY)—— 能处理更多种类的数据,服务更多行业
- 架构能力(并发写入 + read_duckdb)—— 从分析引擎升级为可写入的数据平台
- 性能优化(eager aggregate、late materialization)—— 让数据产品体验接近商业数据库
- 立刻升级到 v1.5.x,使用
INSTALL httpfs,LOAD httpfs获取最新功能 - 将你的 JSON 日志库迁移到 VARIANT 类型,ETL 成本降为零
- 用 read_duckdb 做数据湖聚合,替代笨重的 Spark 作业
- 在实时监控场景中试用并发写入能力,验证可行性
想深入了解 DuckDB 在数据产品中的实战应用?duckdblab.org 提供了从入门到商业落地的完整教程系列,包含上述所有功能的详细图解和更多真实案例。学习更多 DuckDB 实战经验 → duckdblab.org
本文信息
| 项目 | 内容 |
|---|---|
| DuckDB 版本 | v1.5.x(部分功能基于 v2.0 Preview) |
| 最后验证 | 2026-09-12 |
| 测试环境 | Linux / x86_64 / 16GB RAM |
| 官方文档 | DuckDB Documentation |
| GitHub | pengzz9527/duckdb-blog |
如发现错误,欢迎通过 GitHub Issue 或邮件 contact@duckdblab.org 反馈。