news 2026/10/14 3:05:30

MySQL索引优化:从B+树原理到慢查询实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL索引优化:从B+树原理到慢查询实战

做后端开发这几年,最常被拉到群里去救火的场景,十有八九是数据库慢查询。系统跑得好好的,突然某天接口变慢、CPU飙高,一看日志全是SELECT走了全表扫描,几百万行的表一次查询几秒钟才回来。这种时候,十次里有九次是索引设计出了问题。这篇内容我想把 MySQL 索引这条线彻底讲透,从底层原理到实战调优,再到我踩过的那些坑,一次性梳理清楚。不管你刚接触索引,还是已经写了好几年 SQL 偶尔还是会困惑,这篇应该都能让你少走不少弯路。

MySQL 索引本质上就是数据库维护的一份"目录",它存在的唯一目的就是让查询少读数据。听起来简单,但真要设计好索引,你得理解它底层为什么快、什么场景会失效、不同类型索引各自适合什么查询模式。这篇文章我会围绕这样几个核心问题展开:索引是怎么存储和工作的,联合索引那个"最左前缀"到底怎么回事,EXPLAIN 分析执行计划该怎么看,以及实际业务里建索引时的取舍逻辑。

1. 先把索引的原理吃透:为什么数据库会越查越慢

1.1 没有索引的全表扫描到底有多慢

我们经常在业务代码里说"能用索引就不用全表扫描",但很多人其实没直观感受过"全表扫描"四个字的真实成本。你可以把一张没有索引的表想象成一本没有目录、也没有章节标题的厚书。你想找一个关键词,唯一的办法就是从第一页翻到最后一页,逐行比对。这个动作放在数据库里就是:存储引擎从表的第一行数据开始,一块块地把数据页读进内存,然后逐行检查是否满足WHERE条件。

具体量化下来,一张 1000 万行的表,假设每行占用约 200 字节,那么光是数据文件可能就有 2GB 到 3GB。按 InnoDB 每页 16KB 来计算,这大约是十几万个数据页。全表扫描意味着要遍历这十几万页。即使数据已经部分缓存在内存里,真正从磁盘读取那些冷数据页的随机 IO 开销是非常可观的。磁盘随机读取一次大约 5ms 到 10ms,我们按乐观的 5ms 算,扫完这十几万页理论上就是几百秒的 IO 时间——当然实际中会并发预读、有缓存命中,不至于那么夸张,但一次查询花几百毫秒甚至好几秒是完全正常的。

这就是索引存在的核心理由:索引把无序的数据变成了有序的结构,让查找从"逐行遍历"变成"按目录定位"。一次原本要扫几万个数据页的查询,走索引可能只需要读几层索引节点加上少数几个数据页,IO 次数差了三个数量级。说到底,数据库的绝大部分性能问题,最后都归结为"少读点数据"。

1.2 B+树:索引的物理形态与查询路径

MySQL 里 InnoDB 引擎用的索引结构是 B+ 树,这几乎是面试必问、实战必备的知识点。B+ 树长什么样呢?你可以把它理解成一个多层级的目录结构:根节点是顶层目录,中间层的每个节点存储着若干个"键值+指针",告诉你某个范围的数据在下一层的哪个分支里;最底层的叶子节点才真正保存着有序的数据键值。

和普通的二叉树、B树相比,B+ 树有两个关键设计决定了它在数据库场景下的表现。第一,所有数据都集中在叶子节点,内部节点只存放索引键和子节点指针,不存放真实数据。这意味着同样一个 16KB 的数据页,能容纳的索引键数量非常多,树的层级会被压得很矮。一个三层高的 B+ 树,哪怕索引键只占 8 字节,也能支撑上亿条记录的查找。查询任意一条数据,最多只需要三次磁盘 IO 就能定位到叶子节点,这个 IO 次数几乎就是常数级别,跟数据总量关系不大。

第二,叶子节点之间通过双向链表连接,形成一个有序的链表结构。这个设计对范围查询特别友好。比如查WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31',只要先在 B+ 树上定位到起始位置,然后顺着叶子节点的链表顺序向后遍历,直到超出范围条件为止,不需要回头再走树的路径。这就是为什么索引能同时加速等值查询和范围查询。

