上个月排查一条线上慢SQL,让我印象特别深。报表接口统计近7天订单金额,where条件里的字段全都建了索引,EXPLAIN一跑却是全表扫描,type=ALL,扫描了快80万行,接口直接超时。我当时一边看执行计划一边嘀咕:索引明明都在,为什么就是不走?后来原因特别普通:查询条件里传了一个带引号的数字,而表里的字段是int类型,MySQL在比较时把字段做了隐式转换。你看,这就是典型的索引失效。
说实话,做后端这些年,索引失效的坑我踩过无数遍,身边同事也没少踩。网上一搜能搜出几十种“失效场景”,各家说法不一,甚至有些互相矛盾。这篇文章我想结合自己实际排障的经历,把这些场景系统地梳理一遍。标题说是30种,其实很多场景本质是同一类问题——索引列被“污染”、联合索引顺序不匹配、优化器成本判断问题等等。只要搞清楚索引底层的B+树结构和执行计划的判断逻辑,你也能见一个灭一个。
1. 索引列在SQL里“变了形”——隐式转换与函数运算
这是我在生产环境里遇到频率最高的一类失效,典型的特征是:索引明明建了,SQL看起来也没问题,但EXPLAIN出来就是type=ALL。说白了,就是查询条件里的索引列被MySQL做了一次“转换或计算”,原来的B+树顺序完全派不上用场。
1.1 隐式类型转换:一句没加引号,索引直接报废
先看一个最经典的场景。假设有一张用户表:
sql复制CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`phone` varchar(20) NOT NULL COMMENT '手机号',
`status` tinyint NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `idx_phone` (`phone`)
) ENGINE=InnoDB;
想象一个初级开发者写了这条SQL:
sql复制SELECT id, phone, status FROM user WHERE phone = 13800001111;
phone字段是varchar类型,但参数传的是一个整数。MySQL在执行比较时,会尝试把字段值转换成数字再比对,也就是索引列上被隐式套了一层CAST(phone AS SIGNED)。一旦列参与了函数运算,优化器基本没法沿着idx_phone的B+树去定位,只能把整张表的phone都取出来做一次转换再比较。
我当初排查那个线上故障时,EXPLAIN输出是这样的:
| 字段 | 值 |
|---|---|
| type | ALL |
| possible_keys | idx_phone |
| key | NULL |
| rows | 763452 |
| Extra | Using where |
possible_keys里有索引,但key是NULL,这就是典型的“能用但没用”。修复方式很简单,把参数类型改成跟字段一致:
sql复制SELECT id, phone, status FROM user WHERE phone = '13800001111';
改完再跑EXPLAIN,type变成ref,rows直接掉到个位数。
1.2 隐式转换的另一面:字符集和排序规则不一致
比上面更隐蔽的情况是字符串本身没写错,但两个表关联字段的字符集不一样。比如一张表是utf8mb4,一张表是utf8,关联时MySQL会把utf8那边的字符集转成utf8mb4再比较,这个转换同样会作用在索引列上。
这类问题发生得少,可一旦发生,排查起来特别费劲,因为SQL看起来没有任何问题。建议在建表时统一字符集和排序规则,别图省事一个库一个字符集。如果你遇到“两个字段类型相同、关联条件也正确、但执行计划就是全表扫描”的情况,优先查一下SHOW CREATE TABLE,确认关联字段的字符集是否一致。真实项目里,因为历史原因多个系统共用数据库时,这种坑很常见。
1.3 函数操作:DATE()、LEFT()等函数是索引的“隐形粉碎机”
sql复制WHERE DATE(create_time) = '2024-01-15'
这条SQL非常常见,尤其是做报表统计的时候。create_time上建了索引,但一旦写成DATE(create_time),索引列就被函数包住了。B+树叶子节点上存的是原始的create_time值,而不是日期格式化后的字符串,优化器根本没法用二分法找到对应位置,只能全表扫描。
遇到过不少同学反驳:我就查一天的数据,加个函数怎么了?看执行计划就知道,数据量大时这条SQL能慢到让人崩溃。正确写法是范围查询:
sql复制WHERE create_time >= '2024-01-15 00:00:00'
AND create_time < '2024-01-16 00:00:00'
改成范围条件后,索引列没被加工,优化器可以直接通过索引定位起始位置,再向右扫描。这条优化建议值得贴在每个写报表SQL的同事电脑旁。
类似的还有LEFT(name, 3) = '张'、YEAR(create_time) = 2024、MONTH(create_time) = 1,全部是同一个病根。如果业务上确实高频用到日期函数,可以用MySQL 8.0的函数索引:
sql复制ALTER TABLE t ADD INDEX idx_create_date ((DATE(create_time)));
这样B+树里直接存了函数处理后的值,查询时把等式写完整即可。不过函数索引不是万能的,它会增加写入成本,别为了炫技滥用。
1.4 算术运算和字符串拼接:索引列被当作普通变量算了
有些人写SQL时,习惯把条件表达得很“数学”。比如:
sql复制WHERE id + 5 = 10000
WHERE price * 0.8 > 500
id是主键,price建了索引,但索引列参与算式后,优化器无法直接比较,主键索引也一样失效。这类SQL的优化思路是:把算式移项,让索引列单独留在等式一侧。
sql复制WHERE id = 10000 - 5
WHERE price > 500 / 0.8
字符串拼接同理,比如WHERE CONCAT(first_name, last_name) = '张伟',这种写法对索引是毁灭性的。与其在SQL里拼,不如加一列冗余字段,或者干脆把条件拆成first_name = '张' AND last_name = '伟'。
这里总结一条规律:**索引列一旦参与了函数、算术、拼接中的任何一种操作,索引基本就废了。**判断标准永远只有一个:where条件左侧的列,是否保持原始形态。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 模糊查询、OR、IN:三个“看起来没问题”的写法
如果说第一部分还容易自我检查,那第二部分的三个场景就真的很狡猾。它们的共同点是:单看SQL不觉得有错,数据量小的时候跑得也不慢,等数据量滚起来后就开始全表扫描,而且不好定位。
2.1 前导模糊查询:LIKE '%abc'为什么不能走索引
B+树是有序的,底层存储结构按索引列的值从小到大排列。LIKE 'abc%'能走索引,是因为MySQL可以在B+树上找到第一个前缀为“abc”的记录,然后顺序扫描到最后一个前缀为“abc”的记录,这是一个典型的范围访问。但LIKE '%abc'就不一样了,以通配符开头意味着任意位置都可能出现“abc”,有序的索引根本派不上用场。
| SQL写法 | 是否走索引 |
|---|---|
| WHERE name LIKE '张%' | 能走 |
| WHERE name LIKE '%张' | 基本全表扫描 |
| WHERE name LIKE '%张%' | 基本全表扫描 |
更麻烦的是搜索框里的模糊搜索往往都是%关键词%。遇到这种情况,我的处理优先级是:
- 如果业务允许,改成前缀匹配,配合搜索引擎或分词组件。
- 如果数据量在百万级以下,可以接受一定延迟,配合覆盖索引减少回表。
- 如果数据量大且是核心搜索场景,老实上Elasticsearch之类的专用搜索引擎,别让MySQL硬扛。
有个小技巧值得分享:如果只是查某个后缀特征,比如“查所有以.com结尾的邮箱”,可以考虑把字段反转后建索引,查询时LIKE 'moc.%'。不过这属于偏招,会牺牲写操作的清晰度,建议只在特定场景用。
2.2 OR的“连坐”效应:一个分支拖垮整个查询
sql复制WHERE a = 1 OR b = 2
假设a字段有索引,b字段没有索引。很多人第一反应是:反正a能走索引,先按a=1查出来,再遍历全表找b=2的,最后合并结果不就行了?理论上是这样,但MySQL的优化器往往不这么干。
OR的含义是并集,最终要返回满足任一条件的全部记录。如果其中一个分支无法使用索引,优化器算来算去,发现全表扫描的成本可能比“部分索引+全表扫”更低,于是直接放弃索引。这就是OR的“连坐”效应:只要有一个条件没索引,整条SQL就可能全表扫描。
改写的常见方案是拆成UNION ALL:
sql复制SELECT * FROM t WHERE a = 1
UNION ALL
SELECT * FROM t WHERE b = 2;
当然,如果b上没有索引,UNION ALL的第二个查询仍然是全表扫描,所以本质上还是要让每个分支都有可用的索引。生产实践中还有一种情况,OR两边都有索引,但MySQL没有走index_merge,这时也可以在优化器开关里开启index_merge相关参数,不过最稳妥的还是改写SQL。我见过不少“加个索引问题就好了”的案例,其实改成UNION ALL更可控。
2.3 IN并非洪水猛兽,但列表过大确实危险
网上一搜“IN会不会导致索引失效”,答案五花八门。说实话,IN本身通常能走索引,EXPLAIN会显示range。比如:
sql复制WHERE status IN (1, 2, 3)
如果status有索引,执行计划大概率是range访问。但IN列表过大的时候,优化器会根据成本估算决定是否放弃索引。这个阈值跟eq_range_index_dive_limit参数有关,MySQL 8.0默认是200。当IN列表里的值超过这个数量,优化器可能改用索引统计信息来估算,一旦估算的行数太大,可能直接走全表扫描。
解决方案有几个方向:
- 控制单个IN列表的规模,几百个值以内的场景问题不大,超过的话拆成小批次并行查。
- 把IN改写成
JOIN临时表,让取值范围变成一张驱动表,反而更容易利用索引。 - 对于
NOT IN,绝大多数场景下优化器会直接放弃索引,因为取反条件往往意味着要扫描几乎所有记录,索引带来的优势不明显。
2.4 一个速查表:30种失效场景归类记录
下面这张表是我排查慢查询时常用的对照清单,收录了运维和开发过程中遇到的30种场景。分类整理,方便收藏:
| 类别 | 失效场景 | 说明 |
|---|---|---|
| 索引列被污染 | varchar列与数字比较 | 隐式类型转换导致索引失效 |
| 索引列被污染 | int列与字符串参数比较 | 参数类型不一致时,部分版本也可能失效 |
| 索引列被污染 | 关联字段字符集不一致 | utf8与utf8mb4混用引发转换 |
| 索引列被污染 | 对索引列使用DATE()等函数 | 索引列被包裹 |
| 索引列被污染 | 对索引列使用LEFT()等字符串函数 | 同上 |
| 索引列被污染 | 对索引列做算术运算 | id+5、price*0.8等 |
| 索引列被污染 | 对索引列做CONCAT拼接 | 字符串拼接导致无法定位 |
| 索引列被污染 | 列与列比较(一个表里两列运算) | where a=b时索引可能失效 |
| 模糊查询 | LIKE '%abc' | 前导通配符无法利用B+树 |
| 模糊查询 | LIKE '%abc%' | 同上 |
| 模糊查询 | LIKE '_abc' | 单字符通配符在前同样失效 |
| OR/IN | OR条件混合无索引字段 | 一个分支拖垮整体 |
| OR/IN | OR两边有索引但优化器成本高 | 需检查index_merge |
| OR/IN | NOT IN | 取反条件扫描量过大 |
| OR/IN | NOT EXISTS | 类似NOT IN |
| OR/IN | IN列表超过eq_range_index_dive_limit | 成本估算不准 |
| 联合索引 | 跳过最左列 | 违反最左前缀原则 |
| 联合索引 | 跳过中间列直接用后续列 | 一样可能失效 |
| 联合索引 | 范围条件之后的列 | 范围中断,后续列无法利用 |
| 联合索引 | 查询条件顺序与索引顺序不一致 | SQL写法层面注意 |
| 排序/分组 | ORDER BY字段不在索引中 | filesort |
| 排序/分组 | ORDER BY字段顺序与联合索引不一致 | 同上 |
| 排序/分组 | 混合ASC/DESC排序 | 老版本不支持反向扫描 |
| 排序/分组 | GROUP BY不满足最左前缀 | 产生临时表和filesort |
| 优化器选择 | 表数据量太小 | 全表扫描成本更低 |
| 优化器选择 | 统计信息不准确 | Cardinality失真 |
| 优化器选择 | 查询返回集过大 | 索引扫描+回表比全表扫描慢 |
| 优化器选择 | select *回表成本高 | 覆盖索引被忽略,优化器退缩 |
| 索引设计 | 索引列区分度太低 | 选择性差,优化器不认可 |
| 索引设计 | 字段过长占用太多索引页 | 索引效率低 |
很多看起来“玄学”的索引失效,翻到最后都能落在这张表里。
3. 联合索引:最左前缀原理与那些“断链”的边界
联合索引是索引失效的重灾区,比单列索引的隐式转换还要隐蔽。很多开发者在单列索引上很小心,一碰到联合索引就唉声叹气,其实搞懂B+树怎么排的就没什么神秘的。
3.1 联合索引的B+树相当于一本按多级目录排好的字典
假设我们有联合索引idx_user_status_time(user_id, status, order_time),它的物理存放规则是:先按user_id排序,user_id相同的记录再按status排序,user_id和status都相同的记录再按order_time排序。
这就像查一本字典,先按拼音首字母定位章节,再按声调缩小范围,最后才能按笔画或词语顺序翻到具体那一页。如果你不告诉它首字母是什么,直接翻到某一声调去找字,这本字典帮不了你。
所以联合索引的最左前缀原则本质上不是MySQL的“怪脾气”,而是B+树存储结构天然决定的。最先排序的列在索引查找中占有“统领”地位,后面的列是在前面列相等的前提下才有序的。
3.2 跳过前置列的查询:最典型的最左前缀失效
基于上面的联合索引,看几条SQL:
sql复制WHERE status = 1 AND order_time > '2024-01-01';
WHERE order_time > '2024-01-01';
这两条都跳过了user_id列。索引里的B+树虽然对order_time做了排序,但那是“在user_id和status都相同的前提下”的局部有序,对于全表来说,order_time并不是全局有序的。优化器在线性序列里找不到一个明确的起点去定位,只能放弃索引。
业务上如果说确实需要按order_time单独查询,怎么办?两个可行方案:
- 在
order_time上单独建一个二级索引,让这条SQL走单列索引。 - 如果查询频率不高且数据量可控,接受全表扫描,但一定要在Code Review阶段让团队知道这个代价。
3.3 范围查询之后:为什么后面的索引列“集体罢工”
这个场景我见过太多人解释不清楚。同样以idx_user_status_time为例:
sql复制WHERE user_id = 1001 AND status > 1 AND order_time > '2024-01-01';
user_id的等值条件能用上索引,status的范围条件也能用上索引,但order_time后面的排序条件基本用不上了。原因在于:当status是一个范围(比如status=2、3、4)时,这些记录里order_time并不是按顺序排好的。B+树叶子节点中,只有status相等的前提成立,order_time才有序。一旦进行范围扫描,相邻叶子节点上的order_time可能跳动,索引的有序性被打破。
这也是为什么面试里经常问:“联合索引(a,b,c),查询where a=1 and b>10 and c=5,c能不能用到索引?”答案是c大概率无法利用索引排序,只能做过滤。如果业务上必须这样查,可以尝试调整索引列为(a, c, b),让等值条件在前,范围条件放最后。这个调整思路很实用。
3.4 别把Index Skip Scan当救命稻草
MySQL 8.0.13开始支持Index Skip Scan,即在跳过最左列的情况下,通过扫描索引中不同的值来模拟“跳出最左列查询”。比如索引(gender, name),gender只有两个值,where name = '张三'时有可能走Skip Scan,执行计划里能看到Using index for skip scan。
但这个优化有前提条件,最左列的可区分值不能太多,否则优化器觉得成本太高,照样不鸟你。我建议把它当成一种“意外之喜”,而不是设计依赖。建联合索引时,始终遵循等值条件列放前面、范围条件列放后面、选择性高的列尽量靠前的原则,才能从根本上保证索引的可用性。
4. 排序与分组:走了索引排序的隐藏前提
排序和分组看起来和索引关系不大,但实际上索引B+树天然有序,如果利用得好,可以完全避免filesort和临时表。反过来,利用不好就是隐藏的索引失效场景。
4.1 ORDER BY顺序不匹配:filesort的代价
假设联合索引idx_user_status_time(user_id, status, order_time),执行:
sql复制SELECT user_id, status, order_time
FROM t
WHERE user_id = 1001
ORDER BY order_time DESC;
这里user_id用了等值条件,order_time和联合索引的第三列完全匹配,所以ORDER BY order_time DESC能直接利用索引的有序性,不需要额外排序。
但如果写的是:
sql复制SELECT user_id, status, order_time
FROM t
WHERE user_id > 1001
ORDER BY order_time DESC;
user_id是范围条件,意味着可能命中多个user_id,跨user_id后,order_time的有序性不复存在,MySQL只能把结果集捞出来做一次外部排序。EXPLAIN的Extra列会出现Using filesort。
在MySQL 8.0之前,filesort是排序不到内存就落盘的,代价非常大。所以生产环境里有条硬性要求:ORDER BY的字段顺序尽量与索引顺序保持一致,避免filesort。
4.2 升降序混排:老版本的“偏科”问题
ORDER BY a ASC, b DESC这种写法,在老版本MySQL里几乎没法利用索引做双向排序。原因很简单,索引默认是升序存储的,a升序没问题,但a相同时b又要降序,就冲突了。
MySQL 8.0引入了降序索引,允许在创建索引时明确指定某一列的排序方向:
sql复制CREATE INDEX idx_a_b ON t (a ASC, b DESC);
这样ORDER BY a ASC, b DESC就能完全命中索引排序,不再filesort。如果你的项目还在5.7或更早版本,遇到混合排序需求,要么改排序方向,要么接受filesort,没有太好的第三条路。
4.3 GROUP BY临时表与索引配合
GROUP BY通常会先做排序,再分组。如果分组的字段不在索引里,或者不满足最左前缀,执行计划里会看到Using temporary; Using filesort。临时表如果超过内存阈值会落到磁盘,这类SQL在数据量大时很容易成为慢查询。
优化思路有两个:一是让GROUP BY字段顺序匹配联合索引,二是把高频分组统计结果做成冗余字段或定时汇总表。走索引是治本,冗余是治根,各有适用场景。
5. 优化器视角:统计信息、回表成本与“被放弃”的索引
这一部分最容易被低估。前几类失效都能从SQL写法上找到原因,但第五类的特点很扎心:索引是好的,SQL也没变形,可优化器就是不用。这就要站在MySQL优化器的成本模型上去理解。
5.1 数据量太小:全表扫描反而“更聪明”
表里只有几十行,即使索引存在,优化器也会认为全表扫描更划算。因为走二级索引通常意味着先查索引、再回表取数据,涉及多次随机IO;而全表扫描只需要顺序读取少量数据页,成本更低。
我见过有人拿着一张几百行的表测索引是否失效,得出“索引没用”的结论,其实这只是优化器的合理决策。判断索引失效,更科学的做法是在具备一定数据规模的前提下测试,至少让数据分布接近生产环境。
5.2 统计信息不准确:索引被错误低估
InnoDB通过采样统计索引的Cardinality(区分度),这个值会展示在SHOW INDEX输出里。如果Cardinality明显偏低,优化器就认为这个索引没什么选择性,可能放弃它。
频繁的增删改、大批量delete后没有及时更新统计信息,都会让这个值失真。解决办法很朴素:
sql复制ANALYZE TABLE t;
执行完之后重新看执行计划,有时候“索引失效”的问题就这么消失了。遇到那种“昨天还走索引,今天莫名其妙全表扫描”的诡异情况,第一件事就是这个。
5.3 select * 引发的回表成本问题
二级索引的叶子节点只保存“索引列 + 主键”,并不包含其他字段。如果SELECT需要的数据不在索引里,MySQL查到索引记录后,还要拿着主键回到聚簇索引再查一次,这个动作叫回表。
sql复制SELECT id FROM t WHERE status = 1; -- 覆盖索引,不回表
SELECT * FROM t WHERE status = 1; -- 需要回表
当命中行数很多时,回表次数也很多,优化器算了一下成本,发现不如直接全表扫描一次来得快,于是放弃索引。这也是大厂规范里经常写“禁止select *”的原因之一。把SQL改成只取必要字段,并尽量让查询字段被索引覆盖,能解决不少索引失效的隐患。
5.4 字段过于宽大和低区分度的索引
varchar(255)全列建索引,会让每个索引页能容纳的键数量变少,B+树层级变深,扫描效率降低。优化器同样会评估成本,如果它觉得一个索引页可能扫不了几个有效行,就不会优先选用。解决方案是前缀索引:
sql复制ALTER TABLE t ADD KEY idx_email (email(20));
只取前20个字符建索引。同时,区分度极低的列,比如status只有0和1两个值,单独建索引的意义就不大,优化器经常放弃。这类列更适合放进联合索引中作为辅助列,而不是单独建索引。
6. 用EXPLAIN快速定位索引失效的实战流程
上面说了那么多原理,落到日常工作中,其实可以浓缩成一套标准排查流程。我在团队里带新人时,都是先教EXPLAIN,再教调优。
6.1 从一条超时SQL开始:EXPLAIN关键字段速读
假设生产环境有一条慢SQL:
sql复制SELECT order_id, user_id, amount
FROM orders
WHERE user_id = 12345
AND status = 1
ORDER BY create_time DESC
LIMIT 10;
执行EXPLAIN,输出大致如下:
| id | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|
| 1 | ref | idx_user_status_time | idx_user_status_time | 312 | Using index condition; Using filesort |
重点看四列:
type:访问类型,从好到差大致是system > const > eq_ref > ref > range > index > ALL。看到ALL要警惕全表扫描。key:实际使用的索引。如果possible_keys里有索引但key为NULL,说明索引被放弃。rows:预估扫描行数,数值越大越危险,可以当作成本估算的参考。Extra:出现了Using filesort、Using temporary,说明排序或分组走了临时方案;出现Using where则要注意连接类型。
上面这条SQL的问题比较典型:user_id和status都在联合索引里,但ORDER BY create_time DESC没有匹配索引顺序。尽管查询条件走索引了,排序仍然触发了filesort。优化方向是把联合索引调整成(user_id, status, create_time),让排序也能直接利用索引。
6.2 定位索引失效的“三板斧”排查顺序
我自己排查索引失效问题时,会按固定的顺序来,这样不容易漏:
第一板斧,检查SQL是否让索引列“变了形”。有没有隐式类型转换?有没有对索引列使用函数、算术运算、字符串拼接?这个检查最快,一眼就能看出来。
第二板斧,检查联合索引是否满足最左前缀原则。查询条件的列顺序,是否从联合索引的第一列开始?范围条件后面还有没有需要索引排序或过滤的列?
第三板斧,检查优化器决策是否合理。看EXPLAIN的rows和Cardinality,确认统计信息是否过期,计算一下回表比例。必要时用ANALYZE TABLE刷新统计信息,或者用FORCE INDEX临时验证索引是否真的更优。
如果三层都查完还没头绪,还有更底层的工具:optimizer_trace可以打印优化器完整的成本计算过程。执行:
sql复制SET optimizer_trace = 'enabled=on';
SELECT ...; -- 目标SQL
SELECT * FROM information_schema.OPTIMIZER_TRACE;
然后去看rows_estimation和considered_execution_plans部分,能清楚地看到优化器比较了哪些方案、放弃了哪个索引、为什么放弃。这不是日常排查的标配手段,但遇到“死都解释不通”的场景时很有用。
6.3 面试与Code Review里的高频考点
这篇文章的标题能被搜索引擎和热词带火,说明“索引失效”确实是后端面试的高频考点。我自己做面试官时,常问几个递进式问题:
- 索引失效有哪些常见场景?(这道题就是送分题,考察系统学习能力)
- 为什么隐式类型转换会导致索引失效?(考察是否理解索引的有序性和B+树定位逻辑)
- 联合索引里为什么范围条件之后的列会失效?(考察对叶节点存储顺序的理解)
- EXPLAIN中type字段从最优到最差怎么排列?(考察日常调优经验)
- 覆盖索引为什么能优化查询,它是怎么避免回表的?(考察从成本模型看执行计划的能力)
Code Review时遇到相关改动,我也建议团队至少检查三件事:where条件里索引列是否被函数包裹、联合索引查询是否满足最左前缀、select是否拿了一些根本用不到的字段。
最后说点我自己的体会。索引失效问题之所以反复出现,根本原因不是SQL语法难,而是大家习惯把索引当“缓存”用,却不理解执行计划为什么这么选。我给自己团队的硬性要求是:任何涉及慢查询的改动,EXPLAIN输出必须贴进工单;任何新上线SQL,Review时必须附带执行计划截图。养成这个习惯后,线上索引失效的故障至少少了一大半。索引优化没有银弹,大多数时候就是回到B+树的物理结构,老老实实推演一遍查询路径,答案自然就出来了。
