MySQL索引从原理到实战:B+树、执行计划与慢SQL调优

刚接手一个线上系统的时候,我遇到过一条查询跑两秒多的SQL,全表扫描扫了三百多万行,而业务要求响应在一百毫秒内。当时的我连EXPLAIN都不会看,被DBA指着鼻子说"你这表连索引都没建对"。从那以后,我把MySQL索引从数据结构到执行计划彻底啃了一遍,也踩了不少坑。这篇东西就是基于这些经验整理出来的,想把索引这件事从头到尾讲透,适合刚入门的朋友,也适合那些会用索引但说不清原理的开发同学。

我不会只给你一堆结论,而是把"为什么"也讲明白:为什么B+树成为主流、为什么最左前缀会失效、为什么覆盖索引能省一次回表。这些都是面试高频题,更是日常调优真正用得上的底层逻辑。

1. 慢SQL的根源:全表扫描为什么那么贵

1.1 磁盘IO才是第一成本

很多写业务代码的同学,对数据库性能的认知停留在"加了索引就快,没加索引就慢"。这没错,但不够。真正决定查询快慢的,是数据从磁盘读到内存的次数,也就是磁盘IO。

MySQL默认的数据页大小是16KB,InnoDB存储引擎的数据以页为单位组织。一次全表扫描,意味着要把表所在的每一条记录所在的页都读一遍。假设一行记录平均200字节,一个16KB的页大概能装下80行,三百万行就要37500个页。就算这些页都在缓冲池里,也要走一遍全部页的扫描逻辑;如果其中一部分不在内存里,就要走磁盘IO,一次随机读磁盘的耗时大概是10毫秒级别,碰上机械盘更慢。

索引干的事情,本质上是把"大海捞针"式的查找,变成"按目录翻书"式的定位。有了合适的索引,MySQL可以只读取符合条件的极少数记录所在的页,IO次数从几万次降到几次。

1.2 没有索引时,MySQL是怎么找数据的

在没有可用索引的情况下,InnoDB只能从第一条记录开始,一条一条往下扫,判断每一行是否满足WHERE条件,这个过程就是全表扫描(type=ALL)。这不是MySQL"笨",而是它确实没有更好的信息可以用。

有人说,那我可以把查询结果缓存起来啊。且慢,查询缓存早已在MySQL 8.0中被移除,因为在高并发写入场景下缓存失效太频繁,收益远小于维护成本。真正的出路就是让存储引擎能用更少的IO定位数据,这就是索引存在的意义。

1.3 换个角度理解:索引是空间换时间的典型

索引本质上是额外存储的一种数据结构,它占用磁盘空间,带来写入时的维护开销,但换来读取时的大幅提速。这和字典的部首目录、书本的目录页是一个道理:目录本身也占了几页纸,但你不会因为多这几页纸而觉得书变贵,因为你翻目录要快得多。

理解了这一点,后面很多问题就有了统一答案:为什么不能建太多索引?因为写入时每多一个索引,就要多维护一棵树。为什么不要在大字段上建索引?因为树会变得又大又深。

2. 数据结构选型推演:B+树凭什么成为主角

2.1 候选者逐个淘汰的过程

索引的底层数据结构,历史上出现过Hash、二叉树、红黑树、B树,最后B+树胜出。我们来逐个看它们淘汰出局的原因。

Hash索引:对索引列计算Hash值,定位速度极快,O(1)复杂度。但它只支持等值查询(=、IN、某些情况下),一碰到范围查询(>、<、BETWEEN)就无能为力了,因为Hash值是无序的。另外它不支持最左前缀匹配,不支持用索引排序。业务查询里范围查询太常见,所以Hash索引只能作为辅助,比如InnoDB的内存自适应Hash索引,咱平时建索引用不上它。

普通二叉树/红黑树:二叉树在极端情况下会退化成链表,红黑树虽然能保持平衡,但它的问题是"一个节点只存一个键值",树的层数随数据量增长太快。MySQL的数据是存磁盘的,每往下一层就要做一次磁盘IO。两百万行数据,红黑树大概要二十多层,意味着最坏情况下要二十多次磁盘IO,这个成本不可接受。

