ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AI 写的 SQL 能直接上线吗?上线前必查的 6 项 + EXPLAIN 速查

AI 写的 SQL 能直接上线吗?上线前必查的 6 项 + EXPLAIN 速查 目录一、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;-- PostgreSQLpsql 里执行\dorders2. 数据量级不用精确知道量级就行-- MySQL估算值比 COUNT(*) 快得多SELECTtable_name,table_rowsFROMinformation_schema.tablesWHEREtable_schemayour_db;-- PostgreSQLSELECTrelname,reltuples::bigintFROMpg_classWHERErelnameorders;3. 数据库和版本MySQL 5.7 和 8.0 差别很大窗口函数和 CTE 要 8.0 才有改表结构的能力也不一样。4. 使用场景在线接口、后台报表、一次性修数三者对性能和安全的要求完全不同。不给表结构它会按「常见命名」编字段user_name还是usernamecreate_time还是created_at全靠猜。字段名猜错会直接报错还算好的更麻烦的是索引猜错了——写出来的语句能跑但走不上索引。我在 wescode 里会把建表的迁移文件和对应的 model 文件一起进对话让它先对一遍字段和索引再开始写。⟦截图wescode 对话里 迁移文件和 model 文件后让它核对字段说明文字「先对字段和索引再写 SQL」⟧三、上线前必查的 6 项1. UPDATE / DELETE 的 WHERE 范围修数脚本最怕范围不对。AI 写的条件看着合理但边界可能和你想的不一样还是状态值有没有漏时区对不对。固定动作先把 UPDATE / DELETE 改写成 SELECT COUNT(*)用同样的 WHERE 跑一遍看行数是否符合预期。-- 先确认影响行数SELECTCOUNT(*)FROMordersWHEREstatuspendingANDcreated_at2026-09-01;-- 数量对了再执行。大表分批重复执行直到影响行数为 0-- UPDATE ... LIMIT 是 MySQL 语法UPDATEordersSETstatusexpiredWHEREstatuspendingANDcreated_at2026-09-01LIMIT1000;MySQL 还可以在会话里开安全模式。WHERE 没用到索引列、又没带 LIMIT 的 UPDATE / DELETE 会被直接拒绝SETSESSIONsql_safe_updates1;2. NULL 的语义AI 最常踩的是NOT IN-- 查「没下过单的用户」SELECT*FROMusersWHEREidNOTIN(SELECTuser_idFROMorders);只要orders.user_id里有一个 NULL这条语句一行都不返回。不报错只是结果为空——很容易被当成「确实没有」。改用NOT EXISTSSELECT*FROMusers uWHERENOTEXISTS(SELECT1FROMorders oWHEREo.user_idu.id);同类的还有status ! paid不会返回 status 为 NULL 的行COUNT(col)不计 NULLCOUNT(*)计。哪些列允许 NULL 写在表结构里这也是要把建表语句给它的原因。3. JOIN 之后的重复计算一对多 JOIN 之后再聚合金额会被放大-- 想算每个用户的订单总额和商品件数SELECTo.user_id,SUM(o.amount)AStotal,COUNT(i.id)ASitemsFROMorders oJOINorder_items iONi.order_ido.idGROUPBYo.user_id;一个订单有 3 件商品这个订单的amount就被加了 3 次。语法完全正确结果是错的。改法是先按订单把明细聚合好再 JOINSELECTo.user_id,SUM(o.amount)AStotal,SUM(i.cnt)ASitemsFROMorders oJOIN(SELECTorder_id,COUNT(*)AScntFROMorder_itemsGROUPBYorder_id)iONi.order_ido.idGROUPBYo.user_id;检查方法JOIN 前后各跑一次 COUNT(*)。行数变多了就要确认被聚合的列会不会被重复计算。4. 索引失效的写法就算给了表结构AI 也经常写出用不上索引的条件。最常见的四种写法问题改法WHERE DATE(created_at) 2026-09-01对索引列用了函数created_at 2026-09-01 AND created_at 2026-09-02WHERE phone 13800000000phone 是 varchar隐式类型转换WHERE phone 13800000000WHERE 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,ALGORITHMINSTANT;ALTERTABLEordersADDINDEXidx_user_created(user_id,created_at),ALGORITHMINPLACE,LOCKNONE;执行前把锁等待超时调短等不到锁就失败别把后面的请求堵住下面翻车 1 就是这个SETSESSIONlock_wait_timeout5;大表改结构考虑 gh-ost、pt-online-schema-change 这类在线变更工具。PostgreSQL建索引用CREATE INDEX CONCURRENTLY否则建索引期间写入会被阻塞。它不能在事务块里执行而很多迁移工具默认给每个迁移包一层事务要单独处理。给有数据的表加NOT NULL列又不给默认值会直接失败。通用改列名、删列不要一次做完。滚动发布期间旧代码还在跑列没了就报错。拆成几步加新列 → 双写 → 回填 → 切读 → 下个版本再删旧列。写清楚回滚方案。DROP COLUMN删掉的数据回滚脚本是找不回来的。四、EXPLAIN 速查MySQLEXPLAINSELECT...;字段看什么危险信号type访问方式ALL全表扫描、index扫全部索引key实际用上的索引NULL没用上索引rows预估扫描行数远大于实际返回的行数Extra附加信息Using filesort、Using temporarytype从好到差大致是consteq_refrefrangeindexALL。在线接口的查询出现ALL基本就要处理。EXPLAIN 的输出我会直接贴回 wescode 的对话里让它逐行解释type、rows、Extra分别说明了什么再对照上面这张表自己核对一遍。⟦截图把 EXPLAIN 结果贴进 wescode 对话后的逐行解读说明文字「逐行解读执行计划」⟧两个注意EXPLAIN 只是预估。想看真实执行情况用EXPLAIN ANALYZEMySQL 8.0.18 起支持PostgreSQL 也有但它会真的执行这条语句。在 PostgreSQL 里分析 UPDATE / DELETE要包在事务里再回滚BEGIN;EXPLAINANALYZEUPDATEordersSETstatusexpiredWHERE...;ROLLBACK;测试库上的执行计划不作数。数据量和分布跟线上不一样优化器可能选完全不同的执行计划。至少要在数据量接近的环境里看。五、可复制审查 Prompt你是 DBA 视角的 SQL 审查员。数据库MySQL 8.0InnoDB。 表结构含索引 粘贴 SHOW CREATE TABLE 的输出 数据量级orders 约 3000 万行order_items 约 1 亿行users 约 500 万行 使用场景在线接口峰值 QPS 约 200 / 后台报表每天一次 / 一次性修数 待审查 SQL 粘贴 请检查 1. 结果正确性NULL 语义、JOIN 是否会放大行数导致重复计算、边界条件 2. 执行代价预计使用哪个索引可能出现全表扫描、filesort、临时表的地方索引失效的写法 3. 如果是 UPDATE / DELETEWHERE 范围是否可能过大是否需要分批 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_timeout5;翻车 2测试库上飞快的报表一条报表 SQL测试库几百行数据毫秒级返回。上线后第一次跑把从库 CPU 打满了。EXPLAIN 一看type是ALL。WHERE 里写的是DATE(created_at) BETWEEN ...created_at上的索引完全没用上。测试库数据太少全表扫描也就一眨眼根本看不出来。改成范围条件之后走上了索引。教训有两条审 SQL 时把线上数据量告诉 AIEXPLAIN 要在数据量接近的环境里看测试库上的「很快」说明不了任何问题。七、什么时候不用这么较真一次性的只读查询。在从库或本地跑查完就扔结果对就行。小表。几千行的配置表、字典表全表扫描也无所谓。已经有 SQL 审核平台和慢查询告警的团队。机器能拦的交给机器人重点看结果正确性和 DDL 的执行时机。小结AI 写 SQL 的问题不在语法在于它缺三样信息表结构、数据量、执行那一刻的数据库状态。前两样可以喂给它第三样只能你自己在执行前确认。固定动作先给表结构和数据量级再让它写修数先 COUNT大表分批逐项查 NULL、JOIN 放大、索引失效、分页DDL 显式写 ALGORITHM / LOCK执行前先查长事务执行计划以 EXPLAIN 为准不以 AI 的判断为准上面的检查项我是在 wescode 里写 SQL 时用的官网是 weisyn.com。你们上线 SQL 有没有强制的审核流程或者踩过什么 AI 写 SQL 的坑欢迎评论区聊聊。觉得有用的朋友欢迎点赞、收藏、关注后面会继续分享 AI 编程的实战经验。
返回列表