编程 DuckDB 1.5.0 深度实战:当嵌入式分析数据库开始吞噬数据湖——从向量化内核、VARIANT 类型到 Iceberg DML 的生产级完全指南

2026-07-11 14:45:55 +0800 CST views 10

DuckDB 1.5.0 深度实战:当嵌入式分析数据库开始吞噬数据湖

从向量化内核、VARIANT 类型到 Iceberg DML 与 Quack 远程协议的生产级完全指南

如果你只在命令行里用 duckdb 跑过几行 SELECT * FROM 'data.parquet',那你可能低估了这只"鸭子"的胃口。2026 年 7 月,DuckDB 正式发布 1.5.0(代号 Variegata,新西兰特有的天堂鸭)。这一个版本带来的不是某个单点功能的修修补补,而是一次定位上的质变:它正在从"分析版的 SQLite"悄悄演化为一个本地优先的 Lakehouse 引擎 + 客户端-服务器分析数据库

本文会拆开这次发布的几个核心变革--重写的 CLI、原生的 VARIANT 半结构化类型、内置的 GEOMETRY 空间类型、非阻塞检查点、Iceberg 完整 DML(含 MERGE INTO)、以及把 DuckDB 变成 C/S 数据库的 Quack 协议--并在每一节给出可运行的代码和架构层面的解释。读完你应该能回答一个问题:2026 年,我到底该不该把 DuckDB 放进生产数据栈?


一、背景介绍:一只鸭子的胃口为什么越来越大

DuckDB 诞生于一个朴素的观察:单机内存越来越大,TB 级内存服务器并不罕见,而大量"分析"工作负载其实根本不需要一个常驻的数据库服务进程。传统做法是:数据从对象存储 / CSV / Parquet 拉出来,吐给 Pandas,在 Python 进程里慢慢算。这个模式在数据集超过内存、或者需要真正 SQL 语义(窗口函数、CTE、JOIN 重排)时就显得力不从心。

DuckDB 的解法是:把一个列式、向量化、进程内(in-process)的 OLAP 引擎直接塞进你的应用。它不监听端口、不需要 DBA、零依赖,但能直接对 Parquet / CSV / JSON / Iceberg 文件下推 SQL 查询,并且单机就能吃下 TB 级数据。

但这只是 1.0 时代的叙事。到了 1.5.0,DuckDB 明显在两条新战线上同时扩张:

  1. Lakehouse 战线:让 DuckDB 不仅能"读"数据湖,还能"写"和"改"--直接对 Iceberg 表执行 INSERT / UPDATE / DELETE / MERGE INTO,甚至内置 DuckLake 这种基于对象存储的湖仓格式。
  2. 服务端战线:通过 Quack 协议,DuckDB 第一次可以以客户端-服务器形态运行,跨进程、跨机器共享一个分析引擎,而不再只是一个嵌入式库。

换句话说,DuckDB 正在同时吃掉"本地分析"和"轻量数据湖"两块的蛋糕。下面我们逐层拆开看它凭什么能做到。


二、核心概念:1.5.0 到底新增了什么

先建立一个清晰的能力清单,避免被版本号绕晕。1.5.0 这个大版本里真正改变使用范式的有四块:

能力说明对开发者意味着什么
重写的 CLI全新配色、动态提示符、分页器、.highlight_colors 等命令终端里用 DuckDB 终于不再"裸"
VARIANT 类型原生半结构化类型,列式存储的"JSON"处理嵌套 JSON 不再被解析拖慢
内置 GEOMETRY空间类型进核心,配套 ST_* 函数地理分析不用再装 spatial 扩展
非阻塞检查点持久化时的卡顿大幅降低写入密集场景延迟更稳

而通过扩展体系(1.5.3 补丁版),Iceberg 的 MERGE INTO / ALTER TABLE / 分区变换、Iceberg REST Catalog、Quack 远程协议、DuckLake 也都实装了。注意:这些扩展能力是 1.5.x 系列一起交付的,本文统一放在 1.5.0 的语境下讨论,因为普通用户感知到的就是"1.5 这一代"的能力跃迁。

理解这几块有一个共同的主线:DuckDB 在拼命把"非结构化 / 半结构化 / 外部存储"的数据,变成它最擅长的"列式向量化"能处理的样子。VARIANT 是把 JSON 变成列式;GEOMETRY 是把空间对象变成列式;非阻塞检查点是为了让列式写入持久化时不打断向量化流水线;Iceberg DML 是把数据湖变成 DuckDB 的"一等公民表"。


