DuckDB 全方位指南:分析型数据库中的SQLite

DuckDB 全方位指南:分析型数据库中的”SQLite”

摘要:本文将系统介绍 DuckDB 的核心设计、与主流数据库的深度对比、C++ 完整集成方案(含 CMake 配置)、高性能 Parquet 读取实战,以及生产环境中的最佳实践。无论你是想替换 Pandas 处理大数据,还是在 C++ 应用中内嵌分析引擎,这篇文章都能给你完整答案。


目录

  1. 什么是 DuckDB?
  2. 核心架构原理
  3. 深度对比:DuckDB vs. 竞品数据库
  4. C++ 集成完整指南
    • 4.1 CMake 集成配置
    • 4.2 Amalgamation 方式
    • 4.3 核心 API 详解
    • 4.4 高性能 Parquet 读取
    • 4.5 批量写入:Appender 接口
  5. Python 快速上手(附对比)
  6. 适用场景总结
  7. 生产环境最佳实践
  8. 常见问题 FAQ

1. 什么是 DuckDB?

DuckDB 是一款专注于 OLAP(联机分析处理) 的嵌入式关系型数据库。如果说 SQLite 是为了解决”嵌入式事务处理(OLTP)”而生,那么 DuckDB 的出现就是为了解决本地大规模数据的高速分析

它采用 C++11 编写,无外部依赖,核心是一个高性能的列式矢量化查询执行引擎

核心特性

特性 说明
零拷贝集成 与 Pandas、Polars、Arrow 等内存格式直接交换数据,无需序列化
列式存储 针对聚合查询(SUM、AVG、GROUP BY)进行了深度优化
单文件运行 整个数据库就是一个 .duckdb 文件,无需安装、配置和启动服务器进程
现代 SQL 支持窗口函数、复杂连接、CTE、直接查询 Parquet/CSV/JSON 文件
超内存计算 数据量超出 RAM 时自动 Spill 到磁盘,不报 OOM
多线程并行 单查询自动利用所有 CPU 核心

2. 核心架构原理

理解 DuckDB 为何快,需要了解两个关键设计:

2.1 列式存储 vs. 行式存储

1
2
3
4
5
6
7
8
9
10
11
行式存储(SQLite / MySQL):
┌────┬────────┬──────────┬──────────┐
│ id │ name │ amount │ category │ ← 一行存在一起
├────┼────────┼──────────┼──────────┤
1 │ Alice │ 100.5 │ Tech │
2 │ Bob │ 200.0 │ Office │
└────┴────────┴──────────┴──────────┘

列式存储(DuckDB / ClickHouse):
amount: [100.5, 200.0, 150.0, ...] ← 同一列连续存储
category: ["Tech", "Office", "Tech", ...]

当执行 SELECT SUM(amount) FROM sales WHERE category = 'Tech' 时,列式存储只需读取 amountcategory 两列,跳过其他所有列,I/O 量可以减少 90%+。

2.2 矢量化执行引擎

DuckDB 不是逐行处理,而是每次处理一批(默认 2048 行)数据,利用 CPU 的 SIMD 指令批量计算:

1
2
传统逐行执行:   row1 → compute → row2 → compute → row3 → compute ...
矢量化执行: [row1, row2, ..., row2048] → SIMD批量compute → 下一批

这使得 DuckDB 在聚合运算上比行式数据库快 10x ~ 100x


3. 深度对比:DuckDB vs. 竞品数据库

维度 DuckDB SQLite ClickHouse(单机) Pandas / Polars
主要定位 本地分析 (OLAP) 本地事务 (OLTP) 大规模数仓 (OLAP) 数据处理库
数据布局 列式 行式 列式 内存对象
部署方式 嵌入式 Library 嵌入式 Library 服务端 Server 编程语言库
查询引擎 矢量化执行 逐行处理 矢量化 + 分布式 API 调用
擅长场景 100GB 内复杂聚合 高频点查、增删改 PB 级、高并发分析 数据清洗、特征工程
内存管理 智能缓存,支持超内存 极简缓存 贪婪占用 易 OOM
并发写入 单写多读 单写多读 高并发写入 N/A
安装复杂度 极低(单文件) 极低(单文件) 中等(服务配置) 低(pip install)
SQL 标准 现代 SQL(窗口函数等) 基础 SQL 方言较多 非标准

选型决策树

1
2
3
4
5
6
7
你的数据量 > 1TB?
├── 是 → 需要多机集群?
│ ├── 是 → ClickHouse / Spark
│ └── 否 → ClickHouse 单机
└── 否 → 主要是写入还是查询?
├── 高频写入(OLTP) → SQLite / PostgreSQL
└── 分析查询(OLAP) → DuckDB ✅

