SQL常见索引失效导致慢查询情况
数据库索引(尤其是B+树结构的索引)的作用类似于字典的目录,它需要是有序的。任何破坏这种有序性的操作,都可能使数据库优化器放弃使用索引,转而进行更低效的全表扫描。
索引失效的场景:
- 联合索引不满足最左匹配原则。
- 模糊查询最前面的为不确定匹配字符。
- 索引列参与了运算。
- 索引列使用了函数。
- 索引列存在类型转换。
- 索引列使用 is not null 查询。
1. 联合索引不满足最左匹配原则
原理:
联合索引(复合索引)是多个列上共同构建的一个索引,例如 INDEX idx_name_age (name, age)
。这个索引的先按name
排序,在name
相同的情况下再按age
排序。
最左匹配原则是指,查询条件必须从联合索引的最左列开始,并且不能跳过中间的列,才能充分利用该索引。
失效示例:
-- 表结构: user表,有联合索引 idx_name_age (name, age)-- 情况1:缺少最左列 `name`
SELECT * FROM user WHERE age = 25;-- 情况2:跳过了中间列 (假设索引是 (a, b, c))
SELECT * FROM user WHERE a = 'foo' AND c = 'bar'; -- 跳过了b,只能用a的部分索引
为什么失效?
因为索引的排序规则是先name
后age
。如果你只用age
来查询,age
在整个索引树里是无序的(因为先按name
分了组),数据库无法利用索引的有序性进行快速查找,只能全表扫描。
特殊情况 - 索引下推(ICP):
在现代数据库(如MySQL 5.6+)中,即使跳过了中间列,如 WHERE a = 'foo' AND c = 'bar'
,数据库仍然会使用索引(a)
进行初步筛选(a = 'foo'
),然后再进行c = 'bar'
的判断。这比完全不用索引好,但不如完全匹配所有列高效。
2. 模糊查询最前面的为不确定匹配字符
原理:
索引的有序性是基于列值的完整内容。如果查询条件的开头是通配符,就无法利用索引的有序性。
失效示例:
-- 假设在 `name` 字段上有索引
SELECT * FROM user WHERE name LIKE '%三'; -- 以'%'开头,索引失效
SELECT * FROM user WHERE name LIKE '%三%'; -- 以'%'开头,索引失效-- 不会失效的案例:
SELECT * FROM user WHERE name LIKE '张%'; -- 以确定字符开头,索引有效
为什么失效?
LIKE '张%'
可以转化为范围查询(name >= '张' AND name < '丈'
),可以利用索引的有序性快速定位。而LIKE '%三'
要求查询所有以'三'结尾的值,这在索引树中是完全无序的,只能遍历所有数据。
3. 索引列参与了运算
原理:
索引中存储的是列的原始值。如果对列进行运算,数据库优化器无法将运算后的条件与索引中的原始值直接对应。
失效示例:
-- 假设在 `age` 字段上有索引
SELECT * FROM user WHERE age + 1 = 21; -- 索引失效
SELECT * FROM user WHERE YEAR(create_time) = 2023; -- 对列使用函数,也属于此类
为什么失效?
优化器不知道如何将 age + 1
这个条件反向计算出 age = 20
,从而去索引里查找。它只能计算表中每一行的age + 1
的值,然后判断是否等于21,这必然导致全表扫描。
解决方法:
将运算移到等号另一边,使用列的原生值。
SELECT * FROM user WHERE age = 20; -- 有效
4. 索引列使用了函数
原理:
这与“参与运算”的原理完全相同。索引存储的是原始值,而不是函数计算后的值。
失效示例:
-- 假设在 `create_time` (datetime类型) 字段上有索引
SELECT * FROM orders WHERE DATE(create_time) = '2023-10-26'; -- 索引失效
SELECT * FROM user WHERE LENGTH(name) = 2; -- 索引失效
为什么失效?
优化器无法通过 DATE(create_time) = '2023-10-26'
这个条件去索引树里查找,因为索引树是按 create_time
的原始值排序的,而不是按 DATE(create_time)
排序的。
解决方法:
使用范围查询代替函数。
SELECT * FROM orders
WHERE create_time >= '2023-10-26 00:00:00'
AND create_time < '2023-10-27 00:00:00'; -- 索引有效
5. 索引列存在类型转换
原理:
当操作符两边的数据类型不一致时,数据库会进行隐式类型转换(Implicit Cast),以使它们可以比较。这个转换过程相当于对列使用了函数。
失效示例:
-- 假设 `phone` 字段是字符串类型(varchar),并且有索引
SELECT * FROM user WHERE phone = 13800138000; -- 数据库会将phone列转为数字,索引失效-- 假设 `id` 字段是整数类型(int),并且有索引
SELECT * FROM user WHERE id = '123'; -- 数据库会将字符串'123'转为数字,索引仍然有效(原因看下方)
为什么失效?
WHERE string_column = number
会导致数据库调用 CAST(string_column AS SIGNED)
函数,将字符串列转为数字,这就回到了第4点“对列使用函数”,导致索引失效。
注意:
WHERE int_column = ‘123’
通常不会导致索引失效,因为数据库会将字符串常量 ‘123’
转换为数字 123
,而不是对int_column
列进行操作。关键是看转换是否作用于索引列本身。
6. 索引列使用 is not null 查询
原理:
这个情况相对复杂,是否失效取决于数据库优化器的成本计算和表的具体情况。
单列索引:在很多情况下,使用
IS NOT NULL
是可以使用索引的,尤其是当索引本身具有高选择性(即NULL值很少)时。数据库可以通过遍历索引来跳过所有为NULL的条目。联合索引:如果联合索引的最左列查询条件是
IS NOT NULL
,由于其选择性太差(不符合最左匹配的精确查询),优化器可能认为全表扫描比回表查询大量数据更快,从而选择不使用索引。
总结:
IS NOT NULL
不一定会导致索引失效。优化器会评估使用索引的成本(需要回表的行数)和全表扫描的成本。如果表中大多数值都是NULL,用索引很快。如果几乎都不是NULL,那么用索引需要回表几乎所有的行,可能不如直接全表扫描。
失效示例(可能发生的情况):
-- 假设在 `address` 字段上有索引,但99%的记录都不为NULL
SELECT * FROM user WHERE address IS NOT NULL; -- 优化器可能选择全表扫描
总结与记忆技巧
可以将索引想象成一本按【姓氏+名字】排序的通讯录。
最左匹配:你无法快速找出所有叫“伟”的人,因为通讯录是先按“姓”排序的。
模糊前缀:你无法快速找出所有姓氏以“王”字结尾的人。
列运算/函数:你想找“姓氏长度等于1”的人,通讯录的目录帮不了你,你得自己翻。
类型转换:你把要找的“中文姓名”用拼音报出来,目录也无法直接匹配。
IS NOT NULL:你想找出所有“电话号码不为空”的人。如果只有少数人没电话,用目录很快(跳过没电话的)。如果人人都有电话,用目录反而慢(得把所有人的号码看一遍),不如直接按顺序翻名单。