当前位置: 首页 > news >正文

Mysql索引失效问题及其原因

MySQL 中使用索引不一定有效。索引好比字典的目录,用对了能快速找到字,用错了或者没必要用,反而可能拖慢速度。你需要“侦探工具”——EXPLAIN 命令,才能看清你的查询是不是真的用了索引,以及用得好不好。

为什么说索引不一定有效?

这里我们要抛开“索引万能”的误区。索引的本质是空间换时间,它通过额外的存储空间,牺牲一些写入性能(因为每次增删改查都要维护索引),来提高查询性能。但这个“提高”是有条件的。

索引就像一把瑞士军刀,功能很多,但你不能指望它在所有场合都发挥作用。

我们可以从几个角度来探讨:

  1. 数据的特性(数据的“样貌”)

    • 区分度低(选择性差)的列: 想象一个性别列,只有“男”和“女”两种值。如果对它建立索引,你在查询 gender = '男' 时,依然需要扫描表中近一半的数据(假设男女比例均衡)。这种情况下,全表扫描可能比走索引再回表更高效,因为全表扫描是顺序 I/O,而回表引入大量随机 I/O。MySQL 优化器通常会足够聪明地选择不走索引。
    • 经常更新的列: 索引的维护成本很高。每次对索引列的数据进行增删改,都需要调整索引结构。如果一个列频繁更新,索引的维护成本可能会超过查询带来的收益。
    • 数据量太小: 如果表里只有几百甚至几千条记录,全表扫描可能比构建 B+树的查找路径更快,因为索引查找在小数据量时,寻路和回表的开销显得不划算。
  2. 查询的特性(查询的“意图”)

    • 没有利用到索引的最左前缀原则: 复合索引(或称联合索引)如 (col1, col2, col3),它的查找是按照 col1 -> col2 -> col3 的顺序进行的。如果你只用 col2col3 来查询,或者查询条件跳过了 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 = '张三';

执行后,会返回一个表格,其中有很多列,我们重点关注以下几列:

  1. id: SELECT 查询的序列号,越大越优先执行。

  2. select_type: 查询类型,如 SIMPLE (简单查询), PRIMARY (主查询), SUBQUERY (子查询)等。

  3. table: 查询的表名。

  4. partitions: 匹配的分区(如果分表)。

  5. type最重要!):访问类型,这是判断索引使用效率的关键指标。从最好到最差依次是:

    • system:表只有一行记录(等于系统表),这是最好的。
    • const:通过唯一索引或主键索引访问单行数据,非常快。
    • eq_ref:对于每个来自先前的表的记录,从该表中读取一行。常见于 JOIN 语句中使用了主键或唯一索引。
    • ref:使用非唯一索引查找匹配行的所有行。例如,普通索引的等值查询。
    • range:索引范围扫描。例如,BETWEEN, >, <, IN 等操作。
    • index:全索引扫描,只遍历索引树。虽然也用了索引,但扫描了整个索引,效率不如 refrange。比 ALL 好,因为索引通常比数据行小,且按顺序存储。
    • ALL最差!):全表扫描。这意味着没有使用索引或者索引选择性太差,优化器放弃了索引。你需要特别关注并优化它。
  6. possible_keys: MySQL 认为可能用到的索引。

  7. key重要!):MySQL 实际选择使用的索引。如果为 NULL,表示没有使用索引。

  8. key_len: 实际使用的索引的长度(字节数)。对于复合索引,可以根据这个值判断索引最左前缀原则的使用情况。

  9. ref: 哪一列或常量被用于key索引查找。

  10. rows重要!):MySQL 估计要扫描的行数。这个值越小越好。

  11. filtered: 表示存储引擎返回的数据在服务器层过滤后,剩下多少百分比的数据。例如,filtered10.00 意味着 90% 的行被过滤掉了。

  12. Extra重要!):额外信息,包含很多有用的提示。

    • Using filesort:表示需要对结果进行外部排序(内存或磁盘),性能很差,通常需要优化。
    • Using temporary:表示使用了临时表来处理查询,性能也较差。
    • Using index覆盖索引!这是非常好的情况,查询所需的所有列都包含在索引中,无需回表。
    • Using where:表示在存储引擎层得到数据后,还需要在服务器层进行条件过滤。
    • Using index condition索引条件下推。MySQL 5.6 引入的优化,部分 WHERE 条件可以在索引扫描时被下推到存储引擎层进行过滤,减少回表次数。
    • Using join buffer:表示使用了连接缓冲区(通常在 JOIN 操作中)。

