作为一名数据库工程师,我经常遇到这样的困惑:明明已经为查询条件添加了索引,但查询性能依然不尽如人意。经过多年的实战经验,我发现80%的索引失效问题都集中在几个典型场景中。今天我将详细剖析这五大索引失效现场,并分享相应的解决方案和优化思路。
在实际项目中,我遇到一个典型的案例:用户表使用VARCHAR类型存储ID,但查询时却使用了数字类型。表结构如下:
sql复制CREATE TABLE `user` (
`id` VARCHAR(20) NOT NULL,
`name` VARCHAR(50) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB;
执行查询时使用了数字类型:
sql复制SELECT * FROM user WHERE id = 10001;
EXPLAIN结果显示type=ALL,key=NULL,表示进行了全表扫描。
MySQL在执行比较操作时,会进行隐式类型转换。当比较VARCHAR和INT类型时,MySQL会将数字转换为字符串进行比较。上述查询实际上等价于:
sql复制SELECT * FROM user WHERE CAST(id AS CHAR) = 10001;
这种隐式转换导致索引无法使用,因为索引是按照原始字段值构建的,而不是转换后的值。
sql复制SELECT * FROM user WHERE id = '10001';
sql复制ALTER TABLE user MODIFY COLUMN id INT NOT NULL;
sql复制SELECT * FROM user WHERE id = CAST(10001 AS CHAR);
重要提示:在设计表结构时,应该根据实际业务需求选择合适的数据类型。如果字段存储的是数字,就应该使用数值类型而非字符串类型。
在用户搜索场景中,常见的查询方式如下:
sql复制SELECT * FROM user WHERE name LIKE '%小明%';
EXPLAIN结果显示type=ALL,表示进行了全表扫描。
B+树索引是基于前缀匹配的。当使用LIKE '%xxx%'时,由于通配符出现在开头,MySQL无法利用索引的有序性,只能进行全表扫描。
sql复制SELECT * FROM user WHERE name LIKE '小明%';
sql复制-- 添加反转字段
ALTER TABLE user ADD COLUMN name_reversed VARCHAR(50);
UPDATE user SET name_reversed = REVERSE(name);
CREATE INDEX idx_name_reversed ON user(name_reversed);
-- 查询时组合使用
SELECT * FROM user
WHERE name LIKE '小明%' OR name_reversed LIKE CONCAT('%', REVERSE('小明'));
sql复制ALTER TABLE user ADD FULLTEXT INDEX ft_name(name);
SELECT * FROM user WHERE MATCH(name) AGAINST('小明');
实战经验:在实际项目中,我们通常会为搜索字段建立正向和反向两个索引,这样可以有效解决前后模糊匹配的问题。
在订单查询场景中,常见的错误写法:
sql复制SELECT * FROM order WHERE DATE(order_date) = '2026-03-01';
虽然order_date字段有索引,但EXPLAIN显示type=ALL。
当索引列参与函数运算时,MySQL需要对每一行数据都应用该函数,导致无法使用索引。
sql复制SELECT * FROM order
WHERE order_date >= '2026-03-01 00:00:00'
AND order_date < '2026-03-02 00:00:00';
sql复制ALTER TABLE order ADD COLUMN order_date_date DATE;
UPDATE order SET order_date_date = DATE(order_date);
CREATE INDEX idx_order_date_date ON order(order_date_date);
-- 查询优化后
SELECT * FROM order WHERE order_date_date = '2026-03-01';
sql复制ALTER TABLE order
ADD COLUMN order_date_date DATE AS (DATE(order_date)) STORED,
ADD INDEX idx_order_date_date(order_date_date);
性能对比:在百万级数据量的测试中,使用范围查询比使用DATE()函数快约50倍。
多条件查询时使用OR:
sql复制SELECT * FROM user WHERE name = '小明' OR phone = '13800138000';
即使name和phone都有独立索引,EXPLAIN仍可能显示type=ALL。
MySQL优化器在处理OR条件时,认为需要扫描多个索引然后合并结果,当数据量不大时,优化器可能认为全表扫描比索引合并更高效。
sql复制SELECT * FROM user WHERE name = '小明'
UNION ALL
SELECT * FROM user WHERE phone = '13800138000';
sql复制SELECT * FROM user WHERE name IN ('小明', '小红', '小刚');
sql复制SELECT * FROM user WHERE name = '小明'
UNION ALL
SELECT * FROM user WHERE phone = '13800138000' AND name != '小明';
sql复制SET optimizer_switch = 'index_merge=on,index_merge_union=on';
性能测试:在千万级用户表中,UNION ALL方案比OR方案快约10倍。
对于联合索引idx_name_phone(name, phone),执行以下查询:
sql复制SELECT * FROM user WHERE phone = '13800138000';
EXPLAIN显示未使用索引。
联合索引遵循最左前缀原则。idx_name_phone的索引结构是先按name排序,再按phone排序。如果查询条件不包含name,就无法利用索引的有序性。
sql复制SELECT * FROM user WHERE name = '小明' AND phone = '13800138000';
sql复制-- 如果经常单独查询phone字段
CREATE INDEX idx_phone ON user(phone);
sql复制-- 确保开启优化器开关
SET optimizer_switch = 'skip_scan=on';
sql复制-- 根据查询频率调整列顺序
CREATE INDEX idx_phone_name ON user(phone, name);
设计原则:联合索引的列顺序应该按照字段的选择性从高到低排列,同时考虑常用查询条件。
sql复制SELECT * FROM user WHERE name IS NOT NULL;
-- 优化方案
SELECT * FROM user WHERE name > '';
sql复制SELECT * FROM user WHERE name != '小明';
-- 优化方案
SELECT * FROM user WHERE name > '小明' OR name < '小明';
sql复制SELECT * FROM user WHERE id IN (SELECT user_id FROM order WHERE amount > 1000);
-- 优化方案
SELECT u.* FROM user u JOIN order o ON u.id = o.user_id WHERE o.amount > 1000;
sql复制SELECT * FROM user WHERE name IS NULL;
-- 优化方案
-- 确保NULL值确实需要查询,或考虑使用默认值替代NULL
sql复制SELECT * FROM sys.schema_unused_indexes;
sql复制-- 普通查询
SELECT * FROM user WHERE name = '小明';
-- 优化为覆盖索引查询
SELECT id, name FROM user WHERE name = '小明';
-- 确保索引包含所有查询字段
CREATE INDEX idx_covering ON user(name, phone, email);
sql复制-- MySQL 5.6+默认开启
SET optimizer_switch = 'index_condition_pushdown=on';
sql复制SET optimizer_switch = 'mrr=on,mrr_cost_based=off';
sql复制SET optimizer_switch = 'batched_key_access=on';
SET join_buffer_size = 4M;
原始查询:
sql复制SELECT * FROM orders
WHERE DATE(create_time) = '2023-01-01'
AND status = 'completed'
AND customer_id IN (SELECT id FROM customers WHERE vip_level > 3);
优化方案:
原始查询:
sql复制SELECT * FROM posts
WHERE user_id IN (SELECT friend_id FROM friendships WHERE user_id = 1001)
ORDER BY create_time DESC
LIMIT 20;
优化方案:
在实际项目中,我发现索引优化是一个持续的过程。随着数据量的增长和查询模式的变化,需要定期审查和调整索引策略。EXPLAIN是诊断查询性能问题的利器,应该成为每个开发者的必备技能。