会员登录 - 用户注册 - 设为首页 - 加入收藏 - 网站地图 MySQL 自适应哈希索引—构造!

MySQL 自适应哈希索引—构造

时间:2025-11-05 13:51:14 来源:益强数据堂 作者:系统运维 阅读:216次

曾经优化慢查询时,适应索引经常在日志中看到 truncate,哈希当时一直疑惑 truncate 为什么会慢。构造

转到数据库方向之后,适应索引又碰到过几次 truncate 执行时间过长,哈希导致 MySQL 短暂卡住的构造问题。

经过源码分析和同事测试验证,适应索引发现这几次的哈希问题都跟自适应哈希索引有关,所以,构造深入研究下自适应哈希索引就很有必要了。适应索引

自适应哈希索引大概会有 3 篇文章。哈希第 1 篇,构造我们来看看自适应哈希索引的适应索引构造过程。

本文基于 MySQL 8.0.32 源码,哈希存储引擎为 InnoDB。构造

1、准备工作

创建测试表:

复制CREATE TABLE `t1` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `i1` int DEFAULT 0, PRIMARY KEY (`id`) USING BTREE, KEY `idx_i1` (`i1`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb31.2.3.4.5.6.

插入测试数据:

复制INSERT INTO `t1`(`id`, `i1`) VALUES (10, 101), (20, 201), (30, 301);1.2.

示例 SQL:

复制SELECT * FROM `t1` WHERE `i1` >= 101 AND `i1` < 3011.2.

2、AHI 有什么好处?

自适应哈希索引,英文全称 Adaptive Hash Index,简称 AHI。

根据上下文需要,后续内容会混用自适应哈希索引和 AHI,不再单独说明。

顾名思义,自适应哈希索引也是一种索引,使用哈希表作为存储结构,自适应的意思是我们无法干涉它的创建、企商汇使用、更新、删除,而是由 InnoDB 自行决定什么时候创建、怎么创建、什么时候更新和删除。

我们知道 InnoDB 的主键索引、二级索引都是 B+ 树结构。执行 SQL 语句时,以下几种场景都需要定位到索引叶子结点中的某条记录:

insert 需要定位到插入目标位置的前一条记录。查询优化阶段,select、update、delete 预估扫描区间记录数量时,需要定位到扫描区间的第一条和最后一条记录。查询执行阶段,select、update、delete 需要定位到扫描区间的第一条记录。

得益于多叉结构,B+ 树的层级一般都比较少,通常情况下,从根结点开始,最多经过 3 ~ 4 层就能定位到叶子结点中的记录。免费信息发布网

这个定位过程看起来挺快的,单次执行也确实比较快,但是对于频繁执行的场景,还是有一点优化空间的。

为了追求极致性能,InnoDB 实现了 AHI,目的就是能够根据搜索条件直接定位到索引叶子结点中的记录。

图片

上图是一棵高为 4 的 B+ 树示意图,定位索引叶子结点记录,需要从根结点开始,经过内结点、叶结点,最后定位到记录,路径为:① -> ② -> ③ -> ④

图片

AHI 会使用多个 hash 桶来存储哈希值到记录地址的映射,上图是一个 hash 桶的示意图。

橙色块表示记录地址组成的链表,因为多条记录可能被映射到 hash 桶中的同一个位置。

AHI 定位一条记录时,云南idc服务商根据 where 条件中的某些字段计算出哈希值,然后直接到 hash 桶中找到对应的记录。

如果多条记录被映射到 hash 桶中的同一个位置,那么找到的是个链表,需要遍历这个链表以找到目标记录。

3、原理介绍

AHI 能提升定位记录的效率,但是,有得到必然有付出,天上掉馅饼的事在计算机世界里是不会发生的。

为了简洁,这里我们把定位索引叶子结点中的记录简称为定位记录,后面内容也依此定义,不再单独说明。

高效定位记录,付出的代价是存储哈希值到记录地址的映射占用的内存空间、以及构造映射花费的时间。

因为有时间、空间成本,所以,InnoDB 希望构造 AHI 之后,能够尽可能多的用上,做到收益大于成本。直白点说,就是需要满足一定的条件,才能构造 AHI。

AHI 采用的是组团构造逻辑,也就是以数据页为单位,满足一定条件之后,就会为数据页中的所有记录构造 AHI,主要流程如下:

图片

第 1 步,为数据页所属的索引计数(index->search_info->hash_analysis)。

SQL 执行过程中,每次定位某个索引叶子结点中的记录,该索引的计数都会加 1。

如果索引计数达到 17,进入第 2 步,否则,执行流程到此结束。

因为第 2、3 步为构造信息计数、为数据页计数也是需要时间成本的,所以,这里设置了第 1 道槛,只有索引被使用一定次数之后,才会执行第 2、3 步。

第 2 步,为构造信息计数(index->search_info->n_hash_potential)。

对于 select、insert、update、delete,定位记录时,搜索条件和叶子结点中的记录比较,会产生两个边界,左边为下界,右边为上界,基于下界和上界可以得到用于构造 AHI 的信息,我们称之为构造信息。

图片

以上是定位记录时产生的下界、上界示意图。定位记录过程中,下界和上界会不断向目标记录靠近,最终,下界或上界的其中一个会指向目标记录。

如果某次定位记录时,基于下界或上界得到的构造信息,和索引对象中保存的构造信息一致,该构造信息计数加 1。否则,该索引计数清零,构造信息计数清零或重置为 1(具体见下一小节的介绍)。

第 3 步,为数据页计数(block->n_hash_helps)。

定位到索引叶子结点记录之后,就知道了该记录所属的数据页,如果本次得到的构造信息和数据页对象中保存的构造信息相同,数据页计数加 1,否则数据页计数重置为 1。

第 4 步,为数据页构造 AHI。

如果满足以下两个条件,第 3 步的数据页就具备了构造 AHI 的资格:

构造信息计数大于等于 100。数据页计数大于数据页中记录数量的十六分之一。

具备构造 AHI 的资格之后,对于以下三种情况之一,会为数据页构造 AHI:

该数据页之前没有构造过 AHI。该数据页之前构造过 AHI,并且构造 AHI 之后,数据页会从零开始重新计数,重新计数大于数据页中记录数量的两倍。该数据页之前构造过 AHI,但是本次确定的构造信息和之前不一样了。

注意:从各个条件判断,到最终构造 AHI 的整个流程,并不是在执行一条 SQL 的过程中完成的,而是在执行多条 SQL 的过程中完成的。

到这里,构造 AHI 的主要流程就介绍完了,构造过程的具体细节,请继续往下看。

4、构造过程

定位记录会调用 btr_cur_search_to_nth_level(),这也是 AHI 的构造入口:

复制// storage/innobase/btr/btr0cur.cc void btr_cur_search_to_nth_level(...) { ... if (btr_search_enabled && !index->disable_ahi) { // AHI 的构造入口 btr_search_info_update(cursor); } ... }1.2.3.4.5.6.7.8.9.10.

btr_search_enabled = true(默认值),表示 InnoDB 启用了 AHI。

!index->disable_ahi = true,即 index->disable_ahi = false,表示 index 对应的索引没有禁用 AHI。

只有内部临时表、没有主键的表禁用了 AHI。

也就是说,对于正常的用户表、系统表,!index->disable_ahi = true,会调用 btr_search_info_update(),进入 AHI 的构造流程。

(1)索引计数

构造 AHI 的第 1 步,就是调用 btr_search_info_update() 进行索引计数。

复制// storage/innobase/include/btr0sea.ic static inline void btr_search_info_update(btr_cur_t *cursor) { const auto index = cursor->index; ... // 索引计数加 1 const auto hash_analysis_value = ++index->search_info->hash_analysis; // BTR_SEARCH_HASH_ANALYSIS = 17(硬编码) if (hash_analysis_value < BTR_SEARCH_HASH_ANALYSIS) { /* Do nothing */ return; } ... btr_search_info_update_slow(cursor); }1.2.3.4.5.6.7.8.9.10.11.12.13.14.

btr_search_info_update() 每次被调用都会增加索引计数(++index->search_info->hash_analysis)。

自增之后,如果索引计数小于 17,不需要进入 AHI 构造流程的下一步,直接返回。

如果索引计数大于等于 17,调用 btr_search_info_update_slow(),进入 AHI 构造流程的下一步。

看到这里,大家是否会有疑问:对于一条能使用索引的 select 语句,如果 where 条件只有一个扫描区间,执行过程中,btr_search_info_update() 最多会被调用几次?

我们通过 1. 准备工作小节的示例 SQL 来揭晓答案,SQL 如下:

复制SELECT * FROM `t1` WHERE `i1` >= 101 AND `i1` < 3011.2.

执行计划如下:

图片

通过执行计划可知,示例 SQL 执行过程中,会使用索引 idx_i1。

查询优化阶段,MySQL 需要定位到扫描区间的第一条和最后一条记录,用于计算扫描区间覆盖的记录数量:

定位扫描区间的第一条记录,即满足 id >= 101 的第一条记录,第 1 次调用 btr_cur_search_to_nth_level()。定位扫描扫描区间的最后一条记录,即满足 id < 301 的最后一条记录,第 2 次调用 btr_cur_search_to_nth_level()。

查询执行阶段,从存储引擎读取第一条记录之前,需要定位扫描区间的第一条记录,即满足 id >= 101 的第一条记录,第 3 次调用 btr_cur_search_to_nth_level()。

定位扫描区间的第一条和最后一条记录,都是定位索引叶子结点中的记录。

到这里,我们就得到了前面那个问题的答案:3 次。

(2)构造信息计数

如果某个索引的计数达到了 17,就会进入 AHI 构造流程的第 2 步,根据本次定位记录过程中得到的下界和上界,确定使用索引的前几个字段构造 AHI,以及对于索引中前几个字段值相同的一组记录,构造 AHI 时选择这组记录的第一条还是最后一条。

3.原理介绍小节的第 2 步,我们已经把这些信息命名为构造信息。

基于定位记录时得到的下界和上界确定构造信息、为构造信息计数的逻辑由 btr_search_info_update_hash() 完成。

复制// storage/innobase/btr/btr0sea.cc void btr_search_info_update_slow(btr_cur_t *cursor) { ... const auto block = btr_cur_get_block(cursor); ... btr_search_info_update_hash(cursor); ... }1.2.3.4.5.6.7.8.

btr_search_info_update_hash() 的代码有点长,我们分 2 段介绍。

介绍第 1 段代码之前,我们先来看看表示构造信息的结构体定义(index->search_info->prefix_info):

复制// storage/innobase/include/buf0buf.h // 为了方便阅读,以下结构体定义对源码做了删减 struct btr_search_prefix_info_t { /** recommended prefix: number of bytes in an incomplete field */ uint32_t n_bytes; /** recommended prefix length for hash search: number of full fields */ uint16_t n_fields; /** true or false, depending on whether the leftmost record of several records with the same prefix should be indexed in the hash index */ bool left_side; }1.2.3.4.5.6.7.8.9.10.11.

btr_search_prefix_info_t 包含 3 个属性:

n_fields、n_bytes:索引的前 n_fields 个字段,第 n_fields + 1 个字段的前 n_bytes 字节用于构造 AHI。left_side:如果索引中多条记录的前 n_fields 个字段内容、第 n_fields + 1 个字段前 n_bytes 字节的内容相同,我们把这样的一组记录称为前缀相同的记录。对于前缀相同的记录:left_side = true 时,选择最左边的记录(第一条记录)构造 AHI。left_side = false 时,选择最右边的记录(最后一条记录)构造 AHI。

接下来,我们开始介绍 btr_search_info_update_hash() 的第 1 段代码逻辑。

复制// storage/innobase/btr/btr0sea.cc /

(责任编辑:系统运维)

上一篇:同样的先下载PHP源码包,键入“cd /usr/local/src”回车并执行“sudo wget http://cn2.php.net/distributions/php-5.3.8.tar.gz”下载PHP源码包。解压后进入php目录中,“cd php-5.3.8”回车,并执行“sudo ./configure --prefix=/usr/local/server/php --with-config-file-path=/usr/local/server/php --enable-mbstring --enable-ftp --with-gd --with-jpeg-dir=/usr --with-png-dir=/usr --with-mysql=mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-pear --enable-sockets --with-freetype-dir=/usr --enable-gd-native-ttf --with-zlib --with-libxml-dir=/usr --with-xmlrpc --enable-zip --enable-fpm --enable-fpm --enable-xml --enable-sockets --with-gd --with-zlib --with-iconv --enable-zip --with-freetype-dir=/usr/lib/ --enable-soap --enable-pcntl --enable-cli”回车。出现下图就可以继续进行下一步。再接着键入“sudo make && make install”。等待操作完成之后,复制启动脚本。输入“sudo cp ./sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm”回车,接着再执行“sudo chmod +x /etc/init.d/php-fpm”。修改PHP-FPM配置文件,依次执行“cd /usr/local/server/php/etc”、“mv php-fpm.conf.default php-fpm.conf”、“vi php-fpm.conf”编辑配置文件。去掉25行前的分号。修改第131和132行的user和group为当前用户(安装系统时设置的帐户名)。去掉161、166、171、176行前面的分号,如下图。保存并退出。PHP-FPM启动及退出分别使用命令“/etc/init.d/php-fpm start”与“/etc/init.d/php-fpm stop”。
下一篇:以用名字全拼做域名的利与弊(个人品牌建设的关键之一及域名选择的重要性)
推荐内容
  • 一. 先上两张图看看复制代码代码如下: 2. 安装Ubuntu tweak,用它来设置主题和图标4. 使用系统自带的软件中心安装,使用鼠标点击安装包即可,下附安装顺序(可参考ubuntu_mac_theme/web中的网页,由于使用了代理,打开时安全软件可能会提示)复制代码代码如下:复制代码代码如下:sudo unzip mac-fonts.zip -d /usr/share/fontssudo fc-cache -f -v启动tweak-tool设置成你喜欢的就可以了,如最上面的unity tweak tool图9. 重启系统10. 问题 a. slingscold有时可能会不生效,注销当前用户重新登陆试试11.以上是离线包安装,假如你想直接使用apt-get命令安装,可参考[1]中的链接网页(这个比较慢)或者第3步里下载的 包中的ubuntu_mac_theme/web下的网页(离线网页包)
  • 前端开发效率提高之代码规范篇
  • 15个必须知道的JavaScript数组方法
  • 万字总结之设计模式(扫盲篇)
  • Ramlog 以系统守护进程的形式运行。在系统启动时它创建虚拟磁盘(ramdisk),将 /var/log 下的文件复制到虚拟磁盘中,同时把虚拟磁盘挂载为/var/log。然后所有的日志就会更新到虚拟磁盘上。而当 ramlog 重启或停止时,需要记录到硬盘上的日志就会保留在目录/var/log.hdd中。而关机的时候,(ramdisk上的)日志文件会重新保存到硬盘上,以确保日志一致性。Ramlog 2.x默认使用tmpfs文件系统,同时也可以支持ramfs和内核ramdisk。使用rsync(译注:Linux数据镜像备份工具)这个工具来同步日志。注意:假如突然断电或者内核崩溃(kernel panic)时,没有保存进硬盘的日志将会丢失。假如你拥有够多的可用内存,而又想把日志放进虚拟磁盘,就安装ramlog吧。它是笔记本用户、带有UPS的系统或是直接在flash中运行的系统的优良选择,可以节省日志的写入时间。Ramlog的运行机制以及步骤如下:        Ramlog 由第一个守护进程(这取决于你所安装过的其它守护进程)启动。        然后创建目录/var/log.hdd并将其硬链至/var/log。        假如使用的是tmpfs(默认)或者ramfs 文件系统,将其挂载到/var/log上。        而假如使用的是内核ramdisk,ramdisk会在/dev/ram9中创建,并将其挂载至/var/log。默认情况下ramlog会占用所有ramdisk的内存,其大小由内核参数ramdisk_size指定。        接着其它的守护进程被启动,并在ramdisk中更新日志。Logrotate(译注:Linux日志轮替工具)和 ramdisk 配合的也很好。        重启(默认一天一次)ramlog时,目录/var/log.hdd将借助rsync与/var/log保持同步。日志自动保存的频率可以通过cron(译注:Linux例行性工作调度)来控制。默认情况下,ramlog 的调度任务放置在目录/etc/cron.daily下。        系统关机时,ramlog在最后一个守护进程关闭之前关闭。        在ramlog关闭期间,/var/log.hdd中的文件将被同步至/var/log,接着/var/log和/var/log.hdd都被卸载,然后删除空目录/var/log.hdd。在Ubuntu中安装Ramlog首先需要用以下命令,从这里下载.deb安装包: wget http://www.tremende.com/ramlog/download/ramlog_2.0.0_all.deb下载ramlog_2.0.0_all.deb安装包完毕,使用以下命令进行安装:复制代码代码如下:sudo dpkg -P ramlog注意:假如ramlog卸载之前仍在运行,需要重启系统完成整个卸载工作。
  • 嵌入式Linux网络编程,终于有人将网络的七层讲清楚了