PostgreSQL 中唯一索引的工作原理

引言

在研究分区表相关解决方案时,曾聚焦于如何在包含多个子表的分区表内,实现跨分区的数据唯一性保障。这类功能有时被称为全局索引,相关领域的讨论可参考邮件线程此处。尽管其中提出的思路具备一定合理性,但对应的实现方式却引发了较多争议 ------ 该方式改变了分区索引的存储逻辑,本质是将所有分区索引合并存储,并以TableOid作为内部引用的键值。

基于此,探索一种替代方案:在不改变 PostgreSQL 原有核心机制的前提下,实现跨分区唯一性保障。而要推进这一探索,首要任务是先厘清 PostgreSQL 分区表中,唯一性机制的底层工作原理。

CREATE INDEX 语句的唯一性保障

PostgreSQL 会通过以下基本流程检查唯一性冲突:

  1. 对目标子分区表执行堆扫描(heap scan)。
  2. 将可见元组(visible tuples)存储到一个BTSpool结构(记为 spool1)中,将无效元组(dead tuples)存储到另一个BTSpool结构(记为 spool2)中。因此,这里会用到两个BTSpool结构;若表中无无效元组,或无需保障唯一性,则 spool2 可能为 NULL。BTSpool结构可理解为索引元组的集合。
  3. 若 spool1 和 spool2 存在,则对其进行排序。
  4. 排序算法内置重复检测功能:若排序后有两个相同的元组连续出现,即判定为重复;若该索引要求唯一性,则会在此处抛出错误。
  5. 若排序过程中未检测到重复,PostgreSQL 会基于 spool1 和 spool2 构建索引树。
  6. 索引创建完成后,销毁所有BTSpool结构。
  7. 上述逻辑位于src/backend/access/nbtree/nbtsort.c文件的btbuild()函数中,并由indexcmds.c文件的DefineIndex函数调用。根据活跃子分区表的数量,DefineIndex会多次调用btbuild

INSERT 与 UPDATE 语句的唯一性保障

在规划器(planner)和优化器(optimizer)阶段,PostgreSQL 已确定新数据应插入或更新至哪个子分区表:

  1. PostgreSQL 首先将堆元组(heap tuple)插入目标堆关系(heap relation)。
  2. 随后调用src/backend/access/nbtree/nbtinsert.c文件中的_bt_doinsert()函数,尝试插入与该堆元组关联的新索引元组。
  3. 若索引要求进行唯一性检查,PostgreSQL 会根据新堆元组构建扫描键(scan key),并调用_bt_check_unique()函数从堆分区表中查询是否存在匹配的现有元组。
  4. 若当前子分区中未查询到匹配的堆元组,则无冲突。
  5. 若当前子分区中查询到匹配的堆元组,则需执行以下额外检查:
  6. 若查询到的元组尚未提交(例如,另一个后端进程仍在处理该元组且未提交),当前进程会在此处等待,直至该后端进程提交或回滚。
  7. 当该后端进程提交或回滚后,当前进程会再次查询同一元组。
  8. 若该后端进程回滚,则无法查询到重复元组,因此无冲突。
  9. 若该后端进程提交,则仍可查询到重复元组,因此存在潜在冲突。
  10. 在抛出错误前,PostgreSQL 会再执行一次检查:获取当前待插入堆元组的可见性状态。这是为了覆盖一种特殊场景------当当前后端进程尝试插入或更新数据时,另一个后端进程正在执行CREATE UNIQUE INDEX CONCURRENTLY(并发创建唯一索引)操作。
  11. 检查待插入的当前元组是否能从堆关系中查询到。
  12. 若能查询到,则当前元组仍处于可见状态,必然存在冲突。
  13. 若无法查询到,则当前元组已变为不可见状态,不视为冲突。
  14. 若未检测到冲突,则继续构建索引树。

上述逻辑主要位于src/backend/access/nbtree/nbtinsert.c文件的_bt_doinsert()_bt_check_unique()函数中。

ATTACH 操作的唯一性保障

待附加(ATTACH)的表可能已定义唯一索引(或非唯一索引),也可能完全没有索引。ATTACH 操作过程中存在两种潜在情况:

  • 向分区表附加无索引的表时,PostgreSQL 会自动为该附加表创建新索引,且索引参数与原分区表保持一致。目前,该索引的创建流程遵循2.0节中定义的步骤。
  • 向分区表附加已定义唯一索引的表(且该索引的键与分区表的全局唯一索引键相同)时,PostgreSQL 不会为该附加表创建新索引,而是直接完成附加操作。
相关推荐
柯南二号19 分钟前
【Java后端】MyBatis 和 MyBatis-Plus (MP) 的区别
java·数据库·tomcat
C++chaofan23 分钟前
游标查询在对话历史场景下的独特优势
java·前端·javascript·数据库·spring boot
小蒜学长30 分钟前
springboot房地产销售管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端
0wioiw01 小时前
PostgreSQL(②基础命令)
数据库·postgresql
xcLeigh1 小时前
KingbaseES数据库:兼容 SQL 语法及 Oracle 过程化语言的语法基础
数据库
FinTech老王1 小时前
一场“无感换心”手术:金仓数据库如何让电子证照系统平滑告别MongoDB
数据库·mongodb
周杰伦的稻香1 小时前
MySQL中的空间碎片率计算分析
android·数据库·mysql
重启的码农1 小时前
kv数据库-leveldb (13) 缓存 (Cache)
数据库
重启的码农1 小时前
kv数据库-leveldb (12) 数据块 (Block)
数据库
lypzcgf1 小时前
Coze源码分析-资源库-创建数据库-后端源码-应用/领域/数据访问层
数据库·go·后台·coze·coze源码分析·ai应用平台·agent平台