54.5. 索引唯一性检查

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

因为 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的索引元组的值可能被重新计算。 如果索引定义涉及不是真正不可变(immutable)的函数,我们可能会在索引中错误的区域查找。 检查目标行可以在再检查中被找到确保我们正在扫描原始插入时使用的相同的元组值。