上一篇:openGauss源码学习(四)SeqScan扫描算子 执行器部分
文章目录
前言
博客中介绍了代价估算和路径选择的一些基本概念和SeqScan的代价模型,大家也都知道一般条件选择率比较低的情况下更容易命中索引,反之更倾向于全表扫描。
本篇博客就从优化器源码来分析一下表上存在索引的情况下是如何选择更优的扫描路径,以及索引扫描算子的代价模型是怎样的。
一、Overview
索引在OpenGauss也可以看做是表的一种,索引的结构可以参考博客postgres中B-tree 索引结构深度解析。索引扫描有以下三种:
- Index Only Scan:最简单的索引扫描,不需要回表,只需要扫描索引就可以完成计算。
- Index Scan:每次找到满足条件的索引记录后,需要回表获取其他列的数据,一般是查询涉及到了索引列以外的列。
- Bitmap Index Scan:一般用于涉及多个索引或者索引条件OR连接起来的情况,将多个条件拆分成不同的Bitmap Index Scan,分别计算条件后取OR或AND,最后再根据位图结果统一从page中扫描,可以减少回表的次数。
二、索引路径生成
1. 调用关系
索引扫描路径生成的逻辑和全表扫描路径生成逻辑类似,调用栈如下:
make_one_rel
set_base_rel_pathlists
set_rel_pathlist
SetRelationPath
create_index_paths
其中create_index_paths
函数是入口函数,该函数实现了根据元数据为表生成索引路径的功能。
2. create_index_paths
下面对create_index_paths函数的源码进行分析,分析一下是如何根据约束条件选择索引的。
void create_index_paths(PlannerInfo* root, RelOptInfo* rel)
{
...
/* Examine each index in turn */
/*
* rel->indexlist中记录了表上所有的索引。
* 链表是在get_relation_info函数中构造的,
* 根据pg_index的indrelid字段找到表上的索引。
*/
foreach (lc, rel->indexlist) {
IndexOptInfo* index = (IndexOptInfo*)lfirst(lc);
...
/*
* Identify the restriction clauses that can match the index.
*/
/*
* 根据rel中的约束条件(restrictinfo)匹配索引,
* 匹配到的clause和index记录在rclauseset中,
* 后面章节对这个函数展开说明。
*/
match_restriction_clauses_to_index(rel, index, &rclauseset);
/*
* Build index paths from the restriction clauses. These will be
* non-parameterized paths. Plain paths go directly to add_path(),
* bitmap paths are added to bitindexpaths to be handled below.
*/
/* 根据之前流程选取到的索引来生成索引路径 */
get_index_paths(root, rel, index, &rclauseset, &bitindexpaths);
...
/*
* 后面还有很多流程,比如生成参数化索引扫描路径、
* 生成bitmap index scan扫描路径等等,
* 这里就先不展开说明了,主要介绍一下上面两个函数。
*/
}
...
}
2.1 match_clause_to_indexcol
经过层层调用和有效性检查,最后实际实现主要功能的函数是match_clause_to_indexcol
。简单来说,这个函数负责找到和索引相匹配的约束条件,并将该约束条件记录到indexclauses里。源码和解析如下:
/*
* 'index' is the index of interest.
* 'indexcol' is a column number of 'index' (counting from 0).
* indexcol对应索引中的列号
* 'rinfo' is the clause to be tested (as a RestrictInfo node).
*/
static void match_clause_to_indexcol(IndexOptInfo* index, RestrictInfo* rinfo, IndexClauseSet* clauseset)
{
...
/*
* 篇幅原因,这里以最常见的操作符为例来说明代码,
* 需要获取到操作符左右两端的relids(说明了涉及到哪些表)
* 如果是一元操作符,那么就不涉及索引的选择。
*/
if (is_opclause(clause)) {
leftop = get_leftop(clause);
rightop = get_rightop(clause);
if (leftop == NULL || rightop == NULL)
return false;
left_relids = rinfo->left_relids;
right_relids = rinfo->right_relids;
expr_op = ((OpExpr*)clause)->opno;
expr_coll = ((OpExpr*)clause)->inputcollid;
plain_op = true;
} ...
/*
* Check for clauses of the form: (indexkey operator constant) or
* (constant operator indexkey). See above notes about const-ness.
*/
/*
* 根据表达式和indexcol来检查表达式是否和索引列匹配,
* 比如create table t1(a int, b int);
* create index id1 on t1(b);
* select * from t1 where b=1;
*
* 那么此时left_relids就是1,说明'='操作符左侧只涉及t1表,
* 且leftop为Var, varno=1说明是第一个rte, varattno=2
* 说明是表的第二列,也就是b列。
* right_relids为0,说明'='操作符右边不涉及表。
*
* 此时,match_index_to_operand函数负责检查index的indexcol列
* 是否和leftop一致,数据结构如上所说,满足检查条件。
*
* right_relids为0说明格式满足 indexkey operator constant。
*
* contain_volatile_functions约束则是因为索引扫描的条件不会每次都计算,
* 而volatile函数每次返回的值可能都不同,如果可以为volatile类型函数,
* 则可能出现索引扫描和全表扫描结果不一致的情况。
*/
if (match_index_to_operand(leftop, indexcol, index, true) && !bms_is_member(index_relid, right_relids) &&
!contain_volatile_functions(rightop)) {
if (IndexCollMatchesExprColl(idxcollation, expr_coll) && is_indexable_operator(expr_op, opfamily, true))
return true;
/*
* If we didn't find a member of the index's opfamily, see whether it
* is a "special" indexable operator.
*/
/*
* match_special_index_operator检查是否满足特殊的操作符,
* 比如LIKE/ICLIKE/正则表达式等。
*/
if (plain_op && match_special_index_operator(clause, opfamily, idxcollation, true))
return true;
return false;
}
...
}
2.2 build_index_paths
get_index_paths内的主要函数为build_index_paths,函数主要功能是根据之前匹配到的过滤条件来生成实际的索引路径。
static List* build_index_paths(PlannerInfo* root, RelOptInfo* rel, IndexOptInfo* index, IndexClauseSet* clauses,
bool useful_predicate, SaOpControl saop_control, ScanTypeControl scantype)
{
...
/*
* 1. 收集索引对应的过滤条件,这部分条件作为Index Cond,在后续搜索btree等
* 索引数据结构时作为索引条件。其他条件则作为Filter,当一条索引记录满足条件时
* 还需要检查是否满足Filter。
*/
for (indexcol = 0; indexcol < index->nkeycolumns; indexcol++) {
ListCell* lc = NULL;
foreach (lc, clauses->indexclauses[indexcol]) {
RestrictInfo* rinfo = (RestrictInfo*)lfirst(lc);
if (IsA(rinfo->clause, ScalarArrayOpExpr)) {
/* Ignore if not supported by index */
if (saop_control == SAOP_PER_AM && !index->amsearcharray)
continue;
found_clause = true;
if (indexcol > 0)
found_lower_saop_clause = true;
} else {
if (saop_control != SAOP_REQUIRE)
found_clause = true;
}
index_clauses = lappend(index_clauses, rinfo);
clause_columns = lappend_int(clause_columns, indexcol);
outer_relids = bms_add_members(outer_relids, rinfo->clause_relids);
}
/*
* If no clauses match the first index column, check for amoptionalkey
* restriction. We can't generate a scan over an index with
* amoptionalkey = false unless there's at least one index clause.
* (When working on columns after the first, this test cannot fail. It
* is always okay for columns after the first to not have any
* clauses.)
*/
if (index_clauses == NIL && !index->amoptionalkey)
return NIL;
}
/*
* 2. 判断索引pathkey的有序性是否和query中的排序操作匹配,
* 这部分在后续分析sort算子的时候再进一步分析。
*/
...
/*
* 3. 检查是否可以生成IndexOnlyScan路径
* 主要逻辑是为索引涉及的列和过滤条件涉及的列分别生成一个bitmapset
* 然后比较过滤条件对应的bitmapset是否是索引涉及的列的bitmapset的子集
*/
index_only_scan = (scantype != ST_BITMAPSCAN && check_index_only(root, rel, index));
/*
* 4. 生成IndexPath
*/
if (found_clause || useful_pathkeys != NIL || useful_predicate || index_only_scan) {
if ((relHasbkt && !index->crossbucket)) {
useful_pathkeys = NIL;
}
ipath = create_index_path(root,
index,
index_clauses, // IndexCond,后续生成计划和Filter时需要用到
clause_columns,
orderbyclauses,
orderbyclausecols,
useful_pathkeys,
index_is_ordered ? ForwardScanDirection : NoMovementScanDirection,
index_only_scan,
outer_relids,
upper_params,
loop_count);
result = lappend(result, ipath);
}
...
}
2.3 create_indexscan_plan
生成IndexScan计划
static Scan* create_indexscan_plan(
PlannerInfo* root, IndexPath* best_path, List* tlist, List* scan_clauses, bool indexonly)
{
...
qpqual = NIL;
foreach (l, scan_clauses) {
RestrictInfo* rinfo = (RestrictInfo*)lfirst(l);
Assert(IsA(rinfo, RestrictInfo));
if (rinfo->pseudoconstant)
continue; /* we may drop pseudoconstants here */
opquals = lappend(opquals, rinfo->clause);
/* 约束条件已经出现在IndexCond里,不需要重复添加到Filter */
if (list_member_ptr(indexquals, rinfo))
continue; /* simple duplicate */
/* 前缀索引,索引只包含部分数据,需要添加到filter保证条件完整的被计算 */
if (clause_relate_to_prefixkey(rinfo, best_path->indexinfo, prefixkeys)) {
qpqual = lappend(qpqual, rinfo);
continue;
}
if (is_redundant_derived_clause(rinfo, indexquals))
continue; /* derived from same EquivalenceClass */
if (!contain_mutable_functions((Node*)rinfo->clause)) {
List* clausel = list_make1(rinfo->clause);
/* 约束条件可以从IndexCond中推导出来,那也没必要加到filter中。 */
if (predicate_implied_by(clausel, indexquals))
continue; /* provably implied by indexquals */
if (best_path->indexinfo->indpred) {
if (baserelid != (unsigned int)linitial2_int(root->parse->resultRelations) &&
get_parse_rowmark(root->parse, baserelid) == NULL)
if (predicate_implied_by(clausel, best_path->indexinfo->indpred))
continue; /* implied by index predicate */
}
}
/* 添加到qpqual中,该变量就是最后EXPLAIN计划中IndexScan算子的filter */
qpqual = lappend(qpqual, rinfo);
}
...
}
3 代价模型
cost_index函数内对IndexScan的代价进行了估算,简单来说主要受索引类型和数据量影响,具体估算模型如下:
startup_cost = indexStartupCost + max_IO_cost + csquared * (min_IO_cost - max_IO_cost)
run_cost = indexRunCost + cpu_per_tuple * tuples_fetched
indexStartupCost: 索引的初始代价,和索引类型有关,比如btree类型可以分析btcostestimate_internal函数。
max_IO_cost + csquared * (min_IO_cost - max_IO_cost) :这部分则是计算索引IO的代价,和索引页面以及是否回表有关。具体逻辑可以进一步分析cost_index函数。
indexRunCost:索引的执行代价,也和索引类型以及数据量有关,和indexStartupCost在同一个函数内计算。
总结
索引扫描的逻辑相比于顺序扫描要复杂很多,优化器部分选择路径和计算代价也更为精细,篇幅原因本博客只对主要流程做了说明。