[第五届 OceanBase 数据库大赛] 决赛复盘 | 2025 全国大学生计算机系统能力大赛

日期:2025.12.20(凌晨) 12.25

个人总结:

有段时间没有写blog了,自从ICPC退役之后,一刻也没有为iCPC哀悼,紧接着登场的是OB比赛。

印象中西安区域赛是10月中旬那里吧,基本上打完西安区域赛就到ob的比赛了。

决赛是22号的下午6点结束,距离比赛结束还有两天的时间,很不幸的是22号我有考试,并且这门课这学期从来没有看过,所以实际的code时间大概就剩下了今天这一整天了。能不能把接下来的优化做出来,感觉已经很不好说了,由于改动到了一个很基本的头文件,导致现在要等很久编译,索性来补一篇这场比赛的记录。

ob的比赛真的要比我想象中的要难不少,其实在暑假打完数据库的比赛后听说了ob的比赛有点想参加,但是听说特别难于是放弃。

但是有个队伍差一个人,是上一次数据库比赛的前一个队伍,他们问我要不要一起打,我直接狠狠答应。于是就有了非常痛苦煎熬的道路。


update:12.25

决赛排名23名,无缘全国前20了qwq。

perface:

参赛队伍是[登山],初赛排名全国排名第十,决赛排名23名。

初赛之所以排名高,是因为我们在比赛前就规划好了先把去年2024年的题给做了,保证每个题都起码有一个人做过。由于那段时间和ICPC的时间重合,所以基本上都是白天ICPC,晚上数据库走起,初赛负责的题目并不是太多。有一个时间差,导致我们初赛满分的很快,这里感谢队长,把rag和初赛ob赛题的内容给包了,让我们初赛通过的时间很快。

但是我觉得我个人非常感觉后悔的一件事就是,在初赛满分了之后,我去搞了一个minilsm的lab,中间提了两个PR就紧接着去ob决赛了。现在想来,为什么当时打算做这个mini-lsm,是因为觉得决赛可能会用到,但是没有过这种性能优化的经验,导致走了很多弯路,最终这个lsm其实并没有对我的优化起到帮助。(虽然提了人生中前两个PR让我有点小窃喜)。

最后决赛两天一夜的时间去实现缓存,但是却没有实现出来,说实在的打击挺大的,因为我个人觉得这个缓存的实现难度并不是太高,甚至我最开始不是使用的KVCache,而是直接套了个hashmap去搞,但是最终火焰图发现在走get_next_batch_with_cache的时候几乎都会走laod_more_data,也就代表着我的缓存几乎没有什么用。在还有几分钟就要结束的时候才发现到了这个,真的觉得自己是个傻b。尽管拼命去找为什么了,最后还是惨淡收场。

ok说的有点长了,这里说一下本人负责的题目:
初赛:(除去了简单题):alias + simple_sub_query + complex_sub_query + create_view

复赛:内核赛道:index_merge优化 + 构建ID列索引 + 下推topn优化 + 缓存dim_iter or token_iter (rag 赛道是一点没有看过)

初赛

由于距离初赛都过去了快2个月的时间,我对于代码也并不是印象太深刻了,所以在这里挖一个坑吧,以后后续说不定回来填。

决赛

首先大概说一下,决赛的内容就是让我们去改动seekdb的源码,对于一条sql语句(类似是这个样子,实际的筛选条件由测试脚本决定)

SELECT docid_col, MATCH(fulltext_col) AGAINST('test') as _score
FROM test_insert_en
WHERE MATCH(fulltext_col) AGAINST('text')
  and base_id in ('base_id_1', 'base_id_2', 'base_id_3')
  and id < 1000
ORDER BY _score DESC
LIMIT 10;

进行优化,QPS肯定越高越好。初始的QPS官方评测机跑出来是 14 QPS上下浮动。

这里先大概说明,以下四个优化大概最后能优化到QPS600+。

index_merge 优化

其实这个思路的来源是官方的决赛指导中,提出了一个二阶段index merge优化的思路,让我知道了原来还有索引合并这种东西。官方可能第一阶段没有研究的很好?因为是演示时拿的oceanbase,然后直接建立在了index merge的基础上给了我们一个优化思路,但是seekdb本身甚至跑不起来index merge。

于是我觉得,可以先尝试实现index merge,之后再考虑这个二阶段的事情。

然后翻阅了源码后发现,seekdb原生是支持or条件的index merge,并没有支持and条件的(也没有支持IN条件表达式)。所以我们的目的就是让seekdb去支持and条件下的index merge。其实这部分可以去看oceanbase的源码,但是我当时脑子其实并没有转过来,因为发现ob的源码里是对于index merge and的情况去搞了一个index_merge_and_iter,我觉得有点麻烦,索性就直接自己搞了。

这里首先说明的是,在join_order.cpp里面,做了一些关于index_merge_or的一些情况。这个文件主要是用来进行一些路径的创建。
这里有一个大概的执行顺序(我也不是特别的确定):对于我们要走的index merge方法,优化器会先生成一条index merge的路径,然后根据这个路径去创建逻辑计划树,再然后是物理执行树,然后会是一个迭代器树,主要的代码运行也基本都是耗在这个迭代器树上,会进行各种扫描。

然后我们会发现,join_order.cpp里面,有一些关于index_merge_or的内容,但是缺少了and的情况。

所以我们就是要在这里进行修改。


create_access_paths -> create_index_merge_access_paths -> get_candi_index_merge_trees       ->  get_valid_index_merge_indexes
                                                                                            ->  generate_candi_index_merge_trees
                                                          !candi_index_trees.empty()        ->  check_candi_index_trees_match_hint
                                                                                            ->  ......
                                                                                            ->  get_valid_index_merge_indexes
                                                                                            ->  generate_candi_index_merge_trees
                                                                                            ->  prune_candi_index_merge_trees

                                                       -> do_create_index_merge_paths       ->  choose_best_selectivity_branch
                                                                                            ->  root_node->formalize_index_merge_tree()
                                                                                            ->  build_access_path_for_scan_node
                                                                                            ->  create_one_index_merge_path
                                                                                            ->  ......
                                                                                            ->  access_paths.push_back(static_cast<AccessPath*>(index_merge_path)))

                                                       -> check_index_merge_paths_contain_fts

                      (!is_match_hint && !contain_fts) -> prune_index_merge_path

这里是我当时做的时候列出来的一个函数的执行过程。

其中generate_candi_index_merge_trees 这个函数是用来构建一些候选的index_merge_tree。

这里有一个小地方就是:我们可以通过简单地打日志的办法,发现对于filter来说,如果支持了这种or的情况,那么多半filter的count()就是1,ob会对于这种情况,也就是filter搞出来一个树,这个树本身也是为了支持or条件而搞出来了的。那些or条件的内容都是filter的children。

