OceanBase源码解读:向量索引刷新器 ob_vector_index_refresh

向量检索是 OceanBase 4.3 的头号新特性:CREATE VECTOR INDEX 建 HNSW/IVF 索引,DBMS_VECTOR 包负责管理。但索引不是建完就一劳永逸——基表每天有新向量写入,索引必须持续”追平”增量。追平这件事的执行者,就是本篇主角 src/storage/vector_index/ob_vector_index_refresh.cpp(717 行)。全文围绕一个类 ObVectorIndexRefresher 展开:增量搬运(REFRESH_DELTA)与全量重建(REBUILD_COMPLETE)两条路径,外加一套快照一致性 + 表锁并发控制。读懂它,就打通了 DBMS_VECTOR.REFRESH_INDEX 从调用到生效的完整链路。

一、向量索引的”三张表”世界

OceanBase 的向量索引在存储层不是一棵独立的树,而是一组内部表:

  • 基表:用户的原始表,含向量列;
  • delta buffer 表(domain table):建索引后,基表新写入的向量增量先进这张缓冲表,暂不进索引;
  • index id 表:向量索引”正本”,存放 vid(向量 ID)、type、分区键、scn 等列。

所谓”刷新索引”,本质就是把 delta buffer 里积压的增量搬进 index id 表;积压占比过高时干脆整体重建。刷新入口 DBMS_VECTOR.REFRESH_INDEX 最终落到 ObVectorIndexRefresher::refresh()

二、核心数据结构与初始化

类本身极薄,只持有两个指针,全部动作靠拼内部 SQL 和一次 DDL RPC 完成:

class ObVectorIndexRefresher {
public:
  int init(sql::ObExecContext &ctx,
           ObVectorRefreshIndexCtx &refresh_ctx);
  int refresh();  // 总分派:REBUILD_COMPLETE→do_rebuild / REFRESH_DELTA→do_refresh
private:
  sql::ObExecContext *ctx_;              // 会话 + SQL 代理
  ObVectorRefreshIndexCtx *refresh_ctx_; // 租户/基表ID/delta表ID/index id表ID/SCN/阈值/方式
  bool is_inited_;
};

init 只做指针赋值,但校验了三个关键依赖:会话(供 check_status 响应 kill)、SQL 代理(跑内部 SQL)、事务对象(保证搬运与删除在同一事务)。refresh_ctx_ 里的 refresh_method_ 决定走增量还是全量,refresh_threshold_delta_rate_threshold_ 分别是两条路径的触发开关。

三、一致性锚点:先取快照 SCN

动手前先从本租户事务服务取”当前读快照版本”:

transaction::ObTransService *txs = MTL(transaction::ObTransService *);
txs->get_read_snapshot_version(timeout_ctx.get_abs_timeout(), current_scn);

这个 SCN 是整场刷新的一致性边界:SCN 之前的增量”本次必须处理”,之后写入的留给下一轮。所有行数统计都用 AS OF SNAPSHOT scn 做快照读,确保基表、delta buffer、index id 表的计数落在同一时点,互相才有可比性。

四、增量刷新 do_refresh:六步瀑布

错误码瀑布风格依次完成:① 取三张表 schema → ② 状态检查(UNAVAILABLE 返回 OB_EAGAINrefresh_index_inner 内部重试,其余不可用直接报错)→ ③ 回收站检查(库/表在回收站则静默跳过)→ ④ 快照计数对比 refresh_threshold_,积压不够本次不动 → ⑤ 动态拼列名、设 DDL 级超时 → ⑥ 核心搬运。核心就两条 SQL:

INSERT INTO `index_id表`(scn列, vid, type, part列)
SELECT ora_rowscn, vid, type, part列
FROM `delta_buf表` WHERE ora_rowscn <= :scn;

DELETE FROM `delta_buf表` WHERE ora_rowscn <= :scn;

ora_rowscn 是行的数据版本锚点:小于等于 SCN 的都是”本次应处理”的数据。INSERT…SELECT 是快照读,搬运与删除在同一事务里,失败整体回滚——不丢增量、不重复处理。列名不是写死的:get_vector_index_col_names 按 schema 动态生成,delta buffer 表排除 scn/vector 前缀伪列,index id 表取 scn 前缀列加原列,天然兼容分区表与列扩展。

五、并发控制:排他锁 + 温和重试

搬运开始前先抢 delta buffer 表的 OBJ_TYPE_REFRESH_VECTOR_INDEX 排他锁:try_lock 失败就睡 100ms 循环重试,每轮先 check_status()——用户 kill 会话能立刻打断死等。锁挂在事务上,提交即释放。策略与物化视图刷新同款:宁可多等,也不让两个刷新并发跑。

六、全量重建 do_rebuild:先算账再动手

重建前先做一笔经济账:

} else if (0 != base_table_row_cnt &&
    (index_id_table_row_cnt + domain_table_row_cnt) * 1.0
      / base_table_row_cnt < refresh_ctx_->delta_rate_threshold_) {
  // 积压占比太小,重建不划算
  triggered = false;
}

delta_rate_threshold_=0 表示无条件重建;基表为空时直接重建。另有快路径:check_only_change_search_params 发现新旧参数只差搜索项(如 HNSW 的 ef_search)时置位 rebuild_index_online——改参数即可,不必真重建。真要重建时,组装 ObRebuildIndexArg(类型 REBUILD_INDEX_TYPE_VEC),RPC rebuild_vec_index 发给 RootService,复用 DDL 框架异步执行,wait_ddl_finish 阻塞等待且支持取消。IVF 重建前还要先取向量维度——重算聚类质心离不开它。

小结

ob_vector_index_refresh.cpp 展示了 OB 处理”二级结构追平主表”的通用套路:快照 SCN 定一致性边界 → 表锁定并发边界 → INSERT INTO SELECT 搬数据 → 阈值决定增量还是全量 → 大活外包给 DDL 框架。它与物化视图刷新(ob_mview_refresh)同宗同源,对照着读收获更大。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注