diff --git a/content/zh/post/xtuRemi/.keep b/content/zh/post/xtuRemi/.keep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git "a/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\345\265\214\345\245\227\345\276\252\347\216\257\350\267\257\345\276\204\346\210\220\346\234\254\347\257\207\357\274\210\345\210\235\346\255\245\344\274\260\350\256\241\357\274\211\345\211\215\350\250\200.md" "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\345\265\214\345\245\227\345\276\252\347\216\257\350\267\257\345\276\204\346\210\220\346\234\254\347\257\207\357\274\210\345\210\235\346\255\245\344\274\260\350\256\241\357\274\211\345\211\215\350\250\200.md" new file mode 100644 index 0000000000000000000000000000000000000000..0e6c422994639aee4f1de8951162158568e459bd --- /dev/null +++ "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\345\265\214\345\245\227\345\276\252\347\216\257\350\267\257\345\276\204\346\210\220\346\234\254\347\257\207\357\274\210\345\210\235\346\255\245\344\274\260\350\256\241\357\274\211\345\211\215\350\250\200.md" @@ -0,0 +1,110 @@ +# SQL引擎之查询优化——costsize.cpp嵌套循环路径成本篇(初步估计)前言 + +本篇依旧通过函数解读与代码标注方式介绍三大经典表连接方式之一nestloop join路径成本初步计算,下一篇将介绍嵌套循环连接路径的成本和结果大小的最终估计。 + +## Nested Loop Join(NLJ) +NLJ是通过两层循环,用第一张表做Outter Loop,第二张表做Inner Loop,Outter Loop的每一条记录跟Inner Loop的记录作比较,符合条件的就输出。 +![NLJ](/api/attachments/370021 "NLJ") + +## 函数解读 +`void initial_cost_nestloop(PlannerInfo* root, JoinCostWorkspace* workspace, JoinType jointype, Path* outer_path,Path* inner_path, SpecialJoinInfo* sjinfo, SemiAntiJoinFactors* semifactors, int dop);` + **作用:进行嵌套循环连接路径成本的初步估计。(或者说是数据处理,做基础工作)** + **参数:** + * 'workspace',将被填入启动成本、总成本,以及可能的其他数据,供final_cost_nestloop(下一篇nestloop最终计算)使用; + * 'jointype',是要执行的连接的类型; + * 'outer_path',是连接的外部输入; + * 'inner_path',是连接的内部输入; + * 'sjinfo',是关于连接的额外信息,用于选择性估计; + * 'semifactors',包含有效的数据,如果连接类型是SEMI或ANTI。 + +## 小结 +在路径成本进行处理时,从外层到内层进行计算,先进行初步处理,将处理数据传入下一步final_cost_nestloop。 + + +## 代码标注 +```cpp +void initial_cost_nestloop(PlannerInfo* root, JoinCostWorkspace* workspace, JoinType jointype, Path* outer_path, + Path* inner_path, SpecialJoinInfo* sjinfo, SemiAntiJoinFactors* semifactors, int dop) +{ + /* 参数化(完全可以跟着ID来理解) */ + Cost startup_cost = 0; + Cost run_cost = 0; + /* 外连接行数计算 */ + double outer_path_rows = PATH_LOCAL_ROWS(outer_path) / dop; + Cost inner_rescan_start_cost; + Cost inner_rescan_total_cost; + Cost inner_run_cost; + Cost inner_rescan_run_cost; + errno_t rc = 0; + /* + * 拷贝函数memset_s,在这里将0,转为无符号字符后的值,拷贝到OpMemInfo中的前几个字符, + * 返回值是EOK,表示没有违反运行时间限制,其默认值是0。 + */ + rc = memset_s(&workspace->inner_mem_info, sizeof(OpMemInfo), 0, sizeof(OpMemInfo)); + /* 根据日志与返回值进行安全性检测 */ + securec_check(rc, "\0", "\0"); + + /* 重新扫描估计内部关系的成本 */ + cost_rescan(root, inner_path, &inner_rescan_start_cost, &inner_rescan_total_cost, &workspace->inner_mem_info); + + /* 源数据的成本计算 */ + /* 内外层的启动成本 */ + startup_cost += outer_path->startup_cost + inner_path->startup_cost; + /* 真正运行成本,剔除启动成本 */ + run_cost += outer_path->total_cost - outer_path->startup_cost; + /* 支付所有内部路径重新扫描的启动成本 */ + if (outer_path_rows > 1) + run_cost += (outer_path_rows - 1) * inner_rescan_start_cost; + /* 内部路径运行成本与重新扫描的运行成本计算 */ + inner_run_cost = inner_path->total_cost - inner_path->startup_cost; + inner_rescan_run_cost = inner_rescan_total_cost - inner_rescan_start_cost; + + if (jointype == JOIN_SEMI || jointype == JOIN_ANTI) { + double outer_matched_rows; + Selectivity inner_scan_frac; + + /* + * SEMI or ANTI join: 执行者将在第一次匹配后停止。 + */ + run_cost += inner_run_cost; + /* 匹配整数规定化 */ + outer_matched_rows = rint(outer_path_rows * semifactors->outer_match_frac); + /* 对于至少有一个匹配项的外侧行,如果匹配项是均匀分布的, + * 我们可以期望内侧扫描在内侧行的一小部分1/(match_count+1)后停止。 + * 由于它们可能不是完全均匀分布的,我们对这个分数应用2.0的模糊系数 */ + inner_scan_frac = 2.0 / (semifactors->match_count + 1.0); + + /* 为有匹配的额外外部图元增加内部运行成本 */ + if (outer_matched_rows > 1) + run_cost += (outer_matched_rows - 1) * inner_rescan_run_cost * inner_scan_frac; + + /* + * 处理不匹配的行的成本取决于以下因素的细节而不同,将被保存到workspace,为final_cost_nestloop保存数据。 + */ + workspace->outer_matched_rows = outer_matched_rows; + workspace->inner_scan_frac = inner_scan_frac; + workspace->inner_mem_info.regressCost *= Max(outer_matched_rows, 1.0); + } else { + /* 正常情况下,对每一外行扫描整个输入rel,包括内部运行成本 */ + run_cost += inner_run_cost; + if (outer_path_rows > 1) + run_cost += (outer_path_rows - 1) * inner_rescan_run_cost; + /* 调整workspace内部存储信息 */ + workspace->inner_mem_info.regressCost *= Max(outer_path_rows, 1.0); + } + + /* CPU费用数据保留 */ + /* 计算结果保留至公共结果字段 */ + workspace->startup_cost = startup_cost; + workspace->total_cost = startup_cost + run_cost; + /* 为final_cost_nestloop保存私有数据 */ + workspace->run_cost = run_cost; + workspace->inner_rescan_run_cost = inner_rescan_run_cost; + /* 异常抛出 */ + ereport(DEBUG1, + (errmodule(MOD_OPT_JOIN), + errmsg("Initial nestloop cost: startup_cost: %lf, total_cost: %lf", + workspace->startup_cost, + workspace->total_cost))); +} +``` \ No newline at end of file diff --git "a/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\345\265\214\345\245\227\345\276\252\347\216\257\350\267\257\345\276\204\346\210\220\346\234\254\347\257\207\357\274\210\346\234\200\347\273\210\344\274\260\350\256\241\357\274\211.md" "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\345\265\214\345\245\227\345\276\252\347\216\257\350\267\257\345\276\204\346\210\220\346\234\254\347\257\207\357\274\210\346\234\200\347\273\210\344\274\260\350\256\241\357\274\211.md" new file mode 100644 index 0000000000000000000000000000000000000000..c36388c71c307a08884e52d1a25a9bc3df345838 --- /dev/null +++ "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\345\265\214\345\245\227\345\276\252\347\216\257\350\267\257\345\276\204\346\210\220\346\234\254\347\257\207\357\274\210\346\234\200\347\273\210\344\274\260\350\256\241\357\274\211.md" @@ -0,0 +1,108 @@ +# SQL引擎之查询优化——costsize.cpp嵌套循环路径成本篇(最终估计) + +## 前言 +本篇将对nestloop的计算做最后的解读。(要完结撒花辽) + +## 函数解读 +`void final_cost_nestloop(PlannerInfo* root, NestPath* path, JoinCostWorkspace* workspace, SpecialJoinInfo* sjinfo,SemiAntiJoinFactors* semifactors, bool hasalternative, int dop);` + **作用:**嵌套循环连接路径的成本和结果大小的最终估计。 + **参数续介绍:** + * 'path'中数据基本上已存在(从initial_cost_nestloop传入),只是除开rows and cost 字段还待计算。 + * 'workspace'是initial_cost_nestloop的结果。 + * 其余与initial_cost_nestloop意义相同,场景不同。 + +## 方法 +final_cost_nestloop对成本的计算主体思路是将成本分为匹配与不匹配的成本,匹配的成本在初步处理已经给出,对于不匹配的成本还考虑了重叠有交集的情况,当然还是期望不重叠,后续还对未使用的方法的成本进行估计,这个成本(启动成本)理解下来应该是很小的,但是没有去掉,估计值会接近一些,后面对不匹配的成本进行总体计算。 +匹配的运行成本会进行总体情况占比处理(即占据整体比例 num/inner_path_rows),但未匹配的运行成本不会纳入,只对整体图元进行计算。最后匹配的成本加上未匹配的初始成本(不称启动成本,因为在整体上是忽略不计的)加上CPU的成本加上由于EC函数所需的多重joinpath的stream成本,因为流被调用但不能在此路径中并行的行动的成本,最后输出嵌套循环路径成本——stream cost,startup cost, total cost。 + +*** +## 代码标注 +```cpp +void final_cost_nestloop(PlannerInfo* root, NestPath* path, JoinCostWorkspace* workspace, SpecialJoinInfo* sjinfo, + SemiAntiJoinFactors* semifactors, bool hasalternative, int dop) +{ + /* + * 成本计算做准备,主要包括外部成本与内部成本, + * 每个又包括启动与运行成本。 + */ + Path* outer_path = path->outerjoinpath; + Path* inner_path = path->innerjoinpath; + double outer_path_rows = PATH_LOCAL_ROWS(outer_path) / dop; + double inner_path_rows = PATH_LOCAL_ROWS(inner_path) / dop; + Cost startup_cost = workspace->startup_cost; + Cost run_cost = workspace->run_cost; + Cost inner_rescan_run_cost = workspace->inner_rescan_run_cost; + Cost cpu_per_tuple = 0.0; + QualCost restrict_qual_cost; + double ntuples; + bool ppi_used = false; + bool method_enabled = true; + + /* 从上层(set_path_rows)传递下来的路径和相关的行,用以正确的行估计值标记路径 */ + set_rel_path_rows(&path->path, path->path.parent, path->path.param_info); + + /* + * 当内部路径或外部路径中含有EC函数时,为连接路径设置多个EC函数,而不是通过StreamPath进行跟踪, + * 如果inner_path或outer_path是EC的functioinScan,没有following 。 + * 这这里为其设置多重joinpath。 + */ + set_joinpath_multiple_for_EC(root, &path->path, outer_path, inner_path); + + if (inner_path->param_info != NULL && outer_path->parent != NULL) + /* 检测重叠,交集重复计算 */ + ppi_used = bms_overlap(inner_path->param_info->ppi_req_outer, outer_path->parent->relids); + /* 如果使用ppi(有重叠),系统使用enable_index_nestloop来判断是否使用这个路径,难题,循环重叠 */ + method_enabled = ppi_used ? u_sess->attr.attr_sql.enable_index_nestloop + : (u_sess->attr.attr_sql.enable_nestloop || !hasalternative); + if (!method_enabled) + /* 相当于另类去简化连接的禁用情况(disable_cost) */ + startup_cost += g_instance.cost_cxt.disable_cost; + + /*源数据的成本 */ + if (path->jointype == JOIN_SEMI || path->jointype == JOIN_ANTI) { + /* 上一步保留下来的匹配成本 */ + double outer_matched_rows = workspace->outer_matched_rows; + Selectivity inner_scan_frac = workspace->inner_scan_frac; + + /* 计算所处理的图元数量(而不是排放的数量!),外部与内部是两者,交叉相乘 */ + ntuples = outer_matched_rows * inner_path_rows * inner_scan_frac; + + /* 对不匹配的外链行 */ + if (has_indexed_join_quals(path)) { + run_cost += (outer_path_rows - outer_matched_rows) * inner_rescan_run_cost / inner_path_rows; + /* 不对不匹配的进行总体评估,但还是计算总体情况 */ + } else { + run_cost += (outer_path_rows - outer_matched_rows) * inner_rescan_run_cost; + ntuples += (outer_path_rows - outer_matched_rows) * inner_path_rows; + } + } else { + /* 计算处理的图元数量,不匹配的 */ + ntuples = outer_path_rows * inner_path_rows; + } + + /* CPU成本:cost_qual_eval处理,加上CPU启动、运行成本 */ + cost_qual_eval(&restrict_qual_cost, path->joinrestrictinfo, root); + startup_cost += restrict_qual_cost.startup; + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost + restrict_qual_cost.per_tuple; + run_cost += cpu_per_tuple * ntuples; + + path->path.startup_cost = startup_cost; + path->path.total_cost = startup_cost + run_cost; + path->path.stream_cost = outer_path->stream_cost; + /* 未启用方法计算 */ + if (!method_enabled) + path->path.total_cost *= g_instance.cost_cxt.disable_cost_enlarge_factor; + + /* openGuass存储方式转为了字节的方式 */ + if (path->innerjoinpath->pathtype == T_Material) + copy_mem_info(&((MaterialPath*)path->innerjoinpath)->mem_info, &workspace->inner_mem_info); + + /* 结果显示 */ + ereport(DEBUG2, + (errmodule(MOD_OPT_JOIN), + errmsg("final cost nest loop: stream_cost: %lf, startup_cost: %lf, total_cost: %lf", + path->path.stream_cost, + path->path.startup_cost, + path->path.total_cost))); +} +``` \ No newline at end of file diff --git "a/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\216\222\345\272\217\346\210\220\346\234\254\347\257\207.md" "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\216\222\345\272\217\346\210\220\346\234\254\347\257\207.md" new file mode 100644 index 0000000000000000000000000000000000000000..59376e4d61653585bcd1e4b6d7a241220d61507c --- /dev/null +++ "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\216\222\345\272\217\346\210\220\346\234\254\347\257\207.md" @@ -0,0 +1,148 @@ +# SQL引擎之查询优化——costsize.cpp排序成本篇 + +## 前言 +本篇对递归联合成本进行计算并估计输出大小,并分析了对一个关系进行排序的成本,同时该成本包括了输入数据的成本(普遍是不考虑的)。 + +## cost_sort +**作用:**确定并返回对一个关系进行排序的成本,包括读取输入数据的成本。 +**sort_nums** +* 如果需要排序的数据总量小于sort_mem,我们将进行内存排序,这不需要I/O和对t个图元进行约t*log2(t)个图元的比较。 + 如果总容量超过sort_mem,我们就切换到磁带式合并算法。 总共仍然会有大约t*log2(t)个元组的比较,但是我们还需要在每个合并过程中对每个元组进行一次写入和读取。 我们预计会有大约ceil(logM(r))次合并(取≥logM(r)最小整数),其中r是初始运行的数量,M是tuplesort.c使用的合并顺序。 + +**磁带式合并算法(tape-style merge algorithm)** + * disk traffic(磁盘流量) = 2 * relsize * ceil(logM(p / (2*sort_mem)) + * cpu = comparison_cost * t * log2(t) + * 磁盘流量被假定为3/4的顺序和1/4的随机访问,随机更适合小样本,这点在前篇成本计算就已经提出,假如这个数据是根据大数据分析后得出的效率更高,是合情合理的... + +**堆快排方法:**如果排序是有界的(即只需要前k个结果图元),并且k个图元可以放入sort_mem中,可以使用一个堆方法,在堆中只保留k个图元;这将需要大约t*log2(k)图元比较,也既是堆快排时间复杂度。 + +**额外成本(理应成本):**默认情况下,系统对每个元组的比较收取两个运算符的evals,这在大多数情况下应该是正确的。调用者可以通过指定非零的comparison_cost来调整这一点;一般来说,这是为任何额外的额外的工作,以便为比较运算符的输入做准备。 + + +## 代码标注 +```cpp +/* + * cost_recursive_union + * 确定并返回执行递归联合的成本,以及估计的输出大小。 + * + * 已经给出了非递归和递归条款的计划。 + * + * 请注意,参数和输出都是Plans,而不是本模块其他部分的Paths。 这是因为我们不需要为递归联合设置Path表示法---我们只有一种方法可以做到。 + */ + + void cost_recursive_union(Plan* runion, Plan* nrterm, Plan* rterm) +{ + Cost startup_cost; + Cost total_cost; + double total_rows; + double total_global_rows; + + /* 对非递归项估计 */ + startup_cost = nrterm->startup_cost; + total_cost = nrterm->total_cost; + total_rows = PLAN_LOCAL_ROWS(nrterm); + total_global_rows = nrterm->plan_rows; + + /* + * 任意假设需要10次递归迭代,并且已经设法很好地解决了其中每一次的成本和输出大小。 这些假设很不可靠,但很难看出如何做得更好。 + */ + total_cost += 10 * rterm->total_cost; + total_rows += 10 * PLAN_LOCAL_ROWS(rterm); + total_global_rows += 10 * rterm->plan_rows; + + /* + *还对每行收取cpu_tuple_cost,以说明操作tuplestores的成本。 + */ + total_cost += u_sess->attr.attr_sql.cpu_tuple_cost * total_rows; + + runion->startup_cost = startup_cost; + runion->total_cost = total_cost; + set_plan_rows(runion, total_global_rows, nrterm->multiple); + runion->plan_width = Max(nrterm->plan_width, rterm->plan_width); +} + +/* + * cost_sort + * 确定并返回对一个关系进行排序的成本,包括读取输入数据的成本. + */ + void cost_sort(Path* path, List* pathkeys, Cost input_cost, double tuples, int width, Cost comparison_cost, + int sort_mem, double limit_tuples, bool col_store, int dop, OpMemInfo* mem_info, bool index_sort) +{ + Cost startup_cost = input_cost; + Cost run_cost = 0; + double input_bytes = relation_byte_size(tuples, width, col_store, true, true, index_sort) / SET_DOP(dop); + double output_bytes; + double output_tuples; + long sort_mem_bytes = sort_mem * 1024L / SET_DOP(dop); + + dop = SET_DOP(dop); + + if (!u_sess->attr.attr_sql.enable_sort) + startup_cost += g_instance.cost_cxt.disable_cost; + + /* + * 要确保排序的成本永远不会被估计为零,即使传入的元组数量为零。 此外,不能做log(0)... + */ + if (tuples < 2.0) { + tuples = 2.0; + } + + /* 包括默认的每次比较的成本 */ + comparison_cost += 2.0 * u_sess->attr.attr_sql.cpu_operator_cost; + + if (limit_tuples > 0 && limit_tuples < tuples) { + output_tuples = limit_tuples; + output_bytes = relation_byte_size(output_tuples, width, col_store, true, true, index_sort); + } else { + output_tuples = tuples; + output_bytes = input_bytes; + } + + if (output_bytes > sort_mem_bytes) { + /* + * CPU成本 + * + * 假设约有N个对数N的比较 + */ + startup_cost += comparison_cost * tuples * LOG2(tuples); + + /* 磁盘成本 */ + startup_cost += compute_sort_disk_cost(input_bytes, sort_mem_bytes); + } else { + if (tuples > 2 * output_tuples || input_bytes > sort_mem_bytes) { + /* + * 使用有界堆排序,在内存中只保留K个图元,图元比较的总数为N log2 K;但常数因素比quicksort要高一些。 对它进行调整,使成本曲线在交叉点上是连续的。 + */ + startup_cost += comparison_cost * tuples * LOG2(2.0 * output_tuples); + } else { + /* 对所有的输入图元使用普通的quicksort */ + startup_cost += comparison_cost * tuples * LOG2(tuples); + } + } + + if (mem_info != NULL) { + mem_info->opMem = u_sess->opt_cxt.op_work_mem; + mem_info->maxMem = output_bytes / 1024L * dop; + mem_info->minMem = mem_info->maxMem / SORT_MAX_DISK_SIZE; + mem_info->regressCost = compute_sort_disk_cost(input_bytes, mem_info->minMem); + /* 特殊情况,如果阵列大于1G,所以系统必须溢出到磁盘 */ + if (output_tuples > (MaxAllocSize / TUPLE_OVERHEAD(true) * dop)) { + mem_info->maxMem = STATEMENT_MIN_MEM * 1024L * dop; + mem_info->minMem = Min(mem_info->maxMem, mem_info->minMem); + } + } + + /* + *还对每个提取的元组收取少量费用(任意设置为等于操作者成本)。 系统不收取cpu_tuple_cost,因为排序节点不做质量检查或投影,所以它的开销比大多数计划节点要少。 注意在这里使用tuples而不是output_tuples是正确的------否则的话,LIMIT的上限会按比例计算运行成本,所以会重复计算LIMIT。 + */ + run_cost += u_sess->attr.attr_sql.cpu_operator_cost * tuples; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; + + if (!u_sess->attr.attr_sql.enable_sort) + path->total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); +} +``` \ No newline at end of file diff --git "a/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\272\242\345\207\272\346\210\220\346\234\254\344\270\216\346\236\201\351\231\220\346\210\220\346\234\254\347\257\207\357\274\210\344\270\212\357\274\211\345\211\215\350\250\200.md" "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\272\242\345\207\272\346\210\220\346\234\254\344\270\216\346\236\201\351\231\220\346\210\220\346\234\254\347\257\207\357\274\210\344\270\212\357\274\211\345\211\215\350\250\200.md" new file mode 100644 index 0000000000000000000000000000000000000000..0d2bc1983447e820050d3a1d2bc02bd29be7ec86 --- /dev/null +++ "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\272\242\345\207\272\346\210\220\346\234\254\344\270\216\346\236\201\351\231\220\346\210\220\346\234\254\347\257\207\357\274\210\344\270\212\357\274\211\345\211\215\350\250\200.md" @@ -0,0 +1,60 @@ +# SQL引擎之查询优化——costsize.cpp溢出成本与极限成本篇(上)前言 + +本篇介绍排序磁盘溢出成本,极限成本相关调整。 + +## 函数解读 +1.`double compute_sort_disk_cost(double input_bytes, double sort_mem_bytes)` + **作用:**计算sorter磁盘溢出成本。 + **参数:**input_bytes,是输入关系的字节数;sort_mem_bytes,是排序运算器的工作内存。 + **RETURN:**估计的磁盘成本 + **提问:**既然输入的是一个确定的数(double),为什么不能得到一个确切的成本,而需要估计? + 总页成本:`npageaccesses = 2.0 * npages * log_runs;`之所以需要乘以2,因为在单位内存下取的是1/2,但这单位内存又被log计算,而且本身总页数值偏大,所以这个总页成本是偏大的。 + 为什么是一个估计值,上面这段话已经给出部分答案,剩下答案就在于,前面一篇文章**SQL引擎之查询优化——costsize.cpp逻辑成本篇**中提到过,设定有1/4的数目是随机访问的,也即是小成本计算,其余以顺序访问进行计算,详情请转至逻辑成本篇,随机访问的计算本身带有估计值,整体来讲,磁盘成本也是一个估计值,得不到确切值。 + +2.`double adjust_limit_row_count(double lefttree_rows);` + **作用:**调整限制行数。 + **目的:**以防止lefttree_rows < default_limit_rows而导致计算xxx成本出错(跳过该子计划的成本计算) + (详情,代码标注已经解释很清楚) + +## 代码标注 +```cpp +double compute_sort_disk_cost(double input_bytes, double sort_mem_bytes) +{ + /* + *我们将不得不使用基于磁盘的排序来处理所有图元 + */ + /* 通过输入字节大小与运行内存,计算总页数 */ + double npages = ceil(input_bytes / BLCKSZ); + /* 单位字节下排序内存成本 */ + double nruns = (input_bytes / sort_mem_bytes) * 0.5; + /* 通过tuplesort_merge_order得到该输入内存下合并中输入磁带的数量 */ + double mergeorder = tuplesort_merge_order(sort_mem_bytes); + double log_runs; + double npageaccesses; + + /* 计算logM(r)为log(r) / log(M) ,这里计算的是单位页成本*/ + if (nruns > mergeorder) { + log_runs = ceil(log(nruns) / log(mergeorder)); + } else { + log_runs = 1.0; + } + npageaccesses = 2.0 * npages * log_runs; + /* 假设3/4的访问是连续的,1/4的访问不是,换而言之,有3/4的是顺序访问,1/4的是随机访问,前面对计算sort篇已经提到过*/ + return npageaccesses * (u_sess->attr.attr_sql.seq_page_cost * 0.75 + u_sess->attr.attr_sql.random_page_cost * 0.25); +} + +/* 调整限制行数,目的:以防止lefttree_rows < default_limit_rows而导致计算出错(跳过该子计划)*/ +double adjust_limit_row_count(double lefttree_rows) +{ + /* 默认行数不合理的情况下,使用子计划行数与默认行数进行百分比调整 */ + if (u_sess->attr.attr_sql.default_limit_rows < 0) { + return -(lefttree_rows * u_sess->attr.attr_sql.default_limit_rows / 100); + } else { + /* 默认行数合理的情况下,直接使用默认限制行数和子计划行数的最小值进行调整, + * 以防止lefttree_rows < default_limit_rows而导致计算出错(跳过该子计划) + */ + return Min(u_sess->attr.attr_sql.default_limit_rows, lefttree_rows); + } +} +``` +下一篇将对极限成本的计算进行解读~ \ No newline at end of file diff --git "a/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\272\242\345\207\272\346\210\220\346\234\254\344\270\216\346\236\201\351\231\220\346\210\220\346\234\254\347\257\207\357\274\210\344\270\213\357\274\211\345\211\215\350\250\200.md" "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\272\242\345\207\272\346\210\220\346\234\254\344\270\216\346\236\201\351\231\220\346\210\220\346\234\254\347\257\207\357\274\210\344\270\213\357\274\211\345\211\215\350\250\200.md" new file mode 100644 index 0000000000000000000000000000000000000000..d72d47efe15957410e277809c630d914838cf6d5 --- /dev/null +++ "b/content/zh/post/xtuRemi/SQL\345\274\225\346\223\216\344\271\213\346\237\245\350\257\242\344\274\230\345\214\226\342\200\224\342\200\224costsize.cpp\346\272\242\345\207\272\346\210\220\346\234\254\344\270\216\346\236\201\351\231\220\346\210\220\346\234\254\347\257\207\357\274\210\344\270\213\357\274\211\345\211\215\350\250\200.md" @@ -0,0 +1,82 @@ +# SQL引擎之查询优化——costsize.cpp溢出成本与极限成本篇(下)前言 + +本篇将对极限成本的计算进行详细解读。 + +## 函数解读 +`void cost_limit(Plan* plan, Plan* lefttree, int64 offset_est, int64 count_est)` + **作用:**计算极限成本并调整极限节点的输出行数。 + **参数:** + plan,极限(限制)计划; + lefttree,子计划; + offset_est,极限偏移值; + count_est,极限计数值(总值)。 + **如何来调整极限节点的输出与成本?** + 思路很简单,首先剔除偏移值,更新计数量后以计数量进行极限成本的计算,重点是在整个计算流程中都对数值进行合理化“调整”(属实是调整数,毕竟行计算使用int更符合实际)。 + 值得一提的是,系统保证计算总行数不会为零的方法是在每一次调整完毕后避免总行数归零化,这样子可以解锁为什么以前解读公式时,可以大胆使用plan_rows做分母或者做对数运算。 + `if (plan->plan_rows < 1) + plan->plan_rows = 1;` + +## 流程图 +![调整与计算流程](/api/attachments/369993 "调整与计算流程") + +## 代码标注 +```cpp +void cost_limit(Plan* plan, Plan* lefttree, int64 offset_est, int64 count_est) +{ + /* 处理偏移量 */ + if (offset_est != 0) { + double offset_rows; + + if (offset_est > 0) { + offset_rows = (double)offset_est; + /* 子计划是被复制的计划而且是在数据节点上执行的情况 */ + if (is_replicated_plan(lefttree) && is_execute_on_datanodes(lefttree)) { + /* 整体偏移值:所有子计划的数据节点的数量(每一个时间节点都是一个子计划)*/ + offset_rows *= ng_get_dest_num_data_nodes(lefttree); + } + } else + /* 将子计划行数估计值强制为一个系统认为合理的值。*/ + offset_rows = clamp_row_est(lefttree->plan_rows * 0.10); + /* 子计划边界限制调整子计划 */ + if (offset_rows > lefttree->plan_rows) + offset_rows = lefttree->plan_rows; + if (plan->plan_rows > 0) + /* 不难理解,计划的启动成本将要包含偏移成本的最终成本, + * 使用偏移量/极限量做偏移比例系数,乘以正式成本, + * 加上原启动成本,就完成了更准确的启动成本计算 + */ + plan->startup_cost += (plan->total_cost - plan->startup_cost) * offset_rows / plan->plan_rows; + /* 真正计划行数:出去偏移行数量 */ + plan->plan_rows -= offset_rows; + /* 合理化,不足一行的补满一行,这就是前面保证除以计划行数时不会为零的原因 */ + if (plan->plan_rows < 1) + plan->plan_rows = 1; + } + /* 对极限成本的计算以及输出行数的调整计算*/ + if (count_est != 0) { + double count_rows; + + if (count_est > 0) { + count_rows = (double)count_est; + /* 获取数据节点上的所有数量 */ + if (is_execute_on_datanodes(lefttree)) { + count_rows *= ng_get_dest_num_data_nodes(lefttree); + } + } else + /* 行数估计值转化,调用限制量进行约束 */ + count_rows = clamp_row_est(adjust_limit_row_count(lefttree->plan_rows)); + if (count_rows > plan->plan_rows) + count_rows = plan->plan_rows; + /* 存在计划量(一般情况是一定有的,除法是空计划) */ + if (plan->plan_rows > 0) + /* 极限成本 = 极限启动成本 + 真正计数成本,计数系数采用计数量与总行数的比值,前面已经将偏移值移除 */ + plan->total_cost = + plan->startup_cost + (plan->total_cost - plan->startup_cost) * count_rows / plan->plan_rows; + plan->plan_rows = count_rows; + /* 合理化,同上 */ + if (plan->plan_rows < 1) + plan->plan_rows = 1; + } +} +``` +下面将对基本连接关系嵌套循环连接成本进行解读~ \ No newline at end of file diff --git a/content/zh/post/xtuRemi/clausesel.cpp.md b/content/zh/post/xtuRemi/clausesel.cpp.md new file mode 100644 index 0000000000000000000000000000000000000000..007ccacf8cd9a92e1a5838c897ce4f6c373f1ce4 --- /dev/null +++ b/content/zh/post/xtuRemi/clausesel.cpp.md @@ -0,0 +1,208 @@ +## clausesel.cpp部分拆解 + +**作用:**计算子句选择性 + +**核心思想:**clauselist_selectivity ()—计算一个隐含AND的布尔表达式子句列表的选择性。 + 基本方法是取子句的选择性的乘积。 然而,只有当子句具有独立的概率时,这才是正确的, 而在现实中它们往往不是独立的。 因此如何更加智能的进行处理是关键! + SQL引擎对此唯一的额外智能是识别 "范围查询"。例如 "x > 34 AND x < 42"。 句子被识别为可能的范围查询组件,如果它们是限制性条款,其运算符有 scalarltsel()或scalargtsel()作为其限制性选择估计器。系统将引用同一变量的这种形式的子句配对起来。 这类不可配对的子句只是简单地乘以选择性的乘以正常的方式。 但是当系统找到一个配对时,知道选择性代表了低限和高限的相对位置。所以系统不需要把选择性计算成hisel * losel,可以把它算作hisel + losel - 1(为了形象化。 hisel是范围内低于高限的部分,而 losel是高于低限的部分;所以hisel可以被直接解释为一个0...1的值,但是我们需要在将losel转换为1-losel之前将其解释为一个值。那么可用的范围就是1-losel到hisel。然而,这种计算方式重复排除了空值,所以实际上我们需要 hisel + losel + null_frac - 1)。 + +**优势分析:**较为客观地将子句选择性通过选择估计器进行初步量化处理,更好地处理非独立性子句的选择性,也为了计算子句的选择性而不选择简单的低限与高限的乘积,而是将其作为一个量化范围,并进行重复性检验,有效的降低了选择误差。 + **优化建议:**既然会引用同一变量的形式子句,如果计算选择性,是否可以依据匹配程度,如同一变量总数、子句返回形式等等更加偏向于hisel高限,当然这是匹配程度高的情况,反之匹配程度低可以更加偏向于losel,计算公式可以采用主成分分析或者层次分析之类的数学方法解决。 + +**数据结构简析:** + +``` +/* + * 用于积累clauselist_selectivity中可能的范围查询子句对的信息的数据结构。 + */ +typedef struct RangeQueryClause { + struct RangeQueryClause* next; /* 链接列表中的下一个 */ + Node* var; /* 子句的公共变量 */ + bool have_lobound; /* 找到一个低限条款了吗? */ + bool have_hibound; /* 是否找到了高边界的子句? */ + Selectivity lobound; /* Selectivity of a var > something clause */ + Selectivity hibound; /* Selectivity of a var < something clause */ +} RangeQueryClause; + +/* 从限制信息中的子句获取var数据的上下文 */ +typedef struct { + PlannerInfo* root; /* 计划信息节点 */ + int varRelid; /* varRelid是0或一个范围表索引 */ + RatioType ratiotype; /* 过滤比率或连接比率 */ + SpecialJoinInfo* sjinfo; /* 用于joinrel的特殊连接信息 */ + VariableStatData filter_vardata; /* 用于自我过滤的var数据 */ + VariableStatData semijoin_vardata1; /* 半联接/反联接左边的args的var数据 */ + VariableStatData semijoin_vardata2; /* 用于半/反连接的右方args的var数据 */ +} get_vardata_for_filter_or_semijoin_context; +``` + +**代码标注:** + +``` +Selectivity clauselist_selectivity( + PlannerInfo* root, List* clauses, int varRelid, JoinType jointype, SpecialJoinInfo* sjinfo, bool varratio_cached) +{ + Selectivity s1 = 1.0; + RangeQueryClause* rqlist = NULL; + ListCell* l = NULL; + List* varlist = NIL; + List* clauselist = clauses; + ES_SELECTIVITY* es = NULL; + MemoryContext ExtendedStat = NULL; + MemoryContext oldcontext; + + /* 如果只有一个子句,无需配对,直接进入claim_selectivity()即可。*/ + if (list_length(clauses) == 1) + return clause_selectivity(root, (Node*)linitial(clauses), varRelid, jointype, sjinfo, varratio_cached); + + /* 初始化es_selectivity类,当被set_baserel_size_estimates调用时,list_length(clauses)可为0 */ + if (list_length(clauses) >= 2 && + (jointype == JOIN_INNER || jointype == JOIN_FULL || jointype == JOIN_LEFT || jointype == JOIN_ANTI || + jointype == JOIN_SEMI || jointype == JOIN_LEFT_ANTI_FULL)) { + ExtendedStat = AllocSetContextCreate(CurrentMemoryContext, + "ExtendedStat", + ALLOCSET_DEFAULT_MINSIZE, + ALLOCSET_DEFAULT_INITSIZE, + ALLOCSET_DEFAULT_MAXSIZE); + oldcontext = MemoryContextSwitchTo(ExtendedStat); + es = New(ExtendedStat) ES_SELECTIVITY(); + Assert(root != NULL); + s1 = es->calculate_selectivity(root, clauses, sjinfo, jointype, NULL, ES_EQJOINSEL); + clauselist = es->unmatched_clause_group; + (void)MemoryContextSwitchTo(oldcontext); + } + + /* + * 对条款进行初始扫描。 任何看起来不像是潜在的rangequery条款的东西都会被乘以s1并被遗忘。任何看起来像都会被插入到一个rqlist条目中。 + */ + foreach (l, clauselist) { + Node* clause = (Node*)lfirst(l); + RestrictInfo* rinfo = NULL; + Selectivity s2; + + /* 始终使用clause_selectivity来计算选择性 */ + s2 = clause_selectivity(root, clause, varRelid, jointype, sjinfo, varratio_cached, true); + + /* + * 检查是否被传递了一个RestrictInfo; + * 如果是假常数的RestrictInfo,那么s2要么是1.0,要么是0.0;只需使用这一点,而不是寻找范围对。 + */ + if (IsA(clause, RestrictInfo)) { + rinfo = (RestrictInfo*)clause; + if (rinfo->pseudoconstant) { + s1 = s1 * s2; + continue; + } + clause = (Node*)rinfo->clause; + } else + rinfo = NULL; + + /* + *如果子句是范围查询,如'between and', + *我们应该扫描范围查询对,并计算出最终的选择性。 + */ + OpExpr* expr = (OpExpr*)clause; + bool varonleft = true; + if (is_rangequery_clause(clause, rinfo, &varonleft)) { + /* + * 如果它不是一个"<"或">"操作符,就把选择权通用地合并进来。 但如果它是正确的操作,就把该子句添加到rqlist中,以便以后处理。 + */ + switch (get_oprrest(expr->opno)) { + case F_SCALARLTSEL: + addRangeClause(&rqlist, clause, varonleft, true, s2); + break; + case F_SCALARGTSEL: + addRangeClause(&rqlist, clause, varonleft, false, s2); + break; + default: + /* 只需将选择性合并到一般情况下,进行处理 */ + if ((uint32)u_sess->attr.attr_sql.cost_param & COST_ALTERNATIVE_CONJUNCT) + s1 = MIN(s1, s2); + else + s1 = s1 * s2; + break; + } + continue; + } + + /* 不是正确的形式,所以要泛化处理也就是一般化处理。*/ + if ((uint32)u_sess->attr.attr_sql.cost_param & COST_ALTERNATIVE_CONJUNCT) + s1 = MIN(s1, s2); + else + s1 = s1 * s2; + } + + /* + *现在扫描范围查询对列表。 + */ + while (rqlist != NULL) { + RangeQueryClause* rqnext = NULL; + + if (rqlist->have_lobound && rqlist->have_hibound) { + /* 成功地匹配了一对范围子句 */ + Selectivity s2; + /* + * 与缺省值完全相等可能意味着选择功能的失败。 这不是完美的,但应该足够好了。 + */ + if (rqlist->hibound == DEFAULT_INEQ_SEL || rqlist->lobound == DEFAULT_INEQ_SEL) { + s2 = DEFAULT_RANGE_INEQ_SEL; + } else { + s2 = rqlist->hibound + rqlist->lobound - 1.0; + + /* 调整对NULL的双重排除 */ + s2 += nulltestsel(root, IS_NULL, rqlist->var, varRelid, jointype, sjinfo); + + /* + 零或略微为负的s2应该被转换成一个小的正值;可能是在处理一个非常狭小的范围, + 由于四舍五入的错误而得到了一个假的结果。然而,如果s2是非常负的, + 那么我们可能在范围的一边或两边有默认的选择性估计,而在上面由于某种原因未能识别。 + */ + if (s2 <= 0.0) { + if (s2 < -0.01) { + /* + * 没有可用的数据——使用一个默认的估计值,即使是小的,但不是真正的小。 + */ + s2 = DEFAULT_RANGE_INEQ_SEL; + } else { + /* + * 这只是四舍五入的误差;使用一个小的正数值 + */ + s2 = 1.0e-10; + } + } + } + /* 在一对子句的选择性中合并 */ + s1 *= s2; + } else { + /* 只找到一对中的一个,将其通用合并 */ + if (rqlist->have_lobound) + s1 *= rqlist->lobound; + else + s1 *= rqlist->hibound; + } + varlist = lappend(varlist, rqlist->var); + /* 释放存储并推进 */ + rqnext = rqlist->next; + pfree_ext(rqlist); + rqlist = rqnext; + } + + /*缓存范围查询的var比率,并且有范围查询的var。*/ + if (varratio_cached && varlist != NIL) + set_varratio_for_rqclause(root, varlist, varRelid, s1, sjinfo); + + list_free_ext(varlist); + + /* 扩展统计所使用的自由空间 */ + if (es != NULL) { + clauselist = NIL; + /* 释放已使用空间,保证空间充足 */ + list_free_ext(es->unmatched_clause_group); + delete es; + MemoryContextDelete(ExtendedStat); + } + + return s1; +} +``` + diff --git a/content/zh/post/xtuRemi/clausesel.cpp2.md b/content/zh/post/xtuRemi/clausesel.cpp2.md new file mode 100644 index 0000000000000000000000000000000000000000..40540a3858016524136571741e1d9155f6d67cfc --- /dev/null +++ b/content/zh/post/xtuRemi/clausesel.cpp2.md @@ -0,0 +1,390 @@ +## clausesel.cpp2 + + + + + +**代码标注** + +``` +/* 产生arg列表,const转换为expr类型 */ +static List* switch_arg_items(Node* funExpr, Const* cnst, Oid* eqlOprOid, Oid* inputcollid, bool isequal) +{ + List* argList = NULL; + Node* arg = NULL; + Const* cnp = NULL; + Oid argType = InvalidOid; + + if (IsA(funExpr, FuncExpr) && ((FuncExpr*)funExpr)->funcformat == COERCE_IMPLICIT_CAST) { + FuncExpr* fun_expr = (FuncExpr*)funExpr; + arg = (Node*)linitial(fun_expr->args); + argType = exprType(arg); + HeapTuple typeTuple; + Oid funcId = 0; + Oid constType = exprType((Node*)cnst); + Datum constValue = (Datum)0; + + /* 我们只处理其分类不同的数据类型 */ + if (TypeCategory(argType) == TypeCategory(constType)) { + return NIL; + } + + CoercionPathType pathtype = find_coercion_pathway(argType, constType, COERCION_IMPLICIT, &funcId); + + if (pathtype != COERCION_PATH_NONE) { + MemoryContext current_context = CurrentMemoryContext; + bool outer_is_stream = false; + bool outer_is_stream_support = false; + ResourceOwner currentOwner = t_thrd.utils_cxt.CurrentResourceOwner; + ResourceOwner tempOwner = ResourceOwnerCreate(t_thrd.utils_cxt.CurrentResourceOwner, "SwitchArgItems", + MEMORY_CONTEXT_OPTIMIZER); + t_thrd.utils_cxt.CurrentResourceOwner = tempOwner; + + if (IS_PGXC_COORDINATOR) { + outer_is_stream = u_sess->opt_cxt.is_stream; + outer_is_stream_support = u_sess->opt_cxt.is_stream_support; + } + + PG_TRY(); + { + constValue = OidFunctionCall1(funcId, ((Const*)cnst)->constvalue); + } + PG_CATCH(); + { + MemoryContextSwitchTo(current_context); + FlushErrorState(); + + /* 以防它们没有被调回 */ + if (IS_PGXC_COORDINATOR) { + u_sess->opt_cxt.is_stream = outer_is_stream; + u_sess->opt_cxt.is_stream_support = outer_is_stream_support; + } + + /*释放PG_TRY的OidFunctionCall1中应用的资源。*/ + ResourceOwnerRelease(tempOwner, RESOURCE_RELEASE_BEFORE_LOCKS, false, false); + ResourceOwnerRelease(tempOwner, RESOURCE_RELEASE_LOCKS, false, false); + ResourceOwnerRelease(tempOwner, RESOURCE_RELEASE_AFTER_LOCKS, false, false); + t_thrd.utils_cxt.CurrentResourceOwner = currentOwner; + ResourceOwnerDelete(tempOwner); + + return NIL; + } + PG_END_TRY(); + + /*释放PG_TRY的standard_planner中应用的资源。*/ + ResourceOwnerRelease(tempOwner, RESOURCE_RELEASE_BEFORE_LOCKS, false, false); + ResourceOwnerRelease(tempOwner, RESOURCE_RELEASE_LOCKS, false, false); + ResourceOwnerRelease(tempOwner, RESOURCE_RELEASE_AFTER_LOCKS, false, false); + t_thrd.utils_cxt.CurrentResourceOwner = currentOwner; + ResourceOwnerDelete(tempOwner); + + if (IS_PGXC_COORDINATOR) { + u_sess->opt_cxt.is_stream = outer_is_stream; + u_sess->opt_cxt.is_stream_support = outer_is_stream_support; + } + } + + if (constValue) { + typeTuple = SearchSysCache1(TYPEOID, ObjectIdGetDatum(argType)); + if (!HeapTupleIsValid(typeTuple)) { + return NIL; + } + Form_pg_type type = (Form_pg_type)GETSTRUCT(typeTuple); + cnp = makeConst( + argType, exprTypmod(arg), type->typcollation, type->typlen, constValue, false, type->typbyval); + ReleaseSysCache(typeTuple); + } + } + if (cnp != NULL) { + argList = lappend(argList, arg); + argList = lappend(argList, cnp); + + if (argType == VARCHAROID) { + argType = TEXTOID; + } + + HeapTuple opertup; + opertup = SearchSysCache4(OPERNAMENSP, + CStringGetDatum(isequal ? "=" : "<>"), + ObjectIdGetDatum(argType), + ObjectIdGetDatum(argType), + ObjectIdGetDatum(PG_CATALOG_NAMESPACE)); + if (!HeapTupleIsValid(opertup)) { + return NIL; + } + + *eqlOprOid = HeapTupleGetOid(opertup); + + ReleaseSysCache(opertup); + + *inputcollid = exprCollation(arg); + } + + return argList; +} + +/* 限制信息转换 */ +static List* do_restrictinfo_conversion(List* args, Oid* eqlOprOid, Oid* inputcollid, bool isequal) +{ + AssertEreport(list_length(args) == 2, MOD_OPT, ""); + + bool lIsConst = false; + bool rIsConst = false; + Node* lNode = (Node*)linitial(args); + Node* rNode = (Node*)list_nth(args, 1); + + List* argsList = NULL; + + if (IsA(lNode, Const)) { + lIsConst = true; + } + + if (IsA(rNode, Const)) { + rIsConst = true; + } + + if (lIsConst == true && rIsConst == false) { + argsList = switch_arg_items(rNode, (Const*)lNode, eqlOprOid, inputcollid, isequal); + } else if (lIsConst == false && rIsConst == true) { + argsList = switch_arg_items(lNode, (Const*)rNode, eqlOprOid, inputcollid, isequal); + } + + return argsList; +} + +/* + * get_vardata_for_filter_or_semijoin: 获取var数据并缓存过滤器或半/反连接的选择性。 + * + * 参数。 + * @in root: 计划信息节点 + * @in clause: 估计LIMIT的图元 + * @in varRelid: varRelid要么是0,要么是一个rangetable索引,当varRelid不是0时,只有属于该关系的变量在计算选择性时被考虑。 + * @in selec:子句的选择性。 + * @in sjinfo: 连接关系的特殊连接信息 + * @in type: 筛选率或连接率 + * + * 返回:void + */ +static void get_vardata_for_filter_or_semijoin( + PlannerInfo* root, Node* clause, int varRelid, Selectivity selec, SpecialJoinInfo* sjinfo, RatioType type) +{ + get_vardata_for_filter_or_semijoin_context context; + bool vardataIsValid = false; + + /*构建上下文成员。*/ + context.root = root; + context.varRelid = varRelid; + context.ratiotype = type; + context.sjinfo = sjinfo; + errno_t rc = EOK; + + rc = memset_s(&context.filter_vardata, sizeof(VariableStatData), 0, sizeof(VariableStatData)); + securec_check(rc, "\0", "\0"); + rc = memset_s(&context.semijoin_vardata1, sizeof(VariableStatData), 0, sizeof(VariableStatData)); + securec_check(rc, "\0", "\0"); + rc = memset_s(&context.semijoin_vardata2, sizeof(VariableStatData), 0, sizeof(VariableStatData)); + securec_check(rc, "\0", "\0"); + + /*为子句获得瓦尔达塔步行者。*/ + vardataIsValid = get_vardata_for_filter_or_semijoin_walker(clause, &context); + + /* 如果vardata无效,我们不需要设置var比率。*/ + if (!vardataIsValid) { + return; + } + + /* 为过滤器或半/反连接设置var比率。*/ + if (RatioType_Filter == type) { + set_varratio_after_calc_selectivity(&context.filter_vardata, RatioType_Filter, selec, NULL); + ReleaseVariableStats(context.filter_vardata); + } else { + set_varratio_after_calc_selectivity(&context.semijoin_vardata1, RatioType_Join, selec, sjinfo); + set_varratio_after_calc_selectivity(&context.semijoin_vardata2, RatioType_Join, selec, sjinfo); + + ReleaseVariableStats(context.semijoin_vardata1); + ReleaseVariableStats(context.semijoin_vardata2); + } +} + +/* + * get_vardata_for_filter_or_semijoin_walker:为子句获取vardata walker。 + * + * 参数。 + * @in node: 限制信息中的子句节点。 + * @in context:参数中的上下文,将从子句的args中获取vardata。 + * + * 返回: bool(true:vardata is valid) + */ +static bool get_vardata_for_filter_or_semijoin_walker(Node* node, get_vardata_for_filter_or_semijoin_context* context) +{ + List* args = NIL; + Node* other = NULL; + Node* left = NULL; + Node* clause = NULL; + bool varonleft = false; + + if (node == NULL) + return false; + + /*从不同子句的args中获取vardata信息。*/ + if (IsA(node, Var)) { + Var* var = (Var*)node; + /* + *我们也许不应该在这里看到一个上层的Var,但如果我们看到了,就返回默认的选择性...。 + */ + if (var->varlevelsup == 0 && (context->varRelid == 0 || context->varRelid == (int)var->varno)) { + examine_variable(context->root, (Node*)var, context->varRelid, &context->filter_vardata); + return true; + } + + return false; + } else if (not_clause(node)) { + clause = (Node*)get_notclausearg((Expr*)node); + } else if (is_opclause(node)) { + OpExpr* opclause = (OpExpr*)node; + Oid opno = opclause->opno; + + if (RatioType_Join == context->ratiotype) { + bool join_is_reversed = false; + get_join_variables(context->root, + opclause->args, + context->sjinfo, + &context->semijoin_vardata1, + &context->semijoin_vardata2, + &join_is_reversed); + return true; + } else { + Oid eqlOprOid = 0; + List* argList = NULL; + Oid inputcollid = 0; + /*只处理=或<>运算符 */ + if (get_oprrest(opno) == EQSELRETURNOID || get_oprrest(opno) == NEQSELRETURNOID) { + argList = do_restrictinfo_conversion( + opclause->args, &eqlOprOid, &inputcollid, get_oprrest(opno) == EQSELRETURNOID); + } + + if (argList != NULL) + args = argList; + else + args = opclause->args; + + return get_restriction_variable( + context->root, args, context->varRelid, &context->filter_vardata, &other, &varonleft); + } + } else if (IsA(node, ScalarArrayOpExpr)) { + left = (Node*)linitial(((ScalarArrayOpExpr*)node)->args); + examine_variable(context->root, left, context->varRelid, &context->filter_vardata); + return true; + } else if (IsA(node, RowCompareExpr)) { + args = list_make2(linitial(((RowCompareExpr*)node)->largs), linitial(((RowCompareExpr*)node)->rargs)); + return get_restriction_variable( + context->root, args, context->varRelid, &context->filter_vardata, &other, &varonleft); + } else if (IsA(node, NullTest)) { + left = (Node*)((NullTest*)node)->arg; + examine_variable(context->root, left, context->varRelid, &context->filter_vardata); + return true; + } else if (IsA(node, BooleanTest)) { + left = (Node*)((BooleanTest*)node)->arg; + examine_variable(context->root, left, context->varRelid, &context->filter_vardata); + return true; + } else if (IsA(node, RelabelType)) { + clause = (Node*)((RelabelType*)node)->arg; + } else if (IsA(node, CoerceToDomain)) { + clause = (Node*)((CoerceToDomain*)node)->arg; + } + + return get_vardata_for_filter_or_semijoin_walker(clause, context); +} + +/* + * is_rangequery_clause:该子句是否是范围查询。 + * + * 参数: + * @in clause: 限制信息中的子句节点 + * @in rinfo:限制信息中的条款节点。 + * @in varonleft:识别子句中的var在左边还是右边。 + * + * 返回: bool(true:该条款是范围查询) + */ +static bool is_rangequery_clause(Node* clause, RestrictInfo* rinfo, bool* varonleft) +{ + bool isrqclause = false; + + /* + * 看看它是否像一个限制性条款,一边是假常数。 (比这更复杂的东西可能不会以我们所期望的简单方式表现出来)。 这里的大多数测试都可以用rinfo来完成,比没有rinfo更有效率。 + */ + if (is_opclause(clause) && list_length(((OpExpr*)clause)->args) == 2) { + OpExpr* expr = (OpExpr*)clause; + + if (rinfo != NULL) { + isrqclause = (bms_membership(rinfo->clause_relids) == BMS_SINGLETON) && + (is_pseudo_constant_clause_relids((Node*)lsecond(expr->args), rinfo->right_relids) || + (*varonleft = false, + is_pseudo_constant_clause_relids((Node*)linitial(expr->args), rinfo->left_relids))); + } else { + isrqclause = (NumRelids(clause) == 1) && + (is_pseudo_constant_clause((Node*)lsecond(expr->args)) || + (*varonleft = false, is_pseudo_constant_clause((Node*)linitial(expr->args)))); + } + } + + return isrqclause; +} + +/* + * is_rangequery_contain_scalarop: 如果范围查询包含标量运算符。 + * + * 参数。 + * @in clause: 限制信息中的子句节点 + * @in rinfo: 限制信息 + * + * 返回: bool(true:范围查询包含标量运算符) + */ +static bool is_rangequery_contain_scalarop(Node* clause, RestrictInfo* rinfo) +{ + bool varonleft = false; + + if (is_rangequery_clause(clause, rinfo, &varonleft)) { + OpExpr* expr = (OpExpr*)clause; + + if ((F_SCALARLTSEL == get_oprrest(expr->opno)) || (F_SCALARGTSEL == get_oprrest(expr->opno))) + return true; + } + + return false; +} + +/* + * set_varratio_for_rqclause:为范围查询子句设置var比率。 + * + * 参数。 + * @in root: 计划信息节点 + * @in varlist: 范围查询子句包含许多变量 + * @in varRelid: varRelid是0或者是一个范围表的索引,当varRelid不是0的时候。 + * 只有属于该关系的变量在计算选择性时被考虑。 + * @in ratio: 根据估计的连接比例。 + * @in sjinfo: 当前关系与其他关系连接的连接信息。 + * + * 返回:void + */ +static void set_varratio_for_rqclause( + PlannerInfo* root, List* varlist, int varRelid, double ratio, SpecialJoinInfo* sjinfo) +{ + ListCell* lc = NULL; + + foreach (lc, varlist) { + VariableStatData vardata; + Node* node = (Node*)lfirst(lc); + + examine_variable(root, node, varRelid, &vardata); + + if (sjinfo == NULL) + set_varratio_after_calc_selectivity(&vardata, RatioType_Filter, ratio, NULL); + else + set_varratio_after_calc_selectivity(&vardata, RatioType_Join, ratio, sjinfo); + + ReleaseVariableStats(vardata); + } +} +``` + diff --git a/content/zh/post/xtuRemi/costsize.md b/content/zh/post/xtuRemi/costsize.md new file mode 100644 index 0000000000000000000000000000000000000000..be24b0d85bc9f8011e4cb45d9f70161367a72934 --- /dev/null +++ b/content/zh/post/xtuRemi/costsize.md @@ -0,0 +1,41 @@ +## SQL引擎之查询优化—costsize.cpp计算关系与代价路径(起航) + +**前言:**本系列文章将对costsize.cpp文件进行个人解读,由浅入深,对计算关系与代价路径进行一一解读 + + + +首先介绍计算路径成本主要的单位参数: + +1. seq_page_cost 顺序获取页面的成本 +2. random_page_cost 一个非连续的页面获取的成本 +3. cpu_tuple_cost 处理一个元组的典型CPU时间的成本 +4. cpu_index_tuple_cost 处理一个索引元组的典型CPU时间的成本 +5. cpu_operator_cost 执行一个操作符或函数的CPU时间成本 + +g_instance.cost_cxt.effective_cache_size:粗略估计Postgres+OS-level(操作系统级)磁盘缓存的磁盘页数 + +优势:用户可以设置上面的参数,如此可以防止默认值对某一特定平台有很大的偏差,给予了系统很大的容错性。 + + + +为每条路径计算了两个成本 + +总成本(total_cost):获取所有元组的估计总成本 + +启动成本(startup_cost):在获取第一个图元之前所花费的成本。 + + + +当然,假如有一个限制(LIMIT)路径,并且这个LIMIT将在系统中作为一个top_level最高级别计划顶点而被应用,我们大可不必去计算total_cost,怎么去解决这个问题?在查询优化中调用者可以通过在startup_cost与total_cost之间进行插值来获取部分成果的成本。 + +实际上: +$$ +actual.cost = startup.cost + (total.cost - startup.cost) * tuples.to.fetch / path->rows +$$ +tuples_to_fetch / path->rows:实际成本系数,简而言之就是实际取值路径与基础总路径(基础关系行数)比值作为实际成本系数, + + + +可以放心的是,系统例程做了非零处理,保证不把行数设置为零,所以不会有零除法,而且一个基础关系的行数的设置是不考虑任何LIMIT的,不会考虑特殊情况,公式的普适性得到保证 + +**地址:**src/gausskernel/optimizer/path/costsize.cpp \ No newline at end of file diff --git a/content/zh/post/xtuRemi/costsize2.md b/content/zh/post/xtuRemi/costsize2.md new file mode 100644 index 0000000000000000000000000000000000000000..4f7aa956a906cdc59268e3d444d9f9a1f6a0dff5 --- /dev/null +++ b/content/zh/post/xtuRemi/costsize2.md @@ -0,0 +1,352 @@ +## 前言 +本篇介绍costsize.cpp中扫描成本的计算,以及对数据结果的合理处理(尽管是强制化),还有对其他路径如sort/agg/group by/material/windowfun进行全局设置。 + +## 函数解释 + +``` +double clamp_row_est(double nrows); +``` + +作用:将行数强制规定为一个非零整型,结果输出会更好。 +—为什么需要非零且整数? +—目的是为了在插值成本是可能会被除以零,同时对大于1的nrows采取rint,四舍五入取整来进行合理化操作。 + +``` +void set_path_rows(Path* path, double rows, double multiple); +``` + +作用:为其他路径特别是下层路径提供全局和本地的行的接口,便于引用 + +``` +void set_rel_path_rows(Path* path, RelOptInfo* rel, ParamPathInfo* param_info); +``` + +作用:通过set_path_rows为有效的baserel或joinrel路径设置本地和全局的行。 + +``` +static void set_parallel_path_rows(Path* path); +``` + +作用:在并行后设置真实路径行。 + 因为在我们对路径进行并行操作后,在一个节点与全局的返回的影响下,rows都会变动(增加),这对并行连接内部路径时非常重要。 + +``` +void cost_seqscan(Path* path, PlannerInfo* root, RelOptInfo* baserel, ParamPathInfo* param_info); +``` + +作用:确定并返回依次扫描一个关系的成本。 + baserel : 要扫描的关系 + param_info :是ParamPathInfo,如果这是一个参数化的路径,否则就是NULL。 + +``` +void cost_samplescan(Path* path, PlannerInfo* root, RelOptInfo* baserel, ParamPathInfo* param_info); +``` + +作用:确定并返回使用采样扫描一个关系的成本。 + path:seqscan或cstorescan路径。 + root:当前查询级别的plannerinfo结构。 + baserel:要扫描的关系。 + param_info:ParamPathInfo,如果这是一个参数化的路径,否则为空。 + +``` +void cost_cstorescan(Path* path, PlannerInfo* root, RelOptInfo* baserel); +``` + +作用:确定并返回扫描一个列存储的成本。 + 值得一提的是,cost_cstorescan更像是cost_seqscan的一个特例“参数化路径—cstorescan”。 + +``` +void cost_dfsscan(Path* path, PlannerInfo* root, RelOptInfo* baserel); +``` + +作用:确定并返回扫描一个DFS关系的成本。 + 同样也是cost_seqscan的一个特例化参数路径—dfsscan,与其不同的是,dfsscan与cstorescan对扫描成本的计算加入了平行化启动成本g_instance.cost_cxt.disable_cost + +## 代码标注 +```cpp +/* + * clamp_row_est + * 将行数估计值强制为一个合理的值。 + */ +double clamp_row_est(double nrows) +{ + /* + * 强制要求估计值至少为一行,以使解释输出看起来更好, + * 并避免在插值成本时可能被除以零。让它也成为一个整数。 + */ + if (nrows <= 1.0) { + nrows = 1.0; + } else { + /* 四舍五入取整操作 */ + nrows = rint(nrows); + } + + return nrows; +} + +/* 为下层路径的sort/agg/group by/material/windowfun路径设置本地和全局的行。*/ +void set_path_rows(Path* path, double rows, double multiple) +{ + path->rows = rows; + path->multiple = multiple; +} + +/* 为baserel或joinrel路径设置本地和全局的行。*/ +void set_rel_path_rows(Path* path, RelOptInfo* rel, ParamPathInfo* param_info) +{ + /* 用正确的行估计值标记路径 */ + if (param_info != NULL) + set_path_rows(path, param_info->ppi_rows); + else + set_path_rows(path, rel->rows, rel->multiple); +} + +/* + * 在并行后设置真实路径行。 + * + * @in_param path: 需要修正的路径。 + */ +static void set_parallel_path_rows(Path* path) +{ + int dop = SET_DOP(path->dop); + + /* + * 当我们并行复制路径时,一个节点和全局的返回行都会增加。这在并行连接内部路径时很有用。 + */ + if (is_replicated_path(path)) { + path->rows *= dop; + } +} + +/* + * cost_seqscan + * 确定并返回依次扫描一个关系的成本。 + * 在set_plain_rel_size()中会考虑分区表的修剪比例。 + * 'baserel'是要扫描的关系 + * 'param_info'是ParamPathInfo,如果这是一个参数化的路径,否则就是NULL。 + */ +void cost_seqscan(Path* path, PlannerInfo* root, RelOptInfo* baserel, ParamPathInfo* param_info) +{ + Cost startup_cost = 0; + Cost run_cost = 0; + double spc_seq_page_cost; + QualCost qpqual_cost; + Cost cpu_per_tuple = 0.0; + int dop = SET_DOP(path->dop); + bool disable_path = enable_parametrized_path(root, baserel, (Path*)path); + + /* 只应适用于基础关系 */ + Assert(baserel->relid > 0); + Assert(baserel->rtekind == RTE_RELATION); + + /* 用正确的行估计值标记路径 */ + set_rel_path_rows(path, baserel, param_info); + set_parallel_path_rows(path); + + /* 获取包含表的表空间的估计页面成本 */ + get_tablespace_page_costs(baserel->reltablespace, NULL, &spc_seq_page_cost); + get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost); + + startup_cost += qpqual_cost.startup; + if (!u_sess->attr.attr_sql.enable_seqscan || disable_path) + startup_cost += g_instance.cost_cxt.disable_cost; + + /* + * 当我们对扫描节点进行并行处理时,磁盘费用和CPU费用将会与所有的并行线程相等分。 + */ + run_cost += u_sess->opt_cxt.smp_thread_cost * (dop - 1); + run_cost += spc_seq_page_cost * baserel->pages / dop; + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost + qpqual_cost.per_tuple; + run_cost += cpu_per_tuple * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples) / dop; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; + + if (!u_sess->attr.attr_sql.enable_seqscan || disable_path) + path->total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); +} + +/* + * 作用:确定并返回使用采样扫描一个关系的成本。 + * + * 参数。 + * @in path: seqscan或cstorescan路径。 + * @in root: 当前查询级别的plannerinfo结构。 + * @in baserel: 要扫描的关系。 + * @in param_info: ParamPathInfo,如果这是一个参数化的路径,否则为空。 + * + * Return: void + */ +void cost_samplescan(Path* path, PlannerInfo* root, RelOptInfo* baserel, ParamPathInfo* param_info) +{ + Cost startup_cost = 0; + Cost run_cost = 0; + RangeTblEntry* rte = NULL; + TableSampleClause* tsc = NULL; + double spc_seq_page_cost, spc_random_page_cost, spc_page_cost; + QualCost qpqual_cost; + Cost cpu_per_tuple = 0.0; + + /* 只应适用于带有tableample条款的基础关系 */ + AssertEreport(baserel->relid > 0, + MOD_OPT, + "The relid is invalid when determining the cost of scanning a relation using sampling."); + rte = planner_rt_fetch(baserel->relid, root); + AssertEreport(rte->rtekind == RTE_RELATION, + MOD_OPT, + "Only base relation can be supported when determining the cost of scanning a relation using sampling."); + tsc = rte->tablesample; + AssertEreport(tsc != NULL, + MOD_OPT, + "Samling method and parameters is null when determining the cost of scanning a relation using sampling."); + + set_rel_path_rows(path, baserel, param_info); + + /* 获取包含表的表空间的估计页面成本 */ + get_tablespace_page_costs(baserel->reltablespace, &spc_random_page_cost, &spc_seq_page_cost); + + /* 如果sampleType为SYSTEM_SAMPLE,则假定为随机访问,否则为顺序访问 */ + spc_page_cost = (tsc->sampleType == BERNOULLI_SAMPLE) ? spc_seq_page_cost : spc_random_page_cost; + + /* + * 磁盘成本(baserel->pages已经被设置为采样方法将访问的页面数量) + */ + run_cost += spc_page_cost * baserel->pages; + + /* + * CPU成本(baserel->tuples已经被设置为采样方法将选择的图元数量)。 + * 请注意,我们忽略了TABLESAMPLE参数表达式的执行成本; + * 它们在每次扫描中只被评估一次,而且在大多数情况下,它们可能是简单的常数。 + * 我们也不对取样方法内部可能进行的计算收取任何费用。 + */ + get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost); + + startup_cost += qpqual_cost.startup; + + if (baserel->orientation == REL_COL_ORIENTED) { + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost / COL_TUPLE_COST_MULTIPLIER + qpqual_cost.per_tuple; + } else if (baserel->orientation == REL_TIMESERIES_ORIENTED) { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Unsupported Using Index FOR TIMESERIES."))); + } else { + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost + qpqual_cost.per_tuple; + } + + run_cost += cpu_per_tuple * baserel->tuples; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; +} + +/* + * cost_cstorescan + * 确定并返回扫描一个列存储的成本。 + * 在set_plain_rel_size()中会考虑分区表的修剪比例。 + */ +void cost_cstorescan(Path* path, PlannerInfo* root, RelOptInfo* baserel) +{ + double spc_seq_page_cost; + Cost startup_cost = 0; + Cost run_cost = 0; + Cost cpu_per_tuple = 0.0; + int dop = SET_DOP(path->dop); + + /* 只应适用于基础关系 */ + Assert(baserel->relid > 0 && baserel->rtekind == RTE_RELATION); + + set_rel_path_rows(path, baserel, NULL); + set_parallel_path_rows(path); + + /* 获取包含表的表空间的估计页面成本 */ + get_tablespace_page_costs(baserel->reltablespace, NULL, &spc_seq_page_cost); + + startup_cost += baserel->baserestrictcost.startup; + + if (!u_sess->attr.attr_sql.enable_seqscan) + startup_cost += g_instance.cost_cxt.disable_cost; + + /* + * 当我们对扫描节点进行并行处理时,磁盘费用和CPU费用将会与所有的并行线程相等分。 + */ + run_cost += u_sess->opt_cxt.smp_thread_cost * (dop - 1); + run_cost += spc_seq_page_cost * baserel->pages / dop; + cpu_per_tuple = + u_sess->attr.attr_sql.cpu_tuple_cost / COL_TUPLE_COST_MULTIPLIER + baserel->baserestrictcost.per_tuple; + run_cost += cpu_per_tuple * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples) / dop; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; + + if (!u_sess->attr.attr_sql.enable_seqscan) + path->total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); +} + +/* + * 确定并返回扫描一个DFS关系的成本。 + * path:扫描路径。 + * root:PlannerInfo结构。 + * baserel: 要扫描的关系。 + */ +void cost_dfsscan(Path* path, PlannerInfo* root, RelOptInfo* baserel) +{ + Cost startup_cost = 0; + Cost run_cost = 0; + double spc_seq_page_cost; + Cost cpu_per_tuple = 0.0; + int dop = SET_DOP(path->dop); + + /* + * 同样适用于基础关系 + */ + AssertEreport( + baserel->relid > 0, MOD_OPT, "The relid is invalid when determining the cost of scanning a DFS relation."); + AssertEreport(baserel->rtekind == RTE_RELATION, + MOD_OPT, + "Only base relation can be supported when determining the cost of scanning a DFS relation."); + + set_rel_path_rows(path, baserel, NULL); + set_parallel_path_rows(path); + + /* + *获取包含表的表空间的估计页面成本。 + */ + get_tablespace_page_costs(baserel->reltablespace, NULL, &spc_seq_page_cost); + + startup_cost = baserel->baserestrictcost.startup; + /* 平行化启动成本。*/ + if (!u_sess->attr.attr_sql.enable_seqscan) + startup_cost += g_instance.cost_cxt.disable_cost; + + /* + * 当我们对扫描节点进行并行处理时,磁盘费用和CPU费用将会与所有的并行线程相等分。 + */ + run_cost += u_sess->opt_cxt.smp_thread_cost * (dop - 1); + cpu_per_tuple = + u_sess->attr.attr_sql.cpu_tuple_cost / COL_TUPLE_COST_MULTIPLIER + baserel->baserestrictcost.per_tuple; + + run_cost += (cpu_per_tuple * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples)) / dop + + (spc_seq_page_cost * baserel->pages) / dop; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; + + if (!u_sess->attr.attr_sql.enable_seqscan) + path->total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); + + /* + *DFS表的数据重新分配。 + */ + if (true == u_sess->attr.attr_sql.enable_cluster_resize && root->query_level == 1 && + root->parse->commandType == CMD_INSERT) { + root->dataDestRelIndex = baserel->relid; + } +} +``` \ No newline at end of file diff --git a/content/zh/post/xtuRemi/costsize3.md b/content/zh/post/xtuRemi/costsize3.md new file mode 100644 index 0000000000000000000000000000000000000000..35c807f51ddbe030ce0ad243ab3b04d3eec8857d --- /dev/null +++ b/content/zh/post/xtuRemi/costsize3.md @@ -0,0 +1,356 @@ +估计被取走的主表页数,并计算I/O成本。 + +当索引排序与表的排序不相关时,我们使用Mackert和Lohman提出的近似方法(详见index_pages_fetched())来计算取用的页数,然后对取用的每一页收取spc_random_page_cost。 + +当索引的排序与表的排序完全相关时(例如,就在CLUSTER之后),获取的页数应该正好是选择性(selectivity )* table_size。 更重要的是,除了第一次之外,所有的都是顺序取数,而不是在不相关的情况下发生的随机取数。 因此,如果页面数量超过1,我们应该收取pc_random_page_cost + (pages_fetched - 1) * spc_seq_page_cost + +对于部分相关的索引,我们应该在这两个估计值之间计算成本进行“收费”。 目前,我们在这两个估计值之间进行了线性插值,在相关平方的基础上对估计值进行线性插值 + +如果是只扫描索引,那么我们就不需要获取可见性地图显示所有图元都可见的任何堆页。因此,相应地减少估计的堆取数。我们使用测得的整个堆中全部可见的部分,这可能与这个查询要获取的堆的子集不是特别相关;但目前还不清楚如何做得更好。 + + + + + +```c++ +/* + * apply_random_page_cost_mod + * 在产生的MAX/MIN成本上应用一个逻辑过滤器。 + * 当处理有少量随机访问的小表时,这个mod很有效。 + * + * 注1:Random_page_cost衡量的是在硬盘上进行随机访问的效率,我们认为它与刚才的页数有一定关系。 + * 与页数有关,只是暂时的。 + * + * 注意2: 我们确实想根据页数动态地评估成本,因为对于较小的表来说,随机访问和顺序访问不会有太大的区别,因此在这些情况下,成本应该被调低。 + * + * 这个模型将帮助计划者更好地促进索引扫描路径。 + */ + double apply_random_page_cost_mod(double rand_page_cost, double seq_page_cost, int num_of_page) +{ + /* + * 在num_of_page = 1000的时候平滑出来。 + * 我们在随机测试的基础上得到这个数字。尽量不要让这个数字太大,因为在大多数计划中,索引扫描可能会占主导地位。 + */ +#define COST_SLOPE_FACTOR 0.005 +#define COST_PAGE_THRESHOLD 1000 + double slope_factor = COST_SLOPE_FACTOR; + + /* Logistic回归函数 */ + double new_page_cost = (seq_page_cost >= rand_page_cost) ? rand_page_cost : \ + LOGISTIC_FUNC(num_of_page, COST_PAGE_THRESHOLD, rand_page_cost, seq_page_cost, slope_factor); + + ereport(DEBUG2, + (errmodule(MOD_OPT), + (errmsg("Estimating random page cost = %lf with sql_beta_feature = RAND_COST_OPT.", new_page_cost)))); + + return new_page_cost; +} +/* + * is_predpush_dest + * 检查predpush dest是否是索引的一部分。 + * 如果匹配则返回true。 + */ +static bool is_predpush_dest(PlannerInfo* root, Relids indexes) +{ + HintState *hstate = root->parse->hintState; + if (hstate == NULL) { + return false; + } + + if (hstate->predpush_hint == NULL) { + return false; + } + + ListCell *lc = NULL; + foreach (lc, hstate->predpush_hint) { + PredpushHint *predpushHint = (PredpushHint*)lfirst(lc); + if (predpushHint->dest_id != 0 && predpushHint->candidates != NULL && \ + bms_is_member(predpushHint->dest_id, indexes)) { + return true; + } + } + return false; +} + +/* + * enable_parametrized_path + * 如果启用,禁用所有不匹配的路径,以获得我们需要的参数化路径。 + */ + static bool enable_parametrized_path(PlannerInfo* root, RelOptInfo* baserel, Path* path) +{ + /* 断言验证path存在 */ + Assert(path != NULL); + + if (ENABLE_PRED_PUSH_FORCE(root) && is_predpush_dest(root, baserel->relids)) { + if (path->param_info) { + /* 子集匹配判断真假 */ + return !bms_is_subset(path->param_info->ppi_req_outer, predpush_candidates_same_level(root)); + } else { + return true; + } + } + + return false; +} + +/* + * cost_index + * 确定并返回使用索引扫描一个关系的成本。 + * + * 'path'描述了所考虑的索引扫描,除了本例程要设置的字段外,其他都是完整的。 + * 'loop_count'是索引扫描的重复次数,以考虑到缓存行为的估计 + */ + void cost_index(IndexPath* path, PlannerInfo* root, double loop_count) +{ + IndexOptInfo* index = path->indexinfo; + RelOptInfo* baserel = index->rel; + bool indexonly = (path->path.pathtype == T_IndexOnlyScan); + List* allclauses = NIL; + Cost startup_cost = 0; + Cost run_cost = 0; + Cost indexStartupCost; + Cost indexTotalCost; + Selectivity indexSelectivity; + double indexCorrelation, csquared; + double spc_seq_page_cost, spc_random_page_cost; + Cost min_IO_cost, max_IO_cost; + QualCost qpqual_cost; + Cost cpu_per_tuple = 0.0; + double tuples_fetched; + double pages_fetched; + bool ispartitionedindex = path->indexinfo->rel->isPartitionedTable; + bool disable_path = enable_parametrized_path(root, baserel, (Path*)path) || \ + (!u_sess->attr.attr_sql.enable_indexscan); + + /* 只应适用于基础关系 */ + AssertEreport(IsA(baserel, RelOptInfo) && IsA(index, IndexOptInfo), + MOD_OPT, + "The nodeTag of baserel is not T_RelOptInfo, or the nodeTag of index is not T_IndexOptInfo" + "when determining the cost of scanning a relation using an index."); + AssertEreport(baserel->relid > 0, + MOD_OPT, + "The relid is invalid when determining the cost of scanning a relation using an index."); + AssertEreport(baserel->rtekind == RTE_RELATION, + MOD_OPT, + "Only base relation can be supported when determining the cost of scanning a relation using an index."); + + set_rel_path_rows(&path->path, baserel, path->path.param_info); + + /* 用正确的行估计值标记路径 */ + if (path->path.param_info) { + /*还可以得到应该由扫描强制执行的条款集 */ + allclauses = list_concat(list_copy(path->path.param_info->ppi_clauses), baserel->baserestrictinfo); + } else { + /* allclauses应该只是rel的限制性条款 */ + allclauses = baserel->baserestrictinfo; + } + + if (disable_path) + startup_cost += g_instance.cost_cxt.disable_cost; + /* 我们不需要检查enable_indexonlyscan;indxpath.c做了这个 */ + /* 调用针对索引访问方法的代码来估计扫描索引的处理成本,以及索引的选择性(即我们必须检索的主表图元的比例)和它与主表图元顺序的相关性。*/ + OidFunctionCall7(index->amcostestimate, + PointerGetDatum(root), + PointerGetDatum(path), + Float8GetDatum(loop_count), + PointerGetDatum(&indexStartupCost), + PointerGetDatum(&indexTotalCost), + PointerGetDatum(&indexSelectivity), + PointerGetDatum(&indexCorrelation)); + + /* + * 保存amcostestimate的结果,以便可能用于位图扫描规划。 + * 我们不需要保存indexStartupCost或indexCorrelation,因为一个 因为位图扫描并不关心这两者。 + */ + path->indextotalcost = indexTotalCost; + path->indexselectivity = indexSelectivity; + + /*这里包括了触摸索引本身的所有费用 */ + startup_cost += indexStartupCost; + run_cost += indexTotalCost - indexStartupCost; + + /* 估计获取的主表图元的数量 */ + tuples_fetched = clamp_row_est(indexSelectivity * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples)); + + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("Computing IndexScanCost: tuples_fetched: %lf, indexSelectivity: %lf, indexTotalCost: %lf", + tuples_fetched, indexSelectivity, indexTotalCost))); + + /* 获取包含表的表空间的估计页面成本 */ + get_tablespace_page_costs(baserel->reltablespace, &spc_random_page_cost, &spc_seq_page_cost); + + /* important*3 */ + /* 估计获取的主表页的数量,并计算I/O成本。*/ + + /* 费用模数标志 */ + double old_random_page_cost = spc_random_page_cost; + bool use_modded_cost = ENABLE_SQL_BETA_FEATURE(RAND_COST_OPT); + + if (loop_count > 1) { + /* + * For repeated indexscans, the appropriate estimate for the + * uncorrelated case is to scale up the number of tuples fetched in + * the Mackert and Lohman formula by the number of scans, so that we + * estimate the number of pages fetched by all the scans; then + * pro-rate the costs for one scan. In this case we assume all the + * fetches are random accesses. + */ + pages_fetched = index_pages_fetched( + tuples_fetched * loop_count, (BlockNumber)baserel->pages, (double)index->pages, root, ispartitionedindex); + + if (indexonly) + pages_fetched = ceil(pages_fetched * (1.0 - baserel->allvisfrac)); + + /* Apply cost mod */ + spc_random_page_cost = RANDOM_PAGE_COST(use_modded_cost, old_random_page_cost, \ + spc_seq_page_cost, pages_fetched); + + max_IO_cost = (pages_fetched * spc_random_page_cost) / loop_count; + + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("Computing IndexScanCost(loop_count > 1): max_pages_fetched: %lf, max_IO_cost: %lf", + pages_fetched, max_IO_cost))); + + /* + * In the perfectly correlated case, the number of pages touched by + * each scan is selectivity * table_size, and we can use the Mackert + * and Lohman formula at the page level to estimate how much work is + * saved by caching across scans. We still assume all the fetches are + * random, though, which is an overestimate that's hard to correct for + * without double-counting the cache effects. (But in most cases + * where such a plan is actually interesting, only one page would get + * fetched per scan anyway, so it shouldn't matter much.) + */ + pages_fetched = ceil(indexSelectivity * (double)baserel->pages); + + pages_fetched = index_pages_fetched( + pages_fetched * loop_count, (BlockNumber)baserel->pages, (double)index->pages, root, ispartitionedindex); + + if (indexonly) + pages_fetched = ceil(pages_fetched * (1.0 - baserel->allvisfrac)); + + /* Apply cost mod after new pages fetched */ + spc_random_page_cost = RANDOM_PAGE_COST(use_modded_cost, old_random_page_cost, \ + spc_seq_page_cost, pages_fetched); + + min_IO_cost = (pages_fetched * spc_random_page_cost) / loop_count; + + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("Computing IndexScanCost(loop_count > 1): min_pages_fetched: %lf, min_IO_cost: %lf", + pages_fetched, min_IO_cost))); + } else { + /* + * Normal case: apply the Mackert and Lohman formula, and then + * interpolate between that and the correlation-derived result. + */ + pages_fetched = index_pages_fetched( + tuples_fetched, (BlockNumber)baserel->pages, (double)index->pages, root, ispartitionedindex); + + if (indexonly) + pages_fetched = ceil(pages_fetched * (1.0 - baserel->allvisfrac)); + + /* Apply cost mod */ + spc_random_page_cost = RANDOM_PAGE_COST(use_modded_cost, old_random_page_cost, \ + spc_seq_page_cost, pages_fetched); + + /* max_IO_cost is for the perfectly uncorrelated case (csquared=0) */ + max_IO_cost = pages_fetched * spc_random_page_cost; + + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("Computing IndexScanCost(loop_count = 1): max_pages_fetched: %lf, max_IO_cost: %lf", + pages_fetched, max_IO_cost))); + + /* min_IO_cost is for the perfectly correlated case (csquared=1) */ + pages_fetched = ceil(indexSelectivity * (double)baserel->pages); + + if (indexonly) + pages_fetched = ceil(pages_fetched * (1.0 - baserel->allvisfrac)); + + if (pages_fetched > 0) { + /* Apply cost mod after new pages fetched */ + spc_random_page_cost = RANDOM_PAGE_COST(use_modded_cost, old_random_page_cost, \ + spc_seq_page_cost, pages_fetched); + + min_IO_cost = spc_random_page_cost; + if (pages_fetched > 1) + min_IO_cost += (pages_fetched - 1) * spc_seq_page_cost; + } else { + min_IO_cost = 0; + } + + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("Computing IndexScanCost(loop_count = 1): min_pages_fetched: %lf, min_IO_cost: %lf", + pages_fetched, min_IO_cost))); + } + + min_IO_cost = Min(min_IO_cost, max_IO_cost); + /* + * Now interpolate based on estimated index order correlation to get total + * disk I/O cost for main table accesses. + */ + csquared = indexCorrelation * indexCorrelation; + + run_cost += max_IO_cost + csquared * (min_IO_cost - max_IO_cost); + + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("Computing IndexScanCost: max_IO_cost: %lf, min_IO_cost: %lf, csquared: %lf, IO_run_cost: %lf", + max_IO_cost, min_IO_cost, csquared, max_IO_cost + csquared * (min_IO_cost - max_IO_cost)))); + + /* + * Estimate CPU costs per tuple. + * + * What we want here is cpu_tuple_cost plus the evaluation costs of any + * qual clauses that we have to evaluate as qpquals. We approximate that + * list as allclauses minus any clauses appearing in indexquals. (We + * assume that pointer equality is enough to recognize duplicate + * RestrictInfos.) This method neglects some considerations such as + * clauses that needn't be checked because they are implied by a partial + * index's predicate. It does not seem worth the cycles to try to factor + * those things in at this stage, even though createplan.c will take pains + * to remove such unnecessary clauses from the qpquals list if this path + * is selected for use. + */ + cost_qual_eval(&qpqual_cost, list_difference_ptr(allclauses, path->indexquals), root); + + startup_cost += qpqual_cost.startup; + + if (path->path.parent->orientation == REL_COL_ORIENTED) + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost / 10 + qpqual_cost.per_tuple; + else if (path->path.parent->orientation == REL_TIMESERIES_ORIENTED) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Unsupported Using Index FOR TIMESERIES."))); + else + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost + qpqual_cost.per_tuple; + + run_cost += cpu_per_tuple * tuples_fetched; + + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("Computing IndexScanCost: cpu_per_tuple: %lf, tuples_fetched: %lf, cpu_run_cost: %lf", + cpu_per_tuple, tuples_fetched, cpu_per_tuple * tuples_fetched))); + + path->path.startup_cost = startup_cost; + path->path.total_cost = startup_cost + run_cost; + path->path.stream_cost = 0; + + if (disable_path) + path->path.total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); + /* clamp weighted cost below 1e30 for double overflow */ + double weight = u_sess->attr.attr_sql.cost_weight_index; + path->path.startup_cost = (1e30f / weight > path->path.startup_cost) ? (path->path.startup_cost * weight) : (1e30); + path->path.total_cost = (1e30f / weight > path->path.total_cost) ? (path->path.total_cost * weight) : (1e30); + ereport(DEBUG2, + (errmodule(MOD_OPT), + errmsg("IndexScan Cost startup_cost: %lf, total_cost: %lf, pages_fetched: %lf", + path->path.startup_cost, path->path.total_cost, pages_fetched))); +} +``` + diff --git a/content/zh/post/xtuRemi/costsize4.md b/content/zh/post/xtuRemi/costsize4.md new file mode 100644 index 0000000000000000000000000000000000000000..f405ee712522ef100887873a72e9ba3feae4b8b9 --- /dev/null +++ b/content/zh/post/xtuRemi/costsize4.md @@ -0,0 +1,87 @@ +使用Mackert和Lohman在"Index Scans Using a Finite LRU Buffer: A Validated I/O Model"中提出的一个近似值: + +PF = +min(2TNs/(2T+Ns), T) when T <= b + 2TNs/(2T+Ns) when T > b and Ns <= 2Tb/(2T-b) + b + (Ns - 2Tb/(2T-b))*(T-b)/T when T > b and Ns > 2Tb/(2T-b) + +其中 + +​ T = # 表中的页 + N = # 表中的图元 + s = selectivity = 需要扫描的表的一部分 + b = # 可用的缓冲页数(这里包括内核空间) + +系统假设g_instance.cost_cxt.effective_cache_size是整个查询可用的缓冲页总数,然后在查询中的所有表和当前考虑的索引中按比例分配这个空间。 (这忽略了查询中使用的其他索引所需要的空间,但是由于不知道哪些索引会被使用,所以不能很好的估计;而且在任何情况下,计算所有的表都可能是一个高估的结果,因为根据连接计划,不是所有的表都会被同时扫描。) + +乘积Ns是获取的图元数;在这里传入该乘积,而不是计算它。 "pages "是所考虑的对象(无论是索引还是表)的页数。"index_pages "是要添加到总表空间的数量,这是由query_planner为我们计算的。 + +调用者应确保tuples_fetched大于0,并四舍五入为整数(见 clamp_row_est)。结果也将同样大于零,并且是整数。 + +添加一个输入参数,以指示是否为分区索引。因为计算分区索引的基本统计信息的方法不能满足普通表的逻辑,所以必须特别处理它。 + +``` +/* + * index_pages_fetched + * 估计在考虑到缓存效应后实际获取的页面数量。 + */ + double index_pages_fetched( + double tuples_fetched, BlockNumber pages, double index_pages, PlannerInfo* root, bool ispartitionedindex) +{ + double pages_fetched; + double total_pages; + double T, b; + + /* T是表中的页数,但不允许它为零,因为在近似值中做分母*/ + T = (pages > 1) ? (double)pages : 1.0; + + /* 计算假定争夺缓存空间的页面数量 */ + total_pages = root->total_table_pages + index_pages; + total_pages = Max(total_pages, 1.0); + + /* + * 估计实际获取的页面数量是很特别的。 + * 但是因为计算分区索引的基本统计信息的方法不能满足普通表的逻辑,尽管看上去不合法,系统也只能依旧执行。 + */ + if (ispartitionedindex) { + T = (T > total_pages ? T : total_pages); + } else { + AssertEreport(total_pages >= T, + MOD_OPT, + "The number of pages in table is larger than total_pages" + "when estimating the number of pages actually fetched."); + } + + /* b是u_sess->attr.attr_sql.effective_cache_size的按比例份额 */ + b = (double)u_sess->attr.attr_sql.effective_cache_size * T / total_pages; + + /* 迫使它成为正数和积分 */ + if (b <= 1.0) { + b = 1.0; + } else { + b = ceil(b); + } + + /* Mackert和Lohman公式计算 */ + if (T <= b) { + pages_fetched = (2.0 * T * tuples_fetched) / (2.0 * T + tuples_fetched); + if (pages_fetched >= T) { + pages_fetched = T; + } else { + pages_fetched = ceil(pages_fetched); + } + } else { + double lim; + + lim = (2.0 * T * b) / (2.0 * T - b); + if (tuples_fetched <= lim) { + pages_fetched = (2.0 * T * tuples_fetched) / (2.0 * T + tuples_fetched); + } else { + pages_fetched = b + (tuples_fetched - lim) * (T - b) / T; + } + pages_fetched = ceil(pages_fetched); + } + return pages_fetched; +} +``` + diff --git a/content/zh/post/xtuRemi/costsize5.md b/content/zh/post/xtuRemi/costsize5.md new file mode 100644 index 0000000000000000000000000000000000000000..1be6e2297b4cacdd810cf7db63feac0682031e08 --- /dev/null +++ b/content/zh/post/xtuRemi/costsize5.md @@ -0,0 +1,283 @@ +``` +/* + * get_indexpath_pages + * 确定一个位图索引路径中使用的索引的总大小。 + * + * 注意:如果同一个索引在一个位图树中使用了不止一次,我们将多次计算它。 + */ + static double get_indexpath_pages(Path* bitmapqual) +{ + double result = 0; + ListCell* l = NULL; + /* 不同位图索引路径的节点类型 */ + if (IsA(bitmapqual, BitmapAndPath)) { + BitmapAndPath* apath = (BitmapAndPath*)bitmapqual; + + foreach (l, apath->bitmapquals) { + result += get_indexpath_pages((Path*)lfirst(l)); + } + } else if (IsA(bitmapqual, BitmapOrPath)) { + BitmapOrPath* opath = (BitmapOrPath*)bitmapqual; + + foreach (l, opath->bitmapquals) { + result += get_indexpath_pages((Path*)lfirst(l)); + } + } else if (IsA(bitmapqual, IndexPath)) { + IndexPath* ipath = (IndexPath*)bitmapqual; + + result = (double)ipath->indexinfo->pages; + } else { + /* 异常抛出 */ + ereport(ERROR, + (errmodule(MOD_OPT), + errcode(ERRCODE_UNRECOGNIZED_NODE_TYPE), + errmsg("unrecognized node type of a bitmap index path when get pages: %d", nodeTag(bitmapqual)))); + } + + return result; +} + +/* + * cost_bitmap_heap_scan + * 确定并返回使用位图index-then-heap计划扫描一个关系的成本。 + * + * 'baserel'是要扫描的关系 + * 'param_info'是ParamPathInfo,如果这是一个参数化的路径,否则是NULL + * 'bitmapqual'是IndexPaths、BitmapAndPaths和BitmapOrPaths的一个树状结构 + * 'loop_count'是索引扫描的重复次数,用于估计缓存行为的因素 + * + * 注意:bitmapqual中的IndexPaths组件应该使用相同的loop_count来计算成本。 + */ +void cost_bitmap_heap_scan( + Path* path, PlannerInfo* root, RelOptInfo* baserel, ParamPathInfo* param_info, Path* bitmapqual, double loop_count) +{ + Cost startup_cost = 0; + Cost run_cost = 0; + Cost indexTotalCost; + Selectivity indexSelectivity; + QualCost qpqual_cost; + Cost cpu_per_tuple = 0.0; + Cost cost_per_page; + double tuples_fetched; + double pages_fetched; + double spc_seq_page_cost, spc_random_page_cost; + double T; + bool ispartitionedindex = path->parent->isPartitionedTable; + bool partition_index_unusable = false; + bool containGlobalOrLocalIndex = false; + bool disable_path = enable_parametrized_path(root, baserel, (Path*)path); + + /* 只应适用于基础关系 */ + AssertEreport(IsA(baserel, RelOptInfo), + MOD_OPT, + "The nodeTag of baserel is not T_RelOptInfo" + "when determining the cost of scanning a relation using a bitmap index-then-heap plan."); + AssertEreport(baserel->relid > 0, + MOD_OPT, + "The relid is invalid when determining the cost of scanning a relation" + "using a bitmap index-then-heap plan."); + AssertEreport(baserel->rtekind == RTE_RELATION, + MOD_OPT, + "Only base relation can be supported when determining the cost of scanning a relation" + "using a bitmap index-then-heap plan."); + + /* 用正确的行估计值标记路径 */ + set_rel_path_rows(path, baserel, param_info); + + /* + * 支持分区(partiton)索引不可用。 + * 这里不支持位图索引不可用。如果位图路径包含不可用的索引路径,将enable_bitmapscan设置为关闭。因此,如果选择了索引路径,它将进行分区全部/部分不可用的索引扫描。 + */ + if (ispartitionedindex) { + if (!check_bitmap_heap_path_index_unusable(bitmapqual, baserel)) + partition_index_unusable = true; + + /* If the bitmap path contains Global partition index OR local partition index, set enable_bitmapscan to off */ + if (CheckBitmapHeapPathContainGlobalOrLocal(bitmapqual)) { + containGlobalOrLocalIndex = true; + } + } + + if (!u_sess->attr.attr_sql.enable_bitmapscan || partition_index_unusable || containGlobalOrLocalIndex || + baserel->bucketInfo != NULL || disable_path) { + startup_cost += g_instance.cost_cxt.disable_cost; + } + + /* + *获取位图的总成本,以及其总的选择性。 + */ + cost_bitmap_tree_node(bitmapqual, &indexTotalCost, &indexSelectivity); + + startup_cost += indexTotalCost; + + /* 获取包含表的表空间的估计页面成本。*/ + get_tablespace_page_costs(baserel->reltablespace, &spc_random_page_cost, &spc_seq_page_cost); + + /* + * 估计获取的主表页数。 + */ + tuples_fetched = clamp_row_est(indexSelectivity * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples)); + + T = (baserel->pages > 1) ? (double)baserel->pages : 1.0; + + if (loop_count > 1) { + /* + * 对于重复的位图扫描,将Mackert和Lohman公式中获取的图元数按扫描次数放大,这样我们就能估计出所有扫描获取的页数。然后按比例对一次扫描进行计算。 + */ + pages_fetched = index_pages_fetched(tuples_fetched * loop_count, + (BlockNumber)baserel->pages, + get_indexpath_pages(bitmapqual), + root, + ispartitionedindex); + + pages_fetched /= loop_count; + } else { + /* + * 对于单次扫描,需要获取的堆页数与Mackert和Lohman公式中的T <= b的情况相同。(即不需要重读)。 + */ + pages_fetched = (2.0 * T * tuples_fetched) / (2.0 * T + tuples_fetched); + } + if (pages_fetched >= T) { + pages_fetched = T; + } else { + pages_fetched = ceil(pages_fetched); + } + + /* + * 对于少量的页面,我们应该收取spc_random_page_cost的费用,而如果几乎所有表的页面都被读取,那么收取spc_seq_page_cost的费用更合适。这种影响也是非线性的。如果没有更好的主意,可以这样插值来确定每页的成本。 + */ + if (pages_fetched >= 2.0) + cost_per_page = spc_random_page_cost - (spc_random_page_cost - spc_seq_page_cost) * sqrt(pages_fetched / T); + else + cost_per_page = spc_random_page_cost; + + run_cost += pages_fetched * cost_per_page; + + /* + * 估计每个元组的CPU成本。 + * + * 通常情况下,索引量不需要在每个元组中重新检查......但并不总是如此,特别是如果有足够多的元组涉及到位图变得有损。 目前,只是假设它们总是被重新检查。 这意味着我们对所有的扫描条款收取全部的费用。 + */ + get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost); + + startup_cost += qpqual_cost.startup; + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost + qpqual_cost.per_tuple; + + run_cost += cpu_per_tuple * tuples_fetched; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; + + if (!u_sess->attr.attr_sql.enable_bitmapscan || partition_index_unusable || disable_path) + path->total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); +} + +/* + * cost_bitmap_tree_node + * 从一个位图树节点中提取成本和选择性(索引/和/或)。 + */ + void cost_bitmap_tree_node(Path* path, Cost* cost, Selectivity* selec) +{ + if (IsA(path, IndexPath)) { + *cost = ((IndexPath*)path)->indextotalcost; + *selec = ((IndexPath*)path)->indexselectivity; + + /* + * 对每个检索到的元组收取少量费用(0.1 * n),以反映操作位图的成本。 这主要是为了确保位图扫描的成本不会与索引扫描检索单个元组的成本相同。 + */ + *cost += 0.1 * u_sess->attr.attr_sql.cpu_operator_cost * PATH_LOCAL_ROWS(path); + } else if (IsA(path, BitmapAndPath)) { + *cost = path->total_cost; + *selec = ((BitmapAndPath*)path)->bitmapselectivity; + } else if (IsA(path, BitmapOrPath)) { + *cost = path->total_cost; + *selec = ((BitmapOrPath*)path)->bitmapselectivity; + } else { + ereport(ERROR, + (errmodule(MOD_OPT), + errcode(ERRCODE_UNRECOGNIZED_NODE_TYPE), + errmsg("unrecognized node type when extract cost and selectivity from a bitmap tree node: %d", + nodeTag(path)))); + *cost = *selec = 0; /* keep compiler quiet */ + } +} + +/* + * cost_bitmap_and_node + * 估计一个BitmapAnd节点的成本 + * + * 注意,这只考虑了索引扫描和位图创建的成本,而不是最终的堆访问。从这个意义上说,这个对象并不是真正的路径,但是它有足够多的类似路径的属性(尤其是成本),可以把它当作一个路径来对待。 + */ + + void cost_bitmap_and_node(BitmapAndPath* path, PlannerInfo* root) +{ + Cost totalCost; + Selectivity selec; + ListCell* l = NULL; + + /* + * 我们在假设输入是独立的情况下估计AND的选择性。 + * + * BitmapAnd本身的运行时间成本估计为每个需要的tbm_intersect的100倍cpu_operator_cost。 + */ + totalCost = 0.0; + selec = 1.0; + foreach (l, path->bitmapquals) { + Path* subpath = (Path*)lfirst(l); + Cost subCost; + Selectivity subselec; + + cost_bitmap_tree_node(subpath, &subCost, &subselec); + + selec *= subselec; + + totalCost += subCost; + if (l != list_head(path->bitmapquals)) + totalCost += 100.0 * u_sess->attr.attr_sql.cpu_operator_cost; + } + path->bitmapselectivity = selec; + set_path_rows(&path->path, 0); /* per above, not used */ + path->path.startup_cost = totalCost; + path->path.total_cost = totalCost; + path->path.stream_cost = 0; +} +/* + * cost_bitmap_or_node + * 估计一个BitmapOr节点的成本 + * + * 参见cost_bitmap_and_node的注释。 + */ + void cost_bitmap_or_node(BitmapOrPath* path, PlannerInfo* root) +{ + Cost totalCost; + Selectivity selec; + ListCell* l = NULL; + + /* + * 我们估计OR的选择性时假设输入是不重叠的,因为在 "x IN (list) "类型的情况下经常是这样的。 当然,我们在最后将其钳制为1.0。 + */ + totalCost = 0.0; + selec = 0.0; + foreach (l, path->bitmapquals) { + Path* subpath = (Path*)lfirst(l); + Cost subCost; + Selectivity subselec; + + cost_bitmap_tree_node(subpath, &subCost, &subselec); + + selec += subselec; + + totalCost += subCost; + if (l != list_head(path->bitmapquals) && !IsA(subpath, IndexPath)) + totalCost += 100.0 * u_sess->attr.attr_sql.cpu_operator_cost; + } + path->bitmapselectivity = Min(selec, 1.0); + set_path_rows(&path->path, 0); /* per above, not used */ + path->path.startup_cost = totalCost; + path->path.total_cost = totalCost; + path->path.stream_cost = 0; +} +``` + diff --git a/content/zh/post/xtuRemi/costsize6.md b/content/zh/post/xtuRemi/costsize6.md new file mode 100644 index 0000000000000000000000000000000000000000..35d564f5b6a4f7cdb40399969c446db48f15a6d1 --- /dev/null +++ b/content/zh/post/xtuRemi/costsize6.md @@ -0,0 +1,209 @@ +``` +/* + * cost_tidscan + * 确定并返回使用TIDs扫描一个关系的成本。 + */ + void cost_tidscan(Path* path, PlannerInfo* root, RelOptInfo* baserel, List* tidquals) +{ + Cost startup_cost = 0; + Cost run_cost = 0; + bool isCurrentOf = false; + Cost cpu_per_tuple = 0.0; + QualCost tid_qual_cost; + int ntuples; + ListCell* l = NULL; + double spc_random_page_cost; + + /* 只应适用于基础关系 */ + AssertEreport(baserel->relid > 0, + MOD_OPT, + "The relid is invalid when determining the cost of scanning a relation using TIDs."); + AssertEreport(baserel->rtekind == RTE_RELATION, + MOD_OPT, + "Only base relation can be supported" + "when determining the cost of scanning a relation using TIDs."); + + /* 目前,tidscans从未被参数化 */ + set_rel_path_rows(path, baserel, NULL); + + /* 计算我们期望检索到的图元数量 */ + ntuples = 0; + foreach (l, tidquals) { + if (IsA(lfirst(l), ScalarArrayOpExpr)) { + /* 数组的每个元素产生1个元组 */ + ScalarArrayOpExpr* saop = (ScalarArrayOpExpr*)lfirst(l); + Node* arraynode = (Node*)lsecond(saop->args); + + ntuples += estimate_array_length(arraynode); + } else if (IsA(lfirst(l), CurrentOfExpr)) { + /* CURRENT OF产生1个元组 */ + isCurrentOf = true; + ntuples++; + } else { + /* 这只是CTID = something, count 1 tuple */ + ntuples++; + } + } + + /* + * 我们必须强制对WHERE CURRENT OF进行TID扫描,因为只有nodeTidscan.c了解如何正确地进行扫描。 因此,只有当 CURRENT OF 不存在时,才具备 u_sess->attr.attr_sql.enable_tidscan。 还要注意cost_qual_eval将CurrentOfExpr计算为具有启动成本g_instance.cost_cxt.disable_cost,在这里减去这个成本;这是为了防止其他计划类型,如执行seqscan。 + */ + if (isCurrentOf) { + AssertEreport(baserel->baserestrictcost.startup >= g_instance.cost_cxt.disable_cost, + MOD_OPT, + "The one-time cost of base relation is less than disable_cost" + "when determining the cost of scanning a relation using TIDs."); + startup_cost -= g_instance.cost_cxt.disable_cost; + } else if (!u_sess->attr.attr_sql.enable_tidscan) + startup_cost += g_instance.cost_cxt.disable_cost; + + /* + * TID限定表达式将被计算一次,任何其他baserestrict则在每个检索元组上计算一次。 + */ + cost_qual_eval(&tid_qual_cost, tidquals, root); + + /* 获取包含表的表空间的估计页面成本 */ + get_tablespace_page_costs(baserel->reltablespace, &spc_random_page_cost, NULL); + + /* 磁盘成本 --- 假设每个元组在不同的页面上 */ + run_cost += spc_random_page_cost * ntuples; + + /* CPU成本 */ + startup_cost += baserel->baserestrictcost.startup + tid_qual_cost.per_tuple; + cpu_per_tuple = + u_sess->attr.attr_sql.cpu_tuple_cost + baserel->baserestrictcost.per_tuple - tid_qual_cost.per_tuple; + run_cost += cpu_per_tuple * ntuples; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; + + if (!u_sess->attr.attr_sql.enable_tidscan && !isCurrentOf) + path->total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); +} + +/* + * cost_subqueryscan + * 确定并返回扫描子查询RTE的成本。 + * + * 'baserel'是要扫描的关系 + * 'param_info'是ParamPathInfo,如果这是一个参数化的路径,否则就是NULL。 + */ + void cost_subqueryscan(Path* path, PlannerInfo* root, RelOptInfo* baserel, ParamPathInfo* param_info) +{ + Cost startup_cost; + Cost run_cost; + QualCost qpqual_cost; + Cost cpu_per_tuple = 0.0; + + /* 只应适用于属于子查询的基础关系 */ + AssertEreport( + baserel->relid > 0, MOD_OPT, "The relid is invalid when determining the cost of scanning a subquery RTE."); + AssertEreport(baserel->rtekind == RTE_SUBQUERY, + MOD_OPT, + "Only subquery in FROM clause can be supported" + "when determining the cost of scanning a subquery RTE."); + + /* 用正确的行估计值标记路径 */ + set_rel_path_rows(path, baserel, param_info); + + /* + * 路径的成本是评估子计划的成本,加上评估任何附加在SubqueryScan节点上的限制性条款的成本。加上cpu_tuple_cost,以考虑到选择和投影的开销。 + */ + path->startup_cost = baserel->subplan->startup_cost; + path->total_cost = baserel->subplan->total_cost; + + get_restriction_qual_cost(root, baserel, param_info, &qpqual_cost); + + startup_cost = qpqual_cost.startup; + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost + qpqual_cost.per_tuple; + run_cost = cpu_per_tuple * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples); + + path->startup_cost += startup_cost; + path->total_cost += startup_cost + run_cost; + path->stream_cost = get_subqueryscan_stream_cost(baserel->subplan); + ereport(DEBUG2, + (errmodule(MOD_OPT_SUBPLAN), + errmsg("subqueryscan stream_cost: %lf, startup_cost: %lf, total_cost: %lf", + path->stream_cost, + path->startup_cost, + path->total_cost))); +} +/* + * cost_functionscan + * 确定并返回扫描一个函数RTE的成本。 + */ + void cost_functionscan(Path* path, PlannerInfo* root, RelOptInfo* baserel) +{ + Cost startup_cost = 0; + Cost run_cost = 0; + Cost cpu_per_tuple = 0.0; + RangeTblEntry* rte = NULL; + QualCost exprcost; + + AssertEreport( + baserel->relid > 0, MOD_OPT, "The relid is invalid when determining the cost of scanning a function RTE."); + rte = planner_rt_fetch(baserel->relid, root); + AssertEreport(rte->rtekind == RTE_FUNCTION, + MOD_OPT, + "Only function in FROM clause can be supported" + "when determining the cost of scanning a function RTE."); + + /* 函数扫描从未被参数化 */ + set_rel_path_rows(path, baserel, NULL); + + /* + * 估计执行函数表达式的成本。 + * + * 目前,nodeFunctionscan.c总是在返回任何行之前将函数执行完毕,并将结果缓存在tuplestore中。因此,函数的评估成本是所有的启动成本,而每行的成本是最小的。 + */ + cost_qual_eval_node(&exprcost, rte->funcexpr, root); + + startup_cost += exprcost.startup + exprcost.per_tuple; + + /* 增加扫描的CPU费用 */ + startup_cost += baserel->baserestrictcost.startup; + cpu_per_tuple = u_sess->attr.attr_sql.cpu_tuple_cost + baserel->baserestrictcost.per_tuple; + run_cost += cpu_per_tuple * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples); + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; +} +/* + * cost_valuesscan + * 确定并返回扫描一个VALUES RTE的成本。 + */ + void cost_valuesscan(Path* path, PlannerInfo* root, RelOptInfo* baserel) +{ + Cost startup_cost = 0; + Cost run_cost = 0; + Cost cpu_per_tuple = 0.0; + + /* 只应适用于属于数值列表的基础关系 */ + AssertEreport( + baserel->relid > 0, MOD_OPT, "The relid is invalid when determining the cost of scanning a VALUES RTE."); + AssertEreport(baserel->rtekind == RTE_VALUES, + MOD_OPT, + "Only VALUES list can be supported when determining the cost of scanning a VALUES RTE."); + + /* valuesscans从未被参数化 */ + set_rel_path_rows(path, baserel, NULL); + + /* + *目前,估计列表评估成本为每个列表的一个操作者评估(可能很假,但值得更聪明吗?) + */ + cpu_per_tuple = u_sess->attr.attr_sql.cpu_operator_cost; + + /* 增加扫描的CPU费用 */ + startup_cost += baserel->baserestrictcost.startup; + cpu_per_tuple += u_sess->attr.attr_sql.cpu_tuple_cost + baserel->baserestrictcost.per_tuple; + run_cost += cpu_per_tuple * RELOPTINFO_LOCAL_FIELD(root, baserel, tuples); + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; +} +``` + diff --git a/content/zh/post/xtuRemi/costsize7.md b/content/zh/post/xtuRemi/costsize7.md new file mode 100644 index 0000000000000000000000000000000000000000..3fcf809f15b26ddce61e878fa6b8ec8b115d4def --- /dev/null +++ b/content/zh/post/xtuRemi/costsize7.md @@ -0,0 +1,143 @@ +``` +/* + * cost_recursive_union + * 确定并返回执行递归联合的成本,以及估计的输出大小。 + * + * 我们给出了非递归和递归条款的计划。 + * + * 请注意,参数和输出都是Plans,而不是本模块其他部分的Paths。 这是因为我们不需要为递归联合设置Path表示法---我们只有一种方法可以做到。 + */ + + void cost_recursive_union(Plan* runion, Plan* nrterm, Plan* rterm) +{ + Cost startup_cost; + Cost total_cost; + double total_rows; + double total_global_rows; + + /* 我们可能对非递归项有体面的估计 */ + startup_cost = nrterm->startup_cost; + total_cost = nrterm->total_cost; + total_rows = PLAN_LOCAL_ROWS(nrterm); + total_global_rows = nrterm->plan_rows; + + /* + * 任意假设需要10次递归迭代,并且已经设法很好地解决了其中每一次的成本和输出大小。 这些假设很不可靠,但很难看出如何做得更好。 + */ + total_cost += 10 * rterm->total_cost; + total_rows += 10 * PLAN_LOCAL_ROWS(rterm); + total_global_rows += 10 * rterm->plan_rows; + + /* + *还对每行收取cpu_tuple_cost,以说明操作tuplestores的成本。 (我们不担心可能的溢出到磁盘的成本)。 + */ + total_cost += u_sess->attr.attr_sql.cpu_tuple_cost * total_rows; + + runion->startup_cost = startup_cost; + runion->total_cost = total_cost; + set_plan_rows(runion, total_global_rows, nrterm->multiple); + runion->plan_width = Max(nrterm->plan_width, rterm->plan_width); +} + +/* + * cost_sort + * 确定并返回对一个关系进行排序的成本,包括读取输入数据的成本. + * 如果需要排序的数据总量小于sort_mem,我们将进行内存排序,这不需要I/O和对t个图元进行约t*log2(t)个图元的比较。 + * 如果总容量超过sort_mem,我们就切换到磁带式合并算法。 总共仍然会有大约t*log2(t)个元组的比较,但是我们还需要在每个合并过程中对每个元组进行一次写入和读取。 我们预计会有大约ceil(logM(r))次合并,其中r是初始运行的数量,M是tuplesort.c使用的合并顺序。 + * disk traffic(磁盘流量) = 2 * relsize * ceil(logM(p / (2*sort_mem)) + * cpu = comparison_cost * t * log2(t) + * 如果排序是有界的(即只需要前k个结果图元),并且k个图元可以放入sort_mem中,我们使用一个堆方法,在堆中只保留k个图元;这将需要大约t*log2(k)图元比较。 + * 磁盘流量被假定为3/4的顺序和1/4的随机访问 + * 默认情况下,我们对每个元组的比较收取两个运算符的evals,这在大多数情况下应该是正确的。调用者可以通过指定非零的comparison_cost来调整这一点;一般来说,这是为任何额外的额外的工作,以便为比较运算符的输入做准备。 + * + * 'pathkeys'是一个排序键的列表 + * 'input_cost'是读取输入数据的总成本 + * 'tuples'是关系中图元组的数量 + * 'width'是元组的平均宽度,单位是字节 + * 'comparison_cost'是每次比较的额外成本,如果有的话 + * 'sort_mem'是允许用于排序的工作内存的千字节数 + * 'limit_tuples'是对输出图元数量的限制;如果没有限制则为-1 + * 'mem_info'是内存控制模块所使用的操作者最大和最小信息。 + * + void cost_sort(Path* path, List* pathkeys, Cost input_cost, double tuples, int width, Cost comparison_cost, + int sort_mem, double limit_tuples, bool col_store, int dop, OpMemInfo* mem_info, bool index_sort) +{ + Cost startup_cost = input_cost; + Cost run_cost = 0; + double input_bytes = relation_byte_size(tuples, width, col_store, true, true, index_sort) / SET_DOP(dop); + double output_bytes; + double output_tuples; + long sort_mem_bytes = sort_mem * 1024L / SET_DOP(dop); + + dop = SET_DOP(dop); + + if (!u_sess->attr.attr_sql.enable_sort) + startup_cost += g_instance.cost_cxt.disable_cost; + + /* + * 我们要确保排序的成本永远不会被估计为零,即使传入的元组数量为零。 此外,不能做log(0)... + */ + if (tuples < 2.0) { + tuples = 2.0; + } + + /* 包括默认的每次比较的成本 */ + comparison_cost += 2.0 * u_sess->attr.attr_sql.cpu_operator_cost; + + if (limit_tuples > 0 && limit_tuples < tuples) { + output_tuples = limit_tuples; + output_bytes = relation_byte_size(output_tuples, width, col_store, true, true, index_sort); + } else { + output_tuples = tuples; + output_bytes = input_bytes; + } + + if (output_bytes > sort_mem_bytes) { + /* + * CPU成本 + * + * 假设约有N个对数N的比较 + */ + startup_cost += comparison_cost * tuples * LOG2(tuples); + + /* 磁盘成本 */ + startup_cost += compute_sort_disk_cost(input_bytes, sort_mem_bytes); + } else { + if (tuples > 2 * output_tuples || input_bytes > sort_mem_bytes) { + /* + * 我们将使用有界堆排序,在内存中只保留K个图元,图元比较的总数为N log2 K;但常数因素比quicksort要高一些。 对它进行调整,使成本曲线在交叉点上是连续的。 + */ + startup_cost += comparison_cost * tuples * LOG2(2.0 * output_tuples); + } else { + /* 我们将对所有的输入图元使用普通的quicksort */ + startup_cost += comparison_cost * tuples * LOG2(tuples); + } + } + + if (mem_info != NULL) { + mem_info->opMem = u_sess->opt_cxt.op_work_mem; + mem_info->maxMem = output_bytes / 1024L * dop; + mem_info->minMem = mem_info->maxMem / SORT_MAX_DISK_SIZE; + mem_info->regressCost = compute_sort_disk_cost(input_bytes, mem_info->minMem); + /* 特殊情况,如果阵列大于1G,所以我们必须溢出到磁盘 */ + if (output_tuples > (MaxAllocSize / TUPLE_OVERHEAD(true) * dop)) { + mem_info->maxMem = STATEMENT_MIN_MEM * 1024L * dop; + mem_info->minMem = Min(mem_info->maxMem, mem_info->minMem); + } + } + + /* + *还对每个提取的元组收取少量费用(任意设置为等于操作者成本)。 我们不收取cpu_tuple_cost,因为排序节点不做质量检查或投影,所以它的开销比大多数计划节点要少。 注意在这里使用tuples而不是output_tuples是正确的------LIMIT的上限会按比例计算运行成本,所以我们会重复计算LIMIT,否则的话。 + */ + run_cost += u_sess->attr.attr_sql.cpu_operator_cost * tuples; + + path->startup_cost = startup_cost; + path->total_cost = startup_cost + run_cost; + path->stream_cost = 0; + + if (!u_sess->attr.attr_sql.enable_sort) + path->total_cost *= + (g_instance.cost_cxt.disable_cost_enlarge_factor * g_instance.cost_cxt.disable_cost_enlarge_factor); +} +``` +