那如果我们全是and的情况,ob并不会搞出来一个树,而就是一个个节点本身,也就是说,如果我们的谓词筛选是: where a < 1 and b > 1, 这种情况filter的count()就是2。

所以我的代码修改就是在这个函数里,加上对于and情况的支持:

       // 检查所有filters是否都是可索引的简单条件
       ObSEArray<ObIndexMergeNode*, 4> valid_nodes;
       ObSEArray<bool, 4> valid_flags;
       
       for (int64_t i = 0; OB_SUCC(ret) && i < filters.count(); ++i) {
         ObRawExpr *filter = filters.at(i);
         if (OB_ISNULL(filter) || 
             filter->get_expr_type() == T_OP_OR || 
             filter->get_expr_type() == T_OP_AND) {
           continue; 
         }
         
         ObIndexMergeNode *candi_node = NULL;
         bool is_valid = false;
         if (OB_FAIL(generate_candi_index_merge_node(ref_table_id,
                                                    filter,
                                                    valid_index_ids,
                                                    valid_index_cols,
                                                    candi_node,
                                                    is_valid))) { //在这个函数里面我们也会做适当地修改
           LOG_WARN("failed to generate node for simple filter", K(ret), KPC(filter));
         } else if (is_valid && OB_NOT_NULL(candi_node)) {
           valid_nodes.push_back(candi_node); //如果发现ok了,我们就放进去。
           valid_flags.push_back(true);
         } else {
           valid_flags.push_back(false);
         }
       }
           // 如果所有条件都可索引,则构建AND树
       if (valid_flags.count() == filters.count() && valid_flags.count() > 1 && valid_nodes.count() > 1) {
         ObIndexMergeNode *and_root = NULL; //这里我就十分暴力的去创建出来一个INderMergeNode root,去支持and的情况。
         if (OB_NOT_NULL(and_root = OB_NEWx(ObIndexMergeNode, allocator_))) {
           and_root->node_type_ = INDEX_MERGE_INTERSECT;
           for (int64_t i = 0; i < valid_nodes.count(); ++i) {
             if (OB_FAIL(and_root->children_.push_back(valid_nodes.at(i)))) {
               LOG_WARN("failed to add child to AND root", K(ret));
             }
           }
           adlog::DEBUG("and_root->node_type_ ",(int64_t)and_root->node_type_);
           adlog::DEBUG("and_root->children_ count() ",and_root->children_.count());
           if (OB_FAIL(and_root->formalize_index_merge_tree())) {
             LOG_WARN("failed to formalize AND tree", K(ret));
           } else {
             if (OB_FAIL(candi_index_trees.push_back(and_root))) {
               LOG_WARN("failed to push back AND tree", K(ret));
             } else {
               and_root->gen_code_ = 114514;
               adlog::DEBUG("Added AND merge tree for ", valid_nodes.count(), " conditions");
             }
           }
           adlog::DEBUG("and_root->node_type_ ",(int64_t)and_root->node_type_);
           adlog::DEBUG("and_root->children_ count() ",and_root->children_.count());
         }
       }

但是这里其实还有一点,就是ob本身其实时时刻刻都会有index_merge的情况。这应该是一些内部的优化吧,我并没有很深入了解。但是这种情况其实是和我们要创建index_merge_and的情况是冲突的。

这里就不得不提一下这里做法的弊端了:由于赛题本身是对混合查询做优化,所以这里index_merge的优化其实是很重要的。有一个关于计算cost的情况我并没有提及,是因为赛时时间过于紧张,个人期望是可以走index merge就会走index merge,所以cost方面我直接设定为0。这显然并不好,后续应当对这种情况进行完善。

接着说回来,对于后台时刻进行别的index merge的情况,这里对于ObIndexMergeNode,我创建了一个gen_code_,用来标记判断是后台的别的情况的index merge,还是我这边为了混合查询而做的index merge。(这里是用于进行后续的判断的)。

