@[toc]

兼容 是对前人努力的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点
在上一篇文章中,我们深入探讨了OID机制,了解到KES的OID有全局和局部之分,还知道了它最大的坑就是回卷问题。说实话,OID这东西在系统表里挺好用的,但到了业务表就捉襟见肘了。我之前有个项目,就是因为用OID做业务主键,结果表数据一上来各种问题。那么问题来了,既然OID有这么多局限,KES又是如何解决行标识符问题的呢?答案就是ROWID------KES为了兼容Oracle而设计的另一套方案。
ROWID------兼容性与性能的平衡
聊完OID,再说说ROWID。这玩意儿是KES为了兼容Oracle搞出来的,但实现思路跟Oracle完全不一样。我记得第一次看到KES的ROWID实现时,还愣了一下------怎么跟Oracle差这么多?
Oracle的ROWID vs KES的ROWID
Oracle的ROWID是实打实的物理地址,包含文件号、块号、行号这些信息,直接能定位到磁盘上的位置,性能极其快。我之前做Oracle项目的时候,用ROWID做查询,那速度确实没得说。但有个问题,物理地址会变------表重建了、行迁移了,ROWID就变了,不能当长期标识用。这个坑我也踩过,当时以为ROWID是稳定的,结果做数据同步的时候各种对不上。
KES的ROWID不一样,它是逻辑标识,基于事务ID生成的:
sql
-- 开启ROWID支持
test=# set default_with_rowid = true;
SET
test=# create table student(
sno int,
name varchar(10),
birthday date
);
CREATE TABLE
test=# insert into student values(1, 'li', '2018-1-1');
INSERT 0 1
test=# select rowid, * from student;
rowid | sno | name | birthday
-------------------------+-----+------+---------------------
AAAAAAAAADQCAAAAAAAAAAA | 1 | li | 2018-01-01 00:00:00
(1 row)
这个ROWID是23个字符的Base64编码,看起来挺长,实际存储只有4-18字节。它的组成是这样的:
- 前6位:事务回卷次数(0-5)
- 中间6位:事务XID
- 后11位:事务内插入的行号
说实话,刚看到这个设计的时候我觉得挺有意思的,居然是用事务ID来生成。
sql
-- 同一事务里插入多行,ROWID递增
test=# insert into student values
(2, 'wang', '2018-1-2'),
(3, 'zhang', '2018-1-3');
INSERT 0 2
test=# select rowid, sno, name from student;
rowid | sno | name
-------------------------+-----+-------
AAAAAAAAADQCAAAAAAAAAAA | 1 | li
AAAAAAAAADQCAAAAAAAAAAB | 2 | wang
AAAAAAAAADQCAAAAAAAAAAC | 3 | zhang
(3 rows)
看到了吧,最后几个字符从AAA变成AAB、AAC,说明是同一个事务里的第二、三行。这个设计有个好处,ROWID单调递增,能用来排序。Oracle的ROWID就没这个特性,因为它是物理地址,没有顺序概念。不过话说回来,实际业务中用ROWID排序的场景好像也不多,主要是用来做唯一标识。
参数互斥------一个容易踩的坑
KES里default_with_rowid和default_with_oids这两个参数是互斥的,而且ROWID优先级更高。这个设计我当时研究了好一会儿才搞明白:
sql
-- 两个参数都开着
test=# show default_with_oids;
default_with_oids
-------------------
on
(1 row)
test=# show default_with_rowid;
default_with_rowid
--------------------
on
(1 row)
-- 实际生效的是ROWID
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
但有个反直觉的情况,如果default_with_oids开着、default_with_rowid关着,想在DDL里强制建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)!
这个报错信息挺让人困惑的,为啥全局参数开着就不让建?我后来想想,KES的设计思路大概是:要么全局用OID,要么全局用ROWID,两种机制混用容易出问题。虽然可以理解,但迁移的时候确实得小心,特别是那些只用部分表需要ROWID的场景。我当时就遇到过一个项目,大部分表用OID,个别表要迁移Oracle的ROWID逻辑,折腾了好久。
ROWID类型------还能当列来用
KES还支持把ROWID当数据类型用,这功能挺有意思的,我之前都没注意到:
sql
test=# create table rowid_tt1(id rowid);
CREATE TABLE
test=# insert into rowid_tt1 values('AAAAAAAAAAABAAAAAAAAAAA');
INSERT 0 1
test=# select * from rowid_tt1;
id
-------------------------
AAAAAAAAAAABAAAAAAAAAAA
(1 row)
-- 可以比较大小
test=# insert into rowid_tt1 values('AAAAAAAAAAABAAAAAAAAAAB');
INSERT 0 1
test=# insert into rowid_tt1 values('AAAAAAAAAAABAAAAAAAAAAC');
INSERT 0 1
test=# select * from rowid_tt1 where id >= 'AAAAAAAAAAABAAAAAAAAAAB';
id
-------------------------
AAAAAAAAAAABAAAAAAAAAAB
AAAAAAAAAAABAAAAAAAAAAC
(2 rows)
不过要注意,ROWID类型只支持23个字符,多了少了都不行:
sql
test=# insert into rowid_tt1 values('AAAAAAAAAAABAAAAAAAAAAA44444444');
ERROR: invalid input syntax for type rowid: "AAAAAAAAAAABAAAAAAAAAAA44444444"
test=# insert into rowid_tt1 values('AAAAAAAAAAABAAAAA');
ERROR: invalid input syntax for type rowid: "AAAAAAAAAAABAAAAA"
这个特性在数据迁移时挺有用的,可以把Oracle导出的ROWID直接存进来,虽然逻辑意义不一样,但至少不会报错。我之前有个迁移项目,Oracle那边大量使用ROWID做关联,迁移到KES的时候这个功能帮了大忙。
元数据访问------系统表的那些事儿
说到行标识符,就不得不提系统表。KES的系统表设计有些细节不一样。我记得刚接触KES的时候,还专门花时间研究了这些系统表,毕竟要做一些数据库管理工具。
sys_class------对象的大管家
所有数据库对象(表、索引、视图、序列)的信息都在sys_class里,OID就是它的主键:
sql
test=# select oid,relname,relkind from sys_class
where relname like 't%' limit 10;
oid | relname | relkind
-------+----------------+---------
16700 | teachers | r
16706 | teachers_pkey | i
16708 | teachers_copy | r
24903 | tt1 | r
16733 | ttab | r
(10 rows)
relkind字段表示对象类型:r是普通表,i是索引,v是视图,S是序列。通过OID关联,能查到对象的详细信息:
sql
-- 查表的字段
test=# select attname,atttypid,attnum
from sys_attribute
where attrelid = 16700;
attname | atttypid | attnum
--------------+----------+--------
tableoid | 26 | -6
cmax | 29 | -5
xmax | 28 | -4
cmin | 29 | -3
xmin | 28 | -2
ctid | 27 | -1
teacher_id | 1700 | 1
teacher_name | 1043 | 2
(8 rows)
注意前面几个负数字段,tableoid、cmax、xmax这些,都是系统字段,平时查select *看不到,但确实存在。ctid就是物理位置标识,格式是(块号,行号),比如(0,1)表示第0块第1行。这个ctid在某些优化场景下挺有用的,比如避免全表扫描。
sys_type------类型系统
KES的类型信息在sys_type里,包括基本类型、复合类型、域类型等:
sql
test=# select oid,typname,typtype from sys_type
where typname in ('int4','varchar','rowid');
oid | typname | typtype
-------+---------+---------
23 | int4 | b
1043 | varchar | b
34010 | rowid | b
(3 rows)
typtype='b'表示基本类型,'c'是复合类型,'d'是域类型。创建自定义类型也会在sys_type里登记:
sql
test=# create type address_type as (
city varchar(50),
street varchar(100)
);
CREATE TYPE
test=# select oid,typname,typtype from sys_type
where typname = 'address_type';
oid | typname | typtype
-------+---------------+---------
34567 | address_type | c
(1 row)
这些元数据在数据库逆向工程、迁移工具开发时特别有用,但要注意,系统表结构可能随版本变化,别写死了。我之前有个工具就是写死了系统表字段,结果KES升级后全挂了,后来改成动态查询才解决。
兼容性治理------参数配置的艺术
KES在兼容Oracle方面做了很多工作,但也引入了复杂的参数体系。有些参数必须初始化时设置,有些可以运行时修改,搞混了会很麻烦。说实话,我第一次看到这么多参数的时候头都大了。
关键GUC参数一览
sql
-- 大小写敏感,只能initdb时设置
test=# show enable_ci;
enable_ci
-----------
off
(1 row)
-- ROWID支持,可以运行时改
test=# show default_with_rowid;
default_with_rowid
--------------------
off
(1 row)
-- OID支持,可以运行时改
test=# show default_with_oids;
default_with_oids
-------------------
off
(1 row)
enable_ci这个参数要特别注意,只能在initdb时设置。如果应用对大小写敏感,必须初始化时关掉,否则后期没法改。我之前有个项目就因为这个参数设置错误,导致后面各种问题,最后不得不重建数据库。
还有个坑是enable_func_colname,开了之后查询结果的列名会包含函数参数:
sql
test=# set enable_func_colname=on;
SET
test=# select sum(1+2);
sum(1+2)
----------
3
(1 row)
test=# set enable_func_colname=off;
SET
test=# select sum(1+2);
sum
-----
3
(1 row)
这个参数在迁移Oracle应用时可能有用,但也会导致某些ORM框架识别不了列名,得权衡利弊。我记得有个Spring项目就因为这个参数,MyBatis映射全乱了。
参数冲突处理
前面说过ROWID和OID互斥,实际处理逻辑是这样:
- 如果default_with_rowid=true,优先创建ROWID隐含列
- 如果default_with_rowid=false但default_with_oids=true,创建OID隐含列
- DDL中with rowid的优先级高于全局参数
sql
-- 全局关着,但建表时指定
test=# set default_with_rowid to false;
SET
test=# set default_with_oids to false;
SET
test=# create table t1(id int) with rowid;
CREATE TABLE
test=# \d t1
Table "public.t1"
Column | Type | Collation | Nullable | Default
--------+---------+-----------+----------+---------
id | integer | | |
Indexes:
"t1_rowid_key" UNIQUE CONSTRAINT, btree ("rowid")
-- 全局开着,建表时指定OID会被忽略
test=# set default_with_rowid to true;
SET
test=# create table t2(id int) with oids;
CREATE TABLE
test=# select rowid, id from t2 limit 1;
rowid | id
-------------------------+----
AAAAAAAAADQCAAAAAAAAAAA | 1
(1 row)
这种参数优先级设计虽然合理,但容易让人误以为with oids会生效,实际测试验证才是正道。我之前就犯过这个错,以为建表时指定with oids就能用OID,结果运行时发现用的是ROWID,还好及时测试发现了。
实战场景------ROWID能干啥
理论说了半天,实际项目中ROWID能干嘛?说几个我遇到的场景,应该能给大家点启发。
场景一:批量删除优化
大表删除是个头疼事,直接delete where status='inactive'可能锁表锁半天。用ROWID分批处理就好多了:
sql
-- 开启ROWID
set default_with_rowid = true;
-- 建测试表
create table log_table(
id serial,
log_time timestamp,
status varchar(20)
) with rowid;
-- 插入100万测试数据
insert into log_table(log_time, status)
select now() - (random() * 365 || ' days')::interval,
case when random() > 0.3 then 'ACTIVE' else 'INACTIVE' end
from generate_series(1, 1000000);
-- 分批删除
do $$
declare
batch_size int := 10000;
deleted_count int;
begin
loop
delete from log_table
where rowid in (
select rowid from log_table
where status = 'INACTIVE'
limit batch_size
);
get diagnostics deleted_count = row_count;
exit when deleted_count = 0;
commit;
end loop;
end $$;
这个方法的好处是每次只删1万条,锁表时间短,不会影响其他业务。ROWID有索引,查起来也快。我之前有个日志表,累积了几千万条数据要清理,用这个方法跑了一晚上就搞定了。
场景二:游标更新
Oracle里常用的CURRENT OF游标更新,KES可以用ROWID替代:
sql
-- Oracle写法
/*
declare
cursor c1 is select * from emp for update;
begin
for rec in c1 loop
update emp set sal = sal * 1.1
where current of c1;
end loop;
end;
*/
-- KES写法
declare
cursor c1 is select rowid, * from emp for update;
v_rowid rowid;
begin
for rec in c1 loop
v_rowid := rec.rowid;
update emp set sal = sal * 1.1
where rowid = v_rowid;
end loop;
end;
虽然多查了个rowid字段,但逻辑清晰,也避免了CURRENT OF在某些场景下失效的问题。这个我在迁移存储过程的时候用过,效果还不错。
场景三:并发控制
用ROWID做乐观锁,比版本号字段简洁:
sql
-- 查询时缓存ROWID
select id, name, rowid from user_profile where user_id = 1001;
-- 更新时验证
update user_profile
set score = 90
where user_id = 1001 and rowid = 'AAAAAAAAADQCAAAAAAAAAAA';
-- 如果影响行数为0,说明数据已被他人修改
-- 应用层可以提示用户刷新
这种写法不用额外加version字段,利用ROWID自带的唯一性做校验,适合简单场景。但要注意,ROWID在DELETE后可能被重用,高并发场景还是要用version字段更稳妥。我之前有个低频更新的配置表,就用这个方法,简化了不少代码。
一些思考
研究完这些机制,感觉KES的设计团队是在做一道平衡题。一方面要兼容Oracle生态,让迁移成本尽量低;另一方面又要保持技术独立性,不能完全照搬Oracle的实现。说实话,这个平衡挺难找的。
OID这套东西,从PG继承过来后做了本地化改造,全局变局部,解决了并发性能问题,但也牺牲了全局唯一性。这个取舍见仁见智,看具体应用场景。我个人觉得,如果KES能提供一个开关,让用户选择是用全局还是局部OID,可能会更好一点------当然,这会增加实现复杂度。
ROWID的实现更典型,Oracle用物理地址,KES用逻辑标识。物理地址快但不稳定,逻辑标识慢点但可排序、可持久化。哪种更好?没有标准答案,取决于你的业务需求。不过从我个人的经验来看,逻辑标识在实际应用中更容易处理一些,至少不用担心表重建后ROWID变的问题。
参数体系也是个双刃剑,兼容性参数越多,迁移越容易,但配置复杂度也越高。default_with_rowid和default_with_oids互斥这个设计,我能理解背后的逻辑,但确实让初学者容易踩坑。我记得刚开始用KES的时候,被这些参数折腾了好久,后来才慢慢搞清楚优先级关系。
从技术演进的角度看,早期数据库追求标准化,希望所有数据库都遵循同一套SQL规范。后来发现不现实,Oracle、MySQL、SQL Server各有各的特色,用户选型后就被绑定死了。国产数据库要突围,兼容性是必经之路,但兼容到什么程度、如何保持自身特色,是门艺术。
KES在OID和ROWID上的实践,算是找到了一个相对平衡的点。既提供了Oracle兼容性,又有自己的技术实现,虽然细节上还有些粗糙,但方向是对的。未来如果能进一步简化参数配置,提供更清晰的迁移指南,相信会有更多人愿意尝试。
结语
行标识符这东西,平时开发可能不太注意,但深入进去会发现涉及很多底层机制。OID的系统表全局性、ROWID的逻辑标识设计、参数互斥的处理逻辑,每一块都体现了数据库设计者的思考。
技术选型时,别被表面功能迷惑,要深入理解实现原理。Oracle的ROWID快,但那是物理地址换来的;KES的ROWID兼容,但实现逻辑完全不同。迁移时如果不搞清楚这些细节,很容易掉坑。我之前就犯过这个错,以为ROWID都是一样的,结果各种问题。
最后说一句,国产数据库这几年进步很快,从单纯模仿到自主创新,路还很长。KES作为老牌国产数据库,在兼容性和自主性上的探索,值得学习。希望未来能看到更多技术突破,真正在关键领域替代国外产品。当然,这需要我们这些技术人员的共同努力,毕竟国产数据库的发展离不开用户的支持和反馈。