行标识符的抉择:深入理解数据库领域的OID与ROWID机制

0 阅读11分钟

行标识符的抉择:深入理解数据库领域的OID与ROWID机制

做数据库开发的人,大概都遇到过这样一个场景:某天你接手一个迁移项目,发现源端的存储过程里大量使用ROWID来做游标定位和行级追踪。你心里咯噔一下——这东西在新数据库里还灵不灵?

我确实踩过这个坑。一次Oracle迁移,对方有个跑了八年的计费系统,核心逻辑全靠ROWID撑着。当时我就想,有没有一个数据库能把OID和ROWID两套机制都讲明白、用扎实?

后来深度使用了KingbaseES(以下简称KES),才把这笔账彻底理清楚。这篇文章,我就从OID和ROWID入手,聊聊KES在这套行标识符机制上的设计思路和实践细节。

一、OID:从系统表走来,带着历史的痕迹

1. OID到底是什么?

OID全称是Object Identifier,也就是对象标识符。在KES里,它本质上是一个4字节的无符号整数,用来标记数据库里的各种“对象”——表、视图、函数、类型等都算。

你可以把它理解成数据库内部给每个对象发的“身份证号”。系统表里这个号是显式的,但普通用户表默认不生成OID。

验证一下当前KES的版本:

test=# select version();
                        version
--------------------------------------------------------
 KingbaseES V008R006C007B0024 on x86_64-pc-linux-gnu
(1 row)

2. 系统表的OID:全局连续,跨表统一

KES的系统表中,OID是全局唯一且连续递增的。这个“全局”意味着,不同类型的对象(比如一个类型和一个函数)会共享同一个OID计数器。

做个实验:先创建一个集合类型,再创建一个函数,观察它们的OID。

-- 创建自定义集合类型
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的范围分布:

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列:

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

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

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对应什么表?

test=# select 16700::regclass;
 regclass
----------
 teachers
(1 row)

查表结构时,传统写法要关联sys_class:

SELECT attrelid, attname, atttypid, attlen, attnum, attnotnull
FROM sys_attribute
WHERE attrelid = (SELECT oid FROM sys_class WHERE relname = 'teachers');

用regclass简化后:

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不生效。

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,可以在建表时单独指定:

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索引:

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

test=# alter table t set with rowid;

2. ROWID的数据类型细节

ROWID的表示形式是23个字符的64进制字符串,由A-Za-z0-9+/这64个字符组成。实际存储是变长的,占用4到18个字节。

它的内部结构由三部分构成:

  • 事务回卷次数(0-5位,32位)
  • 插入时的事务XID(6-11位,32位)
  • 事务内已插入元组的个数(12-22位,64位)

取值范围:

  • 最小值:AAAAAA + AAAAAB + AAAAAAAAAAA
  • 最大值:D////// + D////// + P//////////

ROWID支持的关系操作符包括:=>>=<<=!=。比较逻辑是:先比较高位的“事务回卷次数”,再比较“事务XID”,最后比较“事务内插入次数”。这个设计保证了后插入的数据ROWID一定大于先插入的ROWID——单调递增。

长度不对会直接报错:

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_rowiddefault_with_oids都为true时,ROWID的优先级更高。

反过来,如果default_with_oids为true而default_with_rowid为false时,显式创建WITH ROWID的表会报错:

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:一张表看清差异

对比维度OIDROWID
适用范围系统表全局,普通表局部所有表(需显式或参数开启)
数据类型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,更是为了在数据库选型、迁移评估、性能优化时,能做出更明智的判断。毕竟,行标识符虽然只是数据库众多特性中的一个小点,但它牵连着存储引擎、事务机制、查询优化等多个核心模块。

希望这篇文章能帮你少踩一些我曾经踩过的坑。