SQL索引失效场景全汇总
SQL索引失效场景汇总

MySQL索引一直是我们日常面试和开发过程中难以避开的一个点。今天主要来给大家总结下索引失效的一些场景。
前置知识:explain命令
1.type结果字段
# type扫描方式由快到慢
system > const > eq_ref > ref > range > index > ALL

解析:
- system: 系统表,少量数据,往往不需要进行磁盘IO
- const: 常量查找,通过主键或唯一索引查找单条记录
- eq_ref: 主键primary key或者非空唯一索引扫描
- ref: 非主键非唯一索引等值扫描
- range: 范围扫描
- index: 全索引扫描
- ALL: 全表扫描
2. extra结果字段
Extra 字段提供了 MySQL 执行查询时的额外信息,用于描述具体的执行策略和优化情况。

-
性能较好的情况:
Using index: 使用覆盖索引,无需回表,性能最佳
Using index condition: 使用索引条件下推(ICP)优化
Using where: 在存储引擎层过滤部分数据 -
性能一般的情况:
NULL: 使用索引定位,但需要回表获取数据
Using temporary: 需要创建临时表(如 GROUP BY 操作)
Using filesort: 需要文件排序,无法利用索引排序 -
性能较差的情况:
Using where; Using index: 部分条件下推到存储引擎,但仍需在服务器层过滤
No tables used: 查询不涉及任何表(如 SELECT 1)
Impossible WHERE: WHERE 条件永远为假
3. 其他字段
- possible_keys: 可能使用的索引(优化器评估)
- key: 实际选择的索引
- key_len: 使用的索引长度(字节数)
- rows: 预估扫描行数,用于评估查询代价
- filtered: 按表条件过滤后的行百分比,辅助判断索引有效性。存储引擎返回数据后在Server层过滤的比例。计算方式: (满足条件的行数 / 存储引擎返回的行数) × 100%。
一般来说:filtered 值越小越好,因为这意味着通过 WHERE 语句过滤掉了大量不符合条件的记录,减少了需要处理的数据量,从而提高查询性能。例如,filtered 值为 10% 表示只有 10% 的行符合查询条件,其他 90% 的行被过滤掉了,这通常会显著提升查询效率。如果 filtered 值很大(接近 100%),则意味着 WHERE 子句的条件可能并未有效地减少扫描的数据量,查询效率可能较低。

环境准备
为保证结论严谨,这里给出我的MySQL版本,不同版本之间可能会有差异。我这里的MySQL采用的版本为8.0.42。大家可以通过docker方式或者直接安装。

实战
📢注意:
- 索引失效 ≠ 完全不使用索引
- 索引失效 = 无法使用最优的索引查找方式。比如索引查找功能(type=ref或者type=range)
- 可能退化为全索引扫描(index)而非全表扫描(ALL)
走索引不一定比全表扫描更快,是否需要让MySQL走索引需要结合自己数据结构和部分,来综合判断
1. 不满足最左匹配原则
注意:
- 索引失效 ≠ 完全不使用索引
- 索引失效 = 无法使用最优的索引查找方式。比如索引查找功能(type=ref或者type=range)
- 可能退化为全索引扫描(index)而非全表扫描(ALL)
索引失效情况:必定失效
原因:复合索引必须遵循最左前缀原则,跳过左侧索引列会导致后续索引列无法使用
方案:
- 确保查询条件包含复合索引的最左列
- 重新设计索引顺序,将高频查询字段放在左侧
验证SQL:
-- 创建复合索引
CREATE TABLE users (id INT PRIMARY KEY,name VARCHAR(50),age INT,city VARCHAR(50),INDEX idx_name_age_city (name, age, city)
);-- ❌错误查询:跳过name列,直接使用age
EXPLAIN SELECT * FROM users WHERE age = 25;
-- 结果:type不为ref,没有使用原本的复合索引-- ✅正确查询:遵循最左前缀
EXPLAIN SELECT * FROM users WHERE name = 'John';
-- 结果:type为ref,使用复合索引

