受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

62.2. 索引访问方法函数 #

索引访问方法必须在 IndexAmRoutine 中提供如下索引构建和维护函数:

IndexBuildResult *
ambuild (Relation heapRelation,
         Relation indexRelation,
         IndexInfo *indexInfo);

构建一个新索引。索引关系已经在物理上创建,但还是空的。它必须填入访问方法所需的任何固定数据,以及表中所有现有元组对应的条目。通常,ambuild 函数会调用 table_index_build_scan() 来扫描表中已有的元组,并计算需要插入索引的键值。该函数必须返回一个通过 palloc 分配的结构体,其中包含有关新索引的统计信息。amcanbuildparallel 标志指示该访问方法是否支持并行索引构建。设为 true 时,系统会尝试为构建分配并行工作进程。只支持非并行索引构建的访问方法应把该标志保留为 false

void
ambuildempty (Relation indexRelation);

构建一个空索引,并将其写入给定关系的初始化分支(INIT_FORKNUM)。只有不记日志的索引才会调用此方法;写入初始化分支的空索引会在每次服务器重启时复制到主关系分支上。

bool
aminsert (Relation indexRelation,
          Datum *values,
          bool *isnull,
          ItemPointer heap_tid,
          Relation heapRelation,
          IndexUniqueCheck checkUnique,
          bool indexUnchanged,
          IndexInfo *indexInfo);

向现有索引中插入一个新元组。valuesisnull 数组给出要建立索引的键值,而 heap_tid 是要建立索引的 TID。如果该访问方法支持唯一索引(其 amcanunique 标志为真),那么 checkUnique 指示要执行的唯一性检查类型。这取决于唯一约束是否可延迟;详见 Section 62.5。通常,访问方法只会在执行唯一性检查时需要 heapRelation 参数(因为这时它必须到堆中核实元组是否仍然存活)。

indexUnchanged 布尔值提供了关于待索引元组性质的提示。当它为真时,该元组是索引中某个现有元组的副本。这个新元组是一个逻辑上未改变的后继 MVCC 元组版本。当执行一次 UPDATE,且它不修改该索引覆盖的任何列、但仍然需要在索引中创建新版本时,就会出现这种情况。索引 AM 可以利用这个提示,在同一逻辑行的许多版本积聚的索引区域决定是否应用自底向上的索引删除。请注意,更新非键列或只出现在部分索引谓词中的列,并不会影响 indexUnchanged 的值。核心代码使用一种低开销方法来确定每个元组的 indexUnchanged 值,这种方法允许出现误报和漏报。索引 AM 不得把 indexUnchanged 视为关于元组可见性或版本情况的权威信息来源。

该函数返回的布尔值只有在 checkUniqueUNIQUE_CHECK_PARTIAL 时才有意义。在这种情况下,返回真表示新条目已知是唯一的,而返回假表示它可能不是唯一的(因此必须安排一次延迟的唯一性重检)。对于其他情况,建议恒定返回假。

某些索引可能不会为所有元组建立索引。如果某个元组不应被索引,aminsert 应当直接返回而不做任何事。

如果索引 AM 希望在一条 SQL 语句中的连续索引插入之间缓存数据,它可以在 indexInfo->ii_Context 中分配空间,并把数据指针存入 indexInfo->ii_AmCache(该指针初始为 NULL)。如果在索引插入后需要释放内存之外的资源,可以提供 aminsertcleanup,它会在内存释放之前被调用。

void
aminsertcleanup (Relation indexRelation,
                 IndexInfo *indexInfo);

清理由 indexInfo->ii_AmCache 在连续插入之间维护的状态。如果这些数据需要额外的清理步骤(例如释放已钉住的缓冲区),而仅释放内存还不够,那么这个函数就很有用。

IndexBulkDeleteResult *
ambulkdelete (IndexVacuumInfo *info,
              IndexBulkDeleteResult *stats,
              IndexBulkDeleteCallback callback,
              void *callback_state);

