
兼容 是对前人努力的尊重 是确保业务平稳过渡的基石 然而 这仅仅是故事的起点
上篇讲了表空间自动创建和NFS环境变量的问题,这篇咱们接着聊,说说数据迁移中最让人头疼的行标识符问题。
那会儿我帮客户迁移数据,原库是Oracle,目标库是KES。迁移完了之后,客户说:"我们要按ROWID查询数据,之前Oracle就是这么玩的。"结果我执行了一条SELECT...WHERE ROWID = 'xxx',直接报错:ROWID不存在。
客户一脸懵逼:"Oracle明明有ROWID啊,你们这个数据库不行吧?"
我赶紧去查文档,发现KES确实有ROWID,但不是Oracle那种格式。另外还有OID,这俩玩意儿到底啥关系、咋用、啥时候用,文档写得云里雾里的,搞得我也很头大。
后来花了不少时间研究,总算搞清楚了这套机制。今天就把OID和ROWID扒个底朝天,让各位以后遇到类似问题不再踩坑。
行标识符的前世今生
为什么需要行标识符
先说个背景知识:为什么数据库需要行标识符?
你想啊,数据库里的数据是存在磁盘上的文件里的。一张表可能有几百万、几千万行数据,查询的时候总得有个方式定位到具体的某一行吧?主键可以定位数据行,但主键是业务层面的逻辑,不一定适合所有场景。
比如数据仓库里有些表没设主键,但你需要给每一行数据一个内部标识;比如你要做数据去重,需要比较两行数据是不是同一行;比如外部系统给你的数据没有主键,你得自己想办法追踪每一行。
这时候行标识符就派上用场了。它是数据库内部给每一行数据打的标签,跟业务无关,纯粹是存储引擎层面的定位机制。
技术演进:从Oracle到PG再到KES
说到行标识符的历史,得从Oracle讲起。
Oracle的ROWID是最早的也是最成熟的实现,格式大概是这样的:OOOOOOO.FBBBBB.DDDD,由对象号、文件号、块号、行号组成,18个字符。Oracle的ROWID是物理地址的直接表示,定位数据行飞快,所以很多Oracle DBA习惯用ROWID做快速查询和去重。
PostgreSQL走了另一条路。PG默认没有ROWID,但它有OID(Object Identifier)。OID是PostgreSQL内部的对象标识符,最初设计是给系统表用的,后来也被一些扩展用来标识数据行。PG的OID是全局连续的4字节整数,最大值42.9亿左右。
KES继承自PostgreSQL,保留了OID机制。但后来又引入了自己的ROWID,作为对业务场景的补充。这里有个很有意思的地方:KES的ROWID不是Oracle那种物理地址,而是一种逻辑标识符,格式和Oracle完全不同。
OID机制深度解析
OID是什么
OID全称是Object Identifier,对象标识符。KES用它来唯一标识数据库对象,比如表、视图、函数、类型这些。OID是个4字节的无符号整数,最大值大概42.9亿。
关键点来了:KES的普通表默认是没有OID的。
你创建一个普通表,然后想查OID,会发现这列根本不存在:
sql
-- 默认创建表
CREATE TABLE orders (id INT, name VARCHAR(50));
INSERT INTO orders VALUES (1, 'test');
-- 想查OID?
SELECT oid, id FROM orders;
-- 报错:列 "oid" 不存在
如果你想让表带上OID,有两种方式:
sql
-- 方式一:建表时指定 WITH OIDS
CREATE TABLE orders_with_oid (id INT) WITH OIDS;
INSERT INTO orders_with_oid VALUES (1);
-- 方式二:开启 GUC 参数
SET default_with_oids = true;
CREATE TABLE orders_auto_oid (id INT);
INSERT INTO orders_auto_oid VALUES (1);
开启之后,OID是隐藏列,你用SELECT *是看不到的,但可以直接查询:
sql
-- 查询OID
SELECT oid FROM orders_auto_oid;
-- 返回 1(表内自增)
系统表OID和普通表OID的区别
这里有个KES和PostgreSQL不太一样的地方,我之前被坑过。
系统表的OID是全局唯一的,跨表、跨对象连续分配。比如你创建一个自定义函数,然后再创建一个自定义类型,它们的OID是连续的:
sql
-- 创建函数,查看OID
CREATE OR REPLACE FUNCTION func_test() RETURNS INT AS $$
BEGIN
RETURN 1;
END;
$$ LANGUAGE plpgsql;
SELECT oid, proname FROM sys_proc WHERE proname = 'func_test';
-- 假设返回 oid=55136
-- 再创建类型
CREATE TYPE mytype AS (val INT);
SELECT oid, typname FROM sys_type WHERE typname = 'mytype';
-- 返回 oid=55137,和函数OID连续
普通表的OID是表内局部自增的,每个表有自己的OID序列,从1开始独立计数,跟其他表不搭噶。这个设计挺合理的,避免了业务表大量插入导致全局OID快速耗尽的问题。
sql
-- 创建两个表,都开启OID
SET default_with_oids = true;
CREATE TABLE table_a (id INT);
INSERT INTO table_a VALUES (1);
SELECT oid FROM table_a; -- 返回 1
CREATE TABLE table_b (id INT);
INSERT INTO table_b VALUES (1);
SELECT oid FROM table_b; -- 返回 1,不是 2!
regclass:OID的便捷别名
有个比较有意思的数据类型叫regclass,它是OID的语法糖,专门用来简化表名到OID的转换。
sql
-- 查某个表的OID
SELECT 'orders'::regclass::oid;
-- 返回类似 16452 这样的数字
-- 直接从系统表查也行
SELECT oid, relname FROM sys_class WHERE relname = 'orders';
regclass的好处是你可以传表名字符串,它自动帮你转换成OID。在写一些工具函数或者做元数据查询的时候特别方便。
OID回卷问题
重点来了!OID虽然能唯一标识数据行,但它有个致命缺陷:4字节整数最大值只有42.9亿左右,计数器溢出后会循环复用,出现重复。
这事儿听起来挺吓人,但实际上没那么恐怖。原因如下:
- 普通表的OID是表内局部计数的,一个表要插入42.9亿行才会回卷,这个数据量基本不可能达到
- 系统表虽然全局计数,但系统表数量有限,增长很慢
- 实际业务中,建议用SERIAL或BIGSERIAL做主键,别用OID
如果你确实担心OID重复的问题,可以在OID列上建唯一索引:
sql
CREATE TABLE t (id INT) WITH OIDS;
CREATE UNIQUE INDEX ON t(oid);
系统检测到重复OID会重新分配,直到找到唯一值。但话说回来,这方法只适合小表,大表会很慢。
最佳实践:OID适合系统内部使用,不建议在业务表中作为主键。真正需要唯一标识,用BIGSERIAL吧,能撑20亿以上的数据量,够用了。
sql
-- 正确的做法
CREATE TABLE business_table (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50)
);
-- 而不是用 OID 当主键
CREATE TABLE bad_example (
id INT
) WITH OIDS;
ROWID机制深度解析
和Oracle ROWID的区别
终于说到ROWID了,这玩意儿坑了我好久。
Oracle的ROWID是物理地址定位,格式固定:OOOOOOO.FBBBBB.DDDD(对象号.文件号.块号.行号)。KES的ROWID是逻辑标识符,由系统自动生成,格式是23个字符的Base64编码字符串。
两个东西完全不是一回事儿!
所以你从Oracle迁移到KES,想用WHERE ROWID = 'xxx'这种语法是行不通的。KES的ROWID是内部生成的,外部应用根本没法预测它的值。
ROWID长啥样
sql
-- 创建表,开启ROWID
CREATE TABLE test_table (
id INT,
name VARCHAR(50)
);
INSERT INTO test_table VALUES (1, 'Alice'), (2, 'Bob');
-- 查询ROWID
SELECT rowid, id, name FROM test_table;
-- 输出大概是这种格式:
-- rowid | id | name
-- -----------------------+----+------
-- AAAAAd9AABAAAGLTAAA | 1 | Alice
-- AAAAAd9AABAAAGLTAAH | 2 | Bob
注意:默认创建的表不会自动带ROWID。需要开启相关参数才行。
default_with_rowid vs default_with_oids
这俩参数是互斥的,ROWID优先级更高。
- default_with_oids:控制普通表是否默认带OID,默认false
- default_with_rowid:控制普通表是否默认带ROWID,默认...要看具体版本
关键点来了:这两个参数不能同时为true。如果两个都开了,ROWID会覆盖OIDS,意思是优先使用ROWID。
sql
-- 验证互斥
SET default_with_oids = true;
SET default_with_rowid = true;
CREATE TABLE test_both (id INT);
-- 这个表会带ROWID,不会带OID
ROWID支持比较操作符,可以用于排序和分组:
sql
-- 按ROWID排序(近似插入顺序)
SELECT * FROM test_table ORDER BY rowid;
-- 按ROWID去重
SELECT DISTINCT ON (rowid) * FROM test_table;
ROWID的实际应用场景
说真的,ROWID在KES里用得不多。几个可能用到的场景:
1. 没有主键的表需要唯一标识
sql
-- 某些历史遗留表没有主键
CREATE TABLE legacy_data (data TEXT);
-- 加个隐藏的rowid列用于内部追踪
ALTER TABLE legacy_data ADD COLUMN rowid TEXT GENERATED ALWAYS AS (rowid) STORED;
2. 数据导入时的去重参考
sql
-- 导入前查询现有数据的最大ROWID
SELECT MAX(rowid) FROM target_table;
-- 导入后比对新旧数据
SELECT * FROM source_data WHERE rowid > :last_rowid;
3. 近似插入顺序的查询
sql
-- 按插入顺序查询(适用于堆表)
SELECT * FROM heap_table ORDER BY rowid;
说实话,大部分场景下用主键或者序列就够了,ROWID更多是KES兼容某些Oracle语法的产物,实际业务中很少直接用。
三大技术点的关联思考
表空间管理、环境配置、行标识符的内在联系
写到这里回头看看文章的内容,我发现它们其实有一条暗线:都是KES在企业级部署和数据管理场景下的核心机制。
表空间管理决定数据"放在哪里",是存储规划的基础; 环境变量配置决定程序"能不能跑起来",是部署自动化的前提; 行标识符决定数据行"怎么被找到",是数据访问的底层支撑。
这三个机制相互独立,但在大型系统部署和数据迁移场景下,往往会同时遇到。我在客户现场翻车,某种意义上就是这三个环节同时出了问题。
运维最佳实践
基于这些经验,我总结了几条最佳实践:
1. 表空间管理
diff
- 生产环境建议关闭 auto_createtblspcdir,手动控制存储路径
- 路径规划要提前做,遵循统一的命名规范:/data/tbs_业务名_用途
- 表空间和数据目录分盘存放,避免IO竞争
- 定期监控表空间使用率,提前预警存储不足
2. 环境配置
diff
- 环境变量写在 .bashrc 里(重要!)
- NFS环境下部署前一定要 source ~/.bashrc
- 用检查脚本验证环境变量、挂载参数、权限设置
- 建立标准化的部署脚本模板,避免重复踩坑
3. 行标识符
diff
- 普通表不建议用OID当主键,用SERIAL或BIGSERIAL
- 从Oracle迁移时,ROWID语法需要改写
- 需要唯一标识时优先考虑BIGSERIAL,能撑20亿+
- OID更多是系统内部使用,不暴露给业务层
总结与展望
回顾上篇的两个坑
上篇聊的表空间自动创建和NFS环境变量问题,本质上都是KES在易用性上的优化。auto_createtblspcdir让部署流程少一步手动mkdir,NFS环境支持让共享存储场景下的安装更顺畅。但这些优化也带来了新的困惑点------默认值在不同版本间不一致、非登录Shell下环境变量不加载等等。
理解这些机制的工作原理,才能在出问题的时候快速定位,而不是瞎试一通浪费时间。
OID和ROWID的区别一句话总结
OID是系统级对象标识符,全局或表内唯一,适合元数据管理; ROWID是数据行逻辑标识符,23字符Base64编码,适合内部追踪。
两者都不是给业务表当主键用的,想用唯一标识老老实实用序列。
技术演进的思考
KES作为基于PostgreSQL内核研发的国产数据库,在保留PG核心特性的同时,也做了很多面向企业客户的定制优化。表空间目录自动创建、NFS环境下的部署支持、ROWID机制的引入,都是这种思路的体现。
但技术选型永远是在"功能丰富"和"简单易用"之间找平衡。功能越多,学习成本越高;默认行为越自动化,遇到特殊场景越容易懵。作为DBA,我们能做的就是理解这些机制的工作原理,在需要的时候能快速定位和解决问题。
下一步学习方向
如果你对KES的技术细节感兴趣,建议关注以下几个方向:
markdown
1. WAL日志机制和检查点优化
2. MVCC并发控制和事务隔离级别
3. 索引类型选择和执行计划分析
4. 备份恢复和高可用架构
好了,关于行标识符的坑就聊到这里。如果觉得有用,欢迎转发给需要的朋友,也欢迎来评论区交流你的踩坑经历