经常遇到业务线的同学问,既然页面I/O对MySQL查询性能影响较大,那么对于一次MySQL查询,底层要进行多少次页面I/O呢?
为了回答这个问题,下文我们简化几个概念:
不得不说这是一个非常棒的问题,跟咱们的日常查询密切相关。这个问题看似简单,但回答起来并不那么容易。首先我们来看下MySQL B+树索引的结构:
我们都知道MySQL里,索引是用B+树来实现的。B+树的叶子结点才保存具体数据(聚簇索引保存的是完整行数据,而二级辅助索引保存的是索引值+主键,非覆盖索引查询还得回表),中间结点都是用来索引叶子结点的。
从索引结构可以看出,每次查询都要访问到叶子结点,其访问的页面数正好就是索引的高度h。例如,一次主键上的点查询SELECT * FROM USER WHERE id=1
,那么要查询h1个页面才能找到叶子结点里的行数据,也即进行h1次页面I/O。(另外,二级索引基本都加载在内存里了,这里我们暂忽略这种情况。)
综上,查询对应的页面I/O数跟利用的索引有关,主要分为以下几种情况:
从上可以看出,h的大小势必成为索引性能的一个关键。那么通常表的索引高度h是多大呢?
我们假设中间结点的扇出系数为k、叶子结点的行记录数为n,则叶子结点数为k^(h-1)
,总记录数为k^(h-1)*n
。
在InnoDB里,每个页面默认16KB,假设主键是4B的int类型。对于中间节点,每个主键值后有个页号4B,还有6B的其他数据(参考《MySQL技术内幕:InnoDB存储引擎》),那么扇出系数k=16KB/(4B+4B+6B)≈1170。我们再假设每行记录大小为1KB,则每个叶子结点可以容纳的记录数n=16KB/1KB=16。
在高度h=3时,叶子结点数=1170^2 ≈137W,总记录数=1170^2*16=2190W!!也就是说,InnoDB通过三次索引页面的I/O,即可索引2190W行记录。
同理,在高度h=4时,总行数=1170^3*16≈256亿条!!!
这只是我们的理论分析,InnoDB也没有提供相应的视图进行查看,那么我们该如何查看索引的真实高度呢?
姜承尧大神在《查看 InnoDB表中每个的索引高度》一文中有介绍查看索引真实高度的方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-83vYtQkU-1660010756226)(https://gitee.com/waterystone/article/raw/master/0002mysql_index_height/index_page_level.png)]
如上图所示,InnoDB是索引组织表,每个页都包含一个PAGE_LEVEL的信息(见上图右半部分),用于表示当前页所在索引中的高度。默认叶子节点的高度为0,那么Root页的PAGE_LEVEL+1就是这棵索引的高度。
接下去的问题就是怎样得到一张表所有索引的Root页所在的位置呢?在《MySQL技术内幕:InnoDB存储引擎》书中分析过
SELECT b.name, a.name, index_id, type, a.space, a.PAGE_NO
FROM information_schema.INNODB_SYS_INDEXES a,
information_schema.INNODB_SYS_TABLES b
WHERE a.table_id = b.table_id
AND a.space <> 0;
运行结果如下:
其中hexdump
就可以快速定位到各索引树的高度信息了。例如,我们通过如下命令查看user表聚簇索引的高度:
$hexdump -C -s 49216 -n 10 user.ibd
0000c040 00 01 00 00 00 00 00 00 03 2b
这里,49216表示的是16384*3+64
,即从第3个页内偏移量64位置开始读取10个字节,前两个字节为PAGE_LEVEL,后8个字节是index_id,就是上图中看到的index_id=811(0x032b=811)的聚簇索引,这里PAGE_LEVEL为00 01,那么索引树的高度就为2。
综上,查看索引真实高度的方法总结如下:
information_schema.INNODB_SYS_INDEXES
和information_schema.INNODB_SYS_TABLES
找到对应索引Root页在ibd文件的页面号PAGE_NO(聚簇索引通常为3,其他索引往上累加);hexdump -C -s 16384*PAGE_NO+64 -n 10 user.ibd
,前两个字节+1即为索引的高度。我们创建一个user表,其结构如下:
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`card_id` INT(11) NOT NULL DEFAULT '0' COMMENT '身份证号',
`name` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '英文名字',
`age` TINYINT(2) UNSIGNED NOT NULL DEFAULT '0' COMMENT '年龄',
`content` VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '附属信息',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_card_id` (`card_id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
持续向表中插入数据,card_id
字段跟id一样从1开始,name
为32个随机字符,content
为长度在[500,1000]之间的随机字符。数据示例如下:
我们通过hexdump -C -s 49216 -n 10 user.ibd && hexdump -C -s 65600 -n 10 user.ibd && hexdump -C -s 81984 -n 10 user.ibd
查看三个索引的真实高度,汇总如下:
总行数 | 聚簇索引高度 | uniq_card_id高度 | idx_name高度 |
---|---|---|---|
10 | 1 | 1 | 1 |
300 | 2 | 1 | 1 |
400 | 2 | 1 | 2 |
1200 | 2 | 1 | 2 |
1300 | 2 | 2 | 2 |
2W | 2 | 2 | 2 |
3W | 3 | 2 | 2 |
9W | 3 | 2 | 2 |
10W | 3 | 2 | 3 |
100W | 3 | 2 | 3 |
200W | 3 | 3 | 3 |
2200W | 3 | 3 | 3 |
2300W | 3 | 3 | 4 |
2600W | 3 | 3 | 4 |
2700W | 4 | 3 | 4 |
从真实数据可以看出,聚簇索引在2700W时高度才升为4,比上文分析的2190W要慢500W左右,这主要应该是因为2190W是基于行记录为1KB来估算的,而我们插入的数据在[0.5KB,1KB]之间,从页使得叶子结点可以容纳更多的行记录。
另外发现一个有趣的现象,idx_name
索引在2300W时高度就升为4。这主要是因为name
长度为32,扇出系数k=16KB/(32B+4B+6B)≈390,这比聚簇索引的1170要小的多(或者说要瘦的多),造成索引“瘦高”的情况。
information_schema.INNODB_SYS_INDEXES
和information_schema.INNODB_SYS_TABLES
找到对应索引Root页在ibd文件的页面号PAGE_NO(聚簇索引通常为3,其他索引往上累加);hexdump -C -s 16384*PAGE_NO+64 -n 10 user.ibd
,前两个字节+1即为索引的高度。看到这里,是不是有点心动了?那还不赶快用本文的方法去看下自己负责表的索引高度?
附赠快速查看命令:
#聚簇索引(第一个)
hexdump -C -s 49216 -n 10 user.ibd
#第二个索引
hexdump -C -s 65600 -n 10 user.ibd
#第三个索引
hexdump -C -s 81984 -n 10 user.ibd
关于作者
杜云杰,高级架构师,转转架构部负责人,转转技术委员会执行主席,腾讯云MVP。负责服务治理、MQ、云平台、APM、IM、分布式调用链路追踪、监控系统、配置中心、分布式任务调度平台、分布式ID生成器、分布式锁等基础组件。微信号:waterystone
,欢迎建设性交流。