☰
AI 写的 SQL 能直接上线吗?上线前必查的 6 项 + EXPLAIN 速查
2026/9/29 21:53:23 网站建设 项目流程

目录

    • 一、SQL 的「对」有三层
    • 二、先把这 4 样东西喂给它
    • 三、上线前必查的 6 项
      • 1. UPDATE / DELETE 的 WHERE 范围
      • 2. NULL 的语义
      • 3. JOIN 之后的重复计算
      • 4. 索引失效的写法
      • 5. 分页和排序
      • 6. DDL:锁和回滚
    • 四、EXPLAIN 速查(MySQL)
    • 五、可复制审查 Prompt
    • 六、两次翻车
      • 翻车 1:加个字段,接口全超时
      • 翻车 2:测试库上飞快的报表
    • 七、什么时候不用这么较真
    • 小结

AI 写 SQL 有个特点:语法几乎不会错。

JOIN、子查询、窗口函数张口就来,格式还很整齐。问题出在它不知道的东西上:你的表有多大、建了哪些索引、数据库是什么版本、这条语句是在线接口在调,还是半夜跑一次的报表。

所以 AI 写的 SQL,「能跑」和「能上线」之间隔着一层它看不到的信息。这篇记我审 AI 写的 SQL 时固定检查的 6 项,附一份 EXPLAIN 速查和一段审查 Prompt。以 MySQL 为主,PostgreSQL 有差异的地方单独标了。


一、SQL 的「对」有三层

层次含义AI 的表现
语法对能执行,不报错基本不出问题
结果对返回的数据符合预期看情况,NULL 和 JOIN 常出错
代价对在真实数据量下不拖垮数据库它不知道数据量,只能猜

测试库里几百行数据,三层都能「通过」。真正的问题都在后两层,而且要到线上数据量才暴露。


二、先把这 4 样东西喂给它

1. 表结构,含索引

-- MySQLSHOWCREATETABLEorders;-- PostgreSQL(psql 里执行)\d+orders

2. 数据量级

不用精确,知道量级就行:

-- MySQL:估算值,比 COUNT(*) 快得多SELECTtable_name,table_rowsFROMinformation_schema.tablesWHEREtable_schema='your_db';-- PostgreSQLSELECTrelname,reltuples::bigintFROMpg_classWHERErelname='orders';

3. 数据库和版本

MySQL 5.7 和 8.0 差别很大:窗口函数和 CTE 要 8.0 才有,改表结构的能力也不一样。

4. 使用场景

在线接口、后台报表、一次性修数,三者对性能和安全的要求完全不同。

不给表结构,它会按「常见命名」编字段:user_name还是username,create_time还是created_at,全靠猜。字段名猜错会直接报错,还算好的;更麻烦的是索引猜错了——写出来的语句能跑,但走不上索引。

我在 wescode 里会把建表的迁移文件和对应的 model 文件一起@进对话,让它先对一遍字段和索引,再开始写。

⟦截图:wescode 对话里 @ 迁移文件和 model 文件后让它核对字段,说明文字「先对字段和索引,再写 SQL」⟧


三、上线前必查的 6 项

1. UPDATE / DELETE 的 WHERE 范围

修数脚本最怕范围不对。AI 写的条件看着合理,但边界可能和你想的不一样:<还是<=,状态值有没有漏,时区对不对。

固定动作:先把 UPDATE / DELETE 改写成 SELECT COUNT(*),用同样的 WHERE 跑一遍,看行数是否符合预期。

-- 先确认影响行数SELECTCOUNT(*)FROMordersWHEREstatus='pending'ANDcreated_at<'2026-09-01';-- 数量对了再执行。大表分批,重复执行直到影响行数为 0-- (UPDATE ... LIMIT 是 MySQL 语法)UPDATEordersSETstatus='expired'WHEREstatus='pending'ANDcreated_at<'2026-09-01'LIMIT1000;

MySQL 还可以在会话里开安全模式。WHERE 没用到索引列、又没带 LIMIT 的 UPDATE / DELETE 会被直接拒绝:

SETSESSIONsql_safe_updates=1;

2. NULL 的语义

AI 最常踩的是NOT IN:

-- 查「没下过单的用户」SELECT*FROMusersWHEREidNOTIN(SELECTuser_idFROMorders);

只要orders.user_id里有一个 NULL,这条语句一行都不返回。不报错,只是结果为空——很容易被当成「确实没有」。

改用NOT EXISTS:

SELECT*FROMusers uWHERENOTEXISTS(SELECT1FROMorders oWHEREo.user_id=u.id);

同类的还有:status != 'paid'不会返回 status 为 NULL 的行;COUNT(col)不计 NULL,COUNT(*)计。哪些列允许 NULL 写在表结构里,这也是要把建表语句给它的原因。

3. JOIN 之后的重复计算

一对多 JOIN 之后再聚合,金额会被放大:

-- 想算每个用户的订单总额和商品件数SELECTo.user_id,SUM(o.amount)AStotal,COUNT(i.id)ASitemsFROMorders oJOINorder_items iONi.order_id=o.idGROUPBYo.user_id;

一个订单有 3 件商品,这个订单的amount就被加了 3 次。语法完全正确,结果是错的。

改法是先按订单把明细聚合好,再 JOIN:

SELECTo.user_id,SUM(o.amount)AStotal,SUM(i.cnt)ASitemsFROMorders oJOIN(SELECTorder_id,COUNT(*)AScntFROMorder_itemsGROUPBYorder_id)iONi.order_id=o.idGROUPBYo.user_id;

检查方法:JOIN 前后各跑一次 COUNT(*)。行数变多了,就要确认被聚合的列会不会被重复计算。

4. 索引失效的写法

就算给了表结构,AI 也经常写出用不上索引的条件。最常见的四种:

写法问题改法
WHERE DATE(created_at) = '2026-09-01'对索引列用了函数created_at >= '2026-09-01' AND created_at < '2026-09-02'
WHERE phone = 13800000000(phone 是 varchar)隐式类型转换WHERE phone = '13800000000'
WHERE name LIKE '%伟'前导通配符调整需求,或改用全文索引
联合索引(a, b),条件只有b = ?不满足最左前缀,通常用不上调整索引或查询条件

隐式转换那条最隐蔽:字符串列和数字比较,MySQL 会把列的值逐行转成数字再比,索引就用不上了。反过来(数字列和字符串比较)不受影响。

另一头是改索引:加索引或删索引之前,得先弄清楚项目里有哪些查询在用这张表。我会在 wescode 里问「项目里哪些地方查询了 orders 表,WHERE 和 ORDER BY 分别用了哪些列」,把这些地方列出来逐条对照:新索引能不能用上,删掉旧索引会影响谁。用命令行的话,git grep -n orders加上 ORM 的查询方法名一起搜;表名是动态拼出来的地方要额外留意。

5. 分页和排序

  • LIMIT 不带 ORDER BY:返回顺序不确定,翻页时可能重复或者漏数据。
  • 深分页:LIMIT 100000, 20要先扫过 100020 行、再丢掉前 100000 行,越往后翻越慢。改成按上一页最后一条的 id 往后取:
SELECT*FROMordersWHEREid>?-- 上一页最后一条的 idORDERBYidLIMIT20;

AI 写分页基本都是 OFFSET 写法,数据量小的时候看不出问题。

6. DDL:锁和回滚

改表结构的迁移脚本,是 AI 最「不知道自己不知道」的地方。语句本身通常没错,风险在执行的那一刻。

MySQL

显式写上ALGORITHM和LOCK。当前版本不支持这种方式时会直接报错,而不是悄悄退化成锁表拷贝:

ALTERTABLEordersADDCOLUMNremarkVARCHAR(255)NULL,ALGORITHM=INSTANT;ALTERTABLEordersADDINDEXidx_user_created(user_id,created_at),ALGORITHM=INPLACE,LOCK=NONE;

执行前把锁等待超时调短,等不到锁就失败,别把后面的请求堵住(下面翻车 1 就是这个):

SETSESSIONlock_wait_timeout=5;

大表改结构,考虑 gh-ost、pt-online-schema-change 这类在线变更工具。

PostgreSQL

  • 建索引用CREATE INDEX CONCURRENTLY,否则建索引期间写入会被阻塞。它不能在事务块里执行,而很多迁移工具默认给每个迁移包一层事务,要单独处理。
  • 给有数据的表加NOT NULL列又不给默认值,会直接失败。

通用

  • 改列名、删列不要一次做完。滚动发布期间旧代码还在跑,列没了就报错。拆成几步:加新列 → 双写 → 回填 → 切读 → 下个版本再删旧列。
  • 写清楚回滚方案。DROP COLUMN删掉的数据,回滚脚本是找不回来的。

四、EXPLAIN 速查(MySQL)

EXPLAINSELECT...;
字段看什么危险信号
type访问方式ALL(全表扫描)、index(扫全部索引)
key实际用上的索引NULL(没用上索引)
rows预估扫描行数远大于实际返回的行数
Extra附加信息Using filesort、Using temporary

type从好到差大致是:const>eq_ref>ref>range>index>ALL。在线接口的查询出现ALL,基本就要处理。

EXPLAIN 的输出我会直接贴回 wescode 的对话里,让它逐行解释type、rows、Extra分别说明了什么,再对照上面这张表自己核对一遍。

⟦截图:把 EXPLAIN 结果贴进 wescode 对话后的逐行解读,说明文字「逐行解读执行计划」⟧

两个注意:

