Files
ChuSQL/docs/performance.md
T
LIm1tZz7 8ce4239452 v26.10.1 正式版
修复部分问题,优化性能,进一步推进类型系统
2026-10-05 22:22:23 +08:00

13 KiB
Raw Permalink Blame History

性能基准

测试方法与复现

benchmark/ 中的应用名为 benchmark4csql。它启动真实 chusql-server,通过本机 TCP 使用共享客户端协议,服务端调用 Rust 存储;不以纯内存引擎测量代替端到端性能。每次运行创建独立临时数据库,结束后停止服务并清理数据。任一 SQL、结果校验或并发请求失败会使运行失败。

在 benchmark/ 中构建优化版本,并在 PATH 中放置 release 存储动态库:

$env:TMP = 'D:\Workspace\ChuSQL\logs\cli-test-tmp'
$env:TEMP = $env:TMP
$env:PATH = 'D:\Workspace\ChuSQL\chusql-core\storage\target\release;' + $env:PATH
$env:GHC_CHARENC = 'UTF-8'
stack build chusql-core-model chusql-core-engine chusql-interface chusql-server chusql-bootstrap benchmark4csql --force-dirty --ghc-options=-O2
stack exec -- benchmark4csql 10000 50000

首次运行前从 chusql-core/storage/ 执行 cargo build --release,并创建上面的临时目录。Linux 使用 LD_LIBRARY_PATH 指向 release 存储目录。两个参数分别为用户数、订单数,默认 10,000 / 50,000,均需至少 100。

读取场景先预热一次,再采样七次,记录均值、p50、p95、最大延迟与请求数/秒。p95 使用最近秩;七次采样的 p95 等于最大值,只适合比较本次场景,不能作为稳定的生产尾延迟。并发场景使用独立的已认证 TCP 会话,1、4、8 个客户端各执行 100 次点查,同步开始;吞吐以整个并发阶段的墙上耗时计算,连接与登录不计入该阶段。

加载主表每条 INSERT 包含 500 行。另测逐条持久化提交、200 条 INSERT 的单次事务提交、更新、删除和事务回滚。写入保留正常 WAL、数据刷盘与检查点路径,没有关闭持久化。查询耗时包含 TCP、SQL 解析、语义检查、引擎执行与响应解码;不包含 UI 渲染。读测试主要校验结果行数,加载 COUNT、并发点查值和回滚后值另行校验。

实测环境

  • 测量日期:2026-10-05,Asia/Shanghai。
  • CPU:AMD Ryzen 7 7840H,8 核 / 16 逻辑处理器。
  • 可见物理内存:31.2 GiB。
  • 系统:Windows 11 专业版,10.0.26200。
  • 数据目录:D 盘 NTFS 上的仓库 logs/ 临时子目录;未测存储设备裸吞吐。
  • 编译:GHC 9.10.3,Haskell 本地包 -O2;Rust release 动态库。
  • 规模:10,000 用户、50,000 订单、5,000 条 512 字节字符串宽行;订单按用户循环分配,年龄按 100 个值循环分布。

性能测试结果

完整运行返回退出码 0,输出 PRESSURE_OK all result checks passed 和 BENCHMARK_OK all scenarios completed。原始日志保存于本机 logs/benchmark-performance.log。下面保留该次成功运行的数据;首次失败运行不参与统计。

启动、引导、认证及创建基准库合计 0.62 秒。纯 TCP Ping 200 次平均 0.100 ms;schema catalog 200 次平均 0.267 ms。主键点查另外连续执行 100 次,平均 0.300 ms,100/100 命中。

读取场景

每项七个测量样本,单位为毫秒。请求吞吐由总采样耗时计算,为串行吞吐。

