用六个用户,检查 AI 写的订单统计 SQL

小周debug Lv.2 2026/10/22 18:25:13 浏览 1

统计“每个用户的已支付订单数和金额”,没下过单的人也应该保留一行 0, 0。如果 AI 给的是下面这条 SQL,查询不会报错,这些人却会直接消失:

SELECT u.id AS user_id,
       COUNT(o.id) AS paid_count,
       COALESCE(SUM(o.amount_cents), 0) AS paid_cents
FROM users AS u
LEFT JOIN orders AS o ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY u.id;

这里的需求是“每个用户”,而 WHERE 又把没有匹配订单时补出的 NULL 行过滤了。不能只拿一个正常下单的用户试一下。

我把这个问题做成了一个可直接跑的小样例:两张表、六个用户、三条故意写错的查询和一条正确查询。全部是构造数据,未调用模型,也不代表哪款模型的错误率。

下载 SQL、六用户夹具、检查器、测试和完整提示词包

先把应该算出什么定下来

这份样例只认 status = 'paid',金额用整数分。取消、退款和 NULL 状态都不计入;它不是完整的财务结算口径。

用户构造订单paid_countpaid_cents
1两笔 paid,各 1000 分22000
2完全没有订单00
3只有 cancelled00
4一笔 paid,金额 010
5paid 1200 分,另有 cancelled 9900 分11200
6一笔 NULL 状态,另有 refunded00

用户 4 是为了区分“金额为零”和“没有订单”;用户 1 则防止用 SUM(DISTINCT amount_cents) 把两笔同额订单合并了。预期结果已经独立写在 expected.json,不会临时从待测 SQL 生成答案。

解压后运行

只用 Python 标准库,不需要 API key 或额外安装包。本地验证版本为 Python 3.12.14、SQLite 3.53.1。

python build_db.py
python check_sql.py bad_where.sql
python check_sql.py good.sql
python verify.py

第一条创建的是附件内的教学库 demo.sqlite。bad_where.sql 的结果只剩用户 1、4、5,检查器会报告 missing: [2, 3, 6] 并以 1 退出;good.sql 应为 PASS、6 行、退出码 0。

bad_count.sql 和 bad_distinct.sql 也能单独传给检查器。前者把没有已支付订单的人算成 1 单,后者把用户 1 的金额算成 1000 分。SQL 都能执行,错的是结果。

检查器按 user_id 对照,检查列名、整数值、缺行、多行、重复 ID 和数值差异,不要求行顺序相同。参数或 SQL 执行错误走标准错误输出,退出码为 2;比较不一致退出 1,只有符合这份预期才退出 0。

最终 ZIP 已在新目录解压重跑,14 组检查通过,包含上述反例、乱序、重复 ID、多余 ID,以及语法错误、写入和 ATTACH 被拒绝的情况;测试前后样例库哈希未变化。build_db.py 遇到已经存在的库会拒绝覆盖,不需要为了再测一条查询而重建。

这条查询怎样改

SELECT u.id AS user_id,
       COUNT(o.id) AS paid_count,
       COALESCE(SUM(o.amount_cents), 0) AS paid_cents
FROM users AS u
LEFT JOIN orders AS o
  ON o.user_id = u.id AND o.status = 'paid'
GROUP BY u.id;

把订单状态放进连接条件,保留没有 paid 订单的用户。o.id 是非空主键,COUNT(o.id) 不会把外连接补出的空行算成订单;不能随手换成 COUNT(*)。金额没有匹配项时由 COALESCE 补成整数 0,也不按金额去重。

交给 AI 改时,把验收条件一起给它

包里有完整 prompt.txt。把原查询保存为 candidate.sql,可以直接使用这段任务要求:

请只修改 candidate.sql,使用 SQLite 语法。
按 fixture.sql 的两张表统计每个用户的 paid 订单数和金额。
结果列固定为 user_id, paid_count, paid_cents,均为整数,
每个用户一行;没有 paid 订单时后两列为 0。
零元 paid 要计数,两笔同额 paid 要分别求和。
不得修改夹具、expected.json 或检查器。
仅在 demo.sqlite 不存在时运行 python build_db.py;
已存在则保留,再运行
python check_sql.py candidate.sql,报告实际退出码和差异。
不能运行时明确说未运行。

换成自己的业务,先人工确认应有的输出,再换夹具和预期;不要让同一次 SQL 生成顺便决定什么算“正确”。本工具只比较当前用例,覆盖不了时区、退款分摊、重复支付等未建模规则,也没有做真实数据库方言或性能验证。

检查器以只读方式打开指定 SQLite 库,另加查询限制;这些不是运行任意不可信 SQL 的通用沙箱。这里只在随包的本地教学库上验证,不应直接连接业务库试错。

来源:SQLite《SELECT》《Built-in Aggregate Functions》、OpenAI《Prompting》,2026-10-22 核对当前文档;本例 SQL、夹具和检查器为原创材料。

评论 0

发表评论

登录后参与讨论

登录

暂无评论