9.1第51章

From PostgreSQL wiki
Jump to navigationJump to search

本章定义 PostgreSQL 核心系统和 索引访问方法之间的接口, 后者管理独立的索引类型。除了在这里声明的东西之外,核心系统对索引一无所知, 因此我们可以通过书写累加上来的代码,开发一种完全新的索引类型。

PostgreSQL 里的所有索引技术上都叫做从属索引; 也就是说,索引在物理上是与它描述的表文件想分离的。每个索引是以其自己的物理关系的方式存储的, 因此它们也在 pg_class 表里面有记录描述。 一个索引的内容是完全在其索引访问方法的控制之下的。实际上,所有索引访问方法都把索引分裂成标准大小的页面, 这样他们就可以使用普通的存储管理器和缓冲区管理器来访问索引的内容了。 (所有现有的索引访问方法更是使用 Section 50.3 里面描述的标准的页面布局, 并且索引元组头都使用同样的格式;但是这些东西都不是强制访问方法执行的。(意思说必要的话你可以不用这些标准格式。))

索引实际上是一些数据的键值与元组标识符(或者说 TIDs)之间的映射, 这些元组标识符是索引的父表中的行版本(元组)的标识。 一个 TID 由一个块号和一个改块内的项编号组成(参阅 Section 50.3)。 这些就是从该表中抓取某个特定行版本的足够的信息。 索引并不直接知道在 MVCC 下,同一个逻辑行可能有多个现存的版本; 对于索引而言,每个元组都是一个独立的对象,都需要自己的索引条目。 因此,对一行的更新总是为该行创建全新的索引条目, 即使键值没有改变也如此。已经废弃的元组的索引条目是在废弃元组自己被回收的时候回收(通过清理)。 48.1. 索引的系统表记录

每个索引访问方法都在系统表 pg_am 里面用一行来描述(参阅 Section 42.3)。 一个 pg_am 行的主要内容是引用 pg_proc 里面的记录,用来标识索引访问方法提供的索引访问函数。 这些函数的接口(API)在本章后面描述。另外,pg_am 的数据行生命了几个索引访问方法的固定属性, 比如,它是否支持多字段索引。 目前还没有创建、删除 pg_am 记录的特殊支持; 任何想写这么一个新的访问方法的人都需要能够自己向这个表里面插入合适的新行。

要想有真正用处,一个索引访问方法还必须有一个或多个操作符表, 定义在 pg_opclass, pg_amop,和 pg_amproc 里面。 这些记录允许规划器判断哪些查询的条件可以适用于用这个索引访问方法创建的索引。 操作符表在 Section 32.14 里面定义,是读取本章的前提之一。

一个独立的索引是由一行 pg_class 记录以物理关系的方式描述的,加上一个 pg_index 行,表示该索引的逻辑内容 — 也就是说,它所拥有的索引字段集,以及被相关的操作符表捕获的这些字段的语义。 索引字段(键值)可以是下层表的字段,也可以是该表的数据哈工上的表达式。 索引访问方法通常不关心索引的键值来自何妨(它总是操作预处理完毕的键值), 但是它会对 pg_index 里面的操作符表信息很感兴趣。 所有这些表记录都可以当作 Relation 数据结构的一部分访问, 这个数据结构会在对该索引的所有操作上都传递到对应的函数中。

