PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

63.6. 索引代价估算函数 #

amcostestimate 函数会收到描述一次可能的 索引扫描的信息,包括已被确定为可与该索引一起使用的 WHERE 和 ORDER BY 子句的列表。 它必须返回访问索引的代价的估算以及 WHERE 子句的选择性 (即在索引扫描期间将被取回的父表行的比例)。 对简单情形,代价估算器几乎全部工作都可以通过调用优化器中的标准 例程完成;拥有 amcostestimate 函数的意义在于允许索引访问 方法提供索引类型特定的知识,以防有可能改进标准估算。

每个 amcostestimate 函数必须具有签名:

void
amcostestimate (PlannerInfo *root,
                IndexPath *path,
                double loop_count,
                Cost *indexStartupCost,
                Cost *indexTotalCost,
                Selectivity *indexSelectivity,
                double *indexCorrelation,
                double *indexPages);

前三个参数是输入:

root

规划器关于正在处理的查询的信息。

path

正在考虑的索引访问路径。 除代价和选择性值之外的所有字段都有效。

loop_count

应当计入代价估算的索引扫描重复次数。 当考虑用于嵌套循环连接内侧的参数化扫描时,这通常大于一。 注意,代价估算仍应针对仅仅一次扫描; 较大的 loop_count 意味着考虑跨多次扫描 的某些缓存效应可能是合适的。

后五个参数是按引用传递的输出:

*indexStartupCost

设为索引启动处理的代价

*indexTotalCost

设为索引处理的总代价

*indexSelectivity

设为索引选择性

*indexCorrelation

设为索引扫描顺序与底层表顺序之间的相关系数

*indexPages

设为索引叶页面数量

注意,代价估算函数必须用 C 编写,不能用 SQL 或任何可用的过程 语言编写,因为它们必须访问规划器/优化器的内部数据结构。

索引访问代价应当使用 src/backend/optimizer/path/costsize.c 所用的参数计算:一次顺序磁盘块取回的代价为 seq_page_cost,一次非顺序取回的代价为 random_page_cost,处理一个索引行的代价通常应 取为 cpu_index_tuple_cost。 此外,对索引处理期间调用的任何比较操作符 (尤其是 indexqual 本身的求值),应当收取 cpu_operator_cost 的适当倍数。

访问代价应当包括与扫描索引本身相关的所有磁盘和 CPU 代价,但 不包括取回或处理由索引标识的父表行的 代价。

“启动代价”是总扫描代价中在我们能够开始取回第一行 之前必须支出的部分。 对大多数索引这可以取为零,但启动代价高的索引类型可能想把它设为 非零。

indexSelectivity 应当被设为在索引扫描期间 将被取回的父表行的估算比例。 对有损查询,这通常会高于实际通过给定限定条件的行的比例。

indexCorrelation 应当被设为索引顺序与表 顺序之间的相关性(介于 -1.0 和 1.0 之间)。 它用于调整从父表取回行的代价估算。

indexPages 应当被设为叶页面的数量。 它用于估算并行索引扫描的工作进程数量。

当 loop_count 大于一时,返回的数字应当是 该索引任何一次扫描的期望平均值。

Cost Estimation

一个典型的代价估算器会按如下步骤进行:

  1. 基于给定的限定条件估算并返回将被访问的父表行的比例。 在没有任何索引类型特定知识的情况下,使用标准优化器函数 clauselist_selectivity():

    *indexSelectivity = clauselist_selectivity(root, path->indexquals,
                                               path->indexinfo->rel->relid,
                                               JOIN_INNER, NULL);
    

  2. 估算扫描期间将被访问的索引行数。 对许多索引类型,这等于 indexSelectivity 乘以索引中的行数, 但它可能更多。 (注意,索引以页面和行计的大小可以从 path->indexinfo 结构获得。)

  3. 估算扫描期间将被取回的索引页面数量。 这可能只是 indexSelectivity 乘以索引以页面计的 大小。

  4. 计算索引访问代价。 一个通用的估算器可能会这样做:

    /*
     * Our generic assumption is that the index pages will be read
     * sequentially, so they cost seq_page_cost each, not random_page_cost.
     * Also, we charge for evaluation of the indexquals at each index row.
     * All the costs are assumed to be paid incrementally during the scan.
     */
    cost_qual_eval(&index_qual_cost, path->indexquals, root);
    *indexStartupCost = index_qual_cost.startup;
    *indexTotalCost = seq_page_cost * numIndexPages +
        (cpu_index_tuple_cost + index_qual_cost.per_tuple) * numIndexTuples;
    

    不过,上面的做法没有考虑把索引读取摊销到重复的索引扫描 之上。

  5. 估算索引相关性。 对单字段上的简单有序索引,可以从 pg_statistic 获取。 如果相关性未知,保守的估算值是零(无相关性)。

代价估算函数的示例可以在 src/backend/utils/adt/selfuncs.c 中找到。