从索引中删除元组。这是一个批量删除操作,通常应通过扫描整个索引并检查每个条目是否应被删除来实现。必须调用传入的 callback 函数,其调用形式为 callback(TID, callback_state) returns bool,以确定由其引用 TID 标识的某个索引条目是否应删除。该函数必须返回 NULL,或者返回一个通过 palloc 分配的结构体,其中包含此次删除操作影响的统计信息。如果不需要向 amvacuumcleanup 传递信息,返回 NULL 也是可以的。

由于 maintenance_work_mem 有限,当待删除的元组很多时,ambulkdelete 可能需要被调用多次。参数 stats 是此前对该索引上一次调用的结果(在一次 VACUUM 操作中的第一次调用时它为 NULL)。这使得 AM 可以在整个操作过程中累积统计信息。通常,如果传入的 stats 非 NULL,ambulkdelete 会修改并返回同一个结构体。

IndexBulkDeleteResult *
amvacuumcleanup (IndexVacuumInfo *info,
                 IndexBulkDeleteResult *stats);

在一次 VACUUM 操作(零次或多次 ambulkdelete 调用)之后执行清理。它不一定要做返回索引统计信息之外的事情,但也可能执行批量清理,例如回收空索引页面。stats 是最后一次 ambulkdelete 调用返回的结果;如果因为没有需要删除的元组而没有调用 ambulkdelete,则为 NULL。如果结果不是 NULL,它必须是一个通过 palloc 分配的结构体。其中的统计信息将用于更新 pg_class,并在指定了 VERBOSE 时由 VACUUM 报告。如果索引在整个 VACUUM 操作期间完全没有变化,返回 NULL 也是可以的;否则应返回正确的统计信息。

amvacuumcleanup 也会在 ANALYZE 操作完成时被调用。在这种情况下,stats 总是 NULL,而且任何返回值都会被忽略。这种情况可以通过检查 info->analyze_only 来区分。我们建议访问方法在这种调用中除了做插入后的清理外什么都不做,而且只在自动清理工作进程中这样做。

bool
amcanreturn (Relation indexRelation, int attno);

检查索引是否能够在给定列上支持 仅索引扫描,做法是返回该列原始的被索引值。属性编号从 1 开始,也就是说第一列的 attno 为 1。若支持则返回真,否则返回假。如果支持包含列,这个函数应始终对包含列返回真,因为不能取回的包含列几乎没有意义。如果访问方法根本不支持仅索引扫描,那么它的 IndexAmRoutine 结构中的 amcanreturn 字段可以设为 NULL。

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

估计一次索引扫描的开销。该函数将在后面的 Section 62.6 中详细讨论。

int
amgettreeheight (Relation rel);

计算树形索引的高度。这一信息会通过 path->indexinfo->tree_height 提供给 amcostestimate 函数,可用于支持开销估计。其结果不会在其他地方使用,因此这个函数实际上也可以用来计算索引上任何一种适合装入整数、且开销估计函数可能想知道的数据。如果计算代价较高,把结果缓存到 RelationData.rd_amcache 中可能会有用。

bytea *
amoptions (ArrayType *reloptions,
           bool validate);

解析并验证索引的 reloptions 数组。只有当该索引存在非空的 reloptions 数组时才会调用此函数。reloptions 是一个 text 数组,其中的条目形如 name=value。该函数应构造一个 bytea 值,并将其复制到索引 relcache 条目的 rd_options 字段中。这个 bytea 值中的数据内容由访问方法自行定义;大多数标准访问方法使用结构体 StdRdOptions。当 validate 为真时,若存在未识别选项或无效取值,函数应报告合适的错误消息;当 validate 为假时,无效条目应被静默忽略。(当装载已存储在 pg_catalog 中的选项时,validate 为假;此时只有在访问方法改变了选项规则时才可能发现无效条目,而忽略过时条目是合适的。)如果希望采用默认行为,返回 NULL 也是可以的。