pg_am 中的有些标志字段的含义并不那么直观。 amcanunique 的需求在 Section 48.5 里讨论, 那些 amconcurrent 相关的在 Section 48.4 里。 amcanmulticol 标志断言该索引访问方法支持多字段索引, amoptionalkey 断言它允许对那种在第一个索引字段上没有给出可索引限制子句的扫描。 如果 amcanmulticol 为假,amoptionalkey 实际上说的是该访问方法是否允许不带限制子句的全索引扫描。 那些支持多字段索引的访问访法必须支持那些在省略了除第一个字段以外的其他字段的约束的扫描; 不过,系统允许这些访问访法要求在第一个字段上出现一些限制,这一点是通过把 amoptionalkey 设置为假来实现的。 amindexnulls 断言该索引记录是为 NULL 键值创建的。 因为大多数可以索引的操作符都是严格的,因此不能对 NULL 输入返回 TRUE, 所以,第一眼看见会觉得不为空值存储索引记录的想法很吸引人: 因为他们不可能被一个索引扫描返回。不过,这个想法在一个给出的索引字段上没有限制子句的索引扫描的情况下就不行了; 这样的扫描应该包括空值行。实际上,这意味着设置了amorderstrategy为真的索引必须索引空值, 因为规划器可能会决定在根本没有扫描键字的时候使用这样的索引。这样的索引必须可以在完全没有扫描键字的情况下运行。 另外一个限制是一个支持多字段索引的索引访问方法必须支持第一个字段后面的字段的空值的索引, 因为规划器会认为这个索引可以用于那些没有限制这些字段的查询。比如,假设我们有个索引在 (a,b) 上, 而一个查询的条件是 WHERE a = 4。 系统会认为这个索引可以用于扫描 a = 4 的数据行, 如果索引忽略了 b 为空的数据行,那么就是错误的。 不过,如果第一个索引字段值是空,那么忽略它是 OK 的。(GiST 目前就这么干。)因此, 只是在索引访问方法索引了所有行,包括任意空值的组合,之后,amindexnulls 才可以设置为真值。

48.2. 索引访问方法函数

索引访问方法必须提供的索引构造和维护函数有:

void ambuild (Relation heapRelation,

        Relation indexRelation,
        IndexInfo *indexInfo);

创建一个新索引。索引关系已经物理上创建好了,但是是空的。 必须用索引访问方法要求的固定数据填充它,还有就是所有已经在表里的元组。 通常,ambuild 函数会调用 IndexBuildHeapScan() 扫描该表以获取现有元组并计算需要插入索引的键字。

bool aminsert (Relation indexRelation,

         Datum *values,
         bool *isnull,
         ItemPointer heap_tid,
         Relation heapRelation,
         bool check_uniqueness);

向现有索引插入一个新元组。values 和 isnull 数组给出需要制作索引的键字值, 而 heap_tid 是要被索引的 TID。 如果该访问方法支持唯一索引(它的 pg_am.amcanunique 标志是真), 那么 check_uniqueness 可以是真,在这种情况下,该索引访问方法必须校验表中不存在冲突的行; 通常这是该索引访问方法会需要 heapRelation 参数的唯一的情况。 参阅 Section 48.5 获取细节。 如果插入了索引记录,则返回 TRUE,否则返回 FALSE。 (FALSE 结果并不表明发生了错误,只是用于类似一种索引访问方法(AM)拒绝给 NULL 建索引或者类似的场合。)

IndexBulkDeleteResult * ambulkdelete (Relation indexRelation,

             IndexBulkDeleteCallback callback,
             void *callback_state);

从索引中删除元组。这是一个"大批删除"的操作, 通常都是通过扫描整个索引,检查每条记录,看看它是否需要被删除来实现的。 我们可以调用传递进来的 callback 函数,调用风格是: callback(TID, callback_state) returns bool, 其作用是判断某个用其引用的 TID 标识的索引条目是否需要删除。 必须返回 NULL 或者是一个 palloc 出来的,包含删除操作之效果的统计的结构。

IndexBulkDeleteResult * amvacuumcleanup (Relation indexRelation,

                IndexVacuumCleanupInfo *info,
                IndexBulkDeleteResult *stats);

在一个 VACUUM 操作(一个或多个 ambulkdelete 调用)之后清理。 一个索引访问方法不一定非要提供这个函数(如果提供了,那么在 pg_am 里的记录必须为零)。 如果提供了这个函数,它通常用于批量清理,比如说回收空的索引页面。 info 提供了一些额外的参数,比如用于统计报告的消息级别, 而 stats 是最后的 ambulkdelete 调用返回的东西。 amvacuumcleanup 可以在返回这个结构之前替换或者修改它。 如果结果不是 NULL,那么它必须是一个 palloc 出来的结构。 如果给出了 VERBOSE,那么其包含的统计信息将被 VACUUM 报告。