顺便说一句,很多人会纠结"为什么不直接用哈希索引"。哈希索引做等值查询确实比 B+ 树还快,O(1) 级别,但没法支持范围查询,也没法支持排序。而现实中大部分查询都是范围、排序、分组这些操作,所以 InnoDB 选择了 B+ 树作为主力索引结构。MySQL 的 Memory 引擎有哈希索引,但用得非常少,核心原因就是适用范围太窄。

1.3 聚簇索引 vs 二级索引:两个核心概念

InnoDB 里所有数据本质上都存在一棵 B+ 树上,这张表的聚簇索引就是数据本身。聚簇索引的叶子节点保存的是完整行数据,而这张表的聚簇索引默认建在主键上。所以 InnoDB 表有且只有一个聚簇索引:你建了主键就用主键建,没显式建主键就用第一个非空的唯一索引,都没有的话引擎会隐藏一个自动生成的 6 字节行号。

二级索引(也叫辅助索引、非聚簇索引)是另一回事。它的叶子节点不存完整行数据,只存索引键和对应主键值。这就引出了一个非常高频的概念:回表。你用一个二级索引去查数据时,先走这棵二级索引 B+ 树,找到匹配记录的主键值,然后再拿这个主键值去聚簇索引的 B+ 树里再查一次,才能取到完整行。这个"查两次树"的过程就叫回表。

回表次数是判断查询性能的重要指标。一个二级索引命中了 1000 行,就要回表 1000 次,每次都是一次主键 B+ 树查找。这就是为什么"覆盖索引"会成为优化重点——如果查询需要的所有列都已经包含在二级索引里,那就不需要回表了,直接读索引叶子节点就能返回结果,查询效率会高一大截。后面我会专门讲这个。

理解了聚簇索引和二级索引,你在设计时就能想明白为什么要给一张表建主键、为什么主键最好是有序的递增整数。如果主键是随机生成的 UUID,新插入的行在 B+ 树上的位置会随机分布,导致大量页分裂和碎片化,写入性能会明显变差。

提示:给 InnoDB 表建主键时优先选自增整数或者单调递增的业务 ID。用 UUID 做主键虽然看起来"无懈可击",但引发的页分裂和碎片问题是真实存在的,数据量大之后写入性能差距非常直观。

2. 索引类型与适用场景:别再一把梭建索引

2.1 唯一索引、普通索引与主键索引怎么选

很多同学刚开始建索引时比较随性,想到哪个字段查得频繁就建哪个,类型也不在意。其实不同类型的索引在语义、约束和性能上有明显差别,选错类型的代价不小。

主键索引就是我们前面说的聚簇索引,每个表只能有一个,特点是叶子节点直接保存整行数据。它同时承担两个职责:约束数据唯一性和作为二级索引回表的"目的地"。主键设计得好不好,直接影响整个表的写入性能和所有二级索引查询的最终回表成本。

唯一索引允许列值为 NULL,也就是说多行 NULL 不违反唯一约束,但非 NULL 值不能重复。它在约束数据唯一性上跟主键类似,但本身是一个二级索引,查询时走的是独立 B+ 树,需要回表。业务场景举例:用户表的手机号字段,逻辑上需要唯一,但可能早期系统允许空手机号,那就不能用主键,可以建一个唯一索引来兜底。这里有个小经验:只有当业务真的需要"数据库层面强制唯一"时才用唯一索引,如果只是查询的时候希望加速某个唯一性很强的字段,建普通索引就够了。因为唯一索引在写入时要多做一次唯一性检查,虽然单次开销不大,但高频写入下累积起来也是成本。

普通索引就纯粹是加速查询用的,没有唯一性约束。设计原则是"用在区分度足够高、查询模式足够固定的列上"。还有一种容易被忽略的情况:在WHERE条件里只做等值匹配的字段,建普通索引就足够了,没必要因为"这个字段好像很特别"就强行加唯一约束。

2.2 联合索引:最左前缀原则详解

联合索引(复合索引)是指在一张表的多个列上共同建一个索引,比如(user_id, status, create_time)。它的核心价值是让多个过滤条件共用一棵 B+ 树,比给每个列分别建单个索引占用空间小、查询效率更高。

联合索引的排序规则是:先按第一个列排序,第一个列相同的再按第二个列排序,以此类推。这就决定了它最核心的检索规则——最左前缀原则:查询条件里必须包含联合索引最左边的列,这个索引才可能被用到。比如索引(a, b, c),WHERE a = 1 AND b = 2能用到,WHERE b = 2 AND c = 3就用不到,因为跳过了最左边的 a。