2. 使用了select *
索引失效情况:不会直接导致索引失效,但可能影响性能。当查询无法使用覆盖索引时,需要回表操作,当预估回表代价过高时,优化器可能选择全表扫描
原因:
- 查询所有字段可能导致无法使用覆盖索引
- 需要回表查询完整数据,增加了I/O开销
- 当结果集数量超过表总行数25%时,优化器倾向于全表扫描
方案:
- 明确指定需要的字段,避免使用SELECT *
- 利用覆盖索引,确保查询字段都在索引中
验证SQL:以走覆盖索引为例
-- 创建覆盖索引
drop table t_user;
CREATE TABLE `t_user` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',`id_no` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '身份编号',`username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '用户名',`age` int(11) DEFAULT NULL COMMENT '年龄',`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),KEY `union_idx` (`id_no`,`username`,`age`),KEY `create_time_idx` (`create_time`)
) ;-- ❌错误查询,*查询所有列,无法走覆盖索引,全表扫描
explain select * from t_user where username = 'Tom2';-- ✅正确查询:只查询需要的字段,走覆盖索引
explain select id_no, username, age from t_user where username = 'Tom2';

3. 索引列上有计算
索引失效情况:必定失效
原因:对索引列进行计算破坏了索引的有序性
方案:
- 将计算操作移到常量一侧
- 预先计算好结果值进行查询
验证SQL:
CREATE TABLE orders (id INT PRIMARY KEY,amount DECIMAL(10,2),created_at TIMESTAMP,INDEX idx_amount (amount)
);-- ❌错误查询:对索引列进行计算
EXPLAIN SELECT * FROM orders WHERE amount + 10 = 100;
-- 结果:type为ALL,未使用索引-- ✅正确查询:将计算移到常量一侧
EXPLAIN SELECT * FROM orders WHERE amount = 90;
-- 结果:type为ref,使用了索引