4. C++ 集成完整指南

4.1 CMake 集成配置

DuckDB 提供两种主流 C++ 集成方式。

方式一:FetchContent 自动下载(推荐,无需手动管理依赖)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cmake_minimum_required(VERSION 3.14)
project(MyAnalyticsApp CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

include(FetchContent)

# 自动拉取 DuckDB,固定版本保证可重现构建
FetchContent_Declare(
duckdb
GIT_REPOSITORY https://github.com/duckdb/duckdb.git
GIT_TAG v1.1.0 # 替换为你需要的版本
GIT_SHALLOW TRUE # 浅克隆,加速下载
)

# 关闭不需要的扩展,大幅缩短编译时间
set(BUILD_UNITTESTS OFF CACHE BOOL "" FORCE)
set(BUILD_SHELL OFF CACHE BOOL "" FORCE)

FetchContent_MakeAvailable(duckdb)

add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE duckdb)

方式二:Amalgamation(单文件集成,适合小项目或快速原型)

duckdb.hppduckdb.cpp官方 Release 页面 下载后放入项目目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.14)
project(MyAnalyticsApp CXX)

set(CMAKE_CXX_STANDARD 17)

# 直接将 duckdb.cpp 作为项目源文件编译
add_executable(my_app
main.cpp
third_party/duckdb/duckdb.cpp # 单文件,包含全部实现
)

target_include_directories(my_app PRIVATE third_party/duckdb)

# DuckDB 需要线程支持
find_package(Threads REQUIRED)
target_link_libraries(my_app PRIVATE Threads::Threads ${CMAKE_DL_LIBS})

编译说明:Amalgamation 的 duckdb.cpp 文件较大,首次编译耗时约 2~5 分钟(取决于机器性能)。建议使用 cmake -DCMAKE_BUILD_TYPE=Release 并开启多线程编译 make -j$(nproc)


4.2 核心 C++ API 详解

以下是一个完整的 C++ 示例,覆盖数据库初始化、建表、插入、查询全流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include "duckdb.hpp"
#include <iostream>
#include <stdexcept>

using namespace duckdb;

int main() {
// ── 1. 初始化数据库 ──────────────────────────────────────
// nullptr → 纯内存数据库(程序退出后数据消失)
// "my_data.duckdb" → 持久化到文件
DuckDB db(nullptr);
Connection con(db);

// ── 2. 建表与插入 ────────────────────────────────────────
con.Query("CREATE TABLE sales ("
" id INTEGER PRIMARY KEY,"
" amount DOUBLE NOT NULL,"
" category VARCHAR NOT NULL,"
" ts TIMESTAMP DEFAULT current_timestamp"
")");

con.Query("INSERT INTO sales(id, amount, category) VALUES"
" (1, 100.5, 'Tech'),"
" (2, 200.0, 'Office'),"
" (3, 150.0, 'Tech'),"
" (4, 80.0, 'Office'),"
" (5, 320.0, 'Tech')");

// ── 3. 聚合查询(窗口函数示例)───────────────────────────
const char* sql = R"(
SELECT
category,
SUM(amount) AS total,
AVG(amount) AS avg_amount,
RANK() OVER (ORDER BY SUM(amount) DESC) AS rank
FROM sales
GROUP BY category
ORDER BY total DESC
)";

auto result = con.Query(sql);

// 检查查询是否出错
if (result->HasError()) {
throw std::runtime_error("Query error: " + result->GetError());
}

// ── 4. 结果迭代(DataChunk 批量获取,性能极高)───────────
std::cout << "Category | Total | Avg | Rank\n";
std::cout << "---------|--------|--------|-----\n";

while (auto chunk = result->Fetch()) {
for (idx_t row = 0; row < chunk->size(); row++) {
std::cout
<< chunk->GetValue(0, row).ToString() << " | "
<< chunk->GetValue(1, row).ToString() << " | "
<< chunk->GetValue(2, row).ToString() << " | "
<< chunk->GetValue(3, row).ToString() << "\n";
}
}

return 0;
}

输出示例:

1
2
3
4
Category | Total  | Avg    | Rank
---------|--------|--------|-----
Tech | 570.5 | 190.17 | 1
Office | 280.0 | 140.0 | 2

4.3 高性能 Parquet 读取

Parquet 是 DuckDB 最强大的杀手锏场景之一。无需任何数据导入,直接在 SQL 中把 .parquet 文件当表查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "duckdb.hpp"
#include <iostream>
#include <filesystem>