但很多人在理解"最左前缀"时会犯一个直觉错误,以为"只要 SQL 里写了最左列就行"。实际上最左前缀指的是查询条件中可用的索引列范围是从最左开始连续的。WHERE a = 1 AND c = 3虽然用了 a,但 b 条件断开之后,c 就没法走索引了,优化器只能在 a 命中的结果里对 c 做过滤。这被称为"索引中断"。

联合索引设计时有一个常见的"口诀":把等值查询的列放前面,把范围查询的列放后面;把区分度高的列放前面,区分度低的放后面。原因在于联合索引中键值是有序的,等值条件可以精确定位范围,而范围条件会终止索引继续向下匹配。举个例子,索引(status, create_time)在业务上常用来查"某个状态下的订单且按时间排序",这时候WHERE status = 1 AND create_time > '2024-01-01'就可以很好地利用这个索引:先定位 status=1 的分支,再顺着有序的 create_time 扫过去,非常自然。

注意:联合索引的列顺序一旦确定,并不是说"随便排都一样"。同样的两个列,(a, b)和(b, a)是语义完全不同的两个索引,能覆盖的查询模式也完全不同。设计前先花几分钟梳理业务里的高频查询组合,比上线后反复加索引省事得多。

2.3 覆盖索引:减少回表的实战价值

前面提到,二级索引叶子节点存的是"索引列 + 主键",如果查询需要的数据恰好全部在索引里,连回表都省了。这被称作覆盖索引。很多人理解覆盖索引只停留在"哦,知道这个概念"的层面,实际优化时却不知道怎么操作。

来看一个具体场景。假设订单表上有一个联合索引(user_id, status),业务上高频查询是统计某个用户某个状态下的订单数:

SELECT COUNT(*) FROM orders WHERE user_id = 123 AND status = 2;

这个查询要的只有user_id和status,两个列都已经在联合索引里,InnoDB 直接在索引 B+ 树上遍历匹配的叶子节点就能返回计数,不需要回表。反之如果查询是:

SELECT order_amount FROM orders WHERE user_id = 123 AND status = 2;

order_amount不在索引里,那 MySQL 就得拿着匹配到的主键回表,把每一行都查一遍才能取到金额。这个差距在数据量大时非常明显——我优化过一个统计接口,加了覆盖字段之后,耗时从 1.8 秒降到了 80 毫秒,原因就是省掉了十几万次回表。

所以在设计联合索引时,不是只考虑WHERE条件里的列,还要把高频查询里SELECT的列考虑进去。这就是所谓的"索引列包含查询所需的所有列"。但也不要走向另一个极端:为了覆盖索引把所有常用列都塞进一个索引。索引列过多会让 B+ 树的节点变大、层级变深,反而降低检索效率。一般原则是覆盖那些真正高频的、核心的查询场景,而不是面面俱到。

2.4 前缀索引与函数索引

字符串类型的列,尤其像title、content、url这类比较长的字段,如果整列建索引,每个索引条目都要存很长的字符,索引体积会很大,而且 B+ 树里字符串比较的开销也高。这时的常用手段是前缀索引:只对字符串的前 N 个字符建索引。

CREATE INDEX idx_url_prefix ON pages (url(20));

前缀索引的取舍在于 N 的选择。N 太小,区分度不够,过滤效果差;N 太大,省空间效果不明显。通常可以通过统计不同前缀长度的基数来判断。比如:

SELECT COUNT(DISTINCT LEFT(url, 10)) AS c10, COUNT(DISTINCT LEFT(url, 20)) AS c20, COUNT(DISTINCT LEFT(url, 30)) AS c30 FROM pages;

和整列的唯一值总数做对比,找到一个"失真率"可以接受的长度。我一般要求前缀区分度达到整列区分度的 90% 以上。不过要提醒一点:前缀索引无法用于覆盖索引(因为索引里存的不是完整值),也无法用于排序和分组。如果查询只是等值匹配,前缀索引通常够用;如果还需要ORDER BY这个字符串字段,那前缀索引帮不上忙。

函数索引是 MySQL 8.0 才正式支持的。它的语法很直接:

CREATE INDEX idx_year ON orders ((YEAR(create_time)));

