Files
ChuBlog/benchmark/README.md
T

4.1 KiB
Raw Blame History

ChuSQL 基准测试

这个目录放性能基准测试,和 chusql-engine、chusql-storage 的功能代码分开, 因为它回答的问题不一样:功能代码回答"对不对",基准测试回答"快不快、快多少"。

benchmark/
├── README.md                  本文件:怎么跑、测什么、结果怎么解读
├── chusql-engine/             Haskell 查询层:优化器开/关的对比
│   ├── stack.yaml             把 chusql-engine 作为本地包引入
│   ├── chusql-bench.cabal
│   └── src/Main.hs
└── chusql-storage/            Rust 存储层:预留(见该目录下的 README)

怎么跑

cd benchmark/chusql-engine
stack run                      # 默认 users=500 行、orders=500 行
stack run -- 1000 1000 0.5     # 也可以自己指定:users 行数、orders 行数、每批目标秒数

第一次跑要编译,之后很快。整个默认跑完约 5 秒。


测什么

四个用例,每个都跑两遍:一遍走"翻译出来的原始工序单",一遍走"优化后的工序单"。 两次的结果行数会被打印出来互相对照,不一致会当场标出来。

用例 想看什么
JOIN + WHERE u.age > 90 谓词下推的主场:条件能筛掉约 90% 的行
JOIN(无 WHERE) 只剩投影下推在起作用,收益应该温和
JOIN + WHERE + ORDER BY + LIMIT 规则叠加时的综合效果
单表 WHERE age > 90 对照组:没有下推空间,优化器只剩固定开销

计时怎么做(两个坑都踩过了)

坑一:Haskell 是惰性的,不强制求值就等于没测。 如果只写 let r = evalRelOp db plan,r 根本没被算出来,测到的是"定义一个变量"的时间。 所以每次必须把结果算到底(本项目里是逐行把列数累加起来当校验和)。

坑二:Windows 上 CPU 时钟的精度只有约 15.6 毫秒。 优化后的查询只要几毫秒,直接测会得到 0.0 ms 这种没意义的数字。

所以计时用的是自适应批量:把同一个用例连续跑 k 次测一整批,k 不断翻倍, 直到一批累计耗时超过目标值(默认 0.3 秒),再折算成单次耗时。 这样时钟精度的误差会被 k 摊薄,几毫秒的用例也能测准。 程序会打印实际用了多大的批次(例如"优化后 64 次/批")。

跑正式计时之前还会预热两遍,避免把首次构造表数据的开销算进去。


实测结果

环境:Windows,GHC 9.10.3,-O2 数据:users 500 行(id / name / age,age 只有 0-99 共 100 种取值), orders 500 行(id / user_id / product),两表配对共 25 万对。

查询 结果行数 未优化 优化后 加速
JOIN + WHERE u.age > 90 45 125.0 ms 7.1 ms 17.7x
JOIN(无 WHERE) 500 132.8 ms 105.5 ms 1.3x
JOIN + WHERE + ORDER BY + LIMIT 10 10 171.9 ms 10.3 ms 16.8x
单表 WHERE age > 90 45 0.111 ms 0.153 ms 0.7x

怎么解读

  • 前两行的差距来自谓词下推。 条件筛掉了约 90% 的用户,配对次数从 25 万降到约 2.25 万, 时间直接掉一个数量级。筛得越狠,省得越多。
  • 第二行只有投影下推。 配对次数一点没变(没有条件可下推),只是每一行变窄了, 所以只有 1.3 倍 —— 收益温和但确实是正的。
  • 第四行反而略慢。 单表查询本来就没有下推空间(条件早已贴着扫描), 而优化器自己要走一遍规则,有几十微秒的固定开销(0.153 − 0.111 ≈ 0.04 ms)。 优化不是零成本的:查询越大、配对越多,这笔账才越划算。

加新用例

在 src/Main.hs 的 queries 列表里加一条 SQL 字符串就行,其余全自动 (会自动翻译、优化、跑两遍、比结果、算加速比)。

想测别的规模,直接命令行传参;想在文档里引用数字,记得把数据规模一起写上 —— 脱离规模谈"快了几倍"是没有意义的。