让 AI 来写几乎所有代码还是感觉有点奇怪——但我决定在这个新项目上认真试一试。背景是:我快没什么项目点子可分享了,于是让 AI 列了一个清单,选了其中一个叫数据库性能测试(Database Performance Testing)的项目。目标是对关系型数据库运行性能测试。我用大约两天时间完成,在这篇文章中,我想分享我的真实感受——既有技术层面的,也有关于 AI 辅助开发流程本身的。
我认为这是开始任何项目之前需要问的第一个问题。碰巧的是,我目前参与的一个项目中,数据性能是系统的关键关注点——这让我开始思考:直接对关系型数据库运行性能测试会是什么样子?
从 QA 角度看,性能不只是 API 如何连接数据库的问题。它还与查询是如何编写的以及数据库如何处理查询并发有关,特别是在同步系统中。一个缓慢的端点并不总是端点的锅——有时是背后隐藏着慢查询。这就是驱动我从此处开始的原因。
这个项目有四个不同的测试场景:
N+1 是访问数据库的应用中一个经典的性能问题——尤其是使用 ORM(如 Entity Framework、SQLAlchemy 或 Sequelize)的系统。这个名字精确地描述了问题:应用程序最终运行的不是一次优化后的查询,而是1条初始查询 + N条额外查询。
考虑一个列出 20 个订单并显示每个订单用户邮箱的电商系统。朴素的做法是:
SELECT id, user_id FROM orders LIMIT :n;
SELECT email FROM users WHERE id = :uid;
第一条查询获取订单。第二条查询随后运行了 20 次——每个订单一次。这就是你的 N+1。大规模下的后果包括:延迟成倍增加、缓存污染、以及用户不可预测的响应时间。
在并发场景中也会出现。两个线程同时拉取用户数据,每个都触发自己的级联查找,会迅速成倍增加数据库负载,而且没有工具很难追踪。
想象两个同时进行的事务:
事务 A:
UPDATE orders SET status = 'pending' WHERE id = 1;
UPDATE orders SET status = 'paid' WHERE id = 2;
事务 B(同时运行,顺序相反):
UPDATE orders SET status = 'paid' WHERE id = 2;
UPDATE orders SET status = 'pending' WHERE id = 1;
每个事务持有对方需要的锁。数据库检测到循环并通过回滚其中一个事务来解决——但这种回滚是有代价的。在大规模下,这类争用会导致明显的延迟尖峰,最坏情况下会锁住应用程序的部分功能。死锁测试的目标不仅是确认死锁可以发生,更是要衡量数据库如何恢复以及时间影响有多大。
这个场景专注于度量 Schema 变更——索引创建、迁移、表修改——对查询执行计划和耗时的影响。它在迁移前后捕获 EXPLAIN ANALYZE 输出,然后对比差异。
特别适用于:
慢查询测试充当一个延迟门禁(latency gate)——一组关键查询必须保持在定义的门限(在 config.py 中配置为 SLOW_QUERY_THRESHOLD_MS)之下。如果任何查询在 CI 中突破了门限,流水线会失败。
可以把它理解为最重要数据库操作的性能预算。本项目中监控了五个关键查询。以下是低负载基准运行的结果:
| 查询 | 平均 (ms) | p95 (ms) | 最大 (ms) |
|---|---|---|---|
| `user_lookup` | 0.20 | 0.29 | 0.35 |
| `order_history` | 0.31 | 0.43 | 0.75 |
| `inventory_search` | 0.24 | 0.32 | 0.62 |
三个查询都轻松通过了门限。但这里的价值不只是绿灯——而是拥有一个历史基线。下次有人添加筛选条件、修改 JOIN 或删除索引时,你会立刻知道这些数字是否发生了变化。
项目结构如下:
.
├── config.py # DB_URL 和 SLOW_QUERY_THRESHOLD_MS
├── conftest.py # pytest fixtures: engine, instrumented_engine
├── pyproject.toml # pytest 配置和标记
├── requirements.txt
├── analysis/
│ ├── n_plus_one_detector.py # N+1 检测和模拟
│ ├── deadlock_simulator.py # 并发死锁演示
│ └── explain_analyzer.py # EXPLAIN 计划捕获和差异比较
├── benchmarks/
│ ├── queries/ # 原始 .sql 文件(12个查询)
│ ├── scenarios/
│ │ └── run_benchmark.py # 容量基准运行器
│ ├── test_n_plus_one.py # N+1 检测测试
│ ├── test_deadlock.py # 死锁测试
│ ├── test_explain.py # EXPLAIN ANALYZE 计划测试
│ └── test_slow_queries.py # 延迟门限测试(5个关键查询)
├── data/
│ ├── seed.py
│ └── distributions.json
├── migrations/
│ ├── baseline/
│ │ └── 001_initial_schema.sql
│ └── v2_add_indexes/
│ └── 002_add_indexes.sql # 回归演示的示例迁移
├── reports/
│ ├── query_regression_report.py # 差异报告器(也可导出到 Grafana)
│ ├── export_metrics.py # 将结果写入 benchmark_results 表
│ ├── output/ # 带时间戳的基准 JSON 结果
│ └── plans/ # 保存的 EXPLAIN 计划
├── scripts/
│ └── setup_schema.py # 应用迁移 + 快照 schema
├── .github/
│ └── workflows/
│ └── performance-tests.yml # CI: schema → seed → pytest
└── docker/
├── docker-compose.yml # PostgreSQL + Grafana
├── init.sql
└── grafana/
├── provisioning/
└── dashboards/benchmark.json # 自动配置的仪表盘
这个结构中值得强调的几点:
analysis/ 设计为可独立运行。 每个模块——N+1 检测、死锁模拟、EXPLAIN 分析——都可以单独执行。你不必每次都运行全套。这在实际中有意义:有时你只关心迁移后的回归追踪,同时运行死锁模拟只是噪音。
explain_analyzer.py 捕获和比较 EXPLAIN 计划。 这是查询回归场景的核心——它在 Schema 变更前后保存计划快照并计算差异。结果以带时间戳的 JSON 文件存储在 reports/plans/ 下。
Grafana 自动配置。 export_metrics.py 在每次运行后将数据写入 benchmark_results 表,grafana/dashboards/benchmark.json 中的仪表盘自动加载。无需手动设置。
最有趣的结果来自查询回归场景。通过迁移 002_add_indexes.sql 添加复合索引(idx_orders_user_created)后,order_history 的 EXPLAIN 计划发生了巨大变化:
索引前——顺序扫描(Sequential Scan):
orders 表(触及 39 个块),过滤了 2,497 行 找到了 3 条匹配记录索引后——位图索引扫描(Bitmap Index Scan):
idx_orders_user_created,只读取了 5 个块执行时间减少了 83%,规划器成本下降了 ~10 倍——从扫描 2500 行的全表扫描到精准的索引查找。如果没有回归追踪,这种变化只有在有人碰巧手动运行 EXPLAIN 时才会被发现。
这正是值得放在 CI 中的验证方式:添加索引,运行测试套件,得到清晰的 before/after 差异,确认优化确实有效。
这部分我想多花点时间聊聊,因为 AI 辅助的工作流和最终的代码产出一样值得反思。
我实际做的是:让 AI 提出项目点子、细化范围,然后迭代地让它生成解决方案——先结构、再各个模块、然后 CI 流水线。我没有从头写太多代码。我真正花时间的是阅读 AI 生成的代码,质疑测试逻辑是否合理,验证场景是否对应了它们要模拟的真实问题。
这个工作流与我自己全部手写的不同之处:
AI 真正帮上忙的地方: 搭建 Docker + Grafana 集成这件事如果我自己手动做要花半天。AI 在几分钟内就有了一个自动配置仪表盘的可用 docker-compose.yml。仅此一项,对于 POC 来说就值得了。
AI 不足的地方: AI 容易产出看起来很自信但需要仔细审查的代码。有几个生成的 SQL 文件的列名与 Schema 不匹配。N+1 检测逻辑需要额外处理才能正确处理并发情况。这些都不是阻塞性问题,但它们强化了一个事实:人机协作不是可选项——它就是工作本身。
我会继续尝试 AI 辅助的工作流,特别是测试工具和 POC 方面。如果你构建过类似的东西,或者对这个方法有想法,欢迎在评论区交流。完整解决方案可以在这里查看。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。