如果你经常按YEAR(create_time)或者DATE_FORMAT(create_time, '%Y-%m')这类函数处理后的结果来过滤,建函数索引能让这些查询不再失效。更早的版本面对"where 条件里对索引列用了函数"这种场景基本无解,这也是索引失效最主要的原因之一。

3. 索引设计与优化:从执行计划开始的完整思路

3.1 EXPLAIN 是第一步:核心字段怎么读

设计索引不能靠猜。无论是排查慢查询还是验证新索引的效果,第一步永远是看执行计划。MySQL 提供EXPLAIN命令,加在 SELECT 查询前面,就能看到优化器打算怎么执行这条 SQL。

EXPLAIN SELECT user_id, status FROM orders WHERE user_id = 123 AND status = 2;

输出里最值得关注的字段是这些:

  • type:访问类型。从好到差大致是system>const>eq_ref>ref>range>index>ALL。ALL就是全表扫描,是我们要消灭的对象;index虽然用了索引,但属于遍历整个索引树,也不算高效;常见的有效类型是ref(等值匹配二级索引)和range(范围匹配)。
  • key:实际用到的索引名。如果为 NULL,说明这条 SQL 没用上任何索引。
  • rows:优化器预估需要扫描的行数。这个值不是精确的,但可以作为参考,通常希望它远小于表的总行数。
  • Extra:这里面藏着很多重要信息。出现Using index说明是覆盖索引扫描,不需要回表;出现Using where说明在存储引擎返回结果后又做了一层条件过滤;出现Using filesort说明排序没有用上索引,效率堪忧;出现Using temporary说明用了临时表,常见于某些 GROUP BY / DISTINCT 场景。

有经验之后,你扫一眼type和key就能判断这条 SQL 有没有救。我最常用的排查路径是:先看type是不是ALL,再看key是不是预期的索引,最后看Extra里有没有filesort和temporary。对应的解决思路分别是:加索引、调整索引列顺序、调整查询写法。

3.2 索引失效的七个高频场景

很多人建了索引,但 SQL 写得不合适,索引照样用不上。我把工作中遇到最多的索引失效场景列出来,每一条都是实际踩过坑总结出来的:

第一,对索引列使用函数或表达式。比如WHERE YEAR(create_time) = 2024,如果 create_time 上有索引,这个写法会让索引失效。解决办法是改成范围条件WHERE create_time >= '2024-01-01' AND create_time < '2025-01-01'。如果你必须要用函数,就像前面说的建函数索引。

第二,隐式类型转换。典型场景是索引列是字符串类型,但查询条件传了数字。比如phone列是VARCHAR类型,WHERE phone = 13800138000,MySQL 会把字符串列隐式转成数字再比较,导致索引失效。解决方法是保证查询参数的类型和列类型一致,这种问题在 ORM 框架里尤其容易埋雷。

第三,前模糊匹配。WHERE name LIKE '%张'这种以通配符开头的写法,B+ 树没法利用有序性来定位起点,只能全扫。但WHERE name LIKE '张%'是可以走索引的,因为字符串排序后,"张"开头的记录是连续的一段。区分这两者很重要。

第四,OR 条件连接。WHERE a = 1 OR b = 2这种写法如果 a 和 b 没有都建索引,优化器很容易放弃索引走全表扫描。替代方案是用UNION ALL拆成两个查询,或者确保 OR 两边都有可用索引。

第五,联合索引跳过最左列。这个前面已经提过,(a, b)联合索引上查WHERE b = 2是用不到的。

第六,NOT IN、!=、<>这些负向查询。它们一般会走全表扫描,除非匹配的行数占比极低,MySQL 认为还不如扫表快。相比这些负向写法,尽量把逻辑转成"正向枚举",比如NOT IN ('cancel')经常可以转成IN ('pending', 'paid', 'done')。

第七,数据量太少的表。优化器会算一笔账:如果走索引需要多次随机 IO,而全表扫描是顺序 IO,表又特别小,顺序扫可能更快。这是优化器主动选择不用索引,不是索引失效。

3.3 索引列怎么选:区分度与查询模式

选索引列,最核心的判断依据是区分度。一个列的取值范围越广、重复值越少,索引的过滤效果越好。极端反例是在性别列上建索引——区分度只有男女两种,查出来一半的数据,这时候走索引反而可能比全表扫描还慢。

