SQL逐字稿
“关于MySQL索引,我想从几个最根本的问题开始聊。咱们不背八股文,就聊聊它到底是怎么一回事,以及我们在实际项目中是怎么用它来解决问题的。”
一、核心价值:为什么我们离不开索引?
“首先,索引解决的其实是一个最朴素的问题:慢。
我给你看一个我们之前遇到过的一个真实案例。有一条非常简单的查询:
SELECT * FROM employees WHERE `name` = 'ddd';
在没有索引的时候,这条SQL跑了将近1.7秒。这放在任何一个线上系统里都是灾难。后来我们给name字段加了一个索引,执行时间直接降到了7毫秒,提升了200多倍。
这个数字背后,就是索引的核心价值:用空间换时间,极大提升查询效率。
那它为什么能这么快?咱们可以想象一下:如果没有索引,数据库要找到所有叫‘ddd’的人,它只能做全表扫描。就像你在一本没有目录的巨厚电话簿里找一个名字,只能一页一页地翻,从头翻到尾。数据量一大,磁盘I/O次数暴增,速度自然就慢下来了。
而有了索引,就相当于给这本电话簿做了一个按名字排序的目录。你想找‘ddd’,直接去目录里定位,目录告诉你它在哪一页,你翻过去就行了。可能只需要一两次磁盘I/O,这就是本质区别。
当然,代价也是有的。就像维护目录需要额外纸张一样,索引要占用磁盘空间。而且,当你增、删、改数据时,不光数据本身要变动,数据库还得去更新所有相关的索引(维护这个目录),所以写操作会变慢。这就是所谓的‘写惩罚’。
但在我们常见的互联网业务里,读写比例严重失衡,往往是99.99%的请求都是查询,只有0.01%是写操作。用这点写入的代价,换来几百倍的读取性能提升,这笔买卖非常划算。”
二、底层基石:B+树为什么是唯一选择?
“好,既然索引这么好,那它底层到底是个什么东西?简单说,索引就是一个排好序的、快速查找的数据结构。
那为什么最终是B+树,而不是其他我们熟悉的数据结构,比如二叉树或者哈希表呢?这是一个经典的面试题。
-
二叉树/红黑树(二叉平衡树)的问题:它们本质上是‘瘦高个’。当数据量达到千万级时,树的高度会非常高。而每一次沿着树向下查找,都可能对应一次磁盘I/O。树太高,就意味着查询过程中的I/O次数太多,速度就上不去了。
-
B树的问题:B树是个‘矮胖子’,一个节点可以有多个子节点,解决了树高的问题。但它有一个关键点:每个节点里,既存键(Key,也就是索引列的值),也存数据(Value,可能是整行数据或主键值)。这带来的问题是,每个节点能存放的键数量有限,而且在进行范围查询时,需要在树的各层之间来回跳跃,效率不够理想。
-
B+树的完美方案:B+树在B树的基础上做了几个至关重要的改进,可以把它想象成一个超级目录系统:
- 非叶子节点只存键和指针,不存数据。这就意味着一个磁盘页(默认16KB)能装下非常多的键。假设键是BigInt(8字节),指针6字节,那么一页就能存大约16000 / 14 ≈ 1170个键。这直接让树变成了‘超级矮胖子’。
- 所有数据都存储在叶子节点,并且叶子节点之间用指针串联成一个有序链表。
- 叶子节点本身包含了所有索引键和数据。
“我给你算一笔账:根据我们之前的计算,一个节点存1170个键。
- 根节点:1页,指向1170个页。
- 第二层:1170个页,每个页再指向1170个页,就是1170 * 1170 ≈ 137万页。
- 第三层:这137万页就是叶子节点了,假设每个叶子节点能存100条记录,那么总容量就是1.37亿条记录。
一个高度仅为3的B+树,就能轻松支撑上亿的数据量,而每次查询最多只需要3次磁盘I/O。 这就是它为什么能成为数据库索引事实标准的原因。而且,因为叶子节点有链表连接,像WHERE id > 100 AND id < 200这样的范围查询,效率极高,找到起点后顺着链表遍历就行。”
三、引擎差异:InnoDB和MyISAM的索引实现
“理解了B+树,我们再来看它在不同存储引擎里的具体实现,这直接影响了我们的使用策略。
-
InnoDB(我们现在基本只用这个):
- 聚簇索引:它的主键索引比较特殊,叶子节点存的不再是‘地址’,而是完整的数据行。也就是说,表数据本身就是这个主键索引的叶子节点。这就是‘聚簇’的含义——数据和主键索引是长在一起的。正因为如此,InnoDB表必须有且只有一个聚簇索引,如果你没显式定义主键,它会找一个唯一的非空索引代替,如果还没有,它会在后台生成一个隐藏的行ID来当主键。
- 非聚簇索引(二级索引):它的叶子节点存储的不是数据地址,而是对应记录的主键值。这意味着,通过二级索引查找数据,需要两步:首先在二级索引树里找到主键值,然后再拿着这个主键值去主键索引树里再查一次,这个过程叫做‘回表’。
-
MyISAM(现在很少用了):
- 它的索引实现就简单很多,不管是主键索引还是普通索引,都是非聚簇的。它的叶子节点存的都是数据记录的物理地址(比如文件偏移量)。数据和索引是完全分开的两个文件(.MYD和.MYI)。
“这里就引出了两个经典的面试题:
- 为什么InnoDB的非主键索引存的是主键值,而不是地址?
主要是为了维护一致性和节省空间。如果数据行移动了(比如页分裂),物理地址就变了,那所有存了地址的索引都得更新,维护成本太高。而主键值是稳定的。而且,如果每个二级索引都存一份完整数据,空间浪费巨大。 - 为什么推荐用自增的整型主键?
- 整型:比较大小比字符串快得多,而且占空间小,能让B+树的节点存更多键。
- 自增:每次插入的新数据都是追加在B+树的最后,不会导致中间节点的分裂和移动。如果是随机主键,会频繁引起树的结构调整(自旋),增加开销。”
四、实战精髓:联合索引与最左前缀原则
“这是索引使用中最核心、也最容易出问题的地方。我们给(name, age, position)建了一个联合索引,它本质上也是一棵B+树,这棵树的键是按照(name, age, position)这个顺序排序的。
这就引出了最左前缀原则:这棵索引树,是先按name排序,name相同的再按age排序,name和age都相同的再按position排序。所以,它的使用方式必须从最左边的列开始匹配。
我举几个例子就明白了:
WHERE name = 'Jack'-> 能用索引。它像电话簿里找所有姓‘Jack’的人。WHERE name = 'Jack' AND age = 30-> 能用索引。像找姓‘Jack’且年龄是30的人。WHERE name = 'Jack' AND age = 30 AND position = 'dev'-> 能用索引。精确匹配。WHERE age = 30-> 不能用索引。因为你无法用年龄在按(name, age, position)排序的目录里快速查找,就像你无法在按姓氏排序的电话簿里直接快速找到所有30岁的人。WHERE name = 'Jack' AND position = 'dev'-> 只能部分使用索引(只用到了name)。因为age断了,position就无法利用索引的有序性了。
理解了这个原则,你就能明白为什么我们建索引时,列的顺序非常重要,它决定了这个索引能覆盖到哪些查询场景。”
五、性能诊断:Explain工具与SQL优化
“当我们发现慢SQL,或者写完一条SQL不确定它快不快时,第一反应就是祭出EXPLAIN这个神器。它不是去执行SQL,而是告诉我们MySQL打算怎么执行这条SQL。
EXPLAIN的结果列很多,我们最关注的是这几列:
-
type:这是性能的黄金指标。从好到坏大概是:
const:通过主键或唯一索引直接找到,比如WHERE id = 1。eq_ref:联表时,被驱动表通过主键或唯一索引被访问。ref:使用普通索引进行等值查询。range:使用索引进行范围查询(BETWEEN, >, <等)。index:遍历整个索引树。虽然比全表扫描好点,但也不理想。ALL:全表扫描,需要优化。
我们的目标就是让查询至少跑到range级别,最好是ref。
-
key:实际用到的索引。如果这里为NULL,说明没用到索引。
-
rows:MySQL预估要扫描的行数。是一个很重要的参考。
-
Extra:包含额外信息,有很多‘危险信号’:
Using filesort:说明MySQL需要额外的一次排序操作,而且可能是在磁盘上完成的,性能极差。Using temporary:使用了临时表,常见于GROUP BY、DISTINCT等操作,也需要警惕。Using index:这是个好消息!说明查询的列全在索引里,直接就从索引树上拿到了数据,不需要‘回表’。这叫做‘覆盖索引’,是终极优化手段之一。
“基于这些分析,我们的优化手段就很清晰了:
- 尽量避免那些让索引失效的操作,比如:在索引列上使用函数、计算、类型转换;使用
LIKE '%xxx';使用OR(有时可以改用UNION)等。 - 如果无法避免全表扫描,尽量使用覆盖索引,减少需要回表的数据量。
- 对于
OR或IN查询范围太大的情况,一个工程上的解决方案是分批处理,比如用多线程,每个线程处理一个子集,最后合并结果。”
总结
“所以,总结一下,我对MySQL索引的理解是:
它本质上是一个基于B+树的、排好序的快速查找结构,是数据库的‘目录’。
它的设计哲学是用写的代价换读的性能,这在读多写少的场景下是绝对正确的选择。
在使用上,核心是理解聚簇索引/非聚簇索引的原理,以及联合索引的最左前缀原则。
最后,所有的优化都要基于EXPLAIN的分析,让数据说话,而不是凭感觉。
可以说,吃透了索引,就抓住了数据库性能优化的一半。”

浙公网安备 33010602011771号