PostgreSQL 使用
唯一索引来强制 SQL 唯一性约束,唯一索引
是不允许存在多个相同键条目的索引。
支持这一特性的访问方法把
amcanunique 设为 true。
(目前只有 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 的索引元组值已被重新计算。
如果索引定义涉及并非真正不可变的函数,我们可能正在检查索引中
错误的区域。
检查目标行能在重新检查中被找到,验证了我们正在扫描的元组值与
原始插入时使用的相同。