我习惯用一条 SQL 快速评估候选列的区分度:

SELECT COUNT(DISTINCT candidate_col) / COUNT(*) AS selectivity FROM big_table;

这个比例叫选择率。一般建议选择率高于 20% 的列才值得建索引。低于这个值,索引的收益会非常有限。

除了区分度,另一个关键指标是查询模式。分析你的业务 SQL,看哪些查询是等值过滤、哪些是范围过滤、哪些是排序分组。等值过滤的列适合放在联合索引前面,范围过滤的列适合放后面,排序分组的列也可以通过索引来避免filesort。

举个例子,一个订单列表页,前端传user_id(等值)、status(等值)、create_time(排序),那联合索引设计成(user_id, status, create_time)就是很自然的方案。优化器可以先用前两列做等值定位,再按第三列的有序性直接返回排序结果,一条索引同时解决了过滤和排序两个问题。

3.4 冗余索引与过度索引:隐形成本

索引不是免费的。每建一个索引,写入的时候就要多维护一棵 B+ 树。对一张高频插入、更新的表来说,索引数量从 3 个涨到 10 个,写入性能会有肉眼可见的下降。我遇到过一张线上日志表,因为历史遗留问题攒了 12 个索引,每秒写入从 2000 条掉到 700 条。所以"索引越多越好"是绝对的反模式。

冗余索引是另一个隐蔽的问题。联合索引(a, b)建好之后,单独的索引a实际上就是冗余的,因为(a, b)已经能覆盖所有以 a 为最左列的查询。发现冗余索引可以通过查询所有索引的信息来排查:

SELECT table_name, index_name, GROUP_CONCAT(column_name ORDER BY seq_in_index) FROM information_schema.statistics WHERE table_schema = '你的库名' AND table_name = 'orders' GROUP BY table_name, index_name;

然后对比哪些索引的最左列是另一个索引的子集,就可以安全删掉冗余的那个。但这步操作要特别谨慎,因为删除索引之前必须用EXPLAIN验证所有相关 SQL 仍然走预期的索引。

注意:生产环境删索引一定要收敛在低峰期操作,并且先备份、先 EXPLAIN 验证、分批执行。删之前再问自己一遍:这个索引真的没人用了吗?很多线上事故都源于"我以为这个索引没人用"。

4. 实操案例:一个慢查询从发现到优化的完整过程

4.1 问题复现与慢查询日志定位

某天线上反馈一个订单列表接口响应变慢,页面从原来的 300ms 涨到了 3 秒以上。我先登录数据库打开慢查询日志的开关,确认当前阈值设置:

SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;

运行一段时间后,从慢查询日志里捞出了这个高频慢 SQL:

# Query_time: 2.87s Lock_time: 0.0002s Rows_sent: 20 Rows_examined: 482934 SELECT order_id, user_id, status, amount FROM orders WHERE user_id = 12345 AND status = 2 ORDER BY create_time DESC LIMIT 20;

注意Rows_examined: 482934,意思是这条查询为了返回 20 行,扫描了 48 万行。这是典型的"查得少、看得多"问题,问题就出在数据访问路径上。

我们先冷静分析一下这个表的结构:orders 表数据量约 300 万行,现有的索引是idx_user_id(单列索引)和idx_create_time(单列索引)。WHERE条件里有user_id和status两个等值条件,排序用create_time。看起来三个字段都有索引,怎么还会扫 48 万行?这正是执行计划能告诉我们答案的地方。

4.2 执行计划分析定位根因

直接在慢 SQL 前面加 EXPLAIN:

EXPLAIN SELECT order_id, user_id, status, amount FROM orders WHERE user_id = 12345 AND status = 2 ORDER BY create_time DESC LIMIT 20;

关键输出如下:

字段值
typeref
keyidx_user_id
rows482934
ExtraUsing where; Using filesort

看这三行信息,问题就清楚了。key用的是idx_user_id,说明优化器选择了 user_id 这个单列索引,但user_id = 12345这个过滤条件只把范围缩小到了该用户的全部订单,而该用户有 48 万条订单记录。status = 2这个条件没法在索引内部过滤,只能靠Using where逐行判断;排序字段create_time也不在索引里,所以出现了Using filesort,拿到匹配数据后还要额外排序。

