MySQL 索引选错的情况

本文介绍了MySQL在查询优化中如何可能误选索引,包括范围过大导致的name索引误用,以及orderby和limit条件下索引选择失误。提供了针对这些问题的解决策略,如强制索引、联合索引和调整查询语句。

前言

对于数据量大的表进行查询,为表建立索引从而加快查询速度是一种常见优化方式,但建立了索引后,有时 MySQL 的查询速度还是不如人意,主要原因可能是 MySQL 并没有选择正确的索引进行查询,在这里给出一些常见的 MySQL 选错索引的情况。

测试环境

MySQL 5.7

初始化表的脚本

表中有 id, name, ctime 三个子段, 其中id为主键, ctime和name都建了普通索引。
然后往表中插入500万数据,各条数据的ctime依次递增1,name为"aaa"、 “bbb”、 “ccc”、 “ddd”、 “eee”、 “fff”的数据各占10%,为"ggg" 的占 30%

DROP PROCEDURE IF EXISTS process_test;
DROP TABLE IF EXISTS temp_table;

CREATE TABLE temp_table (
    `id` BIGINT(20) unsigned NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
    `ctime` BIGINT(20) unsigned NOT NULL DEFAULT '0',
    PRIMARY KEY (`id`),
    KEY `idx_ctime` (`ctime`),
    KEY `idx_name` (`name`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

DELIMITER ;;
CREATE PROCEDURE `process_test`()
BEGIN
  declare n int;
  declare now_time BIGINT;
  set n = 1;
  set now_time = 1666488629;
  while n <= 5000000 do
	if n % 10 = 0 THEN
  		insert into temp_table(name, ctime) values("aaa", now_time+n);
	ELSEIF MOD(n, 10) = 1 THEN
		insert into temp_table(name, ctime) values("bbb", now_time+n);
	ELSEIF MOD(n, 10) = 2 THEN
		insert into temp_table(name, ctime) values("ccc", now_time+n);
	ELSEIF MOD(n, 10) = 3 THEN
		insert into temp_table(name, ctime) values("ddd", now_time+n);
	ELSEIF MOD(n, 10) = 4 THEN
		insert into temp_table(name, ctime) values("eee", now_time+n);
	ELSEIF MOD(n, 10) = 5 THEN
		insert into temp_table(name, ctime) values("fff", now_time+n);
	ELSE
		insert into temp_table(name, ctime) values("ggg", now_time+n);
  	END IF;
	set n = n + 1;
  	
  end while;
END;;
DELIMITER ;

CALL process_test();

MySQL选错索引的情况

情况1 查询范围过大

1.1 复现

如下 sql 所示,这两条 sql 的差别仅仅是 ctime 的查询范围,第一条 sql 的 ctime 范围是11万,第二条sql的ctime范围是10万,按理说这两条语句如果用 ctime 索引的话,那么查询时间应该差不多,因为扫描的数据行数仅仅差了1万行。(注意:复现可能并不一定成功,具体的阈值可能因环境不同而不同

select * from temp_table where 1671488600 > ctime and ctime > 1671488600-110000 and name = "aaa";
select * from temp_table where 1671488600 > ctime and ctime > 1671488600-100000 and name = "aaa";

在实际运行后,第一条 sql 运行了接近一分钟,而第二条 sql 则只要一秒钟不到。
请添加图片描述
通过 explain 语句的执行计划,可以发现第一条 sql 居然是用了 name 索引进行查询。从之前的建表语句可以知道 name 为 “aaa” 的数据有100万条。
请添加图片描述
而第二条 sql 使用 ctime 索引进行查询,仅会扫描10万条数据。因此这两条 sql 的耗时差异巨大。
请添加图片描述

1.2 原因

根据 ctime 和 name 来进行查询时,当ctime的时间跨度稍大时,MySQL有可能认为使用 ctime 作为索引过滤掉的数据并不够多,因此改用 name 索引。

1.3 解决方案

  1. 既然已经知道 MySQL 选错了索引,可以直接使用 force index 来强制 MySQL 使用具体哪一个索引

    缺点:当该索引被删除或者重命名后,这个方法会失效,而且具体的阈值也非常难设置。

  2. 添加联合索引 “(ctime, name)” 或者 “(name, ctime)”,具体顺序要具体问题分析,一般查询更频繁以及区分度更高的字段放前面。一般查询都要限定时间范围,而且时间范围的区分度也很高,因为同一时间戳插入的数据相对较少。所以可以考虑新加入一个 “(ctime, name)” 联合索引,然后原来表中的 ctime 这个索引就冗余了,可以删掉。

    缺点:需要对具体业务进行分析,分析出高频和区分度高的查询字段,同时也要避免建立的冗余索引。

  3. 一般查询都会加上 limit n 限制查询结果的行数,这时可以加上 “order by ctime” 让 MySQL 选择 ctime 索引进行查询(需要有limit)。explain 后可以看到,加上 “order by ctime” 和 “limit 100” 确实可以让MySQL选择 ctime 索引进行查询。
    请添加图片描述
    原理:当有 limit 和 order by 时,需要排序然后选出若干行数据,当加上 "order by ctime"后,MySQL判断如果使用 ”ctime“ 索引进行查询可以避免排序带来的花销,因此会选择使用 ctime 这个索引来查询。

    缺点:当查询条件没有 ctime 时,查询耗时可能会更糟糕,因为此时用ctime 作为索引不能过滤任何数据行,下一种情况就是类似的例子。

情况2 使用了 order by 和 limit 对结果进行排序和限制行数

2.1 复现

如下 sql 所示,查询条件有 ctime 同时语句也加上了 limit 来限制获取的数据后行数,各条 sql 的不同之处在于 order by 的字段不同。如果这些 sql 都利用 ctime 索引来查询,耗时应该相当,因为按照不同的字段进行排序的耗时应该是较小的。

select * from temp_table where 1671488600 > ctime and ctime > 1671488600-100000 order by ctime limit 1000;
select * from temp_table where 1671488600 > ctime and ctime > 1671488600-100000 order by id limit 1000;
select * from temp_table where 1671488600 > ctime and ctime > 1671488600-100000 order by name limit 1000;

运行后,可以发现这三条语句的耗时差距很大,第一条 sql 不到0.1秒就执行完成了,而第三条则要半分钟。
在这里插入图片描述

2.2 原因

如情况1中的解决方案3中所说的,加入order by 和 limit 会让 MySQL 选择需要排序的字段的索引来进行查询。对这三条 sql 使用 explain 可以看到,第一条 sql 使用了 ctime 索引进行查询,第二条 sql 使用主键索引进行全表查询,而第三条则使用了 name 索引来进行查询。
请添加图片描述
请添加图片描述
请添加图片描述
耗时分析:

  1. 使用了 ctime 索引,不需要排序,只查了一千条就可以返回结果。
  2. 使用了主键索引全表扫描,需要查到符合条件的一千条结果才能返回,主键索引在查询的条件中完全没有出现,因此主键过滤不掉任何数据。而根据这个查询条件,实际查询的行数应该为490万左右。
  3. 使用了 name 索引全表扫描,需要查到符合条件的一千条结果才能返回。看上去2和3很类似,但为何还有十多倍的时间差距呢?由于 name 是普通索引,因此每条记录都需要回表

2.3 解决方案

  1. 使用 force index。

    缺点:和1.3中的解决方案1相同。

  2. 删掉 order by 的索引,直接避免 MySQL 使用该索引。

    缺点:建的索引一般都是有用的,删除索引会影响别的使用该索引的查询语句。

  3. sql 语句不加order by。

    缺点:改变业务逻辑,有时我们就是需要排序。

  4. 如果确实希望根据对应的字段进行排序,为了避免 MySQL 节约排序的开销,可以增加排序的字段,让MySQL认为使用排序对应的索引无法节约排序开销,例如,将 ”order by name“ 改为 “order by name, ctime” 。

    缺点:一般的业务需求很少要按照两个字段来排序,因此写了两个排序字段比较反直觉,后续维护代码需要添加注释才好理解。

总结

本文总结了一些 MySQL 索引选错的情况、选错的原因以及对应的解决方案,从这些情况可以看到,MySQL 确实很容易选错索引,而且这些情况其实还是比较容易出现的,很多时候我们也很难在写 sql 的时候就直接发现这些问题。

很多时候,我们能做的就是在发现有慢查询的 sql 后,检查这条 sql 为什么会慢,以上列出的一些情景,希望可以帮助读者以后遇到类似问题后,可以更快的解决问题。至于写 sql 时就预先发现问题,还是一件比较困难的事情,暂时还不属于本文的范畴。

<think>嗯,用户想解决MySQL查询优化器选择错误索引问题。首先我得回忆一下MySQL是怎么选择索引的。根据引用[1],优化器是基于成本模型来选择索引的,但有时候它可能选错,导致查询变慢。用户需要知道具体的方法和建议来纠正这种情况。 首先,可能要考虑强制使用索引。我记得FORCE INDEX或者USE INDEX可以在查询中指定索引,但用户需要知道具体的语法和适用场景。不过要提醒他们不能滥用,因为索引可能随着数据变化而不再合适。 然后,优化统计信息也很重要。ANALYZE TABLE可以更新统计信息,这可能会帮助优化器做出更好的选择。引用[3]提到key_len显示使用的键长度,统计信息不准确的话,key_len可能有问题,导致优化器误判。 接下来是调整索引结构。可能需要添加更合适的索引,或者删除冗余的,减少优化器的选择困难。比如联合索引或者覆盖索引,这样优化器可能会更倾向于使用它们。 另外,查询重写可能有效。比如将子查询改成连接查询,引用[4]中的例子就是这种情况。或者调整WHERE条件的顺序,让索引更有效。 还有优化器提示,比如像/*+ INDEX() */这样的提示,但需要确认MySQL版本是否支持。同时,调整优化器参数,比如optimizer_switch,可能会有帮助,但需要谨慎处理。 监控和分析执行计划是必须的,用EXPLAIN和EXPLAIN ANALYZE查看实际使用的索引和成本。如果发现选错,就需要进一步分析。 数据分布的问题也很关键。如果某些值的数据分布不均匀,优化器可能错误估计行数,这时候可能需要用直方图或者手动干预。 最后,极端情况下可以删除误导性的索引,或者调整隔离级别,减少锁竞争对索引选择的影响。不过这些都是最后的手段,需要测试后再实施。 用户可能需要具体的步骤,比如如何强制索引,如何分析统计信息,或者如何调整查询语句。还要考虑不同情况下的适用方法,比如数据量大的时候可能需要不同的策略。要确保建议全面且实用,同时引用相关的内容来支持每个建议。</think>### 解决 MySQL 查询优化器选择错误索引的方法和建议 #### 一、问题根源分析 MySQL 查询优化器选择索引时基于成本模型,会评估全表扫描和索引扫描的成本,选择成本最低的方式[^1]。但在以下场景可能出错: 1. **统计信息不准确**:索引的基数(Cardinality)或数据分布统计不更新,导致成本估算偏差[^3]。 2. **复杂查询条件**:涉及多列、范围查询或函数时,优化器难以准确估算过滤性。 3. **索引冗余或冲突**:存在多个候选索引时,优化器可能误判最优选择[^2]。 --- #### 二、解决方案与建议 ##### 1. **强制指定索引** 通过 `FORCE INDEX` 或 `USE INDEX` 显式指定索引: ```sql SELECT * FROM table_name FORCE INDEX (index_name) WHERE condition; ``` - **适用场景**:明确知道某个索引更优且长期稳定。 - **风险**:数据分布变化后可能失效,需定期验证。 ##### 2. **更新统计信息** 手动更新表的统计信息: ```sql ANALYZE TABLE table_name; ``` - **原理**:优化器依赖 `STATISTICS` 表中的数据分布信息,更新后可能修正成本估算。 ##### 3. **优化索引结构** - **删除冗余索引**:减少优化器的选择干扰。 - **创建覆盖索引**:包含查询所需的所有字段,减少回表操作。例如: ```sql CREATE INDEX idx_covering ON table_name (col1, col2, col3); ``` ##### 4. **查询重写优化** - **简化条件**:避免在 WHERE 子句中使用函数或复杂表达式。 - **拆分复杂查询**:将子查询转换为连接查询(如引用[4]中的优化方法)。 ##### 5. **使用优化器提示** 通过注释语法提示优化器: ```sql SELECT /*+ INDEX(table_name index_name) */ * FROM table_name WHERE condition; ``` - **注意**:需确认 MySQL 版本支持(8.0 及以上)。 ##### 6. **调整优化器参数** 修改 `optimizer_switch` 参数,关闭某些优化策略: ```sql SET optimizer_switch = 'index_merge=off'; ``` - **适用场景**:优化器错误选择索引合并(Index Merge)时。 ##### 7. **监控与验证** 使用 `EXPLAIN` 和 `EXPLAIN ANALYZE` 对比不同索引的执行计划: ```sql EXPLAIN SELECT * FROM table_name WHERE condition; ``` - **关注字段**:`possible_keys`(候选索引)、`key`(实际选择索引)、`rows`(估算扫描行数)[^3]。 ##### 8. **处理数据倾斜** - **直方图统计**(MySQL 8.0+): ```sql ANALYZE TABLE table_name UPDATE HISTOGRAM ON column_name; ``` - **手动干预**:对分布不均的字段使用条件过滤或分片查询。 --- #### 三、关键操作流程图 ``` 更新统计信息 → 分析执行计划 → 优化索引/查询 → 验证性能提升 ``` #### 四、注意事项 - **谨慎删除索引**:先通过 `EXPLAIN` 确认索引是否被使用。 - **隔离测试**:在非生产环境验证优化效果。 - **长期监控**:数据量变化后重新评估索引策略。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值