B树(多路平衡查找树):一个节点可以存多个键值,每个节点有多个子树分支,层数被大幅压低了。B树已经解决了"层数太深导致IO太多"的问题,为什么还是败给了B+树?关键在于B树的每个节点既不存索引键,又存实际数据,而且为了容下数据,每个节点的索引键数量变得有限。更致命的是,B树做范围查询时,需要在中序遍历过程中不断在树的各层节点间来回跳转,等于不断发起随机IO。

2.2 B+树的胜出点:两条核心差异

B+树和B树的结构上只有两个核心差异,恰恰是这两个差异,让MySQL选择了它:

第一,非叶子节点只存键值不存数据。这样单个节点能容纳的键值数量大幅增加。InnoDB一个页16KB,假设一个索引键8字节加上指针6字节左右,一个叶子节点能存放上千个键,三层B+树就能支撑上千万行数据。这意味着绝大部分查询只需要两到三次磁盘IO就能命中。

第二,叶子节点通过双向链表串起来。所有数据都存在叶子节点,并且叶子节点之间按顺序链接。范围查询只需要找到起点,然后沿着链表顺序往下读即可,连续IO的性能远好于随机IO。ORDER BY排序在很多场景下也能直接利用这个有序链表,省掉一次filesort。

2.3 关于B+树的两个补充认知

关于树的高度:三层B+树大概能存放多少数据?我们粗算一下。非叶子节点的每个条目大约占键值(比如BIGINT做主键是8字节)加指针(约6字节),按14字节算,一个16KB页大约能放1170个条目。假设叶子节点每行数据1KB,一个页放16行;两层非叶子节点加一层叶子节点,总共能放大约1170×1170×16,也就是两千多万行。三层满的状态下只要3次IO,非常高效。

关于页的分裂与合并:B+树插入时如果节点满了,会发生页分裂,把一个节点的部分数据挪到新页中;删除导致节点过空时,也可能发生页合并。这个机制意味着索引列的乱序插入会带来大量页分裂,碎片化严重,影响性能。这也是为什么主键推荐自增而不是UUID——UUID随机性高,插入时页分裂频繁,同时导致索引树节点之间逻辑相邻但物理离散,查询性能也会打折扣。

3. 索引家族全景:从主键到二级索引,每一类都派什么用场

3.1 聚簇索引:InnoDB的"表即索引"

很多人背概念时,把"主键索引"和"聚簇索引"划等号,严格说聚簇索引的定义更宽。InnoDB中,聚簇索引的叶子节点就是整行数据,表数据就是按主键顺序组织在这棵B+树上的。所以InnoDB表实际上是一棵以主键为索引键的大B+树。

如果没有显式主键,InnoDB会找第一个非空的UNIQUE列作为聚簇索引;要是还没有,就自动生成一个隐藏的ROWID。这个设计解释了为什么建表时强烈建议显式指定自增主键:主键是物理存储顺序依据,自增保证顺序插入,页分裂最少;业务列的随机值做主键,则会导致频繁的页分裂和移动。

3.2 二级索引:从"书签"到"正文"的两次定位

非聚簇索引(也叫二级索引、辅助索引)是另外建立的B+树,它的叶子节点存储的是索引键值加主键值,不是完整行数据。当你通过二级索引查询时,先在这棵索引树上找到主键,再拿着主键去聚簇索引树里找完整行,这个过程叫回表。

这里有个关键认知:每一条二级索引都隐藏地包含主键列。假设你给name建了一个索引,索引的叶子节点上实际存的是name和id两个字段。这意味着如果查询只需要name和id,就不需要回表,直接从索引里取就行了。这个特性正是覆盖索引(后面细讲)能省IO的根本原因。

3.3 各类索引的适用场景与禁忌

普通索引(INDEX):最常用的索引类型,不强制唯一性,适用于WHERE、JOIN、ORDER BY中频繁使用的列。允许重复值,这正好对应"一个姓氏有很多人"的场景。

