2227 字
约 7 分钟
18
S24-A24 · AI 写的 SQL:谁能担保它没有漏掉一半条件

一个"自然语言转 SQL"的内部工具上了线,业务同学很喜欢用。某天一份"上月各区域销售额"的报表,结果比财务的少 30%——因为 AI 生成的 SQL 漏了 status != 'CANCELLED' 这个条件(它在另一张表的字段里,AI 没看到)。

AI 生成的 SQL 有三类风险:语义漏条件、性能踩坑、以及权限越界。而它们的共同点是"SQL 能跑通、结果非空",这个"看起来对"是这类风险的共同伪装:SQL 跑通了、返回了、有数——而"有数"恰恰让人"不再怀疑",于是错误一路走到报表与决策里。
于是"看起来对"。

1. 三类风险的分布

风险 表现
语义漏条件 结果偏小/偏大,但"看着合理"
性能坑 全表扫、笛卡尔积、SELECT *
权限越界 查到了不该看的行/表

第一项最难发现:**结果为"非空的、合理的数字"**第一项的"延迟暴露"很长:它可能要等"财务对账"才被发现——而那时"依据这个数字做的决定"已经发出去了。
——而"漏一个过滤条件"通常不会让数字变成 0 或者离谱。

2. "漏条件"的三种成因

成因 说明
字段在别的表 AI 只看了"被问到的那张表"
业务规则是"口头约定" 如"作废单不计入"
同名字段语义不同 amount 在一个表是"含税",另一个是"不含税"

第三项的"常见形态"是"含税与不含税"、"含运费与不含运费"、"主表金额与明细汇总"——三组都"不报错、差几个点到几十个点"。
第三项最危险:它不会报错,只会"算错"——而"差 13% 税率"正好落在"看起来合理"的范围内。

"喂给它什么"决定了"它能看到什么",而"Schema 摘要"要"带语义":

// 一份"够用"的 Schema 摘要(三行一表)
orders(id, user_id, amount DECIMAL--含税, status ENUM--CANCELLED 不计入, created_at)
order_items(order_id, sku_id, qty, price DECIMAL--单价不含税)
dim_region(region_id, name)
// 关键:把"amount 含税""CANCELLED 不计入"这类"语义"写进摘要

3. "三种加固"的做法

加固 做法
语义层(指标定义) 把"销售额"定义成一段"带条件的 SQL 片段"
结构层(Schema 摘要) 把"表关系 + 关键字段语义"喂给它
验证层(对账) 与"已知正确"的口径比对

第一项是"最有效"的:不让它"从零写",而是"用已定义好的指标拼"第一项的"额外收益"是"可审计":"gmv 的定义在一处,谁都能查"——而"AI 每次现写"意味着"每个数字都要单独解释"。
——于是"漏条件"从"模型问题"变成"配置问题"。

// 一个"指标定义"(可复用、可审计)
metric: gmv
sql: |
  SELECT sum(o.amount) FROM orders o
  WHERE o.status NOT IN ('CANCELLED','REFUNDED')   -- 关键条件写在这里
    AND o.created_at >= :start AND o.created_at < :end
// AI 的任务变成"选指标 + 填参数",而不是"重写条件"

"语义对账"是最省事的验证,而它只需要"一个已知正确的结果":

// 三步对账(每次上线新指标时)
1 用"手工 SQL"算一遍(业务确认过的口径)
2 用"AI + 指标定义"算一遍
3 比对:行数、总额、分组数(三者都对,才算通过)
// 判据:三个数都对 → 语义可信;只有一个对 → 通常"漏了过滤条件"

4. "性能坑"的三种识别

坑 识别
全表扫 EXPLAIN 的 type=ALL
笛卡尔积 结果行数异常膨胀
SELECT * 大表 + 宽表 → 传输爆炸

第一项的"阈值"可以很粗:"预估扫描行数 > 100 万"或"type=ALL 且表 > 10 万行"就拒——它拦掉的是"明显灾难",而不是"所有慢查询"。
第一项"必须自动检查":在"执行前"跑一次 EXPLAIN,超过阈值就"拒绝执行"——因为"等它跑完"可能已经拖垮库。

