很多人把“存储过程不走索引”归因到过程写法本身,实际大多数问题都出在过程里的某条 SELECT、UPDATE 或 DELETE 语句。要把问题查准,重点不是猜测,而是把真实参数、真实会话环境和执行计划对上,再逐项验证索引为什么没有被优化器选中。看完这篇,你可以快速判断问题究竟出在 SQL 写法、参数类型、索引设计,还是统计信息与动态执行路径上。
先确认是不是真的没走索引
存储过程本身并不能决定是否使用索引,真正起作用的是其内部 SQL 执行时,优化器对基表访问方式的选择。所谓“不走索引”,多数情况下其实是过程中的某条语句没有触发索引查找。
第一步不要靠经验判断,直接把最可疑的 SQL 单独拎出来跑 EXPLAIN。但这个动作要成立,至少满足两个前提:
- 传入参数必须和调用存储过程时完全一致。例如原调用传的是
'2026-07-01',测试时就不要换成NOW()。 - 会话环境必须一致,包括
SQL_MODE、字符集、collation_connection等设置,否则执行计划可能变化。
看执行计划时,优先关注以下几项:
type=ALL或type=index:通常说明在做全表扫描或全索引扫描,不属于高效利用索引。key=NULL:说明优化器没有选中任何索引。Extra中出现Using filesort、Using temporary:往往意味着WHERE或ORDER BY没有借上索引能力。
如果这里已经看到 type=ALL、key=NULL,基本就能确认问题不在“是不是存储过程”,而在过程内部这条 SQL 的条件、排序或参数匹配方式上。
参数类型不一致,最容易让索引直接失效
这是存储过程里最常见、也最容易被忽略的问题。字段类型和过程参数类型不一致时,数据库常常会发生隐式转换,而隐式转换一旦落在索引列比较上,索引命中率就会明显下降,甚至直接失效。
例如表字段是 BIGINT,过程参数却定义成 IN p_id VARCHAR(32),然后条件写成:
WHERE user_id = p_id
这类写法在 MySQL 中很可能被处理成类似 CAST(p_id AS SIGNED) 的比较过程。表面看条件成立,实际上优化器未必还能按理想方式使用索引。
可以这样验证:
- 先在过程里增加一条
SELECT @p_id := p_id; - 然后执行
EXPLAIN SELECT * FROM t WHERE id = @p_id; - 再对比直接使用
p_id时的执行计划差异
排查到这里,处理方式通常有两个:
- 最直接的办法,是把参数类型改成和字段完全一致。
- 如果暂时不能改签名,就显式转换,例如
WHERE user_id = CONVERT(p_id, UNSIGNED)。
这个问题并不只出现在 MySQL。Oracle、SQL Server 也有同类现象,例如 NUMBER 参数传入字符串、VARCHAR2 参数去查 CHAR 字段,都可能触发隐式转换,最终影响索引选择。
复合索引没按最左前缀使用,等于白建
另一个高频误区,是索引虽然建了,但 SQL 的过滤方式根本没有命中它的有效入口。最典型的就是复合索引顺序错位,或者只使用右侧列。

比如你创建了下面这个索引:
INDEX idx_status_time (status, create_time)
但在存储过程中却只写:
WHERE create_time > '2026-08-01'
这时该索引基本不会被利用,因为没有满足复合索引的最左前缀原则。
判断这类问题时,可以抓住三个要点:
- 要么使用
WHERE status =,要么使用WHERE status = ? AND create_time >,这样才符合(status, create_time)的使用方式。 - 如果只按
create_time查询,而不带status,那么这个复合索引对当前语句基本没有帮助。 - 同一个存储过程如果包含多种查询模式,例如有时按
status查,有时按user_id查,就不要指望一个索引覆盖所有分支。
更实际的做法是:优先覆盖高频、选择性高的查询组合;对低频分支,考虑拆分语句,或在支持的数据库中使用类似 OPTION (RECOMPILE) 的方式,让优化器按分支重算执行计划。
如果你使用的是 MySQL 8.0+,还要注意函数索引的问题。数据库虽然支持函数索引,但前提是查询表达式必须与索引定义完全一致。例如索引定义为:
INDEX idx_upper_name ((UPPER(name)))
那么查询条件也必须写成:
UPPER(name) = 'ABC'
只要表达式不一致,优化器就可能放弃这条索引。
动态 SQL 场景下,纸面执行计划可能不可信
当存储过程中存在动态 SQL 时,排查难度会明显上升。因为你看到的 EXPLAIN 或 EXPLAIN PLAN FOR,很多时候只是预估计划,并没有真正代入运行时参数,结果和真实执行路径可能并不一致。

常见场景包括:
- 使用
CONCAT拼接 SQL 再执行 - 使用
EXECUTE IMMEDIATE - 在 PL/SQL 中用
EXECUTE IMMEDIATE ... USING绑定变量
这类情况下,更可靠的办法是直接抓真实执行信息,而不是只看解释计划:
- Oracle:开启 SQL Trace,例如
DBMS_MONITOR.SESSION_TRACE_ENABLE,抓取实际执行计划。 - SQL Server:查询
sys.dm_exec_query_stats并关联sys.dm_exec_sql_text,重点找execution_count高但total_logical_reads异常大的语句。 - MySQL:开启
slow_query_log,并把long_query_time=0,配合pt-query-digest把真实慢语句抓出来,再逐条EXPLAIN。
换句话说,动态 SQL 不是不能分析,而是分析入口变了。你需要先拿到“真正执行过的 SQL 原文和参数”,再回头判断索引为何没有被选中。
别忽略统计信息:有索引也可能被优化器误判
还有一种情况特别容易被误判为“索引失效”:索引明明存在,SQL 写法看上去也没问题,但优化器依旧没有选它。此时不一定是语句错了,也可能是统计信息已经陈旧。

一个简单信号是执行:
SHOW INDEX FROM table_name
如果看到 Cardinality 明显低于实际数据规模,说明优化器对数据分布的判断已经失真。它在估算扫描代价时会出现偏差,进而错误地放弃本该使用的索引。
这种情况下,盲目改 SQL 往往收益不大,先更新统计信息更有效。例如在 MySQL 中可以直接执行:
ANALYZE TABLE table_name;
很多时候,优化器重新拿到较准确的基数信息后,执行计划就会回到更合理的路径。
因此,排查“存储过程不走索引”时,建议按这个顺序推进:先确认执行计划,再检查参数类型和索引命中条件,随后处理动态 SQL 的真实语句抓取,最后再看统计信息是否陈旧。这样比一上来重写整个存储过程,更容易定位根因。