唯一索引(UNIQUE INDEX):和普通索引的区别是列值不能重复。它除了加速查询,还承担了完整性约束的作用,比如用户表的手机号、订单表的订单号。注意,唯一索引不能为空值可重复:InnoDB允许同一列有多个NULL值。

主键索引(PRIMARY KEY):唯一索引的特例,且不允许NULL。一个表只能有一个主键,但可以有多个唯一索引。

前缀索引:对于TEXT、VARCHAR等大字段,可以只对列的前N个字符建索引,大幅缩小索引体积。代价是可能区分度不够,后文会讲怎么确定前缀长度。

全文索引(FULLTEXT):用于中文分词、搜索场景的倒排索引,注意它是倒排索引而不是B+树。MySQL自带的全文索引在中文场景下分词能力一般,生产上大家更常借助专门的搜索引擎,这里不展开。

合理选择索引类型本身就能避免很多问题。给一个低区分度的列(比如性别)建普通索引,查询优化器扫一遍算了半天,发现走索引跟全表扫描差不多甚至更差,最后还是选全表扫描,这个索引等于白建。

4. 失效场景大起底:索引写了,但查询用不上,多半是这几个原因

4.1 最左前缀失效:复合索引的"第一列"被破坏

复合索引(a, b, c)相当于建立了a、a+b、a+b+c三套索引逻辑,但它的根基是最左前缀原则:查询条件里必须从第一个列开始连续匹配,且中间不能断开。最经典的失效场景就是跳过第一列,比如WHERE b=1 AND c=2,这样的查询无法使用这个复合索引。

为什么?回顾B+树的结构:复合索引的键值按a排序,a相同再按b排序,b相同再按c排序。如果不知道a的范围,你就无法确定b这个字段在树里哪个区间是"连续"的,自然只能全表扫描。同理,WHERE a=1 AND c=2会走索引吗?会,但只用到a=1的索引部分,c=2的部分没生效,要在a确定的区间内做过滤。

实操经验:设计复合索引时,把等值查询列放在前面,范围查询列放在后面。比如经常执行WHERE status=1 AND create_time>'2024-01-01',复合索引应该建(status, create_time),因为status是等值条件,能最大化利用索引的有序性。

4.2 隐式类型转换:你以为查的是数字,MySQL看到的是字符串

经典的坑:索引列是varchar类型,查询条件却写成了WHERE phone=13800138000。MySQL会对列做隐式转换,相当于把索引列包了一层CAST函数,导致索引失效。

判断时注意:索引列参与了函数或者表达式运算,索引就会失效。因为B+树存储的是原始列值,不是函数处理后的值,优化器无法直接利用树的有序性去定位。

同样的还有WHERE DATE(create_time)='2024-01-01' 这种对索引列使用函数的情况。正确做法是改写成范围查询WHERE create_time>='2024-01-01' AND create_time<'2024-01-02'。这不仅能走索引,还能用上索引的range定位能力。

4.3 LIKE模糊查询:前缀匹配能走,中间通配符直接放弃

WHERE name LIKE '张%'是可以走索引的,因为B+树按照字典序排列,'张%'对应一个明确的字典区间,MySQL可以在树中快速定位到起点然后顺序扫描。但WHERE name LIKE '%张'或者WHERE name LIKE '%张%'就失效了,因为通配符在开头时,优化器不知道起点在哪,只能全表扫描。

顺带提一嘴,能用前缀匹配的就别用后缀匹配;实在需要全文搜索的场景,请考虑全文索引,而不要指望普通索引。

4.4 联合索引中的范围列:右边全部作废

复合索引(a, b, c),如果查询是 WHERE a=1 AND b>10 AND c=2。a用的是等值,b用了范围查询,c还能用上索引吗?不能。因为b>10范围内的c字段在B+树里是无序的。这个规律在B+树的复合键排序规则下是必然的。