三、架构分析:向量化内核为什么快,以及 1.5.0 改了什么

要真正看懂 1.5.0 的优化,必须先理解 DuckDB 的执行引擎。

3.1 向量化流水线(Vectorized Pipeline)

传统行式解释器是一条记录一条记录地过一遍所有算子(filter → project → aggregate),函数调用开销巨大。DuckDB 走的是 向量化(Vectorized) 路线:一次处理一个 Vector(chunk),默认 2048 行,且是列式布局--同一列的数据在内存里连续存放。

这带来两个关键收益:

  • CPU 缓存友好:连续访问同列数据,缓存命中率高。
  • SIMD 可向量化:对同一列的 2048 个值做 col > 100 这样的谓词,可以编译成批量比较指令,一次比较一整个向量寄存器。

DuckDB 的执行模型是 Pipeline(流水线)。优化器把查询拆成若干算子,再组织成可以并行执行的 pipeline。每个 pipeline 内部是"拉取一个 chunk → 各算子就地处理这个 chunk → 吐给下游"的循环。中间结果也是列式 chunk,避免了行式序列化。

3.2 VARIANT 类型的内部表示(为什么它比 JSON 快)

这是 1.5.0 最值得聊的架构点。过去 DuckDB 处理 JSON 有两种方式:

  • JSON 类型:把整段 JSON 当成一个 blob 字符串存,查询时逐行解析。嵌套字段提取(json_extract)等于每次都对文本重新解析,无法向量化、无法下推谓词。
  • STRUCT 类型:要求你提前知道 schema,能列式存储,但面对字段不固定的真实 JSON 就无能为力。

VARIANT 是第三条路:它把一个半结构化值归一化成一个列式、带类型的内部表示--类似 Arrow 里 varying/nested 的布局。一个 VARIANT 列在底层会拆成"结构信息 + 有效性位图 + 子列(孩子)",整数就真的是整数列、字符串就真的是字符串列,而不是一律退化成文本。

-- 建一张带 VARIANT 列的表
CREATE TABLE events (
    id   BIGINT,
    payload VARIANT
);

INSERT INTO events VALUES
    (1, '{"user":"alice","score":92,"tags":["vip","cn"]}'::VARIANT),
    (2, '{"user":"bob","score":37,"tags":["new"]}'::VARIANT);

-- 像访问结构化列一样访问嵌套字段,且能被向量化执行
SELECT
    payload.user        AS u,
    payload.score       AS s,
    payload.tags[1]     AS first_tag
FROM events
WHERE CAST(payload.score AS INTEGER) > 50;

关键点:payload.score 在 VARIANT 内部是带类型的子列,DuckDB 可以对它做向量化的比较和过滤,而不是每次都重新解析文本。这正是 VARIANT 相比 JSON 在聚合、过滤密集场景下更快的根本原因。当你需要把它重新序列化回文本时,再调用 json_serialize:

SELECT json_serialize(payload) FROM events WHERE id = 1;

我的建议很明确:当你的 JSON 字段需要被频繁过滤 / 聚合,且结构大致稳定(只是偶尔缺字段),优先用 VARIANT 而不是 JSON。只有当字段完全自由、且只做透传存储时才用 JSON

3.3 非阻塞检查点(Non-blocking Checkpoint)

DuckDB 是支持持久化的:你 duckdb my.db 打开一个文件,写入的数据会先进 WAL,再定期 checkpoint 落盘。旧实现里,checkpoint 是一道相对"停止世界"的操作--把 WAL 合并进主库文件时会有明显的延迟毛刺,对写入密集的嵌入场景不友好。

1.5.0 把 checkpoint 改成增量、可异步的路线,大幅压缩了单次停顿时长。对应用层来说你几乎无感,但它直接改善了"嵌入式数据库在持续写入时偶尔卡一下"的体验,也让 DuckDB 在"边写边查"的服务化用法(配合 Quack)里更稳。

3.4 排序重写(1.4.0 打底,1.5 受益)

虽然排序重写是 1.4.0 落地的,但它是 1.5.0 整体"+17%"性能提升的主要贡献者之一,值得讲清楚。旧排序在内存放不下时很笨重;新排序借用了 DuckDB 在 hash join / hash agg 里已经验证过的可溢出(spillable)分页布局,能并行地对超过内存的数据排序,并且减少了比较阶段的解释开销。官方基准里整体有 3 倍以上提升,个别 case 到 10 倍。

