物化视图刷新跑完了,效果怎么样?刷新前基表变了多少行、刷新花了多久、每条内部 SQL 执行了几次——这些问题的答案都藏在 OceanBase 的刷新统计表里。本文要读的 ob_mview_refresh_stats_collect.cpp(约 480 行)就是统计的”采集层”,它与此前解读过的清理执行器(ob_mview_refresh_stats_purge)一收一放,构成 4.3 物化视图统计体系的闭环。文件核心是两个类:ObMViewRefreshStatsCollection 负责单个物化视图的统计采集,ObMViewRefreshStatsCollector 负责整次 REFRESH 调用(可能一次刷多个 MV)的运行级汇总,最终把数据落到 __all_mview_refresh_stats、__all_mview_refresh_change_stats、__all_mview_refresh_stmt_stats、__all_mview_refresh_run_stats 四张内部表。
核心数据结构:两级采集器
整个采集体系是”一对多”的两级结构。上层 Collector 在用户发起 DBMS_MVIEW.REFRESH 时创建,持有 mview_id → Collection 的哈希表;每刷一个 MV 就通过 alloc_collection() 分配一个下层 Collection。下层对象用 Collector 自带的内存池(ObArenaAllocator,标签 “MVRefStatsColl”)分配,析构时手动逐个调用析构函数再 free,这是 OceanBase 内部 ObNew + allocator 配套的典型内存管理惯例。
另一个关键概念是采集级别(collection_level),分三档:
- NONE:不采集任何统计;
- TYPICAL:采集刷新步数、各基表 MLog 的增/删/改行数;
- ADVANCED:在 TYPICAL 基础上再采集刷新前后的 MV 行数快照,以及每条刷新 SQL 的语句级明细。
级别越高,统计要跑的辅助查询越多、开销越大。用户通过刷新参数选择级别,代码里所有耗时的统计动作都先判断级别再执行——”尽力而为”是这个模块的基调。
关键流程:刷新前/后的采集
单 MV 采集器的生命周期围绕刷新主流程展开:collect_before_refresh 在刷新开始前打前基线,collect_after_refresh 在结束后打后基线,collect_stmt_stats 随每条刷新 SQL 执行被回调。
刷新前的采集逻辑最丰富,先看主框架:
int ObMViewRefreshStatsCollection::collect_before_refresh(ObMViewRefreshCtx &refresh_ctx)
{
...
omt::ObTenantConfigGuard tenant_config(TENANT_CONF(MTL_ID()));
const bool enable_adaptive_refresh = tenant_config.is_valid()
&& tenant_config->_mv_adaptive_complete_refresh_threshold.get() > 0;
collection_level_ = refresh_ctx.refresh_stats_params_.get_collection_level();
if (ObMVRefreshStatsCollectionLevel::ADVANCED == collection_level_) {
// ADVANCED 级别才统计刷新前的 MV 行数,作为 initial_num_rows 前基线
if (OB_TMP_FAIL(ObMViewRefreshHelper::get_table_row_num(trans, tenant_id_, mview_id_,
SCN::invalid_scn(), refresh_ctx.refresh_parallelism_, num_rows))) {
LOG_WARN("fail to get mview row num before refresh", KR(tmp_ret), K(mview_id_));
}
}
refresh_stats_.set_initial_num_rows(num_rows);
...
这里有个值得注意的细节:OB_TMP_FAIL 把错误码写进 tmp_ret 而不污染主返回值 ret。也就是说,统计查询失败只留一条 WARN 日志,刷新主流程照常继续——统计永远是”锦上添花”,绝不能绑架刷新结果。这个设计哲学贯穿全文件。
接下来是遍历每张依赖基表,统计 MLog 里的 DML 行数:
for (int64_t i = 0; OB_SUCC(ret) && i < refresh_ctx.dependency_infos_.count(); ++i) {
const ObDependencyInfo &dep = refresh_ctx.dependency_infos_.at(i);
if (dep.get_ref_obj_type() == ObObjectType::TABLE) {
...
if (table_schema->is_materialized_view()) {
// 嵌套 MV 场景:依赖对象本身也是 MV,用 MV 自己的刷新 SCN 范围
tmp_scn_range = refresh_ctx.mview_refresh_scn_range_;
} else {
tmp_scn_range = refresh_ctx.base_table_scn_range_;
}
if (ObOptimizerUtil::find_item(refresh_ctx.tables_need_mlog_, dep.get_ref_obj_id()) &&
OB_TMP_FAIL(ObMViewRefreshHelper::get_mlog_dml_row_num(
trans, tenant_id_, table_schema->get_mlog_tid(), tmp_scn_range,
refresh_ctx.refresh_parallelism_,
num_rows_ins, num_rows_upd, num_rows_del))) {
LOG_WARN("fail to get mlog dml row num", ...);
}
...
ObMViewRefreshChangeStats change_stats;
change_stats.set_detail_table_id(dep.get_ref_obj_id());
change_stats.set_num_rows_ins(num_rows_ins); // MLog 中 INSERT 行数
change_stats.set_num_rows_upd(num_rows_upd); // UPDATE 行数
change_stats.set_num_rows_del(num_rows_del); // DELETE 行数
change_stats_array_.push_back(change_stats);
}
}
这段代码处理了两个微妙场景:一是嵌套物化视图——依赖对象本身也是 MV 时,统计 MLog 用的 SCN 范围要用 MV 自己的刷新区间而不是基表区间;二是自适应完全刷新开关打开时(enable_adaptive_refresh),即使采集级别只有 TYPICAL 也要统计基表行数,因为这些数据是”基表变化小时退化为完全刷新”这一决策的输入。每张基表的 I/U/D 统计最后组装成一条 ObMViewRefreshChangeStats 存入内存数组,等待 commit 阶段统一落表。
语句级统计只在 ADVANCED 级别生效,每执行一条刷新 SQL 就记一条:SQL 文本全文、sql_id(从内部连接的 session 里取当前 sql_id)、执行耗时、序号。有了它,DBA 能在 __all_mview_refresh_stmt_stats 里完整回放一次刷新的执行轨迹。
落库:三表联写与运行级汇总
单 MV 采集器的 commit() 把内存中攒好的数据按”主表 → 变更表 → 语句表”的顺序依次 INSERT:
int ObMViewRefreshStatsCollection::commit(ObISQLClient &sql_client)
{
...
// refresh_stats:刷新级别、类型、步数、前后行数、结果码等主记录
if (OB_FAIL(ObMViewRefreshStats::insert_refresh_stats(sql_client, refresh_stats_))) { ... }
// change_stats:每张基表一条
for (int64_t i = 0; OB_SUCC(ret) && i < change_stats_array_.count(); ++i) { ... }
// stmt_stats:每条刷新 SQL 一条
for (int64_t i = 0; OB_SUCC(ret) && i < stmt_stats_array_.count(); ++i) { ... }
return ret;
}
注意所有 INSERT 共用 Collector 传下来的同一个事务,保证统计记录与刷新结果原子可见——要么一起出现在统计表里,要么一起消失。
上层 Collector 的 commit() 则是收总账:计算整体耗时、累加各 MV 的失败数与日志清理耗时、把所有涉及基表名拼成逗号串写进 base_tables,最后写 __all_mview_refresh_run_stats。里面有两处值得学习的细节。其一是并行度一致性校验:同一批刷新里各 MV 的实际并行度理应相同,代码逐个比对发现不一致就报 OB_ERR_UNEXPECTED——用统计代码顺手做了内部一致性断言。其二是版本兼容开关:
if (data_version >= DATA_VERSION_4_3_5_1) {
run_stats_.set_parallelism(last_refresh_parallelism);
}
parallelism 列是 4.3.5.1 版本新增的,只有集群最低数据版本达标才写这个字段,避免旧版本 observer 遇到未知列报错。这是升级场景下做特性开关的惯用手法,值得记住。
最后的收尾同样体现”统计不绑架主流程”的原则:统计事务用 trans.end(OB_SUCC(ret)) 提交,即使提交失败也用 COVER_SUCC(tmp_ret) 处理,不让统计侧的错误码覆盖主流程已经得出的结果。
小结
ob_mview_refresh_stats_collect.cpp 展示了 OceanBase 如何为物化视图刷新构建一套”分级、尽力而为、两级汇总”的统计体系:Collection 管 single MV 细账,Collector 管整次调用总账,四级采集粒度(无/典型/高级)让用户按需权衡开销与信息量。工程实践上,OB_TMP_FAIL + WARN 的容错模式、统计与业务同事务落库、数据版本门控新列写入这三招,都可以直接迁移到自己的系统设计里。至此,物化视图统计的采集、清理两侧都已解读完毕。