索引的目的当然是支持那些包含一个可以索引的 WHERE 条件之元组的扫描, 这个条件通常叫修饰词或者扫描键字。 索引扫描的语义在下面的 Section 48.3 里面有更完整的描述。 一个索引访问方法必须提供的与扫描有关的函数有:

IndexScanDesc ambeginscan (Relation indexRelation,

            int nkeys,
            ScanKey key);

开始一个新的扫描。key 数组(长度是 nkeys)为该索引扫描描述索引键字(可能是多个)。 结果必须是一个 palloc 出来的结构。由于实现的原因,索引访问方法必须通过调用 RelationGetIndexScan() 来创建这个结构。 在大多数情况下,ambeginscan 本身除了调用上面这个函数之外几乎不干别的事情; 索引扫描启动时的有趣部分在 amrescan 里。

boolean amgettuple (IndexScanDesc scan,

           ScanDirection direction);

在给出的扫描里抓取下一个元组,向给出的方向移动(在索引里向前或者向后)。 如果抓取到了元组,则返回 TRUE,如果没有抓到匹配的元组,返回 FALSE。 在为 TRUE 的时候,该元组的 TID 存储在 scan 结构里。 请注意"成功"只是意味着索引包含一个匹配扫描键字的条目, 并不是说该元组仍然在堆中存在,或者是能够通过调用着的快照检查(译注:MVCC 快照,用于判断事务边界内的元组可视性)。

boolean amgetmulti (IndexScanDesc scan,

           ItemPointer tids,
           int32 max_tids,
           int32 *returned_tids);

在给出的扫描里抓取多个元组。如果扫描需要继续,则返回 TRUE,如果没有剩下的匹配元组,返回 FALSE。 tids 指向一个调用着提供的 max_tids 条 ItemPointerData 记录的数组, 用于填充匹配元组的 TID。*returned_tids 设置为实际返回的 TID 的数目。 这个数目可以小于 max_tids,或者甚至是零,即使返回值是 TRUE 也如此。 (这样的设计就允许访问方法可以选择对其扫描的最高效的停止点,比如,在索引页的边界上。) amgetmulti 和 amgettuple 不能在同义词索引扫描中使用; 在使用 amgetmulti 的时候还有其它限制,在 Section 48.3 里给出解释。

void amrescan (IndexScanDesc scan,

         ScanKey key);

重启开始给出的扫描,可能使用的是一个新的扫描键字(要想继续使用原来的键字, 给 key 传递一个 NULL)。请注意,我们不可能改变键字的个数。 实际上这个重新开始的特性是在一个嵌套循环连接选取了一个新的外层元组,因此需要一个新的键字比较值, 但扫描键字的结构仍然相同的时候使用的。 这个函数也被 RelationGetIndexScan() 调用, 因此这个函数既用于索引扫描的初始化设置,也用于重复扫描。

void amendscan (IndexScanDesc scan);

结束扫描并释放资源。不应该释放 scan 本身, 但访问方法内部使用的任何锁或者销都应该释放。

void ammarkpos (IndexScanDesc scan);

标记当前扫描位置。访问方法只需要支持每次扫描里面有一个被记住的扫描位置。

void amrestrpos (IndexScanDesc scan);

把扫描恢复到最近标记的位置。

void amcostestimate (PlannerInfo *root,

               IndexOptInfo *index,
               List *indexQuals,
               Cost *indexStartupCost,
               Cost *indexTotalCost,
               Selectivity *indexSelectivity,
               double *indexCorrelation);

估计一个索引扫描的开销。这个函数在下面的 Section 48.6 里详细描述。

