编程 DuckDB 1.5 深度拆解:VARIANT 类型、Quack 远程协议与内置 GEOMETRY,「分析界 SQLite」的一次自我越狱

2026-07-29 08:13:06 +0800 CST views 6

DuckDB 1.5 深度拆解:VARIANT 类型、Quack 远程协议与内置 GEOMETRY,「分析界 SQLite」的一次自我越狱

一、背景:为什么 DuckDB 1.5 值得单独写一篇长文

如果你关注嵌入式分析数据库,DuckDB 这个名字你一定不陌生。它常被称为「分析领域的 SQLite」——进程内运行、单文件存储、零依赖、列式向量化执行,专为 OLAP 场景设计。数据科学家用它替代 Pandas 做重活,工程师用它直接查 Parquet 文件,甚至有人把它编译成 WASM 塞进浏览器里跑分析。

但 1.5 这个版本不一样。这不是一次常规迭代——6500+ commits,近百位贡献者,代号「Variegata」(灵感来自新西兰特有的天堂鸭)。它带来的三个变化,每一个都在动 DuckDB 的「定位根基」:

  1. VARIANT 类型:半结构化数据查询提速 10~100 倍,直接对标 Snowflake 的看家本领;
  2. Quack 远程协议(1.5.3 起成为核心扩展):一条 CALL quack_serve(),进程内数据库秒变客户端-服务器数据库——这是 DuckDB 第一次「越狱」出进程边界;
  3. GEOMETRY 类型内置:空间分析从扩展转正为一等公民,剑指 PostGIS 的轻量场景。

再加上重写的 CLI、非阻塞检查点(non-blocking checkpoint)、整体 17% 的性能提升,这个版本几乎是把「嵌入式分析库」的天花板拆了重盖。

这篇文章我会从工程视角把 DuckDB 1.5 拆开讲:每个新特性背后的设计动机、实现原理、真实代码实战,以及——同样重要的——它不适合干什么。5000 字起步,建议收藏后配咖啡阅读。

二、核心概念回顾:DuckDB 到底是个什么物种

在拆 1.5 之前,先花五分钟对齐认知。已经很熟的读者可以直接跳到第三节。

2.1 进程内(in-process)意味着什么

传统数据库(PostgreSQL、MySQL、ClickHouse)是客户端-服务器架构:你的应用通过 TCP/Unix socket 连接一个独立的数据库进程,每次查询都要经历序列化 → 网络传输 → 反序列化。

DuckDB 反其道而行:数据库引擎以库的形式链接进你的应用进程,查询结果直接以内存中的列式格式(与 Apache Arrow 兼容)交给你,零拷贝、零网络开销。

import duckdb

# 没有连接串,没有端口,没有服务进程
# 数据库就"活"在你的 Python 进程里
result = duckdb.sql("""
    SELECT station, avg(temperature) AS avg_temp
    FROM 'measurements-*.parquet'
    WHERE year = 2026
    GROUP BY station
    ORDER BY avg_temp DESC
    LIMIT 10
""").df()  # 直接变成 Pandas DataFrame,零拷贝

注意上面这段代码:DuckDB 直接把一组 Parquet 文件当表查了,不需要「导入数据」这个步骤。这是它席卷数据科学圈的杀手锏。

2.2 列式 + 向量化:OLAP 的性能基本盘

SQLite 是行式存储(B-Tree 上存整行),逐行解释执行,对 OLTP(高频小事务)友好。DuckDB 是列式存储 + 向量化执行:

  • 列式SELECT avg(price) 只需读 price 这一列,I/O 少一个数量级;
  • 向量化:执行引擎每次处理一批(默认 2048 行)数据,充分利用 CPU 缓存和 SIMD 指令,函数调用开销被摊薄到可忽略。

ClickBench 基准测试里,DuckDB 在部分分析查询上比 SQLite 快三个数量级。有 macOS 应用(时间追踪工具 Trace)从 SQLite 迁移到 DuckDB 后的实测数据:读取查询快 3~5 倍,100 万行数据的库文件从 101.6MB 压缩到 23.1MB——列式存储的压缩率优势肉眼可见。

2.3 单写多读的并发模型

DuckDB 的并发模型是「单进程写,或多进程只读」。它不是为高并发写入设计的,这一点在选型时必须刻在脑门上。1.5 的 Quack 协议对这一点有微妙的改变,后面细说。

三、架构分析:1.5 三大特性的设计动机与实现原理

