编程 DuckDB 深度拆解:从嵌入式 OLAP 到异步 I/O 与 DuckLake——一个「不该存在」的数据库如何改写分析引擎的游戏规则

2026-08-02 21:13:52 +0800 CST views 6

DuckDB 深度拆解:从嵌入式 OLAP 到异步 I/O 与 DuckLake——一个「不该存在」的数据库如何改写分析引擎的游戏规则

引言:为什么 DuckDB 不该存在

数据库世界有一条不成文的规矩:OLTP 用嵌入式(SQLite),OLAP 用分布式(ClickHouse、Presto、Spark)。嵌入式意味着单机、单进程、轻量;分布式意味着多节点、高吞吐、弹性伸缩。这两条路线在过去二十年泾渭分明。

然后 DuckDB 出现了。

2019 年,CWI(荷兰国家数学与计算机科学研究所)的研究者们发布了一个看似违背常识的项目:一个嵌入式的、列式的、面向分析型查询的数据库引擎。它跑在你的笔记本上,不需要服务器,不需要集群,甚至不需要网络——但它的查询性能可以匹敌很多分布式系统。

到 2026 年,DuckDB 已经成为数据工程领域增长最快的开源项目之一。它不只是一个「更好的 SQLite for analytics」,而是在重新定义嵌入式数据库的边界:

  • Quack 协议:从纯嵌入式走向 C/S 架构
  • 异步 I/O:v2.0 即将引入的异步读取管线,TPC-H 查询提速 3 倍
  • DuckLake:用 SQL 构建的湖仓一体格式,挑战 Iceberg 和 Delta Lake

本文将从第一性原理拆解 DuckDB 的架构设计、核心创新和工程取舍,带你理解为什么这个「不该存在」的数据库正在改变整个数据生态。


一、架构总览:嵌入式 OLAP 的第一性原理

1.1 为什么需要嵌入式 OLAP

传统 OLAP 系统(ClickHouse、Druid、Apache Pinot)的架构是这样的:

客户端 → 网络 → 查询协调器 → 多个存储节点 → 磁盘/对象存储

这条链路的每一个环节都引入了延迟和复杂性。对于数据科学家在 Jupyter Notebook 里做交互式探索、对于应用开发者需要在代码里嵌入 SQL 分析、对于小型团队不想运维集群——这些场景下,传统 OLAP 的架构代价太高了。

DuckDB 的第一性原理设计是:把整个分析引擎塞进一个进程,通过低级 API 直接调用,绕过所有网络开销

// DuckDB 的嵌入式调用方式——没有服务器,没有协议
#include "duckdb.hpp"

int main() {
    duckdb::DuckDB db(nullptr);  // 内存数据库
    duckdb::Connection con(db);
    
    // 直接执行 SQL,零网络开销
    con.Query("CREATE TABLE lineitem AS SELECT * FROM read_parquet('data/lineitem.parquet')");
    auto result = con.Query("SELECT l_returnflag, SUM(l_quantity) FROM lineitem GROUP BY 1");
    
    // 结果就在内存里,直接迭代
    for (auto& chunk : *result) {
        for (idx_t i = 0; i < chunk.size(); i++) {
            // 处理每一行...
        }
    }
}

这种设计的核心优势是:数据在进程内流转,没有序列化/反序列化开销,没有 TCP 往返延迟。对于交互式查询,这个差距是数量级的。

1.2 列式存储:为什么分析型查询必须列存

行存 vs 列存的选择不是技术偏好,而是工作负载决定的:

特性行存(PostgreSQL/MySQL)列存(DuckDB/ClickHouse)
单行读取✅ 极快❌ 需要重组多列
范围扫描⚠️ 读取整行,浪费✅ 只读目标列
聚合分析❌ 全表扫描✅ 列级向量化扫描
压缩比⚠️ 同列类型混合✅ 同类型连续存储,压缩 5-10x
写入性能✅ 单行插入快❌ 批量写入才高效

