动态sql中or #{xxx} is null使索引失效,因破坏sargable特性致优化器弃用索引;应改用动态拼接纯净条件,并配合合理索引设计与查询拆分。

动态 SQL 中的 OR #{xxx} IS NULL 为什么让索引失效
不是“一定不走索引”,而是 MySQL 优化器在多数业务场景下会放弃使用索引,转为全表扫描。根本原因是:这类写法破坏了索引的 SARGable(Search ARGument Able)特性——即无法直接拿索引字段和常量做等值/范围比对。
常见错误现象:EXPLAIN 显示 type: ALL、key: NULL,哪怕 user_id 上有索引也压根不用。
- MySQL 5.7+ 虽支持
index_merge,但仅当每个OR分支都对应独立索引且选择性高时才可能启用;实际中三个条件混合OR+IS NULL,基本不会触发 -
#{startTime} IS NULL这类判断会让优化器认为该条件“不可预估”,进而放弃整个 WHERE 子句的索引路径 - 即使所有字段都有单列索引,联合筛选效果也远不如一个设计合理的联合索引
MyBatis 动态 SQL 的安全写法:用 <if></if> 替代 OR IS NULL
真正可控、可预测、能命中索引的方式,是让 SQL 拼出来就是“干净”的条件——没有冗余分支,没有运行时不可推断的逻辑。
使用场景:后台管理页的多条件模糊查询、导出接口、运营后台筛选。
<where><if test="userId != null and userId != ''">
AND user_id = #{userId}
</if><if test="status != null">
AND status = #{status}
</if><if test="startTime != null">
AND create_time >= #{startTime}
</if></where>
- 生成的 SQL 是纯正的
WHERE user_id = ? AND create_time >= ?,优化器能精准选择索引 - 注意判空逻辑要严格:
test="userId != null and userId != ''"比单纯!= null更防误触(尤其字符串类型) - 避免在
<if></if>里写函数调用,比如test="startTime != null and DATE(startTime) == '2024-01-01'"—— 这会让 MyBatis 拼出函数表达式,导致索引失效
预处理语句(PREPARE / EXECUTE)本身不解决索引问题
有人以为“用了预编译就自动优化”,这是误解。预处理只保证参数安全、减少解析开销,但执行计划仍由最终拼出的 SQL 决定。
性能影响:预处理语句在首次执行时生成执行计划并缓存;但如果每次传入的参数组合差异极大(比如有时查 1 条,有时查 80% 的数据),MySQL 可能复用次优计划,反而更慢。
- 不要指望
PREPARE stmt FROM ...; EXECUTE stmt USING ...能绕过OR IS NULL带来的索引失效 - 如果必须用动态拼接(如存储过程内构建 SQL),优先用
CONCAT+ 条件判断拼出完整 WHERE,而不是塞一堆OR - 高并发下大量不同结构的预处理语句,还可能撑爆
performance_schema.prepared_statements_instances表,引发内存压力
真要兼容空参?试试应用层控制 + 多个固定 SQL
比“一个 SQL 扛所有”更高效的做法,是把常见查询模式拆成几条明确的 SQL,由代码逻辑路由。
使用场景:高频查询路径明确(如「只按用户查」「只按状态+时间查」),且参数组合不超过 5–6 种。
- 定义三个 Mapper 方法:
selectByUser、selectByStatusAndTime、selectByAll,各自对应最优索引 - 在 Service 层判断参数组合,调用对应方法,而非塞进一个万能
selectDynamic - 这样每条 SQL 都能走
ref或range类型,EXPLAIN结果稳定,DBA 也容易针对性优化索引
最容易被忽略的一点:索引设计得再好,也救不了被 SELECT * 和 LIKE '%xxx' 拖垮的查询。动态 SQL 的边界,不在怎么拼,而在哪些字段真该进 WHERE、哪些该进应用层过滤。











