AI帮我写了MyBatis动态SQL,查询性能差点拖垮整个系统
上个月订单查询模块重构,我把需求丢给AI,让它帮我用MyBatis动态SQL实现多条件组合查询。AI给出的代码看着挺优雅,条件拼接干净利落,测试环境跑起来也没问题。上线第二天,运维告警群里炸了——订单查询接口P99从200ms飙到3秒,数据库CPU占用率直接打满。
需求背景
我们的订单系统支持多维度查询:按订单号、用户ID、下单时间范围、订单状态、支付方式等组合筛选。原来的查询接口是硬编码的if-else分支,十几个条件组合下来代码已经没法维护了。我想用MyBatis的<where>标签配合<if>条件做动态拼接,让代码更整洁。
AI给出的方案
我把需求描述给AI,它直接生成了完整的Mapper XML:
xml
<select id="queryOrders" resultMap="orderResultMap">
SELECT o.order_id, o.user_id, o.status, o.create_time,
o.pay_method, o.amount, o.update_time
FROM t_order o
<where>
<if test="orderId != null">
AND o.order_id = #{orderId}
</if>
<if test="userId != null">
AND o.user_id = #{userId}
</if>
<if test="status != null">
AND o.status = #{status}
</if>
<if test="payMethod != null">
AND o.pay_method = #{payMethod}
</if>
<if test="startTime != null and endTime != null">
AND o.create_time BETWEEN #{startTime} AND #{endTime}
</if>
</where>
ORDER BY o.create_time DESC
</select>
乍一看没毛病。条件可选、<where>自动处理AND拼接、代码简洁。我甚至觉得比原来那一坨if-else好太多了,直接提交上线。
线上爆炸的根本原因
上线当天晚上流量高峰,DBA紧急拉我进了排查会议室。慢查询日志里全是同一条SQL:
sql
SELECT … FROM t_order ORDER BY o.create_time DESC;
没有任何WHERE条件。因为用户在查询页面没填任何筛选条件直接点了"搜索",所有<if>判断都为null,整条SQL退化成了全表扫描+排序。
t_order有800万条数据,全表扫描加上ORDER BY create_time DESC,MySQL只能做filesort。没有WHERE条件时索引根本用不上,800万行排序直接把临时表撑到了磁盘。
这问题在测试环境根本不会暴露——测试数据就几千条,全表扫描毫秒级完成。AI也不会提醒你:动态SQL最大的风险不是语法错误,而是条件全空时查询退化。
我的修复过程
第一步,给查询加上必填条件兜底。业务上,订单查询至少要有用户ID或时间范围约束:
xml
<select id="queryOrders" resultMap="orderResultMap">
SELECT o.order_id, o.user_id, o.status, o.create_time,
o.pay_method, o.amount, o.update_time
FROM t_order o
<where>
<choose>
<when test="orderId != null">
o.order_id = #{orderId}
</when>
<otherwise>
<!– 必须有userId或时间范围,否则拒绝查询 –>
<if test="userId == null and (startTime == null or endTime == null)">
AND 1=0
</if>
<if test="userId != null">
AND o.user_id = #{userId}
</if>
<if test="startTime != null and endTime != null">
AND o.create_time BETWEEN #{startTime} AND #{endTime}
</if>
</otherwise>
</choose>
<if test="status != null">
AND o.status = #{status}
</if>
<if test="payMethod != null">
AND o.pay_method = #{payMethod}
</if>
</where>
ORDER BY o.create_time DESC
</select>
<choose>确保了查询要么走订单号精准匹配,要么必须有userId或时间范围。当所有必要条件都为空时返回空结果(1=0),而不是全表扫描。
第二步,加查询限制。在Service层对时间范围做了硬约束——最多查30天内的订单,防止有人选一整年时间范围:
java
public PageResult<OrderDTO> queryOrders(OrderQueryParam param) {
// 时间范围不能超过30天
if (param.getStartTime() != null && param.getEndTime() != null) {
long days = (param.getEndTime().getTime() – param.getStartTime().getTime())
/ (1000 * 60 * 60 * 24);
if (days > 30) {
throw new BizException("查询时间范围不能超过30天");
}
}
// 无关键条件时直接返回空
if (param.getOrderId() == null && param.getUserId() == null
&& param.getStartTime() == null) {
return PageResult.empty();
}
return orderMapper.queryOrders(param);
}
第三步,补索引。DBA建议在(user_id, create_time)上建联合索引,让带userId条件的查询能走索引覆盖排序:
sql
ALTER TABLE t_order ADD INDEX idx_user_create (user_id, create_time);
加了联合索引后,带userId的查询直接走索引范围扫描+有序返回,filesort消失了。P99从3秒降回了180ms。
AI没告诉你的那些事
这次踩坑让我意识到,AI生成的代码"语法正确"和"生产可用"之间隔着好几个维度:
空条件退化是动态SQL最隐蔽的杀手。AI写XML时根本不会考虑业务约束——它只关注语法层面条件拼接是否正确。你得自己补上"条件全空时怎么办"的业务逻辑。
测试数据量不足让性能问题在开发阶段完全隐形。几千条测试数据下,全表扫描毫无感知。真正的问题只有在上百万数据的线上环境才会暴露。我的教训是:涉及查询的代码,必须用大数据量做压测,而不是只跑功能测试。
ORDER BY + 无WHERE是MySQL性能噩梦。没有过滤条件时排序操作无法利用索引,只能做全表filesort。动态SQL搭配排序时,一定要确保至少有一个能走索引的WHERE条件。
索引设计要跟查询模式匹配。原来t_order只有order_id主键索引,AI不会帮你分析查询路径并建议联合索引。索引优化得靠你自己根据实际查询模式来补。
后续改进
修复上线后,我又做了几件事防止同类问题复发:
动态SQL不是不好用,是太容易写出"看起来没问题但线上会爆炸"的代码。AI能帮你把语法写对,但查询安全性和性能防护这层,还得自己把关。下次用AI写查询代码,我一定会多问一句:条件全空时,这条SQL到底会变成什么?