DuckDB 的列存设计有一个关键选择:它不像 ClickHouse 那样使用稀疏索引(Mark),而是使用 Row Group 作为基本分区单元。每个 Row Group 包含固定数量的行(通常 122,880 行),存储为一个独立的 chunk。

// DuckDB 的列式数据组织
// 一个 Row Group 的内存布局
struct ColumnChunk {
    vector<UnifiedVectorFormat> data;     // 每列的压缩数据
    validity_mask validity;               // NULL 位图
    Statistics statistics;                // min/max/count 统计信息
    CompressionType compression;          // 压缩算法选择
};

// Row Group 是 DuckDB 的调度单位
// 一个查询会被拆分为多个 Row Group 级别的 Job
// 每个 Job 可以独立地在不同线程上执行

1.3 向量化执行引擎:一次处理一批,不是一行

DuckDB 的执行引擎是向量化的(Vectorized),这和 PostgreSQL 的火山模型(Iterator Model)有本质区别:

火山模型(PostgreSQL)

每次调用 next() 返回一行 → 大量虚函数调用开销
CPU 分支预测频繁失败 → 缓存命中率低

向量化模型(DuckDB)

每次调用 next() 返回一批行(Vector,通常 2048 行)
→ 批量处理减少函数调用
→ SIMD 友好,编译器可以向量化
→ 缓存预取更有效

DuckDB 的执行流水线(Pipeline)由多个 Operator 组成,每个 Operator 处理一个 Vector(一批行)。关键的 Operator 包括:

  • TableScan:从列存中读取 Row Group,解压缩,输出 Vector
  • Filter:基于谓词过滤,使用 Selection Vector 实现行级过滤而不移动数据
  • Projection:列投影,选择需要的列
  • HashJoin:哈希连接,构建端和探测端分离
  • GroupByHash:哈希聚合,支持 perfect hash 优化
  • Window:窗口函数,支持 partition/order 分区
// DuckDB 的向量化执行示例
// 一个简单的 SELECT id, SUM(amount) FROM orders GROUP BY id
// 执行计划:
//   Pipeline 1: TableScan → Filter → Projection → HashGroupBy
//   Pipeline 2: (Pipeline 1 的结果) → ResultCollector

// TableScan Operator 输出 Vector
// 每个 Vector 包含 2048 行的 id 列数据
PhysicalTableScan → 输出 Vector<uint32_t> ids(2048)

// HashGroupBy Operator
// 使用 unordered_map<uint32_t, AggregateState> 做分组
// 每处理一个 Vector,更新一次聚合状态
// 避免了逐行调用 AggregateFunction::Update()

这种设计的核心收益是:批量处理减少了分支预测失败和虚函数调用的开销,同时让 CPU 的 SIMD 单元和缓存预取机制发挥作用。在实际基准测试中,DuckDB 的向量化引擎比逐行执行的引擎快 5-10 倍。


二、Quack 协议:当嵌入式数据库决定「出山」

2.1 为什么需要 C/S 架构

DuckDB 的嵌入式架构有一个天然限制:多进程无法并发写同一个数据库文件。这是因为 DuckDB 在内存中维护了大量状态(Buffer Manager、WAL、Transaction Manager),多进程同时写入会导致状态不一致。

社区对此的反应很有趣——各种 workaround 涌现:

  • Arrow Flight SQL:通过 Apache Arrow 的 Flight 协议暴露 DuckDB 服务
  • MotherDuck:自研 C/S 协议,把 DuckDB 放在云端
  • pg_duckdb:在 PostgreSQL 里跑 DuckDB(所谓的「EleDucken」)
  • 各种自定义 RPC 方案

这些 workaround 说明了一件事:用户需要多进程并发访问 DuckDB 的能力

2.2 Quack 的设计哲学

