三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MySQL索引失效原理与优化实践

MySQL索引失效原理与优化实践

1. 索引失效的本质:当优化器决定放弃索引

MySQL索引失效的根本原因在于查询优化器的成本计算机制。优化器会根据统计信息估算全表扫描和索引扫描的成本,当它认为全表扫描更高效时,就会放弃使用索引。这种"失效"实际上是优化器的主动选择,而非索引本身出现问题。

我曾在处理一个300万行的用户表时遇到典型场景:SELECT * FROM users WHERE status = 1这个简单查询本该使用status字段的索引,但EXPLAIN显示进行了全表扫描。通过SHOW INDEX FROM users查看索引统计信息,发现status字段的基数(Cardinality)值异常低,导致优化器误判。

关键提示:索引失效≠索引损坏,而是优化器基于成本模型的决策结果

2. 六大经典失效场景原理剖析

2.1 最左前缀原则与B+树结构

联合索引(a,b,c)的存储结构决定了它只能按a→b→c的顺序使用。当查询条件缺少a时,B+树的有序性被破坏,索引就会失效。例如:

-- 能使用索引 SELECT * FROM table WHERE a=1 AND b=2 -- 不能使用索引 SELECT * FROM table WHERE b=2

底层原理在于B+树的叶子节点按(a,b,c)排序存储,缺少最左字段时无法利用有序性快速定位。

2.2 隐式类型转换的代价

当字段类型与条件值类型不匹配时,MySQL会进行隐式转换。例如字符串字段用数字查询:

-- phone是varchar类型 SELECT * FROM users WHERE phone = 13800138000

这会导致索引失效,因为需要逐行执行CAST(phone AS signed)操作。我曾用性能测试对比:

  • 使用正确类型:0.5ms
  • 隐式转换:1200ms

2.3 函数操作破坏索引顺序

任何对索引列的函数操作都会使索引失效:

-- 失效案例 SELECT * FROM orders WHERE DATE_FORMAT(create_time,'%Y-%m')='2023-01'

因为B+树存储的是原始值,而非函数计算后的结果。解决方案是改为范围查询:

-- 优化后 SELECT * FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2023-02-01'

2.4 范围查询后的索引列失效

对于联合索引(a,b,c),如果a使用范围查询,后续字段无法使用索引:

-- 只有a能用索引,b和c失效 SELECT * FROM table WHERE a > 1 AND b = 2

这是因为B+树在范围扫描时,后续字段的值是无序的。

2.5 不等于(!=/<>)查询的全表扫描

优化器认为使用索引查不全值再回表的成本可能高于直接全表扫描:

-- 通常会导致全表扫描 SELECT * FROM products WHERE status != 1

2.6 OR条件的短路特性

当OR条件包含非索引列时,整个查询会失效:

-- 假设name有索引而age没有 SELECT * FROM users WHERE name='张三' OR age=20

这是因为MySQL需要同时检查两个条件,无法有效利用索引。

3. 索引统计信息的幕后机制

3.1 基数(Cardinality)的影响

通过SHOW INDEX FROM table看到的Cardinality值是索引选择性的关键指标。当这个值严重偏离实际时(比如字段有大量重复值),优化器会错误估计扫描行数。

手动更新统计信息命令:

ANALYZE TABLE table_name;

3.2 采样页数的配置

MySQL通过采样部分数据页来估算统计信息,innodb_stats_persistent_sample_pages参数控制采样数量。在数据分布不均匀时,增加该值可以提高准确性。

3.3 索引提示的使用技巧

当优化器选择错误时,可以用FORCE INDEX强制使用索引:

SELECT * FROM orders FORCE INDEX(idx_create_time) WHERE DATE(create_time) = '2023-01-01'

但要注意这会使执行计划僵化,建议仅在确有必要时使用。

4. 实战中的特殊失效场景

4.1 ICP特性与失效边界

Index Condition Pushdown(ICP)是MySQL5.6引入的优化,它能在存储引擎层过滤数据。但当出现以下情况时ICP会失效:

  • 使用子查询
  • 使用存储函数
  • 引用外部表的列

4.2 字符集与排序规则冲突

当关联字段的字符集或排序规则不同时,索引会失效:

-- utf8与utf8mb4的关联 SELECT * FROM t1 JOIN t2 ON t1.name = t2.name WHERE t1.name COLLATE utf8mb4_general_ci = t2.name

4.3 分区表的索引陷阱

在分区表中,如果查询条件不包含分区键,所有分区都会被扫描。例如按月分区的orders表:

-- 没有使用分区键month SELECT * FROM orders WHERE user_id=100

4.4 虚拟列索引的注意事项

虚拟列(Generated Column)上的索引在以下情况失效:

  • 使用了非确定性函数如NOW()
  • 虚拟列公式与查询条件不完全匹配

5. 系统化解决方案与最佳实践

5.1 EXPLAIN的深度解读

重点关注以下字段:

  • type:const > ref > range > index > ALL
  • key:实际使用的索引
  • rows:估算扫描行数
  • Extra:Using index(覆盖索引)、Using filesort(需要排序)

5.2 索引优化器提示

-- 推荐写法 SELECT /*+ INDEX(table_name index_name) */ * FROM table_name

比FORCE INDEX更柔性的控制方式。

5.3 索引跳跃扫描优化

MySQL8.0新增的Index Skip Scan特性,可以在特定条件下突破最左前缀限制:

-- MySQL8.0+可能使用索引 SELECT * FROM table WHERE b=2 AND c=3

前提是联合索引(a,b,c)且字段a的离散值较少。

5.4 索引选择策略

建立索引的黄金法则:

  1. 高选择性字段优先
  2. 常用查询条件组合
  3. 避免过度索引
  4. 定期检查冗余索引

检查冗余索引脚本:

SELECT * FROM sys.schema_redundant_indexes;

6. 真实案例:电商系统优化实录

某电商平台的订单查询接口出现性能问题,原始SQL:

SELECT * FROM orders WHERE user_id=123 AND status IN (2,3) AND create_time > '2023-01-01' ORDER BY update_time DESC LIMIT 10

问题诊断:

  1. 存在(user_id)单列索引和(status,create_time)联合索引
  2. 排序字段update_time没有索引
  3. IN条件导致范围查询

优化方案:

  1. 建立(user_id, status, create_time)的联合索引
  2. 添加update_time的倒序索引
  3. 重写为:
SELECT * FROM orders FORCE INDEX(idx_user_status_time) WHERE user_id=123 AND status = 2 AND create_time > '2023-01-01' UNION ALL SELECT * FROM orders FORCE INDEX(idx_user_status_time) WHERE user_id=123 AND status = 3 AND create_time > '2023-01-01' ORDER BY update_time DESC LIMIT 10

优化后响应时间从1200ms降至35ms。这个案例展示了复合索引设计和查询重写的重要性。

← 返回列表