欢迎光临
我们一直在努力

行标识符机制的技术演进与实践(下)——ROWID与实战应用

文章目录

    • ROWID——兼容性与性能的平衡
      • Oracle的ROWID vs KES的ROWID
      • 参数互斥——一个容易踩的坑
      • ROWID类型——还能当列来用
    • 元数据访问——系统表的那些事儿
      • sys_class——对象的大管家
      • sys_type——类型系统
    • 兼容性治理——参数配置的艺术
      • 关键GUC参数一览
      • 参数冲突处理
    • 实战场景——ROWID能干啥
      • 场景一:批量删除优化
      • 场景二:游标更新
      • 场景三:并发控制
    • 一些思考
    • 结语

兼容

是对前人努力的尊重

是确保业务平稳过渡的基石

然而

这仅仅是故事的起点


在上一篇文章中,我们深入探讨了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生成的:

— 开启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 | 20180101 00:00:00
(1 row)

这个ROWID是23个字符的Base64编码,看起来挺长,实际存储只有4-18字节。它的组成是这样的:

  • 前6位:事务回卷次数(0-5)
  • 中间6位:事务XID
  • 后11位:事务内插入的行号

说实话,刚看到这个设计的时候我觉得挺有意思的,居然是用事务ID来生成。

— 同一事务里插入多行,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优先级更高。这个设计我当时研究了好一会儿才搞明白:

— 两个参数都开着
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表会报错:

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当数据类型用,这功能挺有意思的,我之前都没注意到:

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个字符,多了少了都不行:

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就是它的主键:

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关联,能查到对象的详细信息:

— 查表的字段
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里,包括基本类型、复合类型、域类型等:

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里登记:

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参数一览

— 大小写敏感,只能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,开了之后查询结果的列名会包含函数参数:

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的优先级高于全局参数
  • — 全局关着,但建表时指定
    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分批处理就好多了:

    — 开启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替代:

    — 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做乐观锁,比版本号字段简洁:

    — 查询时缓存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作为老牌国产数据库,在兼容性和自主性上的探索,值得学习。希望未来能看到更多技术突破,真正在关键领域替代国外产品。当然,这需要我们这些技术人员的共同努力,毕竟国产数据库的发展离不开用户的支持和反馈。


    赞(0)
    未经允许不得转载:171主机测评 » 行标识符机制的技术演进与实践(下)——ROWID与实战应用
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址