引言
做 Oracle 兼容的国产数据库都清楚,rowid 这个特性点始终是绕不过去的。虽然有一些等价修改方案,但并非适用于所有场景。大部分国产数据库早早实现了 rowid,而 GaussDB 的 rowid 却迟迟没动静,直到今年发布的 507 内核版本,rowid 总算千呼万唤始出来。
三年前我写过一篇关于 openGauss 的 rowid 替代方案(未公开发布),当时用到了 with (hasuids=on),系统会自动创建一个 gs_tuple_uid 字段作为行的唯一标识。但它插入时为空,要更新第一次后才确定值。也就是说,确定表内唯一行得用 nvl(gs_tuple_uid,ctid) 之类的写法,可两列数据类型不同,处理起来略显别扭。而且新增物理存储列会导致插入性能下降。
我在之前另一篇公开文章中,曾详细介绍过 rowid 的诸多使用场景及表现,并测试了不少国产数据库,但几乎所有源自 PostgreSQL 的数据库都无法在不新增物理字段存储的情况下,满足那篇文章里列出的所有 rowid 使用场景。这次我就拿之前文章里的场景,在 GaussDB 5.0.7 版本上重新测一测,看看到底什么情况。
首先要说明的是,GaussDB 的 rowid 不是默认启用的。创建表时需要额外指定表属性才能带上 rowid 字段,例如 create table t_test(a int) with(hasrowid); ,而且无法通过修改 GUC 参数全局打开。
本文测试环境为 GaussDB 507.0.0 集中式。内核部分行为在分布式下存在差异。
rowid 功能测试
1. select * 不显示 rowid
gaussdb=# create table test_rowid_update(id number,content varchar2(200)) with(hasrowid);
CREATE TABLE
gaussdb=# insert into test_rowid_update values (1,'1');
INSERT 0 1
gaussdb=# select * from test_rowid_update ;
id | content
----+---------
1 | 1
(1 row)
gaussdb=# drop table test_rowid_update;
DROP TABLE
gaussdb=#
测试通过。
2. select rowid 不报错
gaussdb=# create table test_rowid_update(id number,content varchar2(200)) with(hasrowid);
CREATE TABLE
gaussdb=# insert into test_rowid_update values (1,'1');
INSERT 0 1
gaussdb=# select rowid,t.rowid,t.* from test_rowid_update t ;
rowid | rowid | id | content
-------------------------------+-------------------------------+----+---------
AAAAAAAAAP//AAAKUYAAAAAAAAAAB | AAAAAAAAAP//AAAKUYAAAAAAAAAAB | 1 | 1
(1 row)
gaussdb=# drop table test_rowid_update;
DROP TABLE
gaussdb=#
测试通过。
3. 通过 rowid 快速检索数据
gaussdb=# create table test_rowid_update(id number,content varchar2(200)) with(hasrowid);
CREATE TABLE
gaussdb=# insert into test_rowid_update select id,'id_'||id from generate_series(1,1000) id;
INSERT 0 1000
gaussdb=# analyze test_rowid_update;
ANALYZE
gaussdb=# select rowid from test_rowid_update where content='id_50';
rowid
-------------------------------
AAAAAAAAAP//AAAKLvAAAAAAAAAAy
(1 row)
gaussdb=#
gaussdb=# explain analyze select t.* from test_rowid_update t where rowid ='AAAAAAAAAP//AAAKLvAAAAAAAAAAy';
QUERY PLAN
------------------------------------------------------------------------------------------------------
--------------------------------------------
Index Scan using test_rowid_update_rowno_idx on test_rowid_update t (cost=0.00..2.27 rows=1 width=11
) (actual time=0.024..0.025 rows=1 loops=1)
Index Cond: (rowno = 50::bigint), (Expression Flatten Optimized)
Filter: (tableoid = 41711::oid), (Expression Flatten Optimized)
Total runtime: 0.260 ms
(4 rows)
gaussdb=# explain analyze select t.* from test_rowid_update t where rowid = (select rowid from test_rowid_update where content='id_50') ;
QUERY PLAN
------------------------------------------------------------------------------------------------------
----------------------------------------------
Index Scan using test_rowid_update_rowno_idx on test_rowid_update t (cost=35.00..37.28 rows=1 width=
11) (actual time=0.843..0.845 rows=1 loops=1)
Index Cond: (rowno = rowid_sequence($1)), (Expression Flatten Optimized)
Filter: (tableoid = rowid_tableoid($0)), (Expression Flatten Optimized)
InitPlan 1 (returns $0)
-> Seq Scan on test_rowid_update (cost=0.00..17.50 rows=1 width=24) (actual time=0.050..0.368 r
ows=1 loops=1)
Filter: ((content)::text = 'id_50'::text), (Expression Flatten Optimized)
Rows Removed by Filter: 999
InitPlan 2 (returns $1)
-> Seq Scan on test_rowid_update (cost=0.00..17.50 rows=1 width=24) (actual time=0.063..0.438 r
ows=1 loops=1)
Filter: ((content)::text = 'id_50'::text), (Expression Flatten Optimized)
Rows Removed by Filter: 999
Total runtime: 1.121 ms
(12 rows)
gaussdb=# explain analyze select t.* from test_rowid_update t where rowno = (select rowno from test_rowid_update where content='id_50');
QUERY PLAN
------------------------------------------------------------------------------------------------------
----------------------------------------------
Index Scan using test_rowid_update_rowno_idx on test_rowid_update t (cost=17.50..19.77 rows=1 width=
11) (actual time=0.308..0.309 rows=1 loops=1)
Index Cond: (rowno = $0), (Expression Flatten Optimized)
InitPlan 1 (returns $0)
-> Seq Scan on test_rowid_update (cost=0.00..17.50 rows=1 width=8) (actual time=0.040..0.287 ro
ws=1 loops=1)
Filter: ((content)::text = 'id_50'::text), (Expression Flatten Optimized)
Rows Removed by Filter: 999
Total runtime: 0.417 ms
(7 rows)
gaussdb=# drop table test_rowid_update;
DROP TABLE
gaussdb=#
这个用法本身能跑通,但执行计划有毛病。在 rowid in select rowid 的场景下出现了两个 InitPlan,额外多扫描了一次表;而换成 rowno 后就只有一个 InitPlan(后面会介绍 rowno 和 rowid 的关系)。
4. 对同一个 rowid 进行多次更新
gaussdb=# create table test_rowid_update(id number,content varchar2(200)) with(hasrowid);
CREATE TABLE
gaussdb=# insert into test_rowid_update values (1,'1');
INSERT 0 1
gaussdb=#
gaussdb=# declare
gaussdb-# i int;
gaussdb-# begin
gaussdb$# for rec in (select rowid rd from test_rowid_update) loop
gaussdb$# update test_rowid_update set content='2' where rowid=rec.rd;
gaussdb$# update test_rowid_update set content='3' where rowid=rec.rd;
gaussdb$# end loop;
gaussdb$# select 1 into i from test_rowid_update where id=1 and content='3';
gaussdb$# end;
gaussdb$# /
ANONYMOUS BLOCK EXECUTE
gaussdb=# select * from test_rowid_update where id=1 and content='3';
id | content
----+---------
1 | 3
(1 row)
gaussdb=# drop table test_rowid_update ;
DROP TABLE
gaussdb=#
测试通过。这说明同一个 rowid 可以定位同一行,即使该行被更新过。
5. 删除表中重复记录
gaussdb=# create table test_rowid_update with(hasrowid) as select id,'n_'||id as content from generate_series(1,10) id;
INSERT 0 10
gaussdb=#
gaussdb=# insert into test_rowid_update select id,'d_'||id from generate_series(1,5) id;
INSERT 0 5
gaussdb=#
gaussdb=# DELETE FROM test_rowid_update t1
gaussdb-# WHERE t1.rowid > (
gaussdb(# SELECT MIN(t2.rowid)
gaussdb(# FROM test_rowid_update t2
gaussdb(# WHERE t1.id = t2.id
gaussdb(# );
DELETE 5
gaussdb=# select * from test_rowid_update;
id | content
----+---------
1 | n_1
2 | n_2
3 | n_3
4 | n_4
5 | n_5
6 | n_6
7 | n_7
8 | n_8
9 | n_9
10 | n_10
(10 rows)
gaussdb=#
测试通过。
通过以上基本测试可以发现,GaussDB 的这个 rowid 没有出现执行结果错误的情况。但这其实是有代价的。
rowid 引入的元数据
先看一眼 pg_attribute:
gaussdb=# create table test_rowid_update(id number,content varchar2(200)) with(hasrowid);
CREATE TABLE
gaussdb=# select attname,atttypid,attlen,attnum,atttypmod,attbyval from pg_attribute where attrelid='test_rowid_update'::regclass;
attname | atttypid | attlen | attnum | atttypmod | attbyval
--------------+----------+--------+--------+-----------+----------
rowno | 20 | 8 | -13 | -1 | t
rowid | 8662 | 24 | -12 | -1 | f
xc_node_hash | 23 | 4 | -11 | -1 | t
xc_node_id | 23 | 4 | -8 | -1 | t
tableoid | 26 | 4 | -7 | -1 | t
cmax | 29 | 4 | -6 | -1 | t
xmax | 28 | 8 | -5 | -1 | t
cmin | 29 | 4 | -4 | -1 | t
xmin | 28 | 8 | -3 | -1 | t
ctid | 27 | 6 | -1 | -1 | f
id | 1700 | -1 | 1 | -1 | f
content | 1043 | -1 | 2 | 204 | f
(12 rows)
可以看到新增了两个隐藏字段:rowid 和 rowno。新增这样的字段,往往会在这个字段上创建唯一索引或者主键:
gaussdb=# create table t_test_rowid(id int,a text) WITH ( hasrowid = ON );
CREATE TABLE
gaussdb=# select pg_get_tabledef('t_test_rowid');
pg_get_tabledef
------------------------------------------------------------------------------------------------------
-----------------------------
SET search_path = public;
+
CREATE TABLE t_test_rowid (
+
id integer,
+
a text
+
)
+
WITH (orientation=row, hasrowid=on, compression=no, storage_type=USTORE, segment=off);
+
CREATE UNIQUE INDEX t_test_rowid_rowno_idx ON t_test_rowid USING ubtree (rowno) WITH (storage_type=US
TORE) TABLESPACE pg_default;
(1 row)
gaussdb=#
可以看到 GaussDB 选择的是创建唯一索引,而不是主键。索引名称是表名后面接 _idx。实测发现,如果 pg_class 中已存在同名对象,那么创建这个表时,索引名称会变成 t_test_rowid_rowno_idx1。
千万别手贱把自动创建的索引加成主键。自动创建的索引不能直接删除,删主键默认会级联删除索引,但这里会删除报错。
接着看 rowno 的值——是个整数,从 1 开始:
gaussdb=# drop table if exists t_test_rowid;
DROP TABLE
gaussdb=# create table t_test_rowid(id int,a text) WITH ( hasrowid = ON );
CREATE TABLE
gaussdb=# insert into t_test_rowid select id,'xxxxxxxxxx' from pg_catalog.generate_series (1,3) id;
INSERT 0 3
gaussdb=# select * from t_test_rowid;
id | a
----+------------
1 | xxxxxxxxxx
2 | xxxxxxxxxx
3 | xxxxxxxxxx
(3 rows)
gaussdb=# select rowid,rowno,* from t_test_rowid;
rowid | rowno | id | a
-------------------------------+-------+----+------------
AAAAAAAAAP//AAAKKfAAAAAAAAAAB | 1 | 1 | xxxxxxxxxx
AAAAAAAAAP//AAAKKfAAAAAAAAAAC | 2 | 2 | xxxxxxxxxx
AAAAAAAAAP//AAAKKfAAAAAAAAAAD | 3 | 3 | xxxxxxxxxx
(3 rows)
gaussdb=#
有自增值,那必然有个序列:
gaussdb=# select relname,relkind from pg_class where relname like 't_test_rowid%' and relowner=pg_current_userid();
relname | relkind
------------------------+---------
t_test_rowid | r
t_test_rowid_rowno_idx | i
t_test_rowid_seq | S
(3 rows)
gaussdb=# \\x
Expanded display is on.
gaussdb=# select * from t_test_rowid_seq;
-[ RECORD 1 ]-+--------------------
sequence_name | t_test_rowid_seq
last_value | 10
start_value | 1
increment_by | 1
max_value | 9223372036854775807
min_value | 1
cache_value | 10
log_cnt | 32
is_cycled | f
is_called | t
uuid | 0
限制来了:最大值 9223372036854775807,也就是说最多只能插入这么多行。注意这不是表最多容纳这么多行,而是纯指 insert 的累计总行数。而且 cache 是 10,如果是短连接,在没开启 GaussDB 507 版本新特性——节点级序列缓存的情况下,序列值的可用数量能掉到十分之一,甚至更低。
在我之前关于 rowid 的文章中,有过这样的分析:
PG 系如果要支持 rowid,目前已知大致有四种方案:
- 借用 ctid。但分区表的不同分区里可能出现相同的 ctid,因此需要结合 tableoid 和 ctid 两个字段;如果是分布式的,还需要结合节点字段。不过记录被 update 时,ctid 一定会变,不能使用同一个 ctid 对某条记录进行两次更新。
- 在表上直接加一个物理存储的列用于存储 rowid,并给它加上唯一约束或索引,其值本身可能直接使用一个全局序列的值。
- 也是在表上直接加一个物理存储的列,但需要引入每个表的序列管理,这个序列值结合 tableoid 计算出一个 base64 编码的值,存到这个表里。
- 新增一种存储引擎,update 数据时在原行上更新,不像 astore 那样更新实际上是插入了新行。
简单来说,PG 系 rowid 的实现其实也就两种:一种是再建一个字段存储唯一值,另一种就是 ctid 或其变种。前者带来大量性能损耗,后者一 update 就会变。
众所周知,Oracle 的 rowid 是一个没有实际存储的虚拟列,它的值表示这一行数据所在的物理地址,因此 rowid 的存在本身并不会对数据插入带来性能损耗。
而 GaussDB 比较像第 3 种方案。只不过它只把序列的值进行了物理存储,即 rowno,然后 rowno 结合 tableoid 计算出 rowid 作为虚拟列。
这就很奇怪了。明明 GaussDB 有了 ustore,且为默认存储引擎,天然具备 ctid 稳定的优势,完全可以用 tableoid 结合 ctid 去做 rowid,为何还要搞个有实际存储的列去存序列号?
我有两个猜测:一是就算在 ustore 下,某些场景的 update 数据仍会发生 ctid 变化,而且他们也没打算基于版本链的方式解决 ctid 变化的问题;二是 GaussDB 至今仍未引入 tidrangescan 算子,而 index range scan 是本身就具备的。
元数据聊完,来看看加上 rowid 的性能表现。
rowid 性能测试
先看两个字段的表,插入一千万行:
不带 rowid:
drop table if exists t_test;
create table t_test(id int,a text);
explain analyze insert into t_test select id,'xxxxxxxxxx' from pg_catalog.generate_series (1,10000000) id;
Insert on t_test (cost=0.00..10.00 rows=1000 width=4) (actual time=1241.054..35016.026 rows=10000000 loops=1)
-> Function Scan on generate_series id (cost=0.00..10.00 rows=1000 width=4) (actual time=1241.038..3803.907 rows=10000000 loops=1)
Total runtime: 35040.289 ms
带 rowid:
drop table if exists t_test_rowid;
create table t_test_rowid(id int,a text) WITH ( hasrowid = ON );
explain analyze insert into t_test_rowid select id,'xxxxxxxxxx' from pg_catalog.generate_series (1,10000000) id;
Insert on t_test_rowid (cost=0.00..10.00 rows=1000 width=4) (actual time=1274.390..88646.218 rows=10000000 loops=1)
-> Function Scan on generate_series id (cost=0.00..10.00 rows=1000 width=4) (actual time=1274.370..3843.351 rows=10000000 loops=1)
Total runtime: 88652.343 ms
两个字段的表,插入一千万行,不带 rowid 约 35 秒,带 rowid 要 88 秒,翻了一倍多。
再测十个字段的表,一百万行:
不带 rowid:
gaussdb=# drop table if exists t_test;
DROP TABLE
gaussdb=# drop table if exists t_test_rowid;
DROP TABLE
gaussdb=# create table t_test(c1 varchar(10),c2 varchar(10),c3 varchar(10), c4 varchar(10),c5 varchar(10),c6 varchar(10),c7 varchar(10),c8 varchar(10),c9 varchar(10),c10 varchar(10));
CREATE TABLE
gaussdb=# explain analyze insert into t_test select 'abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij' from generate_series(1,1000000);
id | operation | A-time | A-rows | E-rows | Peak Memory | A-width |
E-width | E-costs
----+-----------------------------------------+-----------+---------+--------+-------------+---------+---------+---------------
1 | -> Insert on t_test | 10511.589 | 1000000 | 1000 | 484 KB | | 0 | 0.003..10.002
2 | -> Function Scan on generate_series | 525.974 | 1000000 | 1000 | 7314 KB | | 0 | 0.003..10.002
(2 rows)
====== Query Summary =====
------------------------------------------
Datanode executor start time: 0.198 ms
Datanode executor run time: 10511.617 ms
Datanode executor end time: 5.040 ms
Planner runtime: 0.324 ms
Query Id: 6918936402524748121
Total runtime: 10516.876 ms
(6 rows)
gaussdb=#
gaussdb=#
带 rowid:
gaussdb=# create table t_test_rowid(c1 varchar(10),c2 varchar(10),c3 varchar(10), c4 varchar(10),c5 varchar(10),c6 varchar(10),c7 varchar(10),c8 varchar(10),c9 varchar(10),c10 varchar(10)) with(hasrowid);
CREATE TABLE
gaussdb=# explain analyze insert into t_test select 'abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij','abcdefghij' from generate_series(1,1000000);
id | operation | A-time | A-rows | E-rows | Peak Memory | A-width |
E-width | E-costs
----+-----------------------------------------+-----------+---------+--------+-------------+---------+---------+---------------
1 | -> Insert on t_test | 19775.692 | 1000000 | 1000 | 484 KB | | 0 | 0.003..10.002
2 | -> Function Scan on generate_series | 548.358 | 1000000 | 1000 | 7314 KB | | 0 | 0.003..10.002
(2 rows)
====== Query Summary =====
------------------------------------------
Datanode executor start time: 0.063 ms
Datanode executor run time: 19775.717 ms
Datanode executor end time: 5.482 ms
Planner runtime: 0.209 ms
Query Id: 6918936402524748129
Total runtime: 19781.279 ms
(6 rows)
gaussdb=#
不带 rowid 约 10.5 秒,带 rowid 约 19.8 秒。字段越多,rowid 对插入性能的影响会越小,但始终存在不可忽视的损耗。
不过由于创建了一个唯一索引,在字段较多的时候,count 就不用全表扫描了,index only scan 会快得多:
不带 rowid 的 count:
gaussdb=# explain analyze select count(1) from t_test;
id | operation | A-time | A-rows | E-rows | Peak Memory | A-width | E-width | E-costs
----+------------------------------+----------+--------+--------+-------------+---------+---------+----------------
1 | -> Aggregate | 694.211 | 1 | 1 | 22 KB | | 8 | 43070.113..43070.123
2 | -> Seq Scan on t_test | 468.275 | 2000000| 686969 | 45 KB | | 0 | 0.000..41352.690
(2 rows)
====== Query Summary =====
----------------------------------------
Datanode executor start time: 0.072 ms
Datanode executor run time: 694.250 ms
Datanode executor end time: 0.018 ms
Planner runtime: 0.152 ms
Query Id: 6918936402524748222
Total runtime: 694.353 ms
(6 rows)
带 rowid 的 count:
gaussdb=# explain analyze select count(1) from t_test_rowid;
id | operation | A-time | A-rows | E-rows | Peak Memory | A-width | E-width | E-costs
----+-------------------------------------------------------------+--------+--------+--------+-------------+---------+---------+--------------
1 | -> Aggregate | 0.071 | 1 | 1 | 22 KB | | 8 | 4.733..4.743
2 | -> Index Only Scan using t_test_rowid_rowno_idx on t_test_rowid | 0.064 | 0 | 199 | 27 KB | | 0 | 0.000..4.235
(2 rows)
====== Query Summary =====
----------------------------------------
Datanode executor start time: 0.117 ms
Datanode executor run time: 0.097 ms
Datanode executor end time: 0.013 ms
Planner runtime: 0.643 ms
Query Id: 6918936402524748227
Total runtime: 0.248 ms
(6 rows)
695.189ms 对 0.116ms,全表 count 的性能提升巨大。不过真实业务场景对大表做全表 count 的情况非常少,运维场景可能偶尔会用到。
rowid 值的算法
接着说明一下 rowid 值的算法。官方文档的说法是 BASE64 编码,但组件的拆分如下:
| 字段 |
含义 |
字符长度 |
取值范围(BASE64 编码) |
| reserve_flag |
保留位 |
6 |
当前版本 0(AAAAAA) |
| node_slice |
节点的分片编号 |
3 |
最小值 0(AAA),最大值 65535(P//),实际最大值为集群分片数量。集中式固定为 0(AAA) |
| bucket_id |
hash bucket 表的 bucket 编号 |
3 |
最小值 0(AAA),最大值 32767(H//),实际最大值为表的最大 bucket 编号。对于非 hash bucket 表而言,该字段值固定为 65535(P//)。集中式固定为 65535(P//) |
| table_oid |
表的 oid |
6 |
最小值 0(AAAAAA),最大值 4294967295(D/////) |
| row_no |
行的编号 |
11 |
最小值 0(AAAAAAAAAAA),最大值 9223372036854775807(H//////////) |
可以看到对于集中式而言,前三个值都是固定的,只有后面的 tableoid 和 rowno 不同。这也印证了我前面提到的 PG 系各种 rowid 实现方案。
GaussDB 提供了几个 rowid 函数,可以基于 rowid 计算出对应的组件值:
rowid_tableoid(rowid) -> tableoid
rowid_sequence(rowid) -> rowno
rowid_nodeid(rowid) -> node_slice(集中式没有此函数)
rowid_hashbucketid(rowid) -> bucket_id(集中式没有此函数)
但 GaussDB 并没有提供基于这几个值反向计算 rowid 的函数。
官方文档说是 BASE64,但我尝试用数据库内置的 decode/encode 按 base64 编码去算,是算不出来的。换做以前,我可能会自己去推算这个算法,毕竟以前真做过多种类似 BASE64 的算法推演。但那太费时间了,现在有 AI,我直接让 AI 帮我去推算。
这次使用的是 wps 灵犀 claw,模型选择为 GLM5.2。
我:阅读这个文档(GaussDB rowid 官方文档),我给你一个 gaussdb 环境 "192.168.163.227:5732/postgres admin/Gaussdb@123",jdbc 驱动在 "C:\work\github\gaussdbjdbc\gaussdbjdbc.jar",尝试分析出如何使用 sql 或者其他方式,通过给定的 reserve_flag、node_slice、bucket_id、table_oid、row_no 值计算出对应的 rowid 的值。这是一个创建带 rowid 表的示例 CREATE TABLE t1 (c1 varchar2(20)) WITH ( hasrowid = ON);
灵犀:
GaussDB ROWID 编码算法分析
ROWID 是一个 29 字符的 BASE64 编码字符串,由 5 个字段按固定顺序拼接而成:
| 字段 |
字符长度 |
比特位 |
取值范围 |
默认值 |
| reserve_flag |
6 |
36 |
当前版本固定 0 |
AAAAAA |
| node_slice |
3 |
18 |
分片编号 |
AAA (=0) |
| bucket_id |
3 |
18 |
hash bucket 编号 |
P// (=65535) |
| table_oid |
6 |
36 |
表的 OID (最大 2^32-1) |
— |
| row_no |
11 |
66 |
行编号 (最大 2^63-1) |
— |
编码算法使用标准 BASE64 字母表:A=0, B=1, ..., Z=25, a=26, ..., z=51, 0=52, ..., 9=61, +=62, /=63。每个字段独立编码为大端序 (big-endian) 的 base-64 整数,左侧用 A(即 0)填充到固定宽度,最后 5 个字段直接字符串拼接。
编码公式:对于宽度为 w 的字段,值为 v:
result = ""
for i in 1..w:
result = B64_ALPHABET[v mod 64] + result
v = v div 64
已在 GaussDB 中创建了两个 SQL 函数(已验证可用):
-
编码(组件值 → ROWID):
SELECT compute_rowid(
p_reserve_flag := 0, -- 保留位,默认0
p_node_slice := 0, -- 分片编号
p_bucket_id := 65535, -- bucket编号,非hash表默认65535
p_table_oid := 41355, -- 从 pg_class.oid 获取
p_row_no := 1 -- 行编号
);
-- 返回: AAAAAAAAAP//AAAKGLAAAAAAAAAAB
-
解码(ROWID → 各组件值):
SELECT
rowid_decode_field(substr(rowid, 1, 6)) AS reserve_flag,
rowid_decode_field(substr(rowid, 7, 3)) AS node_slice,
rowid_decode_field(substr(rowid, 10, 3)) AS bucket_id,
rowid_decode_field(substr(rowid, 13, 6)) AS table_oid,
rowid_decode_field(substr(rowid, 19, 11)) AS row_no
FROM rowid_test;
Python 版函数:
import string
B64 = string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/"
def encode_field(value, width):
chars = []
for _ in range(width):
chars.append(B64[value & 0x3F])
value >>= 6
return ''.join(reversed(chars))
def compute_rowid(reserve_flag, node_slice, bucket_id, table_oid, row_no):
return ''.join([
encode_field(reserve_flag, 6),
encode_field(node_slice, 3),
encode_field(bucket_id, 3),
encode_field(table_oid, 6),
encode_field(row_no, 11),
])
我:这个 base64 算法和数据库内置 decode/encode 函数里的 base64 算法有什么区别么?
灵犀:
ROWID base64 vs 数据库内置 base64 的区别
两者虽然使用相同的字母表(A-Z a-z 0-9 + /,即 RFC 4648 标准),但编码逻辑完全不同,不能互换使用。
核心差异:
| 维度 |
ROWID base64 |
数据库内置 encode/decode(..., 'base64') |
| 编码对象 |
整数值 |
字节流 (bytea) |
| 编码单位 |
按 6-bit 分组,大端序排列 |
按 3 字节 (24 bit) → 4 字符 |
| Padding |
无 = 填充,左侧用 A(0)补齐到固定宽度 |
有 = 填充(长度不是 4 倍数时补尾) |
| 输入宽度 |
固定字符宽度(3/6/11)由字段定义决定 |
由输入字节数决定输出字符数(⌈n/3⌉×4) |
| 输出长度 |
固定(每个字段长度不变) |
可变(取决于输入字节数) |
| 可逆性 |
整数 ↔ 字符串,无信息丢失 |
字节 ↔ 字符串,无信息丢失 |
具体对比实例,以数值 65535 为例:
ROWID 编码 (width=3): P// ← 3 字符,无 padding
DB base64 (4字节): AAD//w== ← 8 字符,含 == padding
DB base64 (8字节): AAAAAAAA//8= ← 12 字符,含 = padding
以数值 1 为例:
ROWID 编码 (width=11): AAAAAAAAAAB ← 11 字符,无 padding
DB base64 (8字节): AAAAAAAAAAE= ← 12 字符,含 = padding
用数据库 decode 能否解码 ROWID 字段?
不能。实测中 decode('P//', 'base64') 直接报错 invalid end sequence,因为标准 base64 要求输入字符数是 4 的倍数(不足补 =),而 ROWID 字段长度为 3、6、11,都不是 4 的倍数。即使强行补 = 也不行——标准 base64 的 3 字符 P//= 会被解码为 2 字节 0x3F 0xFF(= 16383),而 ROWID 的实际值是 65535。原因是:
ROWID 解码 P//: 15×64² + 63×64 + 63 = 65535(将 3 字符当作 18-bit 大端序整数)
DB 解码 P//: 001111 111111 111111 → 0x3F 0xFF + 丢弃尾部 2bit = 16383(按字节流切分)
标准 base64 把 3 个字符(18 bit)切成 2 个完整字节 + 2 个剩余 bit(被丢弃),而 ROWID 把 3 个字符完整还原为 18-bit 整数,不丢弃任何 bit。
一句话总结:ROWID 的 base64 是一种整数编码(big-endian,固定宽度,无 padding),而数据库内置的 base64 是 RFC 4648 字节流编码(变长,有 padding)。两者字母表相同但语义完全不同,无法通过 encode/decode 互换。
AI 在数据库里创建了这样几个函数:
CREATE OR REPLACE FUNCTION compute_rowid(p_reserve_flag bigint DEFAULT 0, p_node_slice bigint DEFAULT 0, p_bucket_id bigint DEFAULT 65535, p_table_oid bigint, p_row_no bigint)
RETURNS text
LANGUAGE plpgsql
IMMUTABLE AUTHID DEFINER NOT FENCED NOT SHIPPABLE immutable
AS $function$
BEGIN
RETURN rowid_encode_field(p_reserve_flag, 6)
|| rowid_encode_field(p_node_slice, 3)
|| rowid_encode_field(p_bucket_id, 3)
|| rowid_encode_field(p_table_oid, 6)
|| rowid_encode_field(p_row_no, 11);
END;
$function$;
CREATE OR REPLACE FUNCTION rowid_decode_field(s text)
RETURNS bigint
LANGUAGE plpgsql
AUTHID DEFINER NOT FENCED NOT SHIPPABLE immutable
AS $function$
DECLARE
chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
result BIGINT := 0;
i INT;
idx INT;
BEGIN
FOR i IN 1..length(s) LOOP
idx := position(substr(s, i, 1) IN chars) - 1;
result := result*64 + idx;
END LOOP;
RETURN result;
END;
$function$;
CREATE OR REPLACE FUNCTION rowid_encode_field(value bigint, field_width integer)
RETURNS text
LANGUAGE plpgsql
AUTHID DEFINER NOT FENCED NOT SHIPPABLE immutable
AS $function$
DECLARE
chars TEXT[] := ARRAY[
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'0','1','2','3','4','5','6','7','8','9','+','/'
];
result TEXT := '';
v BIGINT;
i INT;
BEGIN
v := value;
FOR i IN 1..field_width LOOP
result := chars[(v & 63) + 1] || result;
v := v >> 6;
END LOOP;
RETURN result;
END;
$function$;
这 3 个函数的 immutable 属性是我自己额外加上去的,AI 生成的不带。我手动测试了一下,编码和解码 rowid 都是正确的。
我本来指望着可以用这几个函数实现基于 rowid 的切片并发扫描,可结果无法得到理想中按表物理页面切割多线程查询的效果,而是按 rowno 走了个索引扫描:
explain analyze select * from t_test_rowid where
rowid between compute_rowid(0,0,65535,'t_test_rowid'::regclass::oid::int,10)
and compute_rowid(0,0,65535,'t_test_rowid'::regclass::oid::int,1000) ;
QUERY PLAN |
---------------------------------------------------------------------------------------------------------------------------------------+---|
[Bypass] |
Index Scan using t_test_rowid_rowno_idx1 on t_test_rowid (cost=0.00..5.35 rows=5 width=15) (actual time=0.126..1.301 rows=991 loops=1) |
Index Cond: ((rowno >= 10::bigint) AND (rowno <= 1000::bigint)), (Expression Flatten Optimized) |
Total runtime: 1.647 ms |
原因就要涉及到目前 GaussDB 里如何根据 rowid 来查数据的机制了。
rowid 查询机制
回顾一下前文演示过的一个执行计划:
gaussdb=# explain analyze select t.* from test_rowid_update t where rowid ='AAAAAAAAAP//AAAKLvAAAAAAAAAAy';
QUERY PLAN
------------------------------------------------------------------------------------------------------
--------------------------------------------
Index Scan using test_rowid_update_rowno_idx on test_rowid_update t (cost=0.00..2.27 rows=1 width=11
) (actual time=0.024..0.025 rows=1 loops=1)
Index Cond: (rowno = 50::bigint), (Expression Flatten Optimized)
Filter: (tableoid = 41711::oid), (Expression Flatten Optimized)
Total runtime: 0.260 ms
(4 rows)
where 条件只用了 rowid,但执行计划却是使用 rowno=50 做索引扫描,查出数据后再用 tableoid 过滤。注意到这是走了常量折叠。改成 PBE 看下:
gaussdb=# prepare p1 as select t.* from test_rowid_update t where rowid =$1;
PREPARE
gaussdb=# explain analyze execute p1('AAAAAAAAAP//AAAKL7AAAAAAAAAAy');
QUERY PLAN
------------------------------------------------------------------------------------------------------
--------------------------------------------
Index Scan using test_rowid_update_rowno_idx on test_rowid_update t (cost=0.00..2.28 rows=1 width=11
) (actual time=0.078..0.080 rows=1 loops=1)
Index Cond: (rowno = rowid_sequence($1)), (Expression Flatten Optimized)
Filter: (tableoid = rowid_tableoid($1)), (Expression Flatten Optimized)
Total runtime: 0.197 ms
(4 rows)
从这个执行计划可以发现,本质上它只是把 rowid=$1 替换成了 rowno = rowid_sequence($1) and tableoid = rowid_tableoid($1) 然后再去执行,这仅仅是在优化器里改写了一下 SQL。
这里有个吐槽点:明明对于一个非分区表的普通表,tableoid 每一行的值都是一样的,这里还要 filter 一下,也没把这个字段放到索引里。
opt_behavior_knob 里增加了 ROWID_OPT,可以控制优化器的行为,默认是开启的。尝试手动关闭看下区别:
gaussdb=# show opt_behavior_knob;
opt_behavior_knob
------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------
ALL,on(ROWID_OPT,SUBPLAN_SORTED_PATH_FIX,PARAM_PATH_SKIP_CACHING_VARRATIO,PREFER_UNIQUE_INDEX,PREFER_
LONGER_INDEX,PPI_COST_MODEL1,PREDICATE_INFER_ENHANCE,ACTUAL_RANGE_BY_INDEX),off()
(1 row)
gaussdb=# set opt_behavior_knob='off(ROWID_OPT)';
SET
gaussdb=# show opt_behavior_knob;
opt_behavior_knob
------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------
ALL,on(SUBPLAN_SORTED_PATH_FIX,PARAM_PATH_SKIP_CACHING_VARRATIO,PREFER_UNIQUE_INDEX,PREFER_LONGER_IND
EX,PPI_COST_MODEL1,PREDICATE_INFER_ENHANCE,ACTUAL_RANGE_BY_INDEX),off(ROWID_OPT)
(1 row)
gaussdb=# explain analyze select t.* from test_rowid_update t where rowid ='AAAAAAAAAP//AAAKL7AAAAAAAAAAy';
QUERY PLAN
------------------------------------------------------------------------------------------------------
------------
Seq Scan on test_rowid_update t (cost=0.00..17.50 rows=1000 width=11) (actual time=0.039..0.260 rows
=1 loops=1)
Filter: (rowid = 'AAAAAAAAAP//AAAKL7AAAAAAAAAAy'::rowid), (Expression Flatten Optimized)
Rows Removed by Filter: 999
Total runtime: 0.335 ms
(4 rows)
走了个全表扫然后过滤,也就是优化被关闭了。
当然上面只是举了谓词 = 的例子,实际上还有谓词的范围查询、order by 优化、min/max 优化、分区剪枝等,但使用约束也挺多,具体可参考官方文档《rowid - dml》的示例,本文不做搬运。
总结
GaussDB 507 版本新增的 rowid 特性,弥补了 GaussDB 长久以来不支持 rowid 的缺憾。但是,我建议不要轻易使用这个功能,因为其对性能的影响远超 SQL 不兼容的影响。SQL 不兼容改改就好了,插入性能太慢无解。尽管华为在这个上面是有一点创新想法的,但依旧落入了 PG 系解决 rowid 的套路陷阱。总体来说,这个实现方案中规中矩,没有惊喜。
参考链接
[1] https://doc.hcs.huawei.com/db/zh-cn/gaussdbqlh/26.861.0/devg-cent/gaussdb-42-1131.html
[2] https://doc.hcs.huawei.com/db/zh-cn/gaussdbqlh/26.861.0/devg-cent/gaussdb-42-1135.html
[3] https://doc.hcs.huawei.com/db/zh-cn/gaussdbqlh/26.861.0/rf-cent/gaussdb-38-0029.html