行标识符的抉择:深入理解数据库领域的OID与ROWID机制
做数据库开发的人,大概都遇到过这样一个场景:某天你接手一个迁移项目,发现源端的存储过程里大量使用ROWID来做游标定位和行级追踪。你心里咯噔一下------这东西在新数据库里还灵不灵?
我确实踩过这个坑。一次Oracle迁移,对方有个跑了八年的计费系统,核心逻辑全靠ROWID撑着。当时我就想,有没有一个数据库能把OID和ROWID两套机制都讲明白、用扎实?
后来深度使用了KingbaseES(以下简称KES),才把这笔账彻底理清楚。这篇文章,我就从OID和ROWID入手,聊聊KES在这套行标识符机制上的设计思路和实践细节。
一、OID:从系统表走来,带着历史的痕迹
1. OID到底是什么?
OID全称是Object Identifier,也就是对象标识符。在KES里,它本质上是一个4字节的无符号整数,用来标记数据库里的各种"对象"------表、视图、函数、类型等都算。
你可以把它理解成数据库内部给每个对象发的"身份证号"。系统表里这个号是显式的,但普通用户表默认不生成OID。
验证一下当前KES的版本:
sql
test=# select version();
version
--------------------------------------------------------
KingbaseES V008R006C007B0024 on x86_64-pc-linux-gnu
(1 row)
2. 系统表的OID:全局连续,跨表统一
KES的系统表中,OID是全局唯一且连续递增的。这个"全局"意味着,不同类型的对象(比如一个类型和一个函数)会共享同一个OID计数器。
做个实验:先创建一个集合类型,再创建一个函数,观察它们的OID。
sql
-- 创建自定义集合类型
cpbd_test=> create type aatyp is table of int;
CREATE TYPE
cpbd_test=> select oid, typname from sys_type where typname = 'aatyp';
oid | typname
-------+---------
55136 | aatyp
(1 row)
-- 创建自定义函数
cpbd_test=> create or replace function func_test05(i int)
return int as
begin
return 1;
end;
CREATE FUNCTION
cpbd_test=> select oid, proname from sys_proc where proname = 'func_test05';
oid | proname
-------+-------------
55137 | func_test05
(1 row)
55136和55137,连续递增。这说明不同系统表之间,OID是打通的。
再验证一下OID的范围分布:
sql
test=# select count(*) from sys_type;
count
-------
1208
(1 row)
test=# select count(*) from sys_type where oid > 1208;
count
-------
1120
(1 row)
test=# select oid from sys_type where oid > 33990;
oid
-------
33992
33993
33995
(3 rows)
系统表的OID并不从1开始,也并非完全连续,但整体趋势是全局递增的。
3. 普通表的OID:局部自治,默认不可见
普通表的情况就完全不同了。在KES中,默认创建的普通表不带OID列:
sql
test=# create table tt5(id int);
CREATE TABLE
test=# insert into tt5 values(10);
INSERT 0 1
test=# select oid, id from tt5;
ERROR: column "oid" does not exist
要让普通表拥有OID,有两种方式:
方式一:修改GUC参数default_with_oids
sql
test=# set default_with_oids to true;
SET
test=# create table tt6(id int);
CREATE TABLE
test=# insert into tt6 values(10);
INSERT 0 1
test=# select oid, id from tt6;
oid | id
-----+----
1 | 10
(1 row)
方式二:建表时显式指定WITH OIDS
sql
test=# set default_with_oids to false;
SET
test=# create table tt7(id int) with oids;
CREATE TABLE
test=# insert into tt7 values(10);
INSERT 0 1
test=# select oid, id from tt7;
oid | id
-----+----
1 | 10
(1 row)
注意,tt6和tt7的OID都是从1开始独立计数的。这就是"局部自治"------每个普通表有自己的OID序列,互不干扰。
这一点和很多人的直觉不同。在其他一些数据库中,普通表的OID是全局的,但KES在这里做了差异化设计。背后的考虑其实很务实:全局OID需要维护一个全局计数器,高并发插入时会成为瓶颈;局部OID把压力分散到各个表,写入性能更好。
4. regclass:OID的语法糖
直接记数字OID不现实,KES提供了一个很方便的类型转换语法------regclass。
想知道某个OID对应什么表?
sql
test=# select 16700::regclass;
regclass
----------
teachers
(1 row)
查表结构时,传统写法要关联sys_class:
sql
SELECT attrelid, attname, atttypid, attlen, attnum, attnotnull
FROM sys_attribute
WHERE attrelid = (SELECT oid FROM sys_class WHERE relname = 'teachers');
用regclass简化后:
sql
SELECT attrelid, attname, atttypid, attlen, attnum, attnotnull
FROM sys_attribute
WHERE attrelid = 'teachers'::regclass;
'teachers'::regclass本质上就是SELECT oid FROM sys_class WHERE relname = 'teachers',但写法简洁了很多。这种OID别名类型在查询系统表时非常实用。
5. OID的局限性与使用建议
OID有几个先天不足:
首先是4字节的长度限制。4字节最大能表示约42.9亿个值,一旦超过就会从头开始循环。虽然可以通过创建唯一索引来避免重复,但数据量一大(比如超过20亿行),每次插入都要检查OID是否重复,性能会急剧下降。
其次是安全性问题。OID单调递增的特性,让攻击者可以通过观察OID的变化趋势来推测数据插入的频率和规模,在某些高安全要求的场景下这是个隐患。
最后是可移植性问题。OID是KES/PG体系特有的概念,在其他数据库中没有直接对应物。如果业务代码强依赖OID,未来再次迁移时会面临同样的困境。
因此,一个合理的建议是:系统表的管理可以依赖OID,但普通业务表的主键应该使用SERIAL或BIGSERIAL,而不是OID。如果表的预期规模超过20亿行,直接上BIGSERIAL。
二、ROWID:KES特有的行级逻辑标识
如果说OID是从系统表演进过来的"老兵",那ROWID就是KES专门为业务场景打造的"新锐"。
ROWID是一个伪列,建表时数据库会自动为每个表生成这个隐含列。它不实际存储在表中,但可以从表中查询。最关键的是,ROWID是逻辑唯一标识,不是物理地址。
1. ROWID的创建方式
方式一:通过GUC参数default_with_rowid
开启参数后,后续所有新建的表都会自动带上ROWID隐含列。需要注意的是,ROWID的优先级高于OID------两个参数同时开启时,OID不生效。
sql
test=# show default_with_oids;
on
test=# show default_with_rowid;
on
test=# create table tt11(id int);
CREATE TABLE
test=# insert into tt11 values(10);
INSERT 0 1
test=# select rowid, id from tt11;
rowid | id
---------------------------------+----
AAAAAAAAADQCAAAAAAAAAAA | 10
(1 row)
test=# select oid, id from tt11;
ERROR: column "oid" does not exist
方式二:建表时显式指定WITH ROWID
如果不想所有表都带ROWID,可以在建表时单独指定:
sql
test=# create table student(
sno int,
name varchar(10),
birthday date,
department varchar(10),
sex varchar(10)
) with rowid;
CREATE TABLE
test=# insert into student values(1, 'li', '2018-1-1', 'physics', 'boy');
test=# insert into student values(5, 'lu', '2018-1-2', 'chinese', 'boy');
test=# insert into student values(3, 'wang', '2018-1-3', 'english', 'girl');
test=# insert into student values(4, 'zhang', '2018-1-4', 'history', 'boy');
test=# insert into student values(2, 'jack', '2018-1-5', 'history', 'boy');
test=# select rowid, * from student;
rowid | sno | name | birthday | department | sex
---------------------------------+-----+-------+---------------------+------------+-----
AAAAAAAAADP5AAAAAAAAAAA | 1 | li | 2018-01-01 00:00:00 | physics | boy
AAAAAAAAADP5AAAAAAAAAAB | 5 | lu | 2018-01-02 00:00:00 | chinese | boy
AAAAAAAAADP5AAAAAAAAAAC | 3 | wang | 2018-01-03 00:00:00 | english | girl
AAAAAAAAADP5AAAAAAAAAAD | 4 | zhang | 2018-01-04 00:00:00 | history | boy
AAAAAAAAADP5AAAAAAAAAAE | 2 | jack | 2018-01-05 00:00:00 | history | boy
(5 rows)
查看表结构会发现,系统自动为ROWID创建了唯一约束和B-tree索引:
sql
test=# \d student
Table "public.student"
Column | Type | Collation | Nullable | Default
------------+----------------------------+-----------+----------+---------
sno | integer | | |
name | character varying(10 char) | | |
birthday | date | | |
department | character varying(10 char) | | |
sex | character varying(10 char) | | |
Indexes:
"student_rowid_key" UNIQUE CONSTRAINT, btree (rowid)
方式三:对已有表增加ROWID
sql
test=# alter table t set with rowid;
2. ROWID的数据类型细节
ROWID的表示形式是23个字符的64进制字符串,由A-Z、a-z、0-9、+、/这64个字符组成。实际存储是变长的,占用4到18个字节。
它的内部结构由三部分构成:
- 事务回卷次数(0-5位,32位)
- 插入时的事务XID(6-11位,32位)
- 事务内已插入元组的个数(12-22位,64位)
取值范围:
- 最小值:
AAAAAA+AAAAAB+AAAAAAAAAAA - 最大值:
D//////+D//////+P//////////
ROWID支持的关系操作符包括:=、>、>=、<、<=、!=。比较逻辑是:先比较高位的"事务回卷次数",再比较"事务XID",最后比较"事务内插入次数"。这个设计保证了后插入的数据ROWID一定大于先插入的ROWID------单调递增。
长度不对会直接报错:
sql
test=# insert into rowid_tt1 values('AAAAAAAAAAABAAAAAAAAAAA44444444');
ERROR: invalid input syntax for type rowid
3. ROWID的使用规范
ROWID可以在以下场景中使用:
- 投影列中直接查询
- WHERE条件中用于过滤(仅支持关系运算符)
- ORDER BY和GROUP BY中作为排序或分组依据
- 存储过程中正常调用
不支持的操作:
- 与关系运算符以外的运算符进行运算
- 插入、更新、删除ROWID的值
索引支持:ROWID可以创建B-tree索引和HASH索引。
4. ROWID与OID的互斥关系
这两个参数不能同时生效。当default_with_rowid和default_with_oids都为true时,ROWID的优先级更高。
反过来,如果default_with_oids为true而default_with_rowid为false时,显式创建WITH ROWID的表会报错:
sql
test=# set default_with_rowid to false;
SET
test=# set default_with_oids to true;
SET
test=# 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)!
三、OID vs ROWID:一张表看清差异
| 对比维度 | OID | ROWID |
|---|---|---|
| 适用范围 | 系统表全局,普通表局部 | 所有表(需显式或参数开启) |
| 数据类型 | 4字节无符号整数 | 23字符64进制字符串 |
| 存储方式 | 固定4字节 | 变长4-18字节 |
| 是否默认开启 | 普通表默认不开启 | 需参数或显式指定 |
| 唯一性保证 | 可能回卷重复 | 单调递增,自带唯一索引 |
| 主要用途 | 系统表元数据管理 | 业务表行级唯一标识 |
| 推荐使用场景 | 系统表查询、元数据关联 | 行快速定位、数据去重 |
四、生产环境中的选型建议
什么时候用OID?
坦白说,普通业务表不建议使用OID。4字节的长度限制和潜在的回卷风险,让它在生产环境中不够可靠。OID的真正价值在于系统表的管理和查询------用regclass语法糖查询元数据时,OID是最自然的选择。
什么时候用ROWID?
ROWID的使用场景更广一些。如果你的业务需要:
- 一个不需要显式维护的自增行标识
- 快速定位和追踪数据行
- 数据去重时需要一个稳定的行级标识
那么ROWID是个不错的选择。它自带唯一索引、单调递增、查询效率高,而且不会像OID那样有回卷风险。
什么时候用显式主键?
大多数时候,我仍然建议使用显式的业务主键或SERIAL/BIGSERIAL列。原因很简单:可移植性。主键是SQL标准的一部分,无论在哪个数据库中都能无缝工作。而OID和ROWID都是KES特有的机制,一旦业务代码强依赖它们,未来的数据库迁移就会重蹈Oracle ROWID的覆辙。
一个合理的组合策略是:
- 系统表管理:依赖OID + regclass
- 业务表行标识:使用显式主键 + 可选的ROWID作为辅助标识
- 高并发写入场景:优先考虑局部计数器方案,而非全局唯一标识
写在最后
OID和ROWID,一个是继承自经典数据库设计的"老物件",一个是KES为国产化场景量身打造的"新工具"。它们各有各的适用边界,也各有各的设计取舍。
理解这两套机制的差异,不只是为了写出正确的SQL,更是为了在数据库选型、迁移评估、性能优化时,能做出更明智的判断。毕竟,行标识符虽然只是数据库众多特性中的一个小点,但它牵连着存储引擎、事务机制、查询优化等多个核心模块。
希望这篇文章能帮你少踩一些我曾经踩过的坑。