而对于generate_candi_index_merge_node这个函数,其实主要是修改

 else {
     ObSEArray<uint64_t, 1> candicate_index_tids;
     if (!get_tables().equal(filter->get_relation_ids())) {
       is_valid_node = false;
     } else if (!(filter->has_flag(IS_SIMPLE_COND) ||
                  filter->has_flag(IS_RANGE_COND) ||
                  filter->has_flag(CNT_MATCH_EXPR) ||
                  (filter->has_flag(IS_IN) ))) {
       is_valid_node = false;

用来支持in的情况。

然后比较关键的还有这个:

// ADVISE 
// 在这个函数里面,是对于 (已经构建好了AND树的and_root,去构建出来路径)
 int ObJoinOrder::do_create_index_merge_paths

其中,这里我们主要是修改了原本对于INTERSECT的情况的判断,因为之前是对于INTERSECT的情况,我们是要选择出来一个最优解的执行的,而不是全都要,去跑index merge。


 int ObJoinOrder::choose_best_selectivity_branch(ObIndexMergeNode *&candi_node) {
   adlog::DEBUG("ObJoinOrder::choose_best_selectivity_branch START");
   int ret = OB_SUCCESS;
 
 
   if (OB_ISNULL(candi_node)) {
     ret = OB_ERR_UNEXPECTED;
     LOG_WARN("get unexpected null", K(ret), KPC(candi_node));
     adlog::DEBUG("choose_best_selectivity_branch:: get unexpected null");
   } else if (INDEX_MERGE_UNION == candi_node->node_type_) {
     adlog::DEBUG("choose_best_selectivity_branch:: INDEX_MERGE_UNION == candi_node->node_type_");
     for (int64_t i = 0; OB_SUCC(ret) && i < candi_node->children_.count(); ++i) {
       if (OB_FAIL(SMART_CALL(choose_best_selectivity_branch(candi_node->children_.at(i))))) {
         LOG_WARN("failed to choose best branch for child", K(ret), KPC(candi_node->children_.at(i)));
       }
     }
   } else if (INDEX_MERGE_INTERSECT == candi_node->node_type_) {
     adlog::DEBUG("choose_best_selectivity_branch:: INDEX_MERGE_INTERSECT == candi_node->node_type_");
 
     bool has_scan_children = true;  
     // for (int64_t i = 0; i < candi_node->children_.count(); ++i) {
     //   if (OB_NOT_NULL(candi_node->children_.at(i)) &&
     //       (candi_node->children_.at(i)->node_type_ != INDEX_MERGE_SCAN and
     //        candi_node->children_.at(i)->node_type_ != INDEX_MERGE_FTS_INDEX)) {
     //     adlog::DEBUG("candi_node->children_.at(i)->node_type_ is error idx: ",i," type : ",(int64_t)candi_node->children_.at(i)->node_type_);
     //     has_scan_children = false;
     //     break;
     //   }
     // }
     adlog::DEBUG("choose_best_selectivity_branch::  candi_node->children_.count() ",candi_node->children_.count());
     adlog::DEBUG("choose_best_selectivity_branch::  has_scan_children ",has_scan_children);
 
     bool go_index_merge_path_flag = true;
 
     if(candi_node->gen_code_ != 114514) go_index_merge_path_flag = false;
 
     if(go_index_merge_path_flag){
       for (int64_t i = 0; OB_SUCC(ret) && i < candi_node->children_.count();++i) {
         if
         (OB_FAIL(SMART_CALL(choose_best_selectivity_branch(candi_node->children_.at(i)))))
         {
           LOG_WARN("failed to choose best branch for child", K(ret),
           KPC(candi_node->children_.at(i)));
         }
       }
     } else {
       //......
       //......
 
 
   }
   adlog::DEBUG("ObJoinOrder::choose_best_selectivity_branch END");
 
   return ret;
 }

这里就是对于我们之前的gen_code_去进行判断了,之所以说之前有冲突就是在这个地方。所以我们的gen_code_就派上用了,可以通过这个来区别开来,这个INTERSECT,到底是我们要跑的index merge and树,还是后台运行的别的内容。

大概是改完以上的内容后,可以跑通index_merge了,可以用explain检测一下。

但是大概率会出现Error的情况。因为关于实现index_merge_and,我们还需要修改index_merge_iter.cpp里面的内容。

之所以会出现Error的情况,是因为seekdb原生不支持index_merge_and,所以在ObDASIndexMergeIter::inner_init函数里面,对于INTERSECT的情况专门返回了ERROR。

所以我们改一下就好。


    if (OB_UNLIKELY(merge_type_ != INDEX_MERGE_UNION and merge_type_ != INDEX_MERGE_INTERSECT)) {
      ret = OB_INVALID_ARGUMENT;
      LOG_WARN("invalid merge type", K(merge_type_));
    } else
      if (OB_FAIL(CURRENT_CONTEXT->CREATE_CONTEXT(mem_ctx_, context_param))) {
      LOG_WARN("failed to create index merge memctx", K(ret));
    } else {
      common::ObArenaAllocator &alloc = mem_ctx_->get_arena_allocator();
      child_iters_.set_allocator(&alloc);
	  //....

这样做了之后,我们的sql语句应该是会有返回结果的,但是召回率不出意外是有问题的。
这是因为intersect_get_next_rows函数内部实现的不对。

其实这里我们参考一下union_get_next_rows的做法就大概能知道,这里少了关于result_buffer_的应用。导致我们返回的行有了数据覆盖等问题。

所以我们应该仿照union_get_next_rows,去重写一下这个intersect_get_next_rows就好了。

值得注意的是:关于这个函数的返回条件了,我最开始并没有写“当有迭代器扫描结束后就直接使得ret = ITER_END”,导致测试的时候一直超时。所以这个点一定要写。

大概的实现内容:


int ObDASIndexMergeIter::intersect_get_next_rows(int64_t &count, int64_t capacity)  
{
  int ret = OB_SUCCESS;
  adlog::DEBUG("ObDASIndexMergeIter::intersect_get_next_rows start");
  // {
  //   adlog::DEBUG("INTERSECT rowkey_exprs_ count", K(rowkey_exprs_->count()));  
  //   for (int64_t i = 0; i < rowkey_exprs_->count(); i++) {
  //     // adlog::DEBUG("rowkey_expr", i, std::to_string(*rowkey_exprs_->at(i)));
  //     {
  //       char buf[1024];  
  //       int64_t pos = 0;  
  //       pos = rowkey_exprs_->at(i)->to_string(buf, sizeof(buf));  
  //       adlog::DEBUG("rowkey_expr", i, std::string(buf, pos));
  //     }
  //   }
  // }
  
  count = 0;  
  result_buffer_.reuse();
  
  while (OB_SUCC(ret) && count < capacity) {  
    /* try to fill each child store */  
    int64_t output_idx = OB_INVALID_INDEX;  
    int cmp_ret = 0;  
    int64_t child_rows_cnt = 0;  
      
    for (int64_t i = 0; OB_SUCC(ret) && i < child_stores_.count(); i++) {
      IndexMergeRowStore &child_store = child_stores_.at(i);
      child_rows_cnt = 0;
      if (!child_store.have_data()) {
        if (!child_store.iter_end_) {
          ObDASIter *child_iter = child_iters_.at(i);
          if (OB_ISNULL(child_iter)) {
            ret = OB_ERR_UNEXPECTED;
            adlog::DEBUG("unexpected nullptr", K(i));
          } else {

            ret = child_iter->get_next_rows(child_rows_cnt, capacity);

            if (OB_FAIL(ret)) {
              if (OB_ITER_END != ret) {
                adlog::DEBUG("WARNING child_iter->get_next_rows(child_rows_cnt, capacity);");
              }
            }
            adlog::DEBUG("child_iter: ",K(i),"  ", K(child_rows_cnt));

            if (OB_ITER_END == ret && child_rows_cnt > 0) {
              ret = OB_SUCCESS;
            }
            if (OB_SUCC(ret)) {
              if (OB_FAIL(child_store.save(true, child_rows_cnt))) {
                adlog::DEBUG("WARNING failed to save child rows", K(child_rows_cnt), K(ret));
              } else if (OB_FAIL(compare(i, output_idx, cmp_ret))) {
                adlog::DEBUG("WARNING index merge failed to compare row", K(i), K(output_idx), K(ret));
              } else if (child_iter->get_type() == DAS_ITER_SORT) {
                adlog::DEBUG("i: ",i,"child_iter->get_type() == DAS_ITER_SORT" );
                reset_datum_ptr(child_iter->get_output(), child_rows_cnt);
              }
            } else if (OB_ITER_END == ret) {
              child_store.iter_end_ = true;
              ret = OB_SUCCESS;
            } else {
              adlog::DEBUG("WARNING failed to get next rows from child iter", K(ret));
            }
          }
        }
      } else if (OB_FAIL(compare(i, output_idx, cmp_ret))) {
        adlog::DEBUG("WARNING index merge failed to compare row", K(i), K(output_idx), K(ret));
      }
    }

    if (OB_FAIL(ret)) {
      adlog::DEBUG("WARNING OB_FAIL(RET)");
    } else if (output_idx == OB_INVALID_INDEX) {  
      ret = OB_ITER_END;
      adlog::DEBUG("ret = OB_ITER_END");
    } else {    
      bool all_matched = true;
      for (int64_t i = 0; OB_SUCC(ret) && i < child_stores_.count(); i++) {
        if (output_idx == i) {  
          continue;
        }
        if ((!child_stores_.at(i).have_data())) {
            all_matched = false;  // 这个child没有数据,不匹配
            ret = OB_ITER_END;
        } else if (OB_FAIL(compare(i, output_idx, cmp_ret))) {  
          adlog::DEBUG("index merge failed to compare row", K(i), K(output_idx), K(ret));  
        } else if (cmp_ret == 0) {  
          
        } else { 
          all_matched = false;  // rowkey不匹配  
        }
      }

      if (OB_FAIL(ret)) {
        adlog::DEBUG("OB_FAIL(ret)");
        continue;
      }
      if (!all_matched) {
        child_stores_.at(output_idx).cur_idx_++;  
        continue;
      }
      for (int64_t i = 0; OB_SUCC(ret) && i < child_stores_.count(); i++) {
        if (output_idx == i) {  
          continue;
        }
        child_stores_.at(i).to_expr();
      }
      child_stores_.at(output_idx).to_expr();
      if (OB_FAIL(save_row_to_result_buffer())) {  
        adlog::DEBUG("failed to save row to result buffer", K(ret)); 
      } else {  
        count += 1;
      }

    }  
  }

  if(OB_FAIL(ret)){
    if (ret == OB_ITER_END) {
      adlog::DEBUG("intersect_get_next_rows : OB_FAIL(ret) ret == OB_ITER_END");
    } else {
      adlog::DEBUG("intersect_get_next_rows : OB_FAIL(ret)");
    }
  }
  adlog::DEBUG("intersect_get_next_rows :",K(count));
  
  if (OB_ITER_END == ret && count > 0) {  
    ret = OB_SUCCESS;  
  }  
  if (OB_SUCC(ret) && count > 0) {
    if (OB_FAIL(result_buffer_.to_expr(count))) {
      adlog::DEBUG("failed to convert result buffer to exprs", K(ret));  
      LOG_WARN("failed to convert result buffer to exprs", K(ret));  
    }  
  }  
  return ret;  
}

这里的排查内容意外的很辛苦,因为我一度怀疑过很多地方,例如什么rowkey不匹配,FTS返回的结果有问题,没有SORT等,最后发现都没有问题。只能说其实怀疑的这些地方,通过检测union的正确性,就可以不用浪费那么多时间,可能这也是为后面没有时间写出来缓存埋下伏笔了吧。

实现这个优化,QPS会从14到150那里。

构建ID列索引

这个其实反倒是没有注意到,因为我发现我的测试脚本最开始很有问题,官方后来修复了之后我并没有重新拉取,而且导致我的本地出现了一些莫名其妙的问题。例如,我这边本地测试的时候,id列其实是有索引的。但是我后来才知道原来并没有索引,这个在官方最后一周给出来了,通过这个优化,可以从150QPS到200QPS。

这里是队友写的了,我就直接贴代码了:

主要是在ObCreateTableResolver::resolve_table_elements

    if (OB_SUCC(ret)) {
      // MySQL 模式下,AUTO_INCREMENT 列必须有索引。如果用户未手动为自增列建索引,
      // 自动为该列创建一个普通(非唯一)索引,避免违反引擎约束。
      if (lib::is_mysql_mode() && 0 != autoinc_column_id && !table_schema.is_external_table()) {
        ObCreateTableStmt *create_table_stmt = static_cast<ObCreateTableStmt*>(stmt_);
        ObColumnSchemaV2 *autoinc_col = NULL;
        if (OB_ISNULL(create_table_stmt)) {
          ret = OB_ERR_UNEXPECTED;
          SQL_RESV_LOG(WARN, "unexpected null create_table_stmt when auto add index for autoinc", K(ret));
        } else if (OB_ISNULL(autoinc_col =
                    create_table_stmt->get_create_table_arg().schema_.get_column_schema(
                        autoinc_column_id))) {
          ret = OB_ERR_UNEXPECTED;
          SQL_RESV_LOG(WARN, "failed to get autoinc column schema when auto add index",
                       K(ret), K(autoinc_column_id));
        } else if (autoinc_col->is_rowkey_column()) {
          // 已经在主键/rowkey 上,无需额外索引
        } else {
          // 构造一个仅包含自增列的普通本地索引
          HEAP_VARS_2((ObCreateIndexStmt, create_index_stmt), (ObPartitionResolveResult, resolve_result)) {
            reset();
            index_attributes_set_ = OB_DEFAULT_INDEX_ATTRIBUTES_SET;
            index_arg_.reset();
            sort_column_array_.reset();
            store_column_names_.reset();
            hidden_store_column_names_.reset();
            index_keyname_ = NORMAL_KEY;
            index_scope_ = NOT_SPECIFIED; // MySQL 默认本地索引
            name_generated_type_ = GENERATED_TYPE_SYSTEM;
            has_index_using_type_ = false;
            ObColumnSortItem sort_item;
            sort_item.column_name_ = autoinc_col->get_column_name_str();
            sort_item.prefix_len_ = -1;
            sort_item.order_type_ = common::ObOrderType::ASC;
            ObString uk_name;
            if (OB_FAIL(resolve_index_name(nullptr, sort_item.column_name_, false, uk_name))) {
              SQL_RESV_LOG(WARN, "resolve auto index name failed", K(ret));
            } else if (OB_FAIL(add_sort_column(sort_item))) {
              SQL_RESV_LOG(WARN, "add auto index column failed", K(ret), K(sort_item));
            } else if (OB_FAIL(generate_index_arg(false /*process_heap_table_primary_key*/))) {
              SQL_RESV_LOG(WARN, "generate auto index arg failed", K(ret));
            } else {
              ObCreateIndexArg &create_index_arg = create_index_stmt.get_create_index_arg();
              ObSArray<ObPartitionResolveResult> &resolve_results =
                  create_table_stmt->get_index_partition_resolve_results();
              ObSArray<obrpc::ObCreateIndexArg> &index_arg_list =
                  create_table_stmt->get_index_arg_list();
              index_arg_.index_key_ = static_cast<int64_t>(index_keyname_);
              if (OB_FAIL(create_index_arg.assign(index_arg_))) {
                LOG_WARN("fail to assign auto index arg", K(ret));
              } else if (OB_FAIL(resolve_results.push_back(resolve_result))) {
                LOG_WARN("fail to push back auto index resolve result", K(ret));
              } else if (OB_FAIL(index_arg_list.push_back(create_index_arg))) {
                LOG_WARN("fail to push back auto index arg", K(ret));
              }
            }
          }
        }
      }
    }

下推topn

其实本来是打算去实现官方说的二阶段优化的,但是我写到中间,发现太难实现了。需要改掉很多内容,路径,逻辑计划执行计划迭代器树等内容都需要更改。所以直接放弃了。

但是有个我们可以发现一个内容,就是去用explain,我们可以发现,主要是有两层,第一层是一个sort算子,里面内置了limit,去限制个数。第二层是我们目前已经改过了的index merge。

会发现其实有一个很耗时的内容,就是我们第二层扫描的结果很多,但是都需要返回到第一层再进行过滤,这个太慢了,我们完全可以在第二层的时候就把他过滤掉,毕竟我们是可以获取到这些数据的。耗时的一个关键点还有是因为我们在index merge的时候,那个result_buffer是进行拷贝操作的,所以也会有耗时内容。

那么我们要做什么呢?

首先我们是需要把limit给提取出来。但是注意,提取的并不是limit的value,而是limit_expr。

我一开始的时候没有考虑明白,把limit_value给记录了下来,放到了index_merge_ctdef里面,但是这样显然是不对的。毕竟这个ctdef是在构建执行计划的时候才会采用,但是我们之后再执行这样的语句的时候,其实并不会构建ctdef,而是会走缓存路线,只会构建出来rtdef。所以我们要做的,应该是把limit_expr给存到ctdef里面,然后每一次重新生成rtdef的时候,通过limit_expr把值算出来,再存到rtdef里面。

关于limit_expr的提取,我是在ObLogPlan::candi_allocate_order_by做的。

我的目的主要是,希望可以检测到,如果我们是这个sort+limit的形式,并且下层是index_merge的时候,把sort算子去掉,提取到它的limit_expr,然后放到我们的下层去。

  } else {
    for (int64_t i = 0; OB_SUCC(ret) && i < candidates_.candidate_plans_.count(); i++) {
      bool is_reliable = false;
      CandidatePlan candidate_plan = candidates_.candidate_plans_.at(i);
      OPT_TRACE("generate order by for plan:", candidate_plan);
      if (OB_FAIL(create_order_by_plan(candidate_plan.plan_tree_,
                                       order_items,
                                       topn_expr,
                                       is_fetch_with_ties))) {
        LOG_WARN("failed to create order by plan", K(ret));
      } else if (NULL != topn_expr && OB_FAIL(is_plan_reliable(candidate_plan.plan_tree_,
                                                               is_reliable))) {
        LOG_WARN("failed to check if plan is reliable", K(ret));
      } else if (is_reliable) {
        ret = limit_plans.push_back(candidate_plan);
      } else {

        // 在这里试试看能不能把sort算子给去掉 advise 
        adlog::DEBUG("在这里试试看能不能把sort算子给去掉");
        do {
          int ret = OB_SUCCESS;
          ObLogicalOperator *&top = candidate_plan.plan_tree_;
          if (OB_ISNULL(top) || log_op_def::LOG_SORT != top->get_type()) {  
            // 不是sort算子,无需处理  
            adlog::DEBUG("不是sort算子啊");
          } else {  
            ObLogSort *sort_op = static_cast<ObLogSort*>(top);  
            ObLogicalOperator *child = sort_op->get_child(0);  
            if (OB_NOT_NULL(child) && log_op_def::LOG_TABLE_SCAN == child->get_type()) {
              ObLogTableScan *table_scan = static_cast<ObLogTableScan *>(child);
              if (table_scan->use_index_merge()) {
                // 使用的index merge
                adlog::DEBUG("下层用的是index merge");
                // get_limit_expr
                // auto && limit_expr = table_scan->get_limit_expr();
                if (sort_op->get_topn_expr() != nullptr) {
                  // (limit_expr) = (sort_op->get_topn_expr());
                  table_scan->set_limit_expr(sort_op->get_topn_expr());
                  // 保留 Sort 算子用于精排(两阶段检索)
                  sort_op->set_topn_expr(nullptr);
                  adlog::DEBUG("把sort里面的limit_expr放到了当前的table_"
                               "scan的limit_count_expr里了");
                  table_scan->set_parent(nullptr);
                }
                // 保留 Sort 算子,不跳过
                top = child;
              }
              else {
                adlog::DEBUG("但是下层使用的不是index merge");
              }
            } else {
              adlog::DEBUG("下层不是table scan啊");
            }  
          } 
          
        }while(0);


        ret = order_by_plans.push_back(candidate_plan);
      }

另外,当时其实并没有分析的很明白。

ObSelectLogPlan::allocate_plan_top里也做了一遍。


    // allocate root exchange
    if (OB_SUCC(ret) && is_final_root_plan()) {
      // allocate material if there is for update without skip locked.
      // FOR UPDATE SKIP LOCKED does not need SQL-level retry, hence we don't need a MATERIAL to
      // block the output.
      if (optimizer_context_.has_no_skip_for_update() 
          && OB_FAIL(candi_allocate_for_update_material())) {
        LOG_WARN("failed to allocate material", K(ret));
        //allocate temp-table transformation if needed.
      } else if (!get_optimizer_context().get_temp_table_infos().empty() &&
                 OB_FAIL(candi_allocate_temp_table_transformation())) {
        LOG_WARN("failed to allocate transformation operator", K(ret));
      } else if (OB_FAIL(candi_allocate_root_exchange())) {
        LOG_WARN("failed to allocate root exchange", K(ret));
      } else {
        LOG_TRACE("succeed to allocate root exchange", K(candidates_.candidate_plans_.count()));
      }
    }

    // 在这里看能不能消除掉sort
    if (OB_SUCC(ret)) {  
      for (int64_t i = 0; OB_SUCC(ret) && i < candidates_.candidate_plans_.count(); i++) {
        ObLogicalOperator *&top = candidates_.candidate_plans_.at(i).plan_tree_;
        if (OB_ISNULL(top) || log_op_def::LOG_SORT != top->get_type()) {
          continue;
        }
        ObLogicalOperator *child = top->get_child(0);
        if (OB_ISNULL(child) or
            log_op_def::LOG_TABLE_SCAN != child->get_type()) {
          adlog::DEBUG("下层不是table scan啊");
          continue;
        }

        ObLogTableScan *table_scan = static_cast<ObLogTableScan *>(child);
        if (!table_scan->use_index_merge()) {
          adlog::DEBUG("下层用的不是index merge 啊");
          continue;
        }
        adlog::DEBUG("下层用的是index merge");
        ObLogSort *sort_op = static_cast<ObLogSort *>(top);
        
        if (sort_op->get_topn_expr() != nullptr) {
          table_scan->set_limit_expr(sort_op->get_topn_expr());
          // 保留 Sort 算子用于精排(两阶段检索)
          sort_op->set_topn_expr(nullptr);
          adlog::DEBUG("把sort里面的limit_expr放到了当前的table_"
                       "scan的limit_count_expr里了");
          table_scan->set_parent(nullptr);
        }

        // 保留 Sort 算子,不跳过
        top = child;
       
      }  
    }

  }
  return ret;
}

但是我们做了以上内容后,explain发现,竟然上层的sort算子还是没有取消掉,明明明这里确实是提取出来了limit_expr。这是为什么呢?

是因为ObSelectLogPlan::candi_allocate_order_by_if_losted这个函数会进行一个补充,把丢失的order给弄回来。

所以这里我们也需要再特判一下:


int ObSelectLogPlan::candi_allocate_order_by_if_losted(ObIArray<OrderItem> &order_items)
{
  int ret = OB_SUCCESS;
  bool re_allocate_happened = false;
  ObSEArray<CandidatePlan, 8> order_by_plans;
  if (!order_items.empty()) {
    candidates_.is_final_sort_ = true;
    for (int64_t i = 0; OB_SUCC(ret) && i < candidates_.candidate_plans_.count(); i++) {
      ObLogicalOperator *top = candidates_.candidate_plans_.at(i).plan_tree_;
      CandidatePlan &plan = candidates_.candidate_plans_.at(i);

      bool can_skip_sort = false;  
      if (OB_NOT_NULL(top) && log_op_def::LOG_TABLE_SCAN == top->get_type()) {  
        ObLogTableScan *table_scan = static_cast<ObLogTableScan*>(top);  
        if (table_scan->use_index_merge() and table_scan->get_limit_expr() != nullptr) {  
          can_skip_sort = true;  
          adlog::DEBUG("这里标识跳过");  
        }  
      }  
        
      if (!can_skip_sort) {  
        if (OB_FAIL(create_order_by_plan(plan.plan_tree_, order_items, NULL, false))) {  
          LOG_WARN("failed to create order by plan", K(ret));  
        }  
      }  

      // if (OB_FAIL(create_order_by_plan(plan.plan_tree_, order_items, NULL, false))) {
      //   LOG_WARN("failed to create order by plan", K(ret));
      // } else
      if (OB_FAIL(order_by_plans.push_back(plan))) {
        LOG_WARN("failed to push back", K(ret));
      } else if (top != candidates_.candidate_plans_.at(i).plan_tree_) {
        re_allocate_happened = true;
      }
    }
    candidates_.is_final_sort_ = false;
    if (OB_SUCC(ret) && re_allocate_happened) {
      int64_t check_scope = OrderingCheckScope::CHECK_SET;
      if (OB_FAIL(update_plans_interesting_order_info(order_by_plans, check_scope))) {
        LOG_WARN("failed to update plans interesting order info", K(ret));
      } else if (OB_FAIL(prune_and_keep_best_plans(order_by_plans))) {
        LOG_WARN("failed to prune and keep best plans", K(ret));
      } else { /*do nothing*/ }
    }
  }
  return ret;
}

做了以上内容后,我们现在可以把上层的sort算子去掉了,并且把limit_expr给提取了table_scan->set_limit_expr(sort_op->get_topn_expr());里面。

接下来,在ObTableScanOp::init_attach_scan_rtdef内容中,对于这个刚才说的情况进行判断:

    //
    if (attach_ctdef->op_type_ == DAS_OP_INDEX_MERGE) {
      auto && non_const_ctdef = const_cast<ObDASBaseCtDef*>(attach_ctdef);
      auto &&merge_ctdef = static_cast<ObDASIndexMergeCtDef*>(non_const_ctdef);
      ObDASIndexMergeRtDef *merge_rtdef = static_cast<ObDASIndexMergeRtDef*>(attach_rtdef);  
      int64_t limit = 0;  
      bool is_null = false;  
      
      LOG_INFO("INDEX_MERGE detected", "has_limit_expr", OB_NOT_NULL(merge_ctdef->limit_expr_));
      
      if (OB_NOT_NULL(merge_ctdef->limit_expr_) &&   
          OB_FAIL(calc_expr_int_value(*merge_ctdef->limit_expr_, limit, is_null))) {
        LOG_WARN("failed to calc limit expr", K(ret)); 
      } else if (!is_null && limit > 0) {  
        merge_rtdef->topn_limit_ = limit;  
        merge_rtdef->enable_topn_pushdown_ = true;
        LOG_INFO("TopN pushdown ENABLED in table_scan_op", K(limit));
      } else {
        merge_rtdef->enable_topn_pushdown_ = false;
        LOG_INFO("TopN pushdown DISABLED in table_scan_op", K(is_null), K(limit));
      }
    }  

这样,就在merge_rtdef里面成功的赋值了,我们现在剩下的任务,就是在index_merge_iter.cpp里面搞一个堆出来,使得我们可以在这一层成功的过滤掉数据。

首先是在inner_init函数里面加上以下内容:(有一些自己搞的初始化的内容先暂时别管)

        if (OB_SUCC(ret)) {
          if (OB_FAIL(result_buffer_.init(max_size_, eval_ctx_, output_, mem_ctx_->get_malloc_allocator()))) {
            LOG_WARN("failed to init merge result buffer", K(ret));
          } else {
            if (merge_rtdef_->enable_topn_pushdown_) {
              result_buffer_.enable_topn_ = true;
              result_buffer_.topn_finish_ = false;
              result_buffer_.first_flag_ = true;
              result_buffer_.topn_limit_ = merge_rtdef_->topn_limit_ ;
            } else {
              result_buffer_.enable_topn_ = false;
              result_buffer_.topn_finish_ = false;
              result_buffer_.first_flag_ = true;
            }
          }
        }

然后稍加改造:

int ObDASIndexMergeIter::save_row_to_result_buffer() {
  int ret = OB_SUCCESS;
  if (!result_buffer_.enable_topn_) {
    // LOG_INFO("save_row: using normal add_row");
    return result_buffer_.add_row();
  }
  // LOG_INFO("save_row: using TopN add_row_with_topn");
  return result_buffer_.add_row_with_topn();
}

这里就不详细展示ObDASIndexMergeIter::MergeResultBuffer::add_row_with_topn()的内容了,就是简单地加入行和score,放到堆里面。

只是最后输出的内容,我们用first_flag进行了一个标记,当我们是index_merge第一次走完了的时候,就把堆里面的内容放到一个数组里面。然后之后我们如果再要输出结果的话,就直接从数组里面开始输出就好了。和堆就没有什么关系了。(代码中间删去了一些检测或者析构的内容)

//advise
int ObDASIndexMergeIter::MergeResultBuffer::to_expr_from_topn(int64_t &size, int64_t capacity)  
{
  int ret = OB_SUCCESS;
  common::ObArray<TopNRow *> sorted_rows;
  //...
  if (first_flag_) {
    adlog::DEBUG("开始转移 : topn_heap_->count(): ", topn_heap_->count());
    out_arr_.reserve(topn_heap_->count());
    while (topn_heap_->count() > 0) {
      TopNRow *top = topn_heap_->top();
      adlog::DEBUG("top: score: ", top->score_);

      if (OB_FAIL(out_arr_.push_back(top))) {
        adlog::DEBUG("failed to push  to topn_heap_out_", K(ret));
        break;
      }
      topn_heap_->pop();

    }
    first_flag_ = false;
    arr_cur_idx_ = -1;
    std::sort(out_arr_.begin(), out_arr_.end(),
    [](const TopNRow *a, const TopNRow *b) {
      return a->score_ > b->score_;
    });
  }

  if (arr_cur_idx_ == out_arr_.count() - 1) {
    size = 0;
    return ret;
  }

  int64_t begin_idx_ = arr_cur_idx_ + 1;
  int64_t end_idx_ = std::min(arr_cur_idx_ + capacity + 1, out_arr_.count());


  // Convert sorted rows to expressions in batch
  {
    ObEvalCtx::BatchInfoScopeGuard batch_info_guard(*eval_ctx_);
    batch_info_guard.set_batch_size(end_idx_ - begin_idx_);
    
    for (int64_t i = begin_idx_; OB_SUCC(ret) && i < end_idx_; ++i) {
      batch_info_guard.set_batch_idx(i);
      TopNRow *row = out_arr_.at(i);
      adlog::DEBUG("输出的分数 i: ", i , " socre: ", row->score_);
      if (OB_ISNULL(row) || OB_ISNULL(row->row_data_)) {
        ret = OB_ERR_UNEXPECTED;
        LOG_WARN("unexpected null row", K(ret), K(i));
      } else if (OB_FAIL(row->row_data_->to_expr<true>(*exprs_, *eval_ctx_))) {
        LOG_WARN("failed to convert row to expr", K(ret), K(i));
      }
    }
    
    if (OB_SUCC(ret)) {
      size = end_idx_ - begin_idx_;
    }
    // Clean up all rows
    // ......
    arr_cur_idx_ = end_idx_ - 1;
  }
  
  return ret;
}

这样,就基本实现了topn下推到index merge层,QPS从200会提升到300这里。这也是我们队伍在本次比赛做的所有优化了。下面提到的缓存在思路上是完全可以实现的,所以也来稍微讲讲。

缓存token_iter(dim_iter)

其实这里,我个人认为,缓存dim_iter也好,还是缓存dim_iter里面的token_iter,都是一样的。毕竟本身都是为了把结果记录下来。

其实思路很简单,就是通过火焰图分析后,发现这个获取全文索引的分数和doc_id有点太慢了,我们这里做一个缓存,把数据都记录下来应该会快很多。

其实自己的思路有点乱七八糟,最开始的时候,只是想着,当这个迭代器扫描结束了之后,我们把扫描得到的数据存到一个hashmap里面,以后再有它一样token的迭代器要扫描的话,就直接用之前获取到的数据就好了。

但是这样实现了之后,发现这个缓存竟然没怎么起到作用。原因是因为竟然很多缓存迭代器不会扫到ITER_END。这里后续大概了解了下,貌似是ob本身对于这个有topn情况下对于全文检索的一种优化,并不会把迭代器所有的内容都扫描完。所以关于这个缓存是如何存放的就成了一个问题。

我后来想的做法就是,既然迭代器无法保证全扫描过,那么我直接强制让这个迭代器在第一次扫的时候,强制他走完,把结果存到缓存里面,这样应该就可以了。

从理论上来讲,应该是没有问题的,但是结果却发现返回ERROR。再细看,是发现貌似是底层的一个compare出错了,我并不是太理解为什么,虽然dim_iter和token_iter都是对于batch去搞,每一次自己的bathc结束了就会进行底层的扫描,但是我还是觉得强制扫描完了之后,走这个缓存也不会有什么问题。

但是很不幸的是错了,而且比赛迫在眉睫(还有就是我觉得这个貌似对于内存来讲不太友好),我不得不再想一个别的办法:就是我们对于一个token_iter来说,它每次读取多少数据我们是知道的,那么我们可以来判断一下,当前这个迭代器读取的数据,缓存是否有对应的数据,如果有的话,那么就直接读取缓存就好了。如果没有,那么就说明我们缓存的内容不够本次的read。那么就让存放缓存的那个迭代器继续走batch,直到缓存存放的数据够这个迭代器用了,就停下来。相当于就是做了一个按需存放缓存,这个做法对内存也应该会更加的友好。


class DDManager {  
  public:  
    hash::ObHashMap<ObString, TokenCacheData*> cache_map_;  
    hash::ObHashMap<ObString, ObTextRetrievalDaaTTokenIter*> origin_iter_map_;
    hash::ObHashMap<ObString, int64_t> cnt_map_;
    common::ObArenaAllocator allocator_;
    int64_t map_max_size_{150};
      
  public:  
  DDManager() : allocator_(ObMemAttr(MTL_ID(), "TokenCacheAlloc")) {
      cache_map_.create(map_max_size_, "TokenCache", "TokenCache");
      origin_iter_map_.create(map_max_size_,"Iter","TokenIter");
      cnt_map_.create(map_max_size_,"aaaaa","bvbbbb");
    }  
      
    ~DDManager() {  
		// ......
    }  
      
    static DDManager& get_instance() {  
      static DDManager instance;  
      return instance;  
    }  
      
    TokenCacheData* get_cache_data(const ObString &token) {  
      TokenCacheData *cached_data = nullptr;  
      cache_map_.get_refactored(token, cached_data);  
      return cached_data;  
    }  
      
    int create_cache_data(const ObString &token, TokenCacheData *&cached_data) {  
      int ret = OB_SUCCESS;  
      void *buf = allocator_.alloc(sizeof(TokenCacheData));  
      if (OB_ISNULL(buf)) {  
        ret = OB_ALLOCATE_MEMORY_FAILED;  
      } else {  
        cached_data = new(buf) TokenCacheData();  
        ret = cache_map_.set_refactored(token, cached_data);  
      }  
      return ret;  
    }
  
    ObTextRetrievalDaaTTokenIter* get_origin_iter(const ObString &token) {  
      ObTextRetrievalDaaTTokenIter *cached_data = nullptr;  
      origin_iter_map_.get_refactored(token, cached_data);  
      return cached_data;  
    }  
      
    int push_origin_iter(const ObString &token, ObTextRetrievalDaaTTokenIter *&cached_data) {  
      int ret = OB_SUCCESS;  
        
      ret = origin_iter_map_.set_refactored(token, cached_data);  
      
      return ret;
    }
  
    int create_token_iter(const ObString &token, const ObTextRetrievalScanIterParam &orig_param) {  
      int ret = OB_SUCCESS;  
  
      // 创建新的param,使用Manager的分配器  
      ObTextRetrievalScanIterParam new_param = orig_param;  
      new_param.allocator_ = &allocator_;  // 替换分配器  
  
      // 创建新迭代器  
      void *buf = allocator_.alloc(sizeof(ObTextRetrievalDaaTTokenIter));  
      if (OB_ISNULL(buf)) {  
        ret = OB_ALLOCATE_MEMORY_FAILED;  
      } else {  
        auto && new_iter = new(buf) ObTextRetrievalDaaTTokenIter();  
        if (OB_FAIL(new_iter->init(new_param))) {  
          LOG_WARN("failed to init token iter", K(ret));  
        } else if (OB_FAIL(origin_iter_map_.set_refactored(token, new_iter))) {  
          LOG_WARN("failed to store iter", K(ret));  
        }  
      }  
      return ret;  
    }
  
    // ....
  };

这是当时乱写的一个版本。

然后我们在ObTextRetrievalDaaTTokenIter里加上

class ObTextRetrievalDaaTTokenIter final : public ObISRDaaTDimIter
{
  // ADVISE 加上以下内容
  ADCacheData * own_cache_data_{nullptr};  
  bool use_cache_{false};  
  int64_t cache_read_idx_{-1};
  
  ObString token_name_;
  int force_build_cache();
  int init_with_cache();
  bool init_with_cache_flag_{false};

然后这是当时实现的一些函数:


int ObTextRetrievalTokenIter::get_next_batch_with_cache(const int64_t capacity,
                                                        int64_t &count) {
  int ret = OB_SUCCESS;
  count = 0;


  int64_t available_count =
  own_cache_data_->total_count_ - (cache_read_idx_ + last_read_count_ - 1 + 1);
  adlog::DEBUG("ObTextRetrievalTokenIter::get_next_batch_with_cache start");
  adlog::DEBUG("available_count: ", available_count);

  bool have_to_more_data_flag = false;

 // 这里就是判断本次阅读的数量是否够,如果不够的话,就判断一下是否还可以让对应的迭代器接着去读数据存数据
 // is_completed_这个就是表示迭代器是否扫描结束了、
  if (available_count < capacity and (!own_cache_data_->is_completed_)) {
    ret = load_more_data_to_cache(capacity - available_count);
    if (ret == OB_ITER_END) {
      own_cache_data_->is_completed_ = true;
      adlog::DEBUG("map里的缓存迭代器走到头了");
      ret = OB_SUCCESS;
    }
  } else{
    adlog::DEBUG("起到了缓存的效果");
  }
  available_count = own_cache_data_->total_count_ - (cache_read_idx_ + last_read_count_ - 1 + 1);
  int64_t read_count = std::min(available_count, capacity);
  adlog::DEBUG("token缓存里面: cache_read_idx_: ", cache_read_idx_," read_count: ",read_count, " total: ",own_cache_data_->total_count_);

  if (read_count <= 0) {
    ret = OB_ITER_END;
  } else {
    count = read_count;
    cache_read_idx_ += last_read_count_;
    last_read_count_ = read_count;
  }


  return ret;
}

int ObTextRetrievalTokenIter::load_more_data_to_cache(int64_t need_data_cnt) {
  // 获取缓存管理器实例
  adlog::DEBUG("开始尝试让缓存追加数据");
  int ret = OB_SUCCESS;
  TokenCacheManager &cache_mgr = TokenCacheManager::get_instance();

  // 获取存储的原始迭代器
  ObTextRetrievalTokenIter *origin_iter = cache_mgr.get_origin_iter(token_name_);
  if (OB_ISNULL(origin_iter)) {
    ret = OB_ERR_UNEXPECTED;
    LOG_WARN("failed to get origin iter from cache manager", K(ret),
             K(token_name_));
    adlog::DEBUG("怎么会 origin_iter 会 空指针");
  } else {
    // 获取或创建缓存数据
    TokenCacheData *cache_data = cache_mgr.get_cache_data(token_name_);
    if (OB_ISNULL(cache_data)) {

      adlog::DEBUG("ERROR! 竟然没有cache_data");
    }

    // 从原始迭代器扫描数据并添加到缓存
    int64_t loaded_count = 0;
    while (OB_SUCC(ret) && loaded_count < need_data_cnt) {
      int64_t batch_count = 0;
      // int64_t capacity = std::min(need_data_cnt - loaded_count, origin_iter->max_batch_size_);

      if (OB_FAIL(origin_iter->get_next_batch(origin_iter->max_batch_size_, batch_count))) {
        if (OB_UNLIKELY(OB_ITER_END != ret)) {
          LOG_WARN("failed to get next batch from origin iter", K(ret));
        } else {
          // ret = OB_SUCCESS;
          adlog::DEBUG("map里的 缓存迭代器 到的末尾");
          break; // 到达末尾,正常结束
        }
      } else if (batch_count > 0) {
        // 提取数据并添加到缓存
        if (OB_FAIL(extract_and_add_data_to_cache(*origin_iter, *cache_data, batch_count))) {
          LOG_WARN("failed to extract and add data to cache", K(ret));
        } else {
          loaded_count += batch_count;
        }
      }
    }

    if (OB_SUCC(ret)) {
      LOG_DEBUG("successfully loaded data to cache", K(loaded_count), K(token_name_));
      adlog::DEBUG("应该是追加成功了 loaded_count: ", loaded_count, "  token_name_: ", token_name_);
    }
  }

  return ret;
}

关于缓存的大概内容就是这样,按道理讲应该是对的? 但是很可惜有问题。

如果这个实现出来了,QPS应该会跑到600那里的,对应到决赛榜单就会是第19名,堪堪进去。

postscript

最终,这个比赛结束了。失败总会贯穿人生始终。来年大概也不会参加这个比赛了,两个队友要考研,我应该到时候会找实习 or work吧。

感谢我的队友,让我这次比赛可以专注于内核方面,rag赛道我是一点也不知道啊。

其实本来以为进20无望,但是实现出来了topn下推,以及rag赛道分数突然高了起来,导致最终又有了动力去搞,虽然结局并不美好...

最终还献祭了自己挂了一科阿巴阿巴。

其实这个比赛,前半个多月,我都没有什么进展,卡index merge的bug也卡了很久。一开始也打算去实现一些谓词过滤的下推,还没有实现出来。

这是第一次去阅读这么大型的代码,其实个人感觉貌似没有学到很多数据库的知识。倒是感觉到了ob数据库代码一些设计是很牛的。

其实学到更多的,就是看这些代码,去理解他是怎么跑起来的,怎么做可以让他跑得更快。

总的来说,其实还是很有意思的。说不定明年还有机会可以再参加一次呢?

接下来要考虑找实习的内容了,希望自己可以有一个不错的结果。

posted @ 2025-12-25 19:56  AdviseDY  阅读(22)  评论(0)    收藏  举报