场景 返回行数 均值 p50 p95 / 最大值 请求/秒
用户全表扫描 10,000 23.768 18.961 36.309 42.07
订单全表扫描 50,000 114.094 118.676 131.074 8.76
主键点查 1 0.415 0.278 0.742 2,408.23
name 点查,无二级索引 1 21.776 21.908 23.257 45.92
name 点查,有二级索引 1 0.338 0.312 0.466 2,961.71
主键闭区间范围,100 行 100 0.675 0.644 0.985 1,482.33
按年龄排序,LIMIT 100 100 19.361 20.748 21.720 51.65
100 个年龄值分组计数 100 18.139 17.843 22.200 55.13
用户与订单过滤连接 4,500 144.929 142.598 158.723 6.90
过滤派生表与订单连接 4,500 141.797 141.535 148.438 7.05
CTE 后按年龄分组 100 28.539 29.146 30.921 35.04
无关联 COUNT 标量子查询,外层 LIMIT 100 100 30.777 29.844 34.692 32.49
EXISTS,20 个不同外层绑定 20 2,061.770 2,070.403 2,123.953 0.49
512 字节字符串宽行全表扫描 5,000 22.969 22.791 24.485 43.54

相关 EXISTS 用派生表先限定外层为前 20 个用户,每次子查询按 orders.user_id = u.id 匹配;orders.user_id 没有二级索引。每个绑定具有五条订单,外层与内层都读取真实存储。

并发点查

每客户端 100 次主键点查。单请求延迟合并所有客户端样本计算;总吞吐用并发阶段墙上时间计算,不能用单请求延迟均值的倒数替代。

客户端 请求数 墙上耗时 ms 总吞吐 请求/秒 单请求均值 ms p50 ms p95 ms 最大 ms
1 100 29.094 3,437.10 0.290 0.250 0.544 1.183
4 400 37.862 10,564.79 0.374 0.335 0.574 1.796
8 800 48.877 16,367.72 0.481 0.437 0.735 2.027

写入、索引及事务

这些写入阶段各测量一次,不报告分位数。场景按顺序执行,数据库大小、已打开表数量和索引状态会变化。

场景 处理行数 总耗时 ms 行/秒
用户加载,500 行/提交 10,000 762.419 13,116.15
订单加载,500 行/提交 50,000 3,915.697 12,769.12
512 字节宽行加载,500 行/提交 5,000 1,084.740 4,609.40
writes 逐条持久化 INSERT 200 1,119.0 178.73
writes 事务中 200 条 INSERT,含 BEGIN / COMMIT 200 67,549.539 2.96
batch_t 加载,100 行/提交 10,000 1,309.4 7,637.09
items 加载,100 行/提交 50,000 6,336.8 7,890.42

其他单次耗时:

操作 耗时 ms
10,000 行 users 建 name 索引 239.7
UPDATE writes 的 400 行 16.4
事务内 UPDATE writes 的 400 行 0.5
ROLLBACK 上述修改 0.3
DELETE writes 的 200 行 10.3
DELETE users 单行 48.1
DELETE users 全部剩余行 765.7
50,000 行 items 无索引 code 点查 135.2
50,000 行 items 建 code 索引 1,089.8
建索引后 code 点查 0.4

回滚后校验 writes.id = 1 的 age 仍为 2,事务中写入的 0 没有落入正式数据。删除后 writes 行数为 200。加载、连接及并发点查的结果校验全部通过。

性能瓶颈分析

本节描述首次基准时的实现;事务差分优化后的结果见文末复测。

1. 事务提交的整库差分是首要排查点

本次 200 条事务 INSERT 比 200 次独立持久化 INSERT 慢约 60 倍。事务阶段开始时,数据库已有 10,000 用户、50,000 订单及 5,000 宽行,事务只修改 writes 表。事务内一次 400 行 UPDATE 仅耗时 0.5 ms,说明不能将整个事务阶段的 67.55 秒简单归因于 SQL 更新计算或 fsync。

代码依据:服务端 Session.hs 的 BEGIN 和 COMMIT 都获取整库快照。transactionDiffs 对具有整数 id 的表先走 keyedDiff,即使 staged 与 base 完全相同,也会建立关联列表并对每行执行线性 lookup / any;50,000 行未修改表仍可能进行平方级比较。COMMIT 分别调用冲突检查和操作生成,可能重复计算差分。这是与实测异常最吻合的瓶颈候选。

当前没有 BEGIN、逐条语句、COMMIT 的独立计时或 CPU profile,不能给各部分分摊比例。下一步应先拆分计时,再对未修改表快速跳过、以 Map / HashMap 做按 id 差分、复用一次差分计算结果,并考虑记录实际修改表与键。整库快照及内存统计重算也需要测量。不能据当前实现假设把 Web 编辑放进一个大事务就会更快。