3.1 VARIANT 类型:半结构化数据的「自动碎纸机」

问题:JSON 类型的性能天花板

DuckDB 之前处理 JSON 的方式简单粗暴——JSON 类型本质上是存文本。灵活是灵活了,但每次查询都要:

  1. 从磁盘读出整个 JSON 字符串(哪怕你只要其中一个字段);
  2. 运行时解析 JSON;
  3. 把提取出的值从 VARCHAR 转换成目标类型。

三重开销叠加,数据量一大就是灾难。

方案:Shredding(碎片化存储)

VARIANT 类型的核心思路是:写入时自动把 JSON「打碎」到独立的列中存储,读取时只取需要的碎片

-- 建表时用 VARIANT 替代 JSON
CREATE TABLE events (
    id BIGINT,
    payload VARIANT   -- 而不是 JSON
);

-- 写入时,DuckDB 自动分析 payload 的结构
INSERT INTO events
SELECT id, payload::VARIANT
FROM read_json('events-*.json.gz');

-- 查询时只从磁盘取 payload.user.country 这一"碎片列"
SELECT payload.user.country, count(*)
FROM events
GROUP BY 1;

它的工程巧妙之处在于三点:

第一,shredding 是自动且自适应的。 你不需要预定义 schema。每行的 payload 可以有不同结构——可观测性日志里 service A 记录 latency_ms,service B 记录 queue_depth,VARIANT 都能各存各的列。这正是「半结构化数据」的典型形态:大体相同,又不完全一致。

第二,类型被保留了。 JSON 类型提取出来的值永远是 VARCHAR,要自己 CAST;VARIANT 存储时就已经推断并保留了原生类型(BIGINT、DOUBLE、TIMESTAMP……),查询时零转换开销。

第三,列裁剪(column pruning)穿透到了 JSON 内部。 以前列裁剪只到「payload 列」为止;现在能精确到 payload.user.country 这个子路径。你只要 JSON 里的两个字段,就只有两个字段的 I/O。

官方内部基准显示,典型半结构化查询相比 JSON 类型有 10~100 倍的提升。这个数字看起来夸张,但结合上面三点分析,其实是合理的:文本解析开销归零 + I/O 降一个数量级 + 类型转换归零,乘起来就是两个数量级。

与 Snowflake VARIANT 的对比

Snowflake 的 VARIANT 是这套思路的商业鼻祖(其 shredding 论文影响深远),Parquet 社区也在推进 Variant 规范。DuckDB 选择跟进这个方向而不是自创格式,意味着未来与 Iceberg/Parquet 生态的 Variant 数据可以低成本互通——这步棋下得很清醒。

3.2 Quack 协议:进程内数据库的「越狱」

动机:单机分析的最后一公里问题

DuckDB 的进程内架构有个天然短板:数据和计算绑死在一个进程里。团队协作时想共享一个 DuckDB 数据库?以前的选项都很别扭:

  • 把 .duckdb 文件放共享盘:多进程写会锁冲突;
  • 导出到 Parquet 再共享:数据不再实时;
  • 上 MotherDuck(商业云服务):不是所有场景都愿意上云。

方案:给鸭子装个网络接口

2026 年 5 月,DuckDB 团队发布了 Quack——一个把 DuckDB 变成客户端-服务器数据库的远程协议。从 1.5.3 开始,Quack 成为核心扩展,首次使用时自动安装加载,开箱即用:

-- 服务端:任何一个 DuckDB 进程都能变成"服务器"
CALL quack_serve(
    'quack:localhost',
    token = 'super_secret'
);
CREATE TABLE hello AS FROM VALUES ('world') v(s);
-- 客户端:另一台机器上的 DuckDB
CREATE SECRET (
    TYPE quack,
    TOKEN 'super_secret'
);
ATTACH 'quack:analytics.internal' AS remote;
SELECT * FROM remote.hello;

注意这个设计的精妙之处:Quack 不是给 DuckDB 加了一个「服务器模式」,而是让任意 DuckDB 实例可以互为客户端/服务器。拓扑是对等的、临时的、按需的。你可以:

  • 在一台大内存机器上 quack_serve,团队成员各自 ATTACH 上来跑查询;
  • 让边缘节点的 DuckDB 把聚合结果写回中心节点;
  • 配合 DuckLake(DuckDB 的湖仓格式),Quack 服务端充当轻量元数据服务。

架构含义:DuckDB 的定位正在漂移

