1
背景介绍
统计信息是 TaurusDB 优化器生成高效执行计划的关键依据,其核心功能包括基数估算(Cardinality Estimation)、数据分布分析、物理存储统计。具体来说:
基数估算:估算索引列不同值的数量,决定索引选择。
数据分布分析:通过直方图(Histogram)记录列值分布,优化范围查询。
物理存储统计:记录表大小、索引页数量等,辅助 I/O 成本计算。
本文将基于源码介绍 TaurusDB 统计信息的采集、更新、持久化机制的设计原理,同时介绍 TaurusDB 主备统计信息同步机制。
2
原理介绍
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 统计信息分类

TaurusDB 的统计信息按照类型分类,可分为表级、索引级以及列级统计信息,优化器在不同算子下选择对应的统计信息做代价估算。
表级统计信息,描述表的基本物理特征,帮助优化器估算全表扫描成本。
索引级统计信息,评估索引的选择性和扫描成本,帮助优化器决定索引使用策略。
列级统计信息,记录列值分布,优化范围查询(WHERE col BETWEEN X AND Y)的选择率估算。
3
源码解析
在数据库实时运行过程中,统计信息在内存中实时发生变化,下面将对 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 Ndict_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)。
对于以上两种情况
//获取树高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 * 10level = 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 ,按照触发方式,主要分为两类:
主动更新
用户主动执行ANALYZE TABLE、DISCARD TABLESPACE / IMPORT TABLESPACE、TRUNCATE TABLE或ALTER TABLE。
后台触发
Innodb后台线程 dict_stats_thread 自动执行,当检测到表的修改行数累积到一定数量时(innodb_stats_persistent_sample_pages配置),会自动触发统计信息的重新收集并更新。
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方案改进
时效性不足
准确性不足
更新失效场景
社区 MySQL 后台线程触发更新,依赖统计计数器,当表对象从缓存中被淘汰后重新加载时,stat_modified_counter 会被重置,导致后台更新失效,如在多表更新场景中。
TaurusDB 已向社区提出该问题(Bug#117809)。
4
总结
本文从源码角度介绍了 TaurusDB 统计信息的采集、更新、持久化和使用机制,同时介绍 TaurusDB 主备统计信息同步机制,并指出了当前统计信息实现上存在的问题,希望能帮助您更好地理解和优化 TaurusDB 的统计信息管理。如果感兴趣,可以根据本文提到的函数名去阅读源码。
[1] TaurusDB Statement Outline特性,https://support.huaweicloud.com/kerneldesc-gaussdbformysql/gaussdbformysql_20_0018.html

END
华为云数据库 新用户特惠
Flexus云数据库RDS
6个月366元(原价1200)/
一年722.4元(原价2400)
RDS for MySQL单机版 1年729.12元
扫码抢购

活动时间:2025/4/15-2025/5/31
戳“阅读原文”,了解更多



