Mysql索引失效问题及其原因
MySQL 中使用索引不一定有效。索引好比字典的目录,用对了能快速找到字,用错了或者没必要用,反而可能拖慢速度。你需要“侦探工具”——EXPLAIN
命令,才能看清你的查询是不是真的用了索引,以及用得好不好。
为什么说索引不一定有效?
这里我们要抛开“索引万能”的误区。索引的本质是空间换时间,它通过额外的存储空间,牺牲一些写入性能(因为每次增删改查都要维护索引),来提高查询性能。但这个“提高”是有条件的。
索引就像一把瑞士军刀,功能很多,但你不能指望它在所有场合都发挥作用。
我们可以从几个角度来探讨:
-
数据的特性(数据的“样貌”)
- 区分度低(选择性差)的列: 想象一个性别列,只有“男”和“女”两种值。如果对它建立索引,你在查询
gender = '男'
时,依然需要扫描表中近一半的数据(假设男女比例均衡)。这种情况下,全表扫描可能比走索引再回表更高效,因为全表扫描是顺序 I/O,而回表引入大量随机 I/O。MySQL 优化器通常会足够聪明地选择不走索引。 - 经常更新的列: 索引的维护成本很高。每次对索引列的数据进行增删改,都需要调整索引结构。如果一个列频繁更新,索引的维护成本可能会超过查询带来的收益。
- 数据量太小: 如果表里只有几百甚至几千条记录,全表扫描可能比构建 B+树的查找路径更快,因为索引查找在小数据量时,寻路和回表的开销显得不划算。
- 区分度低(选择性差)的列: 想象一个性别列,只有“男”和“女”两种值。如果对它建立索引,你在查询
-
查询的特性(查询的“意图”)
- 没有利用到索引的最左前缀原则: 复合索引(或称联合索引)如
(col1, col2, col3)
,它的查找是按照col1 -> col2 -> col3
的顺序进行的。如果你只用col2
或col3
来查询,或者查询条件跳过了col1
,那么这个索引可能就无效了,或者只能利用到一部分,无法完全发挥优势。 - 查询条件中使用了函数、表达式、类型转换等:
WHERE YEAR(date_col) = 2023
。对date_col
使用YEAR()
函数,会导致索引失效,因为数据库要先计算出YEAR(date_col)
的值,再进行比较,它无法直接利用date_col
上的有序索引。 - 模糊查询以
%
开头:WHERE name LIKE '%王%'
。这种查询无法利用索引,因为索引是排好序的,但你查询的字符串开头是不确定的,无法从索引的树结构中快速定位。而WHERE name LIKE '王%'
则可以使用索引。 - 使用了
OR
连接索引列和非索引列:WHERE indexed_col = 1 OR non_indexed_col = 2
。通常会使整个查询放弃索引,改为全表扫描,因为优化器可能认为无法同时高效利用两个索引。 - 使用了
!=
或NOT IN
等负向查询: 对于大部分数据,这些负向查询也可能导致索引失效,优化器通常会选择全表扫描。 - 优化器判断全表扫描更快: 这是最核心的一点。MySQL 的查询优化器根据统计信息评估不同执行方案的成本(I/O次数、CPU消耗等)。如果它估算出全表扫描的成本低于走索引再回表的成本,它就会选择全表扫描。这主要发生在数据量小,或者索引区分度非常低时。
- 没有利用到索引的最左前缀原则: 复合索引(或称联合索引)如
如何排查索引效果?——EXPLAIN
命令
EXPLAIN
是 MySQL 提供的一个非常强大的“透视镜”,它能让你看到数据库内部是如何执行你的 SQL 查询的。它会模拟优化器执行 SQL 语句的过程,然后告诉你它将如何处理你的 SQL 语句。
使用方法:
在你的 SQL 查询语句前面加上 EXPLAIN
关键字即可。
EXPLAIN SELECT * FROM users WHERE name = '张三';
执行后,会返回一个表格,其中有很多列,我们重点关注以下几列:
-
id
: SELECT 查询的序列号,越大越优先执行。 -
select_type
: 查询类型,如SIMPLE
(简单查询),PRIMARY
(主查询),SUBQUERY
(子查询)等。 -
table
: 查询的表名。 -
partitions
: 匹配的分区(如果分表)。 -
type
(最重要!):访问类型,这是判断索引使用效率的关键指标。从最好到最差依次是:system
:表只有一行记录(等于系统表),这是最好的。const
:通过唯一索引或主键索引访问单行数据,非常快。eq_ref
:对于每个来自先前的表的记录,从该表中读取一行。常见于 JOIN 语句中使用了主键或唯一索引。ref
:使用非唯一索引查找匹配行的所有行。例如,普通索引的等值查询。range
:索引范围扫描。例如,BETWEEN
,>
,<
,IN
等操作。index
:全索引扫描,只遍历索引树。虽然也用了索引,但扫描了整个索引,效率不如ref
和range
。比ALL
好,因为索引通常比数据行小,且按顺序存储。ALL
(最差!):全表扫描。这意味着没有使用索引或者索引选择性太差,优化器放弃了索引。你需要特别关注并优化它。
-
possible_keys
: MySQL 认为可能用到的索引。 -
key
(重要!):MySQL 实际选择使用的索引。如果为NULL
,表示没有使用索引。 -
key_len
: 实际使用的索引的长度(字节数)。对于复合索引,可以根据这个值判断索引最左前缀原则的使用情况。 -
ref
: 哪一列或常量被用于key
索引查找。 -
rows
(重要!):MySQL 估计要扫描的行数。这个值越小越好。 -
filtered
: 表示存储引擎返回的数据在服务器层过滤后,剩下多少百分比的数据。例如,filtered
为10.00
意味着 90% 的行被过滤掉了。 -
Extra
(重要!):额外信息,包含很多有用的提示。Using filesort
:表示需要对结果进行外部排序(内存或磁盘),性能很差,通常需要优化。Using temporary
:表示使用了临时表来处理查询,性能也较差。Using index
:覆盖索引!这是非常好的情况,查询所需的所有列都包含在索引中,无需回表。Using where
:表示在存储引擎层得到数据后,还需要在服务器层进行条件过滤。Using index condition
:索引条件下推。MySQL 5.6 引入的优化,部分WHERE
条件可以在索引扫描时被下推到存储引擎层进行过滤,减少回表次数。Using join buffer
:表示使用了连接缓冲区(通常在 JOIN 操作中)。
实践排查流程(以 users
表为例)
-
场景一:理想情况(覆盖索引)
EXPLAIN SELECT name, id FROM users WHERE name = '张三';
可能的结果解释(核心列):
id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE users ref idx_name idx_name 402 const 2 Using index type
:ref
—— 很好,通过索引查找。key
:idx_name
—— 实际使用了idx_name
索引。rows
: 2 —— 估计只扫描 2 行,非常高效。Extra
:Using index
—— 完美! 这就是覆盖索引,没有回表。
-
场景二:会回表的情况
EXPLAIN SELECT * FROM users WHERE name = '张三';
可能的结果解释(核心列):
id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE users ref idx_name idx_name 402 const 2 Using where type
:ref
—— 依然通过索引查找,不错。key
:idx_name
—— 实际使用了idx_name
索引。rows
: 2 —— 估计扫描 2 行。Extra
:Using where
—— 注意这里!没有Using index
。虽然用了索引,但因为SELECT *
需要获取name
和id
之外的列,所以会有回表操作。Using where
指的是在存储引擎返回数据后,MySQL 服务器层还需要进行额外的过滤,但在这里更多的是指示它确实需要访问完整行数据。
-
场景三:索引失效(全表扫描)
EXPLAIN SELECT * FROM users WHERE age = 25; -- age 列没有索引
可能的结果解释(核心列):
id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE users ALL NULL NULL NULL NULL 4 Using where type
:ALL
—— 糟糕! 全表扫描,性能最差。possible_keys
:NULL
—— MySQL 压根没觉得有哪个索引可能用得上。key
:NULL
—— 实际没用任何索引。rows
: 4 —— 表里只有 4 行,所以扫描 4 行。如果表是百万千万行,这就是灾难。
EXPLAIN SELECT * FROM users WHERE name LIKE '%三'; -- 模糊查询开头
可能的结果解释(核心列):
id select_type table type possible_keys key key_len ref rows Extra 1 SIMPLE users ALL NULL NULL NULL NULL 4 Using where - 同样是
ALL
,虽然name
列有索引,但因为模糊匹配规则,导致索引失效。
- 索引不是越多越好: 索引会占用磁盘空间,并且在插入、更新、删除数据时,MySQL 需要额外时间来维护这些索引,这会降低写入性能。所以,索引是把双刃剑,要适量且精准。
- 如何选择合适的索引: 考虑查询频率、列的区分度(重复值少说明区分度高),以及是否能形成覆盖索引。
- 数据库优化是一个持续的过程: 业务需求和数据分布是会变化的。之前有效的索引可能随着时间推移变得无效,反之亦然。所以,定期使用
EXPLAIN
检查慢查询,是 DBA 和开发者的日常工作。 - 统计信息的重要性: MySQL 优化器会根据表的统计信息(如行数、列的基数即唯一值的数量、数据分布等)来决定是否使用索引、使用哪个索引。如果统计信息不准确,优化器可能会做出错误判断。你可以通过
ANALYZE TABLE
命令来更新表的统计信息。
区分度低数据量低造成索引更慢的原因
核心论点:I/O模式的本质区别
-
全表扫描的魅力:顺序 I/O
- 在磁盘上: 数据库表的数据在物理存储上是按照主键(或其他某种规则)连续存放的(聚簇索引的特性)。全表扫描就像是磁头从磁盘的起始位置开始,不间断地往后读取,直到表的末尾。这种顺序读取对机械硬盘来说是最快的,因为它避免了磁头频繁寻找不同位置的开销。对于固态硬盘(SSD)来说,虽然随机I/O的惩罚没有机械硬盘那么大,但顺序I/O依然能更好地利用其内部并行读取的特性,并且通常带来更少的CPU开销和更高的缓存命中率。
- 在内存中: 当数据被加载到内存(缓冲池)中时,顺序扫描意味着 CPU 能够沿着内存地址连续地访问数据。这种访问模式对 CPU 的缓存(L1, L2, L3 cache)非常友好,可以预取数据,大大提高处理效率。
-
回表操作的痛点:随机 I/O
- 二级索引的叶子节点:
(索引值, 主键ID)
- 当你通过二级索引查找到一批主键ID(例如
ID=1, ID=500, ID=10, ID=2000, ID=5
)时,这些主键ID在主表(聚簇索引)中的物理位置是高度分散且无序的。主键1
可能在磁盘块 A,5
在磁盘块 B,10
在磁盘块 C,等等。 - 磁盘寻道: 数据库为了获取这些完整的数据行,就需要根据每个主键ID,让磁盘磁头“跳”到不同的物理位置去读取对应的数据页。每一次“跳跃”都是一次昂贵的随机 I/O。
- 大量随机 I/O 的叠加: 如果需要回表的数据行很多(比如占总行数的30%以上,这是个经验值,不同数据库和硬件环境下阈值不同),那么这些零散的随机I/O累积起来的开销,就可能远远超过一次性顺序扫描全表的开销。
- 二级索引的叶子节点:
为什么区分度低和数据量小会强化这个效应?
1. 区分度低(选择性差的列):
- 当你在
gender
这种低区分度列上查询时,即使使用了索引,它会返回大量的主键ID(例如,一半的数据都是男性,就会返回表中一半的ID)。 - 这意味着,数据库不得不执行大量的回表操作。而这些回表操作是随机I/O。
- 此刻,优化器会比较:
- 方案一: 大量的随机I/O(回表),加上最初扫描二级索引的开销。
- 方案二: 一次性的顺序I/O(全表扫描)。
- 在多数情况下,当回表数据量超过一定比例时,优化器会觉得“与其跳来跳去,不如规规矩矩从头到尾读一遍更省事”,于是选择了全表扫描。
2. 数据量小:
- 当表中的总数据量很小(例如几千上万行,几十MB到几百MB),整个表的数据(包括主键索引和所有列的值)很可能轻松地被载入到内存中的缓冲池里。
- 一旦数据都在内存里,无论是全表扫描还是索引查找,都变成了内存操作。
- 内存中的顺序访问优势: 即使是内存,线性地、连续地访问数据(全表扫描)通常比跳跃式地、随机地访问(索引查找和回表的内存版本)效率更高,尤其能更好地利用CPU缓存。
- 消除I/O优势: 索引的主要优势在于减少磁盘I/O。但当数据都在内存里时,磁盘I/O的瓶颈就不存在了。此时,索引的“寻路”和“跳转”的逻辑开销,可能反而比简单的内存线性扫描更大。
总结你的观点:
是的,你可以精确地认为,区分度低和数据量小的情况导致索引效率低下,甚至不如全表扫描的根本原因,就在于:
- 对于区分度低的查询: 索引虽然能快速定位主键ID,但由于符合条件的ID太多,会触发大量随机回表I/O,这个总成本可能高于一次性的顺序全表扫描I/O。
- 对于数据量小的表: 整个表的数据可能已经被加载到内存中。此时,全表扫描是高效的顺序内存访问,而索引的结构化查找和随后的回表操作(即使也是内存访问),其逻辑跳转和随机内存访问的开销,反而可能显得不划算。在这种情况下,索引减少I/O的优势不复存在。
理解I/O模式(顺序 vs 随机)以及内存与磁盘的性能差异,是理解数据库性能优化的核心基石。