实践排查流程(以 users 表为例)

  1. 场景一:理想情况(覆盖索引)

    EXPLAIN SELECT name, id FROM users WHERE name = '张三';
    

    可能的结果解释(核心列):

    idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
    1SIMPLEusersrefidx_nameidx_name402const2Using index
    • type: ref —— 很好,通过索引查找。
    • key: idx_name —— 实际使用了 idx_name 索引。
    • rows: 2 —— 估计只扫描 2 行,非常高效。
    • Extra: Using index —— 完美! 这就是覆盖索引,没有回表。
  2. 场景二:会回表的情况

    EXPLAIN SELECT * FROM users WHERE name = '张三';
    

    可能的结果解释(核心列):

    idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
    1SIMPLEusersrefidx_nameidx_name402const2Using where
    • type: ref —— 依然通过索引查找,不错。
    • key: idx_name —— 实际使用了 idx_name 索引。
    • rows: 2 —— 估计扫描 2 行。
    • Extra: Using where —— 注意这里!没有 Using index。虽然用了索引,但因为 SELECT * 需要获取 nameid 之外的列,所以会有回表操作。Using where 指的是在存储引擎返回数据后,MySQL 服务器层还需要进行额外的过滤,但在这里更多的是指示它确实需要访问完整行数据。
  3. 场景三:索引失效(全表扫描)

    EXPLAIN SELECT * FROM users WHERE age = 25; -- age 列没有索引
    

    可能的结果解释(核心列):

    idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
    1SIMPLEusersALLNULLNULLNULLNULL4Using where
    • type: ALL —— 糟糕! 全表扫描,性能最差。
    • possible_keys: NULL —— MySQL 压根没觉得有哪个索引可能用得上。
    • key: NULL —— 实际没用任何索引。
    • rows: 4 —— 表里只有 4 行,所以扫描 4 行。如果表是百万千万行,这就是灾难。
    EXPLAIN SELECT * FROM users WHERE name LIKE '%三'; -- 模糊查询开头
    

    可能的结果解释(核心列):

    idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
    1SIMPLEusersALLNULLNULLNULLNULL4Using where
    • 同样是 ALL,虽然 name 列有索引,但因为模糊匹配规则,导致索引失效。
  • 索引不是越多越好: 索引会占用磁盘空间,并且在插入、更新、删除数据时,MySQL 需要额外时间来维护这些索引,这会降低写入性能。所以,索引是把双刃剑,要适量且精准。
  • 如何选择合适的索引: 考虑查询频率、列的区分度(重复值少说明区分度高),以及是否能形成覆盖索引。
  • 数据库优化是一个持续的过程: 业务需求和数据分布是会变化的。之前有效的索引可能随着时间推移变得无效,反之亦然。所以,定期使用 EXPLAIN 检查慢查询,是 DBA 和开发者的日常工作。
  • 统计信息的重要性: MySQL 优化器会根据表的统计信息(如行数、列的基数即唯一值的数量、数据分布等)来决定是否使用索引、使用哪个索引。如果统计信息不准确,优化器可能会做出错误判断。你可以通过 ANALYZE TABLE 命令来更新表的统计信息。

区分度低数据量低造成索引更慢的原因

核心论点:I/O模式的本质区别

  1. 全表扫描的魅力:顺序 I/O

    • 在磁盘上: 数据库表的数据在物理存储上是按照主键(或其他某种规则)连续存放的(聚簇索引的特性)。全表扫描就像是磁头从磁盘的起始位置开始,不间断地往后读取,直到表的末尾。这种顺序读取对机械硬盘来说是最快的,因为它避免了磁头频繁寻找不同位置的开销。对于固态硬盘(SSD)来说,虽然随机I/O的惩罚没有机械硬盘那么大,但顺序I/O依然能更好地利用其内部并行读取的特性,并且通常带来更少的CPU开销和更高的缓存命中率。
    • 在内存中: 当数据被加载到内存(缓冲池)中时,顺序扫描意味着 CPU 能够沿着内存地址连续地访问数据。这种访问模式对 CPU 的缓存(L1, L2, L3 cache)非常友好,可以预取数据,大大提高处理效率。
  2. 回表操作的痛点:随机 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 随机)以及内存与磁盘的性能差异,是理解数据库性能优化的核心基石。

http://www.dtcms.com/a/308601.html

相关文章:

  • 【Leetcode】2683. 相邻值的按位异或
  • 五、cv::SparseMat的介绍和使用
  • 是采用示例模板,还是采用json的结构化数据,哪种方式会让llm的输出更加稳定?
  • 查询目前服务器所占的带宽的命令(上传和下载)
  • CNN卷积神经网络之LeNet和AlexNet经典网络模型(三)
  • c语言的编译链接
  • Winform PathGradientBrush类使用
  • C#程序本地运行正常,通过网络下载报错:FileLoadException:“未能加载文件或程序集“xxx.dll”或它的某一个依赖项。
  • 【pycharm的使用】
  • Qwen3-30B-A3B-Thinking-2507 推理模型深度评测
  • 250721脑电分析课题进展——手工特征总结
  • lsof命令
  • SSO面临的问题
  • 为什么有时神经元会输出类似(甚至一样)?
  • 行业分享丨从工具应用到体系进化:东风商用车仿真体系建设与实践
  • 【源力觉醒 创作者计划】文心一言与deepseek集成springboot开发哪个更方便
  • 【力扣】面试经典150题总结01-数组/字符串
  • Dev-C++ 6.3 安装与使用指南:适合新手的C/C++编程工具
  • Allegro实用技巧-Snap-命令行移动
  • Android端RTMP低延迟播放器在工业与智能场景下的架构与落地
  • MySQL 中 CHAR 和 VARCHAR 类型有什么区别?
  • 一次性接收大量上传图片,后端优化方式
  • 【Git】Git 实战:完整拉取项目所有分支和标签,切换远程仓库,解决保护分支推送冲突
  • Linux Flathub软件管理方法 使用指南
  • 搭建个人博客
  • 决策树实现回归任务
  • 利用可观测性进行高效故障治理:从预防到改进的完整实践
  • 从Excel到工时管理系统:企业如何选择更高效的工时记录工具?
  • 第二十九章:AI的“原子与批次”:高维数据表示与操作精炼【总结前面(1)】
  • Windows 安全中心是什么?如何关闭 Windows 11 的安全中心