2026 年 5 月,DuckDB 团队正式发布了 Quack 协议——一个全新的、基于 HTTP 的 C/S 协议。

Quack 的设计哲学可以总结为三点:

1. 基于 HTTP,而不是自创协议

# Quack 服务端——启动后监听 HTTP
CALL quack_serve('quack:localhost', token = 'super_secret');

# Quack 客户端——通过 HTTP 连接
ATTACH 'quack:localhost' AS remote;
FROM remote.hello;

为什么选 HTTP?因为 HTTP 生态已经非常成熟:

  • 负载均衡(nginx、HAProxy、云 LB)天然支持
  • 认证/授权(OAuth、JWT)有标准方案
  • 防火墙和安全策略对 HTTP 友好
  • DuckDB-Wasm 可以原生使用 Quack(浏览器里直接连远程 DuckDB)

2. 最小往返次数

传统数据库协议(如 PostgreSQL 的 extended query protocol)需要多次往返:Parse → Bind → Execute → Fetch。Quack 的设计目标是:一个查询只需要一次请求-响应往返

// Quack 的请求-响应模型
// 客户端发送:查询 + 参数
// 服务端返回:结果集(流式传输)

// 序列化使用 application/duckdb MIME type
// 这是 DuckDB 内部的 WAL 序列化格式,已经过多年优化

3. 安全优先

Quack 默认绑定 localhost,启动时生成随机认证 token。它不默认启用 SSL——因为 localhost 通信不需要。如果要对外暴露,建议用 nginx 做反向代理并终结 SSL。