冷静看,Quack 不会也不打算取代 PostgreSQL 或 ClickHouse 的服务端角色——它没有连接池管理、没有细粒度权限体系(目前是 token 级别)、没有高可用方案。但它填补了一个真实存在的空档:「几个人 / 几个进程共享一份分析数据」这种不值得部署一套数据库服务的场景

从 SQLite 的历史看,这一步很有意思:SQLite 至今拒绝内置网络协议(社区搞了 rqlite、LiteFS 等一堆外挂方案),而 DuckDB 官方直接下场。嵌入式数据库的边界,正在被重新划定。

3.3 GEOMETRY 内置:空间分析的一等公民

1.5 之前,空间功能靠 spatial 扩展提供;1.5 把 GEOMETRY 类型和一批核心空间函数直接内置进主引擎。区别看似只是「要不要 INSTALL spatial」,实际影响深得多:

  1. 类型系统级集成:GEOMETRY 现在能参与内置的统计信息、min/max 剪枝,空间谓词过滤可以下推到存储层;
  2. 零部署摩擦:WASM、Lambda、受限环境里不用再操心扩展分发;
  3. 与 GeoParquet 生态直通:直接读写带几何列的 Parquet 文件。
-- 不需要 INSTALL spatial 了,开箱即用
CREATE TABLE poi AS
SELECT * FROM 'points_of_interest.parquet';

-- 查出距离天安门 5 公里内的所有咖啡店
SELECT name, ST_Distance_Sphere(
    geom,
    ST_Point(116.3975, 39.9087)
) AS dist_m
FROM poi
WHERE category = 'cafe'
  AND ST_DWithin_Spheroid(geom, ST_Point(116.3975, 39.9087), 5000)
ORDER BY dist_m;

对 90% 的「算个距离、判断个包含关系、做个空间 join」需求,你不再需要为此维护一个 PostGIS 实例。复杂的坐标系转换、拓扑修复等重活仍然是 PostGIS 的领地——但轻量场景的地盘,DuckDB 这次是明抢。

3.4 非阻塞检查点与 17% 性能提升

1.5 还有一批不那么显眼但很硬核的引擎改进:

非阻塞检查点(Non-blocking Checkpoint)。以前 DuckDB 做 checkpoint(把 WAL 合并进主存储)时会阻塞写入,长事务场景下容易感知到卡顿。1.5 实现了检查点与常规读写的并发执行——这是向「严肃数据库」演进的重要一步,尤其对 Quack 服务端这种长时间运行的场景意义重大。

整体 17% 的性能提升。来自一系列微观优化的叠加:更好的表达式融合、聚合哈希表改进、Parquet reader 的预取策略优化等。对一个已经以快著称的引擎,一个版本再抠出 17%,工程功力可见一斑。

3.5 CLI 大翻新:终端党的福利

CLI 被彻底重写:

-- 全新配色方案,关键字/字符串/数字/错误各有专属颜色,可自定义
.highlight_colors column_name darkgreen bold_underline
.highlight_colors numeric_value red bold

-- 动态提示符:始终显示当前数据库和 schema
memory D ATTACH 'my_database.duckdb';
memory D USE my_database;
my_database D SELECT 42;

还有内置分页器(大结果集不再刷屏)、更聪明的自动补全。虽然是「体验向」改进,但 DuckDB CLI 本来就是很多人做临时数据勘探的第一入口,这波投入不亏。

四、代码实战:用 1.5 新特性重构一条日志分析流水线

纸上谈兵结束,来一个贴近真实工作的完整例子:分析微服务的 JSON 结构化日志。这是 VARIANT 的主战场。

4.1 场景与数据

假设你有一批网关日志,NDJSON 格式,每天几个 GB:

{"ts":"2026-07-28T10:23:41Z","service":"gateway","level":"info","req":{"method":"POST","path":"/api/orders","latency_ms":231,"user":{"id":88123,"region":"cn-north"}},"resp":{"status":200}}
{"ts":"2026-07-28T10:23:42Z","service":"payment","level":"error","req":{"method":"POST","path":"/api/pay","latency_ms":3021},"error":{"code":"UPSTREAM_TIMEOUT","retry":true}}

注意结构的「半一致性」:正常请求有 resp,出错的有 error;有的带 user,有的没有。这在传统数仓里要么建一张巨宽的稀疏表,要么存 JSON 文本忍受慢查询。

4.2 建表与摄入