4. 索引列用了函数
索引失效情况:必定失效
原因:函数操作破坏了索引的有序性
方案:
- 避免在WHERE条件中对索引列使用函数
- 使用等价的比较条件替代函数操作
- 创建函数索引(MySQL 8.0+)
验证SQL:
drop table student;
CREATE TABLE student (id INT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50),height INT,INDEX idx_height (height)
);-- 插入测试数据。模拟真实业务场景,保证优化器做出正确抉择
insert into student(name,height)
SELECT CONCAT('Student', seq),150 + FLOOR(RAND() * 50)
FROM (SELECT a.N + b.N * 10 + c.N * 100 AS seqFROM (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a,(SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b,(SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) c
) t WHERE seq <= 500;-- 比如我们要查询体重在130之间的数据
-- ❌错误查询:对height索引列使用SUBSTR函数
EXPLAIN SELECT * FROM student WHERE SUBSTR(height, 1, 2) = '13';
-- 结果:type=ALL,全表扫描-- ✅正确写法:直接使用索引列比较
EXPLAIN SELECT * FROM student WHERE height >= 170 AND height < 180;
-- 结果:type = range,使用索引 idx_height

5. 字段类型不同
注意:
- 索引失效 ≠ 完全不使用索引
- 索引失效 = 无法使用最优的索引查找方式。比如索引查找功能(type=ref或者type=range)
- 可能退化为全索引扫描(index)而非全表扫描(ALL)
索引失效情况:必定失效
原因:类型不匹配导致隐式类型转换
方案:
- 确保查询条件与索引列数据类型完全一致
- 在应用程序层进行类型转换
- 使用CAST函数显式转换类型
验证SQL:
CREATE TABLE account (id INT PRIMARY KEY,account_id VARCHAR(20),INDEX idx_account_id (account_id)
);-- 插入测试数据
INSERT INTO account VALUES (1, '123'), (2, '456');-- ❌错误查询:字符串字段与数字比较
EXPLAIN SELECT * FROM account WHERE account_id = 123;
-- 结果:type不为ref,未使用索引-- ✅正确查询:类型匹配
EXPLAIN SELECT * FROM account WHERE account_id = '123';
-- 结果:type为ref,使用了索引

6. like左边包含%
注意:
- 索引失效 ≠ 完全不使用索引
- 索引失效 = 无法使用最优的索引查找方式。比如索引查找功能(type=ref或者type=range)
- 可能退化为全索引扫描(index)而非全表扫描(ALL)
索引失效情况:必定失效
原因:前缀模糊匹配破坏了索引的有序性
方案:
- 使用后缀匹配LIKE ‘keyword%’
- 对于复杂模糊查询,使用全文索引
验证SQL:
CREATE TABLE articles (id INT PRIMARY KEY,title VARCHAR(100),INDEX idx_title (title)
);-- 插入测试数据
INSERT INTO articles (id, title) VALUES
(1, 'MySQL索引优化指南'),
(2, 'Python数据分析实战'),
(3, 'Java并发编程详解'),
(4, '前端Vue框架入门'),
(5, 'Docker容器化部署'),
(6, 'Redis缓存设计与优化'),
(7, 'Spring Boot微服务架构'),
(8, 'Linux系统运维手册'),
(9, '机器学习算法原理'),
(10, '数据库设计规范');-- ❌错误查询:前缀模糊匹配
EXPLAIN SELECT * FROM articles WHERE title LIKE '%MySQL';
-- 结果:type为ALL,未使用索引-- ✅正确查询:后缀精确匹配
EXPLAIN SELECT * FROM articles WHERE title LIKE 'MySQL%';
-- 结果:type为range,使用了索引

7. 列对比
索引失效情况:必定失效
原因:两个列之间的比较无法利用单列索引
方案:
- 避免列间直接比较,改为与常量比较
- 使用应用程序逻辑处理列间关系
- 创建复合索引覆盖列对比场景
- 使用CASE WHEN语句重写逻辑
验证SQL:
CREATE TABLE comparison (id INT PRIMARY KEY,column_a INT,column_b INT,INDEX idx_column_a (column_a)
);-- ❌错误查询:两列对比
EXPLAIN SELECT * FROM comparison WHERE column_a = column_b;
-- 结果:type为ALL,未使用索引-- ✅正确查询:单列比较
EXPLAIN SELECT * FROM comparison WHERE column_a = 10;
-- 结果:type为ref,使用了索引

8. 使用or关键字
索引失效情况:可能失效
原因:如果多个 OR 子句分别作用于同一字段的不同值(如 WHERE col = A OR col = B),则依然可以走索引;
若涉及多个不同字段,则需要每个字段都有独立索引才有可能使用索引合并(Index Merge)策略;否则退化成全表扫描。
方案:
- 使用UNION/UNION ALL替代OR
- 确保所有OR条件都能使用索引
- 创建复合索引覆盖OR条件
- 使用CASE WHEN语句优化复杂逻辑
验证SQL:
CREATE TABLE employees (id INT PRIMARY KEY,name VARCHAR(50),department VARCHAR(50),salary INT,INDEX idx_department (department),INDEX idx_salary (salary)
);-- ❌错误写法:索引可能失效,两个不同字段的OR查询
EXPLAIN SELECT * FROM employees WHERE department = 'IT' OR salary > 50000;
-- 结果:可能type为ALL,取决于数据分布-- ✅正确写法:走索引查询,可拆分为两个语句,然后通过union连接操作进行合并
EXPLAIN SELECT * FROM employees WHERE department = 'IT' union SELECT * FROM employees WHERE salary > 50000;

9. not in
索引失效情况:not in可能失效
原因:not in如果范围过多,通常不走索引,因为否定条件(如id NOT IN (1, 2, 3))难以通过索引高效处理。MySQL倾向于全表扫描逐行检查是否满足条件。在 SQL 标准中,任意与 NULL 进行比较的结果都是未知(unknown),因此整个 NOT IN (…) 表达式都会返回 false 或 unknown,从而无法命中索引。推荐使用 NOT EXISTS 替代,它不会受到 NULL 影响。
解决:
- 减少IN的值数量:尽量控制IN中的值数量,或者使用范围查询(如BETWEEN)替代。
- 使用NOT EXISTS替代NOT IN:NOT EXISTS更容易利用索引
- 改用表连接
验证SQL:
-- drop table sales_records,stores;
-- 商品销售记录表
CREATE TABLE sales_records (id BIGINT PRIMARY KEY,product_id INT,store_id INT,sale_time DATETIME,INDEX idx_store_id (store_id),INDEX idx_sale_time (sale_time)
);-- 门店表
CREATE TABLE stores (store_id INT PRIMARY KEY,store_name VARCHAR(100),is_active TINYINT
);-- 插入测试数据演示效果
-- 插入销售记录数据(10000条记录)
INSERT INTO sales_records (id, product_id, store_id, sale_time)
SELECTseq AS id,FLOOR(RAND() * 100) + 1 AS product_id,FLOOR(RAND() * 500) + 1 AS store_id,DATE_ADD('2023-02-05 00:00:00', INTERVAL FLOOR(RAND() * 1440) MINUTE) AS sale_time
FROM (SELECT @row := @row + 1 AS seqFROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t4,(SELECT @row := 0) t5LIMIT 10000) numbers;-- 插入门店数据(500个门店)
INSERT INTO stores (store_id, store_name, is_active)
SELECTseq AS store_id,CONCAT('Store_', seq) AS store_name,CASE WHEN seq % 3 = 0 THEN 0 ELSE 1 END AS is_active
FROM (SELECT @row2 := @row2 + 1 AS seqFROM (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,(SELECT @row2 := 0) t3LIMIT 500) numbers;-- ❌错误:通过not in查询大范围数据
explain SELECTsr.product_id,COUNT(*) as sale_count
FROM sales_records sr
WHERE sr.sale_time >= '2023-02-05 00:00:00'AND sr.sale_time < '2023-02-06 00:00:00'AND sr.store_id NOT IN (SELECT store_idFROM storesWHERE is_active = 0)
GROUP BY sr.product_id;-- ✅正确:改用表连接
explain SELECTsr.product_id,COUNT(*) as sale_count
FROM sales_records sr
LEFT JOIN stores s ON sr.store_id = s.store_id AND s.is_active = 0
WHERE sr.sale_time >= '2023-02-05 00:00:00'AND sr.sale_time < '2023-02-06 00:00:00'AND s.store_id IS NULL
GROUP BY sr.product_id;

10. Order by场景
索引失效情况:可能失效
原因:排序字段无索引或与查询条件索引不匹配
方案:
- 为排序字段创建合适的索引
- 确保排序字段顺序与索引顺序一致
- 使用覆盖索引避免回表
- 对于复杂排序,考虑分库分表
验证SQL:
drop table users;
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50) NOT NULL,email VARCHAR(100) NOT NULL,age INT,city VARCHAR(50),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,INDEX idx_username (username),INDEX idx_age_city (age, city),INDEX idx_created_at (created_at)
);-- 创建存储过程:插入测试数据(10万条),模拟真实场景,否则可能因数据量太少优化器做出不同选择
DELIMITER $$
CREATE PROCEDURE GenerateTestData()
BEGINDECLARE i INT DEFAULT 1;WHILE i <= 100000 DOINSERT INTO users (username, email, age, city, created_at) VALUES (CONCAT('user', i),CONCAT('user', i, '@example.com'),FLOOR(RAND() * 80) + 18, -- 年龄在18-98之间ELT(FLOOR(RAND() * 5) + 1, '北京', '上海', '广州', '深圳', '杭州'),DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY));SET i = i + 1;END WHILE;
END$$
DELIMITER ;
-- 执行存储过程生成数据
CALL GenerateTestData();
-- 删除存储过程
DROP PROCEDURE GenerateTestData;-- ❌错误查询:order by字段中包含非索引列
EXPLAIN SELECT age,city,email FROM users WHERE age > 25 ORDER BY city, age,email;-- ✅正确查询:order by字段尽量包含索引列
EXPLAIN SELECT age,city FROM users WHERE age > 25 ORDER BY age,city;