这里有两个优化方向:一是让status也在索引里参与过滤,二是让排序走索引避免 filesort。最直接的办法就是建一个联合索引(user_id, status, create_time),同时覆盖三个关键操作。

4.3 索引方案设计与验证

我的设计方案是:

CREATE INDEX idx_user_status_time ON orders (user_id, status, create_time);

这个联合索引从左到右依次处理了等值条件 user_id、等值条件 status 和排序字段 create_time。B+ 树会先按 user_id 分组,再在组内按 status 排序,最后在相同的 (user_id, status) 记录里面按 create_time 有序排列。这样优化器定位到目标用户的特定状态分支后,顺着 create_time 的有序链表从尾部往前取 20 条即可,不需要额外的排序动作。

建完索引之后立即验证执行计划:

EXPLAIN SELECT order_id, user_id, status, amount FROM orders WHERE user_id = 12345 AND status = 2 ORDER BY create_time DESC LIMIT 20;

这次输出:

字段值
typeref
keyidx_user_status_time
rows246
ExtraUsing index condition

rows从 48 万降到了 246,Using filesort消失了。这里我顺手确认了一件事:amount字段不在索引里,所以 Extra 里没有出现Using index,查询定位到目标行之后仍需要回表取 amount。考虑到这个接口单次返回只有 20 行,回表 20 次的开销完全可以接受,没必要为了追求"完美覆盖索引"把 amount 也塞进去——那样会让索引体积变大,反而得不偿失。

4.4 优化前后数据对比

索引生效后的实测数据如下:

指标优化前优化后
Rows_examined482934约 250
查询耗时2.87s28ms
接口 P95 耗时3.2s180ms

执行计划里预估扫描 246 行,实际扫描约 250 行,和预估基本一致。查询速度提升了两个数量级。整个优化过程其实只做了一件事:把用户查询的过滤路径从"单列索引定位大范围+逐行过滤+外部排序"改成了"联合索引直接精确定位+有序扫描"。这就是索引设计最核心的价值——让查询的每一步都在索引结构里完成,而不是靠回表硬扛。

这个案例还有一个值得说的点:之前idx_user_id和idx_create_time两个单列索引其实是历史遗留的"随手索引"。在联合索引上线后,idx_user_id作为idx_user_status_time的最左前缀,已经属于冗余索引,我后续在低峰期将其删除。idx_create_time则被保留,因为还有其他的时间范围查询场景需要单独使用它。

5. 常见问题与排查技巧实录

5.1 索引失效排查速查表

实践里遇到各种奇怪问题,有时候明明有索引,优化器就是不走。我把高频问题整理成一张速查表,方便你排查时对照:

症状可能原因排查/解决方案
明明建了索引,EXPLAIN 显示全表扫描查询条件对索引列用了函数改成范围条件或建函数索引
等值查询中索引没用上隐式类型转换核对列类型与参数类型是否一致
LIKE 查询很慢通配符在开头确认是否可以用右侧通配符或全文索引
联合索引只命中部分条件查询跳过了最左列调整查询条件或按最左前缀重建索引
查询很快但 ORDER BY 慢排序字段不在索引中把排序列纳入联合索引
查询走了索引但还是慢回表行数太多用覆盖索引或缩小查询范围
优化器放弃索引表太小或返回行占比太高确认统计信息是否过期,必要时 ANALYZE TABLE

5.2 索引碎片与维护策略

索引用久了会出现碎片。原因是 InnoDB 的 B+ 树频繁插入、删除时会发生页分裂和页合并,导致数据页在物理上不连续,叶子节点的链表面临更多随机 IO。实际表现就是:索引的逻辑结构没变,但扫描效率下降了。

判断碎片程度可以查表的统计信息:

SHOW TABLE STATUS LIKE 'orders'\G

看Data_free字段,这个数值代表表中空闲的碎片空间。如果Data_free相对表大小占比很高,比如超过 10%,就该考虑整理一下了。

常见的碎片整理手段是:

ALTER TABLE orders ENGINE = InnoDB;

或者:

OPTIMIZE TABLE orders;

这两种方式本质都是重建表,会重新整理数据和索引,消除碎片。但要注意:它们都会锁表,大表执行时会阻塞读写。在 MySQL 8.0 中,ALTER TABLE ... ENGINE=InnoDB配合在线 DDL 的ALGORITHM=INPLACE已经可以做到一定程度的在线重建,但生产环境仍然建议放在低峰期。如果是千万级以上的大表,更稳妥的做法是使用在线工具分批次迁移,避免一次性锁表太久。

