MySQL索引底层原理与慢SQL优化实战:从B+树到联合索引设计
2026/9/24 20:04:16 网站建设 项目流程

从一次慢查询说起吧。有个同事做报表导出,一条SQL跑了快3秒,数据量也就几十万行,按理说不该这么慢。我先让他把SELECT的WHERE条件列出来,再一看表结构——好家伙,整整十来个字段,里面连一个索引都没建。加了两个联合索引之后,同样的查询直接掉到40毫秒上下,报表接口的P99延迟降了一个数量级。那次之后我越来越觉得,MySQL索引这东西,懂的人觉得简单,但真要在生产环境里设计好、排查对,细节里的门道是真不少。这篇文章就把我对MySQL索引的理解完整梳理一遍,从数据结构到实操设计,再到故障排查,一次讲透。

  • 适合谁看:刚入门想搞懂索引原理的开发者;写SQL经常莫名慢、不知道加什么索引的后端;以及面试前想系统过一遍索引知识点的求职者。
  • 能解决什么问题:搞清楚索引为什么快、什么时候该建索引、建什么样的索引、索引为什么会失效、以及拿到一条慢SQL之后怎么定位和优化。

1. 索引的本质与底层数据结构

1.1 索引到底是什么

索引的本质是一种排好序的数据结构,它的存在意义只有一个——减少磁盘IO次数,让查询更快。

你可以把索引理解为新华字典的“偏旁部首检字表”。字典正文是按拼音排的,但你想按部首找一个字的时候,不会一页页翻,而是先去检字表里找到这个字在第几页,然后再翻到那一页去拿正文。索引就是这张“检字表”,它告诉你数据在磁盘的哪个位置,省去了全表扫描的代价。

MySQL的索引是在存储引擎层实现的,不是MySQL服务层统一实现。所以不同存储引擎(InnoDB、MyISAM、Memory等)的索引结构并不一样,用法上也有差异。目前主流生产环境几乎都在用InnoDB,所以后面讲的内容默认都以InnoDB为准。

1.2 为什么MySQL默认选择B+树

索引的数据结构候选有很多:哈希表、二叉树、红黑树、B树、B+树。MySQL最终选择了B+树,这不是随意的,而是每个候选都有明显短板。

  • 哈希表:单条等值查询确实快到极致,O(1)复杂度。但哈希索引天生不支持范围查询(>、<、BETWEEN),也不支持排序,联合索引的多个字段也无法部分匹配。再加上哈希碰撞处理会引入不确定性,InnoDB只在自适应哈希索引(Adaptive Hash Index)场景下把B+树索引优化成哈希结构,不能作为通用索引方案。
  • 二叉树:极端情况下会退化成链表,比如按递增主键插入,树的高度直接等于节点数量,查询退化到O(N)。
  • 红黑树:虽然通过自平衡把高度控制在O(logN),但它是二叉结构,树高度依然太高。数据量大到千万级别时,红黑树高度在30到40层之间。如果每一层的节点都要做一次磁盘IO,那就是30到40次IO,延迟不可接受。
  • B树:多路平衡查找树,一个节点能存多个键值,树的高度被大幅压缩。但B树的节点既存索引键也存数据(或者数据指针),每个节点能容纳的键数量有限,相同数据量下树还是偏“胖”。而且B树的中序遍历比较复杂,做范围查询时需要回溯到父节点,效率不如B+树。
  • B+树:在B树基础上做了两个关键优化。第一是数据只存在叶子节点,非叶子节点只存索引键,这样单个节点能容纳的键数量更多,树更矮;第二是叶子节点之间用双向链表连接,范围查询、排序、分组都能通过顺序遍历叶子链表完成,不需要像B树那样回溯。

以一个3层B+树为例估算一下:InnoDB一页默认16KB,假设主键是BIGINT(8字节),加上6字节的行指针,每页大约能存16 * 1024 / 14 ≈ 1170个索引键。第二层同样的结构,第三层是叶子节点,假设每行数据1KB,那么叶子页能存16行。算下来一棵3层B+树能支撑约1170 * 1170 * 16 ≈ 2000万行数据。也就是说,在2000万行规模下,从根节点查到一个叶子节点只需要3次磁盘IO,这就是B+树强悍的地方。