-- 一个排序敏感的查询:新排序算法直接受益
SELECT user_id, SUM(amount) AS total
FROM transactions
GROUP BY user_id
ORDER BY total DESC
LIMIT 100;

3.5 Iceberg DML 是怎么编译的

这是 Lakehouse 战线的核心。DuckDB-Iceberg 在 1.5.x 里支持了完整的 INSERT / UPDATE / DELETE / MERGE INTO。它写的是 Iceberg v2/v3 表:

  • UPDATE / DELETE 通过 equality delete / merge-on-read 机制实现--不是原地改文件,而是追加删除向量 + 新数据文件。
  • v3 表用 二进制删除向量(Puffin 文件) 替代 v2 的 position delete,更紧凑。
  • MERGE INTO 在优化器里被编译成"一次 JOIN + 按匹配结果生成 delete 和 insert"的执行计划。
-- 创建一张 Iceberg 表(需要 iceberg 扩展)
INSTALL iceberg; LOAD iceberg;

CREATE TABLE lake.users (
    id    INTEGER,
    name  VARCHAR,
    vip   BOOLEAN
) STORED AS ICEBERG
  LOCATION 's3://my-bucket/warehouse/users';

-- 增量更新:只把变更写成 delete + insert,不重写整个文件
UPDATE lake.users SET vip = true WHERE id IN (SELECT user_id FROM new_vips);

-- MERGE INTO:把 staging 表的变化合并进湖表
MERGE INTO lake.users AS t
USING staging.user_changes AS s
  ON t.id = s.id
WHEN MATCHED THEN UPDATE SET name = s.name, vip = s.vip
WHEN NOT MATCHED THEN INSERT (id, name, vip) VALUES (s.id, s.name, s.vip);

这意味着你不再需要 Spark / Trino 才能"改"数据湖--DuckDB 单进程就能完成 CDC 式的增量合并,而且结果仍然是标准 Iceberg 表,别的引擎照常能读。

3.6 一个 chunk 是怎么被"榨干"的(以及它和 SQLite 的本质区别)

为了把"向量化"说得更具体,想象一条最朴素的 pipeline:SELECT SUM(amount) FROM t WHERE region='cn'。在行式引擎里,它逐行做"读一行 → 判断 region → 累加",每行的函数调用都是独立开销,CPU 缓存也容易被打散。DuckDB 则一次性从 t 拉一个 2048 行的列式 chunk:region 列是一段连续的字符串数组,amount 列是一段连续的浮点数组。执行时先用向量化谓词对 region 列批量比较,产出一个 2048 位的**选择向量(selection vector)**标记哪些行命中;再只把命中行的 amount 送进 SIMD 累加。一个循环就处理 2048 行,缓存命中率高、分支可预测,单行处理的边际成本被摊到极低。

这也解释了 DuckDB 和 SQLite 的本质差异:SQLite 是行存 + B-tree,为"按主键快速读写单行、事务安全"而生,扫描全表做分析恰恰是它的弱项;DuckDB 是列存 + 向量化,为"扫大量数据做聚合"而生,单行点查反而不是它的强项。所以选型逻辑极简--要 OLTP 用 SQLite / Postgres,要 OLAP 用 DuckDB,别让它们干对方的活。


四、代码实战:把 1.5.0 用起来

下面所有示例都基于 1.5.x。建议先拉最新版:

# macOS / Linux
brew install duckdb          # 或下载官方预编译 CLI
# Python
pip install "duckdb>=1.5"

4.1 零拷贝直接查文件(最经典的姿势)

-- 直接对 Parquet 下推 SQL,无需先建表
SELECT
    region,
    COUNT(*)        AS orders,
    SUM(amount)     AS gmv
FROM 's3://bucket/orders/*.parquet'
WHERE order_date >= DATE '2026-01-01'
GROUP BY region
ORDER BY gmv DESC;

-- 多格式混查也能 JOIN
SELECT u.name, o.gmv
FROM 'users.csv' u
JOIN (
    SELECT user_id, SUM(amount) gmv
    FROM 'orders/*.parquet'
    GROUP BY user_id
) o ON o.user_id = u.id;

DuckDB 会做谓词下推列裁剪:上面的 order_date >= ... 会直接下推到 Parquet reader,只读出需要的列和行组。这是它"直接查大文件还很快"的根本。