11. 使用 != 或 <>
type扫描方式由快到慢:system > const > eq_ref > ref > range > index > ALL
索引失效情况:通常失效
原因:不等值查询无法有效利用索引的有序性
方案:
- 对于离散值,使用多个等值查询UNION
- 使用范围查询结合NOT条件
- 重新设计业务逻辑,避免不等值查询
验证SQL:
drop table test_orders;
CREATE TABLE test_orders (id INT AUTO_INCREMENT PRIMARY KEY,status VARCHAR(20),created_at TIMESTAMP,Index idx_status(status)
);-- 创建存储过程,插入测试数据。让优化器因数据分布走全表扫描
DELIMITER $$
CREATE PROCEDURE InsertSkewedData()
BEGINDECLARE i INT DEFAULT 1;-- 插入数据,让 'completed' 状态只占极少数(<1%)WHILE i <= 10000 DOIF i <= 50 THEN -- 只有0.5%是completed状态INSERT INTO test_orders (status, created_at) VALUES ('completed', NOW());ELSE -- 99.5%是非completed状态INSERT INTO test_orders (status, created_at) VALUES (ELT(FLOOR(RAND() * 4) + 1, 'pending', 'processing', 'cancelled', 'shipped'), NOW());END IF;SET i = i + 1;END WHILE;
END$$
DELIMITER ;
-- 执行存储过程
CALL InsertSkewedData();-- ❌错误查询:!=导致不直接走索引。导致全表扫描
EXPLAIN SELECT * FROM test_orders WHERE status != 'completed';-- ✅正确查询:当状态值比较少的时候,可通过union优化为等值查询。type结果为ref
explain SELECT * FROM test_orders WHERE status = 'pending'
UNION ALL
SELECT * FROM test_orders WHERE status = 'processing'
UNION ALL
SELECT * FROM test_orders WHERE status = 'cancelled'
UNION ALL
SELECT * FROM test_orders WHERE status = 'shipped';