很多面试题喜欢问这个,实际设计中也要记住:尽量把范围查询列放到复合索引的最后。如果你的查询模式固定是a=1 and b between and c=2,那复合索引应该设计成(a, c, b),把范围列b放最后。

4.5 负向查询与OR场景:优化器的"预算"选择

WHERE status <> 1、NOT IN、NOT LIKE这类负向查询,在某些情况下会被优化器放弃索引。原因是负向查询往往匹配大量行,走索引反而要做大量回表随机IO,不如全表扫描顺序读高效。

OR条件的坑更微妙:WHERE a=1 OR b=2,即使a和b各自都有索引,MySQL也未必能合并使用两个索引,除非用到**索引合并(Index Merge)**优化。早期版本对OR的处理比较弱,经常直接全表扫。如果你确认a、b都有独立索引且区分度高,可以通过EXPLAIN观察Extra列是否显示Using union或Using intersect来判断是否走了索引合并。更稳妥的做法是改写为UNION ALL分支查询,或者重新设计索引。

4.6 关于IS NULL和IS NOT NULL:不是绝对失效,要看数据分布

有个流传很广的说法是"IS NULL不走索引",这在MySQL较新版本里并不绝对。优化器会基于索引统计信息估算,返回行数极小时可能用索引,返回行数多时选择全表扫描。所以别死记结论,要用EXPLAIN实测。这也引出一个通用的调优态度:一切以执行计划为准,不要在脑子里背一堆注定会过时的"铁律"。

5. 聚簇索引、回表与覆盖索引:用好这三者的关系,查询能快一个量级

5.1 回表的成本账,比你想象中贵

刚才说了,二级索引叶子节点存的是索引键+主键值。如果你查询的列不在这个二级索引里,就得靠主键到聚簇索引树里再查一次完整行。一次回表就是一次聚簇索引树的B+树搜索,大约是两到三次磁盘IO。如果我们通过二级索引筛选出一条记录回表一次还好,但如果筛选出几千几万条,就要回表几千几万次,这是真正的性能灾难。

这里有个真实对比:一张一千万行的订单表,WHERE user_id=12345 ORDER BY create_time DESC LIMIT 20。如果你只建了(user_id)单列索引,MySQL需要先取出该用户的所有订单(可能几百上千条),每个都回表拿到整行,再在内存里排序取前20条。如果我改成(user_id, create_time)复合索引,叶子节点上已经按create_time排好序,直接按索引顺序取出前20条记录回表,连filesort都省了。查询耗时从几百毫秒直接降到几毫秒。

5.2 覆盖索引:把"查询列"塞进索引树

覆盖索引指的是查询所需的全部列,都能在二级索引树里直接拿到,不用回表。Extra列显示Using index就代表发生了覆盖索引。

举个例子:表里有a、b、c三列,复合索引(a, b),执行SELECT a, b FROM t WHERE a=1。因为索引叶子节点有a和b,而且二级索引里还附带主键id,所以这个查询完全不需要碰聚簇索引树。

这里面有个容易被忽略的小知识:只要二级索引的键列和主键列cover住查询列,就算覆盖索引。所以SELECT的列越少,越容易借助覆盖索引。很多开发习惯SELECT *,这会让覆盖索引失效,被迫回表拿所有列。

5.3 索引下推(ICP):MySQL 5.6后的重要优化

有了复合索引(a,b),查询WHERE a=1 AND b>10。在MySQL 5.6之前,InnoDB会先用a=1从索引树定位到一批记录,然后逐条回表取出整行,再对b>10做过滤。5.6引入的ICP(Index Condition Pushdown)把b>10的过滤下推到存储引擎层:在索引遍历过程中,直接判断索引字段b是否满足条件,满足的才回表。

所以这条SQL在5.6以后的版本里,回表的次数会大幅减少,Extra列通常显示Using index condition。这个优化提醒我们:复合索引的设计不仅影响定位效率,还影响回表次数。把过滤条件里最常用的列放进复合索引,往往能同时享受ICP的收益。

5.4 一个辅助判断技巧:看Extra列的三个关键词

