引言
在关系型数据库的底层设计中,"如何唯一标识一行数据"是绕不开的核心问题。KingbaseES(KES)给出了自己的答案------不止一套,而是三套并行的机制:OID、ROWID、以及自增主键。
三者看似都在解决"行唯一性"这件事,实则定位迥异,各有专属的使用场景与技术边界。本文从源码设计出发,系统梳理这三套机制的实现原理、参数治理规则与工程实践中的选择逻辑。
一、OID:数据库对象的全局编号体系
1.1 OID 的双重身份
OID(Object Identifier,对象标识符)在 KES 中承担着两个截然不同的角色,这是很多人产生混淆的根源。
角色一:系统表的全局主键
KES 的绝大多数系统目录表(sys_class、sys_type、sys_proc 等)都以 OID 作为主键,且这个 OID 是跨系统表全局递增的。下面这段实验可以直接说明问题:
sql
-- 先建一个集合类型,sys_type 中 OID 落在 55136
CREATE TYPE aatyp IS TABLE OF INT;
SELECT oid, typname FROM sys_type WHERE typname = 'aatyp';
-- OID | TYPNAME
-- -------+---------
-- 55136 | aatyp
-- 紧接着建一个函数,sys_proc 中 OID 落在 55137
CREATE OR REPLACE FUNCTION func_test05(i INT) RETURN INT AS ...
SELECT oid, proname FROM sys_proc WHERE proname = 'func_test05';
-- OID | PRONAME
-- -------+-------------
-- 55137 | func_test05
两个不同系统表、两类不同对象,OID 连续递增------这不是巧合,而是 KES 全局 OID 生成器的设计结果。正因如此,你可以用一个 OID 值在整个数据库中定位到唯一的对象。
角色二:普通表的局部行号
这是 KES 与 PostgreSQL 的一处重要分叉:
| 特性 | PostgreSQL 普通表 OID | KES 普通表 OID |
|---|---|---|
| 作用域 | 全局唯一 | 仅表内局部 |
| 默认开启 | 历史版本默认开启 | 默认关闭 |
| 计数方式 | 全局递增 | 每表从 1 起独立计数 |
这意味着在 KES 中,不同表里可能存在 OID 值都为 1 的行,跨表用 OID 做关联是无效的。
1.2 如何为普通表开启 OID
sql
-- 方式一:修改 GUC 参数(影响后续所有新建表)
SET default_with_oids TO true;
CREATE TABLE tt6(id INT);
-- 方式二:建表时显式声明
CREATE TABLE tt7(id INT) WITH OIDS;
-- 查询时通过伪列访问
SELECT oid, id FROM tt7;
-- oid | id
-- -----+----
-- 1 | 10
OID 是 4 字节无符号整数,上限约 42.9 亿。超出后会循环重用,没有内置去重保障------这是它不适合做业务主键的根本原因。
1.3 regclass:OID 的快捷访问通道
KES 提供了一个实用的类型别名 regclass,让 OID 与对象名称之间的转换变得极为简洁:
sql
-- 用 OID 反查表名
SELECT 16700::regclass;
-- REGCLASS
-- ----------
-- teachers
-- 在系统表查询中替代子查询
-- 传统写法(冗长)
SELECT attname FROM sys_attribute
WHERE attrelid = (SELECT oid FROM sys_class WHERE relname = 'teachers');
-- regclass 写法(简洁)
SELECT attname FROM sys_attribute
WHERE attrelid = 'teachers'::regclass;
'表名'::regclass 本质上是一次类型转换,等价于从 sys_class 中查 OID,但省去了显式 JOIN,在编写元数据查询脚本时效率提升明显。
二、ROWID:KES 特有的行逻辑标识
2.1 设计定位
ROWID 是 KES 针对业务场景专门设计的行级逻辑唯一标识,在国产数据库中对标 Oracle 的 ROWID 概念,但实现机制有本质区别------KES 的 ROWID 是逻辑 ID,不是物理存储地址。
这意味着:
- 行迁移、页面整理等物理操作不会改变 ROWID
- ROWID 在整个数据库范围内单调递增
- 系统自动维护,无需开发者干预
2.2 开启方式与优先级规则
sql
-- GUC 参数方式(全局生效)
SET default_with_rowid TO true;
CREATE TABLE tt11(id INT);
INSERT INTO tt11 VALUES(10);
SELECT rowid, id FROM tt11;
-- ROWID | ID
-- -------------------------+----
-- AAAAAAAAADQCAAAAAAAAAAA | 10
建表时也可单独声明:
sql
CREATE TABLE student(
sno INT,
name VARCHAR(10),
birthday DATE,
department VARCHAR(10),
sex VARCHAR(10)
) WITH ROWID;
建表完成后,系统会自动创建 ROWID 唯一约束索引:
Indexes:
"student_rowid_key" UNIQUE CONSTRAINT, btree ("rowid")
优先级规则是 KES 参数治理中需要特别注意的一点:
default_with_rowid开启时,default_with_oids自动失效。
两个参数同时为 true,建表只生成 ROWID;default_with_oids 开启而 default_with_rowid 关闭时,强制用 WITH ROWID 建表会直接报错:
sql
SET default_with_rowid TO false;
SET default_with_oids TO true;
CREATE TABLE tt16(id INT) WITH ROWID;
-- ERROR: can not create table with rowid
-- (default_with_rowid is false, and default_with_oids is true)!
2.3 ROWID 数据类型深度解析
ROWID 的外观是一个 23 位字符串 ,字符集为 64 进制(A-Z、a-z、0-9、+、/),实际存储采用 4~18 字节变长结构,编码规则如下:
[事务回卷次数: 0~5位] + [插入时事务XID: 6~11位] + [事务内插入行号: 12~22位]
合法范围:
| 边界 | 值 |
|---|---|
| 最小值 | AAAAAA+AAAAAB+AAAAAAAAAAA |
| 最大值 | D//////+D//////+P////////// |
字符串长度不匹配将直接拒绝写入:
sql
INSERT INTO rowid_tt1 VALUES('AAAAAAAAAAABAAAAAAAAAAA44444444');
-- ERROR: invalid input syntax for type rowid
ROWID 支持的操作场景:
| 场景 | 支持情况 |
|---|---|
| SELECT 列表 | ✅ |
| WHERE 条件 | ✅ |
| ORDER BY | ✅ |
| GROUP BY | ✅ |
| 存储过程调用 | ✅ |
| 算术运算 | ❌ |
| 支持的比较操作符 | =、>、>=、<、<=、!= |
| 可建索引类型 | B-tree、HASH |
后插入的行 ROWID 严格大于先插入的行,单调性有保障,这使得 ROWID 可以用来进行基于插入顺序的高效排序与范围扫描。
三、核心机制对比:OID vs ROWID vs 自增主键
三套机制并存,工程实践中该如何选择?下表从多个维度做直接对比:
| 维度 | OID | ROWID | SERIAL / BIGSERIAL |
|---|---|---|---|
| 唯一性范围 | 系统表全局;普通表局部 | 全库全局 | 表内唯一 |
| 存储类型 | 4字节整数 | 变长字节(4~18字节) | 4/8字节整数 |
| 默认开启 | 否 | 否(需参数或DDL) | 否(需显式定义) |
| 自动索引 | 无 | 有(唯一B-tree) | 视主键声明而定 |
| 物理地址绑定 | 否 | 否(逻辑ID) | 否 |
| 上限与回绕 | ~42.9亿,会循环 | 理论上限极高 | BIGSERIAL约922亿亿 |
| 适用场景 | 系统表管理、元数据查询 | 业务行快速定位 | 业务主键 |
| 业务表推荐 | ❌ | ⚠️(辅助标识) | ✅ |
核心结论:
- 系统表操作、元数据查询 :用 OID,配合
regclass等别名类型降低 SQL 复杂度 - 需要快速行定位、兼容 Oracle ROWID 语义的场景:用 KES ROWID
- 业务表主键 :优先
SERIAL(数据量大时用BIGSERIAL),语义清晰,索引可控,跨数据库移植性好
四、GUC 参数治理与兼容性风险
4.1 参数组合的四种状态
default_with_oids=false / default_with_rowid=false → 默认行为,无隐藏列
default_with_oids=true / default_with_rowid=false → 新建表含 OID 伪列
default_with_oids=false / default_with_rowid=true → 新建表含 ROWID 伪列(推荐)
default_with_oids=true / default_with_rowid=true → ROWID 生效,OID 被覆盖
4.2 工程实践中的踩坑点
踩坑一 :SESSION 级修改参数后,只影响当前会话内新建的表,重连后恢复默认。生产环境如果要全局生效,必须写入 kingbase.conf。
踩坑二 :OID 列不会出现在 \d 描述或 SELECT * 结果中,容易误判为不存在。
踩坑三:从 PostgreSQL 迁移到 KES 时,如果业务代码依赖普通表 OID 的全局唯一性,会出现数据定位错误,需在迁移前逐一排查。
五、小结
KES 的行标识体系是一个层次分明的设计:OID 负责系统层面的对象寻址,ROWID 承接业务层面的行逻辑标识,自增主键则是开发者掌控的显式标识。
三者不互相替代,但彼此之间存在优先级规则和参数互斥约束。吃透这套机制,不仅能写出更高效的元数据查询,也能在迁移、调优、问题排查中少踩坑、快定位。