DuckDB 插件开发实战:Mac 版 Everything
用 SQL 查文件——Mac 版 Everything 完整体验
—— 从产品效果到代码,一篇看全
本文是《DuckDB 插件开发实战:Mac 版 Everything》系列的第 1 篇。整个系列以一个真实的 DuckDB 扩展——apfs 插件(简单版本everything)为案例,带你从零走完插件开发的完整链路。这篇先体验效果,再看项目长什么样。
Mac 版 Everything 是什么?
Windows 上有个神器叫 Everything——敲个文件名,毫秒级返回全盘搜索结果。Mac 上呢?Spotlight 算半个,但它没法做这种事:
-
• “我磁盘上最大的 10 个文件是什么?” -
• “哪种文件类型占了最多空间?” -
• “有没有同名同大小的重复文件?”
Spotlight 能搜文件名,但做不了分析。你需要的不是搜索框,是一个数据库。
这就是 apfs 扩展做的事——把你的整个 macOS 文件系统变成一张 SQL 表。一句 SELECT,文件名、路径、大小、时间戳、权限,全在手边。
它还有一个配套的 Web UI——Mac Everything:

上图是实际运行截图:658 万文件已索引,搜索响应 396ms,支持文件类型过滤(All / Files / Folders / Images / Documents / Code)、多列排序、右键操作。这个 Web UI 的实现会在第 5 篇详细拆解,这篇先聚焦 DuckDB 扩展本身。
整个项目的技术栈:
三步跑通
第 1 步:编译
cd /path/to/duckdb-apfs# Debug 版(开发用,约 5-10 分钟首次编译)make debug# 或 Release 版(推荐实际使用,查询快 22-77 倍)make
编译产物是一个带 apfs 扩展的 DuckDB CLI,直接启动就能用,不需要额外 LOAD。
第 2 步:启动
./build/debug/duckdb
第 3 步:查
-- 搜索文件名包含 "duckdb" 的文件SELECT*FROM apfs_search('duckdb') LIMIT 5;
就这样。没有配置文件,没有服务器,没有 pip install——一个二进制文件搞定。
apfs 扩展提供了什么?
三个 SQL 函数,覆盖”扫描”、”搜索”、”变更检测”三个场景:
|
|
|
|
apfs_scan(path, mode) |
|
|
apfs_search(keyword) |
|
|
apfs_has_changes(path, since) |
|
|
三个函数返回相同的 11 列 schema:
path │ 完整路径name │ 文件名extension │ 扩展名(不含 .)size │ 文件大小(字节)file_type │ file / directory / symlink / othermodified_at │ 最后修改时间created_at │ 创建时间accessed_at │ 最后访问时间permissions │ 权限字符串(如 rwxr-xr-x)owner │ 所有者用户名depth │ 相对扫描根目录的深度
两种扫描模式:快 vs 准
apfs_scan 有两种工作模式,这是设计上的一个关键取舍:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/tmp 等未索引目录 |
|
建议:日常用 spotlight,需要 100% 覆盖时切 fts。
Debug vs Release:性能差距有多大?
实测数据(M2 MacBook Pro 16GB,macOS,duckdb-apfs):
|
|
-O0, 573MB) |
-O2, 43MB) |
|
SELECT 1 |
|
|
50x |
/tmp(214 文件) |
|
|
10x |
~/Downloads(10.5 万文件) |
|
|
5.7x |
|
|
|
|
4.2x |
|
|
|
|
77x |
ILIKE '%config%' |
|
|
22x |
apfs_search('duckdb')
|
|
|
|
几个值得注意的点:
-
1. 纯查询差距最大——聚合查询 77 倍、ILIKE 搜索 22 倍。查询是纯 CPU 密集的,Release 的 -O2优化 + SIMD + 内联全面生效。 -
2. 扫描差距较小——fts 扫描是 I/O bound( stat()系统调用),编译器优化帮不了太多,但仍有 4-6 倍差距(来自 DataChunk 填充和 UTF-8 处理的优化)。 -
3. apfs_search 几乎没差距——Spotlight MDQuery 是 macOS 系统调用,瓶颈在操作系统索引层,不在 DuckDB 代码里。
结论:日常使用务必用 release 版。Debug 只在开发调试 C++ 扩展代码时使用。
三个实战场景
场景 1:找出磁盘上最大的 10 个文件
-- 先扫描 Downloads 目录建表CREATE TABLE files ASSELECT*FROM apfs_scan('/Users/max/Downloads', 'fts');-- 找最大的 10 个文件SELECT name, ROUND(size /1024.0/1024.0, 2) AS mb, extension, modified_atFROM filesWHERE file_type ='file'ORDERBY size DESCLIMIT 10;
场景 2:按扩展名统计磁盘占用
SELECT extension,COUNT(*) AS cnt, ROUND(SUM(size) /1024.0/1024.0, 2) AS mbFROM filesWHERE file_type ='file'GROUPBY extensionORDERBY mb DESCLIMIT 10;
场景 3:找出可能的重复文件
SELECT name, ROUND(size /1024.0/1024.0, 2) AS mb,COUNT(*) AS copiesFROM filesWHERE file_type ='file'AND size >1024*1024-- 只看大于 1MB 的GROUPBY name, sizeHAVING copies >1ORDERBY size DESCLIMIT 10;
同名同大小——大概率是重复下载或者复制遗留。
apfs_search:全盘即时搜索
如果你只是想快速找个文件,不需要先建表:
-- 搜索所有文件名包含 "config" 的文件SELECT name, path, ROUND(size/1024.0, 1) AS kbFROM apfs_search('config')LIMIT 20;-- 搜索所有 PDF 文件SELECT name, path, ROUND(size/1024.0/1024.0, 2) AS mbFROM apfs_search('*.pdf')ORDERBY size DESCLIMIT 10;
apfs_search 走的是 Spotlight 索引,全盘搜索通常在几秒内返回。
apfs_has_changes:轻量变更检测
SELECT apfs_has_changes('/Users/max/Documents',TIMESTAMP'2026-04-17 00:00:00');-- 返回 true 或 false
这个函数不遍历文件系统,而是用一个轻量的 Spotlight 查询检查有没有任何文件的修改时间在给定时间之后。耗时大约 50-200ms。第 4 篇会详细拆解它的实现原理。
DuckDB 扩展项目长什么样?
看完效果,来看看 duckdb-apfs 的项目结构:
duckdb-apfs/├── src/ # C++ 扩展源码(5 个 .cpp + 5 个 .hpp,共 ~1200 行)│ ├── apfs_extension.cpp # 扩展入口,函数注册(180 行)│ ├── apfs_fts_scanner.cpp # fts 模式实现│ ├── apfs_spotlight_scanner.cpp # Spotlight 模式实现│ ├── apfs_has_changes.cpp # 变更检测│ ├── apfs_utils.cpp # 工具函数│ └── include/ # 头文件├── test/sql/apfs/ # 8 个 sqllogictest 测试文件(574 行)├── duckdb/ # DuckDB 源码(git submodule)├── extension-ci-tools/ # CI 工具链(git submodule)├── CMakeLists.txt # 37 行,构建配置├── Makefile # 8 行,快捷命令├── extension_config.cmake # 6 行,扩展注册└── vcpkg.json # 依赖管理
这不是随便画的——就是 DuckDB 官方 extension-template 的标准结构。所有第三方扩展(httpfs、spatial、delta 等)都长这样。
四个关键文件
一个 DuckDB 扩展项目,核心就四个配置文件 + 你的 C++ 代码。
1. extension_config.cmake——告诉 DuckDB “我有个扩展”
# extension_config.cmake(全部就 6 行)duckdb_extension_load(apfs SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR} LOAD_TESTS)
duckdb_extension_load 宏做三件事:把 apfs 注册为扩展名、指定源码目录、LOAD_TESTS 让 make test 能发现测试文件。
2. CMakeLists.txt——怎么编译
cmake_minimum_required(VERSION 2.8.12...3.29)set(TARGET_NAME apfs)set(EXTENSION_NAME ${TARGET_NAME}_extension)set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension)project(${TARGET_NAME})include_directories(src/include)set(EXTENSION_SOURCES src/apfs_extension.cpp src/apfs_fts_scanner.cpp src/apfs_spotlight_scanner.cpp src/apfs_has_changes.cpp src/apfs_utils.cpp)build_static_extension(${TARGET_NAME}${EXTENSION_SOURCES})set(PARAMETERS "-warnings")build_loadable_extension(${TARGET_NAME}${PARAMETERS}${EXTENSION_SOURCES})# macOS 框架链接if(APPLE)find_library(CORE_FOUNDATION_LIB CoreFoundation)find_library(CORE_SERVICES_LIB CoreServices)if(CORE_FOUNDATION_LIB AND CORE_SERVICES_LIB)target_link_libraries(${EXTENSION_NAME}${CORE_FOUNDATION_LIB}${CORE_SERVICES_LIB})target_link_libraries(${LOADABLE_EXTENSION_NAME}${CORE_FOUNDATION_LIB}${CORE_SERVICES_LIB})endif()endif()
两个关键宏:
-
• build_static_extension:生成静态库.a,链接进duckdbCLI 二进制 -
• build_loadable_extension:生成共享库.duckdb_extension,可通过LOAD动态加载
apfs 扩展额外链接了 macOS 的 CoreFoundation 和 CoreServices 框架——因为 Spotlight API 在这两个框架里。
3. Makefile——快捷入口
PROJ_DIR := $(dir $(abspath $(lastword$(MAKEFILE_LIST))))EXT_NAME=apfsEXT_CONFIG=${PROJ_DIR}extension_config.cmakeinclude extension-ci-tools/makefiles/duckdb_extension.Makefile
就 8 行。所有编译逻辑都在 extension-ci-tools 里,你不需要自己写。
4. src/include/apfs_extension.hpp——扩展类定义
#pragma once#include"duckdb.hpp"namespace duckdb {classApfsExtension : public Extension {public:voidLoad(ExtensionLoader &loader)override;std::string Name()override;std::string Version()constoverride;};} // namespace duckdb
每个 DuckDB 扩展都继承 Extension 基类,实现三个方法:
-
• Load():注册你的函数——这是扩展的”main” -
• Name():返回扩展名(”apfs”) -
• Version():返回版本号
编译流程
编译产物:
|
|
|
|
|
|
build/debug/duckdb
|
|
build/release/duckdb
|
|
|
|
|
build/debug/extension/apfs/apfs.duckdb_extension |
|
首次编译会很慢(要编译整个 DuckDB 源码),后续只改 src/ 下的文件,增量编译只需几秒。
加速技巧:
# 用 Ninja 替代 Make(并行度更好)GEN=ninja make debug# 只编译 apfs 扩展(跳过其他扩展)BUILD_EXTENSIONS="apfs" make debug
两个 git submodule
项目依赖两个子模块:
# .gitmodules[submodule "duckdb"] path = duckdb url = https://github.com/duckdb/duckdb.git[submodule "extension-ci-tools"] path = extension-ci-tools url = https://github.com/duckdb/extension-ci-tools.git
duckdb/ 子模块指向一个特定的 DuckDB 版本。扩展版本必须与 DuckDB 版本严格匹配——这是 DuckDB 扩展生态的一个基本约束。
扩展入口:LoadInternal
扩展的”main 函数”在 src/apfs_extension.cpp:
staticvoidLoadInternal(ExtensionLoader &loader){// 1. 注册 apfs_scan(两个重载:1 参数和 2 参数)TableFunctionSet scan_set("apfs_scan");TableFunction scan_one_arg("apfs_scan", {LogicalType::VARCHAR}, ApfsScanFunction, ApfsScanBindOneArg, ApfsScanInit); scan_one_arg.named_parameters["since"] = LogicalType::TIMESTAMP; scan_set.AddFunction(scan_one_arg);TableFunction scan_two_args("apfs_scan", {LogicalType::VARCHAR, LogicalType::VARCHAR}, ApfsScanFunction, ApfsScanBindTwoArgs, ApfsScanInit); scan_two_args.named_parameters["since"] = LogicalType::TIMESTAMP; scan_set.AddFunction(scan_two_args); loader.RegisterFunction(scan_set);// 2. 注册 apfs_searchTableFunction search_func("apfs_search", {LogicalType::VARCHAR}, ApfsSearchFunction, ApfsSearchBindWrapper, ApfsSearchInit); loader.RegisterFunction(search_func);// 3. 注册 apfs_has_changes(标量函数) loader.RegisterFunction(CreateApfsHasChangesFunction());}
三个函数就这样注册完了。loader.RegisterFunction() 告诉 DuckDB:”以后有人调 apfs_scan,用我这几个回调函数处理。”
文件末尾还有一个 C 入口点——动态加载时 DuckDB 通过 dlsym 找到它:
extern"C" {DUCKDB_CPP_EXTENSION_ENTRY(apfs, loader) { duckdb::LoadInternal(loader);}}
验证扩展是否加载
SELECT*FROM duckdb_extensions() WHERE extension_name ='apfs';
┌────────────────┬─────────┬───────────┬──────────────┐│ extension_name │ loaded │ installed │ install_path │├────────────────┼─────────┼───────────┼──────────────┤│ apfs │ true │ true │ (BUILT-IN) │└────────────────┴─────────┴───────────┴──────────────┘
(BUILT-IN) 表示静态链接——编译时就内嵌进了 CLI 二进制。
如果你想从零开始
DuckDB 官方提供了扩展模板仓库,一键生成脚手架:
# 在 https://github.com/duckdb/extension-template 点 "Use this template"# 或手动克隆git clone --recurse-submodules https://github.com/duckdb/extension-template.git my_extensioncd my_extension# 修改扩展名(模板默认叫 "quack")# 1. extension_config.cmake 里改名# 2. CMakeLists.txt 里改 TARGET_NAME# 3. src/ 下的文件名和类名make debug./build/debug/duckdb -c "SELECT quack('world');"# Hello, world! 🐥
模板自带一个 quack() 标量函数作为示例——改改就是你的扩展。
总结
三个要点:
-
1. apfs 扩展把 macOS 文件系统变成了 SQL 表——11 列元数据,支持全部 SQL 操作 -
2. 两种扫描模式:fts(慢但全覆盖)和 spotlight(秒级但依赖索引),按需选择 -
3. 这是一个完整的 DuckDB 扩展项目——C++ 插件 ~1200 行 + Go Web UI ~1700 行 + 完整测试,接下来我们逐层拆解
下一篇,我们从零开始构建 apfs 插件,完整走一遍决策过程。
参考资料
-
• DuckDB Extension Template -
• DuckDB 官方文档 – Extensions Overview -
• Windows Everything — apfs 扩展的灵感来源 -
• duckdb-apfs 源码: extension_config.cmake(6 行,扩展注册) -
• duckdb-apfs 源码: CMakeLists.txt(38 行,完整构建配置) -
• duckdb-apfs 源码: src/apfs_extension.cpp(180 行,扩展入口 + 3 个函数注册)
夜雨聆风