通常,任何索引访问方法函数的 pg_proc 记录都应该显示正确数目的参数, 只是把类型都声明为类型 internal (因为大多数参数的类型都是 SQL 不识别的类型, 并且我们不希望用户直接调用该函数)。返回类型根据具体情况声明为 void, internal,或者 boolean。

48.3. 索引扫描

在一个索引扫描里,索引访问方法负责把它拿到的那些据说匹配扫描键字的所有元组之 TID 的回流。访问方法不会卷入从索引的父表中实际抓取这些元组的动作中, 也不会判断他们是否通过了扫描的时间条件测试或者是其它条件。

一个扫描键字是形如 index_key operator constant 的 WHERE 子句的内部表现形式,这里的索引键字是索引中的一个字段, 而操作符是和该索引字段相关联的操作符表的一个成员。一个索引扫描拥有零个或者多个扫描键字, 他们是隐含着 AND 的关系 — 返回的元组被认为是满所所有列出的条件的元组。

操作符表可能会指出改索引对于某些特定的操作符是有损耗的; 这就暗示着该索引扫描会返回所有通过扫描键字的条目,加上一些可能没通过扫描键字的条目。 核心系统的索引扫描机制然后就会再次在堆元组上使用该操作符,以校验这些条目是否真正应该选取。 对于无损耗的操作符,索引扫描必须返回全部匹配的条目,不需要重复校验。

请注意,确保找到所有条目以及确保所有条目都通过给出的扫描键字的条件完全是访问方法的责任。 还有,核心系统将只是简单的吧所有匹配扫描键字和操作符表的 WHERE 子句传递过来, 而不会做任何语义分析,以判断他们是否冗余或者是否相互矛盾。 举例来说,给出 WHERE x > 4 AND x > 14,这里的 x 是一个 b-tree 索引字段,那么把第一个扫描键字识别成冗余的和可抛弃的工作是 b-tree amrescan 函数的事。amrescan 过程中所需要的预处理的范围将由索引访问方法把扫描键字缩减为一个"正常"形式的具体需要而定。

amgettuple 函数有一个 direction 参数, 它可以是 ForwardScanDirection(正常情况)或者 BackwardScanDirection。 如果 amrescan 之后的第一次调用声明 BackwardScanDirection, 那么匹配条件的索引记录集是从后向前扫描的,而不是通常的从前向后扫描, 因此 amgettuple 必须返回索引中最后的匹配元组,而不是通常情况下的第一条。 (这些事情只会是那些设置了 pg_am.amorderstrategy 为非零的, 号称自己支持排序扫描的访问方法上会发生。)在第一次调用之后,amgettuple 必须准备从最近返回的条目的位置开始,在两个方向上进行扫描步进。

访问方法必须支持在扫描里"标记"一个位置并且在后面的操作中返回到标记的位置。 同样的位置可能会被回复好几次。不过,每次扫描只需要记住一个位置; 新的 ammarkpos 调用覆盖前面标记的位置。

扫描位置和标记位置(如果存在)都必须在面对索引中存在并发插入和删除的时候保持一致性。 如果一条并发新插入的记录并未被一次扫描返回(而如果扫描开始的时候该记录存在,则会被返回), 或者说扫描通过重新扫描或者回头扫描返回这样的记录 — 即使它第一次跑的时候没有返回这样的行, 对于系统来说,这些情况都是可以接受的。 类似的还有,一个并发的删除可以反映,也可以不反应一个扫描的结果。 重要的是,插入或者删除不会导致扫描会略过或者重复返回本身不是被插入或者删除的条目。 (对于没有设置 pg_am.amconcurrent 的索引类型, 只要在处理进行扫描的同个后端里的这些插入和删除动作就足够了。 但是如果 amconcurrent 为真,那么就必须处理其它后端进行的插入或者删除。)