bool
amproperty (Oid index_oid, int attno,
            IndexAMProperty prop, const char *propname,
            bool *res, bool *isnull);

amproperty 方法允许索引访问方法覆盖 pg_index_column_has_property 及其相关函数的默认行为。如果访问方法对索引属性查询没有任何特殊行为,那么它的 IndexAmRoutine 结构中的 amproperty 字段可以设为 NULL。否则,调用 pg_indexam_has_property 时,amproperty 方法会收到均为零的 index_oidattno;调用 pg_index_has_property 时,收到有效的 index_oid 和为零的 attno;调用 pg_index_column_has_property 时,则收到有效的 index_oid 和大于零的 attnoprop 是一个枚举值,用来标识当前测试的属性;propname 则是原始属性名字符串。如果核心代码不认识该属性名,那么 prop 会是 AMPROP_UNKNOWN。访问方法可以通过检查 propname 是否匹配来自定义属性名(应使用 pg_strcasecmp 进行匹配,以与核心代码保持一致);对核心代码已知的属性名,最好检查 prop。如果 amproperty 方法返回 true,就表示它已经确定了属性测试结果:它必须设置 *res 为要返回的布尔值,或者把 *isnull 设为 true 以返回 NULL。(这两个引用变量在调用前都会初始化为 false。)如果 amproperty 方法返回 false,核心代码就会按其常规逻辑继续决定属性测试结果。

支持排序操作符的访问方法应当实现 AMPROP_DISTANCE_ORDERABLE 属性测试,因为核心代码不知道如何完成该测试,只会返回 NULL。若实现成本低于打开索引并调用 amcanreturn(这正是核心代码的默认行为),那么实现 AMPROP_RETURNABLE 测试也可能是有利的。对于其他所有标准属性,默认行为应当已经足够。

char *
ambuildphasename (int64 phasenum);

返回给定构建阶段编号的文本名称。阶段编号是在索引构建期间通过 pgstat_progress_update_param 接口报告的,随后阶段名称会出现在 pg_stat_progress_create_index 视图中。

bool
amvalidate (Oid opclassoid);

在访问方法能够合理做到的范围内,为指定操作符类验证系统目录条目。例如,这可能包括检查是否提供了所有必需的支持函数。如果该 opclass 无效,amvalidate 函数必须返回假。发现的问题应通过 ereport 消息报告,通常使用 INFO 级别。

void
amadjustmembers (Oid opfamilyoid,
                 Oid opclassoid,
                 List *operators,
                 List *functions);

在访问方法能够合理做到的范围内,验证拟议新增到某个操作符族中的操作符成员和函数成员;如果默认依赖类型不合适,还要设置它们的依赖类型。该函数会在 CREATE OPERATOR CLASS 期间以及 ALTER OPERATOR FAMILY ADD 期间调用;在后一种情况下,opclassoidInvalidOidList 参数是 OpFamilyMember 结构体的列表,其定义见 amapi.h。此函数所做的检查通常只是 amvalidate 所做检查的一个子集,因为 amadjustmembers 不能假定自己看到的是一整套成员。例如,检查支持函数的签名是合理的,但不适合检查是否提供了所有必需的支持函数。任何问题都可以通过抛出错误来报告。OpFamilyMember 结构体中与依赖相关的字段由核心代码初始化:如果这是 CREATE OPERATOR CLASS,则创建对 opclass 的硬依赖;如果这是 ALTER OPERATOR FAMILY ADD,则创建对 opfamily 的软依赖。如果其他行为更合适,amadjustmembers 可以调整这些字段。例如,GIN、GiST 和 SP-GiST 总是把操作符成员设为对 opfamily 的软依赖,因为在这些索引类型中,操作符和 opclass 之间的联系相对较弱;因此允许操作符成员自由增删是合理的。可选支持函数通常也会被赋予软依赖,以便在必要时能够移除。