2. 不同绑定的相关子查询仍重复扫描

20 个 EXISTS 绑定平均 2.06 秒,而单次 50,000 行订单扫描约 114 ms;两者量级相符。缓存只能合并相同绑定,这里的 20 个用户 id 都不同,因此仍需多次读取订单。执行路径 Eval.hs 将子查询逐绑定执行,没有半连接,EXISTS 也先得到子查询结果再判断是否为空。

优先验证 orders.user_id 二级索引及基于统计的点查选择,再评估半连接、EXISTS 找到首行即可停止、或单次物化内层匹配键。这个推断有查询结构和耗时量级支持,但本次没有对索引版相关子查询做对照,不把预期收益当作已测结果。

3. 扫描、结果传输与全量排序限制大结果查询

50,000 行扫描比 10,000 行扫描慢约 4.8 倍。无索引 name 点查只返回一行,耗时仍接近用户全表扫描;索引后均值下降约 64 倍,说明扫描工作量具有决定性影响。宽行加载吞吐降至约 4,609 行/秒,低于短行的约 1.3 万行/秒。

sortRows 使用完整 sortBy 后再 LIMIT,返回 100 行并不代表只处理 100 行。当前全表结果会经过存储解码、引擎求值及 TCP JSON 编码。下一步可测 Top-K 排序、分页或流式返回,以及存储、引擎、编码各阶段的时间和分配量。本次没有冷缓存、磁盘裸吞吐、分阶段 CPU 或内存峰值测量,无法把扫描耗时全部归到磁盘。

4. 批量提交有效,读并发扩展低于线性

批量加载达到约 1.3 万行/秒,200 次独立提交只有约 179 行/秒。数据和已打开表数量不同,这不是严格的单变量对照;但与 WAL 提交、表刷盘和检查点固定成本被摊薄的执行路径一致。Rust 的 group_via_wal 还会刷所有已打开表,应单独测量并评估只刷本组实际修改表的收益及恢复正确性。

8 客户端点查总吞吐约为 1 客户端的 4.8 倍,单请求均值从 0.290 ms 上升至 0.481 ms。表明只读请求有并发收益,也存在共享资源、调度或协议开销。本次没有混合读写或并发写压测,不据此推断写扩展能力;当前每库写独占的限制仍在。

使用范围

这是单机回环、主要为热缓存、管理员身份的单次基准,不是生产容量承诺。未覆盖远程网络、鉴权角色开销、崩溃恢复耗时、并发写冲突或大型宽表。七次读采样和单次写测量不足以建立统计置信区间,后续优化应使用相同参数、多次独立运行和分阶段 profile 比较。

2026-10-05 事务差分优化复测

使用相同机器、优化构建及 10,000 用户、50,000 订单规模运行完整基准,所有场景及结果校验通过(BENCHMARK_OK)。日志为 logs/repair-txn-performance.log。

COMMIT 跳过未修改表,以 IntMap / IntSet 查询整数键差分,冲突检查与操作生成共享差分;保持原行顺序及重复 id 的首行查找语义。

指标 耗时(ms)
200 次独立持久化 INSERT 1,134.7
事务 200 条 INSERT 总计 687.355
BEGIN 269.806
事务内 200 条 INSERT 41.129
COMMIT 376.345

事务总耗时从首次的 67,549.539 ms 降至 687.355 ms,约快 98 倍,吞吐从 2.96 升至 290.97 行/秒;本次事务写入约比逐条持久化快 1.65 倍。两次独立运行未建立统计置信区间,倍数仅描述这两次测量。

BEGIN 与 COMMIT 仍获取整库快照,合计约占事务总耗时的 94%;COMMIT 计时也包含差分、冲突检查和持久化,不能全部归因于快照。整库快照传输、修改表比较及刷盘仍需进一步分阶段测量。

20 个不同绑定的相关 EXISTS 平均 2,135.218 ms(首次 2,061.770 ms),该瓶颈仍存在,本轮没有改变子查询执行方式。