一条订单查询从20毫秒涨到1.4秒,46万行的表,order_no上索引建着。EXPLAIN打出来,type是ALL,key那列空的。索引在,优化器不用它。这条SQL上线一年多没动过。最近两周才慢下来,数据量也没暴涨。
索引树按列里存的原值排好序,每一层记的是一段范围。WHERE里写成DATE(created_at) = '2026-10-01',比较的对象变成函数返回值,树上的顺序跟这个值没有对应关系,只能从头扫一遍。把函数挪到等号右侧,改成created_at >= '2026-10-01' AND created_at <= '2026-10-01 23:59:59',列保持原样,索引就能用。SUBSTRING、LEFT、YEAR、MONTH、UNIX_TIMESTAMP包在列外面,性质相同。范围条件可以顺着叶子节点往下走,等值条件能直接定位到那一段。函数把这两条路同时堵住,索引根本用不了。优化器只剩下全表扫描一条路,跟代价高低无关。
另一种是隐式类型转换,两边的类型对不上。order_no在表里是varchar,查询写成WHERE order_no = 20261001001,数字没加引号,MySQL按数值比较,会把列里每个值转成double再比。转换发生在列这一侧,索引直接作废。反过来,int列写成WHERE id = '123',转换落在常量那侧,索引照用。这一条不好记,判断办法是看函数或转换作用在哪一边,只要作用在列上,索引就没戏。手机号、订单号、邮编这类看着像数字的字段,最容易踩。
EXPLAIN里key是NULL、type是ALL、rows接近表的总行数,三个一起出现基本可以定案。possible_keys有值而key为空,说明索引被评估过但没被选中,多半是条件写法的问题。想看得更细,加FORMAT=JSON看代价估算。也可以跑完SHOW WARNINGS,优化器改写后的语句会打出来,函数有没有被搬到常量侧一目了然。
改法按条件类型分。函数包列,就把函数拆到常量那边,日期区间、年份区间都这么写。前导模糊LIKE '%关键字'没有可用前缀,换成全文检索(中文要装ngram分词器),或者把关键词单独建一张检索表。OR两边只有一边有索引,整条语句会退化成全表扫,拆成两条分别查再UNION。联合索引(a,b,c)只按b和c过滤用不上,这是最左前缀的规则,跟写法无关,得单独给b建一条。
MySQL 8.0.13起可以建函数索引,CREATE INDEX idx_d ON orders ((DATE(created_at))),表达式外面套两层括号。建完查询里也得写同样的表达式,改成范围条件反而用不上它。函数索引占空间,写入时还要维护。只在SQL确实改不动的时候用。
改完再跑EXPLAIN,key有值,rows从46万降到三位数,耗时回到20毫秒。上线后把慢查询日志的long_query_time调回1秒。观察2天,同类语句不再冒出来就算收住。你手上要是还有按手机号查、按日期统计的接口,照这个顺序过一遍,比等用户报慢要早。
A5创业网 版权所有