楠风测开 · 测试工程师复习日记 Day15
给SQL建立可回归的业务契约,再讨论AI改写和执行效率。
前言
朋友们好,我是楠风。今天从Linux阶段转到MySQL。
做接口自动化时,我们经常增加一个数据库断言:接口成功后,查询订单表,确认记录存在。但存在性检查只覆盖了最小的一部分。状态迁移、金额、支付流水、库存,以及失败后的回滚,才共同构成业务结果。
这篇用一个订单报表说明:怎么把SQL从"能运行"推进到"能回归",又怎样审核AI给出的优化建议。示例以MySQL 8.4、InnoDB为参照,使用合成数据;没有本机MySQL性能实测,也不提供虚构的提速比例。
一、先为查询写一份业务契约
假设我们有四张表:orders保存订单,order_items保存商品明细,payments保存支付尝试,products保存商品当前价格与库存。
先把需求写成可以检查的规则:
text
输出粒度:每张订单一行。
覆盖范围:保留已支付、待支付、取消的订单。
明细金额:数量 × 下单时单价,再按订单汇总。
成功支付:只统计status='SUCCESS'的支付记录。
缺失支付:报表展示为0,但订单仍要保留。
顺序要求:按订单ID排列,便于固定样例逐行比较。
这里的规则是教学场景,不代表所有电商系统。退款、部分支付、优惠分摊、币种换算都可能改变核对口径,接入真实系统前应单独明确。
金额字段使用DECIMAL(10,2),避免用近似浮点值表达这组两位小数金额。还要固定舍入位置;"每行先舍入再加总"和"合计后舍入"可能有不同结果。MySQL精确数值类型
二、让样例数据主动暴露问题
只造一张订单、一条明细、一笔支付,很多错误SQL也能通过。
我们至少准备以下四种记录:
| 订单 | 明细 | 支付记录 | 应检查的现象 |
|---|---|---|---|
| 1001 | 键盘2件,合计199.80 | 一次失败、一次成功 | 失败支付不计入金额 |
| 1002 | 鼠标1件,59.90 | 没有记录 | 订单不能被关联过滤 |
| 1003 | 键盘1件,99.90 | 一次失败,订单已取消 | 无成功支付不等于仍可支付 |
| 1004 | 键盘和鼠标,合计159.80 | 成功支付99.90和59.90 | 两个一对多关系不能重复计数 |
金额都来自固定样例,可以手算预期。性能数据则不能这样手算成"实测结果"。正确性夹具和性能造数最好分开管理,后续Day16的十万行表专门用于访问路径实验。
三、复现一次一对多连接导致的金额翻倍
下面用CTE临时构造1004的数据,不依赖已有表,不修改数据库。复制到MySQL 8.4查询窗口即可用于核对预期。
sql
WITH orders_demo AS (
SELECT 1004 AS id, 159.80 AS total_amount
), items_demo AS (
SELECT 1004 AS order_id, 1 AS quantity, 99.90 AS unit_price
UNION ALL SELECT 1004, 1, 59.90
), payments_demo AS (
SELECT 1004 AS order_id, 99.90 AS amount
UNION ALL SELECT 1004, 59.90
)
SELECT o.id, COUNT(*) AS joined_rows,
SUM(i.quantity * i.unit_price) AS item_total,
SUM(p.amount) AS paid_total
FROM orders_demo AS o
JOIN items_demo AS i ON i.order_id = o.id
JOIN payments_demo AS p ON p.order_id = o.id
GROUP BY o.id;
预期是4行组合、两个合计均为319.60。这是一个故意展示错误的查询。原因不是SUM失灵,而是聚合之前,商品明细和支付都被连接放大了。
修复时先约束中间结果的粒度:
sql
WITH orders_demo AS (
SELECT 1004 AS id, 159.80 AS total_amount
), items_demo AS (
SELECT 1004 AS order_id, 1 AS quantity, 99.90 AS unit_price
UNION ALL SELECT 1004, 1, 59.90
), payments_demo AS (
SELECT 1004 AS order_id, 99.90 AS amount
UNION ALL SELECT 1004, 59.90
), item_sum AS (
SELECT order_id, SUM(quantity * unit_price) AS amount
FROM items_demo GROUP BY order_id
), payment_sum AS (
SELECT order_id, SUM(amount) AS amount
FROM payments_demo GROUP BY order_id
)
SELECT o.id, o.total_amount,
COALESCE(i.amount, 0.00) AS item_total,
COALESCE(p.amount, 0.00) AS paid_total
FROM orders_demo AS o
LEFT JOIN item_sum AS i ON i.order_id = o.id
LEFT JOIN payment_sum AS p ON p.order_id = o.id
ORDER BY o.id;
预期订单、明细、支付三个金额都是159.80。生产表中的payment_sum还要按业务定义筛选成功状态;这里构造的两条记录已经明确都是成功支付。
再加一组反例:把两条支付都改成79.90。正确合计仍是159.80,而SUM(DISTINCT amount)会得到79.90。因此,按金额去重不是修复交易重复的方法。
四、把NULL、筛选和排序也纳入回归
金额正确后,还不能只比较结果总行数。
左连接的筛选位置
下面是业务表的查询片段:
sql
SELECT o.id, COUNT(p.id) AS success_count
FROM orders AS o
LEFT JOIN payments AS p
ON p.order_id = o.id AND p.status = 'SUCCESS'
GROUP BY o.id
ORDER BY o.id;
它保留没有成功支付的订单。如果AI把状态筛选移动到WHERE,空匹配订单会丢失。COUNT(p.id)为0与COUNT(*)统计外连接保留行也不同。JOIN语义 · 聚合函数说明
空值与业务状态
paid_at IS NULL用于检查缺失支付时间,不意味着订单一定还可以付款。取消订单也可能符合这个条件。代码里把空值转换成0或空字符串之前,要明确这样转换是否会掩盖数据异常。NULL语义
日期边界与稳定排序
sql
SELECT id, status, total_amount
FROM orders
WHERE user_id = 101
AND created_at >= '2026-10-01 00:00:00'
AND created_at < '2026-10-02 00:00:00'
ORDER BY created_at, id;
在样例约定的同一时区下,验证当天零点、次日零点、同一时间多张订单这三种情况。使用分页时尤其需要稳定排序;改写后第一屏看起来相同,不代表后续页不会漏或重复。
五、数据库断言还应覆盖事务结果
查询只能告诉我们当前状态,事务演练用于确认修改是否作为一个整体完成。
以扣减2件键盘、创建订单为例,应用逻辑应明确成下面的流程。它是伪代码,异常处理要按实际语言实现:
text
开启事务
尝试UPDATE products SET stock=stock-2
WHERE sku='KEYBOARD' AND stock>=2
读取本次UPDATE的受影响行数
如果不是1:回滚并结束本次下单
写入订单和明细
如果任意一步异常:回滚并返回失败
只有全部成功:提交
这里的受影响行数要紧接更新读取。不要先执行别的语句,再把已经改变含义的返回值拿来判断库存。
自动化用例至少覆盖成功提交、库存不足、订单号重复、明细写入失败。每个用例记录初始库存和实验订单号,结束后检查订单、明细、库存三个结果,不能只断言接口报错。
给单库事务包上try/except并不自动解决跨服务一致性。第三方支付、消息和邮件有自己的副作用,数据库回滚不会替它们撤销操作。事务控制说明
另外,索引和表结构变更属于另一个边界:一些DDL会隐式提交,不能把建索引放进业务回滚用例里,期待最后一句ROLLBACK把它撤掉。隐式提交说明
六、给AI一个有约束的SQL评审任务
我的建议是让AI输出"候选修改与验证计划",而不是直接要求它宣布优化成功。
text
请评审下面的SQL,环境是MySQL 8.4 / InnoDB。
我会提供表DDL、索引、脱敏数据、原SQL、业务预期。
第一轮只做语义检查:
- 每张订单一行,不能丢失无支付订单;
- 两笔同额合法支付不能被去重;
- 时间范围、NULL和状态规则不能暗中改变;
- 给出错误SQL的最小反例和预期值。
第二轮再做性能候选:
- 说明需要什么执行计划或统计证据;
- 给出一次只改一个因素的比较方法;
- 无实测时不填写耗时和提效结论。
应用里执行动态SQL时还要使用参数绑定,不要为了让AI示例看起来完整,就把用户输入拼接进SQL文本。占位符格式由具体驱动决定,不能混用不同语言的写法。
评审后保留三份材料:原始需求、候选SQL、验收记录。发现问题时,这三份材料能说明是哪条业务规则被改变,而不是停留在"AI生成的代码有问题"。
七、从单次查询到可回归检查
对固定夹具,建议按唯一订单ID逐行比较结果:字段集合、状态、金额、空值、重复次数以及业务需要的顺序。金额在程序中使用十进制定点类型或项目统一金额表示,不要先转浮点再做严格相等断言。
若旧SQL本来有错,比较目标应该是业务预期,不是让新版本和错误结果保持一致。四表报表还应单独检查无明细订单;把缺失金额展示为0之后,不能失去对异常关系的判断。
性能比较放在正确性通过之后。先看EXPLAIN,再决定是否在隔离实验环境运行EXPLAIN ANALYZE;后者确实会执行查询,不是只展示静态建议。EXPLAIN官方说明
最后留一份小型交付
这一篇完成后,项目里应留下四样东西:一份数据关系说明、一组能暴露问题的夹具、正确查询与反例,以及事务成功/失败的验收记录。
它们也是后面做接口自动化平台时可以复用的资产:断言依据、数据准备、失败证据,都能从这里继续长出来。
下一篇 Day16《MySQL 索引 + AI 索引建议》,我们用独立的十万行实验表看查询路径和索引代价。你会优先补哪一类数据库断言:金额、状态、库存,还是重复请求?欢迎分享你的取舍。