MySQL索引失效的15个场景:EXPLAIN告诉你答案
MySQL索引失效的15个场景:EXPLAIN告诉你答案
适读人群:Java后端开发、数据库优化工程师、有一定MySQL使用经验的开发者 | 阅读时长:约28分钟
开篇故事
2022年618大促前两周,我们做了一次全面的慢查询治理。
DBA小王找我:「老张,这条SQL昨天跑了47秒,但我明明看到那个字段上有索引,你帮我看看是怎么回事。」
SELECT * FROM order_detail
WHERE DATE(created_at) = '2022-06-01'
AND status = 1;created_at字段确实有索引,status字段也有索引,但EXPLAIN一看:
+----+-------------+--------------+------------+------+...+------+----------+---------+-------+------------+
| id | select_type | table | partitions | type |...| key | key_len | ref | rows | Extra |
+----+-------------+--------------+------------+------+...+------+----------+---------+-------+------------+
| 1 | SIMPLE | order_detail | NULL | ALL |...| NULL | NULL | NULL |12883921| NULL |
+----+-------------+--------------+------------+------+...+------+----------+---------+-------+------------+type=ALL,全表扫描,1288万行。原因是DATE(created_at)对字段做了函数运算,索引直接失效了。
这种问题太典型了。工作15年,我见过的索引失效场景不下几十个,今天把最高频的15个整理出来,每个都有EXPLAIN验证,帮你把这些坑一次踩完。
一、为什么索引会失效:先搞清楚根因
MySQL使用索引的前提是:查询条件能在索引的B+树结构中定位到对应的叶节点。
只要破坏了这个前提,索引就会失效。失效的本质原因只有几类:
- 对索引字段做了计算或函数运算:B+树按原始值构建,计算后的值无法在树上定位
- 隐式类型转换:查询值类型与字段类型不匹配,MySQL内部进行了隐式转换
- 违反最左前缀原则:联合索引的B+树按左到右排序,跳过左边的字段就无法利用索引
- 范围查询后的字段:范围查询右边的字段索引失效(联合索引场景)
- 优化器放弃索引:数据量/选择性等因素导致全表扫描比索引扫描更快
二、底层原理:B+树结构决定了使用规则
在进入15个场景之前,先把B+树结构图印在脑子里:
B+树索引结构(以 idx_name_age_status 为例)
[根节点]
('Alice',25,'ACTIVE')
/ \
[内部节点] [内部节点]
('Alice',18,'ACTIVE') ('Bob',30,'ACTIVE')
/ \ / \
[叶节点] [叶节点] [叶节点] [叶节点]
行指针... 行指针... 行指针... 行指针...
↕ 双向链表连接各叶节点 ↕叶节点中,数据按 (name, age, status) 的组合值排序。
- 如果你查
WHERE name = 'Alice',可以在B+树上直接定位,索引生效 - 如果你查
WHERE age = 25(跳过name),B+树上没有单独按age排序的结构,索引失效 - 如果你查
WHERE LOWER(name) = 'alice',计算后的值不在B+树上,索引失效
三、完整解决方案与代码
下面逐一分析15个失效场景,每个都附带EXPLAIN输出和修复方案。
测试表结构:
CREATE TABLE user_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
order_no VARCHAR(32) NOT NULL,
status TINYINT NOT NULL DEFAULT 0, -- 0未支付 1已支付 2已取消
amount DECIMAL(10,2) NOT NULL,
remark VARCHAR(200),
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
INDEX idx_user_id (user_id),
INDEX idx_order_no (order_no),
INDEX idx_created_at (created_at),
INDEX idx_user_status (user_id, status, created_at) -- 联合索引
) ENGINE=InnoDB;
-- 插入1000万条测试数据(用存储过程生成)
DELIMITER $$
CREATE PROCEDURE gen_data()
BEGIN
DECLARE i INT DEFAULT 0;
WHILE i < 10000000 DO
INSERT INTO user_order(user_id, order_no, status, amount, remark, created_at, updated_at)
VALUES (
FLOOR(RAND() * 100000),
CONCAT('NO', LPAD(i, 10, '0')),
FLOOR(RAND() * 3),
ROUND(RAND() * 10000, 2),
'test remark',
DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY),
NOW()
);
SET i = i + 1;
IF i % 100000 = 0 THEN COMMIT; END IF;
END WHILE;
END$$
DELIMITER ;
CALL gen_data();场景1:对索引字段使用函数
-- 失效写法(在created_at上有索引,但使用了DATE()函数)
EXPLAIN SELECT * FROM user_order WHERE DATE(created_at) = '2022-06-01';EXPLAIN输出:
+----+-------------+------------+------+---------------+------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+---------------+------+---------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | NULL | NULL | NULL | NULL | 9876543 | Using where |
+----+-------------+------------+------+---------------+------+---------+------+----------+-------------+
-- type=ALL,全表扫描近千万行,耗时约 8.4秒正确写法:
-- 将函数运算移到值上,让字段保持原始形式
EXPLAIN SELECT * FROM user_order
WHERE created_at >= '2022-06-01 00:00:00'
AND created_at < '2022-06-02 00:00:00';EXPLAIN输出:
+----+-------------+------------+-------+---------------+---------------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+---------------+---------------+---------+------+--------+-------------+
| 1 | SIMPLE | user_order | range | idx_created_at| idx_created_at| 5 | NULL | 27321 | Using where |
+----+-------------+------------+-------+---------------+---------------+---------+------+--------+-------------+
-- type=range,扫描约2.7万行,耗时约 0.02秒场景2:隐式类型转换(字符串字段用数字查询)
-- order_no是VARCHAR类型,用整数查询
EXPLAIN SELECT * FROM user_order WHERE order_no = 1234567890;
-- ^^^^^^^^^^
-- 整数,不是字符串!EXPLAIN输出:
+----+-------------+------------+------+---------------+------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+---------------+------+---------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | NULL | NULL | NULL | NULL | 9876543 | Using where |
+----+-------------+------------+------+---------------+------+---------+------+----------+-------------+
-- 全表扫描!MySQL将字段转换为数字再比较,等价于 CAST(order_no AS SIGNED) = 1234567890正确写法:
EXPLAIN SELECT * FROM user_order WHERE order_no = '1234567890';
-- ^^^^^^^^^^^
-- 加引号!+----+-------------+------------+------+---------------+--------------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+---------------+--------------+---------+-------+------+-------+
| 1 | SIMPLE | user_order | ref | idx_order_no | idx_order_no | 99 | const | 1 | NULL |
+----+-------------+------------+------+---------------+--------------+---------+-------+------+-------+
-- type=ref,精确查找,1行Java代码中的对应问题:
// 错误:传入Long类型,JDBC可能触发隐式转换
String sql = "SELECT * FROM user_order WHERE order_no = ?";
preparedStatement.setLong(1, 1234567890L); // 应该用setString
// 正确
preparedStatement.setString(1, "1234567890");
// MyBatis中的错误写法(mapper.xml)
// <select>SELECT * FROM user_order WHERE order_no = #{orderNo, jdbcType=BIGINT}</select>
// 正确写法
// <select>SELECT * FROM user_order WHERE order_no = #{orderNo, jdbcType=VARCHAR}</select>场景3:违反最左前缀原则(跳过联合索引左侧字段)
-- 联合索引 idx_user_status (user_id, status, created_at)
-- 跳过user_id,直接用status查询
EXPLAIN SELECT * FROM user_order WHERE status = 1;EXPLAIN输出:
+----+-------------+------------+------+-----------------+------+---------+------+----------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+-----------------+------+---------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | NULL | NULL | NULL | NULL | 9876543 | Using where |
+----+-------------+------------+------+-----------------+------+---------+------+----------+-------------+
-- 索引完全不用正确写法(利用最左前缀):
-- 从user_id开始,可以命中索引
EXPLAIN SELECT * FROM user_order WHERE user_id = 12345 AND status = 1;+----+-------------+------------+------+-----------------+-----------------+---------+-------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------+-----------------+-----------------+---------+-------------+------+-------------+
| 1 | SIMPLE | user_order | ref | idx_user_status | idx_user_status | 9 | const,const | 12 | Using index |
+----+-------------+------------+------+-----------------+-----------------+---------+-------------+------+-------------+
-- type=ref,Using index(覆盖索引),极快场景4:范围查询右边的字段失效
-- 联合索引 idx_user_status (user_id, status, created_at)
-- user_id使用范围查询,status和created_at失效
EXPLAIN SELECT * FROM user_order
WHERE user_id > 50000 AND status = 1 AND created_at > '2022-01-01';EXPLAIN输出:
+----+-------------+------------+-------+-----------------+-----------------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+-----------------+-----------------+---------+------+---------+-------------+
| 1 | SIMPLE | user_order | range | idx_user_status | idx_user_status | 8 | NULL | 4950123 | Using where |
+----+-------------+------------+-------+-----------------+-----------------+---------+------+---------+-------------+
-- key_len=8,只用了user_id(BIGINT=8字节),status和created_at的索引未使用优化思路(调整索引字段顺序):
-- 把等值查询的字段放前面
ALTER TABLE user_order
DROP INDEX idx_user_status,
ADD INDEX idx_user_status_v2 (user_id, status, created_at);
-- 等值字段在前,范围字段在后
EXPLAIN SELECT * FROM user_order
WHERE user_id = 12345 AND status = 1 AND created_at > '2022-01-01';+----+-------------+------------+-------+--------------------+--------------------+---------+------+------+------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+-------+--------------------+--------------------+---------+------+------+------------------+
| 1 | SIMPLE | user_order | range | idx_user_status_v2 | idx_user_status_v2 | 14 | NULL | 8 | Using index cond |
+----+-------------+------------+-------+--------------------+--------------------+---------+------+------+------------------+
-- key_len=14(8+1+5),三个字段都用上了场景5:LIKE以通配符开头
-- 以%开头,无法在B+树上定位起始位置
EXPLAIN SELECT * FROM user_order WHERE order_no LIKE '%NO123%';+----+---...---+------------+------+---------------+------+--------+------+----------+-------------+
| id | ... | table | type | possible_keys | key | ... | ... | rows | Extra |
+----+---...---+------------+------+---------------+------+--------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | NULL | NULL | NULL | NULL | 9876543 | Using where |
+----+---...---+------------+------+---------------+------+--------+------+----------+-------------+优化方案:
-- 方案1:改为前缀匹配(业务允许的情况下)
EXPLAIN SELECT * FROM user_order WHERE order_no LIKE 'NO123%';
-- type=range,索引生效
-- 方案2:使用覆盖索引(只查索引字段,回避回表)
EXPLAIN SELECT id, order_no FROM user_order WHERE order_no LIKE '%NO123%';
-- 如果order_no在覆盖索引中,type=index(全索引扫描比全表扫描快)
-- 方案3:模糊搜索场景迁移到Elasticsearch
-- 数据量大的模糊查询本就不适合MySQL场景6:OR条件中有未索引字段
-- user_id有索引,但remark没有索引
-- OR会导致整体放弃索引
EXPLAIN SELECT * FROM user_order WHERE user_id = 12345 OR remark = 'test';+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| id | ... | table | type | possible_keys | key | ... | ... | rows | Extra |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | idx_user_id | NULL | NULL | NULL | 9876543 | Using where |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+解决方案:
-- 方案1:给remark加索引(如果选择性够高)
ALTER TABLE user_order ADD INDEX idx_remark (remark);
-- 方案2:改写为UNION(两个独立查询各走各的索引)
EXPLAIN
SELECT * FROM user_order WHERE user_id = 12345
UNION
SELECT * FROM user_order WHERE remark = 'test';场景7:NOT IN / NOT EXISTS
-- NOT IN 通常走全表扫描
EXPLAIN SELECT * FROM user_order WHERE status NOT IN (0, 2);+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| id | ... | table | type | possible_keys | key | ... | ... | rows | Extra |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | NULL | NULL | NULL | NULL | 9876543 | Using where |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+解决方案:
-- 改写为 IN(正向查询)
-- status只有0、1、2三种值,NOT IN (0,2) 等价于 status = 1
EXPLAIN SELECT * FROM user_order WHERE status = 1;
-- 如果status单独有索引(选择性够高),可以走索引
-- 但对于只有3个枚举值的status,优化器可能仍然选择全表扫描(覆盖率>30%时)场景8:IS NULL / IS NOT NULL(视情况而定)
-- IS NULL 在MySQL 8.0+通常可以走索引
EXPLAIN SELECT * FROM user_order WHERE remark IS NULL;
-- IS NOT NULL 有时会放弃索引
EXPLAIN SELECT * FROM user_order WHERE remark IS NOT NULL;注意:这取决于NULL值的比例。如果99%的行remark为NULL,IS NOT NULL扫描99%的数据,优化器会选全表扫描;如果NULL值极少,IS NOT NULL可以走索引。
场景9:索引列参与运算(算术运算)
-- 对索引字段做数学运算
EXPLAIN SELECT * FROM user_order WHERE user_id + 1 = 12346;+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| id | ... | table | type | possible_keys | key | ... | ... | rows | Extra |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | NULL | NULL | NULL | NULL | 9876543 | Using where |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+解决方案:
-- 将运算移到右侧,字段保持原始值
EXPLAIN SELECT * FROM user_order WHERE user_id = 12346 - 1;
-- type=ref,走索引场景10:字符集或排序规则不一致(隐式转换)
-- 如果两张表的字符集不同(如一个utf8,一个utf8mb4)
-- JOIN时会发生隐式转换,驱动表的索引失效
-- 查看表的字符集
SHOW CREATE TABLE user_order\G
SHOW CREATE TABLE user_info\G
-- 如果user_order.user_id是utf8mb4,user_info.id是utf8
EXPLAIN SELECT * FROM user_order o
JOIN user_info u ON o.user_id = u.id;
-- o.user_id的索引可能失效,因为需要转换字符集解决方案:
-- 统一字符集(推荐在建表时就统一使用utf8mb4)
ALTER TABLE user_info CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;场景11:负向条件(!=、<>)
EXPLAIN SELECT * FROM user_order WHERE status != 1;+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| id | ... | table | type | possible_keys | key | ... | ... | rows | Extra |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | NULL | NULL | NULL | NULL | 9876543 | Using where |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+对于低基数字段(只有几个枚举值),!=通常会全表扫描。可以改写为:
-- status只有0、1、2,!= 1 等价于 IN (0, 2)
EXPLAIN SELECT * FROM user_order WHERE status IN (0, 2);场景12:ORDER BY使用索引外的字段导致filesort
-- user_id有索引,但排序用了amount(无索引)
EXPLAIN SELECT * FROM user_order
WHERE user_id = 12345
ORDER BY amount DESC
LIMIT 10;+----+---...---+------------+------+---------------+-------------+------+-------+------+-----------------------------+
| id | ... | table | type | possible_keys | key | ... | rows | ... | Extra |
+----+---...---+------------+------+---------------+-------------+------+-------+------+-----------------------------+
| 1 | SIMPLE | user_order | ref | idx_user_id | idx_user_id | ... | 98 | ... | Using filesort |
+----+---...---+------------+------+---------------+-------------+------+-------+------+-----------------------------+
-- Using filesort:结果集需要额外排序,当数据量大时很慢解决方案:
-- 创建覆盖order by字段的索引
ALTER TABLE user_order ADD INDEX idx_user_amount (user_id, amount);
EXPLAIN SELECT * FROM user_order
WHERE user_id = 12345
ORDER BY amount DESC
LIMIT 10;
-- Extra: Using index(利用索引的有序性,无需额外排序)场景13:GROUP BY 导致临时表和filesort
EXPLAIN SELECT status, COUNT(*) FROM user_order
WHERE user_id = 12345
GROUP BY status;+----+---...---+------------+------+-----------------+-----------------+------+------+------+---------------------+
| id | ... | table | type | possible_keys | key | ... | rows | ... | Extra |
+----+---...---+------------+------+-----------------+-----------------+------+------+------+---------------------+
| 1 | SIMPLE | user_order | ref | idx_user_status | idx_user_status | ... | 98 | ... | Using index |
+----+---...---+------------+------+-----------------+-----------------+------+------+------+---------------------+
-- 联合索引 (user_id, status) 覆盖了 WHERE + GROUP BY,直接用索引分组若没有合适索引:
Extra: Using temporary; Using filesort
-- 需要创建临时表,性能差场景14:优化器统计信息过期(索引明明存在但不用)
-- 有时索引存在,但优化器的统计信息不准确,导致错误选择全表扫描
-- 查看表的统计信息
SHOW INDEX FROM user_order;
-- 重新收集统计信息
ANALYZE TABLE user_order;
-- 强制使用索引(临时验证用,不建议长期使用)
EXPLAIN SELECT * FROM user_order
FORCE INDEX (idx_user_id)
WHERE user_id = 12345;场景15:索引合并(Index Merge)时的注意事项
-- MySQL有时会合并多个索引的结果(Index Merge)
EXPLAIN SELECT * FROM user_order
WHERE user_id = 12345 OR order_no = 'NO0000012345';+----+---...---+------------+-------------+---------------------------+---------------------------+------+------+------+-------------------------------------------+
| id | ... | table | type | possible_keys | key | ... | rows | ... | Extra |
+----+---...---+------------+-------------+---------------------------+---------------------------+------+------+------+-------------------------------------------+
| 1 | SIMPLE | user_order | index_merge | idx_user_id,idx_order_no | idx_user_id,idx_order_no | ... | 100 | ... | Using union(idx_user_id,idx_order_no); |
| | | | | | | | | | Using where |
+----+---...---+------------+-------------+---------------------------+---------------------------+------+------+------+-------------------------------------------+
-- type=index_merge,两个索引的结果取并集
-- 这通常不如使用单个高选择性索引高效
-- 最好改写为UNION四、踩坑实录
坑1:以为加了索引就一定会用
-- 创建了status索引
ALTER TABLE user_order ADD INDEX idx_status (status);
-- 但status只有0、1、2三种值,选择性极低(1/3)
-- MySQL优化器认为全表扫描比索引扫描+大量回表更快
EXPLAIN SELECT * FROM user_order WHERE status = 1;报错/现象:
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| id | ... | table | type | possible_keys | key | ... | ... | rows | Extra |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
| 1 | SIMPLE | user_order | ALL | idx_status | NULL | NULL | NULL | 9876543 | Using where |
+----+---...---+------------+------+---------------+------+------+------+----------+-------------+
-- possible_keys显示有索引,但key=NULL,优化器放弃了索引!这种情况不是索引失效,是优化器的正确选择。低基数字段单独加索引没有意义。
解决方案是联合索引:
-- 低基数字段参与联合索引,靠前面的高基数字段定位,status参与过滤
INDEX idx_user_status (user_id, status)
-- 先用user_id(高基数)精准定位,再用status过滤坑2:EXPLAIN显示走索引,但实际执行很慢
这个坑坑了我们组一个同学好几天。
-- EXPLAIN显示走idx_created_at
EXPLAIN SELECT id, user_id, amount FROM user_order
WHERE created_at >= '2022-01-01'
ORDER BY created_at
LIMIT 1000 OFFSET 9990000;+----+---...---+------------+-------+---------------+---------------+------+------+-----------+-------------+
| id | ... | table | type | possible_keys | key | ... | rows | ... | Extra |
+----+---...---+------------+-------+---------------+---------------+------+------+-----------+-------------+
| 1 | SIMPLE | user_order | range | idx_created_at| idx_created_at| ... | 998 | ... | Using where |
+----+---...---+------------+-------+---------------+---------------+------+------+-----------+-------------+EXPLAIN说只扫998行,但实际执行要13秒!
原因:深分页。索引本身没有失效,但需要先在索引上跳过9990000行,再回表取数据。实际IO操作远比EXPLAIN显示的多。
解决方案(延迟关联):
-- 先在索引上翻页(只走索引,不回表),再JOIN回主表取数据
SELECT o.id, o.user_id, o.amount
FROM user_order o
INNER JOIN (
SELECT id FROM user_order
WHERE created_at >= '2022-01-01'
ORDER BY created_at
LIMIT 1000 OFFSET 9990000
) AS t ON o.id = t.id;
-- 子查询在覆盖索引上翻页,代价小得多坑3:JOIN时驱动表选择不当,导致被驱动表索引失效
-- user_info 有100条记录,user_order有1000万条记录
-- 错误:用大表驱动小表
EXPLAIN SELECT o.*, u.name
FROM user_order o
JOIN user_info u ON o.user_id = u.id
WHERE o.created_at > '2022-01-01';+----+---...---+------------+------+---------------+---------------+------+------+-----------+--------------------+
| id | ... | table | type | possible_keys | key | ... | rows | ... | Extra |
+----+---...---+------------+------+---------------+---------------+------+------+-----------+--------------------+
| 1 | SIMPLE | o | range| idx_created_at| idx_created_at| ... | 5M | ... | Using where |
| 1 | SIMPLE | u | ALL | PRIMARY | NULL | ... | 100 | ... | Using join buffer |
+----+---...---+------------+------+---------------+---------------+------+------+-----------+--------------------+
-- user_info全表扫描100次(每次都是全扫),但因为只有100行所以还能接受
-- 真正的问题是:o扫描了500万行,每行都要在u中查找正确做法:小表驱动大表(NLJ算法):
-- 强制让小表做驱动表
EXPLAIN SELECT /*+ JOIN_ORDER(u, o) */ o.*, u.name
FROM user_order o
JOIN user_info u ON o.user_id = u.id
WHERE o.created_at > '2022-01-01'
AND u.id IN (SELECT id FROM user_info WHERE status = 1);
-- 从小表user_info开始,每行对user_order走idx_user_id索引查找
-- 100次索引查找 >> 500万次全表扫描五、总结与延伸
把这15个场景总结成一个索引失效的排查思路图:
EXPLAIN type = ALL 或者实际执行慢?
|
v
1. 看 key 是否为 NULL(索引完全没用)
|
----+----
| |
NULL 非NULL
| |
v v
检查失效原因 看key_len是否足够
- 函数/运算? (联合索引是否全部用上)
- 隐式转换?
- 最左前缀?
- OR未索引字段?
- LIKE %开头?
- NOT IN/<>?
|
v
看Extra字段
- Using filesort → 排序未用索引
- Using temporary → GROUP BY效率低
- Using join buffer → JOIN无索引一句话记忆口诀:
函数运算加隐转,左前原则要记全。 OR要同索引上,LIKE百分打头难。 范围右边皆失效,低基数字段别单建。
