Index Uniqueness Checks
PostgreSQL使用唯一索引来强制SQL唯一性约束,这些索引是禁止使用相同键的多个条目的索引。支持此特征的访问方法设置了amcanunique。(目前,只有B-树支持它。)
由于MVCC,总是需要允许重复的条目在索引中物理存在:条目可能指的是单个逻辑行的连续版本。实际上,我们想要强制执行的是,没有MVCC快照可以包含两个具有相等索引键的行。这会分解为以下情况:当将新行插入到唯一索引时,必须检查以下情况:
- 如果当前事务已删除冲突的有效行,则没关系。(特别是,由于更新总是在插入新版本之前删除旧行版本,这将允许在不改变密钥的情况下对行进行更新。)
- 如果一个冲突的行已被尚未提交的事务插入,则潜在插入者必须等待以查看该事务是否提交。如果它回滚,那么就不会发生冲突。如果它在不删除冲突行的情况下提交,则存在唯一性违反。(实际上,我们只是等待另一个事务结束,然后重做toto中的可见性检查)。
- 类似地,如果一个冲突的有效行已被尚未提交的事务删除,则潜在插入者必须等待该事务提交或中止,然后重复测试。
此外,在根据上述规则报告唯一性违反之前,访问方法必须重新检查插入的行的活跃性。如果提交 dead ,则不应报告违反。(这种情况不能在插入当前事务创建的行的普通场景中发生。然而,它可以在创建唯一索引时发生。
我们需要索引访问方法来应用这些测试本身,这意味着它必须进入堆,以检查根据索引内容显示的具有重复密钥的任何行的提交状态。这无疑是丑陋的和非模块化的,但是它节省了冗余的工作:如果我们做了一个单独的探测,那么对于冲突的行的索引查找将基本上重复,同时找到插入新行的索引条目的位置。更重要的是,除非冲突检查是新索引条目插入的一个组成部分,否则没有明显的方法来避免竞争条件。
如果唯一的约束是可延迟的,则存在额外的复杂性:我们需要能够为新行插入索引条目,但是推迟任何唯一性违反错误直到语句结束或更晚。为了避免索引的不必要的重复搜索,索引访问方法应该在初始插入期间进行初步的唯一性检查。如果这表明绝对没有冲突的元组,我们就完成了。否则,我们会在执行约束时安排重新检查。如果在重新检查时,插入的元组和具有相同键的其他元组都是实况,则必须报告错误。(注意,为此,“Live”实际上意味着“索引条目的热链中的任何元组都是实况”。为了实现这一点,AM镶函数传递了一个具有以下值之一的校验唯一参数:
- UNIQUE_CHECK_NO 指示不应进行唯一性检查(这不是唯一索引)。
- UNIQUE_CHECK_YES 指示这是一个不可延迟的唯一索引,并且唯一性检查必须立即完成,如上所述。
- UNIQUE_CHECK_PARTIAL 表示唯一的约束是可推迟的。PostgreSQL将使用此模式插入每一行的索引条目。访问方法必须允许重复的条目进入索引,并通过从aminsert返回false来报告任何可能的重复。对于返回false的每行,将安排延迟重新检查。
- 访问方法必须标识可能违反唯一约束的任何行,但它不能报告错误正误。这允许在不等待其他事务完成的情况下执行检查;这里报告的冲突不被视为错误,稍后将重新检查,此时它们不再冲突。
- UNIQUE_CHECK_EXISTING 存在指示这是一个延迟的重新检查一个被报告为潜在唯一性违反的行。虽然这是通过调用aminsert实现的,但在这种情况下,访问方法不能插入新的索引条目。索引条目已经存在。更确切地说,访问方法必须检查是否存在另一个实况索引条目。如果是,如果目标行也仍然存在,则报告错误。
建议在UNIQUE_CHECK_EXISTING现有的调用中,访问方法进一步验证目标行实际上在索引中确实有一个条目,如果没有,则报告错误。这是一个好主意,因为传递给aminsert的索引元组值将被重新计算。如果索引定义涉及不是真的不可变的函数,我们可能会检查索引的错误区域。检查在重新检查中找到目标行验证我们正在扫描与原始插入中使用的元组值相同的元组值。
6. Index Cost Estimation Functions
amcostestimate函数给出描述可能索引扫描的信息,包括已确定可用于索引的WHERE and ORDER BY子句的列表。它必须返回访问索引的成本的估计和WHERE子句的选择性(即,索引扫描期间将检索的父表行的分数)。对于简单的情况,成本估算器的几乎所有工作都可以通过调用优化器中的标准例程来完成;具有AMCOST估计功能的点是允许索引访问方法提供索引类型特定的知识,以提高标准估计的可能性。
每个amcostestimate函数必须具有签名:
voidamcostestimate (PlannerInfo *root, IndexPath *path, double loop_count, Cost *indexStartupCost, Cost *indexTotalCost, Selectivity *indexSelectivity, double *indexCorrelation);
注意,成本估算函数必须用C编写,而不是SQL或任何可用的过程语言,因为它们必须访问规划师/优化器的内部数据结构。
索引访问成本应该使用src/backend/optimizer/path/costsize.c参数来计算:顺序磁盘块获取具有成本seq_page_cost,非顺序获取具有random_page_cost,并且处理一个索引行的成本通常应作为cpu_index_tuple_cost。此外,对于索引处理期间调用的任何比较运算符(特别是索引索引本身),应收取适当的cpu_operator_cost数乘数。
访问成本应该包括与扫描索引本身相关联的所有磁盘和CPU成本,而不是检索或处理由索引标识的父表行的成本。
“start-up cost”是总扫描成本的一部分,在我们开始取第一行之前必须花费。对于大多数索引,可以将其取为零,但具有高启动成本的索引类型可能希望将其设置为非零。
indexSelectivity应设置为索引扫描期间检索的父表行的估计分数。在有损查询的情况下,这通常会比实际传递给定条件的行的分数高。
indexCorrelation应该设置为索引顺序和表顺序之间的相关性(范围在1到1之间)。这用于调整从父表获取行的成本的估计。
当loop_count大于1时,返回的数字应该是对该索引进行任何扫描的平均值。
Cost Estimation
一个典型的成本估计器将进行如下
1. 根据给定的QUAL条件,估计并返回父表行的分数。在没有任何索引类型特定知识的情况下,使用标准优化器函数 clauselist_selectivity():
*indexSelectivity = clauselist_selectivity(root, path->indexquals, path->indexinfo->rel->relid, JOIN_INNER, NULL);
2. 估计扫描期间将访问的索引行的数目。对于许多索引类型,这与索引选择性相同,但索引中的行数却可能更多。(注意,索引在页面和行中的大小可从path -> indexinfo 结构获得)。
3. 估计扫描期间检索到的索引页的数量。这可能只是索引选择性在页面中索引的大小。
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检索。如果相关性未知,保守估计为零(无相关性)。