using namespace duckdb;

int main() {
DuckDB db(nullptr);
Connection con(db);

// ── 场景一:直接查询单个 Parquet 文件 ───────────────────
auto result = con.Query(
"SELECT category, SUM(amount) AS total, COUNT(*) AS cnt "
"FROM 'sales_data.parquet' "
"GROUP BY category "
"ORDER BY total DESC "
"LIMIT 10"
);

// ── 场景二:读取整个目录(通配符)───────────────────────
// DuckDB 会自动合并所有匹配的 Parquet 文件(Hive 分区也支持!)
auto result2 = con.Query(
"SELECT year, SUM(revenue) "
"FROM 'data/sales_*.parquet' " // 通配符
"GROUP BY year"
);

// ── 场景三:Hive 分区目录(自动推断分区列)──────────────
// 目录结构:data/year=2024/month=01/part-0.parquet
auto result3 = con.Query(
"SELECT year, month, SUM(revenue) "
"FROM read_parquet('data/**/*.parquet', hive_partitioning=true) "
"WHERE year = 2024 "
"GROUP BY year, month"
);

// ── 场景四:Parquet 与内存表 JOIN ───────────────────────
con.Query("CREATE TABLE dim_product AS SELECT * FROM 'products.parquet'");
auto result4 = con.Query(
"SELECT p.name, SUM(s.amount) AS total_revenue "
"FROM 'sales_data.parquet' s "
"JOIN dim_product p ON s.product_id = p.id "
"GROUP BY p.name"
);

// ── 场景五:查询结果导出为 Parquet ──────────────────────
con.Query(
"COPY ("
" SELECT category, SUM(amount) AS total FROM 'sales_data.parquet' GROUP BY ALL"
") TO 'output_summary.parquet' (FORMAT PARQUET, COMPRESSION ZSTD)"
);

// ── 结果处理 ─────────────────────────────────────────────
if (!result->HasError()) {
while (auto chunk = result->Fetch()) {
for (idx_t row = 0; row < chunk->size(); row++) {
std::cout << chunk->GetValue(0, row).ToString()
<< ": " << chunk->GetValue(1, row).ToString() << "\n";
}
}
}

return 0;
}

性能参考:在一台普通 MacBook Pro(M2)上,DuckDB 处理一个 10GB Parquet 文件的 GROUP BY 聚合查询,通常可在 3~8 秒内完成,同等任务 Pandas 需要先将文件完整载入内存(约 30GB 展开后),且无法利用多核。


4.4 批量写入:Appender 接口

当需要大批量插入数据时,应使用 duckdb::Appender 接口,它绕过 SQL 解析层,速度比 INSERT INTO10x ~ 50x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include "duckdb.hpp"
#include <iostream>
#include <chrono>
#include <random>

using namespace duckdb;

int main() {
DuckDB db("benchmark.duckdb");
Connection con(db);

con.Query("CREATE TABLE IF NOT EXISTS events ("
" id BIGINT,"
" user_id INTEGER,"
" event VARCHAR,"
" score DOUBLE,"
" ts TIMESTAMP"
")");

// ── 使用 Appender 批量插入 100 万行 ─────────────────────
auto start = std::chrono::high_resolution_clock::now();

{
// Appender 的生命周期结束时自动 Flush
Appender appender(con, "events");

std::mt19937 rng(42);
std::uniform_int_distribution<int> user_dist(1, 10000);
std::uniform_real_distribution<double> score_dist(0.0, 100.0);
std::vector<std::string> event_types = {"click", "view", "purchase", "share"};

for (int64_t i = 0; i < 1'000'000; i++) {
appender.BeginRow();
appender.Append<int64_t>(i); // id
appender.Append<int32_t>(user_dist(rng)); // user_id
appender.Append<std::string>(event_types[i % 4]); // event
appender.Append<double>(score_dist(rng)); // score
appender.AppendDefault(); // ts → 使用默认值
appender.EndRow();

// 每 10 万行手动 Flush 一次(可选,防止内存积压)
if (i % 100'000 == 0) {
appender.Flush();
}
}
// 析构时自动调用 appender.Close()
}

auto end = std::chrono::high_resolution_clock::now();
double ms = std::chrono::duration<double, std::milli>(end - start).count();
std::cout << "插入 100 万行耗时: " << ms << " ms\n";

// 验证
auto result = con.Query("SELECT COUNT(*), AVG(score) FROM events");
while (auto chunk = result->Fetch()) {
std::cout << "总行数: " << chunk->GetValue(0, 0).ToString()
<< ",平均分: " << chunk->GetValue(1, 0).ToString() << "\n";
}

return 0;
}