EXPLAIN 只是预估。想看真实执行情况用EXPLAIN ANALYZE(MySQL 8.0.18 起支持,PostgreSQL 也有),但它会真的执行这条语句。在 PostgreSQL 里分析 UPDATE / DELETE,要包在事务里再回滚:

BEGIN;EXPLAINANALYZEUPDATEordersSETstatus='expired'WHERE...;ROLLBACK;

测试库上的执行计划不作数。数据量和分布跟线上不一样,优化器可能选完全不同的执行计划。至少要在数据量接近的环境里看。


五、可复制审查 Prompt

你是 DBA 视角的 SQL 审查员。数据库:MySQL 8.0(InnoDB)。 表结构(含索引): <粘贴 SHOW CREATE TABLE 的输出> 数据量级:orders 约 3000 万行,order_items 约 1 亿行,users 约 500 万行 使用场景:<在线接口,峰值 QPS 约 200 / 后台报表,每天一次 / 一次性修数> 待审查 SQL: <粘贴> 请检查: 1. 结果正确性:NULL 语义、JOIN 是否会放大行数导致重复计算、边界条件 2. 执行代价:预计使用哪个索引;可能出现全表扫描、filesort、临时表的地方;索引失效的写法 3. 如果是 UPDATE / DELETE:WHERE 范围是否可能过大,是否需要分批 4. 如果是 DDL:是否锁表、能否用 INSTANT / INPLACE、回滚方案 输出格式:问题 → 依据 → 改法。 执行计划相关的判断一律标注「需要 EXPLAIN 验证」,不要把推测写成结论。

最后一句是关键。不加这句,它会很笃定地告诉你「这条会走 idx_user_id 索引」——那只是它的推测,以 EXPLAIN 的结果为准。

这段 Prompt 我在 wescode 里存成了一个自定义技能(就是一份SKILL.md),审 SQL 时直接按这个技能来,不用每次重新粘贴;数据量级和使用场景那两行,每次按实际情况补上。


六、两次翻车

翻车 1:加个字段,接口全超时

给一张大表加字段。AI 给的语句没毛病,MySQL 8.0 下还是 INSTANT,理论上秒级完成。

执行之后卡住了。紧接着,读这张表的接口开始大面积超时。

SHOW PROCESSLIST一看:ALTER 的状态是Waiting for table metadata lock,后面排了一长串普通查询,状态一模一样。

原因是有个长事务一直没提交(后来查到是有人在客户端里开了事务、查完忘了关),拿着这张表的元数据锁。ALTER 要拿排他锁,只能等;而 ALTER 一旦开始排队,后面新来的查询也得排在它后面。一个没提交的事务,加一条本该秒级完成的 DDL,把整张表堵死了。

AI 写的语句没问题,问题是它不知道执行那一刻数据库里正在发生什么。从那以后,执行 DDL 前我固定做两件事:

-- 先看有没有长事务SELECTtrx_id,trx_started,trx_mysql_thread_idFROMinformation_schema.innodb_trxORDERBYtrx_started;-- 锁等待超时调短,等不到就失败SETSESSIONlock_wait_timeout=5;

翻车 2:测试库上飞快的报表

一条报表 SQL,测试库几百行数据,毫秒级返回。上线后第一次跑,把从库 CPU 打满了。

EXPLAIN 一看,type是ALL。WHERE 里写的是DATE(created_at) BETWEEN ...,created_at上的索引完全没用上。测试库数据太少,全表扫描也就一眨眼,根本看不出来。

改成范围条件之后走上了索引。教训有两条:审 SQL 时把线上数据量告诉 AI;EXPLAIN 要在数据量接近的环境里看,测试库上的「很快」说明不了任何问题。


七、什么时候不用这么较真

一次性的只读查询。在从库或本地跑,查完就扔,结果对就行。

小表。几千行的配置表、字典表,全表扫描也无所谓。

已经有 SQL 审核平台和慢查询告警的团队。机器能拦的交给机器,人重点看结果正确性和 DDL 的执行时机。


小结

AI 写 SQL 的问题不在语法,在于它缺三样信息:表结构、数据量、执行那一刻的数据库状态。前两样可以喂给它,第三样只能你自己在执行前确认。

固定动作:

  1. 先给表结构和数据量级,再让它写
  2. 修数先 COUNT,大表分批
  3. 逐项查 NULL、JOIN 放大、索引失效、分页
  4. DDL 显式写 ALGORITHM / LOCK,执行前先查长事务
  5. 执行计划以 EXPLAIN 为准,不以 AI 的判断为准

上面的检查项我是在 wescode 里写 SQL 时用的,官网是 weisyn.com。你们上线 SQL 有没有强制的审核流程,或者踩过什么 AI 写 SQL 的坑,欢迎评论区聊聊。
觉得有用的朋友,欢迎点赞、收藏、关注,后面会继续分享 AI 编程的实战经验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询