# 生产部署推荐:nginx 反向代理
# nginx 配置
server {
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/db.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/db.example.com/privkey.pem;
    
    location / {
        proxy_pass http://127.0.0.1:8361;  # DuckDB Quack 默认端口
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

2.3 性能:百万行秒传

Quack 的性能基准非常亮眼。在官方测试中,百万行数据可以在几秒内通过 socket 传输。这得益于:

  • DuckDB 内部的列式序列化格式天然适合批量传输
  • 避免了行存协议(如 PostgreSQL text format)的序列化开销
  • 支持并行 fetch:客户端可以从多个线程并行拉取结果集
# Python 客户端示例
import duckdb

# 本地 DuckDB 连接远程 Quack 服务
con = duckdb.connect()
con.sql("ATTACH 'quack:ec2-xxx.compute.amazonaws.com' AS remote (TOKEN 'xxx')")

# 查询远程数据——完全透明
result = con.sql("""
    SELECT region, SUM(sales) as total_sales
    FROM remote.analytics.events
    WHERE date >= '2026-01-01'
    GROUP BY region
    ORDER BY total_sales DESC
""")

# 结果已经在本地内存中
df = result.fetchdf()

三、异步 I/O:v2.0 的杀手锏

3.1 问题:同步 I/O 在远程存储上的瓶颈

DuckDB 最初的设计假设是数据在本地 SSD 上。在这个场景下,同步 I/O 没有问题——SSD 延迟低(~100μs)、带宽高(~3GB/s),一个线程发起读请求后很快就能拿到数据。

但随着 DuckDB 被用于查询远程存储(S3、GCS)上的数据,同步 I/O 的问题暴露了:

同步 I/O 的时间线:
线程 1: [发送请求] ----等待网络---- [收到数据] [解码] [计算]
                  ↑ 线程空闲时间 ↑
                  可能几百毫秒到几秒

远程存储的延迟通常在 10-100ms(同区域),这意味着线程在等待 I/O 时有大量空闲时间。64 个 vCPU 的机器上,如果每个线程都在等 I/O,CPU 利用率可能不到 5%。

3.2 异步 I/O 的架构设计

DuckDB v2.0 引入了异步 I/O 管线,核心设计是两个独立的线程池

┌─────────────────────────────────────────────┐
│              DuckDB 异步 I/O 架构            │
├─────────────────────────────────────────────┤
│                                             │
│  REGULAR Pool (Worker Threads)              │
│  ├── 默认:1 per CPU core                   │
│  ├── 执行实际工作:解码、Join、聚合          │
│  └── 空闲时可以执行 I/O 任务                │
│                                             │
│  ASYNC Pool (I/O Threads)                   │
│  ├── 默认:4 × CPU cores (max 256)          │
│  ├── 执行阻塞式 I/O:HTTP 请求              │
│  └── CPU 利用率低,但数量多以隐藏延迟        │
│                                             │
└─────────────────────────────────────────────┘

**Read-Ahead Queue(预读队列)**是异步 I/O 的核心:

// Read-Ahead Queue 的工作流程
// 
// 1. Worker 线程请求新的扫描任务
// 2. 检查队列是否有足够的预读任务
// 3. 如果不够,创建新的 Job(一个 Row Group)和 Fetch Task
// 4. Fetch Task 被调度到 ASYNC Pool
// 5. Job 被加入 Read-Ahead Queue,等待 I/O 完成
// 6. Worker 取出最老的 Job
//    - I/O 完成 → 开始解码
//    - I/O 未完成 → Park,去执行其他 Pipeline 任务
// 7. 最后一个 Fetch Task 完成时,唤醒 Parked 的 Scan Task

class ReadAheadQueue {
    // 队列中同时存在的 Job 数量由 read_ahead_depth 控制
    // 默认:-1(无限,由内存管理器控制预算)
    // 正整数 N:最多 N 个 Job 预读
    // 0:关闭预读,每个 Job 只为自己的数据发起 I/O
    
    std::deque<ReadAheadJob> queue;
    MemoryManager& memory_manager;  // 与 Join/Sort 算子共享内存预算
    
    // 关键:预读和内存管理器协同
    // 当 HashJoin 使用大量内存时,队列会被压缩到 1 个 Job
    // 当内存压力释放,队列自动填满
};

3.3 内存治理:预读不能 OOM

预读的核心风险是:如果解码速度慢于网络传输速度,预读的数据会在内存中堆积,最终导致 OOM

DuckDB 的解决方案是与临时内存管理器(Temporary Memory Manager)协同:

内存分配策略:
┌─────────────────────────────────────┐
│  总可用内存 = buffer_pool_size       │
│                                     │
│  ├── HashJoin 构建端:  40%          │
│  ├── Sort 缓冲区:      30%          │
│  ├── Window 函数:      10%          │
│  └── Read-Ahead Queue: 20%  ← 可配置│
│                                     │
│  当某个 Operator 需要更多内存时,    │
│  会向 MemoryManager 请求,           │
│  Manager 会从其他 Pool 回收          │
└─────────────────────────────────────┘

这意味着:当一个大型 HashJoin 正在运行时,预读队列的深度会被自动压缩到接近 0——扫描行为退化为同步模式。当 HashJoin 完成释放内存后,预读队列自动恢复深度。

-- 配置异步 I/O 参数
SET enable_progressive_spilling = true;  -- 溢出到磁盘
SET memory_limit = '8GB';                -- 总内存限制
SET read_ahead_depth = -1;               -- 默认:由内存管理器控制
SET read_ahead_depth = 64;               -- 或者:固定 64 个 Job 预读
SET read_ahead_depth = 0;                -- 关闭预读(调试用)

3.4 Benchmark:TPC-H Q6 实测

官方在 EC2 r7i.16xlarge(64 vCPU, 512GB RAM)上测试了 TPC-H Q6(SF100,lineitem 表 6 亿行),数据存放在 S3:

版本执行时间加速比
DuckDB v1.5.5(同步)8.230 s1.0x
DuckDB v2.0-dev(异步,内存管理)2.844 s2.9x
DuckDB v2.0-dev(异步,手动调优)2.227 s3.7x

关键观察:

  • 同步 I/O 时,网络吞吐量稳定在 ~5 Gbit/s(25 Gbit/s 的 20%)
  • 异步 I/O 时,网络吞吐量接近 25 Gbit/s(接近网络上限)
  • 手动调优(async_threads=48, http_retries=8, http_retry_wait_ms=50)进一步降低了吞吐量波动
网络吞吐量对比:

v1.5.5 (同步):  ████████░░░░░░░░░░░░░░░░  ~5 Gbit/s
v2.0  (异步):   ████████████████████████░  ~23 Gbit/s
v2.0  (调优):   █████████████████████████  ~25 Gbit/s (接近线速)

3.5 异步 I/O 的工程细节

DuckDB 的异步 I/O 实现有几个值得注意的工程细节:

1. Job 的粒度

对于 Parquet 文件,一个 Job = 一个 Row Group(通常 ~122,880 行)。一个 Row Group 可能被拆分为多个 Fetch Task,取决于查询的 Projection Pushdown:

// Parquet Fetch Task 的拆分逻辑
// 假设查询 SELECT a, c FROM parquet_file WHERE b > 100
// Row Group 包含列 a, b, c, d, e
// 
// 只需要读取 a, c 两列(Projection Pushdown)
// 如果 a, c 在文件中的字节范围相邻,可以合并为一个 Fetch Task
// 否则拆分为两个 Fetch Task

vector<FetchTask> create_fetch_tasks(RowGroup& rg, 
                                     vector<column_t>& projected_columns) {
    vector<FetchTask> tasks;
    for (auto& col_range : merge_adjacent_ranges(rg, projected_columns)) {
        tasks.push_back(FetchTask{
            .url = rg.file_path,
            .offset = col_range.start_byte,
            .length = col_range.end_byte - col_range.start_byte
        });
    }
    return tasks;
}

2. CSV 的特殊处理

CSV 没有 Parquet 那样的列级元信息,所以 Fetch Task 的粒度不同:

  • 一个 Job = 一个扫描边界(固定字节范围)
  • Fetch Task 加载起始 buffer,以及可能需要的后续 buffer(处理跨 buffer 的行边界)

3. Fetch Task 的并发执行

同一个 Job 的多个 Fetch Task 可以并发执行(没有特定的线程分配保证)。所有 Fetch Task 共享一个 countdown,最后一个完成的 Task 会将 Job 标记为 I/O 完成。


四、DuckLake:用 SQL 构建的湖仓一体格式

4.1 湖仓一体的元数据困境

Iceberg 和 Delta Lake 是当前最流行的湖仓一体格式。它们的核心设计是把元数据存储为文件(JSON/Parquet),散布在对象存储中:

Iceberg 元数据结构:
s3://bucket/
├── metadata/
│   ├── v1.metadata.json          # 快照元数据
│   ├── v2.metadata.json
│   ├── v1.manifest-list.json     # 清单列表
│   ├── manifest-0001.avro        # 清单文件
│   └── ...
├── data/
│   ├── part-00000.parquet
│   └── ...
└── ...

这种设计的问题是:元数据操作变成了文件操作。列出所有快照需要扫描 metadata 目录;更新表 schema 需要写新的 metadata.json;做时间旅行查询需要递归解析 manifest 链。

4.2 DuckLake 的核心创新:元数据即 SQL

DuckLake 的核心洞察是:元数据应该存储在数据库里,而不是文件里

-- DuckLake 的元数据存储在 SQL 数据库中
-- 支持 SQLite、PostgreSQL、DuckDB 作为 Catalog

-- 创建一个 DuckLake 表
ATTACH 'ducklake:postgresql://user:pass@host/catalog' AS lake;
CREATE TABLE lake.events (
    id INT PRIMARY KEY,
    event_type VARCHAR,
    ts TIMESTAMP
);

-- 元数据操作变成 SQL 操作:
-- 列出所有表 → SELECT * FROM information_schema.tables
-- 查看表 schema → SELECT * FROM information_schema.columns WHERE table_name = 'events'
-- 查看快照历史 → SELECT * FROM ducklake_snapshots('lake')
-- 时间旅行 → SELECT * FROM lake.events FOR SYSTEM_TIME AS OF TIMESTAMP '2026-01-01'

这种设计带来了几个关键优势:

1. 元数据操作极快

Iceberg 列出所有快照:
  → ListObjects(metadata/) → 可能有数千个文件 → 排序 → 解析 JSON
  
DuckLake 列出所有快照:
  → SELECT * FROM ducklake_snapshots → 单条 SQL,毫秒级

2. 事务由 Catalog 数据库保证

Iceberg 写入流程:
  1. 写 data 文件
  2. 写 manifest 文件
  3. 写 metadata.json
  4. 原子性靠 object storage 的 conditional put 保证
  
DuckLake 写入流程:
  1. 写 data 文件
  2. 在 Catalog 数据库中执行 INSERT/UPDATE(事务由 DB 保证)
  → 天然支持 ACID

3. Data Inlining:小文件问题的终极解决方案

Lakehouse 的经典痛点是「小文件问题」——每次 INSERT 都会产生新文件,导致 metadata 膨胀和查询性能下降。

DuckLake 的解决方案是 Data Inlining:小的写操作(INSERT、UPDATE、DELETE)直接在 Catalog 数据库中执行,不产生新文件:

-- Data Inlining 默认开启,阈值 10 行
CREATE TABLE lake.t (id INT, status VARCHAR);
INSERT INTO lake.t VALUES (1, 'en route'), (2, 'shipped');
DELETE FROM lake.t WHERE id = 1;
UPDATE lake.t SET status = 'delivered' WHERE id = 2;

-- 此时没有产生任何新文件!
FROM ducklake_list_files('lake', 't');  -- 返回空

-- CHECKPOINT 时才刷到对象存储
CHECKPOINT;
FROM ducklake_list_files('lake', 't');  -- 现在有文件了

这意味着:对于高频率的小写入场景(如遥测数据采集),DuckLake 不会产生文件碎片。写入性能接近传统数据库,而查询性能不受影响。

4.3 DuckLake vs Iceberg vs Delta Lake

特性DuckLakeIcebergDelta Lake
元数据存储SQL 数据库文件(JSON/Avro)文件(JSON)
ACID 事务Catalog DB 保证乐观锁 + conditional put乐观锁 + WAL
小文件问题Data Inlining需要 Compaction需要 Compaction
时间旅行SQL 查询解析 manifest 链解析 JSON 日志
生态系统DuckDB/Spark/DataFusion/TrinoSpark/Trino/FlinkSpark/Flink/Trino
删除向量✅(Puffin 文件)✅(v3 规范)
变体类型✅(v3 规范)

五、DuckDB 的查询优化器:被低估的核心

5.1 基于规则的优化(RBO)

DuckDB 的查询优化器借鉴了 PostgreSQL 的 PostgreSQL Query Optimizer(毕竟 parser 就是从 PostgreSQL fork 的),但做了大量改进:

谓词下推(Predicate Pushdown)

-- 原始查询
SELECT * FROM orders 
JOIN customers ON orders.customer_id = customers.id
WHERE orders.amount > 100;

-- 优化后:Filter 被下推到 TableScan
-- orders 的 Filter(amount > 100) 在 Scan 阶段就执行
PhysicalJoin(HashJoin)
├── PhysicalFilter(amount > 100)  ← 下推
│   └── PhysicalTableScan(orders)
└── PhysicalTableScan(customers)

谓词合并(Filter Merge)

-- 多个 Filter 可以合并为一个
WHERE a > 1 AND b < 10 AND c = 'x'
→ PhysicalFilter(a > 1 AND b < 10 AND c = 'x')

5.2 基于代价的优化(CBO)

DuckDB 使用基于代价的优化器来选择 Join 算法和 Join 顺序:

// Join 算法选择
enum class JoinType {
    HASH_JOIN,      // 哈希连接:适合大表 JOIN
    NESTED_LOOP,    // 嵌套循环:适合小表
    INDEX_JOIN,     // 索引连接:有索引时
    PIECEWISE_MERGE // 分段合并:已排序数据
};

// 代价估算基于:
// 1. 表的行数(来自 Statistics)
// 2. 列的基数(NDV - Number of Distinct Values)
// 3. 数据的排序状态
// 4. 内存可用量(影响 HashJoin 是否需要溢出)

5.3 Join 顺序优化

多表 Join 的顺序选择是 NP-hard 问题。DuckDB 使用动态规划(DP)来搜索最优的 Join 顺序:

-- 三表 Join
SELECT * FROM a 
JOIN b ON a.id = b.a_id 
JOIN c ON b.id = c.b_id
WHERE a.x > 100 AND c.y < 50;

-- 可能的 Join 顺序:
-- 1. (a ⋈ b) ⋈ c
-- 2. (a ⋈ c) ⋈ b  ← 可能更优,如果 c.y < 50 过滤掉大部分 c
-- 3. (b ⋈ c) ⋈ a

-- DuckDB 的优化器会:
-- 1. 计算每个表的 Filter 后行数(基于统计信息)
-- 2. 评估每种 Join 顺序的代价
-- 3. 选择代价最低的顺序

六、与竞品的深度对比

6.1 DuckDB vs SQLite

两者都是嵌入式数据库,但设计目标完全不同:

SQLite:  OLTP 嵌入式 → 行存 → 事务优化 → 并发读/写(WAL)
DuckDB:  OLAP 嵌入式 → 列存 → 分析优化 → 并发读/单写

关键差异:

维度SQLiteDuckDB
存储格式B-Tree(行存)列存 + Row Group
查询模型逐行迭代向量化批量处理
并发写WAL 支持不支持多进程写
适用场景应用嵌入式存储数据分析引擎
压缩比低(同类型混合)高(同类型连续)

6.2 DuckDB vs ClickHouse

两者都是列式分析引擎,但架构完全不同:

ClickHouse: 分布式 OLAP → 自研存储引擎 → MergeTree → 多节点
DuckDB:     嵌入式 OLAP → 标准 SQL → 列存 → 单节点
维度ClickHouseDuckDB
部署模式分布式集群嵌入式/C/S
SQL 方言自有方言标准 SQL
写入异步批量写入支持 INSERT/UPDATE/DELETE
事务不支持支持完整 ACID
生态自有生态兼容 PostgreSQL/Parquet/Iceberg
运维复杂度低(单二进制文件)

6.3 DuckDB vs Polars

Polars 是 Rust 编写的 DataFrame 库,和 DuckDB 在数据分析场景有重叠:

Polars:   DataFrame API → Lazy Evaluation → Rust 原生性能
DuckDB:   SQL API → 向量化执行 → C++ 性能

关键差异:

  • API:Polars 用 DataFrame API(df.filter().select().group_by()),DuckDB 用 SQL
  • 生态:DuckDB 可以直接读取 Parquet/CSV/JSON/Iceberg/Delta Lake,Polars 也需要
  • 多语言:DuckDB 有 Python/R/Java/Wasm 绑定,Polars 主要 Rust/Python
  • C/S:DuckDB 有 Quack 协议,Polars 没有

选择建议:如果你习惯 DataFrame API,用 Polars;如果你需要 SQL 或需要 C/S 架构,用 DuckDB。两者不互斥——DuckDB 可以读取 Polars 输出的 DataFrame。


七、生产实践与踩坑清单

7.1 性能调优清单

-- 1. 设置内存限制(重要!不设置会用满所有内存)
SET memory_limit = '16GB';

-- 2. 调整并发线程数
SET threads = 8;  -- 默认是 CPU 核心数

-- 3. 启用并行扫描
SET enable_progressive_spilling = true;

-- 4. 配置异步 I/O(v2.0+)
SET read_ahead_depth = -1;  -- 内存管理器控制
SET async_threads = 32;     -- I/O 线程数

-- 5. 使用 temporary_directory 避免 OOM
SET temporary_directory = '/tmp/duckdb_spill';

-- 6. Parquet 文件优化
-- 每个文件的 Row Group 大小建议 100K-200K 行
-- 压缩算法用 zstd(默认)

7.2 常见踩坑

坑 1:多进程写冲突

# DuckDB 不支持多进程同时写入同一个 .duckdb 文件
# 会报错:Could not set lock on file
# 解决方案:使用 Quack 协议的 C/S 模式

坑 2:内存溢出

-- 查询大表时可能 OOM
-- 解决方案:
SET memory_limit = '8GB';
SET temporary_directory = '/tmp/duckdb_spill';
SET enable_progressive_spilling = true;

坑 3:Parquet 文件过小

# 如果 Parquet 文件太小(<1MB),扫描开销大于收益
# 建议:多个小文件合并为大文件
import duckdb
con = duckdb.connect()
con.sql("""
    COPY (SELECT * FROM read_parquet('small_files/*.parquet'))
    TO 'merged.parquet' (FORMAT PARQUET, ROW_GROUP_SIZE 200000)
""")

坑 4:类型不兼容

-- DuckDB 的类型检查比 PostgreSQL 更严格
-- 例如:VARCHAR 和 INTEGER 不能隐式转换
SELECT * FROM t WHERE id = '123'  -- 可能报错
SELECT * FROM t WHERE id = 123    -- 正确

7.3 DuckDB 适用场景

非常适合

  • 数据科学探索(Jupyter Notebook)
  • ETL 管道中的转换步骤
  • 小团队不想运维数据库集群
  • 应用内嵌入 SQL 分析能力
  • 湖仓一体查询引擎(DuckLake + S3)

⚠️ 需要考虑

  • 高并发写入(>100 TPS)→ 用 PostgreSQL
  • 超大规模数据(>1PB)→ 用 ClickHouse/Spark
  • 实时流处理 → 用 Flink/Kafka Streams

八、总结与展望

DuckDB 的成功不是偶然。它的设计抓住了一个被忽视的需求:很多团队不需要分布式系统,他们只需要一个在笔记本上就能跑的高性能分析引擎

2026 年,DuckDB 的三大创新正在改变游戏:

  1. Quack 协议:让嵌入式数据库走出单进程限制,支持 C/S 架构
  2. 异步 I/O:让远程查询的性能不再是瓶颈
  3. DuckLake:用 SQL 重新定义湖仓一体的元数据管理

展望 v2.0,DuckDB 的路线图还包括:

  • 更多格式的异步 I/O 支持(Native format、JSON)
  • Iceberg V3 完整兼容
  • 更好的多租户支持

一个「不该存在」的数据库,正在证明一个道理:好的架构设计比「正确的技术路线」更重要。


参考资源

  • DuckDB 官方博客:https://duckdb.org/news
  • Quack 协议文档:https://duckdb.org/docs/current/quack/overview.html
  • DuckLake 规范:https://ducklake.select/docs/stable/specification/introduction.html
  • 异步 I/O 深度拆解:https://duckdb.org/2026/07/31/asynchronous-io.html
  • TPC-H 基准测试:https://duckdb.org/docs/current/core_extensions/tpch

推荐文章

html5在客户端存储数据
2024-11-17 05:02:17 +0800 CST
robots.txt 的写法及用法
2024-11-19 01:44:21 +0800 CST
Nginx 防盗链配置
2024-11-19 07:52:58 +0800 CST
rangeSlider进度条滑块
2024-11-19 06:49:50 +0800 CST
程序员茄子在线接单