优化后效果:
PS:具体还需要跟自己数据分布有关系,需要结合实际情况,并不一定走索引就更快。

12. 使用 IS NULL 或 IS NOT NULL
索引失效情况:可能失效
原因:取决于字段是否允许NULL和数据分布。
- 如果字段不允许NULL,IS NULL不会返回任何结果
- 如果NULL值很少,IS NULL可能使用索引。如果null值很多(比如占比90%),优化器评估后则可能走全表扫描
- 如果非NULL值很少,IS NOT NULL可能使用索引
方案:
- 根据数据分布选择合适的查询方式
- 对于允许NULL的字段,考虑使用默认值替代NULL
验证SQL:
-- drop table users;
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50) NOT NULL,email VARCHAR(100) NULL, -- 允许NULLcreated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,INDEX idx_email (email),INDEX idx_created_at (created_at)
);-- 插入测试数据,制造不均匀的NULL分布,比如表数据中email绝大部分(90%)都为null,那么在查询时,优化器可能会选择全表扫描
INSERT INTO users (username, email) VALUES
('user1', NULL),
('user2', NULL),
('user3', NULL),
('user4',NULL),
('user5', NULL),
('user6', NULL),
('user7', 'user7@example.com'),
('user8', NULL),
('user9', NULL),
('user10', NULL);-- 因数据分布原因可能导致全表扫描,因为优化器认为走索引+回表会比全表扫描更耗时
EXPLAIN SELECT * FROM users WHERE email IS NULL;-- 注意📢:如果认为由于其他原因导致优化器做出了错误判断,则可以通过下面命令强制让优化器走索引(生产一般不建议这么操作,即使操作也必须经过严格的验证)
-- EXPLAIN SELECT * FROM users FORCE INDEX (idx_email) WHERE email IS NULL;

