csvql 在 8 GB 文件上查询原始 CSV 比 DuckDB 快约 2.8 倍,内存使用量减少约 6 倍,且无需额外磁盘空间——证明这一点的基准测试还发现了我原本永远找不到的两个真实 bug。
环境设置:同一台机器上查询同一份原始 CSV
DuckDB 发布了它自己的 NYC-taxi CSV 基准测试。这使其成为完美的衡量标准:当我使用竞争对手自己的数据集和查询运行时,我不会被指责为对竞争对手进行了错误配置。
该数据集位于 DuckDB 自己的 blob 存储(blobs.duckdb.org/data/nyc-taxi-dataset)。每个文件包含 2000 万行、51 列,解压后约 8 GB。这四个查询是经典的“十亿次出租车出行”聚合——按 cab type、passenger count、year 和 rounded trip distance 进行 GROUP BY 计数和平均值计算。
使此类基准测试成败的关键规则:两个引擎必须执行相同的工作。DuckDB 的标头数字有两种形式——“带存储”(将 CSV 加载到 DuckDB 的原生列式格式之后)和“不带存储”(每次查询原始 CSV)。csvql 始终直接查询原始 CSV;它没有原生存储。因此公平的较量是两个引擎每次都冷读取原始 CSV:
csvql: csvql "SELECT ... FROM 'trips.csv' ..."
duckdb: SELECT ... FROM read_csv_auto('trips.csv') ... -- 不是预加载的表
进入全屏模式 退出全屏模式
将 csvql-on-raw-CSV 与 DuckDB-on-preloaded-store 进行比较是不诚实的,任何打开脚本的人都会这么说。直接对直接是唯一能经得起审查的比较。
机器:Apple Silicon,macOS。DuckDB: v1.4.2。方法:每个查询运行 5 次取最佳值,OS 页面缓存已预热(两个引擎均等)。
速度:8 GB 文件上约 2.8 倍
| 查询 | csvql | DuckDB | 加速比 |
|---|---|---|---|
Q01 COUNT(*) GROUP BY cab_type
|
1.29 s | 3.55 s | 2.8x |
Q02 AVG(total_amount) GROUP BY passenger_count
|
1.41 s | 3.81 s | 2.7x |
Q03 COUNT(*) GROUP BY passenger_count, year
|
1.36 s | 4.01 s | 2.9x |
Q04 GROUP BY passenger_count, year, ROUND(distance)
|
1.38 s | 3.99 s | 2.9x |
查询延迟,越低越好:
0s 1s 2s 3s 4s
Q01 csvql ██████▌ 1.29
duck ██████████████████ 3.55
Q02 csvql ███████ 1.41
duck ███████████████████ 3.81
Q03 csvql ██████▊ 1.36
duck ████████████████████ 4.01
Q04 csvql ██████▉ 1.38
duck ████████████████████ 3.99
进入全屏模式 退出全屏模式
反直觉的部分在于:文件越大,差距越小。在 417 MB / 100 万行的样本上,csvql 快约 10 倍。在 8 GB 上,约为 2.8 倍。原因?在小文件上,DuckDB 的进程启动和 CSV 读取器初始化主导了挂钟时间,而 csvql 的精简启动优势明显。在大文件上,实际的解析和聚合吞吐量占主导,DuckDB 的并行 CSV 读取器迎头赶上,csvql 稳定在约 2.8 倍。两个数字都是真实的;诚实的做法是发布带文件大小的完整曲线,而不是挑选 10 倍的数据。
内存:约少 6 倍
同一 8 GB 文件,每个查询的峰值内存占用:
| 查询 | csvql | DuckDB |
|---|---|---|
| Q01 | 29 MB | 178 MB |
| Q02 | 30 MB | 210 MB |
| Q03 | 34 MB | 208 MB |
| Q04 | 38 MB | 219 MB |
峰值内存 (MB), 8 GB 文件
csvql █▍ ~30 MB
duckdb ██████████████████████████ ~200 MB
进入全屏模式 退出全屏模式
csvql 通过内存映射扫描流式处理文件,只将 group-by 哈希表保留在内存中——只有几十个组。它一次最多只持有 8 GB 的一小部分。这就是为什么 30 MB 的内存占用可以处理 8 GB 文件的原因。
存储:真正重要的数字
在这里,“原始 CSV,无存储”不再是一个公平性警告,而是变成了全部重点。
在 8 GB 文件上回答这些查询所需的额外磁盘:
csvql 0 字节 — 原地查询 CSV,无需摄取
duckdb 2.1 GB + 21.7s — 必须为其快速路径构建原生存储
进入全屏模式 退出全屏模式
DuckDB 可以比这些原始 CSV 数字更快——但只能通过其“带存储”路径,该路径首先将 CSV 摄取到 2.1 GB 的原生存储中,对于单个 8 GB 文件,一次性成本约为 22 秒(对于完整的多文件数据集则是几分钟)。csvql 达到其数字不需要额外磁盘和摄取。对于针对五分钟前落在磁盘上的 CSV 的临时查询,“无需摄取”是立即得到答案与喝完咖啡后再得到答案之间的区别。
诚实的总结:相同的答案,快约 2.8 倍,内存少约 6 倍,零额外磁盘,零摄取。
我无法运行的测试——以及为什么这是诚实的一部分
你会注意到我没有将 csvql 的数字与 DuckDB 的已发布数字(他们的博客报告 Q01–Q04 在不带存储的情况下为 2.45 / 3.89 / 5.21 / 11.2 s)进行比较。那看起来会很棒——csvql 的约 1.3 s 与他们在 Q04 上的 11.2 s 相比是令人兴奋的 8 倍。但这也是垃圾。
两个原因:
数据集大小不同。DuckDB 已发布的查询数字是在完整约 11 亿行的数据集(全部 65 个文件,约 500 GB 未压缩 CSV)上运行的。我的数字是在单个 2000 万行文件上运行的。这是 55 倍的行数差异。将它们放在同一张表中将是比较 csvql-on-20M 与 DuckDB-on-1.1B——基准测试中的固定比赛。(你可以在他们自己的 Q04 中看到大小差距:11.2 s 在 11 亿行上,而我的本地 DuckDB 在 2000 万行上需要约 4 秒。)
硬件不同。他们的数字来自 M1 Pro;我的数字来自不同的 Apple Silicon 机器。借用竞争对手在其他硅片上测量的数字并用你的数字除以它不是基准测试,而是愿望。
要合法地引用 DuckDB 已发布的数字,csvql 必须运行相同的约 11 亿行数据集——这意味着磁盘上有约 500 GB 的未压缩 CSV。我的机器有 275 GB 可用空间。它物理上放不下。因此,我没有跨硬件和数据大小借用数字,而是在同一台机器上、相同的 OS 缓存状态下,在相同的 2000 万行上运行了两个引擎。这控制了跨硬件比较无法控制的一切。这是一个更小、不那么华丽的结果——但一个更值得信赖的结果。
如果有人有 1 TB 磁盘想要运行完整的 500 GB 并报告 csvql 与 DuckDB 的确切已发布表相比的结果,脚本只需一个标志即可(bench_taxi.sh 65)。我很想看到它。
剧情反转:基准测试发现了我引擎中的两个 bug
我构建基准测试是为了测量速度。它最终成为 csvql 拥有的最好的测试套件,因为在真实数据上运行真实查询立即破坏了其中两个:
Bug #1 — GROUP BY ROUND(trip_distance) 以 ColumnNotFound 崩溃。按生成字符串的函数(STRFTIME)分组可以工作;按生成数字的函数(ROUND)分组则不行——group-key 解析器根本没有这种情况。Q04 根本无法运行,直到我添加了一个。
Bug #2 — ORDER BY COUNT(*) DESC 静默返回未排序的行。没有错误,没有崩溃——只是错误的输出。GROUP BY 代码路径按内部哈希键对结果进行排序,完全忽略了 ORDER BY 子句。这是最可怕的一类 bug:那种返回自信但错误答案的 bug。一个只检查时序的基准测试会直接略过它;将输出与 DuckDB 进行比较,一次 diff 就发现了它。
现在这两个 bug 都已修复,每个都有回归测试,四个规范查询产生与 DuckDB 字节相同的结果(1 与 1.0 浮点格式除外)。道德:针对可信预言机检查的基准测试是戴着秒表的正确性测试。如果我只针对自己运行 csvql,这两个 bug 仍然会存在。
自己重现
一切都在仓库中。不需要 500 GB——样本模式拉取约 417 MB,几秒钟内运行:
git clone https://github.com/melihbirim/csvql
cd csvql && zig build -Doptimize=ReleaseFast
# 快速:100 万行样本(约 417 MB)
./bench/bench_taxi.sh --sample
# 完整:2000 万行(从 DuckDB 的 blobs 下载约 8 GB)
./bench/bench_taxi.sh 1
# 内存 / CPU / 存储而非速度
./bench/bench_taxi.sh --resources 1
进入全屏模式 退出全屏模式
该脚本下载 DuckDB 自己的数据集,在原始 CSV 上运行两个引擎,并打印上面的表格。它是约 130 行 bash——在你信任它之前先阅读它。
要点
- 基准测试直接对直接,否则别费心。整个练习中最重要的行是拒绝将 raw-CSV csvql 与预加载的 DuckDB 进行比较。
- 发布完整曲线。400 MB 上约 10 倍和 8 GB 上约 2.8 倍都是真实的。隐藏一个来突出另一个是基准测试名声不好的原因。
- 有趣的数字不总是速度。对于临时 CSV 工作,约 6 倍更少的内存和零摄取比节省两秒钟更重要。
- 将你的基准测试指向可信的预言机。它兼作你懒得写的正确性测试。它发现了两个我本会发布的 bug。
csvql 是开源的(Zig,单个静态二进制文件,github.com/melihbirim/csvql)。如果你运行完整数据集,请告诉我你得到了什么。
数字:Apple Silicon M2Pro,macOS,DuckDB v1.4.2,最佳 5 次,预热缓存。数据集和查询:DuckDB 的 NYC-taxi 基准测试。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.