4006-998-758
新闻动态

MySQL索引的B+树到底有多高?

2021-12-29

图片


杜云杰——转转架构部负责人,转转技术委员会核心成员,负责服务治理、MQ、云平台、APM、IM、分布式调用链追踪、监控系统、配置中心、分布式任务调度平台、分布式ID生成器、分布式锁等基础组件。


— 1 —

问题


经常遇到业务线的同学问,既然页面I/O对MySQL查询性能影响较大,那么对于一次MySQL查询,底层要进行多少次页面I/O呢?

为了回答这个问题,下文我们简化几个概念:

  • h:统称索引的高度;

  • h1:聚簇索引的高度;

  • h2:二级辅助索引的高度;

  • k:中间结点的扇出系数。


— 2 —

分析


不得不说这是一个非常棒的问题,跟咱们的日常查询密切相关。这个问题看似简单,但回答起来并不那么容易。首先我们来看下MySQL B+树索引的结构[1]:


图片


我们都知道MySQL里,索引是用B+树来实现的。B+树的叶子结点才保存具体数据(聚簇索引保存的是完整行数据,而二级辅助索引保存的是索引值+主键,非覆盖索引查询还得回表),中间结点都是用来索引叶子结点的。


2.1 索引高度h与页面I/O数的关系


从索引结构可以看出,每次查询都要访问到叶子结点,其访问的页面数正好就是索引的高度h。例如,一次主键上的点查询SELECT * FROM USER WHERE id=1,那么要查询h1个页面才能找到叶子结点里的行数据,也即进行h1次页面I/O。(另外,二级索引基本都加载在内存里了,这里我们暂忽略这种情况。)


综上,查询对应的页面I/O数跟利用的索引有关,主要分为以下几种情况:

  • 点查询:

    • 聚族索引:h1

    • 二级索引:

      • 覆盖索引:h2

      • 回表查询:h2+h1

  • 范围查询:这种情况相对比较复杂,但跟点查询的原理类似,读者可自行分析;

  • 全表查询:B+树的叶子结点是通过链表连接起来的,对于全表查询,需要从头到尾将所有的叶子结点访问一遍。


2.2 索引高度h的理论计算


从上可以看出,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也没有提供相应的视图进行查看,那么我们该如何查看索引的真实高度呢?


— 3 —

查看索引真实高度的方法


姜承尧大神在《查看 InnoDB表中每个的索引高度》一文中有介绍查看索引真实高度的方法。


图片


如上图所示,InnoDB是索引组织表,每个页都包含一个PAGE_LEVEL的信息(见上图右半部分),用于表示当前页所在索引中的高度。默认叶子节点的高度为0,那么Root页的PAGE_LEVEL+1就是这棵索引的高度。


接下去的问题就是怎样得到一张表所有索引的Root页所在的位置呢?在《MySQL技术内幕:InnoDB存储引擎》书中分析过<space,3>这个页(即ibd文件的第3个页面,从0开始)是聚簇索引的Root页,在《MySQL内核:InnoDB存储引擎 卷1》中也分析,Root页的位置通常是不会更改的。那么其他索引的Root页所在的位置呢?通过下面的SQL语句可以查出表中各索引的Root页信息:


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;


运行结果如下:

图片


其中<SPACE,PAGE_NO>就是索引的Root页信息,SPACE可以认为是表的ibd文件,PAGE_NO代表ibd文件中的页面号(从0开始)。有了这些信息就可以方便的定位了,因为PAGE_LEVEL在每个Root页的偏移量64位置处,占用两个字节,这样我们通过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。


综上,查看索引真实高度的方法总结如下:

1.通过information_schema.INNODB_SYS_INDEXES和information_schema.INNODB_SYS_TABLES找到对应索引Root页在ibd文件的页面号PAGE_NO(聚簇索引通常为3,其他索引往上累加);

2.通过hexdump -C -s 16384*PAGE_NO+64 -n 10 user.ibd,前两个字节+1即为索引的高度。


— 4 —

验证索引的真实高度


我们创建一个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查看三个索引的真实高度,汇总如下:

2-21122911405aX.png


真实数据可以看出,聚簇索引在2700W时高度才升为4,比上文分析的2190W要慢500W左右,这主要应该是因为2190W是基于行记录为1KB来估算的,而我们插入的数据在[0.5KB,1KB]之间,从页使得叶子结点可以容纳更多的行记录。


另外发现一个有趣的现象,idx_name索引在2300W时高度就升为4。这主要是因为name长度为32,扇出系数k=16KB/(32B+4B+6B)≈390,这比聚簇索引的1170要小的多(或者说要瘦的多),造成索引“瘦高”的情况。


— 5 —

总结



  • 一次查询的页面I/O数跟索引高度h呈正相关,主要分为以下几种情况:

    • 点查询:

      • 聚族索引:h1

      • 二级索引:

        • 覆盖索引:h2

        • 回表查询:h2+h1

    • 范围查询:这种情况相对比较复杂,但跟点查询的原理类似,读者可自行分析;

    • 全表查询:B+树的叶子结点是通过链表连接起来的,对于全表查询,需要从头到尾将所有的叶子结点访问一遍。


  • 索引高度通常为2~4层。在高度h=3、主键为int类型、行记录大小为1KB时,可索引的总行数为1170^2*16=2190W。这也是很多大厂将2000W作为分库分表标准的原因;


  • 查看索引真实高度的方法如下:

    1. 通过information_schema.INNODB_SYS_INDEXES和information_schema.INNODB_SYS_TABLES找到对应索引Root页在ibd文件的页面号PAGE_NO(聚簇索引通常为3,其他索引往上累加);

    2. 通过hexdump -C -s 16384*PAGE_NO+64 -n 10 user.ibd,前两个字节+1即为索引的高度。


  • 索引高度h也跟索引字段的数据类型有关。如果是int或short,扇出系数大,索引效率更好,整个索引看起来属于“矮胖”型;而如果是varchar(32)等,那扇出系数就低了,整个索引看起来属于“瘦高”型,索引效率自然要低些。所以我们在字段选取类型时,其类型越简单效率越好。


看到这里,是不是有点心动了?那还不赶快用本文的方法去看下自己负责表的索引高度?

附赠快速查看命令:

#聚簇索引(第一个)
hexdump -C -s 49216 -n 10 user.ibd

#第二个索引
hexdump -C -s 65600 -n 10 user.ibd

#第三个索引
hexdump -C -s 81984 -n 10 user.ibd

参考资料

[1]MySQL B+树索引的结构: 

https://blog.csdn.net/qq_36118179/article/details/114268820


END


今年的双11和双12刚刚过去不久,作为电商平台的转转是如何进行全链路压测的呢?


杜云杰曾在11月上海举办的“K+全球软件研发行业创新峰会”上作了题为《转转全链路压测方案》的主题演讲,深入剖析了转转之前线上服务压测存在的实际问题,以及转转架构部为此设计的全链路压测的一套闭环解决方案,包括压测流量编排、请求隔离、数据隔离等;讲解了如何保证流量标签的链路透传;以及流量隔离的通用方案,不仅限于全链路压测场景,还适用于秒杀、灰度等。


返回列表