索引失效是每个MySQL开发者都会遇到的性能瓶颈问题。简单来说,就是明明创建了索引,但查询时优化器却选择了全表扫描。这种情况在大数据量场景下尤为致命——我曾经处理过一个案例,一张3000万行的用户表,因为一个简单的LIKE查询导致索引失效,查询时间从毫秒级暴增到30多秒,直接拖垮了整个应用。
索引失效的核心在于MySQL优化器的决策机制。优化器会根据统计信息、查询条件和表结构,计算不同执行计划的成本。当它认为全表扫描比使用索引更高效时,就会放弃使用索引。这种判断有时是合理的(比如查询需要返回表中大部分数据),但更多时候是由于我们编写了不符合索引使用规则的查询语句。
这是最常见的索引失效场景之一。当查询条件中的数据类型与索引列定义不一致时,MySQL会进行隐式类型转换。例如:
sql复制-- phone字段是varchar类型,但查询使用数字
CREATE INDEX idx_phone ON users(phone);
SELECT * FROM users WHERE phone = 123456789; -- 索引失效
这里MySQL需要将phone列的每个值都转换为数字进行比较,相当于对索引列使用了函数,自然无法使用索引。解决方案很简单:保持类型一致:
sql复制SELECT * FROM users WHERE phone = '123456789'; -- 使用索引
实际开发中,我建议使用ORM时特别注意参数类型,或者在数据库设计阶段就统一字段类型,避免这类问题。
在索引列上使用函数是另一个"索引杀手"。例如日期处理:
sql复制CREATE INDEX idx_birthday ON users(birthday);
-- 错误写法
SELECT * FROM users WHERE YEAR(birthday) = 1990;
-- 正确写法
SELECT * FROM users WHERE birthday BETWEEN '1990-01-01' AND '1990-12-31';
函数操作会使优化器无法直接使用索引的排序特性。类似的情况还包括:
LIKE查询的索引使用有严格限制:
sql复制CREATE INDEX idx_name ON users(name);
-- 不能使用索引
SELECT * FROM users WHERE name LIKE '%张三%';
SELECT * FROM users WHERE name LIKE '%张三';
-- 可以使用索引
SELECT * FROM users WHERE name LIKE '张三%';
只有右模糊查询('value%')能使用索引,因为这种模式可以利用索引的排序特性。对于必须使用全模糊的场景,可以考虑以下方案:
OR条件使用不当会导致索引失效:
sql复制CREATE INDEX idx_age ON users(age);
-- 索引失效
SELECT * FROM users WHERE age > 18 OR name = '张三';
这是因为OR条件要求所有涉及的列都有索引才能使用索引。优化方案:
sql复制-- 使用UNION替代OR
SELECT * FROM users WHERE age > 18
UNION
SELECT * FROM users WHERE name = '张三';
注意:UNION会去重,如果确定结果无重复或不需要去重,可以使用更高效的UNION ALL。
联合索引的使用必须遵循最左前缀原则,就像查电话簿必须先按姓氏再按名字查找:
sql复制CREATE INDEX idx_name_age ON users(name, age);
-- 使用索引
SELECT * FROM users WHERE name = '张三';
SELECT * FROM users WHERE name = '张三' AND age = 25;
-- 索引失效
SELECT * FROM users WHERE age = 25;
联合索引的列顺序至关重要。设计时应:
在联合索引中,范围查询会使后续的索引列失效:
sql复制CREATE INDEX idx_age_salary ON users(age, salary);
-- 只有age能用索引
SELECT * FROM users WHERE age > 25 AND salary > 5000;
解决方案是尽量将范围查询放在最后,或者使用等值查询:
sql复制-- 优化方案:枚举age值
SELECT * FROM users WHERE age = 26 AND salary > 5000
UNION ALL
SELECT * FROM users WHERE age = 27 AND salary > 5000
...
不等于操作符(!=, <>)通常会导致全表扫描:
sql复制CREATE INDEX idx_status ON orders(status);
-- 索引失效
SELECT * FROM orders WHERE status != 'completed';
-- 优化方案
SELECT * FROM orders WHERE status IN ('pending', 'processing', 'cancelled');
对于状态不多的场景,用IN替代!=是更好的选择。如果状态很多,可能需要重新考虑业务设计。
IS NULL条件在某些情况下可能不使用索引:
sql复制CREATE INDEX idx_email ON users(email);
-- 可能不使用索引
SELECT * FROM users WHERE email IS NULL;
是否使用索引取决于表中NULL值的比例。如果NULL值很少,索引可能有效;如果NULL值很多,优化器可能选择全表扫描。解决方案:
sql复制-- 如果空字符串和NULL都表示"无邮箱"
SELECT * FROM users WHERE email = '' OR email IS NULL;
虽然IN通常能使用索引,但也有例外:
sql复制CREATE INDEX idx_category ON products(category);
-- IN列表过长可能不使用索引
SELECT * FROM products WHERE category IN ('cat1','cat2',...,'cat100');
当IN列表很大时,优化器可能认为全表扫描更高效。解决方案:
EXPLAIN是诊断索引问题的利器:
sql复制EXPLAIN SELECT * FROM users WHERE name LIKE '%张三%';
关键字段解读:
| 字段 | 理想值 | 说明 |
|---|---|---|
| type | const, ref, range | 表示索引使用类型,all表示全表扫描 |
| key | 索引名称 | 显示实际使用的索引 |
| key_len | 索引长度 | 使用的索引字节数 |
| rows | 较小值 | 预估扫描行数 |
| Extra | Using index | 表示使用覆盖索引,避免回表 |
通过information_schema可以分析索引使用频率:
sql复制SELECT * FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'your_table';
重点关注:
启用慢查询日志可以捕获性能问题:
sql复制-- 设置慢查询阈值(秒)
SET GLOBAL long_query_time = 1;
-- 启用慢查询日志
SET GLOBAL slow_query_log = 'ON';
分析慢日志时注意:
sql复制-- 好的覆盖索引示例
CREATE INDEX idx_covering ON orders(user_id, status, create_time);
-- 查询可以直接使用索引
SELECT user_id, status FROM orders WHERE user_id = 100;
sql复制CREATE INDEX idx_name_prefix ON users(name(10));
sql复制-- 低效写法
SELECT * FROM users LIMIT 1000000, 20;
-- 高效写法
SELECT * FROM users WHERE id > 1000000 LIMIT 20;
sql复制ANALYZE TABLE users; -- 更新统计信息
ini复制innodb_buffer_pool_size = 4G # 通常设为物理内存的70-80%
ini复制sort_buffer_size = 4M
曾经优化过一个电商系统的订单查询,原始查询:
sql复制SELECT * FROM orders
WHERE DATE(create_time) = '2023-01-01'
AND status IN (1,2,3)
ORDER BY amount DESC
LIMIT 100;
问题分析:
优化方案:
最终优化后的查询:
sql复制SELECT o.* FROM orders o
JOIN (
SELECT id FROM orders
WHERE create_time >= '2023-01-01 00:00:00'
AND create_time < '2023-01-02 00:00:00'
AND status IN (1,2,3)
ORDER BY amount DESC
LIMIT 100
) tmp ON o.id = tmp.id;
另一个案例是社交平台的用户搜索,原始查询:
sql复制SELECT * FROM users
WHERE username LIKE '%张%'
OR nickname LIKE '%张%'
OR bio LIKE '%张%';
优化方案:
最终采用方案:
sql复制ALTER TABLE users ADD FULLTEXT INDEX ft_search (username, nickname, bio);
SELECT * FROM users
WHERE MATCH(username, nickname, bio) AGAINST('张*' IN BOOLEAN MODE);
在实际工作中,我总结了一个简单的索引优化流程:
记住,索引优化是一个持续的过程,随着数据量和查询模式的变化,需要定期review和调整索引策略。