1.3 InnoDB聚簇索引的物理存储结构

InnoDB的索引是**聚簇索引(Clustered Index)**组织方式,它指的是:表的数据行本身就存在主键索引的B+树的叶子节点上。

这里有个关键点:InnoDB表不是“数据文件 + 索引文件”的分离结构,而是数据文件本身就是主键索引构成的B+树。叶子节点上存放的是完整的行记录,包括所有字段。这就是为什么InnoDB表必须有主键——没有主键,数据就没法按照有序结构存储。

如果你建表时没有指定主键,InnoDB会用第一个非空唯一索引作为主键;如果连唯一索引都没有,InnoDB会生成一个隐藏的6字节ROW_ID作为聚簇索引。这个隐藏主键你平时感知不到,但它占存储空间,也影响写入性能,所以我一直建议:每张表都应当显式定义主键

除了主键聚簇索引,其他索引(普通索引、联合索引、唯一索引等)都叫二级索引(Secondary Index)。二级索引的叶子节点存储的内容不是完整行数据,而是索引列的值 + 对应主键值

举个例子,一张表有如下结构:

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `age` INT DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`name`) ) ENGINE=InnoDB;
  • 主键索引 idx_primary:B+树叶子节点存的就是整行数据,即(id, name, age)全部字段,按id排序。
  • 二级索引 idx_name:B+树叶子节点存的是(name, id),按name排序。

当你执行SELECT * FROM user WHERE name = '张三'时,MySQL会先走二级索引idx_name,找到name='张三'的叶子节点,取出对应的主键id,然后再用这个id去主键聚簇索引上查一次完整行。这个过程叫做回表(Bookmark Lookup)

如果你的查询只需要name和id两个字段,那么二级索引里就已经有全部需要的数据,不需要回表,这种可以“仅凭索引就满足查询”的场景叫覆盖索引(Covering Index)

2. MySQL索引的类型与适用场景

2.1 按功能分类

MySQL索引按功能维度可以分为四个基础类型:

索引类型特点说明典型使用场景
普通索引(INDEX/KEY)仅加速查询,不约束数据唯一性高频WHERE条件过滤字段
唯一索引(UNIQUE KEY)加速查询 + 保证列值唯一用户手机号、身份证号、订单号
主键索引(PRIMARY KEY)特殊的唯一索引,且不允许为NULL每一张InnoDB表必备
全文索引(FULLTEXT)基于分词匹配,支持模糊检索长文本内容的关键字搜索

一个非常容易踩的坑是:唯一索引不是越多越好。唯一约束的维护需要额外索引结构,每次插入、更新都要做唯一性检查,写入性能会受影响。如果某个字段只是业务上天然唯一(比如用户邮箱),但你其实不依赖数据库来强制唯一,就建普通索引即可,没必要用UNIQUE。

全文索引也容易误解。它和LIKE '%关键字%'完全不是一回事,全文索引是全文检索技术,在InnoDB中基于分词倒排实现,查询语法是:

SELECT * FROM article WHERE MATCH(title, content) AGAINST ('数据库' IN BOOLEAN MODE);

但说实话,生产环境的全文搜索我一般不推荐用MySQL自带的FULLTEXT。数据量大了以后,全文索引的维护成本很高,性能也不如专业的Elasticsearch或专门搜索引擎稳定。MySQL全文索引更适合做轻量级搜索,或者数据量可控的场景。

2.2 按存储结构分类:聚簇索引与非聚簇索引

前面提到过InnoDB是聚簇索引组织方式,数据行存在主键索引B+树的叶子节点上。MyISAM则不同,它是非聚簇索引——数据文件和索引文件独立存储,索引B+树的叶子节点存的不是数据行,而是一个数据行的物理地址指针,无论主键索引还是二级索引都指向这个地址。