索引的目的当然是支持扫描那些匹配可索引 WHERE 条件的元组,这种条件常被称为限定词扫描键。关于索引扫描的语义,将在下面的 Section 62.3 中更详细地说明。一种索引访问方法可以支持普通索引扫描、位图索引扫描,或者两者都支持。索引访问方法必须或可以提供的扫描相关函数如下:

IndexScanDesc
ambeginscan (Relation indexRelation,
             int nkeys,
             int norderbys);

为一次索引扫描做准备。nkeysnorderbys 参数表示扫描中将使用的限定条件和排序操作符数量,这些信息可能有助于空间分配。请注意,此时还没有提供扫描键的实际值。结果必须是一个通过 palloc 分配的结构体。出于实现上的原因,索引访问方法必须通过调用 RelationGetIndexScan() 来创建这个结构体。大多数情况下,ambeginscan 除了做这次调用以及也许获取一些锁之外,不会做太多工作;索引扫描启动中真正有意思的部分在 amrescan 中。

void
amrescan (IndexScanDesc scan,
          ScanKey keys,
          int nkeys,
          ScanKey orderbys,
          int norderbys);

开始或重新开始一次索引扫描,并且可以使用新的扫描键。(若要用之前传入的键重新开始,则为 keys 和/或 orderbys 传递 NULL。)请注意,键的数量或排序操作符的数量都不允许超过传给 ambeginscan 的数量。实践中,这种重启特性通常用于嵌套循环连接选中了新的外层元组,因此需要新的键比较值,但扫描键结构本身保持不变的场景。

bool
amgettuple (IndexScanDesc scan,
            ScanDirection direction);

在给定扫描中取出下一个元组,并按给定方向移动(在索引中向前或向后)。若成功取得元组则返回真;若不再有匹配元组则返回假。在返回真时,元组的 TID 会存入 scan 结构中。请注意,成功仅表示索引中存在一个匹配扫描键的条目,并不表示该元组一定仍然存在于堆中,或者一定能通过调用者的快照测试。成功时,amgettuple 还必须把 scan->xs_recheck 设为真或假。假表示可以确定该索引条目匹配扫描键;真表示这一点并不确定,因此在取到堆元组后必须根据扫描键所代表的条件重新检查它。这一机制支持有损索引操作符。请注意,重检只会扩展到扫描条件;部分索引谓词(如果有)永远不会由 amgettuple 的调用者重新检查。

如果索引支持 仅索引扫描(即其任一列上 amcanreturn 返回真),那么成功时 AM 还必须检查 scan->xs_want_itup;若其为真,就必须返回该索引条目的原始被索引数据。对于 amcanreturn 返回假的列,可以把其值作为 NULL 返回。数据可以通过以下两种形式之一返回:一种是把 IndexTuple 指针存入 scan->xs_itup,其元组描述符为 scan->xs_itupdesc;另一种是把 HeapTuple 指针存入 scan->xs_hitup,其元组描述符为 scan->xs_hitupdesc。(当重建出的数据可能装不进 IndexTuple 时,应使用后一种格式。)无论采用哪种形式,指针所引用数据的管理责任都在访问方法一侧。对这份数据的有效性必须至少保持到该扫描下一次调用 amgettupleamrescanamendscan 为止。

只有当访问方法支持普通索引扫描时,才需要提供 amgettuple 函数。如果不支持,其 IndexAmRoutine 结构中的 amgettuple 字段必须设为 NULL。

int64
amgetbitmap (IndexScanDesc scan,
             TIDBitmap *tbm);