13. 使用 LIMIT 但没有合适的索引
索引失效情况:可能失效
原因:
- 全表扫描的成本估算 当 LIMIT 和 OFFSET 的值较大时,优化器可能认为直接扫描全表并截取结果比使用索引更快。例如,LIMIT 1000, 10 会跳过前 1000 行数据,这种情况下即使有索引,也需要大量随机读取操作。
- 排序与索引不匹配 如果查询中包含 ORDER BY,但排序字段未正确建立索引,MySQL 会先对数据进行排序,再应用 LIMIT。这会导致索引无法被有效利用。
解决:
- 确保排序字段有合适的索引 为 ORDER BY 中的字段建立单列或联合索引
- 减少 OFFSET 的使用 使用子查询替代大偏移量的 OFFSET
验证SQL:这里以排序与索引不匹配 案例演示
--drop table users;
CREATE TABLE `users` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL,`email` varchar(100) NOT NULL,`created_at` datetime NOT NULL,`status` tinyint(1) DEFAULT '1',PRIMARY KEY (`id`)
) ;-- 插入测试数据
INSERT INTO users (name, email, created_at, status)
SELECT CONCAT('User', seq), CONCAT('user', seq, '@example.com'), DATE_ADD('2023-01-01', INTERVAL seq MINUTE),CASE WHEN seq % 10 = 0 THEN 0 ELSE 1 END
FROM (SELECT @row := @row + 1 as seqFROM (SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) t1,(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) t2,(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) t3,(SELECT @row:=0) t4LIMIT 10000
) numbers;-- ❌错误写法:排序字段没有索引
EXPLAIN SELECT id FROM users ORDER BY created_at LIMIT 500, 10;-- ✅正确写法:为排序字段创建索引
ALTER TABLE users ADD INDEX idx_created_at (created_at);
EXPLAIN SELECT id FROM users ORDER BY created_at LIMIT 500, 10;

14. 数据分布影响
注意📢:并不一定走索引就比全表扫描快,具体需要结合自己的数据结构
索引失效情况:可能失效
原因:数据倾斜导致优化器认为走索引更慢,直接选择全表扫描
方案:
- 分析数据分布,更新统计信息
- 对于倾斜数据,使用提示强制索引
比如:
- 我们要查询某个字段为空值的记录,如果表中该字段null过多,比如占比90%,那么优化器可能会走全表扫描,而非走该字段的索引。
- 或者某些值出现频率远高于其他值status 字段中99%的值为 ‘active’,只有1%为其他状态,那么 WHERE status = ‘active’ 时索引效果差,可能导致全表扫描
15. 使用In语句
索引失效情况:可能失效
原因:
- IN列表过大:当 IN 子句中的值过多时,MySQL优化器可能判断使用索引的成本高于全表扫描
- 子查询优化限制:MySQL在处理 IN 子查询时,可能无法准确估算成本
- 统计信息不足:优化器缺乏足够的统计信息来做出最优决策
- 索引选择性差:当IN列表包含大量值时,索引的效率可能不如全表扫描
方案:
- 控制IN列表大小(建议不超过1000个值)
- 使用JOIN替代大IN列表
- 使用临时表或派生表
- 使用EXISTS替代IN
验证SQL:
-- drop table classes,students;
-- 创建班级表
CREATE TABLE classes (id INT AUTO_INCREMENT PRIMARY KEY,class_name VARCHAR(50) NOT NULL,city VARCHAR(20) NOT NULL,is_active TINYINT DEFAULT 1,INDEX idx_city (city),INDEX idx_active (is_active)
);-- 创建学生表
CREATE TABLE students (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50) NOT NULL,class_id INT NOT NULL,score INT NOT NULL,is_active TINYINT DEFAULT 1,INDEX idx_class_id (class_id),INDEX idx_score (score),INDEX idx_active (is_active)
);-- 插入测试数据
INSERT INTO classes (class_name, city, is_active) VALUES
('class1', 'beijing', 1),
('class2', 'beijing', 1),
('class3', 'beijing', 1),
('class4', 'beijing', 1),
('class5', 'shanghai', 1);INSERT INTO students (name, class_id, score, is_active) VALUES
('zhangsan', 1, 85, 1),
('lisi', 1, 92, 1),
('wangwu', 2, 78, 1),
('zhaoliu', 2, 88, 1),
('qianqi', 3, 95, 1),
('sunba', 3, 65, 1),
('zhoujiu', 4, 72, 1),
('wushi', 5, 89, 1);-- ❌错误写法:in的范围过大,这里in后面跟的id值过多
EXPLAIN
SELECT id, name, class_id, score
FROM students
WHERE class_id IN (1,2,3,4);-- ✅正确写法:如果in的范围过大,可尝试用inner方式平替
EXPLAIN
SELECT s.id, s.name, s.class_id, s.score
FROM students s
INNER JOIN (SELECT id FROM classes WHERE city = 'beijing'
) c ON s.class_id = c.id;

