【华为云MySQL技术专栏】TaurusDB统计信息机制解析,提升优化器效率
一、背景介绍
统计信息是 TaurusDB 优化器生成高效执行计划的关键依据,其核心功能包括基数估算(Cardinality Estimation)、数据分布分析、物理存储统计。具体来说:
基数估算:估算索引列不同值的数量,决定索引选择。
数据分布分析:通过直方图(Histogram)记录列值分布,优化范围查询。
物理存储统计:记录表大小、索引页数量等,辅助 I/O 成本计算。
本文将基于源码介绍 TaurusDB 统计信息的采集、更新、持久化机制的设计原理,同时介绍 TaurusDB 主备统计信息同步机制。
二、原理介绍
2.1 统计信息分层架构
TaurusDB 的统计信息体系采用了分层设计模式。在 Server 层,统计信息用于查询优化决策;InnoDB 引擎层,负责统计信息的采集和持久化。核心数据结构通过handler 接口实现了跨层交互,这一设计体现了 MySQL 存储引擎可插拔架构的精髓。
Server 层:管理优化器决策所需的逻辑统计,基于统计信息完成代价估算。
InnoDB 引擎层:提供统计信息采样及持久化机制 (如索引页数量、行数估算)。
Handler 层:缓存运行时统计信息(如记录数、索引基数、数据文件大小等),避免重复访问引擎层。
图1 统计信息分层架构示意图
在 Server 层,通过调用 handler::info() 方法访问 Innodb 层的统计信息,为避免频繁访问引擎层,统计信息会被缓存于 TABLE 对象和 TABLE_SHARE 对象中。在构造 TABLE对象时,会将表的记录数、索引文件大小、数据文件大小等统计信息,存放到 handler::stats 中。
优化器在做代价估算时,会通过访问TABLE对象获取 handler ,以此获取保存在 handler::stats 中的统计信息。
索引信息和单列直方图保存在 TABLE_SHARE 对象中,所有会话的 TABLE 对象共享同一个 TABLE_SHARE 对象,通过访问 TABLE 对象,可以获取 TABLE_SHARE 对象中保存的统计信息。
在 InnoDB 层中,统计信息缓存在 dict_table_t和 dict_index_t 中。dict_table_t 包含表级统计信息,如表修改行计数器、采样数、表估算行数、主键索引大小以及非主键索引大小总和等。dict_index_t 包含索引级统计信息,如索引大小、索引叶子页数以及索引前缀基数等。
Innodb 层的统计信息会持久化在 mysql.innodb_table_stats 和 mysql.innodb_index_stats 两个系统表中。
2.2 统计信息分类
表1 统计信息分类表
TaurusDB 的统计信息按照类型分类,可分为表级、索引级以及列级统计信息,优化器在不同算子下选择对应的统计信息做代价估算。
表级统计信息,描述表的基本物理特征,帮助优化器估算全表扫描成本。
索引级统计信息,评估索引的选择性和扫描成本,帮助优化器决定索引使用策略。
列级统计信息,记录列值分布,优化范围查询(WHERE col BETWEEN X AND Y)的选择率估算。
三、源码解析
在数据库实时运行过程中,统计信息在内存中实时发生变化,下面将对 TaurusDB 统计信息采集、更新、持久化以及主备统计信息同步机制的源码进行解析。
3.1 统计信息采集
如2.2章节所述,为了保证执行计划的稳定性,TaurusDB 对统计信息实施了持久化机制。当手动执行 ANALYZE TABLE、ALTER TABLE 重建表,或者表中的数据修改行数超过阈值10%时(由innodb_rds_recalc_persistent_stats_threshold_pct参数控制,默认值为10%,由后台线程触发),系统会重新计算统计信息,并将其持久化存储。
3.1.1 统计算法简介
InnoDB 采用分层随机采样(Stratified Sampling)来估算索引基数,通过采样索引树的叶子节点来获取统计信息。参数 innodb_stats_persistent_sample_pages 控制采样的叶子节点个数。
采样算法总体逻辑为:随机选取一些叶子节点,读取其记录信息,并基于这些信息推算整个索引的统计信息。
对 Innodb 索引的采样算法的伪代码如下:
dict_stats_analyze_index()
//采样页数(如果表指定了采样数,使用表的指定值,未指定取innodb_stats_persistent_sample_pages参数)
let N = N_SAMPLE_PAGES(index)
try dict_stats_analyze_index_low(N)
if timed out, try again with smaller N
dict_stats_analyze_index_low(N)
//遍历索引的每一个前缀组合
for each n_prefix
//找到足够好的采样层
search for good enough level:
//在好的level上进行分析,获取指定层级的记录总数和不同值总数
dict_stats_analyze_index_level()
//遍历该层的每一个页,获取该层的页面总数、记录数、索引前缀向量在该层的不同值数、索引前缀向量在该层的每个不同值的边界下标
collect statistics about the given level
//如果该层不满足条件,寻找下一层
if we are not satisfied with the level,
search next lower level
//找到了足够好的采样层
we have found a good enough level here
//在该层上进行采样,从该层随机挑选一些记录,估算索引前n_prefix列不同值的个数
dict_stats_analyze_index_for_n_prefix(that level,
stats collected above)
//将采样层不同记录按照范围划分,并对每个范围随机选择记录,分析记录对应的叶子节点数据
dive below some records and analyze the leaf page there:
//根据当前的中间节点记录,下探到对应的叶子节点,并计算叶子节点上n-prefix列不同值的个数
dict_stats_analyze_index_below_cur()
从以上伪代码可以看出,算法的关键在于两点:
(1)找到足够好的采样层(level);
(2)在选择的level上选非叶节点,并下探到对应的叶子节点进行采样。
下面基于源码对以上两个关键动作做源码分析。
3.1.2 统计算法源码分析
TaurusDB 判断一个足够好的 level 的依据是:该层是否存在 'n_sample_pages * 10' 个不同的值。
这个依据考虑了性能影响及统计信息准确度的平衡,既不会因为采集信息少,导致统计信息偏差大,又不会因为采集过多,导致性能开销过高。
对于特殊情况,采样过程会退化为索引的全表扫描,以更快地获取更准确的统计信息:
情况1,整个 B+ 树只有1个根节点;
情况2,用户指定的采样页面数量很大(对于N列的索引,N_SAMPLE_PAGES(index) * N 超过 B+ 树叶子节点的总个数或者大于1000000)。
对于以上两种情况
针对情况1,如果 B+ 树只有一个根节点,则表示 B+ 只有一个页面,直接扫描该页面的记录即可;
针对情况2,Innodb 统计信息的采样过程针对的即是 B+ 的叶子节点,当采样总数超过叶子节点的总数时,最终会采样所有叶子节点。在这种场景下,跳过分层采样过程直接采集所有叶子节点记录,可以更快地完成统计信息的收集。
//获取树高
root_level = btr_height_get(index, &mtr);
//获取索引字段数
n_uniq = dict_index_get_n_unique(index);
//树高只有一层(即只有一个页面),或者采样数
//超过叶子节点数,走全表扫描
if (root_level == 0 || n_sample_pages * n_uniq >
std::min<ulint>(index->stat_n_leaf_pages, 1e6)) {
//对叶子节点进行全表扫描,并将扫描结果直接放入索引的
//dict_index_t内存对象中。
(void)dict_stats_analyze_index_level(
index, 0 /* leaf level */, index->stat_n_diff_key_vals,
&total_recs,&total_pages,
nullptr /* boundaries not needed */,
wait_start_time,&mtr);
return true;
}
除了以上特殊情况外,TaurusDB 按照 B+ 树从高到低的层级,以及索引从 n_prefix (表示索引列数)到1的层级进行遍历采样。过程如下:
step 1:函数首先针对 n_prefix 列,找到包含 `n_sample_pages * 10` 个不同值的最高层级 L;
step 2:从层级L开始,向下查找满足 n_prefix-1 列需要的层级。
//B+树从高到低,从根节点开始访问B+树,找到一个满足不同值个数>=n_sample_pages * 10的一层
const ib_uint64_t n_diff_required = n_sample_pages * 10
level = root_level;
level_is_analyzed = false;
//先找到对于n_prefix列包含>=n_sample_pages * 10个不同值的层级L则对于第n_prefx -1列,它第一个包含>=n_sample_pages * 10个不同值的层级必然<=L(即更低层)
for (n_prefix = n_uniq; n_prefix >= 1; n_prefix--) {
//采样层为1或者该层的不同记录数>=n_sample_pages * 10,则该层为采样层,否则往下一层继续寻找。在寻找n_uniq前缀列最佳层时,n_uniq-1到1前缀列的不同值记录数记录在n_diff_on_level中,可以直接复用
if (level_is_analyzed &&
(n_diff_on_level[n_prefix - 1] >= n_diff_required
|| level == 1)) {
goto found_level;
}
for (;;) {
const uint64_t prev_total_recs = total_recs;
//针对1到n前缀列,找到本层的总记录数,和不同前缀列的不同值个数,填入n_diff_on_level中。
//n_diff_on_level可以在寻找n_uniq-1到1前缀列的最佳层时进行复用,减少计算
if (!dict_stats_analyze_index_level(
index, level, n_diff_on_level, &total_recs,
&total_pages,n_diff_boundaries, wait_start_time,
&mtr)) {
n_sample_pages = prev_total_recs / 2;
}
level_is_analyzed = true;
//如果已经搜索到最后一层中间节点,或者已经找到了包含足够多不同值的level,即找到该n_prefix对应的最佳层。
if (level == 1 ||
n_diff_on_level[n_prefix - 1] >= n_diff_required) {
break;
}
}
found_level:
//估算索引前n_prefix列不同值的个数,上述流程找到合适的level后,将该层不同记录进行平均分组,在每个分组内,随机选取一个记录进行下探,找到第一个no-boring记录,并遍历对应的叶子节点,获取叶子节点内不同记录数。
if (!dict_stats_analyze_index_for_n_prefix(index, n_prefix,
&n_diff_boundaries[n_prefix - 1],
data, wait_start_time, &mtr))
以上从源码角度分析了 TaurusDB 统计信息的采集和计算方法,感兴趣的读者可以在源码中搜索函数名详细看一下实现逻辑。
3.2 统计信息更新机制
TaurusDB 统计信息的更新函数是 dict_stats_update_persistent ,按照触发方式,主要分为两类:
1)主动更新
用户主动执行ANALYZE TABLE、DISCARD TABLESPACE / IMPORT TABLESPACE、TRUNCATE TABLE或ALTER TABLE。
2)后台触发
Innodb后台线程 dict_stats_thread 自动执行,当检测到表的修改行数累积到一定数量时(innodb_rds_recalc_persistent_stats_threshold_pct 配置),会自动触发统计信息的重新收集并更新。
3.2.1 统计信息更新源码分析
-
主动更新方式
直接调用 dict_stats_update 函数,函数内通过 dict_stats_update_persistent 函数更新统计信息,其主要工作即是:
1)调用统计信息采集算法;
2)基于采集的统计信息,计算表级统计信息以及索引级统计信息;
3)调用 dict_stats_save 函数,将统计信息持久化到 innodb_table_stats 和 innodb_index_stats 系统表中。
-
后台触发方式
后台线程 dict_stats_thread 的主要任务就是周期性从 recalc_pool 中获取待更新统计信息的表,并执行 dict_stats_update_persistent 函数更新统计信息。
recalc_pool是一个保存 table_id_t 类型数据的向量。当对表进行DML操作时,会通过 row_update_statistics_if_needed 函数判断,是否需要将表加入到 recalc_pool 中。
void dict_stats_thread() { while (!SHUTTING_DOWN()) { //后台周期性唤起,检查是否有需要更新统计信息的表 wake up process based on rds_recalc_check_interval_ms ;
//备机统计信息更新逻辑:主机将持久化统计信息更新之后,备机需要同步标记内存中对应的统计信息失效并进行重新采集.
rpl dict stats mark statistics invalidated and updated;
//主机统计信息更新:取recalc_pool中第一个表进行统计信息重新采集、更新以及持久化
dict_stats_process_entry_from_recalc_pool(thd);
}
}
在dict_table_t对象中,保存了表的行修改计数器stat_modified_counter。每次对表进行DML操作时,stat_modified_counter都会加1。当累计修改的行数超过当前表总行数的1/10时(由innodb_rds_recalc_persistent_stats_threshold_pct 配置控制),就会将表加入到recalc_pool中,通过后台线程dict_stats_thread进行统计信息更新。
为了解决社区统计信息后台更新不及时的问题,TaurusDB 对 dict_stats_thread 后台线程进行优化,客户可通过配置 innodb_rds_recalc_check_interval_ms 参数,降低更新周期,提高统计信息更新的实时性。
此外,为了解决备机统计信息更新不及时的问题,TaurusDB 引入主备统计信息同步机制,dict_stats_thread 后台线程会定时更新备机的统计信息,提高备机统计信息的实时性。
3.3 统计信息持久化机制
Innodb层的统计信息通过调用 dict_stats_save 函数,再由 dict_stats_exec_sql 执行内置存储过程,来完成表的统计信息的持久化。表级统计信息保存在 innodb_table_stats 表中,而索引级统计信息则保存在 innodb_index_stats 表中。
3.4 TaurusDB统计信息主备同步机制
TaurusDB 是基于共享存储架构,备机共享主机的持久化的统计信息数据。当主机更新持久化统计信息之后,会发送相关的 redo 日志给备机;备机接收并解析所有有关统计信息变更的 redo 日志,获取到需要更新统计信息的表的 table id ,然后将相关的表信息交给后台的 dict_stats_thread 线程进行处理。
dict_stats_thread 线程将所有需要处理的表的 stat_need_reload 字段设置为 true 。当查询进行 open table 操作时,根据 stat_need_reload 字段的值重新采集统计信息。
图2 TaurusDB统计信息主备同步机制
3.4.1 TaurusDB统计信息主备同步源码分析
TaurusDB新增了 MLOG_TABLE_STATS_UPDATE 类型的 redo 日志,通过 mlog_log_uint64(nullptr, table_id, MLOG_TABLE_STATS_UPDATE) 记录主机对统计信息的更新操作,redo 信息中携带了表的 table id 信息,通知备机更新相应表的统计信息。
备机解析到redo日志后,将需要更新统计信息的table id聚合并放入m_recalc_pool_instance,如3.2.1章节所述,备机上的后台线程 dict_stats_thread 会周期性地标记对应表的统计信息为失效,并进行重新采集。
3.5 社区MySQL实现机制的问题
与TaurusDB方案改进
1)时效性不足
2)准确性不足
3)更新失效场景
社区 MySQL 后台线程触发更新,依赖统计计数器,当表对象从缓存中被淘汰后重新加载时,stat_modified_counter 会被重置,导致后台更新失效,如在多表更新场景中。
TaurusDB 已向社区提出该问题(Bug#117809)。
四、总结
本文从源码角度介绍了 TaurusDB 统计信息的采集、更新、持久化和使用机制,同时介绍 TaurusDB 主备统计信息同步机制,并指出了当前统计信息实现上存在的问题,希望能帮助您更好地理解和优化 TaurusDB 的统计信息管理。如果感兴趣,可以根据本文提到的函数名去阅读源码。
[1] TaurusDB Statement Outline特性,https://support.huaweicloud.com/kerneldesc-gaussdbformysql/gaussdbformysql_20_0018.html
- 点赞
- 收藏
- 关注作者
评论(0)