-- 1.5 写法:VARIANT 自动 shredding
CREATE TABLE logs (
    ts       TIMESTAMPTZ,
    service  VARCHAR,
    level    VARCHAR,
    doc      VARIANT
);

INSERT INTO logs
SELECT
    ts, service, level,
    to_variant(json) AS doc
FROM read_ndjson('logs/2026-07-*.ndjson.gz',
                  columns = {ts: 'TIMESTAMPTZ',
                             service: 'VARCHAR',
                             level: 'VARCHAR',
                             json: 'JSON'});

写入时 DuckDB 自动分析每行结构,把 doc.req.latency_ms 这类高频路径 shred 成独立的类型化列,低频/杂乱路径留在兜底存储区。整个过程对用户透明。

4.3 查询:性能差异从哪来

-- P99 延迟最差的接口 TOP 10
SELECT
    doc.req.path                                   AS path,
    count(*)                                       AS reqs,
    quantile_cont(doc.req.latency_ms, 0.99)        AS p99_ms
FROM logs
WHERE ts >= now() - INTERVAL 1 DAY
GROUP BY path
ORDER BY p99_ms DESC
LIMIT 10;

这条查询在 JSON 类型下的执行路径是:读全部 doc 文本 → 逐行解析 JSON → 提取两个字段 → VARCHAR 转数值。在 VARIANT 下变成:只读 req.pathreq.latency_ms 两个碎片列,且 latency_ms 本来就是数值类型。10 GB 日志、只涉及两个字段时,实际 I/O 可能不到 200 MB——两个数量级的差距就是这么来的。

再看一个利用「结构不一致」的查询:

-- 错误分析:只有出错的行才有 error 字段
SELECT
    doc.error.code            AS err_code,
    count(*)                  AS cnt,
    avg(doc.req.latency_ms)   AS avg_latency
FROM logs
WHERE level = 'error'
  AND doc.error IS NOT NULL
GROUP BY err_code
ORDER BY cnt DESC;

没有 error 字段的行读取成本近乎为零——那些碎片列上根本没有它们的数据。

4.4 用 Quack 把结果共享给团队

分析完了,别导 CSV 发群里了。直接把你这台机器变成临时分析服务器:

-- 你的机器上
CALL quack_serve('quack:0.0.0.0', token = getenv('QUACK_TOKEN'));

CREATE TABLE daily_p99 AS
SELECT date_trunc('hour', ts) AS hour,
       doc.req.path AS path,
       quantile_cont(doc.req.latency_ms, 0.99) AS p99
FROM logs GROUP BY 1, 2;
# 同事的 Python 脚本
import duckdb, os
con = duckdb.connect()
con.sql(f"CREATE SECRET (TYPE quack, TOKEN '{os.environ['QUACK_TOKEN']}')")
con.sql("ATTACH 'quack:10.0.1.42' AS wang")
df = con.sql("SELECT * FROM wang.daily_p99 WHERE p99 > 1000").df()

同事拿到的是可以继续用 SQL 加工的活数据,而不是一张死截图。

4.5 加上空间维度

如果日志里带了地理信息(CDN 边缘节点坐标之类),1.5 内置的 GEOMETRY 让空间聚合一步到位:

-- 按 100km 网格聚合各区域的错误率
SELECT
    ST_AsText(ST_SnapToGrid(node_geom, 1.0)) AS grid,
    count(*) FILTER (WHERE level = 'error') * 1.0 / count(*) AS err_rate
FROM logs
JOIN edge_nodes USING (node_id)
GROUP BY grid
HAVING count(*) > 1000
ORDER BY err_rate DESC;

一个引擎里完成时序 + 半结构化 + 空间三种分析,这在以前需要三个系统。

五、性能优化实践清单

结合 1.5 的特性,给一份生产可用的调优清单:

1. 半结构化数据全面换 VARIANT。 存量 JSON 列可以低成本迁移:

ALTER TABLE events ALTER COLUMN payload SET DATA TYPE VARIANT;
-- 或重建表以获得最优 shredding 布局
CREATE TABLE events_v2 AS SELECT * REPLACE (payload::VARIANT AS payload) FROM events;

但注意:如果你的查询模式是「每次都取整个 JSON 原文」(比如日志原样转发),VARIANT 反而多了重组开销,留 JSON 就好。shredding 的收益与「查询只触碰少数字段」的程度成正比。