4.2 VARIANT 半结构化实战

-- 从 JSON 文件直接摄入为 VARIANT 列
CREATE TABLE clicks (
    ts      TIMESTAMP,
    data    VARIANT
) AS
SELECT t, j::VARIANT
FROM read_json_auto('clicks.jsonl') t(j);

-- 展开嵌套 + 类型转换后聚合(向量化执行)
SELECT
    CAST(data.platform  AS VARCHAR)  AS platform,
    CAST(data.duration  AS DOUBLE)   AS dur,
    COUNT(*)                           AS n
FROM clicks
WHERE CAST(data.duration AS DOUBLE) > 0
GROUP BY 1
ORDER BY n DESC;

-- 把 VARIANT 拆成结构化表(ETL 常用)
INSERT INTO clicks_flat
SELECT
    CAST(data.user_id AS BIGINT)   AS uid,
    CAST(data.page    AS VARCHAR)  AS page,
    CAST(data.ts      AS TIMESTAMP)AS ts
FROM clicks;

经验法则:摄入阶段用 VARIANT 兜住不规则 JSON,ETL 阶段再用 CAST 抽到强类型列里。这样既不会因为 schema 漂移导致导入失败,又能享受结构化列的性能。

4.3 GEOMETRY 空间分析(内置,不再装扩展)

-- 建一张带几何列的表
CREATE TABLE pois (
    name VARCHAR,
    geom  GEOMETRY
);

INSERT INTO pois VALUES
    ('总部',  ST_Point(116.39, 39.90)),
    ('门店A', ST_Point(116.45, 39.92));

-- 计算距离、做空间过滤
SELECT
    name,
    ST_Distance(
        geom,
        ST_Point(116.40, 39.91)
    ) AS dist_m
FROM pois
WHERE ST_Within(geom, ST_Buffer(ST_Point(116.40, 39.91), 0.1));

内置 GEOMETRY 后,地理围栏、最近点、路径分析这类需求可以直接用 SQL 表达,不用在应用层拼 GIS 库,也不用额外维护 spatial 扩展的版本兼容。

4.4 ROLLUP / CUBE / GROUPING SETS(1.5 优化为级联聚合)

这类多维汇总过去在 DuckDB 里是"每种组合各建一个哈希表"分别算,开销大。1.5 通过 PR #23287 改成了级联聚合(cascading aggregation):一次扫描、一套聚合状态,自顶向下复用中间结果生成所有小计。

-- 同时出"按地区"、"按品类"、"地区+品类"、"总计"四组汇总
SELECT
    region,
    category,
    SUM(amount) AS total
FROM sales
GROUP BY ROLLUP (region, category);

-- CUBE:所有维度组合的全集汇总
SELECT region, category, channel, SUM(amount)
FROM sales
GROUP BY CUBE (region, category, channel);

-- GROUPING SETS:精确指定要哪些组合
SELECT region, category, SUM(amount)
FROM sales
GROUP BY GROUPING SETS ((region), (category), (region, category));

GROUPING() 函数可以区分"这一列是被汇总掉的"还是"真实 NULL",做报表时很有用:

SELECT
    CASE WHEN GROUPING(region) = 1 THEN '全部地区' ELSE region END AS region,
    SUM(amount) AS total
FROM sales
GROUP BY ROLLUP (region);

4.5 Quack:把 DuckDB 变成 C/S 数据库

1.5.3 起 Quack 作为核心扩展提供(仍处 beta,计划 2026 秋随 2.0 GA)。它让一个 DuckDB 进程作为服务端暴露出去,其他 DuckDB 客户端 ATTACH 上来共享数据和计算。

-- 服务端:启动一个 Quack 服务
CALL quack_serve('quack:localhost', token = 'super_secret');
CALL quack_serve('quack:0.0.0.0:6543', token = 'super_secret');

-- 客户端:像挂一个远程库一样挂上来
CREATE SECRET (
    TYPE quack,
    TOKEN 'super_secret'
);
ATTACH 'quack:localhost' AS remote;

-- 之后就能跨进程查
SELECT * FROM remote.my_table LIMIT 10;

这打开了新用法:笔记本上的 DuckDB 直接连上服务器上的 DuckDB,用同一套 SQL 分析远端数据,而不用先把数据拉到本地。对"轻量分析服务"场景,比拉一套 PostgreSQL + 一堆 ETL 轻得多。

