发布时间:2026-09-21 04:31 更新时间:2026-09-21 04:31 阅读量:0
很多站长遇到慢查询时,第一反应是加索引,结果索引加了一堆,SHOW INDEX 看着挺齐全,SQL 却依旧全表扫描。问题往往不在“有没有索引”,而在“索引用没用上”。MySQL 的优化器非常务实:只有当它认为走索引更划算、并且条件写法确实能被索引利用时,才会去用。本文从 EXPLAIN 执行计划讲起,把函数包裹字段、隐式类型转换、最左前缀等常见失效场景拆开看,让你能自己定位而不是猜。
排查索引问题,第一步永远是 EXPLAIN。在 MySQL 8.0 中还可以用 EXPLAIN ANALYZE 看到实际执行耗时,但先掌握基础输出就够用了。下面是一条典型查询的执行计划查看方式。
mysql -uroot -p your_db
EXPLAIN SELECT id, username, created_at
FROM users
WHERE DATE(created_at) = '2026-09-01';
输出里重点盯四列。type 表示访问类型,出现 ALL 基本就是全表扫描,index 是全索引扫描,ref、range、eq_ref 才算比较理想。key 是实际用到的索引,如果为 NULL 说明一个都没用上;possible_keys 是优化器认为“可能可用”的索引,它不为空而 key 为空,通常意味着写法把索引挡掉了。rows 是预估扫描行数,和表总行数接近就说明扫描范围很大。另外 Extra 列里的 Using where、Using filesort、Using temporary 也值得留意。
记住一个判断顺序:先看 key 是否为空,再看 type 是否退化,最后看 rows 是否异常。这样比逐字读整份计划高效得多。
索引本质上是一棵按列原始值排序的 B+ 树。一旦你在字段上套了函数或做运算,MySQL 就无法直接用这棵树定位,只能把每行算一遍再比较。对字段做函数、算术、类型转换、字符集转换,都会让索引失效。上面那条 SQL 里的 DATE(created_at) 就是典型。
-- 失效写法:字段被函数包裹
EXPLAIN SELECT id FROM users WHERE DATE(created_at) = '2026-09-01';
-- 改写为范围条件,索引可用
EXPLAIN SELECT id FROM users
WHERE created_at >= '2026-09-01 00:00:00'
AND created_at < '2026-09-02 00:00:00';
改写后 created_at 上如果有索引,type 通常会变成 range,rows 也会明显下降。同理,WHERE id + 1 = 100 应改成 WHERE id = 99,WHERE LEFT(name,3) = 'abc' 尽量改成 WHERE name LIKE 'abc%'(注意前导百分号同样会让索引失效,只有前缀匹配才可利用)。
另一种隐蔽情况是索引列参与了排序或分组计算,例如 ORDER BY DATE(created_at),同样无法复用索引的有序性,容易触发 Using filesort。遇到这类需求,可以评估是否新增一个冗余的日期列并建索引,用空间换查询效率,具体是否值得以实际数据量和写入压力为准。
这是线上最常见也最容易被忽略的一类。假设 phone 列是 varchar(20),你写 WHERE phone = 13800000000,MySQL 会把字符串列逐行转成数字再比较,索引直接作废。反过来,如果列是 int,你传字符串 '138',MySQL 会把常量转成数字,索引仍然可用。所以规律是:常量向列类型靠拢时索引可用,列向常量类型转换时索引失效。
-- 假设 phone 为 varchar,下面这条会失效
EXPLAIN SELECT id FROM users WHERE phone = 13800000000;
-- 加引号,让常量保持字符串类型
EXPLAIN SELECT id FROM users WHERE phone = '13800000000';
排查时可以结合 SHOW CREATE TABLE users 确认列类型,再对照 SQL 里的字面量是否带引号。字符集不一致也会造成类似效果,比如连接的 character_set_connection 与列字符集不同,比较时发生转换,索引同样可能用不上。可以用 SHOW VARIABLES LIKE 'character_set%' 查看当前设置,表和连接统一用 utf8mb4 能减少这类麻烦。
联合索引 (a, b, c) 只有在条件从最左列开始连续使用时才高效。只查 b 或 c 无法命中;WHERE a = 1 AND c = 3 只能用到 a,c 仍需回表过滤。范围条件也会中断后续列的使用,例如 WHERE a > 1 AND b = 2,b 往往用不上索引排序。
还有一种情况是索引本身没问题,但优化器主动放弃了它:当预估走索引需要回表的数据比例很高时,全表扫描反而更快,此时 key 会显示为 NULL。可以用 FORCE INDEX 做对比验证,但生产环境不建议长期强绑,更稳妥的做法是优化条件、减少回表,或确认统计信息是否过期,必要时执行 ANALYZE TABLE 更新统计。
小结一下排查路径:拿到慢 SQL,先 EXPLAIN 看 key 与 type,再逐条检查是否有函数包裹、隐式转换、最左前缀断裂,最后用改写后的 SQL 再跑一次计划做对比。索引失效大多不是玄学,而是写法与列类型、索引结构不匹配。建议把 slow_query_log 打开,配合 pt-query-digest 之类的工具定期梳理高频慢查询,把优化做成日常动作,而不是等到页面卡了才回头翻日志。具体参数与工具用法以 MySQL 官方文档和你的实际版本为准。
| 📑 | 📅 |
|---|---|
| Docker 容器健康检查实战:HEALTHCHECK 与 unhealthy 自动重启 | 2026-09-21 |
| Nginx location 匹配优先级实战:=、^~、~ 命中顺序验证 | 2026-09-21 |
| Docker容器缺命令:精简镜像补装与临时容器调试 | 2026-09-20 |
| Nginx 上传大文件报 413 与超时:参数与缓冲区排查 | 2026-09-20 |
| 服务器网卡多队列与中断绑定入门:RPS、RSS 与 irqbalance 怎么取舍 | 2026-09-20 |
| systemd-resolved 与 /etc/resolv.conf 冲突排查 | 2026-09-21 |
| Nginx与PHP上传目录权限:www-data、umask与0777的坑 | 2026-09-21 |
| MySQL 授权与远程访问配置实战:user@host 匹配规则、bind-address 与防火墙三层放行检查 | 2026-09-21 |
| Docker容器时间漂移与crond定时任务错乱:TZ与宿主时间源协同 | 2026-09-21 |
| systemd timer 替代 crontab 实战:OnCalendar、随机延迟与失败重试 | 2026-09-22 |