我的建议是:碎片整理不要按固定周期无脑做。先通过Data_free观察实际状态,同时关注是否真的出现了性能退化。有时候整理完碎片效果不明显,反而白白承担了锁表风险。这个操作应该由数据说话,而不是由"每隔三个月做一次"的惯例驱动。

5.3 几条值得长期坚持的索引规范

做完多个项目的索引优化之后,我总结了几条自己一直在用的规范,就当作分享给大家的实践清单。

第一,联合索引设计前,先列出业务里 Top 20 的高频查询 SQL,把WHERE、ORDER BY、GROUP BY涉及的列全部标记出来,再进行合并设计。不要每看到一个 SQL 就加一个索引,那只会让索引越堆越多。

第二,每一个索引都要能说清楚"它服务了哪几条查询"。如果说不清楚,大概率是冗余索引。我在代码评审时经常问这一句,能挡住不少无效索引的产生。

第三,写 SQL 时保持简单直白。避免在索引列上做函数运算、避免隐式类型转换、避免 OR 强行连接多个不相关的过滤条件、避免前模糊匹配。这些习惯一旦养成,很多性能问题根本不会出现。

第四,索引变更一定要走"先 EXPLAIN、再变更、再 EXPLAIN、再压测"的流程。上线前用模拟数据量验证执行计划是很多人忽略的一步——测试库数据量太小,优化器可能选择全表扫描,导致你根本无法判断索引是否真的生效。最好的做法是在测试环境灌入和生产规模相近的数据,或者至少在压测阶段使用接近真实的数据量。

第五,定期清理无效索引和碎片。每季度做一次索引盘点,结合慢查询日志和 information_schema.statistics 检查是否存在冗余索引、是否有索引长期未被使用。监控慢查询日志的变化趋势,比定期扫描所有 SQL 更高效。

老实说,索引这门功夫没什么高深莫测的秘诀,核心就是理解数据结构、理解优化器、理解数据分布,然后在三者之间找到一个平衡点。我见过不少团队把索引文档写得天花乱坠,实际线上还是一堆慢查询,问题往往出在"设计靠脑补、验证靠感觉"。如果你能养成每个慢查询都打开 EXPLAIN 看一眼的习惯,把每个索引都当作有名字、有职责、有生命周期的数据对象来管理,MySQL 索引这块基本上就不会再给你添乱了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 3:05:25

医保大模型电话客服系统厂商资质核验与选择指南

医保大模型客服系统厂商资质核验核心维度随着医保服务数字化转型加速&#xff0c;各地医保机构的热线咨询量逐年攀升&#xff0c;传统人工客服面临高峰期接通率低、政策解答不统一、人力成本高企等痛点&#xff0c;大模型电话客服系统成为医保智能化升级的核心选型方向。采购这…

作者头像 李华
网站建设 2026/10/14 3:05:23

Bokeh实战:用Python打造可缩放拖拽的交互式数据看板

你如果拿 Python 做过一段时间数据处理&#xff0c;大概率被 Matplotlib 支配过。画个折线、散点不算难&#xff0c;难的是你要给业务方看一份能自己缩放、拖拽、看悬停数值的图表&#xff0c;Matplotlib 往往就有点束手无策。我第一次遇到这个需求时&#xff0c;第一反应是去学…

作者头像 李华
网站建设 2026/10/14 3:04:11

DrissionPage:基于 Python 的自研内核网页自动化工具全解析

网页爬虫浏览器控制 【免费下载链接】DrissionPage Python based web automation tool. Powerful and elegant. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/dr/DrissionPage 点击查看 免费下载 导读 DrissionPage 是当前仓库 DrissionPage 所承载的一款基于 Pytho…

作者头像 李华
网站建设 2026/10/14 3:03:58

SLF4J在SpringBoot中的日志门面实践与优化

1. SLF4J在SpringBoot中的核心价值作为Java生态中最主流的日志门面框架&#xff0c;SLF4J(Simple Logging Facade for Java)在SpringBoot项目中扮演着关键角色。不同于直接调用Log4j或Logback等具体实现&#xff0c;SLF4J通过门面模式提供统一的日志API&#xff0c;这种设计带来…

作者头像 李华