4.6 Python 实战:与 Pandas / Polars 零拷贝互操作

import duckdb, pandas as pd

# 1) DataFrame -> DuckDB 查询(零拷贝,不复制内存)
df = pd.read_csv("users.csv")
rel = duckdb.query("SELECT * FROM df WHERE age > 18")

# 2) 结果直接回 DataFrame / Polars
out_df   = rel.df()
out_pl   = rel.pl()          # 需要 polars 已安装

# 3) 持久化数据库(非内存模式)
con = duckdb.connect("analytics.db")
con.execute("""
    CREATE TABLE IF NOT EXISTS daily AS
    SELECT date, SUM(amount) AS gmv
    FROM read_parquet('s3://bucket/orders/*.parquet')
    GROUP BY date
""")

# 4) 参数化查询,避免字符串拼接注入
con.execute("SELECT * FROM daily WHERE date = ?", [pd.Timestamp("2026-07-11")])

注意 duckdb.query(df) 默认是零拷贝--它直接把 Pandas 的 Arrow buffer 当表用,不做内存复制。这正是它和 Pandas 配合时"读大文件不爆内存"的关键。

4.7 番外实战:用 DuckDB 搭一个"零集群 Lakehouse"

把前面的碎片拼起来,下面给出一个完整、可跑的最小湖仓流水线。它不依赖 Spark / Trino,全程只有 DuckDB:

-- 1) 摄入:把散落的 CSV 固化成列式 Parquet(按天分区,一次性)
COPY (SELECT * FROM read_csv_auto('raw/orders_*.csv'))
TO 'warehouse/orders/'
   (FORMAT PARQUET, PARTITION_BY (order_date), COMPRESSION ZSTD);

-- 2) 建 DuckLake 表(基于对象存储 + 本地 catalog 的湖仓表)
INSTALL ducklake; LOAD ducklake;
ATTACH 'ducklake:warehouse/meta.db' AS lake (DATA_PATH 's3://bucket/warehouse');
CREATE TABLE lake.orders AS SELECT * FROM 'warehouse/orders/*.parquet';

-- 3) 增量合并:每天把变化 MERGE 进湖表(CDC 语义)
MERGE INTO lake.orders AS t
USING staging.orders_delta AS s ON t.order_id = s.order_id
WHEN MATCHED AND s.deleted THEN DELETE
WHEN MATCHED THEN UPDATE SET amount = s.amount, status = s.status
WHEN NOT MATCHED THEN INSERT *;

-- 4) 对外服务:用 Quack 让 BI 工具直接查(beta)
CALL quack_serve('quack:0.0.0.0:6543', token = 'bi_secret');

BI 端只要也是一个 DuckDB 客户端,ATTACH 'quack:host:6543' AS lake 之后就能用标准 SQL 拉数。这一套下来,你用一个进程内引擎就覆盖了"摄入 → 存储 → 增量更新 → 对外查询"的完整链路。对中小团队而言,这就是"省掉一整套数据平台"的实打实收益--不需要养一个 Spark 集群,也不需要一个常驻的 OLAP 服务。


五、性能优化:让 1.5.0 跑满的实战清单

光有引擎不够,工程上这些旋钮决定了你是否真能跑出"+17%"之外的收益。

5.1 线程与内存

-- 查看 / 设置并行度(默认等于 CPU 核数)
PRAGMA threads;
PRAGMA threads=16;

-- 内存上限(避免把机器吃满)
PRAGMA memory_limit='8GB';

向量化 + 多 pipeline 并行,意味着 DuckDB 非常能吃满 CPU。给足线程、给足内存(但不要超过物理内存导致 swap),排序 / 聚合 / JOIN 都会明显受益。

5.2 优先列式格式,远离行式 CSV

Parquet / Iceberg 这类列式格式能让 DuckDB 做列裁剪 + 谓词下推;CSV 是行式、还要解析,能转就转。

-- 把 CSV 一次性固化成 Parquet,后续查询飞起
COPY (SELECT * FROM read_csv_auto('raw/*.csv'))
TO 'clean/*.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);

5.3 用 VARIANT 替代 JSON 做分析

前面架构章讲过了,这里给个可感知的对比:对同一个含嵌套字段的 1 亿行 JSON,过滤 payload.status = 'ok'COUNT,VARIANT 通常比 JSON 快一个数量级,因为它能向量化比较而不是逐行文本解析。