2. 让谓词下推真正生效。 列式引擎的性能核心是「少读数据」。WHERE 条件尽量写在原始列上(能利用 zone map 剪枝),VARIANT 路径上的过滤在 1.5 中也已支持剪枝,但嵌套过深(5 层+)的路径收益会衰减,高频过滤字段建议在摄入时提升为顶层列。

3. 压缩与行组大小。 大批量摄入时用 PRAGMA 控制行组大小,分析型负载偏好更大的行组(默认 122880 行通常够用);导出 Parquet 时显式指定 COMPRESSION 'zstd',比默认 snappy 换来明显更小的文件,解压开销在向量化 reader 面前可以忽略。

4. 内存与溢写。 DuckDB 默认用机器 80% 内存。与应用共存的进程内场景务必设置 SET memory_limit = '8GB',超限的哈希聚合/排序会自动溢写到磁盘(SET temp_directory 指到快盘)。

5. Quack 服务端的运维要点。 长时间运行的 quack_serve 实例要吃到非阻塞检查点的红利,建议:写入走批(微批 INSERT 优于逐行)、定期 CHECKPOINT 控制 WAL 体积、token 用环境变量注入而不是写死在 SQL 里。另外记住它没有内置 TLS 终结,公网暴露前面必须挂一层反代或 VPN。

6. 升级注意事项。 从 1.4.x 升级前,如果用了数据库加密功能,注意 1.4.2 修复过 4 个加密模块的安全漏洞(包括 GCM 降级到 CTR 绕过完整性校验的攻击面)——无论是否升 1.5,加密用户都应确保至少在 1.4.2+。存储格式方面 1.5 向后兼容读取,但用了 VARIANT 的数据库旧版本读不了,团队内版本要对齐。

六、冷静的边界分析:DuckDB 1.5 不适合什么

写深度文章的义务之一是泼冷水:

  • 高并发 OLTP:单写者模型没变。电商下单、用户系统,该 PostgreSQL 还是 PostgreSQL;
  • 多租户在线服务的主数据库:Quack 是团队协作级的共享,不是生产 serving 层。没有细粒度权限、没有 HA、没有资源隔离;
  • PB 级数仓:单机(哪怕很大的单机)之外的规模,仍然是 ClickHouse / StarRocks / 云数仓的战场。DuckDB + DuckLake 能摸到 TB 级湖仓,但分布式执行不是它的路线;
  • 重度 GIS:坐标系管理、拓扑运算、栅格数据,PostGIS 生态的深度短期内无可替代。

有意思的是腾讯云最近的动作:PostgreSQL 实例内直接上线 DuckDB 引擎,一条 SET 语句切换,分析查询自动路由给 DuckDB 执行——「一库多态」。这其实印证了 DuckDB 最舒服的生态位:不是取代谁,而是作为向量化分析引擎嵌进一切需要它的地方。嵌进 Python 进程、嵌进浏览器、嵌进 PG 内核、嵌进你的 CLI。

七、总结与展望

DuckDB 1.5 的三板斧,方向感极强:

  • VARIANT 补齐了半结构化短板,把 Snowflake 引以为傲的能力开源普惠化;
  • Quack 突破进程边界,重新定义了「嵌入式数据库」的外延;
  • GEOMETRY 内置收编轻量空间分析场景。

配合非阻塞检查点和 17% 的性能提升,DuckDB 正在从「数据科学家的玩具」长成「严肃的分析基础设施组件」。它的演进哲学也值得每个做基础软件的人品味:不追分布式的时髦,把单机体验做到极致,然后让协议(Quack)和格式(DuckLake、Parquet Variant)去解决协作问题。

下一步值得盯的三个信号:Quack 会不会长出权限体系和 TLS;Parquet Variant 规范落地后与 Iceberg 生态的互通;以及各大云厂商「嵌入式 DuckDB 引擎」的跟进速度——腾讯云 PG 已经开了头,这条路大概率会热闹起来。

如果你还没试过 DuckDB,pip install duckdb 只要十秒;如果你在用 1.4,冲着 VARIANT 也值得排期升级。分析这件事,有时候真的不需要一个集群。

推荐文章

php内置函数除法取整和取余数
2024-11-19 10:11:51 +0800 CST
使用 Vue3 和 Axios 实现 CRUD 操作
2024-11-19 01:57:50 +0800 CST
Rust 并发执行异步操作
2024-11-19 08:16:42 +0800 CST
Vue3中如何处理异步操作?
2024-11-19 04:06:07 +0800 CST
php 统一接受回调的方案
2024-11-19 03:21:07 +0800 CST
程序员茄子在线接单