性能对比(100 万行插入):

方式 耗时(参考值)
INSERT INTO ... VALUES 逐行 ~60,000 ms
INSERT INTO ... VALUES 批量(每次1000行) ~3,000 ms
Appender 接口 ~300 ms

4.5 多线程与连接池

DuckDB 支持单写多读模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "duckdb.hpp"
#include <thread>
#include <vector>

using namespace duckdb;

int main() {
// 数据库实例在线程间共享
DuckDB db("shared.duckdb");

// 每个线程创建独立的 Connection(线程安全)
auto reader_task = [&db](int id) {
Connection con(db); // Connection 不跨线程共享
auto result = con.Query(
"SELECT COUNT(*) FROM events WHERE user_id = " + std::to_string(id)
);
// 处理结果...
};

std::vector<std::thread> threads;
for (int i = 0; i < 8; i++) {
threads.emplace_back(reader_task, i * 100);
}
for (auto& t : threads) t.join();

return 0;
}

注意:DuckDB 在同一时间只允许一个写入连接。如果你的应用有并发写入需求,请考虑使用批量写入队列,或改用 PostgreSQL。


5. Python 快速上手(附对比)

虽然本文主要面向 C++ 开发者,但 Python 是 DuckDB 最流行的入口,附上对比供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import duckdb
import pandas as pd
import time

# ── DuckDB 处理 CSV/Parquet(无需 Pandas 中间层)────────────
con = duckdb.connect()

# 直接查询文件,无需加载到内存
start = time.time()
result = con.execute("""
SELECT
category,
SUM(amount) AS total,
AVG(amount) AS avg,
COUNT(*) AS cnt
FROM 'large_dataset.parquet' -- 可以是几十 GB 的文件
WHERE ts >= '2024-01-01'
GROUP BY category
ORDER BY total DESC
""").df() # 结果直接返回为 Pandas DataFrame
print(f"DuckDB 耗时: {time.time() - start:.2f}s")

# ── 与 Pandas 零拷贝交互 ─────────────────────────────────────
df = pd.DataFrame({
"id": range(1_000_000),
"value": range(1_000_000)
})

# 直接在 Pandas DataFrame 上运行 SQL!
result = duckdb.sql("SELECT SUM(value) FROM df").fetchone()
print(f"Sum: {result[0]}")

Pandas vs. DuckDB(处理 5GB CSV 文件):

操作 Pandas DuckDB
加载文件 ~120s,需 15GB RAM 无需加载,按需流式读取
GROUP BY 聚合 ~45s ~8s
内存占用峰值 ~15GB ~2GB
超内存处理 OOM 崩溃 ✅ 自动溢出到磁盘

6. 适用场景总结

✅ 推荐使用 DuckDB 的场景

1. 替代 Pandas 处理超大数据集
当数据集接近或超过内存容量,Pandas 报 OOM 时。DuckDB 支持超内存计算,自动 Spill 到磁盘。

2. C++ 应用内嵌分析引擎
开发桌面软件、工业系统、游戏后台需要向用户展示复杂统计报表时,DuckDB 作为 Library 嵌入,无需用户安装任何数据库服务。

3. 数据工程 ETL 与日志处理
编写一条 SQL 处理数 GB 的 .log.parquet.csv 文件,速度远超 Python 脚本,且不需要搭建 Spark 集群。

4. Serverless / Lambda 函数
配合 AWS Lambda 或 Cloud Functions,拉取 S3 上的 Parquet 数据块进行计算,冷启动时间极短(毫秒级),无需维护数据库服务。

5. 数据科学与 BI 原型
在 Jupyter Notebook 中替代 Pandas,或作为轻量级 BI 工具的查询引擎。

❌ 不推荐使用 DuckDB 的场景

1. 高并发点写
需要成千上万客户端同时写入(如社交平台评论、IoT 传感器数据),请选择 PostgreSQLMySQL

2. 超低延迟点查
通过主键查找单条记录(如用户登录验证),SQLite 的延迟更低,且 B-Tree 索引更擅长此类工作。

3. 分布式多机集群
数据量 PB 级且需要水平扩展,请选择 ClickHouseApache Spark


7. 生产环境最佳实践

7.1 内存与性能调优

1
2
3
4
5
6
7
8
9
10
11
-- 限制最大内存(超出后自动溢出到磁盘)
SET max_memory = '8GB';

-- 设置并行线程数(默认为 CPU 核心数)
SET threads = 4;

-- 设置临时文件目录(用于超内存时的 Spill)
SET temp_directory = '/data/tmp/duckdb_spill';

-- 开启进度条(长查询时显示进度)
PRAGMA enable_progress_bar;

7.2 数据持久化与备份

1
2
3
4
5
6
7
8
// 将内存数据库导出到文件
con.Query("EXPORT DATABASE 'backup_dir' (FORMAT PARQUET)");

// 从备份恢复
con.Query("IMPORT DATABASE 'backup_dir'");

// 检查点(将 WAL 合并到主数据库文件)
con.Query("CHECKPOINT");

7.3 索引使用(DuckDB 的索引与众不同)

DuckDB 是列式数据库,大多数查询不需要手动建索引。但对于高频的等值过滤,可以使用MIN-MAX 索引(自动)或ART 索引

1
2
3
4
5
-- ART 索引:适合等值查询和范围查询
CREATE INDEX idx_user ON events(user_id);

-- 查询规划器会自动决定是否使用索引
EXPLAIN SELECT * FROM events WHERE user_id = 42;

注意:DuckDB 的索引对点查(WHERE id = 42)有帮助,但对聚合查询(SUM, GROUP BY)通常没有收益,因为列式存储本身已经足够高效。

7.4 文件格式选择建议

格式 推荐场景 说明
Parquet + ZSTD 归档、长期存储 最高压缩率,列式,DuckDB 原生支持
Parquet + Snappy 频繁读取的分析数据 解压速度快,压缩率适中
CSV 数据交换、人工查看 无压缩,体积大,但通用性最强
DuckDB 原生文件 单机应用的持久化 支持事务、增量更新
Arrow IPC 跨语言内存共享 零拷贝交换,适合与 Python/Rust 协作

8. 常见问题 FAQ

Q1:DuckDB 支持多进程并发访问同一个 .duckdb 文件吗?

不支持。DuckDB 使用文件锁,同一时间只允许一个进程打开数据库文件进行写入。如果需要多进程访问,可以考虑将 DuckDB 作为只读数据仓库,配合写时复制策略。


Q2:DuckDB 适合实时数据流吗?

不太适合。DuckDB 没有流处理引擎,也不支持 SUBSCRIBE/WATCH 语义。对于实时流处理,可以用 Kafka + Flink 处理后将结果存为 Parquet,再由 DuckDB 做离线分析查询。


Q3:Amalgamation 和正常 CMake 构建有什么区别?

Amalgamation 是官方将所有源文件合并为单个 duckdb.cppduckdb.hpp,优点是集成简单、无需处理依赖;缺点是这个文件非常大(编译慢),且无法单独更新某个模块。正式生产项目建议用 CMake FetchContent 方式,便于版本管理和增量编译。


Q4:如何在 C++ 中处理 NULL 值?

1
2
3
4
5
6
7
8
9
10
11
auto result = con.Query("SELECT id, amount FROM sales");
while (auto chunk = result->Fetch()) {
for (idx_t row = 0; row < chunk->size(); row++) {
Value val = chunk->GetValue(1, row); // amount 列
if (val.IsNull()) {
std::cout << "NULL\n";
} else {
std::cout << val.GetValue<double>() << "\n";
}
}
}

Q5:DuckDB 支持 JSON 查询吗?

支持。DuckDB 内置 JSON 扩展:

1
2
3
4
5
6
7
8
-- 直接查询 JSON 文件
SELECT json_extract(data, '$.user.name') AS name
FROM 'events.json';

-- 解析 JSON 字符串列
SELECT json_extract_string(payload, '$.action') AS action
FROM logs
WHERE json_extract_string(payload, '$.user_id') = '42';

总结

DuckDB 填补了一个长期以来的空白:本地嵌入式 OLAP。它让你无需搭建任何服务,就能在 C++、Python、R 等语言中直接对 GB 乃至百 GB 级数据运行复杂的 SQL 分析查询。

核心记忆点:

  • 替换 Pandas 的首选,超内存不 OOM
  • C++ 嵌入分析的最佳选择,Amalgamation 五分钟跑通 Demo
  • 直接查询 Parquet/CSV/JSON,无需 ETL
  • 批量写入用 Appender,比 INSERT 快 10x~50x
  • 不适合高并发写入和分布式场景

参考资源


DuckDB 全方位指南:分析型数据库中的SQLite
https://www.psnow.sbs/2026/03/24/DuckDB-全方位指南:分析型数据库中的SQLite/
作者
Psnow
发布于
2026年3月24日
许可协议