DuckDB 全方位指南:分析型数据库中的”SQLite”
摘要 :本文将系统介绍 DuckDB 的核心设计、与主流数据库的深度对比、C++ 完整集成方案(含 CMake 配置)、高性能 Parquet 读取实战,以及生产环境中的最佳实践。无论你是想替换 Pandas 处理大数据,还是在 C++ 应用中内嵌分析引擎,这篇文章都能给你完整答案。
目录
什么是 DuckDB?
核心架构原理
深度对比:DuckDB vs. 竞品数据库
C++ 集成完整指南
4.1 CMake 集成配置
4.2 Amalgamation 方式
4.3 核心 API 详解
4.4 高性能 Parquet 读取
4.5 批量写入:Appender 接口
Python 快速上手(附对比)
适用场景总结
生产环境最佳实践
常见问题 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' 时,列式存储只需读取 amount 和 category 两列,跳过其他所有列,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 你的数据量 > 1 TB? ├── 是 → 需要多机集群? │ ├── 是 → 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) 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.hpp 和 duckdb.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 )add_executable (my_app main.cpp third_party/duckdb/duckdb.cpp )target_include_directories (my_app PRIVATE third_party/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 () { DuckDB db (nullptr ) ; Connection con (db) ; 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')" ); 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 ()); } 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) ; 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" ); auto result2 = con.Query ( "SELECT year, SUM(revenue) " "FROM 'data/sales_*.parquet' " "GROUP BY year" ); auto result3 = con.Query ( "SELECT year, month, SUM(revenue) " "FROM read_parquet('data/**/*.parquet', hive_partitioning=true) " "WHERE year = 2024 " "GROUP BY year, month" ); 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" ); 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 INTO 快 10x ~ 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" ")" ); auto start = std::chrono::high_resolution_clock::now (); { 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); appender.Append <int32_t >(user_dist (rng)); appender.Append <std::string>(event_types[i % 4 ]); appender.Append <double >(score_dist (rng)); appender.AppendDefault (); appender.EndRow (); if (i % 100'000 == 0 ) { appender.Flush (); } } } 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" ) ; auto reader_task = [&db](int id) { Connection con (db); 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 duckdbimport pandas as pdimport time 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() print (f"DuckDB 耗时: {time.time() - start:.2 f} s" ) df = pd.DataFrame({ "id" : range (1_000_000 ), "value" : range (1_000_000 ) }) 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 传感器数据),请选择 PostgreSQL 或 MySQL 。
2. 超低延迟点查 通过主键查找单条记录(如用户登录验证),SQLite 的延迟更低,且 B-Tree 索引更擅长此类工作。
3. 分布式多机集群 数据量 PB 级且需要水平扩展,请选择 ClickHouse 或 Apache Spark 。
7. 生产环境最佳实践 7.1 内存与性能调优 1 2 3 4 5 6 7 8 9 10 11 SET max_memory = '8GB' ;SET threads = 4 ;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'" ); con.Query ("CHECKPOINT" );
7.3 索引使用(DuckDB 的索引与众不同) DuckDB 是列式数据库,大多数查询不需要手动建索引 。但对于高频的等值过滤,可以使用MIN-MAX 索引 (自动)或ART 索引 :
1 2 3 4 5 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.cpp 和 duckdb.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); 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 SELECT json_extract(data, '$.user.name' ) AS nameFROM 'events.json' ;SELECT json_extract_string(payload, '$.action' ) AS actionFROM logsWHERE 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
不适合高并发写入和分布式场景
参考资源