- 数据库
- OLAP
- 嵌入式数据库
- 数据分析
【免费下载链接】duckdb
DuckDB is an analytical in-process SQL database management system
DuckDB 是一个分析型进程内 SQL 数据库管理系统,其仓库内置了一套完整、可解释的基准测试框架。本文以仓库 benchmark/README.md 为核心主线,系统讲解如何编译基准测试运行器(benchmark_runner)、运行单个/批量基准、解析 CSV 格式的时序输出,并结合 benchmark_runner.cpp、benchmark.hpp 与 interpreted_benchmark.hpp 等源码,深入剖析基准的执行流程、.benchmark文件格式与超时/验证机制,帮助你用这套框架对 DuckDB 的查询性能进行可复现的测量。
一、获取代码并编译基准测试运行器
1.1 克隆仓库
首先从仓库克隆 DuckDB 源码并进入目录:
git clone https://github.com/duckdb/duckdb cd duckdb本文所有路径均以仓库根目录为基准。仓库是只读的,以下操作仅在你的本地克隆中进行。
1.2 通过 Makefile 编译
DuckDB 的根 Makefile 提供了便捷的构建入口。要同时构建基准测试运行器和 TPC-H 相关扩展,执行:
BUILD_BENCHMARK=1 BUILD_TPCH=1 make这里有两个关键环境变量:
BUILD_BENCHMARK=1:开启 benchmark 子目录的构建,最终产出build/release/benchmark/benchmark_runner;BUILD_TPCH=1:启用 TPC-H 扩展(对应 extension/tpch),使tpch目录下的基准可以被加载。
从构建系统看,benchmark/CMakeLists.txt 将benchmark_runner.cpp与interpreted_benchmark.cpp编译为benchmark_runner可执行文件,并链接了duckdb_static静态库、imdb第三方库与test_helpers。CMake 配置项BENCHMARK_ROOT_DIRECTORY用于指定运行器的根目录,默认取PROJECT_SOURCE_DIR(即仓库根目录),它被编译进DUCKDB_ROOT_DIRECTORY宏,供运行器查找 benchmark 文件使用。
1.3 更精细的构建方式
若需要调试符号或指定构建类型,可以参考 benchmark/micro/parser/README.md 中给出的优化构建方式:
BUILD_BENCHMARK=1 make reldebug生成的运行器路径为build/reldebug/benchmark/benchmark_runner。parser 专项基准(见第六节)要求运行器包含CompiledGrammar相关注册,旧版本需要适配。
二、列出全部可用基准
运行器支持通过--list参数打印所有已注册的基准名称:
build/release/benchmark/benchmark_runner --list在源码层面,--list的处理位于 benchmark_runner.cpp:遍历BenchmarkRunner::GetInstance().benchmarks中注册的全部Benchmark对象,将其name输出到 stdout 后立即退出。
基准的注册有两种途径:
- C++ 注册:通过
BenchmarkRunner::RegisterBenchmark()手动注册,benchmark_runner.cpp 将其压入全局 vector; - 解释型(interpreted)基准:
LoadInterpretedBenchmarks()(benchmark_runner.cpp)递归扫描benchmark目录下所有以.benchmark结尾的文件,为每个文件创建一个InterpretedBenchmark实例。
因此--list的输出既包含 C++ 内嵌基准,也包含仓库中数百个.benchmark文件(如 benchmark/micro、benchmark/tpch、benchmark/clickbench 等目录下的基准)。
三、运行单个基准与理解 CSV 时序输出
3.1 运行单个基准
指定一个具体的.benchmark文件路径即可运行单个基准:
build/release/benchmark/benchmark_runner benchmark/micro/nulls/no_nulls_addition.benchmark以 benchmark/micro/nulls/no_nulls_addition.benchmark 为例,它先加载一张约 1 亿行的表,然后执行SELECT MIN(i + 1) FROM integers并校验结果为1。
3.2 CSV 时序输出格式
运行结果以 CSV 格式输出到stderr(注意不是 stdout),包含三列:name、run、timing:
name run timing benchmark/micro/nulls/no_nulls_addition.benchmark 1 0.121234 benchmark/micro/nulls/no_nulls_addition.benchmark 2 0.121702 benchmark/micro/nulls/no_nulls_addition.benchmark 3 0.122948 benchmark/micro/nulls/no_nulls_addition.benchmark 4 0.122534 benchmark/micro/nulls/no_nulls_addition.benchmark 5 0.124102其中timing单位为秒。默认每个基准执行5 次计时运行(timed runs),在此之前还有1 次热运行(warmup run)不计入时序。这一行为对应 benchmark_runner.cpp:timed_runs默认值为 5(成员默认值见 benchmark_runner.hpp),循环执行nruns + 1次,第一次i == 0为热运行,只有i > 0(hotrun)才会输出时序。
热运行的价值在于预热 CPU 缓存、编译好的查询计划等,避免首次执行的冷启动偏差污染测量结果。
3.3 调整计时运行次数
通过--timed-runs n覆盖默认的 5 次:
build/release/benchmark/benchmark_runner benchmark/micro/nulls/no_nulls_addition.benchmark --timed-runs 20参数解析位于 benchmark_runner.cpp:--timed-runs以自定义参数形式收集,随后被解析为uint32_t并写入instance.timed_runs。注意:若传入0会直接报错退出("must be greater than 0"),因为至少需要一次计时。
3.4 将时序写入文件
使用--out=文件路径可以将时序单独写入文件(每行一个时间值,换行分隔):
build/release/benchmark/benchmark_runner benchmark/micro/nulls/no_nulls_addition.benchmark --out=timings.out cat timings.out 0.182472 0.185027 0.184163 0.185281 0.182948注意区别:stderr 的 CSV 输出包含name/run/timing三列;而--out文件只写时序数字本身。实现上,benchmark_runner.cpp 打开输出文件流,LogResult()(benchmark_runner.cpp)在向 stderr 输出后,若out_file处于 good 状态也会将结果写入该文件。此外还有--log=文件用于输出详细的 JSON 日志(来自查询分析器)。
四、用正则表达式选择基准子集
4.1 正则匹配基准
可以传入正则表达式作为名称模式(name pattern)来筛选基准:
build/release/benchmark/benchmark_runner "benchmark/micro/nulls/.*"注意 shell 展开陷阱:正则中的*等字符会被 shell 展开为文件名列表,因此必须使用引号包裹或进行转义,否则命令会失效。
实现层面,benchmark_runner.cpp 使用 RE2 库对基准的name和group分别做FullMatch全匹配;匹配结果按名称排序后运行。若没有任何基准匹配,会返回BenchmarkNotFound错误并打印帮助信息(benchmark_runner.cpp)。例如模式DS.*可匹配 TPC-DS 基准,Q1.*可匹配 TPC-H 的 Q1 系列。
4.2 运行全部基准
不带任何参数时,运行器将执行所有已注册基准:
build/release/benchmark/benchmark_runner对应 benchmark_runner.cpp 中的默认分支:打印表头name\trun\ttiming后逐个运行。这是完整跑一遍仓库基准集(从 micro 到 TPCH/TPCDS/clickbench 等)的标准方式,耗时较长,建议先用小规模子集验证环境。
五、诊断辅助:--info、--query 与 --profile
5.1 --info:查看基准元信息
build/release/benchmark/benchmark_runner benchmark/micro/nulls/no_nulls_addition.benchmark --info display_name:NULL Addition (no nulls) group:micro subgroup:nullsdisplay_name来自.benchmark文件中的name字段,group/subgroup分别对应文件中的group与subgroup字段。实现见 benchmark_runner.cpp,仅打印匹配基准的元信息而不执行。注意--info必须与具体的基准名称/模式一起使用,否则会报InfoWithoutBenchmarkName错误(benchmark_runner.cpp)。
5.2 --query:打印被执行的 SQL
build/release/benchmark/benchmark_runner benchmark/micro/nulls/no_nulls_addition.benchmark --query SELECT MIN(i + 1) FROM integers--query输出基准的 run 查询本身(benchmark_runner.cpp),便于确认某个基准到底在测什么 SQL,无需真正运行。对于 parser 专项基准,--query会按解析顺序打印其所有输入 SQL 文件(见第六节)。
5.3 --profile:输出查询计划树
--profile打印美化后的查询树(query tree),主要用于交互式分析执行计划:
┌─────────────────────────────────────┐ │┌───────────────────────────────────┐│ ││ Query Profiling Information ││ │└───────────────────────────────────┘│ └─────────────────────────────────────┘ SELECT MIN(i + 1) FROM integers ┌─────────────────────────────────────┐ │┌───────────────────────────────────┐│ ││ Total Time: 0.176s ││ │└───────────────────────────────────┘│ └─────────────────────────────────────┘ ┌───────────────────────────┐ │ UNGROUPED_AGGREGATE │ │ min(#0) │ │ 1 │ │ (0.03s) │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ PROJECTION │ │ +(i, 1) │ │ 100000000 │ │ (0.05s) │ └─────────────┬─────────────┘ ┌─────────────┴─────────────┐ │ SEQ_SCAN │ │ integers │ │ i │ │ 100000000 │ │ (0.08s) │ └───────────────────────────┘从计划树可以看到SEQ_SCAN(顺序扫描 1 亿行)→PROJECTION(执行i+1)→UNGROUPED_AGGREGATE(求 MIN)的完整执行链路及各算子耗时。源码中--profile与--detailed-profile分别将profile_info设为NORMAL与DETAILED(benchmark_runner.cpp),最终在 duckdb_benchmark.hpp 中通过PRAGMA profiling_mode=standard/detailed注入连接。
5.4 其他运行器选项
benchmark_runner还支持以下参数(完整列表见 benchmark_runner.cpp 的帮助输出):
| 参数 | 作用 | 默认值 |
|---|---|---|
--threads=n | 设置执行线程数 | 硬件并发数(benchmark_runner.hpp) |
--memory_limit=n | 设置执行内存上限,通过PRAGMA memory_limit生效 | 系统内存的 0.8 倍 |
--out=[file] | 时序输出文件 | 无 |
--log=[file] | JSON 日志输出文件 | 无 |
--info/--query | 打印元信息 / SQL | 无 |
--profile/--detailed-profile | 打印普通 / 详细查询计划 | 无 |
--root-dir | 指定临时数据与 benchmark 查找的根目录 | 仓库根目录(DUCKDB_ROOT_DIRECTORY宏) |
--disable-timeout | 禁用默认 30 秒超时 | 30 秒(benchmark_configuration.hpp) |
--no-summary | 关闭失败汇总输出 | 默认开启汇总 |
[name_pattern] | 基准名称/组正则模式 | 全部 |
超时机制说明:BenchmarkConfiguration::DEFAULT_TIMEOUT = 30秒(benchmark_configuration.hpp)。运行器为每次 run 启动一个监视线程(sleep_thread,benchmark_runner.cpp),每 10ms 检查一次活动状态;超时后调用benchmark->Interrupt(state)(对 DuckDB 基准即conn.Interrupt(),见 duckdb_benchmark.hpp)中断执行,若中断无效则直接退出进程并输出 "Benchmark timeout reached; Interrupt failed."。
六、深入 .benchmark 文件格式
6.1 解释型基准的构成
仓库中绝大多数基准是“解释型基准”(InterpretedBenchmark),由 benchmark/include/interpreted_benchmark.hpp 定义。它以文本文件描述加载、运行、验证三个阶段,核心字段包括:
name:显示名称(对应--info的display_name);group/subgroup:分组信息,用于正则筛选与 group_descriptions.list 的说明索引;load:建表 / 装载数据的 SQL;run:被测查询 SQL;result:期望结果或结果文件,用于验证正确性。
以 benchmark/micro/nulls/no_nulls_addition.benchmark 为例:
name NULL Addition (no nulls) group micro subgroup nulls load CREATE TABLE integers AS SELECT i FROM range(1000) tbl(i), repeat(0, 100000) tbl2(j) run SELECT MIN(i + 1) FROM integers result I 1result I中的I是结果类型标记(此处为 INTEGER),下一行为期望值1。运行器在每次计时后调用Verify()(benchmark_runner.cpp)比对实际结果:不一致时输出INCORRECT并终止该基准,同时写入失败汇总(LogSummary,benchmark_runner.cpp)。查询执行异常输出ERROR,超时输出TIMEOUT,三者均不计入有效时序。
6.2 模板化的 .benchmark.in
大型基准集(如 TPC-H)通过模板文件批量生成。以 benchmark/tpch/sf1/q01.benchmark 为例:
template benchmark/tpch/sf1/tpch_sf1.benchmark.in QUERY_NUMBER=1 QUERY_NUMBER_PADDED=01模板 benchmark/tpch/sf1/tpch_sf1.benchmark.in 通过变量替换填充出完整基准:
include benchmark/tpch/tpch_load.benchmark.in name Q${QUERY_NUMBER_PADDED} group tpch subgroup sf${sf} run extension/tpch/dbgen/queries/q${QUERY_NUMBER_PADDED}.sql result extension/tpch/dbgen/answers/sf1/q${QUERY_NUMBER_PADDED}.csv sf=1 result extension/tpch/dbgen/answers/sf100/q${QUERY_NUMBER_PADDED}.csv sf=100它通过include引入共享的加载脚本(tpch_load.benchmark.in),run与result直接引用 extension/tpch/dbgen/queries 目录中的 SQL 文件和答案 CSV。这种"模板 + 变量 + include"机制让 TPC-H 22 个查询只需维护一份模板与少量变量文件。result后附加的sf=1/sf=100为结果参数,对应不同数据规模下的答案文件。
6.3 基准组与说明文档
benchmark/group_descriptions.list 以[group][subgroup]形式为每个基准组提供人类可读描述,例如:
[tpch]:TPC-H 是面向 OLAP 系统的行业标准基准,包含 22 个查询,测试系统不同层面的优化;[aggregate]:聚合微基准集,测量原始聚合性能;[csv]:CSV 读写性能基准集;[join]/[order]/[window]/[index]等分别覆盖对应算子的微基准。
这些描述与基准文件中的group字段一一对应,可用于理解仓库中基准集的覆盖范围。
七、Parser-only 专项微基准
除了执行查询的基准外,仓库还提供了仅测量 SQL 解析性能的专项基准,详见 benchmark/micro/parser/README.md。它们直接调用Parser::ParseQuery(),不进行绑定、优化与执行,也不需要建表或数据。
7.1 测量边界
每个计时批次包含:构造全新的Parser(复用已编译的语法)、执行ParseQuery(分词、匹配、生成 SQL 语句 AST)、语句数校验与析构。语法构建、查询生成、文件加载与 SQL 执行均不计时,测的是热语法解析而非冷启动。ParserGrammarConstruction是例外:它计时 500 次独立的CompiledGrammar::Create()调用(含析构),衡量语法构建的可重复成本。
7.2 固定工作负载
README 中的关键负载(节选):
| Benchmark | 输入 | 每批次调用次数 | 每次调用语句数 |
|---|---|---|---|
ParserKeywordIdentifiers | 含关键字标识符的 SELECT | 2,000 | 1 |
ParserNestedExpressions | 32 层嵌套coalesce | 1,000 | 1 |
ParserMalformedSelect | select (((((((((((((; | 1,000 | 期望语法错误 |
ParserStatements | 32 条 SELECT | 1,000 | 32 |
ParserTPCH | 22 条 TPC-H 查询 × 50 | 1,100 | 1 |
ParserTPCDS | 99 条 TPC-DS 查询 × 10 | 990 | 1 |
ParserGrammarConstruction | 构建/销毁基础语法 | 500 次构建 | 无 SQL |
运行器报告的“秒/固定批次”可换算为每次ParseQuery的微秒数:秒 × 1,000,000 ÷ 调用次数。这些基准还要求修改工作负载时使用新的基准名称以保留历史可对比性。
7.3 构建与运行 parser 基准
从仓库根目录执行:
BUILD_BENCHMARK=1 make reldebug为保证运行时工件不污染代码检出目录,建议使用临时根目录并符号链接输入:
parser_benchmark_tmp=$(mktemp -d /tmp/duckdb-parser-bench.XXXXXX) ln -s "$PWD/benchmark" "$parser_benchmark_tmp/benchmark" ln -s "$PWD/extension" "$parser_benchmark_tmp/extension" build/reldebug/benchmark/benchmark_runner 'Parser.*' \ --root-dir "$parser_benchmark_tmp" --timed-runs 10--root-dir告诉运行器到临时目录查找benchmark目录,同时保持 SQL 输入文件可见。若要运行单个用例,将'Parser.*'替换为ParserNestedExpressions等具体名称。
7.4 用回归脚本对比版本
scripts/regression/test_runner.py 可用于对比两个版本运行器的 parser 性能:
python3 scripts/regression/test_runner.py \ --old /path/to/base/build/reldebug/benchmark/benchmark_runner \ --new /path/to/current/build/reldebug/benchmark/benchmark_runner \ --benchmarks .github/regression/parser.csv \ --samples 10 --threads 1注意事项:两个二进制都必须包含这些 C++ 基准注册(旧版本可能需要适配CompiledGrammar与ParserOptions::compiled_grammar);对比时使用相同的优化构建设置、同一台机器与电源模式,避免并发构建干扰;该回归清单由 CI 的Bench Parser矩阵任务自动执行。
八、源码视角:一次基准运行的完整生命周期
结合 benchmark_runner.cpp 的RunBenchmark(),一次完整运行遵循以下流程:
- 初始化:调用
benchmark->Initialize(configuration)创建BenchmarkState(对解释型基准即建立连接并执行load查询),若抛出异常则输出ERROR并跳过; - 预热:执行第 0 次 run(
hotrun == false),不计时; - 计时循环:对第 1..n 次 run,
profiler.Start()启动计时(基于duckdb/common/profiler.hpp的Profiler),执行Run(state),profiler.End()停止计时; - 验证:
Verify(state)比对结果,空字符串表示正确(输出时序),否则输出INCORRECT与失败详情; - 清理:每次 run 后调用
Cleanup(state),全部结束后调用Finalize(); - 失败汇总:
main()(benchmark_runner.cpp)在全部基准结束后,将汇总的失败信息打印到 stdout(可用--no-summary关闭)。
超时监视线程与计时并行运行,10ms 粒度轮询,超时优先于验证逻辑。整个生命周期保证了"先验证正确性、再信任时序"的测量原则——一个结果错误的基准绝不会产生可信的性能数字。
九、实践建议与适用前提
- 测量环境:时序高度依赖机器与构建配置,跨机器、跨构建类型(release/reldebug/debug)的数字不可直接比较;基准对比应在相同硬件、相同优化级别与空闲负载下进行。
- 选择基准集:快速验证用
--list+ 正则选取单个微基准;完整评估用无参数全量运行;专项分析用--info/--query/--profile组合。 - 结果导出:
--out输出的纯时序文件适合脚本化处理(如 scripts/regression 下的回归对比脚本);stderr 的 CSV 格式适合人工阅读。 - 默认行为:默认 5 次计时 + 1 次预热、默认 30 秒超时、默认使用硬件并发线程数,均可在命令行覆盖。
- 新增基准:在
benchmark目录下按name/group/subgroup/load/run/result格式添加.benchmark文件即可被自动加载;大型基准集可参考 TPC-H 的模板 + include 机制复用装载脚本。
十、总结
DuckDB 的 benchmark 框架是一个集解释型基准文件、C++ 内嵌基准、正则筛选、CSV 时序输出、超时保护、结果验证与失败汇总于一体的完整测量系统。通过BUILD_BENCHMARK=1 BUILD_TPCH=1 make编译出 benchmark_runner,即可用--list发现基准、用正则定位目标、用--timed-runs/--out/--profile控制测量细节,再结合.benchmark文件格式与源码级的生命周期理解,对 DuckDB 的解析、执行与存储链路做精准、可复现的性能剖析。对于 parser 专项优化,仓库还提供了独立于查询执行的Parser.*基准与配套的回归对比脚本,为深入性能调优提供了完整闭环。
- 数据库
- OLAP
- 嵌入式数据库
- 数据分析
【免费下载链接】duckdb
DuckDB is an analytical in-process SQL database management system
相关推荐
libgit2 CLI 基准测试(Benchmark)框架完全指南:运行、编写与结果解读
libgit2 CLI 基准测试(Benchmark)框架完全指南:运行、编写与结果解读 本指南以 benchmarks/cli/README.md https
开发工具基于 Phoenix 构建 CLI Agent 评估框架:从 Evaluator 编写到 Benchmark 基准测试实战
基于 Phoenix 构建 CLI Agent 评估框架:从 Evaluator 编写到 Benchmark 基准测试实战 导读 本文围绕 Phoenix 官方
可观测性AI 评测LLMOpsAI 应用人工智能ModelScope 本地部署:15 分钟在你自己机器上跑通第一个模型
ModelScope 本地部署:15 分钟在你自己机器上跑通第一个模型 想跑个情感分析,却得先把数据传到别人服务器上?隐私不放心,还得分心等网络。ModelSc
人工智能大模型微调模型评测预训练
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考