平时用EXPLAIN看执行计划时,Extra列的几个关键词要能一眼识别:

  • Using index:覆盖索引,全程没回表,最优。
  • Using index condition:走了索引下推,部分过滤在索引层完成,回表次数减少,很好。
  • Using where:索引定位后,还需要回表拿数据再做一次过滤,注意这通常意味着有额外的非索引列条件。
  • Using filesort:排序没用上索引,在临时表做了排序,小数据量还好,大表是性能杀手。
  • Using temporary:用了临时表,常见于GROUP BY、DISTINCT、多表JOIN等场景。

这个表观之外,再叠加type列综合判断,就能对一条慢SQL的性能问题有个八九不离十的判断。

6. 让EXPLAIN开口说话:执行计划的阅读方法与实战判断

6.1 执行计划里到底该看哪几列

EXPLAIN是MySQL调优最基础的工具,在SQL前面加上EXPLAIN,就能看到优化器选定的执行路线。列很多,不是每列都重要,我的习惯是重点看这几列:

type列:访问类型的等级排序基本是 system > const > eq_ref > ref > range > index > ALL。system和const表示通过主键或唯一索引精准定位到一行,这是最优的;ref是普通等值匹配;range是范围扫描,比ref多扫描一点但还能接受;index表示扫描了整棵索引树;ALL是全表扫描,需要警惕。

key列:实际用到的索引。很多人只看这一列,以为用了索引就万事大吉,其实关键要看type和rows。用了索引但type=index,等于把整棵索引树从头扫到尾,比全表扫描也好不到哪去。

rows列:优化器估算的需要扫描的行数。这个值越大,SQL越危险。它只是估算值,不一定精准,但用来对比不同写法的成本高低很有参考价值。

Extra列:包含了前面提到的Using index、Using where、Using filesort等关键信号。

6.2 实战案例:同一个查询,三种写法的执行计划差异

假设订单表有user_id、status、create_time三列,都建了单列索引。

查询一:SELECT * FROM orders WHERE user_id=123 AND status=1。优化器可能选择用户ID索引(ref),先定位用户的所有订单,再回表对status过滤,Extra显示Using where。这意味着user_id索引生效,但status条件没被利用。

查询二:SELECT user_id, status, create_time FROM orders WHERE user_id=123。如果建立了(user_id, create_time)复合索引,这个查询的查询列正好都在索引里,Extra显示Using index,不需要回表,性能最优。

查询三:SELECT * FROM orders WHERE DATE(create_time)='2024-01-01'。create_time索引列被函数包裹,type=ALL,全表扫描,rows很大。改写成范围查询后type=range,rows大幅下降。

每遇到一个慢SQL,我都建议把几种候选写法都EXPLAIN一遍,比较rows和Extra,选择执行计划最合理的那个。别凭感觉调优,让优化器用执行计划告诉你答案。

6.3 关于索引统计信息:为什么有时候执行计划"不听话"

有一种让人困惑的场景:明明有索引,EXPLAIN显示没走,或者走了却选了另一个索引。这跟优化器依赖的索引统计信息有关。InnoDB通过采样估算索引的区分度(cardinality),如果统计信息过期,优化器就可能做出错误的判断。

解决办法很简单:ANALYZE TABLE 表名可以重新统计索引信息。另一种情况是,优化器比较了走索引和全表扫描的成本后,真的觉得全表扫描更划算——比如这个列区分度太低。这时候骂优化器"笨"是没有用的,要做的是提升索引本身的质量或调整查询写法。

7. 索引设计实战:从选字段到排顺序,一张表该怎么规划索引

7.1 索引字段选择的三个原则

第一,区分度优先。区分度高的字段才能快速把范围缩小到极小,比如用户名、手机号、订单号。区分度极低的字段如性别、状态码,单独建索引通常没什么用,而是应该作为复合索引的靠前等值列。

第二,高频查询优先。不是所有字段都要建索引,而是那些在WHERE、JOIN、ORDER BY、GROUP BY中反复出现的字段。如果一个字段只出现在SELECT列表里,它就不应该进索引——除非你想做覆盖索引。

