阅读(4141) (0)

PostgreSQL 索引唯一性检查

2021-08-24 10:47:25 更新

PostgreSQL使用唯一索引来强制 SQL 唯一性约束,唯一 索引实际上是不允许多个项有相同键的索引。一个支持这个特性的访问方法要 设置amcanunique为真(目前,只有 b-tree 支持它)。在强制唯一性时不会考虑INCLUDE子句中列出的列。

因为 MVCC,必须允许重复的项在物理上存在于索引之中:这些项可能指向某个单一逻辑行的后继版本。实际想强制的行为是,任何 MVCC 快照都不能包含两个具有相同索引键的行。在向一个唯一索引中插入一个新行时需要被检查的情况可分解成:

  • 如果一个有冲突的合法行已被当前事务删除,这是可以的(特别是因为一个 UPDATE 总是在插入新版本之前删除旧版本,这样就允许一个行上的UPDATE 不改变键)。

  • 如果一个有冲突的行已经被还未提交的事务插入,那么准备插入的事务必须等待看看前面那个事务是否提交。如果它回滚额不会有冲突。如果它提交并且没有删除存在冲突的行,则有一个唯一性违背(实际上我们只是等待那个其他事务结束,然后在全部事务里重做可见性检查)。

  • 类似的,如果一个有冲突的有效行被一个准备提交的事务删除,那么另外一个准备插入的事务必须等待该事务提交或者退出,然后重做测试。

此外,在根据上述规则报告唯一性违背之前,访问方法必须重新检查刚被插入的行的存活性。如果已经因为事务的提交而死亡,那么不应当报告任何违背(这种情况不可能出现在插入在同一事务中创建的行的普通场景中。但是在CREATE UNIQUE INDEX CONCURRENTLY的过程中是可能发生的)。

要求索引访问方法自己应用这些测试,这就意味着它必须到达堆来查看那些根据 索引内容有重复键的任意行的提交状态。这无疑是丑陋并且非模块化的,但是这样可以节约重复的工作:如果我们进行一次独立的探测,那么查找一个冲突行的索引查找本质上将在查找插入新行索引项位置时被重复。此外,没有很明显的方法来避免竞争情况,除非冲突检查是插入新索引项动作的一部分。

如果唯一约束是可延迟的,就存在额外的复杂性:我们需要能够为一个新行插入一个索引项,但是推迟任何唯一性违背错误直到语句结束或者更晚。为了避免对索引不必要的重复搜索,索引访问方法应该在初始插入过程中做一次初步的唯一性检查。如果显示绝对不会有冲突的活元组,就可以完成。否则,我们计划一次重新检查,它将在强制约束的时候发生。在重新检查时,如果具有相同键的被插入元组和某个其他元组都活着,则必须报告错误(注意为了这个目的,“活着”实际意味着“在索引项的 HOT 链上的任何元组都活着”)。要实现这一点,需要给 aminsert 传递一个 checkUnique 参数,其中包含下列值之一:

  • UNIQUE_CHECK_NO表明不需要做唯一性检测(这不是一个唯一索引)。

  • 如上所述,UNIQUE_CHECK_YES表明有一个不可延迟的唯 一索引,并且必须立即做唯一性检测。

  • UNIQUE_CHECK_PARTIAL表明唯一性约束是可延迟的。PostgreSQL将会用这个模式来插入每一行的索引项。访问方法必须允许重复的项进入索引,并且通过从 aminsert返回 FALSE 来报告任何可能的重复。对于返回 FALSE 的每一行,将计划一个延迟的重新检查。

    访问方法必须能够标识任何可能违反唯一约束的行,但是对它来说假阳性报告不是错误。这样就允许检查不用等到其他事务都结束;这里报告的冲突不会被当做错误来看待,并且随后将会被重新检查,而到那时它们可能不再是冲突了。

  • UNIQUE_CHECK_EXISTING表明这是一个行的延迟重新检查,该行被报告为一个潜在的唯一性违背。尽管这会通过调用aminsert来实现,在这种情况下这个访问方法能插入一个新索引项。该索引项已经存在。 当然,访问方法必须检查是否有另一个活着的索引项。如果有,并且如果目标行也仍然存活,那么报告错误。

    我们推荐,在一个UNIQUE_CHECK_EXISTING调用中,访问方法进一步验证目标行真的在索引中有一个现有的项,并且如果不是这样就报错。这是个好主意,因为被传到aminsert的索引元组值将已经被重新计算过。如果索引定义涉及不是真正不变的函数,我们可能正在检查索引的错误区域。对重新检查中找到的目标行的检查会验证我们正在扫描之前被用于原始插入的同一元组值。