ROWID 概念本身并不复杂,但它横跨了对象、存储、执行路径和维护操作四个层面。只从 SQL 语法看,ROWID 像一个可以直接查询的伪列;只从存储看,它又是行地址的外部表示;只从执行计划看,它还是 TABLE ACCESS BY ROWID 的核心入口。ROWID 的正确切入点,应当放在“数据库如何定位一行”这一主问题上。
Oracle 的物理存储是分层的。表、分区、索引这些逻辑对象最终都落在具体的 segment 上;segment 由 extent 组成,extent 由 data block 组成;heap table 的 block 内部再通过 row directory 管理行槽位。ROWID 不是脱离这条链路独立存在的一串字符,而是对象号、文件号、块号和块内槽位这几层元数据的编码结果。行地址之所以可解码、可回表、可用于诊断,本质上都来自这条分层存储结构本身。
从这个角度看,ROWID 一开始就不应被理解成“隐藏主键”。主键处理的是逻辑恒等性,回答“这条记录是谁”;ROWID 处理的是物理或逻辑可达性,回答“这条记录现在如何到达”。主键可以稳定而不关心物理落点,ROWID 可以高效而不保证业务语义稳定。两者都可能帮助数据库唯一锁定一行,但二者所属层级并不相同。
Oracle 官方对 ROWID 的定义非常明确:它是 pseudocolumn,不是用户定义的普通列。伪列的含义不是“看起来像列但毫无底层语义”,相反,它表示数据库内核把某种原生能力直接暴露给 SQL 层。用户可以在 SELECT 和 WHERE 中引用 ROWID,但不能把它当成普通列去插入、更新或删除;数据库也不会把它作为用户表定义中的普通列值存入表结构。
这一定义背后有两个边界。第一,ROWID 不属于业务模型,它不是表结构设计的一部分。第二,ROWID 又不是“纯粹虚拟”的,它表达的是当前行地址。也就是说,它不是作为用户列存储,但它的值确实来自真实的存储元数据。若把这两层压成一句“ROWID 是隐藏列”,表述就会既不准确,也不够深。
ROWID 的核心不是“唯一”,而是“可定位”。Oracle 直接把 rowid 解释为 row address,并说明它通常是访问单行最快的方式之一。这里的地址是数据库能直接用来定位行的内部地址表达,而非应用层的逻辑地址。对 heap-organized table,这种地址具有物理含义;对 index-organized table,这种地址则转化为逻辑含义,由 UROWID 承载。
地址属性决定了 ROWID 的全部价值都围绕“访问路径压缩”展开。数据库一旦已知 rowid,就不再需要通过键值比较、范围筛选和额外推导去定位行,而是可以直接落到对应的数据块和槽位。这也是为什么 ROWID 的技术意义不在建模,而在执行。
ROWID 对用户是只读的,这并不是简单的权限策略,而是语义边界。因为 rowid 直接对应行地址,若允许用户随意写入或修改,等于允许用户直接篡改存储引擎维护的行入口。Oracle 因此只允许查询它和用它做条件,不允许把它当成普通列值去维护。
这一点也解释了一个常见误区:能查出来,不等于能把它纳入业务数据治理。数据库开放 ROWID 的目的,是让执行器和用户都能利用地址进行访问、诊断和校验;不是为了把它提升为可由业务层长期持有、长期维护的一种主标识。
Oracle 对 rowid 组成的说明高度稳定:rowid 包含数据对象号、相对文件号、块号和块内行号。把这四层拆开看,ROWID 处理的不是“第几行”这种抽象编号,而是一条完整的定位链:先确定对象,再确定文件,再确定块,最后确定块内入口。
这四层里最容易被低估的是对象号。很多入门文章只强调文件号、块号、行号,仿佛 rowid 只是磁盘地址的简单变形;但 Oracle 扩展 rowid 之所以能够适配 partition object、segment 边界和更复杂的对象体系,关键在 data object number。它把 rowid 从“物理块坐标”提升成了“对象内的物理块坐标”。
Oracle 在存储结构文档里给出的解释非常关键:rowid 中的 row number,是 row directory entry 的索引;row directory entry 再保存块内该行位置的指针。也就是说,外部可见 rowid 绑定的不是“块内第几个字节”,而是“row directory 中第几个入口”。
这一点直接决定了块内重排与 rowid 稳定性的关系。若数据库只是在同一个 block 内挪动行记录的位置,内核可以只更新 row directory entry 中保存的偏移指针,而 rowid 本身保持不变。换言之,块内偏移变化不等于 rowid 变化,真正稳定的是“块号 + 槽位号”这一层入口语义。
Oracle 还区分不同的 rowid 类型。扩展 rowid 是现代 Oracle 最常见的 rowid 形式,它支持对象号,适用于 partitioned object 和现代对象结构;受限 rowid 主要保留用于兼容旧格式;而 UROWID 是 universal rowid,用来承载逻辑 rowid 以及更广义的地址类型。IOT 的 ROWID 伪列之所以不是普通 ROWID,正是因为它要落在 UROWID 语义上。
这个区分不能跳过。因为只要文章后面涉及 IOT,就必须承认:同样名为 ROWID 的伪列,在 heap table 和 IOT 上承载的并不是同一种地址模型。前者更偏物理入口,后者更偏逻辑入口。
ROWID 不是建表时预生成的,也不是解析 SQL 时先行分配的。它和行真正落入存储位置这一动作绑定。只有当数据库已经为该行确定了对象、写入了某个 block,并在块内分配了具体槽位,rowid 的四段式地址才具备完整来源。
因此,从机制上说,ROWID 的生成顺序应理解为:先有物理入口,再有地址表示,而不是先有一个抽象编号,后让数据去适配这个编号。这个顺序关系很重要,因为它决定了 ROWID 永远是存储状态的结果,而不是先验标识。
对 heap table,一条 INSERT 至少隐含了几个关键动作:数据库确定目标对象;空间管理模块找到可写入块;在 block 的 row directory 中分配槽位;把 row piece 写入行数据区;最后根据对象号、文件号、块号和槽位号形成 rowid。虽然 Oracle 文档不会把这几个步骤按“插入八步法”教科书式展开,但 rowid 组成与 row directory 结构已经足以说明这个过程。
不同写入路径还会影响 rowid 的分布形态。若数据库复用已有 block 的空闲空间,rowid 在 block 维度上可能离散;若数据库使用更偏顺序的新块分配路径,rowid 在 block 维度上可能更连续。这里变化的不是 rowid 的本质,而是对象内部地址分布。对后续回表的块局部性而言,这种分布差异是有实际影响的。
rowid 与物理入口绑定,但对查询可见性来说,地址存在不等于事务已提交。也就是说,行可以已经获得地址并写入 block,而是否对其他会话可见,还要结合事务槽和一致性规则判断。rowid 只解决“在哪里”,不单独解决“当前是否可见”。
这也是为什么 rowid 不应被理解成比事务语义更高一级的万能定位符。它可以让数据库直接找到目标 block 和槽位,但真正返回哪一版本、是否返回该行,仍然要服从事务信息和读一致性规则。地址入口与可见版本,本来就是两件事。
Oracle 多处文档都强调,按 rowid 访问是定位单行的最快方式之一,物理 rowid 甚至可以让数据库在极少 I/O 下直接取到目标行。原因不是神秘优化,而是路径长度被压缩到了最低:对象、文件、块、槽位都已知,执行器不再需要额外推导。
从执行器角度看,这种路径的优势主要体现在两点:其一,省去了键值比较和范围查找;其二,访问目标块时只需极少的跳转。也正因此,rowid access 的价值主要出现在单行访问、精确更新和索引回表这些场景,而不是泛化成任意查询都“天然更快”。
TABLE ACCESS BY ROWID 的真实含义执行计划里最容易被看轻的一步是 TABLE ACCESS BY ROWID。这一步并不是附带动作,而是索引命中之后真正把访问落到表块上的关键步骤。Oracle 明确说明,数据库既可以直接从 SQL 条件拿到 rowid,也可以先通过一个或多个索引扫描得到 rowid,然后再据此访问表。
因此,所谓“回表”从实现上看并不是第二次逻辑查询,而是地址消费过程:索引阶段产出 rowid,表访问阶段消费 rowid。索引负责缩小候选范围,rowid 负责把候选结果变成实际块访问。两者之间的接口就是 rowid。
Oracle 对 B-tree 的描述很直接:每个索引键都与一个 rowid 关联。这个设计背后的逻辑并不复杂。索引本身只承担逻辑筛选职责,它可以帮数据库快速找出“可能是目标行”的键;真正把访问落实到基表块的,仍然需要地址入口。对 heap table 而言,这个入口就是 physical rowid。
rowid 在索引中不仅用于回表,也参与重复键区分。对于非唯一键,键值相同的多个叶子项仍然需要依靠 rowid 保持可区分和可排序。也就是说,rowid 不是“查完索引顺便附带的字段”,而是索引叶子结构的组成部分。少了这一层,B-tree 对 heap table 的回表路径就不完整。
这个问题只有放在表组织方式差异里才好解释。Oracle heap table 里的数据行具有可用的 physical rowid,因此二级索引直接保存 rowid,命中索引后即可回到表块;而 InnoDB 之类聚簇组织表没有同样稳定的 heap-table 式物理入口,因此二级索引往往保存主键,再通过主键回到聚簇结构中定位。这里不是“谁设计得更高级”,而是底层表组织方式不同。
放回 Oracle 的语境里,rowid 之所以适合作为二级索引到 heap table 的桥梁,根源在于 heap table 的地址入口语义是成立的。也正因为这样,只要地址入口变化,索引中保存的 rowid 也要随之维护。后文讨论 row movement 和 segment 级重写时,这一点会变得很重要。
Oracle 对 heap-organized table 的说法很明确:每一行都有一个只在该表内唯一的 rowid,它对应 row piece 的物理地址,是 10-byte physical address。对 heap table 而言,rowid 的物理含义是直接成立的。行写到哪里,rowid 就指向哪里;索引回表也正是依赖这一点。
因此,讨论 heap table 的 rowid,本质上是在讨论一条物理入口链路。对象号限定了 segment,文件号和块号限定了 block,槽位号限定了块内入口。rowid 能快、能回表、能用于块级诊断,全部建立在这个物理地址模型上。
IOT 完全不同。Oracle 明确指出,IOT 的 ROWID 伪列返回的是 logical rowid,而不是 physical rowid;若要在列中保存 IOT 的 rowid,应使用 UROWID,而不是普通 ROWID 数据类型。
IOT 的 logical rowid 以主键为核心,还可能附带 physical guess。Oracle 对 secondary index on IOT 的说明是:secondary index 使用 logical rowid 来定位表行;logical rowid 中包含一个 physical guess,可用于尽量直接探测到目标叶块;即使物理位置变化,只要主键不变,logical rowid 仍然有效。也就是说,IOT 中 rowid 的稳定性不再主要依赖物理块不变,而依赖主键语义不变。
Oracle 还特别提醒:通常 rowid 可以唯一标识一行,但存放在同一 cluster 中不同表的行可能具有相同 rowid。这个现象非常重要,因为它直接说明 rowid 不是脱离对象语境的“绝对全库唯一 ID”。在 cluster 里,多张表可共享存储块,rowid 首先表达的是地址,而不是“表名 + 地址”的复合业务标识。
因此,文章里若把 rowid 写成“数据库内部全局唯一隐藏键”,就会在 cluster 语义上出错。更准确的说法只能是:在通常场景下,rowid 足以唯一标识一行;但这种唯一性并不是脱离对象上下文、无条件成立的抽象承诺。
Oracle 明确说明,每个对象分区都可以存放在自己的 segment 中。对 rowid 而言,这意味着对象号在分区场景里不再是一个几乎不变的背景值,而成为地址语义的重要组成部分。行若位于不同 partition segment,即便文件号、块号和槽位看起来相似,rowid 也不会相同,因为 data object number 已经不同。
这也是为什么 rowid 结构里必须有对象号。若没有对象号,分区对象上的地址表达会丢失 segment 语境;有了对象号,rowid 才真正具备“对象内精确定位”的能力,而不只是“文件块坐标”。
Oracle 在逻辑存储结构文档里明确指出:如果数据库在同一 block 内移动一行,则更新 row directory entry 的指针即可,rowid 保持不变。这里最关键的技术点不是“有没有移动”,而是“入口槽位是否仍然保留”。只要 row directory 中代表该行的 entry 仍是原 entry,rowid 就没有必要变化。
因此,块内重排属于“位置变化但入口不变”的场景。它影响的是块内偏移,不影响外部可见地址表达。很多文章把“块内移动”和“行地址变化”直接画等号,错误就出在这里。
Oracle 对 row migration 的表述同样很清楚:当一行原本能放在一个块中,更新后因空间不足搬到新块时,原 row piece 会保留一个 forwarding address 指向新块,而 migrated row 的 rowid 不变。
row migration 的关键特征,是原入口仍然保留,行主体外移。这样一来,旧 rowid 仍能被数据库识别和跟踪,只是访问时需要先经过原入口,再跳到新块。它带来的主要问题是额外 I/O 与访问链路变长,而不是外部 rowid 改写。
Oracle 还区分 row chaining。所谓 chained row,是指一行在插入时就太大,天然无法放进一个 block,只能拆成多个 row piece 存在一串 block 中。它不是“原本单块可容纳,后来更新后被搬走”,而是“从一开始就需要多块承载”。
row chaining 与 row migration 的区别在于,前者讨论的是大行的多 piece 存储,后者讨论的是原有单块行因更新后长度增加而整体迁移。
真正典型会导致 rowid 变化的,是 row movement。Oracle 明确说明:rowid 在特殊情况下会改变;若启用 row movement,partition key update、Flashback Table、shrink table 等都可能改变 rowid;即便 row movement 关闭,导出后再导入也可能导致 rowid 改变。
这类场景的共同特征是:原入口不再作为正式入口继续使用,数据库承认新的物理位置为该行当前有效位置。rowid 一旦编码的是新入口,旧 rowid 就不再具有当前地址意义。与 row migration 相比,row movement 不是“入口保留、主体外移”,而是“正式入口被整体重建”。
在 partitioned object 中,跨分区更新不仅可能改变 block 和槽位,还可能直接改变对象号。因为不同分区可对应不同 segment,而 rowid 又编码了 data object number。行一旦从一个分区迁移到另一个分区,至少对象号这一维就已经改变了。
因此,分区键更新不是普通“块内重排”或“同对象内再分配”,而是对象语境发生变化后的地址重建。rowid 随之改写,并不是附带结果,而是地址模型自身的自然要求。
表移动、表重组、某些 shrink 场景、导出导入,这些操作虽然表现形式不同,但核心都接近:数据库把已有数据重新写入新的物理布局中。只要数据段被整体重写,原对象、原块、原槽位关系就不再能被直接沿用。([Oracle 文档][12])
这类场景尤其能说明一个事实:rowid 的稳定性从来不依赖“业务数据没变”,而依赖“物理入口没变”。行内容完全相同,地址入口仍然可以变化;一旦入口变化,rowid 就应视为重建。
对 heap table,一行一旦插入并形成 rowid,这个 rowid 就开始承担地址入口职责;但“已经生成”并不意味着“以后绝不变化”。Oracle 的文档已经把特殊变化场景列出来:row movement、export/import、某些表维护动作,都可能让已有 rowid 失去稳定性。
因此,rowid 的生命周期更适合这样理解:它在某一存储阶段内有效并可用,但其稳定前提取决于存储入口是否持续存在。rowid 不是业务主键式的生命周期稳定值,而是与物理或逻辑入口共存亡的地址值。
Oracle 早期文档明确提到,删除一行之后,数据库可能把它原来的 rowid 重新分配给新行。这个现象说明 rowid 的唯一性必须结合“当前有效行”来理解,而不能被写成永恒不复用。
也就是说,rowid 的语义始终面向“当前地址入口”。当旧行被删除、旧入口被释放,新行完全可能占据相同的入口组合。把 rowid 当作业务层长期引用值,风险也正在于此:它表达的是地址,不是历史身份。
主键表达逻辑恒等性,rowid 表达当前地址。前者的稳定性不以物理入口不变为前提,后者的意义却完全建立在入口语义上。两者可以都用来找到一行,但并不处于同一层。
Oracle 自己已经给出反例:同一 cluster 中不同表的行可能具有相同 rowid。因此,rowid 的唯一性不应被写成脱离对象上下文的绝对全局唯一。更准确的表述只能是“通常可唯一标识一行”。
row migration 就是反例:行主体迁入新块,但原入口保留 forwarding address,rowid 不变。真正需要强调的是“入口是否被重建”,而不是笼统说“行动了没动”。
Oracle 明确不建议这样做。原因并不神秘:rowid 可能变化,可能复用,在不同表组织方式下语义还不统一;它属于内核定位接口,而不是稳定业务身份。
IOT 的 ROWID 伪列返回 logical rowid,由 UROWID 承载,并以主键语义为核心;heap table 的 rowid 则是 physical rowid,对应 row piece 的物理地址。二者名字相同,模型不同。
ROWID 的核心并不在于它能不能单独拿来查一行,而在于它把 Oracle 的几条底层主线连在了一起:对象如何映射到 segment,block 如何通过 row directory 管理入口,B-tree 如何把 key 变成可回表的地址,heap table 与 IOT 为什么需要两种不同的 rowid 语义,row migration 与 row movement 为什么会呈现出不同的稳定性结果。只要把这些线索串起来,ROWID 就不再是一个被反复提起却经常被写浅的伪列,而会重新回到它本来的位置:Oracle 存储与执行之间最直接的一层接口。
文章
阅读量
获赞