第三,选择性计算。可以用SELECT COUNT(DISTINCT col)/COUNT(*) FROM table估算选择性,值越接近1越好。对TEXT、VARCHAR长字段,先算不同前缀长度的区分度,确定合适的前缀长度再建前缀索引。

7.2 复合索引的列顺序:等值在前,范围在后

这是个反复强调的原则,核心依据是B+树的有序性:等值条件下,复合索引可以继续利用下一列的有序性;一旦遇到范围条件,这一列之后的所有索引列都无法用于定位和排序。

举例说明,业务上最常见的查询是:WHERE user_id=? AND status=? AND create_time BETWEEN ? AND ?。复合索引应该建成(user_id, status, create_time)。前两列(等值)能精确定位分区,第三列(范围)用range扫描,效率非常高。

7.3 索引不是越多越好:每个索引都有写入成本

很多刚接触索引的同学,喜欢给每个出现在WHERE里的字段都建一个单列索引,这是错误的。原因有三:写入时每个索引都要同步更新,插入、更新、删除性能下降;索引占用磁盘空间;优化器面对过多可选索引时,选错索引的概率也增加。

一个表保留四五个索引通常足够。设计时要问自己:这些索引的查询覆盖是否重叠?比如已经建了(a,b)复合索引,还有必要单独建a单列索引吗?通常不需要,因为复合索引的最左前缀可以覆盖a单列查询的场景。

举个例子,我接手过一个表,二十多个索引,很多字段被建了两三遍单列索引。整理后保留五六个,写入性能提升了30%多,查询没有受到影响。删索引前务必确认没有漏掉任何一条依赖它的业务SQL。

7.4 主键的设计:自增整数永远是最稳的选择

关于主键,业内讨论误解很多,但结论相对统一:InnoDB表首选自增整数主键。原因已经在B+树的部分讲过——插入顺序性和主键有序性天然一致,避免页分裂和页移动。

业务主键(如订单号、身份证)可以被设计为逻辑主键,用UNIQUE约束保证唯一性,同时用自增id作为InnoDB的聚簇索引键。实践中这种"逻辑主键+物理自增主键"的组合能有效兼顾业务语义和存储引擎物理组织。

7.5 关于排序的索引设计

ORDER BY能不能走索引,取决于排序字段是否和索引列顺序一致,以及排序方向是否一致。比如索引是(a, b),ORDER BY a, b就能走索引,ORDER BY b就会filesort,因为b在树上的顺序被a主导。ORDER BY a DESC, b DESC在MySQL 8.0中也能倒序利用索引,因为8.0支持降序索引。

如果一条SQL既要WHERE过滤又要ORDER BY排序,优先把WHERE的等值列放索引前面,ORDER BY列放索引后面,这样"过滤+排序"都用上了索引,是性能上最理想的组合。

8. 索引优化的几个深入方向:不走全表扫描之后,还有哪些提升空间

8.1 索引条件下推、MRR和自适应Hash的配合

聊到MySQL 5.6以后的优化器,除了前面提到的索引下推(ICP),还有个**MRR(Multi-Range Read)**优化值得知道。简单说,MRR会把多条需要回表的记录的主键先收集起来排序,再统一去聚簇索引里回表,把多次随机IO变成更接近顺序IO的形式,大幅减少磁盘寻道开销。

另外InnoDB还有一个自适应哈希索引(Adaptive Hash Index),它会在内存中为频繁访问的索引页建立Hash映射,把B+树上的等值查找从多次树遍历变成一次Hash定位。我们无需手动控制,也不需要为它建任何东西,理解即可。

8.2 索引统计信息与ANALYZE的时机

前面提过ANALYZE TABLE可以更新索引统计信息。那么什么时候需要主动执行?经验和直觉告诉我:大批量数据导入、大量UPDATE删除后、以及明显感觉执行计划"反常"时。正常情况下InnoDB会在打开表时自动采样统计,但表数据发生剧烈变化时,自动采样的频率往往跟不上,手动ANALYZE是低成本高收益的保养动作。