拓展
强制索引
在某些情况下,MySQL优化器可能没有选择最优的执行计划,这时我们可以使用强制索引提示来指导优化器的行为。
- 语法:
-- 强制使用特定索引
SELECT * FROM table_name FORCE INDEX (index_name) WHERE conditions;-- 忽略特定索引
SELECT * FROM table_name IGNORE INDEX (index_name) WHERE conditions;-- 建议优化器使用特定索引
SELECT * FROM table_name USE INDEX (index_name) WHERE conditions;
- 使用场景:
- 当优化器选择了错误的索引导致查询性能下降时
- 在复杂的查询中,优化器无法准确评估不同索引的成本
- 需要测试不同索引对查询性能的影响时
- 注意事项:
- 强制索引会绕过优化器的智能决策,应谨慎使用
- 当表结构发生变化时,强制索引可能导致查询失败
- 建议在生产环境中充分测试后再使用强制索引
优化器行为解析
MySQL优化器的核心目标是选择最低成本的执行计划。当查询中包含LIMIT时,优化器可能会认为通过全表扫描获取少量数据的成本低于使用索引的成本。例如,假设一个表有100万行数据,但LIMIT 10只需要返回前10行,优化器可能判断全表扫描更快。
以下是优化器主要考虑的因素:
- 数据分布:如果排序字段的数据分布不均匀,可能导致优化器误判。
- 查询条件:查询条件不够精确(如范围过大),会使优化器倾向于全表扫描。
- 覆盖索引:如果查询涉及的字段未完全覆盖索引,优化器可能放弃使用索引。
总结
| 场景 | 概念描述 | 是否必定失效 | 失效原因 | 解决方案 |
|---|---|---|---|---|
| 1. 最左匹配原则 | 复合索引必须从最左列开始连续使用 | 必定失效 | 跳过左侧索引列导致B+树有序性被破坏 | 查询条件包含复合索引最左列,或重新设计索引顺序 |
| 2. Select * | 查询所有字段 | 不直接失效 | 无法使用覆盖索引,需要回表操作,增加I/O开销 | 只查询需要的字段,利用覆盖索引优化 |
| 3. 索引列计算 | 对索引列进行算术运算 | 必定失效 | 计算操作破坏索引存储的有序性 | 将计算移到常量一侧,保持索引列纯净 |
| 4. 索引列函数 | 对索引列使用函数操作 | 必定失效 | 函数操作改变索引值的原始顺序 | 避免对索引列使用函数,或使用函数索引(MySQL 8.0+) |
| 5. 字段类型不同 | 查询条件与索引列数据类型不匹配 | 必定失效 | 隐式类型转换导致索引无法直接匹配 | 确保查询条件与索引列数据类型一致 |
| 6. Like左模糊 | 使用LIKE '%keyword'前缀模糊匹配 | 必定失效 | 前缀模糊匹配破坏B+树的前缀有序性 | 使用后缀精确匹配LIKE 'keyword%'或全文索引 |
| 7. 列对比 | WHERE条件中进行两列之间的比较 | 必定失效 | 无法利用单列索引进行列间值比较 | 重写查询逻辑,避免列间直接比较 |
| 8. OR查询 | 使用OR连接不同字段的查询条件 | 可能失效 | 当OR条件不能都使用索引时,优化器选择全表扫描 | 使用UNION替代OR,或确保所有OR条件都能命中索引 |
| 9. NOT IN | 使用NOT IN进行排除查询 | 可能失效 | 否定条件难以通过索引高效处理,涉及NULL时更复杂 | 使用NOT EXISTS、LEFT JOIN或减少IN值数量 |
| 10. ORDER BY | 排序字段无索引或与查询索引不匹配 | 可能失效 | 缺少合适索引导致filesort文件排序 | 为排序字段建立索引,确保与查询条件匹配 |
| 11. != 或 <> | 使用不等值查询条件 | 通常失效 | 不等值查询无法有效利用索引的范围扫描特性 | 对于离散值使用多个等值查询UNION ALL替代 |
| 12. IS NULL/NOT NULL | 对索引列进行空值判断 | 可能失效 | 取决于NULL值分布,优化器根据成本选择执行计划 | 根据数据分布决定,或调整索引设计 |
| 13. LIMIT无索引 | 使用LIMIT但缺少合适的排序索引 | 可能失效 | 大偏移量LIMIT需要大量随机I/O,优化器选择全表扫描 | 为排序字段建立索引,使用覆盖索引优化 |
| 14. 数据分布影响 | 数据倾斜导致优化器误判 | 可能失效 | 某些值频率过高,索引选择性差,成本估算不准确 | 分析数据分布,考虑直方图统计或查询重写 |
| 15. IN语句过大 | IN子句中包含过多值 | 可能失效 | IN列表过大时索引效率可能低于全表扫描 | 控制IN值数量,使用JOIN或临时表替代 |
关键注意事项
- 关于"索引失效"的准确定义
- 严格失效: 完全不使用索引(type=ALL)
- 性能退化: 无法使用最优索引查找(如从ref/range退化为index)
- 成本选择: 走索引不一定比全表扫描更快,需结合数据分布判断
- 优化器行为特点,MySQL优化器基于成本选择执行计划,考虑因素包括:
- 数据分布和索引选择性
- 内存和I/O成本估算
- 结果集大小和查询复杂度
- 统计信息的准确性
- 强制索引使用
-- 强制使用特定索引(谨慎使用)
SELECT * FROM table_name FORCE INDEX (index_name) WHERE conditions;-- 忽略特定索引
SELECT * FROM table_name IGNORE INDEX (index_name) WHERE conditions;-- 建议使用索引
SELECT * FROM table_name USE INDEX (index_name) WHERE conditions;
- 最佳实践建议:
索引设计原则:
- 选择高选择性的列建立索引
- 复合索引遵循最左前缀原则
- 避免在索引列上进行计算或函数操作
查询优化技巧:
- 使用EXPLAIN分析执行计划
- 关注数据分布对优化器的影响
- 合理使用覆盖索引减少回表
监控与维护:
- 定期更新统计信息
- 监控慢查询日志
- 根据业务变化调整索引策略
参考文档:
https://dev.mysql.com/doc/refman/8.0/en/explain-output.html#explain-extra-information
https://dev.mysql.com/doc/refman/8.0/en/explain-output.html#explain-output-columns
https://dev.mysql.com/doc/refman/8.0/en/optimization.html
https://dev.mysql.com/doc/refman/8.0/en/optimizer-statistics.html
https://dev.mysql.com/doc/refman/8.0/en/optimization-indexes.html