取出给定扫描中的所有元组,并将其加入调用者提供的 TIDBitmap 中(也就是把这组元组 ID 与位图中已有的集合做 OR)。返回值是取得的元组数量(这可能只是近似计数,例如某些 AM 不会检测重复项)。在把元组 ID 插入位图时,amgetbitmap 可以指出某些具体的元组 ID 需要重新检查扫描条件。这类似于 amgettuplexs_recheck 输出参数。注意:在当前实现中,对这一特性的支持与位图自身的有损存储支持混在一起,因此调用者会对可重检元组同时重新检查扫描条件和部分索引谓词(如果有)。不过,这并不一定永远如此。amgetbitmapamgettuple 不能在同一次索引扫描中同时使用;使用 amgetbitmap 时还有其他限制,详见 Section 62.3

只有当访问方法支持位图索引扫描时,才需要提供 amgetbitmap 函数。如果不支持,其 IndexAmRoutine 结构中的 amgetbitmap 字段必须设为 NULL。

void
amendscan (IndexScanDesc scan);

结束一次扫描并释放资源。scan 结构本身不应被释放,但访问方法内部获取的任何锁或 pin,以及由 ambeginscan 和其他扫描相关函数分配的其他内存,都必须被释放。

void
ammarkpos (IndexScanDesc scan);

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

只有当访问方法支持有序扫描时,才需要提供 ammarkpos 函数。如果不支持,其 IndexAmRoutine 结构中的 ammarkpos 字段可以设为 NULL。

void
amrestrpos (IndexScanDesc scan);

将扫描恢复到最近一次标记的位置。

只有当访问方法支持有序扫描时,才需要提供 amrestrpos 函数。如果不支持,其 IndexAmRoutine 结构中的 amrestrpos 字段可以设为 NULL。

除了支持普通索引扫描外,某些类型的索引还可能希望支持并行索引扫描,这允许多个后端协同执行一次索引扫描。索引访问方法应安排好实现方式,使每个参与协作的进程返回普通非并行索引扫描本应返回的元组集合的一个子集,而且这些子集的并集应等于普通非并行索引扫描的结果集。此外,虽然并行扫描返回的元组不必具有全局顺序,但每个协作后端内部返回的那个子集的顺序必须匹配请求的顺序。可以实现下列函数来支持并行索引扫描:

Size
amestimateparallelscan (Relation indexRelation,
                        int nkeys,
                        int norderbys);

估计并返回执行并行扫描时该访问方法所需动态共享内存的字节数。(这个数字是额外需求,并不替代 ParallelIndexScanDescData 中 AM 无关数据所需的空间。)

nkeysnorderbys 参数表示扫描中将使用的限定条件和排序操作符数量;同样的数值也会传给 amrescan。请注意,此时还没有提供扫描键的实际值。

对于不支持并行扫描,或者所需额外存储字节数为零的访问方法,无需实现此函数。

void
aminitparallelscan (void *target);

在并行扫描开始时,会调用这个函数来初始化动态共享内存。target 会指向一块动态共享内存,其大小至少等于此前 amestimateparallelscan 返回的字节数,而该函数可以用这部分空间存储它想保存的任何数据。

对于不支持并行扫描,或者虽需要共享内存但无需初始化的情况,无需实现此函数。

void
amparallelrescan (IndexScanDesc scan);

如果实现了此函数,当并行索引扫描必须重新开始时就会调用它。它应重置由 aminitparallelscan 建立的任何共享状态,以便扫描从头开始重新执行。

CompareType
amtranslatestrategy (StrategyNumber strategy, Oid opfamily, Oid opcintype);

StrategyNumber
amtranslatecmptype (CompareType cmptype, Oid opfamily, Oid opcintype);

如果实现了这些函数,规划器和执行器就会调用它们,在固定的 CompareType 值与访问方法所使用的特定策略编号之间进行转换。提供了与内置 btree 或 hash 访问方法相似功能的访问方法可以实现这些函数;通过实现这些转换,系统就能理解该访问方法操作的语义,并在多个地方把它们当作 btree 或 hash 索引的替代。如果该访问方法的功能与这些内置访问方法并不相似,就不需要实现这些函数。若不实现,访问方法会在某些规划器和执行器决策中被忽略,但除此之外仍然完全可用。

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。