两者的优缺点非常明显:

对比维度InnoDB聚簇索引MyISAM非聚簇索引
数据存储数据在聚簇索引叶子节点数据在独立文件,索引存物理地址
查询效率主键查询不需要回表主键查询也需要按地址取数据
插入顺序按主键顺序插入效率高与主键顺序无强关联
随机主键插入可能导致页分裂,性能下降影响相对较小
二级索引叶子存主键值,可能回表叶子存数据地址,不回表但数据文件分散

如今MySQL 8.0中MyISAM已经不太推荐使用了,你可以通过理解MyISAM的非聚簇存储模式来对照理解InnoDB的设计。

2.3 联合索引(复合索引)

联合索引是在多个字段上建立的索引,它是实际工作中设计难度最高、收益也最大的一种索引类型。

它的核心特点是遵循最左前缀原则(Leftmost Prefixing)。B+树的多列索引会先按照第一个索引字段排序,字段相同再按第二个字段排序,以此类推。所以MySQL使用联合索引时,只有查询条件从最左边开始连续匹配索引列,才能命中索引。

CREATE TABLE `order` ( `id` INT NOT NULL AUTO_INCREMENT, `user_id` INT NOT NULL, `status` TINYINT NOT NULL, `create_time` DATETIME NOT NULL, PRIMARY KEY (`id`), KEY `idx_user_status_time` (`user_id`, `status`, `create_time`) ) ENGINE=InnoDB;

对上面这个联合索引(user_id, status, create_time)

  • WHERE user_id = 100:命中,最左列user_id匹配。
  • WHERE user_id = 100 AND status = 1:命中。
  • WHERE user_id = 100 AND status = 1 AND create_time > '2024-01-01':命中。
  • WHERE status = 1不命中,因为跳过了最左列user_id。
  • WHERE user_id = 100 AND create_time > '2024-01-01'部分命中,user_id能用到索引,但create_time在范围查询中无法继续使用索引排序,只能回表用user_id过滤后的结果再筛选。

联合索引设计时的一条核心思路是:把等值查询条件放在最前面,范围查询条件放在最后面。因为联合索引一旦遇到范围查询(>、<、BETWEEN),后面的索引列就全部失效了,等于做不了索引排序和过滤。

2.4 索引设计的两大原则

实际设计索引时,我会反复用两个指标衡量一个索引是否合理:cardinality(基数)选择性(Selectivity)

  • 基数(Cardinality):索引列上不同值的数量。基数值越大,说明列能区分度越高。
  • 选择性:基数 / 表总行数,选择性越接近1,索引效果越好。

在MySQL里,你可以直接查看索引基数值:

SHOW INDEX FROM `user`;

注意,这个值在MySQL 8.0之前是统计估算的,不是精确值,数据变化后可能需要ANALYZE TABLE来更新统计信息。如果发现某个索引的cardinality和实际数据严重不符,优化器就可能会误判,放弃索引。

举例:一张100万行的用户表,sex字段只有“男/女”两个值,选择性是2 / 1000000 = 0.0002%。如果你在sex上建索引,查询WHERE sex = '男'会返回约50万行,MySQL优化器一看这个返回值占总数据量比例太高,大概率会直接放弃索引、走全表扫描。这不是索引没用,而是这种“低区分度”字段本身就不适合单独建索引

真正有效的索引字段,选择性应当尽量高,通常是主键、手机号、订单编号、身份证号这样的高唯一性字段。

3. 索引的创建、分析与实战设计

3.1 索引的创建方式

MySQL创建索引有几种途径,不同场景我用不同的方式:

  • 建表时直接定义索引:适合表结构比较稳定、从一开始就明确的索引。
CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `phone` VARCHAR(20) NOT NULL, `name` VARCHAR(50) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_phone` (`phone`), KEY `idx_name` (`name`) ) ENGINE=InnoDB;
  • ALTER TABLE添加索引:适合给已存在的表加索引,生产环境最常用。
ALTER TABLE `user` ADD INDEX `idx_name` (`name`); ALTER TABLE `user` ADD UNIQUE KEY `uk_phone` (`phone`);
  • CREATE INDEX创建索引:功能等价于ALTER TABLE,语义上更直观。
CREATE INDEX idx_name ON `user` (`name`); CREATE UNIQUE INDEX uk_phone ON `user` (`phone`);
  • 删除索引
DROP INDEX idx_name ON `user`;
  • 查看表上的索引
SHOW INDEX FROM `user`;

3.2 三种常见方案对比

很多同学纠结用哪种方式建索引,其实手段上区别不大,核心区别在“要不要先评估”。

方式优点缺点适用场景
建表时定义表结构清晰,索引随表创建表结构变更不灵活新表上线
ALTER TABLE可运行时变更,灵活大表加锁风险日常线上加索引
CREATE INDEX语义清晰与ALTER TABLE本质相同开发者手动加索引

线上给大表加索引,要注意的是MySQL 8.0之前的版本,ALTER TABLE会导致锁表,影响线上写入。如果你在几十万甚至上百万行的大表上直接执行加索引,可能在执行期间阻塞读写。我建议优先考虑使用在线DDL(InnoDB支持ALGORITHM=INPLACE)或者通过工具(如gh-ost、pt-online-schema-change)来平滑加索引,避免长时间阻塞线上业务。

3.3 索引设计三步法

我在设计索引时的通用思考路径,可以总结成三步:

第一步:梳理核心查询路径。

把业务里的高频查询SQL全部列出来,尤其是WHERE子句里的等值条件、范围条件、排序字段、分组字段。比如电商订单库的核心查询是:

SELECT * FROM `order` WHERE user_id = 123 AND status = 1 ORDER BY create_time DESC LIMIT 20;

(user_id, status, create_time)就是一个顺理成章的联合索引候选。

第二步:判断每个字段的区分度。

如果字段选择性太低,比如status只有几个离散值,那它就不适合放索引的最前位置,只适合作为联合索引中间或后面的过滤条件。

第三步:分析是否让查询走向覆盖索引。

如果一条查询频繁执行,而且高频查询返回的字段集合固定,比如业务只需要订单号和状态,那就可以创建一个(user_id, status, order_no)的覆盖索引,让查询完全不用回表。这个收益在大并发查询下非常可观。

3.4 最容易忽略的排序与分组场景

索引不仅用于WHERE过滤,也用于ORDER BY和GROUP BY。

SELECT user_id, COUNT(*) FROM `order` WHERE create_time BETWEEN '2024-01-01' AND '2024-06-30' GROUP BY user_id;

这条SQL如果没有合适索引,MySQL会把符合时间条件的所有行捞出来,再做一个临时表和文件排序(filesort),执行效率很低。如果建一个(create_time, user_id)联合索引,因为B+树天然有序,MySQL可以顺序扫描索引并直接完成分组统计,大大减少临时表开销。

从执行计划上能看到区别:走临时表的执行计划Extra字段会出现Using temporary; Using filesort,而走索引消除排序时,Extra字段没有这个提示。

4. 索引失效的十大典型场景与避坑指南

4.1 对索引列使用函数或表达式计算

这是最经典的索引失效场景。假设你在create_time上建了索引:

-- 索引失效 SELECT * FROM `user` WHERE DATE(create_time) = '2024-01-01'; -- 索引生效 SELECT * FROM `user` WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';

原因很简单:B+树里存放的是原始列值,MySQL只能对原始值做范围匹配。一旦给索引列套了一个函数或计算表达式,优化器就无法直接利用有序结构去定位,只能对全表每个值都算一遍再过滤,索引自然失效了。核心原则是:索引列保持纯洁,不做任何运算

我之前排查过一条慢SQL,WHERE条件里写了WHERE DATEDIFF(NOW(), pay_time) > 7,pay_time上明明有索引,却一直全表扫描。改成pay_time < DATE_SUB(NOW(), INTERVAL 7 DAY)之后,查询从13秒降到了0.2秒。

4.2 隐式类型转换

MySQL在字段类型不匹配的时候,会自动做隐式类型转换。问题在于:一旦类型转换发生在索引列上,索引就失效了

最常见的情况是:手机号字段是VARCHAR类型,但代码里传给SQL的参数是INT。

-- phone是varchar类型 SELECT * FROM `user` WHERE phone = 13800138000;

MySQL会把phone列转成数字再比较,等于对索引列执行了CAST操作,索引失效。解决办法很简单:参数类型与字段类型保持一致

SELECT * FROM `user` WHERE phone = '13800138000';

4.3 LIKE前置通配符

LIKE ‘abc%’可以利用索引,因为MySQL可以在B+树上按前缀做范围扫描;但LIKE ‘%abc%’LIKE ‘%abc’无法利用索引,因为要匹配的字符串可能出现在字段任意位置,无法从有序的B+树直接定位。

如果业务确实需要模糊搜索,有几个替代方案:

  • 前缀搜索固定前缀,比如只搜首字母开头的编码;
  • 对于中缀或后缀模糊匹配,考虑用全文索引(适合大文本、较长内容);
  • 如果是小场景、数据量有限,可以接受全表扫描,但加好LIMIT。

4.4 OR连接非索引字段

OR在MySQL优化器里是个“捣乱分子”。如果OR的两个条件里,有一个字段没有索引,那么即便另一个字段有索引,优化器也只能放弃索引走全表扫描。原因在于全表扫描可以一次性把OR两边的行都捞出来,如果用索引则要分别扫描再合并,优化器在代价估算后往往不会选择索引。

-- 假设user_id有索引,phone没有索引 SELECT * FROM `user` WHERE user_id = 100 OR phone = '13800138000';

改进策略:

  • 给phone也加上索引;
  • 或者改写成UNION ALL:
SELECT * FROM `user` WHERE user_id = 100 UNION ALL SELECT * FROM `user` WHERE phone = '13800138000';

4.5 NOT IN、NOT EXISTS、!=、<>操作符

不等判断通常是范围扫描无法精确定位的场景。MySQL一般会认为NOT IN需要读取大部分数据,直接走全表扫描性价比更高。只有当表数据量小、或者过滤比例很高时,优化器才有可能反选走索引。

这类场景下,我建议改成等值或范围查询的组合,或者直接用应用层过滤。特别是NOT IN里子查询的结果集很大的时候,性能往往灾难。

4.6 联合索引不满足最左前缀

这个前面详细讲过了,再重复强调一次:联合索引(a, b, c),查询条件必须包含a,且不能跳过中间列。

常见误区例子:

-- idx_a_b_c 联合索引,以下查询全部无法完整使用索引 WHERE b = 1 WHERE c = 1 AND b = 1 WHERE a = 1 AND c = 1 -- a可以用,c用不到

注意,MySQL 8.0的**索引跳跃扫描(Index Skip Scan)**在某些场景下可以让跳过最左列的查询也使用索引,但它有严格的前提条件(表不大、优化器认为值得),生产环境依赖它不现实。还是按最左前缀原则来设计查询和索引更稳妥。

4.7 优化器认为全表扫描更快

不是所有带索引的查询MySQL都会用索引。优化器会基于表的行数、索引基数、数据分布等统计信息估算代价,如果它认为全表扫描代价更低,就会放弃索引。这通常发生在:

  • 数据量极小的表;
  • 低区分度字段被查询(比如sex);
  • 数据分布极不均匀(比如status = 1的行占了90%)。

这种情况下,即使索引没“失效”,实际执行计划也确实没走。修正方式是提高索引区分度,或者调整查询条件让过滤比例降低。

4.8 索引失效场景速查表

操作是否可用索引说明
索引列上使用函数/表达式失效禁止对索引列做运算
隐式类型转换失效参数类型保持与字段一致
LIKE 'abc%'可用前缀匹配可用
LIKE '%abc%'失效中缀/后缀无法定位
OR连接非索引列失效两边都建索引或用UNION拆分
NOT IN / != / <>一般失效优化器倾向全表扫描
联合索引跳过最左列失效遵守最左前缀
低区分度字段单独查可能失效优化器估算代价高

5. 用EXPLAIN定位索引问题的实操方法

5.1 EXPLAIN关键字段解读

排查慢SQL的第一板斧就是看执行计划。MySQL提供了EXPLAIN命令,通过它可以看到一条SQL实际会怎么执行。

EXPLAIN SELECT * FROM `order` WHERE user_id = 100 AND status = 1 ORDER BY create_time DESC LIMIT 20;

输出结果里有几个关键列,我逐个说明:

  • type:访问类型,从好到差依次是system > const > eq_ref > ref > range > index > ALL。如果type是ALL,说明全表扫描,必须要警惕。出现system/const说明查询走的唯一索引或主键等值查询,是最理想状态。ref表示非唯一索引等值查询,range表示范围扫描,这些都是合理的访问类型。
  • key:实际用到的索引名。如果key为NULL,说明没走索引。
  • rows:预估扫描行数,优化器会基于这个值做代价估算,rows越小越好。
  • Extra:常见值包括Using index(覆盖索引)、Using where(在存储引擎层过滤)、Using temporary(使用了临时表,通常伴随group by/order by性能问题)、Using filesort(文件排序,如果数据量大往往需要优化)。

最理想的Extra是Using index,代表查询直接从索引中获取全部数据,没有回表。而Using temporary; Using filesort基本可以认为是性能信号灯,需要关注。

5.2 一个慢SQL的完整优化过程

分享一个实际案例。某后台管理系统的订单列表页,按商户ID和时间筛选订单,SQL是:

SELECT * FROM `order` WHERE merchant_id = 2001 AND create_time >= '2024-03-01' ORDER BY create_time DESC LIMIT 20;

执行计划显示type = ALL,key为NULL,rows = 200万。因为order表是200万行的大表,且商户ID在业务上不是高区分度字段(一个商户可能几十万单),单独给merchant_id建索引效果一般。

我最终建的索引是:

ALTER TABLE `order` ADD INDEX `idx_merchant_time` (`merchant_id`, `create_time`);

索引设计逻辑:merchant_id等值条件放前面,create_time做范围过滤和排序放后面。联合索引天然有序,ORDER BY create_time不再需要额外排序,一次索引扫描就能完成。

优化后执行计划:

  • type = ref;
  • key = idx_merchant_time;
  • rows = 213;
  • Extra = Using index condition。

接口响应时间从1.8秒降到了35毫秒左右。这个案例说明,单列索引不一定能解决排序+过滤的组合需求,联合索引往往才是正解

5.3 覆盖索引的极致性能

之前讲回表的时候提过覆盖索引,这里看一个实战应用。在订单表上,高频查询是统计某用户某时间段内各状态的订单数:

SELECT status, COUNT(*) FROM `order` WHERE user_id = 3001 AND create_time BETWEEN '2024-01-01' AND '2024-06-01' GROUP BY status;

联合索引(user_id, create_time, status)可以把这条SQL完全覆盖:通过user_id等值定位、create_time范围扫描、status直接看看叶子节点上的索引值即可完成分组统计,全程不需要回表拿整行数据,Extra展示为Using index。对于大并发高频繁的统计场景,这个优化的收益是百毫秒到微秒级别的差异。

6. 索引常见问题与面试高频题总结

6.1 面试高频问题一问一答

结合这些年的面试经历,MySQL索引有几个问题几乎必问,我把常见问题和清晰的答案整理一下。

为什么InnoDB表必须有主键?

因为InnoDB聚簇索引的叶子节点存储的是整行数据。如果没有主键,InnoDB会用第一个非空唯一索引做主键,再没有就生成隐藏的ROW_ID。隐式主键用户不可见,也无法利用它做高效的查询。显示定义主键,可以选择最适合业务查询的聚簇索引顺序,对写入和查询最有利。

主键用自增ID还是UUID?

这个问题的核心是“主键的有序性”。自增ID按顺序递增,插入B+树时总是追加到最右边,不会触发随机的页分裂;UUID是随机字符串,主键索引的有序性被打破,插入时会导致大量随机位置的页分裂、数据页碎片化,性能会明显差于自增ID。如果业务需要分布式环境生成主键,可以改用雪花ID之类的有序ID,而不是UUID。

为什么索引能让查询变快,但还会让写入变慢?

因为索引是一种额外的数据结构。每次INSERT/UPDATE/DELETE,除了要改数据行,还要维护所有相关索引的B+树结构,比如插入新键时可能需要页分裂、更新时可能需要移动索引项。所以索引越多,写入维护成本越高。设计原则是在满足查询需求的前提下,索引“少而精”,避免冗余索引。

联合索引字段顺序怎么定?

核心思路:等值查询字段放在最前,范围查询字段放最后;区分度高的放前面;经常ORDER BY/GROUP BY的字段也尽量利用索引顺序。具体再结合业务实际流量做取舍。

覆盖索引和回表是什么关系?

二级索引叶子节点存的是“索引列 + 主键值”。查询时如果需要的字段都在二级索引里,就不需要拿主键去聚簇索引再查一次,这叫覆盖索引。如果SELECT的字段里有不在二级索引里的列,就必须用主键回表,回表次数越多性能越差,大分页查询通常就慢在这里。

6.2 常见问题的原因与处理建议

现象可能原因处理建议
查询单行很慢无索引或索引失效EXPLAIN查看执行计划
加了索引还是不生效函数运算、隐式转换、OR连接按第4节逐项排查
索引基数统计严重偏差长时间未更新统计信息ANALYZE TABLE
大分页慢LIMIT 100000, 20需要大量回表延迟关联或游标分页
写多读少表越来越慢索引冗余过多删除无效索引,合并联合索引
排序慢缺少覆盖排序字段的索引联合索引中放ORDER BY字段

6.3 几个实用的避坑经验

第一,给大表加索引要评估锁表风险。

MySQL 8.0之前,在线DDL也不是完全无锁。给大表加索引前,建议先看表数据量,评估执行时间,用gh-ost这类工具做平滑变更,避免在业务高峰期操作。

第二,不要盲目相信索引越多越好。

一个常见的坏习惯是一个查询条件一个索引,结果表上堆了几十个索引。实际上联合索引可以覆盖多个查询,索引数量应该维持在个位数到十几个以内,每个索引都要有明确的收益依据。

第三,写完SQL后用EXPLAIN习惯性过一遍。

这可能是投入产出比最高的习惯了。不管是新写完的SQL还是接手别人的SQL,先EXPLAIN一眼,看type、key、rows、Extra四个字段,基本就能判断这条SQL会不会在数据量大之后出问题。

第四,理解优化器不等于完全相信优化器。

MySQL的优化器是基于统计信息的,统计信息不准时会作出错误判断。如果确认索引没问题但优化器没走,可以用FORCE INDEX临时指定,但优先还是要弄清楚优化器为什么没选索引,比如统计信息过旧、区分度不够。

7. 写在最后的实践体会

做了这么多年开发和数据库优化,我对索引最深的体会有三点。

第一,索引是“空间换时间”的典型,它巧妙地利用B+树的有序特性,把随机磁盘IO转换成顺序IO,让千万级数据量下的查询依然能保持毫秒级延迟。凡是要加速查询,第一反应不是调参,而是看索引设计是否合理。

第二,索引设计一定是结合业务查询模式做的,脱离SQL谈索引没有意义。一张表的索引不是“越多越好”,而是“越准越好”。多花一点时间梳理业务的高频查询路径,一本万利。

第三,排查慢SQL时,EXPLAIN的四个字段——type、key、rows、Extra——基本能判断80%的问题。养成写SQL就跑执行计划的习惯,比记一堆“索引失效口诀”有用得多。

如果你现在正被一条慢SQL困扰,先把SQL丢进EXPLAIN里看一眼,往往答案就已经浮出水面了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询