除了使用 amgettuple,索引扫描页可以用 amgetmulti, 每次调用抓取多条元组的方式完成。这样做可能会比 amgettuple 有显著的效率提升, 因为它可以避免在访问方法内的加锁/解锁的循环。在原理上,amgetmulti 应该和重复调用 amgettuple 的效果相同,不过我们强制了一些限制来简化事情。 首先,amgetmulti 并不接受 direction 参数, 因此它不支持反向扫描,页不支持内部扫描方向的回向。 访问方法也不需要支持在 amgetmulti 扫描中扫描位置的标记以及恢复。 (这些限制几乎没啥开销,因为在 amgetmulti 扫描里使用这些特性是很困难的: 调整调用着的 TID 缓冲列表是一件很复杂的事情。) 最后,amgetmulti 并不保证在返回的元组上的任何锁定, 这就暗示着 Section 48.4 里面的事情。

48.4. 索引锁的考量

一个索引访问方法可以选择它是否支持多个进程对索引的并发更新。 如果该方法的 pg_am.amconcurrent 标志为真, 那么在索引扫描期间,PostgreSQL 核心系统在索引上抓取 AccessShareLock,以及在更新索引期间抓取 RowExclusiveLock。 因为这些锁类型不会冲突,所以访问方法有责任处理任何它自己需要的更细致的锁需求。 把整个索引锁住的排他锁只是在创建索引,删除索引,或者 REINDEX 的时候使用。 如果 amconcurrent 是假,PostgreSQL 仍然在索引扫描的时候抓取 AccessShareLock,但是它在任何更新的时候都抓取 AccessExclusiveLock。 请注意这样就隐含着索引扫描是只读的假设;一个可能在扫描期间修改索引的访问方法仍将需要并发扫描情况下自己实现锁机制。

要记住,一个后端自己的锁是不会自相冲突的;因此, 即使一个非并发的索引类型也必须准备处理后端在自己扫描某个索引的同时插入或者删除其中的条目的情况。 (这当然是支持一个使用该索引找到需要更新的行之 UPDATE 的必要条件。)

制作一个支持并发更新的索引类型通常要求对所需的行为进行广泛的并且细致的分析。 对于 b-tree 和散列索引类型,你可以读取在 src/backend/access/nbtree/README 和 src/backend/access/hash/README 里面描述的设计决策。

除了索引自己内部的一致性要求之外,并发更新创建了一些有关父表(堆) 和索引之间的一致性问题。因为 PostgreSQL 是把堆的访问和更新与索引的访问和更新分开的。 我们用下面的规则处理这样的问题:

   *
     在制作一行的索引记录之前,先做堆记录。 (因此并发的索引扫描很可能看不到堆记录。这么做应该是 OK 的,因为索引读者应该堆为提交的行不感兴趣。 不过需要看看 Section 48.5。)
   *
     如果一条堆记录要被删除(由 VACUUM), 所有其索引记录都必须首先删除。
   *
     对于并发索引类型,一次索引扫描必须在保存有 amgettuple 最后返回的索引页面上维护一个销, 而 ambulkdelete 不能删除其它后端用销固定的索引页面里面的记录。 为何需要这条规则在下面解释。 

如果一个索引是并发的,那么一个索引读者是可能在一条索引记录被 VACUUM 删除之前看到它的,然后在 VACUUM 删除它之后找到其对应的堆记录。 (对于非并发索引,这种情况是不可能的,因为索引级别的锁冲突会消除这种情况。) 如果读者到达该项时,该项编号仍然没有使用,那么这种情况不会导致严重的问题, 因为空的项槽位会被 heap_fetch() 忽略。但是如果第三个后端已经为其它什么东西复用了这个项槽位又如何? 如果使用 MVCC 兼容的快照,那么就不会有问题,因为新占据的槽位当然是太新了, 因而无法通过快照测试。但是,对于非 MVCC 兼容的快照(比如 SnapshotNow), 那么就有可能接受并返回一个实际上并不匹配扫描键字的行。 我们可以通过要求扫描键字在所有场合下都重新检查的方法来避免这种情况,但是这种方法开销太大了。 取而代之的是,我们通过在索引页面上使用一个销,当作一个代理,告诉系统说, 读者可能还在对应堆记录的索引记录上空"飞行"。 在这样的销上面进行 ambulkdelete 块确保 VACUUM 无法在读者完成读取之前删除堆记录。 这种解法增加了一点点运行时开销,而只是在非常罕见的实际有冲突的情况下才导致阻塞开销。