8.3 SQL改写:索引到位之后,查询写法也要跟进

索引建好了,不等于所有SQL都会变快。MySQL优化器虽然越来越智能,但还没到能自动重写一切SQL的程度。几个常见的改写思路:

  • WHERE条件里有OR且无法走索引时,改写为UNION ALL;
  • 避免在索引列上做数学运算、函数处理;
  • 分页深翻页场景,把LIMIT 2000000, 20改写为基于上次查询最大ID的WHERE id>上次最大ID LIMIT 20,因为后者可以一直沿着索引走;
  • JOIN时,被驱动表的关联列上要有索引,否则驱动表每一行都触发被驱动表全表扫描,后果是灾难级的。

8.4 监控索引的实际使用情况

MySQL的Performance Schema和sys.schema_unused_indexes可以帮助找到从未被使用过的索引。这类索引不仅浪费空间,还拖慢写入。定期跑一下SELECT * FROM sys.schema_unused_indexes,把长期无查询命中的索引清理掉,是DBA阶段的日常活儿。

还有个更直接的方法:用SHOW INDEX FROM 表名查看索引基数(cardinality)和区分度。如果基数明显异常低,这个索引基本是无效的。

9. 最后再分享几条保命的索引经验

按老规矩,最后说几点纯实操层面的体会,算是给前面理论的落地补充。

第一,每建一个索引,都要有对应的SQL场景。用"假设某天有人问,你这个索引是为哪条SQL建的"来自检。答不上来,说明这个索引大概率该删。

第二,上线前用慢日志和EXPLAIN做一次全面体检。在测试环境造出足够大且分布贴近生产的数据,模拟真实查询跑一遍,重点看rows、type、Extra三列。很多线上事故,都是因为测试环境数据量太小,里头的索引问题完全暴露不出来。

第三,关于索引失效的说法,别全信网上的旧知识。MySQL版本更新迭代很快,早期版本的优化器行为在不同版本之间变化很大。一切以当前版本下EXPLAIN的实际输出为准,自己动手验证一次比背一百条结论都有用。

第四,把索引和业务查询模式绑定在一起设计。不要等SQL慢了才回头补索引,而是在开始设计一张新表时,先列出这张表最核心的3到5条查询语句,然后针对这些查询设计复合索引。事后补索引不仅痛苦,还可能因为数据量太大导致加索引锁表。

第五,如果一条SQL怎么调都慢,问题可能不在索引而在SQL设计本身。比如必须要对三张大表做复杂JOIN和聚合。这时候要跳出来思考能不能拆SQL、提前汇总、做宽表,或使用其他存储方案,而不是继续在索引世界里打转。索引能解决很多问题,但解决不了所有问题。

希望这篇对索引的系统性梳理,能帮你在面对慢SQL时少一些焦躁,多一些笃定。数据结构、执行计划、索引设计,这三件事串起来,就是MySQL索引从理论到落地的完整脉络。

相关新闻

Linux进程通信之管道详解:匿名管道与FIFO实战

Linux进程通信之管道详解:匿名管道与FIFO实战

Linux下跑多进程程序&#xff0c;第一道坎就是进程通信。我在一个并发任务分发模块里遇到过这样的事&#xff1a;主进程fork出几个工人进程后&#xff0c;父进程里改了变量&#xff0c;子进程完全看不到&#xff0c;反过来也一样。原因其实不复杂——每个进程都有独立的地址空间…

2026/10/21 6:03:25 阅读更多 →
基于Spring Boot的宠物成长记录平台设计与实现——全栈毕设项目思路解析

基于Spring Boot的宠物成长记录平台设计与实现——全栈毕设项目思路解析

毕设季又到了&#xff0c;每年这个时候总有同学在选题和实现之间反复拉扯。如果你正在找一个既不算太复杂、又能把主流技术栈都串起来的题目&#xff0c;基于Spring Boot的宠物成长记录平台是个很合适的方向。这个题目覆盖面广&#xff0c;业务逻辑清晰&#xff0c;从前端页面到…

