在 MySQL 性能优化中,索引失效是最常见的 "性能杀手"。当精心设计的索引突然无法被查询使用,轻则导致慢查询,重则引发数据库负载飙升。
MySQL 的 InnoDB 引擎默认使用 B+树 结构存储索引。理解其特性是避免失效的关键:
有序性: B+树中的数据(索引键值)是有序存储的(根据创建索引时列的顺序)。
最左匹配: 对于复合索引(多列索引),查询条件必须从索引的最左边列开始并且连续地(或部分连续地)使用索引中的列,才能有效利用索引。这是索引失效最常见的原因之一。
范围列阻断: 如果查询条件中对索引中的某一列使用了范围查询(>
, <
, BETWEEN
, LIKE 'prefix%'
等),那么它右侧的所有索引列将无法再使用索引进行检索(但可能用于排序或覆盖索引)。
前缀匹配: 对于字符串类型的索引(CHAR
, VARCHAR
, TEXT
),索引存储的是列值的前缀(长度由索引定义决定)。查询条件也必须匹配这个前缀才能有效利用索引。
问题: 复合索引 idx_name_age (name, age)
。
失效查询 1: SELECT * FROM users WHERE age = 25;
(跳过了最左列 name
)
失效查询 2: SELECT * FROM users WHERE name LIKE '%张%' AND age = 25;
(虽然用了 name
,但 LIKE
以通配符开头导致 name
索引失效,进而 age
也无法利用索引)
原因: B+树首先按 name
排序,在 name
相同的情况下再按 age
排序。直接查询 age
或 name
无法形成有效前缀匹配,数据库无法定位到有序的起始点。
解决方案:
确保查询条件包含复合索引的最左列。
根据查询模式调整索引列的顺序,将最常用作查询条件的列放在左边。
如果必须单独查询非最左列,考虑为其单独创建索引。
问题:
计算: SELECT * FROM orders WHERE YEAR(order_date) = 2023;
(索引 idx_order_date (order_date)
)
函数: SELECT * FROM users WHERE LOWER(name) = 'john doe';
(索引 idx_name (name)
)
隐式类型转换: phone
列是 VARCHAR
,索引 idx_phone (phone)
, 查询 SELECT * FROM contacts WHERE phone = 13800138000;
(数字被隐式转为字符串,可能导致索引失效,取决于 MySQL 版本和设置)
原因: 索引存储的是列的原始值。当对列应用计算、函数或发生类型转换时,MySQL 无法直接使用存储的索引值,它必须先计算/转换每一行的值,然后再进行比较。这等同于没有索引。
解决方案:
避免在索引列上使用函数或计算。 将操作移到常量端:
计算: SELECT * FROM orders WHERE order_date >= '2023-01-01' AND order_date < '2024-01-01';
(利用范围查询,只要 order_date
是索引且范围查询是有效的)
函数: 如果必须使用函数且无法避免,考虑使用函数索引(MySQL 5.7+ 支持虚拟列+索引,MySQL 8.0+ 直接支持函数索引 CREATE INDEX idx_lower_name ON users ((LOWER(name)));
)。
避免隐式类型转换。 确保查询条件中的类型与列定义严格匹配:
SELECT * FROM contacts WHERE phone = '13800138000';
(字符串常量)
问题: SELECT * FROM products WHERE status != 'inactive';
(索引 idx_status (status)
)
原因: 不等于操作是一个非常宽泛的条件。即使 status
有索引,数据库通常需要扫描索引中所有 'inactive'
和非 'inactive'
的条目。优化器可能会判断扫描大部分索引页不如直接全表扫描更快,尤其是在 'inactive'
记录占比较小或较大的情况下。不等于操作不一定总是导致索引失效,但失效风险极高,尤其是在表较大或选择性不高时。
解决方案:
尽量避免使用 !=
/ <>
。考虑用其他方式重写:
明确列出等于的值: SELECT * FROM products WHERE status IN ('active', 'pending');
(如果状态值是离散且有限的)
使用 OR
: SELECT * FROM products WHERE status = 'active' OR status = 'pending';
(效果类似 IN
)
如果业务逻辑允许,使用 >
, <
或 BETWEEN
等范围查询可能更有效。
如果无法避免,评估数据分布。如果 !=
的值占比非常小(例如 status != 'deleted'
且 'deleted'
记录很少),索引 可能 被使用。
LIKE
以通配符 %
开头问题: SELECT * FROM articles WHERE content LIKE '%database%';
(索引 idx_content (content)
)
原因: B+树索引是按列值从头到尾排序的。以 %
开头的 LIKE
查询意味着“包含任意前缀的字符串”,这破坏了索引的有序性,数据库无法定位查找的起点,必须扫描所有条目。LIKE 'database%'
可以利用索引(最左前缀匹配),LIKE '%database'
则不行。
解决方案:
尽量避免以 %
开头的模糊查询。 考虑是否能用后缀索引(MySQL 8.0+ 反转字符串+索引)或全文索引(FULLTEXT
)替代。
如果必须使用,确保查询有足够的选择性(返回结果集很小),否则性能代价很高。
使用专门的全文搜索引擎(如 Elasticsearch)处理复杂的全文搜索需求。
OR
连接非索引列条件问题: SELECT * FROM employees WHERE dept_id = 10 OR salary > 5000;
(索引 idx_dept (dept_id)
, salary
列无索引)
原因: MySQL 在处理 OR
条件时,通常只能为 dept_id = 10
使用索引 idx_dept
。对于 salary > 5000
,因为没有索引,必须进行全表扫描。最终,MySQL 需要将使用索引查到的结果集和全表扫描的结果集合并去重。优化器通常会认为这种“部分索引+部分全表扫描再合并”的成本高于直接全表扫描,从而导致全表扫描。
解决方案:
为 OR
连接的所有列创建单独的索引或包含它们的复合索引。例如,创建索引 idx_salary (salary)
或 idx_dept_salary (dept_id, salary)
。有了 idx_salary
,MySQL 可能 会使用 idx_dept
和 idx_salary
分别查找,然后做 UNION
(索引合并优化 Index Merge,通常是 type=index_merge
, Extra=Using union(...); Using where
),但这不一定比全表扫描快,取决于数据分布和选择性。
更可靠的方法是重写查询为 UNION
:
SELECT * FROM employees WHERE dept_id = 10
UNION
SELECT * FROM employees WHERE salary > 5000;
这样,第一个子查询可以利用 idx_dept
,第二个子查询如果 salary
有索引 idx_salary
就能利用。确保每个 SELECT
都能有效利用索引。
评估是否能用 IN
替代某些 OR
(当 OR
作用在同一列的不同值时)。
IS NULL
或 IS NOT NULL
问题: SELECT * FROM customers WHERE email IS NULL;
(索引 idx_email (email)
)
原因: 在 MySQL 中,IS NULL
通常可以利用索引(ref
或 range
访问),因为索引会记录 NULL
值。IS NOT NULL
则不一定。优化器需要评估表中 NULL
和非 NULL
值的比例。如果表中大部分记录 email IS NOT NULL
,扫描整个索引的成本可能接近全表扫描,优化器可能选择全表扫描。如果 email
定义为 NOT NULL
,则 IS NULL
条件永远为假,查询可能非常快但用不到索引(因为不需要查)。
解决方案:
对于 IS NULL
:通常可以利用索引,无需特别处理。
对于 IS NOT NULL
:
如果该列允许 NULL
但实际 NULL
值非常少(例如,记录是否删除的标志位 deleted_at IS NOT NULL
表示有效数据),索引很可能被使用。
如果 NULL
值很多,优化器可能选择全表扫描。考虑是否需要改变表设计(减少 NULL
列)或使用其他查询方式。
确保列上有索引。
问题: 表 A (utf8mb4)
和表 B (latin1)
通过 varchar
列 name
连接,且 A.name
和 B.name
上都有索引。
SELECT * FROM A JOIN B ON A.name = B.name;
原因: 当两个连接列的字符集(CHARACTER SET
)或排序规则(COLLATION
)不同时,MySQL 需要先对其中一个列的值进行转换,才能进行比较。这种转换会使该列上的索引失效。
解决方案:
统一字符集和排序规则! 这是最根本的解决方案。在设计数据库时,确保相关联的列使用相同的字符集和排序规则(utf8mb4
和 utf8mb4_0900_ai_ci
是当前推荐选择)。
如果无法修改表结构,可以在 ON
子句中使用 CONVERT()
函数强制转换,但这通常也会导致索引失效,应尽量避免。例如: ON A.name = CONVERT(B.name USING utf8mb4)
。
问题: 表很小,或者查询条件匹配了表中绝大部分记录(低选择性)。
原因: 使用索引需要额外的 I/O 操作(读取索引页+回表读取数据页)。如果优化器预判需要扫描的数据行超过总行数的一定比例(通常认为是 20%-30%,但实际很复杂),它可能会认为直接顺序读取整个数据文件(全表扫描)比通过索引随机读取数据页更快,因为顺序 I/O 效率远高于随机 I/O。
解决方案:
这是优化器的合理行为,通常不需要强制使用索引。
如果确信索引更优(例如,数据分布不均匀,优化器统计信息过时),可以使用 FORCE INDEX (index_name)
提示强制使用特定索引,但要谨慎使用,并监控效果。更新统计信息 (ANALYZE TABLE tablename;
) 有时能帮助优化器做出正确决策。
考虑是否需要该查询返回如此大量的数据,是否能通过更精确的条件或分页 (LIMIT
) 减少结果集。
EXPLAIN
是分析 SQL 执行计划、判断索引使用情况的神器。在 SQL 语句前加上 EXPLAIN
或 EXPLAIN FORMAT=JSON
即可。
关键字段解读:
type
: 访问类型,性能从优到劣常见有:
const
/ system
: 通过主键或唯一索引查到一行。
eq_ref
: 关联查询,使用主键或唯一索引进行等值匹配。
ref
: 使用普通索引等值匹配。
range
: 使用索引进行范围扫描 (>
, <
, BETWEEN
, LIKE 'prefix%'
)。
index
: 全索引扫描 (按索引顺序读取所有索引条目,通常比 ALL
好点)。
ALL
: 全表扫描! 索引可能失效或未使用。这是需要重点优化的信号。
possible_keys
: 查询可能使用到的索引。
key
: 查询实际使用的索引。如果是 NULL
,则未使用索引。
key_len
: 实际使用的索引长度(字节数)。可用于判断复合索引使用了多少部分。
rows
: MySQL 预估需要扫描的行数(不是精确值)。越小越好。
Extra
: 额外信息,包含重要提示:
Using where
: 在存储引擎检索行后,服务器层进行了额外的过滤。
Using index
: 覆盖索引!查询所需数据仅从索引中即可获得,无需回表。性能极佳。
Using temporary
: 使用了临时表,通常发生在排序 (ORDER BY
) 或分组 (GROUP BY
) 且无法利用索引排序时。
Using filesort
: 使用了文件排序(外部排序),无法利用索引排序。需要优化 ORDER BY
。
Using join buffer (Block Nested Loop)
: 关联查询未使用索引,使用了连接缓冲区。性能较差。
使用步骤:
在需要分析的 SELECT
语句前加上 EXPLAIN
。
执行 EXPLAIN ...
语句。
分析结果,重点关注 type
, key
, rows
, Extra
。
如果发现 type=ALL
, key=NULL
或 Using filesort
, Using temporary
,说明存在性能问题,需要优化索引或 SQL。
理解最左前缀: 设计和使用复合索引时牢记此原则。
避免计算/函数/类型转换: 保持索引列在查询条件中的“纯洁性”。
慎用 !=
, <>
, 开头 %
的 LIKE
: 评估数据分布,寻找替代方案。
谨慎处理 OR
: 优先考虑 UNION
或确保所有 OR
条件列都有合适索引。
统一字符集: 确保关联列字符集一致。
善用覆盖索引: 只查询索引包含的列 (SELECT col1, col2 FROM ... WHERE ...
,其中 (col1, col2)
是索引),避免回表。
合理设计索引:
只为查询频繁、过滤性好的列创建索引。
避免创建过多索引,影响写性能。
考虑使用前缀索引 (INDEX (column_name(length))
) 节省空间,但注意选择合适的前缀长度以保证选择性。
定期使用 EXPLAIN: 分析关键查询的执行计划。
更新统计信息: 定期运行 ANALYZE TABLE
(尤其在大量数据变更后),帮助优化器做出准确判断。
考虑索引下推 (Index Condition Pushdown, ICP): MySQL 5.6+ 支持,允许在存储引擎层利用索引过滤更多数据,减少回表。通常 EXPLAIN
显示 Using index condition
表示启用。确保 MySQL 版本支持并启用。
当遇到索引失效问题时,可按以下步骤排查: