MySQL性能调优
- 1. 高性能的索引创建策略
- 1.1 只为用于搜索、排序或分组的列创建索引
- 1.2 合理设计多列索引
- 1.3 尽可能设计三星索引
- 1.4 主键尽量是很少改变的列
- 1.5 处理冗余和重复索引
- 1.6 删除未使用的索引
- 1.7 InnoDB中的索引
- 2. 补充资料:磁盘和B+树
- Mysql内核查询成本计算
- 3. Optimizer Trace
- 4. 什么是成本
- 5. 单表查询的成本
- 5.1 Mysql查询成本计算实战
- 1. 根据搜索条件,找出所有可能使用的索引
- 2. 计算全表扫描的代价
- 3. 计算使用不同索引执行查询的代价
本文是按照自己的理解进行笔记总结,如有不正确的地方,还望大佬多多指点纠正,勿喷。
在优化之前,一定要有一个好的表结构。遵循范式。
但是范式也有他的缺点,因此出现了反范式。
其实也有后缀索引
,针对后缀索引其实自己可以反序存储,这样就转化成了前缀索引了。
有时候后缀索引 (suffix index)也有用途(例如,找到某个域名的所有电子邮件地址)。MySQL原生并不支持反向索引,但是可以把字符串反转后存储,并基于此建立前缀索引。可以通过触发器或者应用程序自行处理来维护索引。
1. 高性能的索引创建策略
1.1 只为用于搜索、排序或分组的列创建索引
也就是说,只为出现在WHERE 子句中的列、连接子句中的连接列创建索引,而出现在查询列表中的列一般就没必要建立索引了,除非是需要使用覆盖索引。又或者为出现在ORDER BY或GROUP BY子句中的列创建索引,这句话什么意思呢?比如:
SELECT * FROM order_exp ORDER BY insert_time, order_status,expire_time;
查询的结果集需要先按照insert_time值排序,如果记录的insert_time值相同,则需要按照order_status来排序,如果order_status的值相同,则需要按照expire_time排序。回顾一下联合索引的存储结构,u_idx_day_status索引本身就是按照上述规则排好序的,所以直接从索引中提取数据,然后进行回表操作取出该索引中不包含的列就好了。
当然ORDER BY的子句后边的列的顺序也必须按照索引列的顺序给出,如果给出ORDER BY order_status,expire_time, insert_time的顺序,那也是用不了B+树索引的,原因不用再说了吧。
SELECT insert_time, order_status,expire_time,count(*) FROM order_exp GROUP BY insert_time, order_status,expire_time;
这个查询语句相当于做了3次分组操作:
先把记录按照insert_time值进行分组,所有insert_time值相同的记录划分为一组。
将每个insert_time值相同的分组里的记录再按照order_status的值进行分组,将order_status值相同的记录放到一个小分组里。
再将上一步中产生的小分组按照expire_time的值分成更小的分组。
然后针对最后的分组进行统计,如果没有索引的话,这个分组过程全部需要在内存里实现,而如果有了索引的话,恰巧这个分组顺序又和我们的u_idx_day_status索引中的索引列的顺序是一致的,而我们的B+树索引又是按照索引列排好序的,这不正好么,所以可以直接使用B+树索引进行分组。和使用B+树索引进行排序是一个道理,分组列的顺序也需要和索引列的顺序一致。
1.2 合理设计多列索引
很多人对多列索引的理解都不够。一个常见的错误就是,为每个列创建独立的索引,或者按照错误的顺序创建多列索引。
我们遇到的最容易引起困惑的问题就是索引列的顺序。正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要。反复强调过,在一个多列B-Tree索引中,索引列的顺序意味着索引首先按照最左列进行排序,其次是第二列,等等。所以,索引可以按照升序或者降序进行扫描,以满足精确符合列顺序的ORDER BY、GROUP BY和DISTINCT等子句的查询需求。
所以多列索引的列顺序至关重要。对于如何选择索引的列顺序有一个经验法则:
-
将选择性最高的列放到索引最前列。(就是根据这一列重复值最少的)当不需要考虑排序和分组时,将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化WHERE条件的查找。在这种情况下,这样设计的索引确实能够最快地过滤出需要的行,对于在WHERE子句中只使用了索引部分前缀列的查询来说选择性也更高。
-
然而,性能不只是依赖于索引列的选择性,也和查询条件的有关。可能需要根据那些运行频率最高的查询来调整索引列的顺序,比如排序和分组,让这种情况下索引的选择性最高。
-
同时,在优化性能的时候,可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。
1.3 尽可能设计三星索引
三星索引概念
对于一个查询而言,一个三星索引,可能是其最好的索引。
如果查询使用三星索引,一次查询通常只需要进行一次磁盘随机读以及一次窄索引片的扫描,因此其相应时间通常比使用一个普通索引的响应时间少几个数量级。
三星索引概念是在《Rrelational Database Index Design and the optimizers》 一书(这本书也是《高性能MySQL》作者强烈推荐的一本书)中提出来的。原文如下:
The index earns one star if it places relevant rows adjacent to each other,
a second star if its rows are sorted in the order the query needs,
and a final star if it contains all the columns needed for the query.
索引将相关的记录放到一起则获得一星;
如果索引中的数据顺序和查找中的排列顺序一致则获得二星;
如果索引中的列包含了查询中需要的全部列则获得三星。
二星(排序星):
在满足一星的情况下,当查询需要排序,group by、 order by,如果查询所需的顺序与索引是一致的(索引本身是有序的),是不是就可以不用再另外排序了,一般来说排序可是影响性能的关键因素。
三星(宽索引星):
在满足了二星的情况下,如果索引中所包含了这个查询所需的所有列(包括 where 子句 和 select 子句中所需的列,也就是覆盖索引),这样一来,查询就不再需要回表了,减少了查询的步骤和IO请求次数,性能几乎可以提升一倍。
一星按照原文稍微有点难以理解,其实它的意思就是:如果一个查询相关的索引行是相邻的或者至少相距足够靠近的话,必须扫描的索引片宽度就会缩至最短,也就是说,让索引片尽量变窄,也就是我们所说的索引的扫描范围越小越好。
这三颗星,哪颗最重要?第三颗星。
因为将一个列排除在索引之外可能会导致很多磁盘随机读(回表操作)。第一和第二颗星重要性差不多,可以理解为第三颗星比重是50%,第一颗星为27%,第二颗星为23%,所以在大部分的情况下,会先考虑第一颗星,但会根据业务情况调整这两颗星的优先度。
达成三星索引
现在有表
create table customer(
cno int,
lname varchar(10),
fname varchar(10),
sex int,
weight int,
city varchar(10));
建立索引
create index idx_cust on customer(city,lname,fname,cno);
对于下面的SQL而言,这是个三星索引
select cno,fname from customer where lname =’xx’ and city =’yy’ order by fname;
来评估下:
第一颗星:所有等值谓词的列,是组合索引的开头的列,可以把索引片缩得很窄,符合。
第二颗星:order by的fname字段在组合索引中且是索引自动排序好的,符合。
第三颗星:select中的cno字段、fname字段在组合索引中存在,符合。
达不成三星索引
现在有表
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_name` varchar(100) DEFAULT NULL,
`sex` int(11) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`c_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8;
SQL语句如下:
select user_name,sex,age from test where user_name like 'test%' and sex =1 ORDER BY age
如果我们建立索引(user_name,sex,age):
第三颗星,满足
第一颗星,满足
第二颗星,不满足,user_name 采用了范围匹配,sex 是过滤列,此时age 列无法保证有序的。
上述我们看到,此时索引(user_name,sex,age)并不能满足三星索引中的第二颗星(排序)。
于是我们改改,建立索引(sex, age,user_name):
第一颗星,不满足,只可以匹配到sex,sex选择性很差,意味着是一个宽索引片,
第二颗星,满足,等值sex 的情况下,age是有序的,
第三颗星,满足,select查询的列都在索引列中,
对于索引(sex,age,user_name)我们可以看到,此时无法满足第一颗星,窄索引片的需求。
以上2个索引,都是无法同时满足三星索引设计中的三个需求的,我们只能尽力满足2个。而在多数情况下,能够满足2颗星,已经能缩小很大的查询范围了,具体最终要保留那一颗星(排序星 or 窄索引片星),这个就需要看查询者自己的着重点了,无法给出标准答案。
1.4 主键尽量是很少改变的列
我们知道,行是按照聚集索引物理排序的,如果主键频繁改变(update)
,物理顺序会改变,MySQL要不断调整B+树,并且中间可能会产生页面的分裂和合并等等,会导致性能会急剧降低。
B+树是一个多路平衡树,如果在一个创建好的平衡树上反复的update要反复的分裂合并。
1.5 处理冗余和重复索引
MySQL允许在相同列上创建多个索引,无论是有意的还是无意的。MySQL需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。应该避免这样创建重复索引,发现以后也应该立即移除。
有时会在不经意间创建了重复索引,例如下面的代码:
CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;
这里创建了一个主键,又加上唯一限制,然后再加上索引以供查询使用
。事实上,MySQL的唯一限制和主键限制都是通过索引实现的,因此,上面的写法实际上在相同的列上创建了三个重复的索引。通常并没有理由这样做,除非是在同一列上创建不同类型的索引来满足不同的查询需求。
冗余索引和重复索引有一些不同。如果创建了索引(A B),再创建索引(A)就是冗余索引,因为这只是前一个索引的前缀索引。因此索引(AB)也可以当作索引(A)来使用(这种冗余只是对B-Tree索引来说的)。但是如果再创建索引 (B,A),则不是冗余索引,索引(B)也不是,因为B不是索引(A,B)的最左前缀列。
已有的索引(A),扩展为(A,ID),其中ID是主键,对于InnoDB来说主键列已经包含在二级索引中了,所以这也是冗余的。
解决冗余索引和重复索引的方法很简单,删除这些索引就可以,但首先要做的是找出这样的索引。可以通过写一些复杂的访问INFORMATION_SCHEMA表的查询来找。
1.6 删除未使用的索引
除了冗余索引和重复索引,可能还会有一些服务器永远不用的索引。这样的索引完全是累赘,建议考虑删除。
1.7 InnoDB中的索引
索引在查询中的作用到底是什么?在我们的查询中发挥着什么样的作用呢?
1、一个索引就是一个B+树,索引让我们的查询可以快速定位和扫描到我们需要的数据记录上,加快查询的速度。
2、一个select查询语句在执行过程中一般最多能使用一个二级索引来加快查询,即使在where条件中用了多个二级索引。
2. 补充资料:磁盘和B+树
为什么关系型数据库都选择了B+树,这个和磁盘的特性有着非常大的关系。
如果我们简化一下,可以这么看
一个磁盘由大小相同且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步转动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动。
盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元也是最小读写单元。现在磁盘扇区一般是512个字节~4k个字节。
磁盘上数据必须用一个三维地址唯一标示:柱面号、盘面号、扇区号。
读/写磁盘上某一指定数据需要下面步骤:
(1) 首先移动臂根据柱面号使磁头移动到所需要的柱面上,这一过程被称为定位或查找。
(2)所有磁头都定位到磁道上后,这时根据盘面号来确定指定盘面上的具体磁道。
(3) 盘面确定以后,盘片开始旋转,将指定块号的磁道段移动至磁头下。
经过上面步骤,指定数据的存储位置就被找到。这时就可以开始读/写操作了。
可以看见,磁盘读取依靠的是机械运动,分为寻道时间、旋转延迟、传输时间三个部分,这三个部分耗时相加就是一次磁盘IO的时间,一般大概9ms左右。寻道时间(seek)是将读写磁头移动至正确的磁道上所需要的时间,这部分时间代价最高;旋转延迟时间(rotation)是磁盘旋转将目标扇区移动到读写磁头下方所需的时间,取决于磁盘转速;数据传输时间(transfer)是完成传输数据所需要的时间,取决于接口的数据传输率,在纳秒级,远小于前两部分消耗时间。磁盘读取时间成本是访问内存的几百倍到几万倍之间。
为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存,这个称之为预读
。这样做的理论依据是计算机科学中著名的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。
程序运行期间所需要的数据通常比较集中。
大家可以试运行下面这段代码:
public static void main(String[] args) {
int[][] arr = new int[10000][10000];
int sum = 0;
long startTime = System.currentTimeMillis();
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
/*按行访问数组*/
sum += arr[i][j];
}
}
System.out.println("按行耗时:" + (System.currentTimeMillis() - startTime) + "ms");
sum = 0;
startTime = System.currentTimeMillis();
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[0].length; j++) {
/*按列访问数组*/
sum += arr[j][i];
}
}
System.out.println("按列耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}
就能看到局部性原理对程序性能的影响。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),一般来说,磁盘的顺序读的效率是随机读的40到400倍都有可能,顺序写是随机写的10到100倍(SSD盘则差距要小的多,顺序读写的效率是随机读写的7到10倍,但是有评测表明机械硬盘的顺序写性能稍优于SSD。总的来说Mysql数据库如果由硬盘由机械的换成SSD的,性能会有很大的提升),因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页,页大小通常为4k当然也有16K的,主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
按照磁盘的这种性质,如果是一个页存放一个B+树的节点,自然是可以存放很多的数据的,比如InnoDB里,默认定义的B+树的节点大小是16KB,这就是说,假如一个Key是8个字节,那么一个节点可以存放大约1000个Key,意味着B+数可以有1000个分叉。同时InnoDB每一次磁盘I/O,读取的都是 16KB的整数倍的数据。也就是说InnoDB在节点的读写上是可以充分利用磁盘顺序IO的高速读写特性。
同时按照B+树逻辑结构来说,在叶子节点一层,所有记录的主键按照从小到大的顺序排列,并且形成了一个双向链表。同一层的非叶子节点也互相串联,形成了一个双向链表。那么在实际读写的时候,很大的概率相邻的节点会放在相邻的页上,又可以充分利用磁盘顺序IO的高速读写特性。所以我们对MySQL优化的一大方向就是尽可能的多让数据顺序读写,少让数据随机读写。
Mysql内核查询成本计算
内容:
01.复习Optimizer Trace
02.什么是MySQL执行查询的成本?
03.MySQL查询成本计算实战
04.Index Dive和基于索引统计数据的成本
05.如何用EXPLAIN输出成本?
06.什么是连接查询的的Condition filtering?
07.两表连接的成本分析
08.连接查询用EXPLAIN输出连接成本
09.多表连接的成本分析
10.如何调节成本引导MySQL优化器的执行
11.辨析InnoDB底层的成本统计数据
3. Optimizer Trace
对于MySQL5.6之前的版本来说,只能通过EXPLAIN语句查看到最后优化器决定使用的执行计划,却无法知道它为什么做这个决策。我们可能有这样的疑问:“我就觉得使用其他的执行方案比EXPLAIN输出的这种方案强,凭什么优化器做的决定和我想的不一样呢?为什么MySQL一定要全文扫描,不用索引呢?”
在MySQL 5.6以及之后的版本中,MySQL提出了一个optimizer trace的功能,这个功能可以让我们方便的查看优化器生成执行计划的整个过程
比如对于下面这个SQL语句:
SELECT * FROM order_exp WHERE order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S') AND expire_time> '2021-03-22 18:28:28' AND expire_time<= '2021-03-22 18:35:09' AND insert_time> expire_time AND order_note LIKE '%7排1%' AND order_status = 0;
我们执行如下的命令:
SET optimizer_trace="enabled=on";
SELECT * FROM order_exp WHERE order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S') AND expire_time> '2021-03-22 18:28:28' AND expire_time<= '2021-03-22 18:35:09' AND insert_time> expire_time AND order_note LIKE '%7排1%' AND order_status = 0;
SELECT * FROM information_schema.OPTIMIZER_TRACE\G
可以看见全表扫描的成本:2169.9
使用索引idx_order_no的成本为72.61:
使用索引idx_expire_time的成本为47.81:
最终MySQL使用了idx_expire_time作为这个SQL查询过程中索引:
因为优化器最终会选择成本最低的那种方案来作为最终的执行计划。
但是这些成本怎么来的呢?搞明白了这些成本的由来,就不会再有诸如“为什么MySQL一定要全文扫描,不用索引呢?为什么MySQL要用A索引不用B索引之类的疑问?”了,因为以上的答案都可以用成本分析来解答。所以接下来,我们就要深入MySQL的内核来看看这些成本是如何计算的。
4. 什么是成本
MySQL执行一个查询可以有不同的执行方案,它会选择其中成本最低,或者说代价最低的那种方案去真正的执行查询。什么是执行成本呢?其实在MySQL中一条查询语句的执行成本是由下边这两个方面组成的:
I/O成本
我们的表经常使用的MyISAM、InnoDB存储引擎都是将数据和索引都存储到磁盘上的,当我们想查询表中的记录时,需要先把数据或者索引加载到内存中然后再操作。这个从磁盘到内存这个加载的过程损耗的时间称之为I/O成本。
CPU成本
读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称之为CPU成本。
对于InnoDB存储引擎来说,页是磁盘和内存之间交互的基本单位,MySQL规定读取一个页面花费的成本默认是1.0,读取以及检测一条记录是否符合搜索条件的成本默认是0.2。1.0、0.2这些数字称之为成本常数,这两个成本常数我们最常用到,当然还有其他的成本常数。
注意,不管读取记录时需不需要检测是否满足搜索条件,其成本都算是0.2。
5. 单表查询的成本
5.1 Mysql查询成本计算实战
在一条单表查询语句真正执行之前,MySQL的查询优化器会找出执行该语句所有可能使用的方案,对比之后找出成本最低的方案,这个成本最低的方案就是所谓的执行计划,之后才会调用存储引擎提供的接口真正的执行查询,这个过程总结一下就是这样:
1、根据搜索条件,找出所有可能使用的索引
2、计算全表扫描的代价
3、计算使用不同索引执行查询的代价
4、对比各种执行方案的代价,找出成本最低的那一个
我们依然以上面的查询语句来分析:
SELECT * FROM order_exp WHERE order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S') AND expire_time> '2021-03-22 18:28:28' AND expire_time<= '2021-03-22 18:35:09' AND insert_time> expire_time AND order_note LIKE '%7排1%' AND order_status = 0;
我们一步一步分析一下。
1. 根据搜索条件,找出所有可能使用的索引
MySQL把一个查询中可能使用到的索引称之为possible keys。
我们分析一下上边查询中涉及到的几个搜索条件:
order_no IN ('DD00_6S', 'DD00_9S', 'DD00_10S')
,这个搜索条件可以使用二级索引idx_order_no
。
expire_time> '2021-03-22 18:28:28' AND expire_time<= '2021-03-22 18:35:09'
,这个搜索条件可以使用二级索引idx_expire_time
。
insert_time> expire_time
,这个搜索条件的索引列由于没有和常数比较,所以并不能使用到索引
。
order_note LIKE '%hello%'
,order_note即使有索引,但是通过LIKE操作符和以通配符开头的字符串做比较,不可以适用索引
。
order_status =
0,由于该列上只有联合索引,而且不符合最左前缀原则,所以不会用到索引
。
综上所述,上边的查询语句可能用到的索引,也就是possible keys只有idx_order_no,idx_expire_time。
2. 计算全表扫描的代价
对于InnoDB存储引擎来说,全表扫描的意思就是把聚簇索引中的记录都依次和给定的搜索条件做一下比较,把符合搜索条件的记录加入到结果集,所以需要将聚簇索引对应的页面加载到内存中,然后再检测记录是否符合搜索条件。由于查询成本=I/O成本+CPU成本
,所以计算全表扫描的代价需要两个信息:
聚簇索引占用的页面数
该表中的记录数
这两个信息从哪来呢?MySQL为每个表维护了一系列的统计信息
,关于这些统计信息是如何收集起来的我们放在后边再说,现在看看怎么查看这些统计信息。
MySQL给我们提供了SHOW TABLE STATUS
语句来查看表的统计信息,如果要看指定的某个表的统计信息,在该语句后加对应的LIKE
语句就好了,比方说我们要查看order_exp这个表的统计信息
可以这么写:
SHOW TABLE STATUS LIKE 'order_exp'\G
出现了很多统计选项,但我们目前只需要两个
:
Rows
本选项表示表中的记录条数。对于使用MyISAM存储引擎的表来说,该值是准确的,对于使用InnoDB存储引擎的表来说,该值是一个估计值。从查询结果我们也可以看出来,由于我们的order_exp表是使用InnoDB存储引擎的,所以虽然实际上表中有10567条记录,但是SHOW TABLE STATUS显示的Rows值只有10354条记录。
Data_length
本选项表示表占用的存储空间字节数。使用MyISAM存储引擎的表来说,该值就是数据文件的大小,对于使用InnoDB存储引擎的表来说,该值就相当于聚簇索引占用的存储空间大小,也就是说可以这样计算该值的大小:
Data_length = 聚簇索引的页面数量 x 每个页面的大小
我们的order_exp使用默认16KB的页面大小
,而上边查询结果显示Data_length的值是1589248,所以我们可以反向来推导出聚簇索引的页面数量:
聚簇索引的页面数量 = 1589248 ÷ 16 ÷ 1024 = 97
我们现在已经得到了聚簇索引占用的页面数量以及该表记录数的估计值,所以就可以计算全表扫描成本了。
现在可以看一下全表扫描成本的计算过程:
I/O成本
97 x 1.0 + 1.1 = 98.1
97指的是聚簇索引占用的页面数,1.0指的是加载一个页面的IO成本常数,后边的1.1是一个微调值。
TIPS:MySQL在真实计算成本时会进行一些微调,这些微调的值是直接硬编码到代码里的,没有注释而且这些微调的值十分的小,并不影响我们大方向的分析。
CPU成本
10354x 0.2 + 1.0 = 2071.8
10354指的是统计数据中表的记录数,对于InnoDB存储引擎来说是一个估计值,0.2指的是访问一条记录所需的CPU成本常数,后边的1.0是一个微调值。
总成本:
98.1 + 2071.8= 2169.9
综上所述,对于order_exp的全表扫描所需的总成本就是2169.9。
TIPS:我们前边说过表中的记录其实都存储在聚簇索引对应B+树的叶子节点中,所以只要我们通过根节点获得了最左边的叶子节点,就可以沿着叶子节点组成的双向链表把所有记录都查看一遍。
也就是说全表扫描这个过程其实有的B+树非叶子节点是不需要访问的,但是MySQL在计算全表扫描成本时直接使用聚簇索引占用的页面数作为计算I/O成本的依据,是不区分非叶子节点和叶子节点的。