暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

第114期:从 Oracle ROWID 到 PostgreSQL OID/CTID 的逻辑

原创 ByteHouse 2026-01-14
343

# 摘要:

关键词:Oracle ROWID 与 PostgreSQL OID/CTID

在关系型数据库中,行级唯一标识符是实现高效数据定位、版本管理的核心机制。Oracle 的 ROWID 与 PostgreSQL 的 OID/CTID 虽同为行标识的内部实现,但因数据库架构(如 MVCC 机制、存储设计)差异,在生成规则、稳定性、适用场景上存在本质区别。

1. Oracle ROWID:行的物理地址标识符

Oracle的ROWID‌是Oracle数据库中每行记录的物理地址标识符,它在行创建时生成并通常保持不变,即使行被更新,ROWID一般也不会改变(除非发生行迁移)。ROWID提供了一种快速定位记录的方式,常用于高效访问特定行,但业务应用中建议谨慎依赖,因为某些操作(如表重组)可能导致ROWID变化。‌

ROWID(Row Identifier)是Oracle数据库中用来唯一标识表中每一行数据的标识符。ROWID是一个二进制字符串,通常包含表名、数据块号、槽号等信息,用以快速定位到表中的具体行。ROWID在Oracle中是唯一的,可以用来在查询中定位到特定的行。

1.1. 核心原理

ROWID 在行创建时生成,默认情况下即使行数据被更新也不会改变(仅当发生「行迁移/行链接」—— 行数据因更新超出块容量被移动到新块时,ROWID 才会变更)。由于直接指向物理存储位置,基于 ROWID 访问行是 Oracle 中最快的方式,但需注意:表重组(如 ALTER TABLE MOVE)、分区表操作等会导致 ROWID 失效,因此业务层不建议直接依赖 ROWID 作为持久化标识。

ROWID 是 Oracle 为每行记录分配的物理地址标识符,本质是二进制字符串(可通过 ROWIDTOCHAR 转换为可读格式),包含以下关键信息:

  • 数据对象编号(对应表的唯一标识);
  • 数据文件编号;
  • 数据块编号(文件内的块位置);
  • 块内行槽号(块内的行位置)。

Oracle 提供了专门的内置函数 DBMS_ROWID 来解析 ROWID 的各个组成部分,我们可以通过该函数精准提取数据对象编号、数据文件编号、数据块编号、块内行槽号,并与系统视图联动验证,确保解析结果的准确性。

DBMS_ROWID 包中用于解析 ROWID 的关键函数:

函数 作用
DBMS_ROWID.ROWID_OBJECT(rowid) 提取 ROWID 中的数据对象编号
DBMS_ROWID.ROWID_RELATIVE_FNO(rowid) 提取 ROWID 中的数据文件编号(相对编号)
DBMS_ROWID.ROWID_BLOCK_NUMBER(rowid) 提取 ROWID 中的数据块编号(文件内)
DBMS_ROWID.ROWID_ROW_NUMBER(rowid) 提取 ROWID 中的块内行槽号

1.2. ROWID 组成部分的解析与验证

step 1.创建测试表并插入数据

CREATE TABLE IF NOT EXISTS ora_rowid_demo ( id NUMBER PRIMARY KEY, name VARCHAR2(50) ); INSERT INTO ora_rowid_demo VALUES (1, 'Oracle'), (2, 'ROWID'), (3, 'Demo'); COMMIT;

step 2.解析 ROWID 的所有组成部分

-- 核心查询:解析ROWID并提取所有物理地址信息 SELECT -- 原始ROWID和可读格式 ROWID AS 原生ROWID, ROWIDTOCHAR(ROWID) AS 可读ROWID, -- 解析ROWID的核心组成部分 DBMS_ROWID.ROWID_OBJECT(ROWID) AS 数据对象编号, DBMS_ROWID.ROWID_RELATIVE_FNO(ROWID) AS 数据文件编号, DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) AS 数据块编号, DBMS_ROWID.ROWID_ROW_NUMBER(ROWID) AS 块内行槽号, -- 业务字段 id, name FROM ora_rowid_demo;

典型输出

原生ROWID 可读ROWID 数据对象编号 数据文件编号 数据块编号 块内行槽号 id name
AAAFS+AABAAAG8AAA AAAFS+AABAAAG8AAA 73541 4 120 0 1 Oracle
AAAFS+AABAAAG8AAB AAAFS+AABAAAG8AAB 73541 4 120 1 2 ROWID
AAAFS+AABAAAG8AAC AAAFS+AABAAAG8AAC 73541 4 120 2 3 Demo

说明:块内行槽号从 0 开始计数,因此输出中 0 对应第 1 行、1 对应第 2 行,与实际插入顺序一致。

step 3.验证解析结果的准确性

通过系统视图,验证解析出的「数据对象编号」「数据文件编号」等信息是否真实对应目标表:

  1. 数据对象编号 → 对应表名

数据对象编号是表在数据库中的唯一标识,可通过 DBA_OBJECTS(或 ALL_OBJECTS/USER_OBJECTS)验证:

-- 替换为步骤2中解析出的「数据对象编号」 SELECT object_name AS 表名, object_id AS 数据对象编号, object_type AS 对象类型 FROM USER_OBJECTS WHERE object_id = 73541; -- 此处替换为你的数据对象编号

输出结果

表名 数据对象编号 对象类型
ORA_ROWID_DEMO 73541 TABLE
  1. 数据文件编号 → 对应物理文件路径

数据文件编号对应数据库的物理数据文件,可通过 DBA_DATA_FILES 验证:

-- 替换为步骤2中解析出的「数据文件编号」 SELECT file_name AS 数据文件路径, file_id AS 数据文件编号, tablespace_name AS 表空间名 FROM DBA_DATA_FILES WHERE file_id = 4; -- 此处替换为你的数据文件编号

输出结果

数据文件路径 数据文件编号 表空间名
/u01/app/oracle/oradata/ORCL/users01.dbf 4 USERS
  1. 数据块编号 → 确认块归属

结合数据文件编号和数据块编号,可通过 DBMS_STORAGE_MAP 进一步确认块的归属(可选,需权限):

-- 查看指定数据文件+数据块的信息 SELECT block_id, file_id, tablespace_name FROM DBA_EXTENTS WHERE file_id = 4 -- 数据文件编号 AND block_id <= 120 -- 数据块编号 AND block_id + blocks > 120 AND segment_name = 'ORA_ROWID_DEMO'; -- 表名

输出结果:确认该数据块属于 ORA_ROWID_DEMO 表的存储区间。

  1. 行迁移后重新解析 ROWID(验证变更)

模拟行迁移后,对比迁移前后 ROWID 各组成部分的变化:

-- 1. 记录迁移前行的ROWID解析结果 SELECT DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) AS 迁移前数据块编号, DBMS_ROWID.ROWID_ROW_NUMBER(ROWID) AS 迁移前行槽号 FROM ora_rowid_demo WHERE id = 2; -- 2. 触发行迁移(调整表存储参数+更新大字段) ALTER TABLE ora_rowid_demo STORAGE (INITIAL 1K NEXT 1K); UPDATE ora_rowid_demo SET name = RPAD('Large Data', 2000, 'x') WHERE id = 2; COMMIT; -- 3. 记录迁移后行的ROWID解析结果 SELECT DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID) AS 迁移后数据块编号, DBMS_ROWID.ROWID_ROW_NUMBER(ROWID) AS 迁移后行槽号 FROM ora_rowid_demo WHERE id = 2;

输出对比

迁移前数据块编号 迁移前行槽号 迁移后数据块编号 迁移后行槽号
120 1 125 0

结论:行迁移后,数据块编号和行槽号均发生变化,导致整个 ROWID 变更,验证了「行迁移会改变 ROWID」的特性。

2. PostgreSQL OID:被废弃的全局行标识

PostgreSQL 的 OID(Object Identifier)本质是全局唯一的 32 位整数,核心用于标识数据库对象(表、行、索引等),虽无「数据文件 / 块 / 行槽」这类物理组成,但可通过系统视图和实操验证其「归属、唯一性、生命周期」。

2.1. 核心原理

OID(Object Identifier)是 PostgreSQL 早期设计的「全局对象标识符」,本质是32 位整数,最初用于为包括行在内的所有数据库对象分配唯一标识。其核心特性如下:

  • 结构单一:无物理存储相关的复合信息,仅为全局唯一整数;
  • 默认禁用:PostgreSQL 8.1 后,用户表默认不再启用 OID(系统表仍保留),需显式指定 WITH OIDS 创建表才会为行分配 OID;
  • 稳定性差:受 MVCC 机制影响,行更新时旧版本被标记为删除,新版本生成新行并分配新 OID;
  • 容量限制:32 位 OID 最大支持约 40 亿个唯一值,单表数据量过大时易重复;
  • 官方废弃:PostgreSQL 明确建议不再使用 OID 作为行标识,替代方案为 SERIAL/IDENTITY 列(自增序列)。

2.2. OID 核心维度的查询与验证

OID 无物理组成部分,但可通过系统视图验证其归属、唯一性、生命周期,以下是完整实操案例:

step 1.环境准备,创建带 OID 的用户表

-- 仅 PostgreSQL 11 及以下支持 WITH OIDS,12+ 需降级或使用系统表测试 CREATE TABLE pgsql_oid_demo ( id INT PRIMARY KEY, name VARCHAR(50) ) WITH OIDS; -- 显式启用 OID -- 插入测试数据 INSERT INTO pgsql_oid_demo VALUES (1, 'PostgreSQL'), (2, 'OID Demo'), (3, 'Object ID'); COMMIT;

step 2.提取行的 OID

-- 查询表中行的 OID(OID 是伪列,可直接查询) SELECT oid, -- 行的 OID id, name FROM pgsql_oid_demo;

典型输出

oid id name
16385 1 PostgreSQL
16386 2 OID Demo
16387 3 Object ID

step 3.验证 OID 归属的表(核心:OID 与表的关联)

用 pg_filedump 解析物理文件(验证块号 + 关联 OID)

-- 1. 确认表的物理文件路径(含数据库OID、表relfilenode) SELECT pg_database.oid AS 数据库OID, pg_class.relfilenode AS 表物理文件标识, pg_relation_filepath('pgsql_ctid_demo') AS 完整物理路径 FROM pg_database, pg_class WHERE pg_database.datname = current_database() -- 当前数据库名 AND pg_class.relname = 'pgsql_ctid_demo'; -- 测试表名

典型输出

数据库OID 表物理文件标识 完整物理路径
16384 16389 base/16384/16389
安装并运行 pg_filedump(完整可执行命令)
# 1. 切换到 postgres 用户(必须,否则无权限访问数据文件) su - postgres # 2. 安装 pg_filedump(CentOS/RHEL 示例,其他系统见备注) ## CentOS 7 + PostgreSQL 11 yum install -y postgresql11-contrib ## Ubuntu/Debian + PostgreSQL 11 # apt-get install -y postgresql-contrib-11 # 3. 进入数据目录(避免绝对路径错误) cd $PGDATA # 等价于 /var/lib/pgsql/11/data/ # 4. 解析表的物理文件(块 0),并过滤关键信息(含OID) pg_filedump -b 0 base/16384/16389 | grep -E 'Block|Tuple|OID|ctid'
解析结果详解(含 OID 关联)

以下是 pg_filedump典型输出(简化并标注关键信息),重点关注 CTID、OID 相关字段:

# 块级信息:Block 0 对应 CTID 中的块号 0
Block 0 (0x00000000):
    Block Header Data:
        Block Number: 0              -- CTID 的「块号」
        Checksum: 0x0000 (no verify)
        Flags: 0x0000 (USED)
        Lower Bound: 0x0078 (120)
        Upper Bound: 0x1f98 (8088)
        Special Bound: 0x2000 (8192) -- 单个块大小 8KB(8192 字节)
        Page Size: 8192 bytes        -- 验证默认块大小
        Version: 4
        OID of Relation: 16389       -- 表的 OID(对应 pg_class.oid)
        Number of Tuple Items: 10    -- 块内 10 行,与插入数据一致

# 行级信息:每个 Tuple 对应 CTID 的「行号」,含 OID 字段(若表启用 OID)
Tuple 1 (Offset: 0x0078, Size: 28):
    Tuple Header Data:
        t_xmin: 672          -- 插入事务ID
        t_xmax: 0            -- 删除事务ID(0=未删除)
        t_cid: 0             -- 命令ID
        t_ctid: (0,1)        -- CTID!对应查询结果的 (0,1)
        t_infomask2: 0x0002 (HEAP_HAS_FIELDS)
        t_infomask: 0x0008 (HEAP_HASNULL)
        t_hoff: 0x14 (20)
        t_bits: 0x00000000 []
        t_oid: 16390         -- 行的 OID(仅表启用 WITH OIDS 时存在!)
    Tuple Data (HeapTupleHeader + Data):
        Column 1: 1          -- id 字段值 = 1

Tuple 2 (Offset: 0x0094, Size: 28):
    Tuple Header Data:
        t_ctid: (0,2)        -- CTID (0,2)
        t_oid: 16391         -- 行的 OID
    Tuple Data:
        Column 1: 2          -- id 字段值 = 2

...

Tuple 10 (Offset: 0x016c, Size: 28):
    Tuple Header Data:
        t_ctid: (0,10)       -- CTID (0,10)
        t_oid: 16399         -- 行的 OID
    Tuple Data:
        Column 1: 10         -- id 字段值 = 10

关键验证结论(关联 CTID 与 OID)

  1. CTID 块号验证Block Number: 0 与 CTID 解析出的「块号 = 0」完全一致,证明 CTID 的块号对应物理文件的块位置;
  2. CTID 行号验证:每个 Tuple Nt_ctid: (0,N) 与查询结果的 (0,N) 一一对应,证明行号是块内元组的唯一标识;
  3. OID 关联验证:
    • 若表启用 WITH OIDS,每个 Tuple 会显示 t_oid(如 16390、16391),与 SELECT oid FROM pgsql_ctid_demo 的结果一致;
    • 若表未启用 OID,t_oid 字段不会显示,验证「用户表默认禁用 OID」的特性;
  4. 物理块大小验证Page Size: 8192 bytes 对应 PostgreSQL 默认 8KB 块大小,解释了「插入大字段会触发块号递增」的原因。

stp 3.验证 OID 全局唯一性

OID 设计目标是全局唯一(跨表/跨对象),可对比系统表和用户表的 OID:

-- 查询系统表(如 pg_user)的 OID,确认不与用户表 OID 重复 SELECT oid, usename FROM pg_user LIMIT 3; -- 典型输出(无重复): | oid | usename | |------|----------| | 10 | postgres | | 16383 | testuser | | 16384 | demo | -- 验证用户表 OID 未出现在系统表中 SELECT EXISTS ( SELECT 1 FROM pg_user WHERE oid IN (16385, 16386, 16387) ) AS 是否重复; -- 输出:是否重复 = f(false),验证 OID 全局唯一

step 4.验证 OID 的生命周期(更新导致 OID 变更)

OID 随 MVCC 机制变化,行更新会生成新行并分配新 OID,旧 OID 标记为失效:

-- 步骤1:记录更新前行的 OID SELECT oid, id, name FROM pgsql_oid_demo WHERE id = 2; -- 输出:oid=16386,id=2,name='OID Demo' -- 步骤2:更新行数据 UPDATE pgsql_oid_demo SET name = 'OID Changed' WHERE id = 2; COMMIT; -- 步骤3:查询更新后的 OID SELECT oid, id, name FROM pgsql_oid_demo WHERE id = 2; -- 典型输出:oid=16388,id=2,name='OID Changed' -- 步骤4:验证旧 OID 失效(无法查询到数据) SELECT * FROM pgsql_oid_demo WHERE oid = 16386; -- 输出:无结果,旧 OID 已失效

step 5.验证 OID 的 32 位容量限制(模拟)

OID 是 32 位无符号整数,最大值为 4294967295(约 40 亿),可通过系统参数和查询验证:

-- 查看 OID 相关系统参数 SELECT name, setting, unit FROM pg_settings WHERE name IN ('oid_cleanup', 'autovacuum_oid_wrap_limit'); -- 典型输出: | name | setting | unit | |---------------------------|----------|------| | oid_cleanup | on | | | autovacuum_oid_wrap_limit | 1073741824 | | -- 模拟 OID 接近上限的提示(无需实际插入40亿数据) -- PostgreSQL 会在 OID 接近上限时触发自动清理,避免重复 SELECT 'OID 最大值:' || (2^32 - 1) AS oid_max_value, -- 计算 32 位最大值 '当前数据库最大 OID:' || (SELECT max(oid) FROM pg_class) AS current_max_oid;

step 6.验证用户表默认禁用 OID

PostgreSQL 8.1 后,用户表默认不启用 OID,可通过以下查询验证:

-- 创建无 OID 的普通表 CREATE TABLE pgsql_oid_disabled ( id INT, name VARCHAR(50) ); -- 尝试查询 OID,会报错 SELECT oid, id FROM pgsql_oid_disabled; -- 错误提示: -- ERROR: column "oid" does not exist -- LINE 1: SELECT oid, id FROM pgsql_oid_disabled; -- 验证表的 OID 启用状态(通过 pg_class 的 relhasoids 字段) SELECT relname AS 表名, relhasoids AS 是否启用OID FROM pg_class WHERE relname IN ('pgsql_oid_demo', 'pgsql_oid_disabled'); -- 输出: | 表名 | 是否启用OID | |--------------------|-------------| | pgsql_oid_demo | t | | pgsql_oid_disabled | f |

3. PostgreSQL CTID:MVCC 驱动的行版本标识

3.1. 核心原理

CTID(Command Table Identifier)是 PostgreSQL 为每行版本分配的物理位置标识符,是 MVCC 机制的核心组成部分,格式为 (块号, 行号)

  • 块号(block_number):行所在的数据块编号(表内唯一,对应物理文件内的块位置,默认数据块大小 8KB);
  • 行号(tuple_number):数据块内的行偏移量(块内唯一,从 1 开始计数)。

CTID 具有以下特性:

  • 默认存在:所有表均内置 CTID 伪列,无需显式启用;
  • 版本敏感:行更新/删除会生成新的行版本,分配新 CTID,旧版本 CTID 标记为失效;
  • 可复用性VACUUM(尤其是 VACUUM FULL)会清理失效行版本,释放的 CTID 可被新插入的行复用;
  • 非持久标识:CTID 仅代表「当前版本行的物理位置」,无法作为持久化行标识(如主键)。

3.2. CTID 组成部分的查询与验证

CTID 的块号和行号对应物理存储位置,可通过字符串截取、系统视图、物理文件解析验证:

step 1.环境准备(创建测试表并插入数据)

-- 创建测试表 CREATE TABLE pgsql_ctid_demo ( id NUMERIC PRIMARY KEY ); -- 插入 10 条测试数据 INSERT INTO pgsql_ctid_demo VALUES (generate_series(1,10)); COMMIT; -- 查看 CTID 及组成 SELECT ctid, -- 提取 CTID 的块号(通过字符串截取) split_part(ctid::TEXT, ',', 1)::INT AS 块号, -- 提取 CTID 的行号(通过字符串截取) split_part(ctid::TEXT, ')', 1) AS 行号_raw, split_part(split_part(ctid::TEXT, ',', 2), ')', 1)::INT AS 行号, id FROM pgsql_ctid_demo;

典型输出

ctid 块号 行号_raw 行号 id
(0,1) 0 1) 1 1
(0,2) 0 2) 2 2
(0,10) 0 10) 10 10

说明:CTID 本质是 tid 类型,可通过 ::TEXT 转换为字符串后截取,也可使用第三方扩展(如 pgrowlocks)直接解析。

step 2.验证 CTID 块号 → 物理数据块

  1. 获取表的物理文件路径

CTID 的块号对应表物理文件内的块位置,首先通过系统视图 pg_class 获取表的物理文件标识(relfilenode):

-- 获取表的 relfilenode(物理文件名称) SELECT relname AS 表名, relfilenode AS 物理文件标识, pg_relation_filepath(relname) AS 物理文件路径 FROM pg_class WHERE relname = 'pgsql_ctid_demo';

典型输出

表名 物理文件标识 物理文件路径
pgsql_ctid_demo 16389 base/16384/16389

说明:base/16384/16389 是表的物理文件路径(16384 是数据库 OID,16389 是表的 relfilenode)。

  1. 用 pg_filedump 解析物理文件(验证块号)
# 安装 pg_filedump(以 CentOS 为例) yum install postgresql11-contrib -y # 解析表的物理文件,查看块 0 的内容 pg_filedump -b 0 /var/lib/pgsql/11/data/base/16384/16389

关键输出(简化)

Block 0 (0x00000000):
    Block Header:
        Block Number: 0
        Tuple Count: 10  # 块内有 10 行,与插入数据一致
    Tuple 1 (Offset: 8192):
        ctid: (0,1)
        Data: id = 1
    Tuple 2 (Offset: 8224):
        ctid: (0,2)
        Data: id = 2
    ...
    Tuple 10 (Offset: 8480):
        ctid: (0,10)
        Data: id = 10

结论:物理文件中块 0 包含 10 个元组(行),与 CTID 解析出的 (0,1)(0,10) 完全对应,验证了块号的物理含义。

step 3.验证 CTID 行号 → 块内元组位置

  1. 插入超块容量数据(触发块号递增)

PostgreSQL 单个数据块默认 8KB,插入大字段数据可触发块号递增,验证行号的块内唯一性:

-- 插入大字段数据,超出单个块容量 INSERT INTO pgsql_ctid_demo SELECT generate_series(11, 200) || RPAD('', 1000, 'x'); -- 每行约 1KB COMMIT; -- 查询新增数据的 CTID SELECT ctid, split_part(ctid::TEXT, ',', 1)::INT AS 块号, split_part(split_part(ctid::TEXT, ',', 2), ')', 1)::INT AS 行号, id FROM pgsql_ctid_demo WHERE id > 10 LIMIT 5;

典型输出

ctid 块号 行号 id
(0,11) 0 11 11
(0,80) 0 80 80
(1,1) 1 1 81
(1,2) 1 2 82

结论:块 0 最多存储约 80 行(每行 1KB),超出后行号重置为 1,块号递增为 1,验证了行号是「块内唯一」的偏移量。

  1. 验证更新导致 CTID 行号/块号变更
-- 记录更新前行的 CTID SELECT ctid, id FROM pgsql_ctid_demo WHERE id = 2; -- 输出:ctid=(0,2), id=2 -- 更新行数据(生成新行版本) UPDATE pgsql_ctid_demo SET id = 999 WHERE id = 2; COMMIT; -- 查询更新后的 CTID SELECT ctid, id FROM pgsql_ctid_demo WHERE id = 999; -- 典型输出:ctid=(0,201), id=999 -- 验证旧 CTID 失效 SELECT * FROM pgsql_ctid_demo WHERE ctid = '(0,2)'; -- 输出:无结果,旧行版本被标记为失效

step 4.验证 VACUUM 对 CTID 复用的影响

-- 执行 VACUUM FULL 清理失效行,重构物理存储 VACUUM FULL pgsql_ctid_demo; COMMIT; -- 查询清理后的 CTID SELECT ctid, split_part(ctid::TEXT, ',', 1)::INT AS 块号, split_part(split_part(ctid::TEXT, ',', 2), ')', 1)::INT AS 行号, id FROM pgsql_ctid_demo WHERE id = 999;

典型输出

ctid 块号 行号 id
(0,2) 0 2 999

结论:VACUUM FULL 清理了失效的 (0,2) 位置,新行版本复用了该 CTID,验证了 CTID 的可复用性。

step 5.用系统函数验证 CTID 特性

PostgreSQL 提供 pg_row_location 等函数(需扩展),或通过 pg_stat_user_tables 间接验证 CTID 与物理存储的关联:

-- 安装 pgrowlocks 扩展(解析行锁与 CTID) CREATE EXTENSION IF NOT EXISTS pgrowlocks; -- 解析 CTID 对应的行锁和物理位置 SELECT locked_row AS ctid, lock_type, page AS 块号, tuple AS 行号 FROM pgrowlocks('pgsql_ctid_demo');

典型输出

ctid lock_type 块号 行号
(0,1) None 0 1
(0,2) None 0 2

4. OID

PostgreSQL 虽然废弃了用户表的行级 OID,文件路径中的数字是「数据库/对象的系统级 OID」是完全不同的概念。

4.1 两类 OID 的本质区别

PostgreSQL 中的 OID 分为「系统级对象 OID」和「行级 OID」,二者完全独立:

类型 用途 是否废弃 示例
系统级对象 OID 标识数据库、表、索引、函数等数据库对象 从未废弃 数据库 OID=16384、表 OID=16389
行级 OID 标识用户表中的单行数据 已废弃 行 OID=16390(仅 WITH OIDS 表有)

你看到的 base/16384/16389 中:

  • 16384:是数据库的系统级 OID(每个数据库在集群中唯一);
  • 16389:是表的 relfilenode(早期等于表的系统级 OID,现在是独立的物理文件标识,但本质仍属于系统级对象标识体系)。

4.2. 为什么「废弃行级 OID」却仍保留路径中的 OID?

1. 系统级 OID 是 PostgreSQL 架构的「底层基石」

PostgreSQL 的元数据管理完全依赖「系统级对象 OID」—— 集群中每一个核心对象(数据库、表空间、表、索引、用户、函数等)都必须有唯一的系统级 OID,这是数据库启动、元数据查询、权限控制的基础,不可能废弃。

举个例子,你可以通过系统视图验证这些 OID 的核心作用:

-- 1. 查询当前数据库的系统级 OID(对应路径中的 16384) SELECT oid, datname FROM pg_database WHERE datname = current_database(); -- 输出:oid=16384, datname=postgres -- 2. 查询表的系统级 OID(早期 relfilenode = 表 OID) SELECT oid AS 表的系统OID, relname, relfilenode FROM pg_class WHERE relname = 'pgsql_ctid_demo'; -- 输出:表的系统OID=16389, relname=pgsql_ctid_demo, relfilenode=16389

这些系统级 OID 存储在 pg_database(数据库)、pg_class(表/索引)、pg_user(用户)等系统表中,是 PostgreSQL 识别「哪个文件对应哪个对象」的核心依据 —— 如果没有这些 OID,数据库根本无法定位到「postgres 数据库下的 pgsql_ctid_demo 表对应的物理文件是哪个」。

2. 「废弃的 OID」仅指「行级 OID」

PostgreSQL 官方「废弃 OID」的声明,仅针对用户表的「行级 OID」(即为每一行数据分配的 OID),原因是:

  • 行级 OID 是 32 位整数,单表数据量超过 40 亿时会重复;
  • MVCC 机制下,行更新会生成新 OID,无法作为稳定的行标识;
  • 有更优的替代方案(SERIAL/IDENTITY 列、UUID)。

但系统级对象 OID 不受影响:

  • 数据库/表的系统级 OID 分配频率极低(仅创建对象时分配),32 位足够用;
  • 系统级 OID 一旦分配永不改变(除非删除重建对象),稳定性极高;
  • 是数据库元数据的核心标识,无替代方案。

3. relfilenode 与表 OID 的关系(补充)

你提到的 16389 是表的 relfilenode(物理文件名称),它和表的系统级 OID 的关系:

  • 早期 PostgreSQL:relfilenode = 表的系统级 OID,因此文件路径直接用 OID 命名;
  • 现在 PostgreSQL:relfilenode 可独立于表 OID 变更(如执行 VACUUM FULLALTER TABLE 时,表的物理文件会重建,relfilenode 改变,但表的系统级 OID 不变)。

示例验证:

-- 查看表的初始 relfilenode SELECT relname, relfilenode FROM pg_class WHERE relname = 'pgsql_ctid_demo'; -- 输出:relfilenode=16389 -- 执行 VACUUM FULL 触发 relfilenode 变更 VACUUM FULL pgsql_ctid_demo; -- 再次查看 relfilenode SELECT relname, relfilenode FROM pg_class WHERE relname = 'pgsql_ctid_demo'; -- 输出:relfilenode=16400(已变更) -- 但表的系统级 OID 不变 SELECT oid FROM pg_class WHERE relname = 'pgsql_ctid_demo'; -- 输出:oid=16389(仍为原值)

此时表的物理文件路径会变为 base/16384/16400,但 16384(数据库 OID)始终不变 —— 这进一步说明:文件路径中的数字是「物理文件标识」,核心依赖系统级 OID 体系,与行级 OID 无关。

最后修改时间:2026-01-15 09:29:50
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论