【华为云MySQL技术专栏】TaurusDB统计信息机制解析,提升优化器效率

举报
GaussDB 数据库 发表于 2025/04/17 14:16:46 2025/04/17
【摘要】 1背景介绍统计信息是 TaurusDB 优化器生成高效执行计划的关键依据,其核心功能包括基数估算(Cardinality Estimation)、数据分布分析、物理存储统计。具体来说:基数估算:估算索引列不同值的数量,决定索引选择。数据分布分析:通过直方图(Histogram)记录列值分布,优化范围查询。物理存储统计:记录表大小、索引页数量等,辅助 I/O 成本计算。本文将基于源码介绍 Ta...

一、背景介绍

统计信息是 TaurusDB 优化器生成高效执行计划的关键依据,其核心功能包括基数估算(Cardinality Estimation)、数据分布分析、物理存储统计。具体来说:

基数估算:估算索引列不同值的数量,决定索引选择。

数据分布分析:通过直方图(Histogram)记录列值分布,优化范围查询。

物理存储统计:记录表大小、索引页数量等,辅助 I/O 成本计算。

本文将基于源码介绍 TaurusDB 统计信息的采集、更新、持久化机制的设计原理,同时介绍 TaurusDB 主备统计信息同步机制。

二、原理介绍

2.1 统计信息分层架构

TaurusDB 的统计信息体系采用了分层设计模式。在 Server 层,统计信息用于查询优化决策;InnoDB 引擎层,负责统计信息的采集和持久化。核心数据结构通过handler 接口实现了跨层交互,这一设计体现了 MySQL 存储引擎可插拔架构的精髓。

Server 层:管理优化器决策所需的逻辑统计,基于统计信息完成代价估算

InnoDB 引擎层:提供统计信息采样及持久化机制 (如索引页数量、行数估算)。

Handler 层:缓存运行时统计信息(如记录数、索引基数、数据文件大小等),避免重复访问引擎层。


1.png

统计信息分层架构示意图

在 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 统计信息分类

2.png

统计信息分类表

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 字段的值重新采集统计信息。

3.png

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)时效性不足


社区 MySQL 实现机制依赖主动执行特定操作或者后台线程检测满足阈值触发更新,在高频写入场景中易失效。

TaurusDB 提供了 innodb_rds_recalc_check_interval_ms 参数供客户修改,可以提高统计信息更新的时效性。

2)准确性不足


社区 MySQL 实现机制依赖采样数 innodb_stats_persistent_sample_pages,对于大表或者有数据倾斜的场景,难以采集准确的统计信息。客户可以使用直方图来应对数据倾斜的场景;

TaurusDB 开放了 innodb_stats_persistent_sample_pages 参数,客户可以根据需要调大该值,增加采样准确度。此外,对于因统计信息不准确导致执行计划不稳定的问题,TaurusDB 提供了 Statement Outline特性[1],客户可以指定索引而无需要修改业务。

 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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。