我用AI生成的SQL,差点在生产库上跑了一整夜
我用AI生成的SQL,差点在生产库上跑了一整夜
目录
- 凌晨的告警短信
- 那段"看起来没问题"的SQL
- EXPLAIN输出告诉我真相
- 为什么AI不关心你的索引
- 给AI加规则后重新生成
- 我现在怎么用AI写SQL
凌晨的告警短信
那天凌晨两点,手机连续收到四条阿里云RDS告警:"CPU使用率超过90%","活跃连接数超过200","慢查询阈值触发"。我打开DMS连上去一看,show processlist里躺着一条SQL,Time列显示已经跑了23分钟,state是Sending data。
这条SQL是我下午让AI生成的。
业务场景不复杂:运营要一个数据报表,统计最近30天各商品类目的订单转化率。我懒得手写这条跨五张表的统计查询,就把需求描述贴给了AI:"写一条MySQL查询,统计近30天每个商品类目的订单转化率,需要关联用户表、商品表、类目表、订单表和订单明细表,按转化率降序排列。"
AI很快给出了结果,我在测试环境跑了一下,数据量小的时候返回很快,看起来没问题。代码评审的时候也没人觉得有毛病——五表JOIN在报表场景太常见了。
那段"看起来没问题"的SQL
AI产出的SQL大概是这个结构(脱敏后):
sql SELECT c.category_name, c.category_id, COUNT(DISTINCT o.order_id) AS total_orders, COUNT(DISTINCT oi.order_id) AS converted_orders, ROUND(COUNT(DISTINCT oi.order_id) * 100.0 / COUNT(DISTINCT o.order_id), 2) AS conversion_rate FROM orders o LEFT JOIN users u ON o.user_id = u.user_id LEFT JOIN order_items oi ON o.order_id = oi.order_id LEFT JOIN products p ON oi.product_id = p.product_id LEFT JOIN categories c ON p.category_id = c.category_id WHERE o.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND o.order_status IN (1, 2, 3, 5) AND u.user_type = 'NORMAL' GROUP BY c.category_id, c.category_name ORDER BY conversion_rate DESC;
语法没问题,格式也工整。问题出在它"不知道"的东西上。
EXPLAIN输出告诉我真相
我在生产只读库上跑了EXPLAIN,结果让人头皮发麻:
| table | type | key | rows | Extra | |-------|------|-----|------|-------| | orders | range | idx_create_time | 860000 | Using where; Using temporary; Using filesort | | users | eq_ref | PRIMARY | 1 | Using where | | order_items | ALL | NULL | 1200000 | Using where; Using join buffer | | products | eq_ref | PRIMARY | 1 | NULL | | categories | eq_ref | PRIMARY | 1 | NULL |
order_items表走了全表扫描,rows估算120万行。生产库里order_items表有接近两千万行数据。这个查询在生产上跑完估计要超过40分钟。
问题出在哪里?AI不知道这张表的实际数据分布。它不可能知道——
order_items表缺少必要的索引。order_id字段虽然在order_items表里,但这个字段没有单独的索引,它只是联合主键(order_id, item_id)的一部分。MySQL优化器在某些情况下用不到联合主键的最左前缀。
隐式类型转换。oi.product_id = p.product_id看起来正常,但order_items.product_id是varchar(32),products.product_id是bigint(20)。这种类型不一致在数据量小的时候无所谓,数据量大时索引直接失效。
N+1式统计。COUNT(DISTINCT oi.order_id)在大数据量下的去重计算代价极高。AI可以写出正确的SQL语法,但它不具备"这条SQL在生产库上跑会不会出问题"的判断力。
为什么AI不关心你的索引
这里有个根本性的问题要说清楚:AI生成SQL时看到的是"表结构"和"需求描述",它看不到——
生产中每张表的实际数据量和增长率。一张表有1万行和有2000万行,最优查询策略完全不同。
索引的实际选择性与碎片率。建了索引和索引真的被用到是两码事。order_items表上明明有联合主键,但实际执行计划里MySQL就是选了全表扫描。
MySQL优化器的版本差异。同样的SQL在MySQL 5.7和8.0上执行计划可能完全不同。AI默认参考的是通用文档,不会针对你的具体版本。
给AI加规则后重新生成
发现问题后我没有自己改SQL,而是重新问AI,这次加了规则:
"重写这条SQL,注意:order_items表有2000万行,product_id是varchar类型不是数值类型,避免DISTINCT在2000万行上的性能问题,先聚合再JOIN。"
这次AI生成的SQL好得多:
sql SELECT c.category_name, c.category_id, stats.total_orders, stats.converted_orders, ROUND(stats.converted_orders * 100.0 / stats.total_orders, 2) AS conversion_rate FROM ( SELECT p.category_id, COUNT(DISTINCT o.order_id) AS total_orders, COUNT(DISTINCT CASE WHEN oi.order_id IS NOT NULL THEN o.order_id END) AS converted_orders FROM orders o JOIN users u ON o.user_id = u.user_id AND u.user_type = 'NORMAL' LEFT JOIN order_items oi FORCE INDEX(idx_order_id) ON o.order_id = oi.order_id LEFT JOIN products p ON oi.product_id = p.product_id WHERE o.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND o.order_status IN (1, 2, 3, 5) GROUP BY p.category_id ) stats JOIN categories c ON stats.category_id = c.category_id ORDER BY conversion_rate DESC;
改进点很实在:
用子查询先聚合再JOIN,减少了categories表参与中间结果集。
加了FORCE INDEX(idx_order_id)提示,因为我知道order_items表上有个单独的idx_order_id索引(AI不知道,但我说了它就会用)。
把类型不一致的问题明确告诉AI后,它不会再犯同样的错。
我现在怎么用AI写SQL
这件事之后,我写SQL的流程变成了这样:
先把需求按"表名 + 实际行数 + 索引列表 + 字段类型"的格式丢给AI。比如:
orders: 860万行, 索引idx_create_time(create_time), idx_user_status(user_id, order_status) order_items: 2000万行, 索引idx_order_id(order_id), 联合主键(order_id, item_id) product_id在order_items中是varchar(32),在products中是bigint(20) 需求:统计近30天各商品类目的订单转化率
然后等AI出SQL,我自己跑EXPLAIN看执行计划。如果type列出现ALL,就要追问AI:"这个表2000万行你让它全表扫?换个写法。"
几轮下来,AI产出的SQL质量明显提高。因为它从我的反馈里学到了"大表不能用ALL扫描"、"字段类型不同会导致索引失效"这些约束。
这不是AI变聪明了,是我给的上下文变丰富了。
我后来在团队里做了个分享,标题就叫"AI不知道你的表有多大"。分享完后一个同事说:"这不就是欺负AI没见过生产数据嘛。"我说对,所以你得替它见。
写SQL这件事上,AI能帮你省掉80%的敲键盘时间,但剩下的20%——理解数据规模、看懂执行计划、避开隐式类型转换——还得你自己来。这20%才是区分"能用"和"出事故"的关键。
