曾经优化慢查询时,经常在日志中看到 truncate,当时一直疑惑 truncate 为什么会慢。
转到数据库方向之后,又碰到过几次 truncate 执行时间过长,导致 MySQL 短暂卡住的问题。
经过源码分析和同事测试验证,发现这几次的问题都跟自适应哈希索引有关,所以,深入研究下自适应哈希索引就很有必要了。
自适应哈希索引大概会有 3 篇文章。第 1 篇,我们来看看自适应哈希索引的构造过程。
本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。
创建测试表:
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=utf8mb3
插入测试数据:
INSERT INTO `t1`(`id`, `i1`)
VALUES (10, 101), (20, 201), (30, 301);
示例 SQL:
SELECT * FROM `t1`
WHERE `i1` >= 101 AND `i1` < 301
自适应哈希索引,英文全称 Adaptive Hash Index,简称 AHI。
根据上下文需要,后续内容会混用自适应哈希索引和 AHI,不再单独说明。
顾名思义,自适应哈希索引也是一种索引,使用哈希表作为存储结构,自适应的意思是我们无法干涉它的创建、使用、更新、删除,而是由 InnoDB 自行决定什么时候创建、怎么创建、什么时候更新和删除。
我们知道 InnoDB 的主键索引、二级索引都是 B+ 树结构。执行 SQL 语句时,以下几种场景都需要定位到索引叶子结点中的某条记录:
第一条
和最后一条
记录。第一条
记录。得益于多叉结构,B+ 树的层级一般都比较少,通常情况下,从根结点开始,最多经过 3 ~ 4 层就能定位到叶子结点中的记录。
这个定位过程看起来挺快的,单次执行也确实比较快,但是对于频繁执行的场景,还是有一点优化空间的。
为了追求极致性能,InnoDB 实现了 AHI,目的就是能够根据搜索条件直接定位到索引叶子结点中的记录。
图片
上图是一棵高为 4 的 B+ 树示意图,定位索引叶子结点记录,需要从根结点开始,经过内结点、叶结点,最后定位到记录,路径为:① -> ② -> ③ -> ④
图片
AHI 会使用多个 hash 桶来存储哈希值到记录地址的映射,上图是一个 hash 桶的示意图。
橙色块表示记录地址组成的链表,因为多条记录可能被映射到 hash 桶中的同一个位置。
AHI 定位一条记录时,根据 where 条件中的某些字段计算出哈希值,然后直接到 hash 桶中找到对应的记录。
如果多条记录被映射到 hash 桶中的同一个位置,那么找到的是个链表,需要遍历这个链表以找到目标记录。
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 的资格:
具备构造 AHI 的资格之后,对于以下三种情况之一,会为数据页构造 AHI:
注意:从各个条件判断,到最终构造 AHI 的整个流程,并不是在执行一条 SQL 的过程中完成的,而是在执行多条 SQL 的过程中完成的。
到这里,构造 AHI 的主要流程就介绍完了,构造过程的具体细节,请继续往下看。
定位记录会调用 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);
}
...
}
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 的构造流程。
构造 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);
}
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` < 301
执行计划如下:
图片
通过执行计划可知,示例 SQL 执行过程中,会使用索引 idx_i1。
查询优化阶段,MySQL 需要定位到扫描区间的第一条和最后一条记录,用于计算扫描区间覆盖的记录数量:
查询执行阶段,从存储引擎读取第一条记录之前,需要定位扫描区间的第一条记录,即满足 id >= 101 的第一条记录,第 3 次调用 btr_cur_search_to_nth_level()。
定位扫描区间的第一条和最后一条记录,都是定位索引叶子结点中的记录。
到这里,我们就得到了前面那个问题的答案:3 次。
如果某个索引的计数达到了 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);
...
}
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;
}
btr_search_prefix_info_t 包含 3 个属性:
接下来,我们开始介绍 btr_search_info_update_hash() 的第 1 段代码逻辑。
// storage/innobase/btr/btr0sea.cc
/****** 第 1 段 ******/
static void btr_search_info_update_hash(btr_cur_t *cursor) {
dict_index_t *index = cursor->index;
int cmp;
...
// 索引中,通过几个字段能唯一确定一条记录?
// 对于主键索引,n_unique = 主键段字数量
// 对于二级索引,n_unique = 二级索引字段数量 + 主键字段数量
const uint16_t n_unique =
static_cast(dict_index_get_n_unique_in_tree(index));
const auto info = index->search_info;
/****** if_1 ******/
// 构造信息计数不等于 0
// 说明之前已经确定过构造信息了
if (info->n_hash_potential != 0) {
// info->prefix_info 中
// 保存了之前确定的构造信息
const auto prefix_info = info->prefix_info.load();
...
/****** if_2 ******/
// prefix_info.n_fields
// 表示之前确定的构造信息的字段数量
// std::max(up_match, low_match)
// 表示下界或上界字段数量中较大的那个
if (prefix_info.n_fields == n_unique &&
std::max(cursor->up_match, cursor->low_match) == n_unique) {
// 两个条件都满足,说明:
// 如果本次通过下界、上界确定构造信息
// 会和之前确定的构造信息相同
// 那么,构造信息计数加 1
info->n_hash_potential++;
return;
}
...
const bool low_matches_prefix =
0 >= ut_pair_cmp(prefix_info.n_fields, prefix_info.n_bytes,
cursor->low_match, cursor->low_bytes);
const bool up_matches_prefix =
0 >= ut_pair_cmp(prefix_info.n_fields, prefix_info.n_bytes,
cursor->up_match, cursor->up_bytes);
/****** if_3 ******/
// 这里的构造信息指的是:
// 索引对象中保存的之前确定的构造信息
// prefix_info.left_side = true
// 如果构造信息【大于】下界,且【小于等于】上界
// 构造信息计数(n_hash_potential)加 1
// prefix_info.left_side = false
// 如果构造信息【小于等于】下界,且【大于】上界
// 构造信息计数(n_hash_potential)加 1
if (prefix_info.left_side ? (!low_matches_prefix && up_matches_prefix)
: (low_matches_prefix && !up_matches_prefix)) {
info->n_hash_potential++;
return;
}
}
...
}
如果构造信息计数(info->n_hash_potential)不等于 0,if_1 条件成立,说明索引对象中已经保存了之前确定的构造信息。
但是,确定索引的 AHI 构造信息之后,还需要该索引的构造信息计数、某个数据页的计数满足条件,InnoDB 才会为该索引的该数据页构造 AHI。
所以,索引对象中已经保存了之前确定的构造信息对应两种情况:
构造信息计数被命名为 n_hash_potential(潜在的),就是因为存在第 2 种情况。
if_2、if_3 用于判断:通过本次定位记录时产生的下界或上界得到构造信息,是否和索引对象中保存的构造信息一致,如果一致,则增加构造信息计数。
if_2 包含两个表达式,如果值都为 true,说明上面的判断结果为 true,构造信息计数加 1(info->n_hash_potential++)。
如果 if_2 不成立,再判断 if_3 是否成立。
前面介绍过,对于前缀相同的一组记录,构造 AHI 时,由 left_side 决定选择最左边还是最右边的记录。
对于本次定位记录得到的下界、上界,left_side 决定它们怎么和索引对象中保存的构造信息比较。
left_side = true 时,如果以下两个条件同时满足,构造信息计数(n_hash_potential)加 1:
left_side = false 时,如果以下两个条件同时满足,构造信息计数(n_hash_potential)加 1:
如果 if_3 成立,说明本次定位记录得到的下界或上界的字段数量、字节数,和索引对象中保存的构造信息的字段数量(n_fields)、字节数(n_bytes)一致,构造信息计数加 1(info->n_hash_potential++)。
如果 if_3 不成立,说明构造信息变了,需要执行第 2 段代码,确定新的构造信息,并且重置构造信息计数。
// storage/innobase/btr/btr0sea.cc
/****** 第 2 段 ******/
static void btr_search_info_update_hash(btr_cur_t *cursor) {
...
info->hash_analysis = 0;
cmp = ut_pair_cmp(cursor->up_match, cursor->up_bytes, cursor->low_match,
cursor->low_bytes);
/****** if_4 ******/
if (cmp == 0) {
// where 条件没有定位到匹配的记录
// 构造信息计数清零
info->n_hash_potential = 0;
// 虽然给构造信息赋值了,但是这个信息不会被使用
/* For extra safety, we set some sensible values here */
info->prefix_info = {0, 1, true};
/****** elseif_5 ******/
} else if (cmp > 0) {
// 上界(up_match、up_bytes)大于下界(low_match、low_bytes)
// left_side 都会设置为 true
// 构造信息计数重置为 1
info->n_hash_potential = 1;
ut_ad(cursor->up_match <= n_unique);
// 如果上界字段数量等于 n_unique
// 使用上界作为新的构造信息
if (cursor->up_match == n_unique) {
info->prefix_info = {
/* n_bytes */ 0,
/* n_fields */ n_unique,
/* left_side */ true
};
// 下界字段数量(low_match)小于上界字段数量(up_match)
// 使用下界作为新的构造信息
} else if (cursor->low_match < cursor->up_match) {
info->prefix_info = {
/* n_bytes */ 0,
/* n_fields */ static_cast(cursor->low_match + 1),
/* left_side */ true
};
// 下界字段数量(low_match)等于上界字段数量(up_match)
// 下界字节数(low_bytes)小于上界字节数(up_bytes)
// 使用下界作为新的构造信息
} else {
info->prefix_info = {
/* n_bytes */ static_cast(cursor->low_bytes + 1),
/* n_fields */ static_cast(cursor->low_match),
/* left_side */ true
};
}
/****** else_1 ******/
} else {
// 上界(up_match、up_bytes)小于下界(low_match、low_bytes)
// left_side 都会设置为 false
// 构造信息计数重置为 1
info->n_hash_potential = 1;
ut_ad(cursor->low_match <= n_unique);
// 如果下界字段数量等于 n_unique
// 使用下界作为新的构造信息
if (cursor->low_match == n_unique) {
info->prefix_info = {
/* n_bytes */ 0,
/* n_fields */ n_unique,
/* left_side */ false
};
// 下界字段数量(low_match)大于上界字段数量(up_match)
// 使用上界作为新的构造信息
} else if (cursor->low_match > cursor->up_match) {
info->prefix_info = {
/* n_bytes */ 0,
/* n_fields */ static_cast(cursor->up_match + 1),
/* left_side */ false
};
// 下界字段数量(low_match)等于上界字段数量(up_match)
// 下界字节数(low_bytes)大于上界字节数(up_bytes)
// 使用上界作为新的构造信息
} else {
info->prefix_info = {
/* n_bytes */ static_cast(cursor->up_bytes + 1),
/* n_fields */ static_cast(cursor->up_match),
/* left_side */ false
};
}
}
}
第 2 段代码用于首次或者重新确定构造信息,主要逻辑如下:
ut_pair_cmp(up_match, up_bytes, low_match, low_bytes) 比较本次定位记录得到的上界、下界,比较结果保存到 cmp 中。
如果 cmp 等于 0,命中 if_4,说明 btr_cur_search_to_nth_level() 定位记录时,上界、下界相同(up_match 等于 low_match、up_bytes 等于 low_bytes),也就是没有找到匹配的记录,构造信息计数(info->n_hash_potential)重置为 0。
如果 cmp 大于 0,命中 elseif_5,说明本次定位记录时,下界小于上界,构造信息(prefix_info)的 left_side 属性都会被设置为 true。
if (cursor->up_match == n_unique) 条件成立,说明搜索条件能够唯一确定索引中的一条记录,使用 up_match 作为新构造信息的字段数量,构造信息计数(info->n_hash_potential)重置为 1,重新开始计数。
否则,剩下两种情况,取下界(因为比上界小)作为新构造信息。
cursor->low_match、low_bytes 都从 0 开始,变成数量时需要加 1。
如果 cmp 小于 0,命中 else_1,说明本次定位记录时,下界大于上界,构造信息(prefix_info)的 left_side 属性都会被设置为 false。
if (cursor->low_match == n_unique) 条件成立,说明搜索条件能够唯一确定索引中的一条记录,使用 low_match 作为新构造信息的字段数量,构造信息计数(info->n_hash_potential)重置为 1,重新开始计数。
否则,剩下两种情况,取上界(因为比下界小)作为新构造信息。
cursor->up_match、up_bytes 都从 0 开始,变成数量时需要加 1。
btr_search_update_block_hash_info() 的主要逻辑分为 2 段:
先来看第 1 段代码:
// storage/innobase/btr/btr0sea.cc
/****** 第 1 段 ******/
static bool btr_search_update_block_hash_info(...) {
...
const auto info = cursor->index->search_info;
// last_hash_succ 用于判断 where 条件是否命中了 AHI
// 先设置为 false
info->last_hash_succ = false;
...
// 数据页计数是否大于 0
if (block->n_hash_helps > 0 &&
// 构造信息计数是否大于 0
info->n_hash_potential > 0 &&
// 数据页对象中保存的构造信息
// (可能还没有用来构造过 AHI)
// 和本次确定的构造信息是否相同
block->ahi.recommended_prefix_info.load() == info->prefix_info.load()) {
if (block->ahi.index &&
block->ahi.prefix_info.load() == info->prefix_info.load()) {
// 数据页对象中保存的构造信息(用来构造过 AHI)
// 和本次确定的构造信息相同
// 说明 where 条件命中了 AHI
// 把 info->last_hash_succ 设置为 true
// 下一篇文章讲 AHI 命中时会用到这个属性
info->last_hash_succ = true;
}
// 构造信息没有变化,构造信息计数加 1
block->n_hash_helps++;
} else {
// 构造信息变了,重置数据页计数
block->n_hash_helps = 1;
// 新确定的构造信息保存到数据页对象中
block->ahi.recommended_prefix_info = info->prefix_info.load();
}
...
}
如果以下 3 个条件都满足:
说明数据页的构造信息没有变化,数据页计数加 1(block->n_hash_helps++)。
否则,数据页计数重置为 1,并且保存新的构造信息到数据页对象中(block->ahi.recommended_prefix_info)。
// storage/innobase/btr/btr0sea.cc
/****** 第 2 段 ******/
static bool btr_search_update_block_hash_info(...) {
...
// BTR_SEARCH_BUILD_LIMIT = 100
// 同一个索引的 AHI 构造信息
// 连续 100 次相同
if (info->n_hash_potential >= BTR_SEARCH_BUILD_LIMIT &&
// BTR_SEARCH_PAGE_BUILD_LIMIT = 16
// 同样的 AHI 构造信息
// 命中同一个数据页的次数
// 达到了该数据页中记录数量的十六分之一
block->n_hash_helps >
page_get_n_recs(block->frame) / BTR_SEARCH_PAGE_BUILD_LIMIT) {
// 满足上面 2 个条件之后
// 如果之前没有构造过 AHI,则返回 true
// 表示本次需要构造 AHI
if (!block->ahi.index ||
// 如果之前已经构造过 AHI
// 需要命中同一个数据页的次数
// 达到该数据页中记录数量的 2 倍
// 或者 AHI 构造信息发生了变化
// 才会重新构造 AHI
block->n_hash_helps > 2 * page_get_n_recs(block->frame) ||
block->ahi.recommended_prefix_info.load() !=
block->ahi.prefix_info.load()) {
return true;
}
}
// 不满足上面的一系列条件
// 返回 false
// 表示本次不需要构造 AHI
return false;
}
第 2 段代码,用于判断是否需要为数据页(block)构造或重新构造 AHI,满足以下两个条件,说明具备了为该数据页构造 AHI 的资格:
数据页(block)具备构造 AHI 的资格之后,只有以下三种情况会构造或重新构造 AHI:
满足前面一系列条件之后,就可以为数据页构造 AHI 了。
// storage/innobase/btr/btr0sea.cc
static void btr_search_build_page_hash_index(...) {
...
// block 是本次需要构造 AHI 的数据页控制块
// page 是数据页对象
const auto page = buf_block_get_frame(block);
// 数据页的 AHI 构造信息
const auto prefix_info = block->ahi.recommended_prefix_info.load();
const auto n_fields_for_offsets = btr_search_get_n_fields(prefix_info);
// 删除之前为该数据页构造的 AHI 记录
if (block->ahi.index && block->ahi.prefix_info.load() != prefix_info) {
btr_search_drop_page_hash_index(block);
}
...
// 数据页中的记录数量
const auto n_recs = page_get_n_recs(page);
...
// page_get_infimum_rec(page) 读取 infimum 伪记录
// page_rec_get_next() 读取数据页中的第 1 条用户记录
auto rec = page_rec_get_next(page_get_infimum_rec(page));
Rec_offsets offsets;
...
// 用于构造第 1 条用户记录的 AHI 的 hash 种子
const auto index_hash = btr_hash_seed_for_record(index);
// 计算第 1 条用户记录的 AHI hash 值
auto hash_value =
rec_hash(rec, offsets.compute(rec, index, n_fields_for_offsets),
prefix_info.n_fields, prefix_info.n_bytes, index_hash, index);
size_t n_cached = 0;
// left_side = true
// 对于前缀相同的一组记录(可能有一条或多条记录)
// 标记需要为这一组的第一条记录构造 AHI
if (prefix_info.left_side) {
hashes[n_cached] = hash_value;
recs[n_cached] = rec;
n_cached++;
}
// 循环,标记需要为数据页中第 2 条及以后的用户记录构造 AHI
for (;;) {
const auto next_rec = page_rec_get_next(rec);
if (page_rec_is_supremum(next_rec)) {
// left_side = false 时
// 标记需要为 supremum 伪记录构造 AHI
// 然后结束循环
if (!prefix_info.left_side) {
hashes[n_cached] = hash_value;
recs[n_cached] = rec;
n_cached++;
}
break;
}
// 为当前循环的记录计算用于构造 AHI 的 hash 值
const auto next_hash_value = rec_hash(
next_rec, offsets.compute(next_rec, index, n_fields_for_offsets),
prefix_info.n_fields, prefix_info.n_bytes, index_hash, index);
// hash_value != next_hash_value
// 说明换了一组记录
if (hash_value != next_hash_value) {
/* Insert an entry into the hash index */
// left_side = true
// 为下一组的第一条记录构造 AHI
if (prefix_info.left_side) {
hashes[n_cached] = next_hash_value;
recs[n_cached] = next_rec;
n_cached++;
} else {
// left_side = false
// 为本组的最后一条记录构造 AHI
hashes[n_cached] = hash_value;
recs[n_cached] = rec;
n_cached++;
}
}
rec = next_rec;
hash_value = next_hash_value;
}
...
// 为数据页构造 AHI 之后
// 重置数据页计数
block->n_hash_helps = 0;
// 已经用来构造过 AHI 构造信息保存到
// 数据页对象的 ahi.prefix_info 属性中
block->ahi.prefix_info = prefix_info;
block->ahi.index = index;
// 把每条记录的 AHI hash 值和记录地址
// 插入到 AHI hash 表中
const auto table = btr_get_search_table(index);
for (size_t i = 0; i < n_cached; i++) {
ha_insert_for_hash(table, hashes[i], block, recs[i]);
}
...
}
为数据页构造 AHI 主要分为三大步骤:
AHI 构造流程的前三步都是在判断是否满足某些条件,这些条件的范围从大到小。
先是索引级别,判断索引被命中的次数。
然后,是索引级别的构造信息计数。
构造信息来源于定位记录过程中产生的下界、上界,其源头是 where 条件,我们可以把它看成对 where 条件的抽象,或者更具体点,把它看成 where 条件的分类。
某个构造信息的计数达到指定次数,意味着如果根据这个构造信息(或者说这类 where 条件)构造 AHI,命中率会比较高。
InnoDB 以数据页为单位,一次性为某个数据页中的所有记录构造 AHI。
构造信息计数满足条件之后,还需要进一步决定为哪些数据页构造 AHI,于是就有了数据页计数(实际上是数据页级别的构造信息计数)。
当索引计数、构造信息计数、数据页计数都满足条件之后,某个数据页就初步具备了构造 AHI 的资格,最后,还会根据该数据页是否构造过 AHI、构造信息是否发生变化等条件,做出终极决定:是否为该数据页构造 AHI。
本文转载自微信公众号「一树一溪」,可以通过以下二维码关注。转载本文请联系一树一溪公众号。