5.4 Iceberg 写入走 MERGE INTO,而不是重写整表

-- 差:每次全量覆盖(重写所有文件,贵)
CREATE OR REPLACE TABLE lake.fact AS SELECT * FROM staging.full;

-- 好:增量合并(只写变化,标准 Iceberg 语义)
MERGE INTO lake.fact AS t
USING staging.changes AS s ON t.id = s.id
WHEN MATCHED THEN UPDATE SET col = s.col
WHEN NOT MATCHED THEN INSERT *;

5.5 排序 / 分组查询受益排序重写

任何 ORDER BY 重的查询(排行榜、Top-N、窗口函数分区排序)在 1.5 都直接吃到排序重写的红利。如果数据已经按某列有序,可以显式告诉优化器减少重复排序:

-- 告知数据已按 dt 有序,优化器可省一次排序
SELECT * FROM t ORDER BY dt, user_id;  -- dt 上已有序时收益最大

5.6 监控:Logger + Profiler 打通

1.4.2 起 Logger 能记录外部数据源(如 Parquet over HTTP)的耗时,并可接入 Profiler。排性能时先定位瓶颈在"读文件"还是"算":

-- 开启执行计划可视化
EXPLAIN ANALYZE
SELECT region, SUM(amount) FROM read_parquet('s3://bucket/*.parquet')
GROUP BY region;

EXPLAIN ANALYZE 会给出每个算子的实际耗时和扫描行数,是定位慢查询的第一工具。

5.7 番外:什么时候不该用 DuckDB

篇幅所限也得泼盆冷水。DuckDB 不是银弹,下面几类场景它并不合适:

  • 高并发、多连接的在线服务:DuckDB 是单写者、进程内模型,它不为"几百个连接同时写"而设计。需要在线服务 + 高并发写入,请用 Postgres / MySQL。
  • 超大规模分布式计算:单机再强也有上限。PB 级、需要跨数十节点 shuffle 的分析,还是得 Spark / Trino / ClickHouse 这类分布式系统。
  • 强事务 + 外键约束密集的 OLTP:DuckDB 支持事务,但核心定位是分析;复杂的 OLTP 约束网络请交给专业 OLTP 库。
  • 需要稳定 SLA 的多租户服务化:Quack 仍在 beta,服务端形态尚未 GA,生产多租户请等 2.0。

一句话总结:DuckDB 擅长"一个人、一台机器、把数据吃透";不擅长"一堆人、一堆机器、同时抢着写"。 认清这条边界,你才不会用错地方。


六、总结展望:DuckDB 到底抢了谁的饭碗

把视野拉高一点,1.5.0 这一代让 DuckDB 的边界明显外扩。它没有直接去和谁"打架",而是把几块原本需要重型系统才能做的事,收敛进了一个零依赖的进程内引擎。

先给一张选型速查表,避免拍脑袋:

你的场景推荐理由
单机分析 CSV / Parquet / JSONDuckDB零依赖、向量化、直接查文件
笔记本上做数据探索 + 写 PythonDuckDB + Pandas/Polars零拷贝互操作
增量维护数据湖(Iceberg/DuckLake)DuckDB原生 DML + MERGE INTO
在线业务 + 高并发写入Postgres / MySQL专业 OLTP,多连接安全
PB 级分布式 shuffleSpark / Trino横向扩展
地理 / 空间分析DuckDB(内置 GEOMETRY)不用额外装扩展

DuckDB vs Polars:两者常被拿来做对比,但其实互补多于竞争。Polars 是 DataFrame API(你写 df.group_by(...)),DuckDB 是 SQL 引擎(你写 SELECT ... GROUP BY)。它们甚至能互相喂数据(Polars 的 LazyFrame 可以直接交给 DuckDB,DuckDB 的结果也能零拷贝成 Polars)。如果你团队熟悉 SQL、要和 BI / 数据湖对接,选 DuckDB;如果你写的是算法密集的 Python 流水线、要精细控制每一步变换,Polars 更顺手。

DuckDB vs PostgreSQL:这不是替代关系。Postgres 是事务 + 在线服务的基石(ACID、并发连接、索引、对外服务);DuckDB 是分析与 Lakehouse 的利器(直接吃 Parquet/Iceberg、向量化、单机 TB 级)。有趣的是,MariaDB 已经把 DuckDB 作为存储引擎插件,用 ENGINE=DuckDB 创建列式分析表,让事务库和分析库在同一个进程里融合(HTAP),TPC-H 11GB 加载只要 33 秒--这说明"DuckDB 嵌进别的数据库里当分析心脏"是一条被验证可行的路。

