刚接手一个线上系统的时候,我遇到过一条查询跑两秒多的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索引从理论到落地的完整脉络。