2026/10/21 6:03:25 阅读更多 →
Linux磁盘读写性能测试:从基础原理到fio实战全解析

Linux磁盘读写性能测试:从基础原理到fio实战全解析

有没有遇到过这种情况&#xff1a;一台 Linux 服务器用得好好的&#xff0c;突然某个服务变慢了&#xff0c;排查一圈发现 CPU 和内存都正常&#xff0c;最后定位到磁盘。或者你刚买了一块 NVMe 固态&#xff0c;想在装机后看看真实的读写能力是不是和包装盒上标的一致。再或者…

2026/10/21 6:02:24 阅读更多 →

最新新闻

Linux僵尸、孤儿与守护进程:原理、区别与实战实现

Linux僵尸、孤儿与守护进程:原理、区别与实战实现

1. 项目概述&#xff1a;从“进程尸体”到“后台幽灵”的真实世界映射在 Linux 系统里&#xff0c;你执行ps aux | grep defunct时看到的<defunct>标记&#xff0c;不是报错&#xff0c;而是一具“进程尸体”——它已经死亡&#xff0c;却还没被收尸&#xff1b;你用nohu…

2026/10/21 6:57:46 阅读更多 →
Docker部署Redis的四大核心问题:网络、持久化、配置、集群宣告

Docker部署Redis的四大核心问题:网络、持久化、配置、集群宣告

1. 为什么非得用 Docker 部署 Redis&#xff1f;——从三台裸机故障说起去年在某高校实验室做模拟项目X的后端支撑时&#xff0c;我负责缓存层搭建。最初按老办法&#xff1a;三台物理服务器&#xff0c;每台手动编译安装 Redis 6.2&#xff0c;配好 bind、port、requirepass、…

2026/10/21 6:57:46 阅读更多 →
Docker精讲:从Flask项目构建生产级容器镜像

Docker精讲:从Flask项目构建生产级容器镜像

1. 为什么“快速入门”反而最容易卡在第一步&#xff1f; 很多人点开“Docker 快速入门”教程&#xff0c;前五分钟信心满满——装完 Docker Desktop、跑通 docker run hello-world &#xff0c;就以为自己已经“会了”。结果第二天想把本地写好的 Python 脚本打包成镜像&am…

2026/10/21 6:57:46 阅读更多 →
机器学习到强化学习:四大AI方向实战导航

机器学习到强化学习:四大AI方向实战导航

如果你已经跟着这个系列走到第三篇&#xff0c;大概率不是刚装上 Python 的新手了。前面两篇我们聊了环境搭建、NumPy/Pandas 手感、Scikit-learn 的套路&#xff0c;也跑通了最简单的回归和分类。这些属于"会走"的阶段。这篇导航想做的是另一件事&#xff1a;把机器…

2026/10/21 6:57:46 阅读更多 →
技能提升的底层逻辑:从硬技能到元能力的系统方法

技能提升的底层逻辑:从硬技能到元能力的系统方法

1. 从“skills”这个标题说起&#xff1a;一个被严重低估的元能力“skills”这个词&#xff0c;单独拎出来看&#xff0c;信息量几乎为零。它不像“如何用Python做数据清洗”那样具体&#xff0c;也不像“三小时搭建个人博客”那样有明确交付物。但恰恰是这种模糊性&#xff0c…

2026/10/21 6:57:46 阅读更多 →
降AI率工具怎么选?八类平台实测对比与避坑指南

降AI率工具怎么选?八类平台实测对比与避坑指南

这个选题最近被问得太多了。我自己的后台留言里&#xff0c;隔三差五就有人发来一张检测报告截图&#xff0c;问为什么文章明明是自己写的还是被标成“疑似AIGC生成”&#xff0c;或者干脆是——交作业前一天才开始慌&#xff0c;到处找降AI率的工具。说实话&#xff0c;这事在…

2026/10/21 6:56:45 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →