首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从 Oracle ROWID 到 PostgreSQL OID/CTID 的逻辑

从 Oracle ROWID 到 PostgreSQL OID/CTID 的逻辑

作者头像
用户3107127
发布2026-07-09 16:04:15
发布2026-07-09 16:04:15
90
举报

# 摘要:

关键词: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.创建测试表并插入数据

代码语言:javascript
复制
CREATETABLE IF NOT EXISTS ora_rowid_demo (
    id NUMBER PRIMARY KEY,
    name VARCHAR2(50)
);
INSERTINTO ora_rowid_demo VALUES (1, 'Oracle'), (2, 'ROWID'), (3, 'Demo');
COMMIT;

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

代码语言:javascript
复制
-- 核心查询:解析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)验证:

代码语言:javascript
复制
-- 替换为步骤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 验证:

代码语言:javascript
复制
-- 替换为步骤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 进一步确认块的归属(可选,需权限):

代码语言:javascript
复制
-- 查看指定数据文件+数据块的信息
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 各组成部分的变化:

代码语言:javascript
复制
-- 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. 触发行迁移(调整表存储参数+更新大字段)
ALTERTABLE 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 的用户表

代码语言:javascript
复制
-- 仅 PostgreSQL 11 及以下支持 WITH OIDS,12+ 需降级或使用系统表测试
CREATETABLE pgsql_oid_demo (
    id INT PRIMARY KEY,
    name VARCHAR(50)
) WITH OIDS; -- 显式启用 OID

-- 插入测试数据
INSERTINTO pgsql_oid_demo VALUES
    (1, 'PostgreSQL'), 
    (2, 'OID Demo'), 
    (3, 'Object ID');
COMMIT;

step 2.提取行的 OID

代码语言:javascript
复制
-- 查询表中行的 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)

代码语言:javascript
复制
-- 1. 确认表的物理文件路径(含数据库OID、表relfilenode)
SELECT
    pg_database.oidAS 数据库OID,
    pg_class.relfilenodeAS 表物理文件标识,
    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(完整可执行命令)
代码语言:javascript
复制
# 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 -b0 base/16384/16389 | grep-E'Block|Tuple|OID|ctid'
解析结果详解(含 OID 关联)

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

代码语言:javascript
复制
# 块级信息: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:

代码语言:javascript
复制
-- 查询系统表(如 pg_user)的 OID,确认不与用户表 OID 重复
SELECT oid, usename FROM pg_user LIMIT3;

-- 典型输出(无重复):
| oid  | usename  |
|------|----------|
| 10   | postgres |
| 16383 | testuser |
| 16384 | demo     |

-- 验证用户表 OID 未出现在系统表中
SELECT EXISTS (
    SELECT1FROM pg_user WHERE oid IN (16385, 16386, 16387)
) AS 是否重复;

-- 输出:是否重复 = f(false),验证 OID 全局唯一

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

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

代码语言:javascript
复制
-- 步骤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 亿),可通过系统参数和查询验证:

代码语言:javascript
复制
-- 查看 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,可通过以下查询验证:

代码语言:javascript
复制
-- 创建无 OID 的普通表
CREATETABLE 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.环境准备(创建测试表并插入数据)

代码语言:javascript
复制
-- 创建测试表
CREATETABLE pgsql_ctid_demo (
    id NUMERIC PRIMARY KEY
);

-- 插入 10 条测试数据
INSERTINTO 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):

代码语言:javascript
复制
-- 获取表的 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 解析物理文件(验证块号)
代码语言:javascript
复制
# 安装 pg_filedump(以 CentOS 为例)
yum install postgresql11-contrib -y

# 解析表的物理文件,查看块 0 的内容
pg_filedump -b0 /var/lib/pgsql/11/data/base/16384/16389

关键输出(简化)

代码语言:javascript
复制
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,插入大字段数据可触发块号递增,验证行号的块内唯一性:

代码语言:javascript
复制
-- 插入大字段数据,超出单个块容量
INSERTINTO 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
LIMIT5;

典型输出

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 行号/块号变更
代码语言:javascript
复制
-- 记录更新前行的 CTID
SELECT ctid, id FROM pgsql_ctid_demo WHERE id = 2;
-- 输出:ctid=(0,2), id=2

-- 更新行数据(生成新行版本)
UPDATE pgsql_ctid_demo SET id = 999WHERE 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 复用的影响

代码语言:javascript
复制
-- 执行 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 与物理存储的关联:

代码语言:javascript
复制
-- 安装 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 的核心作用:

代码语言:javascript
复制
-- 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 不变)。

示例验证:

代码语言:javascript
复制
-- 查看表的初始 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-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ByteHouse 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 摘要:
  • 1. Oracle ROWID:行的物理地址标识符
    • 1.1. 核心原理
    • 1.2. ROWID 组成部分的解析与验证
  • 2. PostgreSQL OID:被废弃的全局行标识
    • 2.1. 核心原理
    • 2.2. OID 核心维度的查询与验证
  • 3. PostgreSQL CTID:MVCC 驱动的行版本标识
    • 3.1. 核心原理
    • 3.2. CTID 组成部分的查询与验证
  • 4. OID
    • 4.1 两类 OID 的本质区别
    • 4.2. 为什么「废弃行级 OID」却仍保留路径中的 OID?
      • 1. 系统级 OID 是 PostgreSQL 架构的「底层基石」
      • 2. 「废弃的 OID」仅指「行级 OID」
      • 3. relfilenode 与表 OID 的关系(补充)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档