一、索引失效的常见场景

相信很多开发同学都遇到过这样的情况:明明表上已经建立了索引,但查询速度依然慢得像蜗牛爬。这种时候,十有八九是索引失效了。我们先来看看几种典型的索引失效场景。

最经典的就是在索引列上使用了函数或者运算。比如我们有个用户表,按照手机号的前三位建立了索引:

-- 用户表结构示例(MySQL技术栈)
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) DEFAULT NULL,
  `phone` varchar(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  INDEX `idx_phone_prefix` (`phone`(3))
) ENGINE=InnoDB;

如果查询时使用了函数:

-- 错误示例:在索引列上使用函数导致索引失效
SELECT * FROM users WHERE LEFT(phone, 3) = '138';

这个查询就无法使用我们精心设计的idx_phone_prefix索引了。正确的做法应该是:

-- 正确示例:避免在索引列上使用函数
SELECT * FROM users WHERE phone LIKE '138%';

二、隐式类型转换的坑

另一个常见的坑是隐式类型转换。MySQL在执行比较时,如果发现两边数据类型不一致,会自动进行类型转换,这往往会导致索引失效。

假设我们有个订单表:

-- 订单表结构示例(MySQL技术栈)
CREATE TABLE `orders` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32) DEFAULT NULL,
  `user_id` int(11) DEFAULT NULL,
  `amount` decimal(10,2) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`),
  INDEX `idx_order_no` (`order_no`)
) ENGINE=InnoDB;

如果查询时不小心把字符串类型的order_no和数字比较:

-- 错误示例:隐式类型转换导致索引失效
SELECT * FROM orders WHERE order_no = 12345678;

虽然语法上没问题,但MySQL会把order_no转换成数字再比较,导致索引失效。正确的做法是:

-- 正确示例:确保比较时类型一致
SELECT * FROM orders WHERE order_no = '12345678';

三、联合索引的最左前缀原则

联合索引的使用有个重要的原则叫"最左前缀匹配",如果违反这个原则,索引也会失效。

来看个商品表的例子:

-- 商品表结构示例(MySQL技术栈)
CREATE TABLE `products` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category_id` int(11) DEFAULT NULL,
  `brand_id` int(11) DEFAULT NULL,
  `price` decimal(10,2) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`),
  INDEX `idx_cate_brand_status` (`category_id`, `brand_id`, `status`)
) ENGINE=InnoDB;

以下几种查询方式,索引使用情况各不相同:

-- 情况1:完全匹配最左前缀,索引有效
SELECT * FROM products WHERE category_id = 1 AND brand_id = 5 AND status = 1;

-- 情况2:匹配部分最左前缀,索引部分有效
SELECT * FROM products WHERE category_id = 1 AND brand_id = 5;

-- 情况3:跳过最左列,索引失效
SELECT * FROM products WHERE brand_id = 5 AND status = 1;

-- 情况4:使用范围查询后,后续列索引失效
SELECT * FROM products WHERE category_id = 1 AND brand_id > 5 AND status = 1;

四、OR条件的陷阱

使用OR条件时也容易导致索引失效,特别是当OR连接的条件不是同一个索引时。

比如在用户表上查询:

-- 错误示例:OR条件导致索引失效
SELECT * FROM users WHERE phone LIKE '138%' OR username = '张三';

即使phone和username都有单独的索引,这个查询也无法有效使用索引。解决方案有几种:

  1. 使用UNION ALL改写:
-- 优化方案1:使用UNION ALL
SELECT * FROM users WHERE phone LIKE '138%'
UNION ALL
SELECT * FROM users WHERE username = '张三';
  1. 如果业务允许,可以考虑建立全文索引:
-- 优化方案2:考虑使用全文索引
ALTER TABLE users ADD FULLTEXT INDEX `idx_fulltext_search` (`username`, `phone`);
SELECT * FROM users WHERE MATCH(username, phone) AGAINST('张三 138*' IN BOOLEAN MODE);

五、索引选择性不足的问题

有时候索引失效是因为索引的选择性太差。所谓选择性,是指不重复的索引值与表记录数的比值。

举个例子,我们有个文章表:

-- 文章表结构示例(MySQL技术栈)
CREATE TABLE `articles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) DEFAULT NULL,
  `content` text,
  `status` tinyint(4) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  INDEX `idx_status` (`status`)
) ENGINE=InnoDB;

如果status只有0和1两个值,那么在这个字段上建索引就没什么意义:

-- 选择性差的索引示例
SELECT * FROM articles WHERE status = 1;

这种情况下,MySQL优化器可能会选择全表扫描而不是使用索引,因为索引的选择性太差了。

六、优化实践建议

根据我的经验,总结几个实用的优化建议:

  1. 使用EXPLAIN分析SQL执行计划,这是排查索引问题的第一步。比如:
-- 使用EXPLAIN分析查询
EXPLAIN SELECT * FROM users WHERE phone LIKE '138%';
  1. 考虑使用覆盖索引,避免回表操作:
-- 创建覆盖索引示例
ALTER TABLE users ADD INDEX `idx_phone_cover` (`phone`, `username`);

-- 使用覆盖索引查询
SELECT phone, username FROM users WHERE phone LIKE '138%';
  1. 定期分析表,更新索引统计信息:
-- 更新统计信息
ANALYZE TABLE users;
  1. 对于复杂查询,考虑拆分成多个简单查询,在应用层组合结果。

  2. 监控慢查询日志,及时发现性能问题:

-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';

七、真实案例分享

去年我们遇到一个生产环境的问题:用户反馈订单列表加载特别慢。经过排查,发现是如下查询导致的:

-- 原始问题SQL
SELECT * FROM orders 
WHERE create_time > '2023-01-01' 
AND (status = 2 OR pay_status = 1)
ORDER BY id DESC
LIMIT 20;

这个查询有多个问题:

  1. 使用了OR条件
  2. 排序字段和过滤字段没有关联
  3. 没有有效利用复合索引

我们的优化方案是:

  1. 建立新的复合索引:
ALTER TABLE orders ADD INDEX `idx_status_paystatus_time` (`status`, `pay_status`, `create_time`);
  1. 重写SQL,避免OR条件:
-- 优化后的SQL
SELECT * FROM orders WHERE create_time > '2023-01-01' AND status = 2
UNION ALL
SELECT * FROM orders WHERE create_time > '2023-01-01' AND pay_status = 1 AND status != 2
ORDER BY create_time DESC
LIMIT 20;

优化后,查询时间从原来的3秒多降到了100毫秒以内。

八、总结与思考

索引优化是个需要不断实践和总结的过程。通过这次分享,我们可以得出几个重要结论:

  1. 理解索引的工作原理是优化的基础
  2. 掌握常见索引失效场景可以避免很多低级错误
  3. 合理设计索引结构能显著提升查询性能
  4. 监控和分析是持续优化的关键

最后提醒一点:索引不是越多越好。每个额外的索引都会带来写入性能的开销和维护成本。我们需要在查询性能和写入性能之间找到平衡点。