"表/列暴露"要"白名单",否则"它会去猜哪张表有数据":

// 一份"暴露清单"(白名单,三行)
tables: [orders, order_items, dim_region]        # 只给这三张
columns: 排除 user.phone / user.id_card            # 列级脱敏
joins:   只允许 orders.id = order_items.order_id    # 关系白名单
// 判据:它"看不到表"时,会明确报"缺少数据源",而不是"编一张表"

5. "权限越界"的两道防线

防线 做法
表/列级 只暴露"允许的表与列"给它
行级 强制拼上"租户/部门"过滤条件

第二项是"必须由系统拼"的:**不能"让 AI 记得加"**第二项的"实现"是"在 SQL 外面套一层"强制条件"":把用户 SQL 当成子查询,外层加 WHERE tenant_id = :tid——于是"它写什么都跑不出自己的数据"。
——因为它可能忘,也可能"被用户引导"去掉。

"漏条件"这件事可以用"断言"抓,而它比"人对账"便宜:

-- 三条"业务不变式"断言(在结果集上跑)
1 总额 ≤ 订单表总额(漏了作废过滤会超)
2 分组数 = 区域数(漏了 join 会少)
3 每天的总额与"日汇总表"差 < 0.5%
-- 判据:断言失败 → 拒绝对外输出,而不是"人工看一眼"

6. 排错:五种"AI SQL 问题"

现象 成因 处理方向
数字对不上 漏条件/口径不同 用指标定义
偶尔超时 全表扫 EXPLAIN 前置
查到别人数据 行级过滤缺失 系统强制拼
结果列过多 SELECT * 限制列
同一问题两次不同 无固定模板 用指标 + 参数

第五项的"修法"是"确定性":同一问题 → 同一指标 + 同一参数(模板化),而不是"每次让模型发挥"——可信度来自"可复现",而不是"更聪明"。
第五项是"可信度"问题:同一句自然语言两次生成不同 SQL——于是"业务同学不再相信它"。

7. 边界:什么情况"让 AI 直接写 SQL"可以接受

场景 为什么 做法
探索性分析(只看趋势) 数字不必精确 直接写
内部临时查询(一次性) 不进报表 直接写
小表(万行以内) 性能与口径风险低 直接写
有"人工复核"环节 错误会被拦 直接写

判断口径一句话:"这个数字会不会被拿去"做决定或对外报"。最后一条与"权限"有关的边界:"只读账号"不等于"安全"——一个"能查全表"的只读账号,配上"会写 SQL 的 AI",等于"把全库交给自然语言";所以"暴露面"要"按表/列收敛",而不是"按账号收敛"。
会(报表、结算、考核),就必须"指标定义 + EXPLAIN 前置 + 行级过滤";不会(探索、临时),直接写反而更快。

再补一个"产品"层面的建议:让它"说出它的假设"——"我按'已支付订单'统计,如果你要'含未支付',请说'"。这句话把"隐藏的口径"变成"可见的假设",于是用户能"当场纠正",而不是"事后发现差 30%"。

而"假设可见"还有一个副作用:它逼着"指标定义"更完整——当它每次都要说"我假设了什么",那些"没被定义的口径"就会浮出来,于是"指标库"自然被补全。

这比"事后对账"便宜得多。

所以"让它说假设"这件事,同时是"质量机制"与"指标治理"。

8. 自测

  1. 三类风险里,哪一类最难发现?为什么"漏条件"通常不会让数字变得离谱?
  2. "同名字段语义不同"为什么"不会报错只会算错"?
  3. "指标定义"为什么比"让 AI 从零写"有效?它把问题从"模型"变成了什么?
  4. "全表扫"为什么要"执行前"检查而不是"跑完再看"?
  5. 行级过滤为什么"必须由系统拼",而不能"让 AI 记得加"?
  6. 判断"能否让 AI 直接写 SQL"的那个问题是什么?
S24-A24 · AI 写的 SQL:谁能担保它没有漏掉一半条件
https://www.clxhxhhr.top/posts/6660/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。