pg 统计信息及索引cost 简介
pg 统计信息及索引cost 简介
单列的统计信息
单列的统计信息,通过do_analyze_rel 函数进行生成,主要分为两个部分:
- 数据采样
- 数据统计
数据采样
随机的采取表中的一部分数据进行分析,如果没有指定采样数(一般都没有),使用default_statistics_target (默认100)进行计算,采样容量为 300 * default_statistics_target (默认30000)。
根据 SIGMOD98 中的论文 Random sampling fo r histogram construction: how much is enough 可以得出这个这个已足够,当当表很大是,仍需调大default_statistics_target 。
采样算法采样两阶段算法,第一阶段采样使用S算法对表中的页面进行随机采样,第二个阶段使用 Z(Vitter)算法,它在第一阶段采样出来的页面的基础上对元组进行采样。
数据采样的两个阶段采用不同的算法是因为当对一个表进行统计分析的时候,它的页面数(块数)是可以准确获得到的,也就是说页面采样是在己知总体容量的基础上进行的。而第二阶段的 Z(Vitter)算法是一种蓄水池算法,它主要解决的是在不知道总体容量的情况下如何进
对于外部表:
- pg: 通过AnalyzeForeignTable 函数获取采样外部表的函数acquirefunc(没有则warning,同时也获取relpages),然后调用函数获取采样的行,然后对数据进行统计
- opengauss :通过AnalyzeForeignTable 函数获取采样外部表的函数acquirefunc(没有则warning,同时也获取relpages),但gauss 不使用acquirefunc 获取行, 而是通过AcquireSampleRows采样行(只支持obs,hdfs, obs_cvs, oracle_fdw,pg_fdw,mot,RELKIND_STREAM?支持其他的需要修改代码)
数据统计
分析各个列的统计信息, 对于表达式索引,也会计算表达式值并统计信息。
统计数据会存在pg_statistic 表中,
pages/tuples 会计入对应的表和索引中(Update pages/tuples stats in pg_class, 默认表和索引的行数相同,除非是partial index - 带where条件的索引 )
索引不会使用采样到的pages ,因为这个pages 是表的pages. 而是使用GetOneRelNBlocks 获取的blocks, 作为pages. 对于外表就是0
索引cost 计算
cpu 和 io , 选择率用来确定行数或索引项数
对于seqscan, 选择率用来确定 返回的行数
对于索引扫描,会调用索引的amcostestimate 函数计算索引cost和索引项数目,索引条件的选择率(其实就是列的选择率)用来确定扫描到的索引项数,会基于这个索引项数确定对堆表的io 和cpu cost (pg 索引不是聚簇索引,需要回表查询,除非indexonly)
c
amcostestimate:
/*
* @param loop_count 索引作为内表时,需要循环的次数
* @[out]indexStartupCost 索引扫描自身的启动代价
* @[out]indexTotalCost 索引扫描自身的整体代价
* @[out]indexSelectivity 索引扫描的选择率
* @[out]indexCorrelation 索引的相关系数
* @[out]indexPages 索引page 数,opengauss 没有
*
*/
void (*amcostestimate)(PlannerInfo *root, IndexPath *path, double loop_count, Cost *indexStartupCost,
Cost *indexTotalCost, Selectivity *indexSelectivity, double *indexCorrelation,double *indexPages)
在btree 中,启动代价为约束中的表达式代价,即计算所有的表达式值的代价(因为在每次扫描前需要计算表达式的值,右值)。
总代价为 io代价和 cpu代价的和。
- io代价: 索引扫描的执行次数取决于外表的元组数和是否有ScalarArrayOpExpr(有那么num_sa_scans也是扫描次数),所以num_scans = num_sa_scans * num_outer_scans;因为多次扫描可能导致数据被缓存,所以通过如下方式计算IO cost:
c
/*
对于外表:
在pg 中,index->pages 为0
在 opengauss 中,在 build_simple_rel 中会调用 set_local_rel_size,最终调用clamp_row_est 会把index->pages 转换为1, 也即外表上的索引 index->pages 为1
*/
if (index->pages > 1 && index->tuples > 1)
numIndexPages = ceil(numIndexTuples * index->pages / index->tuples);
else
numIndexPages = 1.0; // numIndexPages 如果小于1 会被设置为1,对于外表即为1
double pages_fetched;
/* total page fetches ignoring cache effects */
pages_fetched = numIndexPages * num_scans;
/* use Mackert and Lohman formula to adjust for cache effects */
pages_fetched = index_pages_fetched(pages_fetched,
index->pages,
(double) index->pages,
root);
/*
* Now compute the total disk access cost, and then report a pro-rated
* share for each outer scan. (Don't pro-rate for ScalarArrayOpExpr,
* since that's internal to the indexscan.)
*/
indexTotalCost = (pages_fetched * spc_random_page_cost)
/ num_outer_scans;
非多次扫描:indexTotalCost = numIndexPages * spc_random_page_cost;
- CPU cost:
c
qual_op_cost = cpu_operator_cost *(list_length(indexQuals) + list_length(indexOrderBys));
indexTotalCost += qual_arg_cost; // 加上start_cost,qual_arg_cost即start cost
indexTotalCost += numIndexTuples * num_sa_scans * (cpu_index_tuple_cost + qual_op_cost); // 加上每个索引项的cpu cost
opengauss 没有下面的cost:
b tree查找非叶子节点的cost:
if (index->tuples > 1) /* avoid computing log(0) */
{
descentCost = ceil(log(index->tuples) / log(2.0)) * cpu_operator_cost;
costs.indexStartupCost += descentCost;
costs.indexTotalCost += costs.num_sa_scans * descentCost;
}
b tree对节点的page 进行二分查找的cost:
descentCost = (index->tree_height + 1) * 50.0 * cpu_operator_cost;
costs.indexStartupCost += descentCost;
costs.indexTotalCost += costs.num_sa_scans * descentCost;
- 相关系数:单列的通过pg_statistic 获取
行数确定
通过调用get_relation_info (add_base_rels_to_query)获取表及表上索引的page 和行数, 对于表调用estimate_rel_size 获取,对于非部分索引,page 通过RelationGetNumberOfBlocks 获取, tuples 即为表的tuples。 部分索引通过estimate_rel_size 获取,但如果获取的行数大于表的行数,则tuples 设置为表行数。
estimate_rel_size 会去获取关系的blocks ,然后计算pages 和tuples, 之后在make_one_rel 中会调用set_base_rel_sizes 调整rows, 宽度
- 外表:
c
static void
set_foreign_size(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte)
{
/* Mark rel with estimated output rows, width, etc */
set_foreign_size_estimates(root, rel);
/* Let FDW adjust the size estimates, if it can */
rel->fdwroutine->GetForeignRelSize(root, rel, rte->relid);
/* ... but do not let it set the rows estimate to zero */
rel->rows = clamp_row_est(rel->rows);
/* also, make sure rel->tuples is not insane relative to rel->rows */
rel->tuples = Max(rel->tuples, rel->rows);
}
- 普通表:
c
void
set_baserel_size_estimates(PlannerInfo *root, RelOptInfo *rel)
{
double nrows;
/* Should only be applied to base relations */
Assert(rel->relid > 0);
nrows = rel->tuples *
clauselist_selectivity(root,
rel->baserestrictinfo,
0,
JOIN_INNER,
NULL);
rel->rows = clamp_row_est(nrows);
cost_qual_eval(&rel->baserestrictcost, rel->baserestrictinfo, root);
set_rel_width(root, rel);
}