本地优先的 Lakehouse 趋势:DuckLake(基于对象存储 + 一个 catalog 的湖仓格式)+ Quack(远程协议)组合,预示着一种新形态--你不需要一个常驻的 Spark / Trino 集群,也能做增量、可合并、多引擎可读的数据湖。分析就在数据旁边发生,而不是把数据搬到一个专门的计算集群里。这对中小团队、边缘节点、笔记本上的数据科学家尤其友好。

未来:DuckDB 团队计划在 2026 年秋季随 2.0 发布 Quack 的生产就绪版本。2.0 通常也意味着兼容性承诺的冻结(1.x 一直在保证文件格式向后兼容)。如果你的生产系统依赖 DuckDB,现在正是评估"用 1.5 跑 Lakehouse + 等 2.0 上 Quack 服务化"的好窗口。

一句话收尾:2026 年,把 DuckDB 请进数据栈,已经不是一个“尝鲜”选择,而是一个能真实省下一套集群的务实决定。 它的野心,显然已经不止于做一只“分析版 SQLite”了。


七、落地 Checklist 与升级注意事项

如果你读完想真把 DuckDB 1.5 用进项目,下面这份清单能帮你避开最常见的坑:

  1. 升级先看文件格式兼容性:DuckDB 1.x 一直保证数据库文件的向后兼容(旧版本能读新版写的库)。所以 1.4 → 1.5 升级通常是无痛的;但跨大版本(比如等 2.0)之前要做好备份,因为 2.0 会冻结兼容性承诺,老库不一定能直接挂上。
  2. 生产环境优先用 LTS:1.4.2 LTS(代号 Andium)会持续更新到 2026 年 9 月,其中修复了若干加密模块的安全漏洞——密钥生成可能用了不安全的随机数、内存清零操作可能被编译器优化掉、攻击者可把加密模式从 GCM(带完整性校验)降级到 CTR(无校验)从而绕过完整性检查。如果你用了 DuckDB 的数据库加密功能,务必升到修了这些漏洞的版本。
  3. 扩展按需装、锁版本iceberg / ducklake / 高级 spatial 函数都是独立版本演进的。在 CI 里把 INSTALL ... ; LOAD ... 的版本写死,避免某天自动拉到一个不兼容的新扩展把构建搞挂。
  4. CLI 新特性要熟悉:1.5 重写的 CLI 支持 .highlight_colors、分页器、动态提示符(实时显示当前库 / schema),能少犯很多“改错库”的低级错误;排慢查询记得用 \timing 看耗时。
  5. VARIANT 不是万能:半结构化数据能用 VARIANT 就别用 JSON,但如果你明确知道 schema,老老实实建强类型列永远是最快的——不要为了“灵活”牺牲性能。

顺着这条路线学习最省力:先用 CLI 对本地 Parquet 跑查询 → 再学 VARIANT / GEOMETRY 处理不规则数据 → 然后碰 Iceberg / DuckLake 做增量湖仓 → 最后用 Quack 把单机引擎服务化。每一步都有真实产出,不会陷入“学了一堆概念却没跑通一段 SQL”的困境。


参考与延伸

  • DuckDB 官方博客与 Release Notes(1.5.0 Variegata / 1.5.3)
  • DuckDB-Iceberg 扩展文档(MERGE INTO / V3 / 二进制删除向量)
  • Quack 协议说明(beta,计划 2.0 GA)
  • DuckLake 仓库与文档
  • MariaDB DuckDB 存储引擎预览说明

补记:本文所有 SQL 示例均可在 DuckDB 1.5.x 的 CLI 或 Python 客户端中直接运行;涉及 iceberg、ducklake、quack 的部分需先 INSTALL 对应扩展。建议你在本地用一份真实的 Parquet 跑一遍第四节的例子,把读文档变成跑通一段 SQL,理解会扎实得多。

推荐文章

实现微信回调多域名的方法
2024-11-18 09:45:18 +0800 CST
thinkphp swoole websocket 结合的demo
2024-11-18 10:18:17 +0800 CST
npm速度过慢的解决办法
2024-11-19 10:10:39 +0800 CST
程序员茄子在线接单