这个解决方法要求索引扫描必须是"同步的":我们必须在扫描完对应的索引记录之后马上抓取每个堆记录。 这样的方案开销比较大,原因有若干个。而"异步的"扫描,可以先从索引里收集很多 TID, 然后再稍后的某时访问堆元组,这样就会烧很多索引锁的开销,以及可以允许更有效的堆访问模式。 但是按照上面的分析,我们在非 MFCC 兼容单块着上必须使用同步方法,而对使用 MVCC 快照的查询, 使用异步扫描应该是可以实现的。

在 amgetmulti 索引扫描里,访问方法不需要保证在任何返回的元组上保持一个销。 (毕竟,除了给最后一个元组加销之外,也没法给其它的加。) 因此,我们只能在 MVCC 兼容的快照里使用这样的扫描。

48.5. 索引唯一性检查

PostgreSQL 使用 唯一索引来强制 SQL 唯一约束, 唯一索引实际上是不允许多条记录有相同键值的的索引。 一个支持这个特性的访问方法要设置 pg_am.amcanunique 为真。 (目前,只有 b-tree 支持它。)

因为 MVCC,我们必须允许重复的条目物理上存在于索引之中:该条目可能指向某个逻辑行的后面的版本。 我们实际想强制的行为是,任何 MVCC 快照都不能包含两条相同的索引键字。 这种要求在向一个唯一索引插入新行的时候分解成下面的几种情况:

   *
     如果一个有冲突的合法行被当前事务删除,这是可以的。(特别是因为一个 UPDATE 总是在插入新版本之前删除旧版本, 这样就允许一个行上的 UPDATE 不用改变键字进行操作。)
   *
     如果一个在等待提交的事务插入了一行有冲突的数据,那么准备插入数据的事务必须等待看看改事务是否提交。 如果该事务回滚,那么就没有冲突。如果它没有删除冲突行然后提交,那么就有一个唯一性违反。 (实际上我们只是等待另外那个事务结束,然后在程序里重做可视性检查。)
   *
     类似的,如果一个有冲突的有效行被一个准备提交的事务删除, 那么另外一个准备提交的插入事务必须等待该事务提交或者退出,然后重做测试。 

我们要求索引访问方法自己进行这些测试,这就意味着它必须检查堆, 以便查看那些根据索引内容表明有重复键字的任意行的提交状态。 这样做毫无疑问地很难看并且也不是模块化的,但是这样可以节约重复的工作: 如果我们进行额外的一次探测,而后面的索引查找冲突行的的动作实际上是和查找插入新行的索引记录重复的动作。 并且,我们没有很显然的方法来避免冲突条件,除非冲突检查是插入新索引条目的整体动作的一部分。

这个方法的主要的局限是没有很方便的方法支持推迟的唯一性检查。

48.6. 索引开销估计函数

系统给 amcostestimate 函数一个 WHERE 子句的列表,这个 WHERE 子句列表是系统认为可以被索引使用的东西。 它必须返回访问该索引的开销估计值以及 WHERE 子句的选择性(也就是说, 在索引扫描期间检索的将被返回的数据行在父表中所占据的比例)。 对于简单的场合,几乎开销估计器的所有工作都可以通过调用优化器里面的标准过程完成; 有 amcostestimate 这个函数的目的是允许索引访问方法提供和索引类型相关的知识, 这样也许可以改进标准的开销估计。

每个 amcostestimate 函数都有下面这样的签名:

void amcostestimate (PlannerInfo *root,

               IndexOptInfo *index,
               List *indexQuals,
               Cost *indexStartupCost,
               Cost *indexTotalCost,
               Selectivity *indexSelectivity,
               double *indexCorrelation);

头四个参数是输入:

root

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

index

   在考虑使用的索引。 

indexQuals

   索引条件子句的列表(隐含是 AND 的); 如果是 NIL 列表(空列表)就表示没有可用的条件。 请注意这个列表包含表达式树,而不是 ScanKey (扫描键字)。 

后面四个参数是传递引用的输出:

  • indexStartupCost
   设置为索引启动处理的开销 
  • indexTotalCost
   设置为索引处理的总开销 
  • indexSelectivity
   设置为索引的选择型 
  • indexCorrelation
   设置为索引扫描顺序和下层的表的顺序之间的相关有效性 

请注意开销估计函数必须用 C 写,而不能用 SQL 或者任何可用的存储过程语言, 因为它们必须访问规划器/优化器的内部数据结构。

索引访问开销应该以 src/backend/optimizer/path/costsize.c 使用的单位进行计算: 一个顺序磁盘块抓取开销是 1.0,一个非顺序抓取开销是 random_page_cost, 而处理一个索引行的开销通常应该取做 cpu_index_tuple_cost。 另外,在任何索引处理期间调用的比较操作符,都应该增加一个数量为 cpu_operator_cost 倍数的开销 (特别是计算索引条件/indexQuals自己的时候)。

访问开销应该包括所有与扫描索引本身相关的磁盘和 CPU 开销, 但是不包括检索或者处理索引标识出来的父表的行的开销。

"启动开销"是总扫描开销中的这样一部分:我们在开始抓取第一行之前,必须花掉的开销。 对于大多数索引,这个可以是零,但是那些启动开销很大的索引类型可能不能把它设置为零。

indexSelectivity 应该设置成在索引扫描期间,父表中的行被选出出来的部分的百分比。 在索引比较松散的情况下,这个值通常比实际通过给出的查询条件之行所占的百分比要高。

indexCorrelation 应该设置成索引顺序和表顺序之间的相关性(范围在 -1.0 到 1.0 之间)。 这个数值用于调整从父表中抓取行的开销估计。

开销估计

一个典型的开销估计器会像下面这样进行处理:

  1.
     基于给出的查询条件,估计并返回父表中将被访问的行的百分比。 如果缺乏索引类型相关得知识,那么使用标准的优化器函数 clauselist_selectivity():
     *indexSelectivity = clauselist_selectivity(root, indexQuals,
                                                index->rel->relid, JOIN_INNER);
  2.
     估计在扫描过程中将被访问的索引行数。对于许多索引类型,这个等于 indexSelectivity 乘以索引中的行数, 但是可能更多。(请注意,页面中的索引尺寸和行数可以从 IndexOptInfo 结构中获得。)
  3.
     估计在扫描中将检索的索引页面数量。这个可能就是 indexSelectivity 乘以索引的总页面数。
  4.
     计算索引访问开销。一个通用的估计器可以这么干:
         /*
          * Our generic assumption is that the index pages will be read
          * sequentially, so they have cost 1.0 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.
          * 一般来说,我们假设索引页面都会顺序读取,所以它们每个的开销是 1.0,而不是 random_page_cost。
          * 同样,我们应该给每次索引行的 indexquals 计算开销。
          * 所有开销都被认为是在扫描过程中逐步增付的。
          */
         cost_qual_eval(&index_qual_cost, indexQuals);
         *indexStartupCost = index_qual_cost.startup;
         *indexTotalCost = numIndexPages +
             (cpu_index_tuple_cost + index_qual_cost.per_tuple) * numIndexTuples;
  5.
     估计索引的相关性。对于简单的在单个字段上的有序索引,这个值可以从 pg_statistic 中检索。如果相关性是未知,那么保守的估计是零(没有相关性)。 

开销估计器函数的例子可以在 src/backend/utils/adt/selfuncs.c 里面找到。