侧边栏壁纸
  • 累计撰写 140 篇文章
  • 累计创建 13 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

【MogDB】MogDB5.2.0重磅发布第三篇-支持rowid

DarkAthena
2024-10-14 / 0 评论 / 0 点赞 / 10 阅读 / 21591 字

一、背景

从ORACLE迁移到国产数据库,经常会遇到rowid的兼容性问题,可能是不支持rowid这个关键字,也可能是rowid数据类型或rowid的行为和oracle不一样。

  • rowid特征:
  1. rowid是表上的一个伪列
  2. rowid字段在数据字典中不存在
  3. select * from tab时,不显示rowid字段
  4. select rowid from tab能查出rowid列
  5. rowid的值对于同一行比较稳定,但有些情况下可能会变
  6. 相同的一个rowid,在不同的时刻可能指向不同的行(比如删除记录再插入,可能会插入到之前的物理位置)
  • rowid常用场景:
  1. 使用rowid排序,确保无主键表的排序结果稳定性
  2. 删除表中的重复记录
  3. 使用rowid定位行,进行dml操作
  4. 把处理过的rowid记录存储下来,表示该行数据已经被处理过,无需对原表更新表示状态的字段(业务队列表)
  • oracle某行的rowid会变的几种情况:
  1. 对分区表的数据记录进行更新分区键字段的操作
  2. 手动执行move命令
  3. 手动执行shrink
  4. 压缩表
  5. 移动表空间

可以看到就算是正常进行dml操作,ORACLE的rowid也是可能会变化的,只是没那么容易变化。

但PG/OG系数据库中类似rowid的这种表示物理位置的字段叫ctid,每次更新这行记录后,都会发生"变化",但其实是"没变"的,更新是旧行标记作废,插入新行,新行的ctid和旧行不一样。

收集了一下目前知名的几家数据库是否支持ROWID

数据库 基于PG/OG 是否支持ROWID 是否有提供改写方案
DM N Y
崖山 N Y
OCEANBASE N Y
KINGBASE Y Y
POLARDB-O Y Y
TDSQL Y Y
UXDB Y Y
GAUSSDB Y N Y
OPENGAUSS Y N 同GAUSSDB
GBASE 8c Y Y
VASTBASE G100 BUILD 16 Y Y
神通 Y Y
海盒 Y
瀚高 Y ? ?

对于从头开始纯自研的数据库来说,rowid的兼容自然不成问题,但基于PG或者源于PG的其他数据库来说,rowid的兼容并不简单,因为使用了astore这种存储引擎。很多家数据库声称支持了rowid,但是实际测试下来,有些场景和ORACLE的结果并不一致。

PG系如果要支持rowid,目前已知的大致有四种方案

  1. 借用ctid,但由于分区表的不同分区里可能出现相同的ctid,因此需要结合tableoid和ctid两个字段;如果是分布式的,还需要结合节点字段。但是记录被update时,ctid一定会变,不能使用同一个ctid去对某条记录进行两次更新。
  2. 在表上直接加一个物理存储的列,用于存储rowid,并且给它加上唯一约束或索引,而其值本身可能直接使用一个全局序列的值。
  3. 也是在表上直接加一个物理存储的列,但需要引入每个表的序列管理,这个序列值结合tableoid计算出一个base64编码的值,存到这个表里。
  4. 新增一种存储引擎,update数据时在原行上更新,不像astore这种更新实际上是插入了新行。

二、准备测试用例

1.select * 不显示rowid

create table test_rowid_update(id number,content varchar2(200));
insert into test_rowid_update values (1,'1');
select * from test_rowid_update ;
drop table test_rowid_update;

2.select rowid 不报错

create table test_rowid_update(id number,content varchar2(200));
insert into test_rowid_update values (1,'1');
select rowid,t.rowid,t.* from test_rowid_update t ;
drop table test_rowid_update;

3.通过rowid快速检索数据

create table test_rowid_update(id number,content varchar2(200));
insert into test_rowid_update select id,'id_'||id from generate_series(1,1000) id;
vacuum test_rowid_update;
vacuum test_rowid_update;
explain analyze select  t.* from test_rowid_update t where rowid = (select rowid from test_rowid_update  where content='id_50') ;
drop table test_rowid_update;

4.对同一个rowid进行多次更新

create table test_rowid_update(id number,content varchar2(200));
insert into test_rowid_update values (1,'1');

declare
i int;
begin
for rec in (select rowid rd from test_rowid_update) loop
update test_rowid_update set content='2' where rowid=rec.rd;
update test_rowid_update set content='3' where rowid=rec.rd;
end loop;
select 1 into i from test_rowid_update where id=1 and content='3';
end;
/
select * from test_rowid_update where  id=1 and content='3';
drop table test_rowid_update ;

5.删除表中重复记录

create table test_rowid_update as select id,'n_'||id as content from generate_series(1,10) id;

insert into test_rowid_update select id,'d_'||id from generate_series(1,5) id;

DELETE FROM test_rowid_update t1
WHERE t1.rowid > (
SELECT MIN(t2.rowid)
FROM test_rowid_update t2
WHERE t1.id= t2.id
);

三、执行测试

1.DM

在线测试环境:https://eco.dameng.com/tour/
在这个环境中测试,得到DM的rowid是一个数字,可以认为它是一个自增主键。每个表的第一行记录的rowid是1,因此它表示一个逻辑位置,而非物理位置,在DM的一些官方文档中可以找到相关描述
https://eco.dameng.com/document/dm/zh-cn/pm/manage-table
https://eco.dameng.com/document/dm/zh-cn/pm/management-table
但是DM的下面这篇文档,却说是一个物理位置,而且演示的用例,rowid也不是数字了
https://eco.dameng.com/document/dm/zh-cn/pm/insertion-deletion-modification
出现这个差异的原因是,DM的表有索引组织表和堆表两种,默认建表是索引组织表,可以通过修改参数来修改默认行为

alter system set 'LIST_TABLE'=1 both;

在堆表情况下,rowid就不是从1开始的数字了,但它仍然是数字类型,该数字通过计算得到,没有进行实际存储

2.OCEANBASE

没有拿到免费的ORACLE租户环境,暂无法测试,但从文档来看

https://www.oceanbase.com/docs/common-oceanbase-database-cn-0000000001967397

OceanBase 数据库当前版本所支持的 ROWID 为逻辑 ROWID

https://www.oceanbase.com/docs/common-oceanbase-database-cn-10000000001701107

ROWID 伪列提供了快速定位表中某一行的能力,ROWID 值由主键编码得到,不会进行实际的存储

http://www.oceanbase.wiki/concept/database-objects/oracle-mode/tables/oracle-mode-data-types/rowid-data-types/

ROWID 伪列的值是每行数据的 Key(对于有主键表是 Primary Key,对于无主键表是系统自动生成的序列),通过 Base64 编码转换而成

https://www.oceanbase.com/docs/common-oceanbase-database-cn-10000000001699615

OceanBase 数据库的无主键表采用分区级自增列作为隐藏主键。

这意味着OceanBase并没有提供物理位置的rowid,而且对于非主键表,也会自动增加一个序列字段作为主键(分布式数据库的表必须要有主键?)。

3.崖山

环境准备:
https://download.yashandb.com/download
https://doc.yashandb.com/yashandb/23.1/zh/快速上手/安装部署/安装前准备/安装前准备.html
https://doc.yashandb.com/yashandb/23.1/zh/快速上手/安装部署/YashanDB产品安装/YashanDB服务端安装.html

yashandb的rowid格式像这个样子"2487:0:0:2532:0",准确指向物理位置,行被update后,rowid不会发生变化。但是注意直接粘贴匿名块到命令行客户端,会无法准确识别语句结束符,暂未找到相关文档说明。

4.KINGBASE

环境准备:

wget https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/05.Docker%E7%89%88%E6%9C%AC/kdb_x86_64_V009R001C001B0030.tar
docker load -i kdb_x86_64_V009R001C001B0030.tar
docker run -tid \
--privileged \
--name kingbase \
-p 54321:54321 \
-e ENABLE_CI=no  \
-e NEED_START=yes \
-e DB_USER=root \
-e DB_PASSWORD=root \
-e DB_MODE=oracle \
kingbase_v009r001c001b0030_single_x86:v1
docker exec -it kingbase /bin/bash
ksql -d test

在建表前需要先开启参数 set default_with_rowid to on; 表上才会有rowid字段,且该字段会是一个实际存在的字段,数据里会直接存储rowid的值,并且在这个字段上会自动创建一个主键约束。
虽然这种方式极大的保证了使用rowid处理数据的结果正确性,但它已经不是物理位置,而是逻辑位置,增加了额外的存储空间,并且会引发DML语句的性能问题,比如插入数据时,会去校验主键约束,数据插入完后,索引也要同步更新,然后vacuum时要同时处理表和索引;更新和删除记录时也是。

5.POLARDB-O

之前2.0版本免费公测的时候测了下,现在没有免费测试方法了,以下不代表最新版的情况。

众所周知,POLARDB-O 1.0来自于EDB,其实EDB处理rowid的方案和kingbase类似,也是要配置一个参数,参数名叫 default_with_rowids。不过kingbase的rowid是个字符串,而EDB的rowid是个bigint, 默认值是一个序列。
POLARDB-O 2.0版本中,无需配置参数(参数名改成了 polar_default_with_rowid),建表时直接就给你加了个 polar_sys_rowid_attr字段。稍微有点坑的是,这个字段在pg_attribute中记录的列号是大于0的,这意味着第三方工具(比如dbeaver)如果把它当成pg去查建表的ddl语句,则会出现表中多一列的情况,可惜当时没测再加列会是什么效果。

CREATE TABLE public.t1 (
a text NULL,
"polar_sys_rowid_attr" int8 NOT NULL DEFAULT nextval('polar_sys_seq'::regclass)
);

CREATE INDEX polar_rowid_17309_index ON public.t1 USING btree("polar_sys_rowid_attr");

既然是新增了存储的信息,必然会导致性能受影响,在polardb官网文档中的性能测试相关描述中,特意说明了是在关闭了rowid这个功能后进行的测试
https://help.aliyun.com/zh/polardb/polardb-for-oracle/difference-between-2-0-and-1-0-versions-of-polardb-for-oracle

性能测试中的参数均保持一致,其中参数 polar_enable_stmt_transaction_rollbackpolar_default_with_rowid设置为 off,其余参数使用默认值。

6.openGauss和GaussDB

环境准备

vi og600.yaml

global:  cluster_name: og600
  user: og600
  base_dir: /opt/og600rc

db_servers:
  - host: 127.0.0.1
    db_port: 21000
curl --proto '=https' --tlsv1.2 -sSf https://cdn-mogdb.enmotech.com/ptk/install.sh | sh
ptk checkos -o root_fix.sh
source root_fix.sh
ptk install -f og600.yaml -p https://opengauss.obs.cn-south-1.myhuaweicloud.com/6.0.0/openEuler20.03/x86/openGauss-All-6.0.0-openEuler20.03-x86_64.tar.gz

su - og600
gsql -r

不支持rowid,但是gaussdb的官方文档中,说明了应该要如何改写
https://support.huaweicloud.com/tg-dws/dws_mt_0128.html

select CAST((xc_node_id||'#'||tableoid||'#'||ctid ) AS TEXT ) AS rowid`

简单来说,gaussdb是分布式的,所以要拼一个xc_node_id,区分是哪个节点的数据;然后拼一个tableoid,因为不同的表中是会有相同的ctid的,为了保证唯一性,就拼了三个字段当成rowid去用。但是一旦该条记录被更新,ctid就会变,用这种方式拼出来的rowid也无法再找到刚刚被更新过的数据。
不过,在ustore引擎下,这个ctid就不会变化了,可以说openGauss/GaussDB从原理上可以支持不变的rowid,但是并没有支持 rowid这个关键字

7.VASTBASE G100 V2.2 BUILD 16

未提供公开的安装介质,找关系申请到了试用版本。
支持在查询中使用rowid伪列,不过情况和openGauss一样,虽然这个rowid长得和ORACLE类似,但是数据一更新,原rowid也无法再查到数据

8.GBASE 8c

环境准备:
centos 7 x86_64

curl --proto '=https' --tlsv1.2 -sSf https://cdn-mogdb.enmotech.com/ptk/install.sh | sh
ptk checkos -o root_fix.sh
source root_fix.sh

useradd gbase
su - gbase

wget https://cdn.gbase.cn/products/34/_eiStkqoJ9JedVQPogBdj/GBase8cV5_5.0.0_%E4%B8%BB%E5%A4%87%E5%BC%8F_x86.zip
unzip GBase8cV5_5.0.0_主备式_x86.zip
mv  GBase8cV5_5.0.0_主备式_x86 gbase
cd gbase/Server
tar -xf GBase8cV5_S5.0.0B28_centos7.8_x86_64.tar.gz
tar -xf GBase8cV5_S5.0.0B28_CentOS_x86_64.tar.bz2
mkdir data

echo "export GAUSSHOME=/home/gbase/gbase/Server">>/home/gbase/.bash_profile &&\
echo "export PATH=\$GAUSSHOME/bin:\$PATH">>/home/gbase/.bash_profile &&\
echo "export LD_LIBRARY_PATH=\$GAUSSHOME/lib:\$LD_LIBRARY_PATH">>/home/gbase/.bash_profile &&\
echo "export PGDATA=\$GAUSSHOME/data">>/home/gbase/.bash_profile
source /home/gbase/.bash_profile

gs_initdb --nodename=primary --pwpasswd=gbase@123 --encoding=UTF-8 --locale=en_US.UTF-8
gs_ctl start
gsql -r -d postgres

和vastbase一样,使用了tableoid+ctid的方式,数据一更新,rowid就变了,原rowid无法再查到数据。
这里发现个差异,gbase 8c 默认匿名块里执行select into 没有找到数据时不会报错,和原版openGauss不一致

9.神通

神通的openGauss版本没有公开下载介质,需要打电话联系客服,比较麻烦。而且也没有openGauss版本对应的文档,暂无法确认rowid的兼容程度。
对于非openGauss的版本,从公开文档中可以看到,神通数据库把ctid这个字段名直接改成了rowid,并且把数据类型修改成了bigint,从用例中的数值长度来看,应该不仅包含ctid的信息了。
http://shentongdata.com/index.php/download/list-26
神通数据库-优化及运维手册

10.UXDB

没有公开下载介质,但可以从官方文档里看到一些描述
https://www.uxsino.com/index.php/list82/82/优炫数据库/0.html
优炫数据库参考手册 V2.1

UXDB数据库的rowid是为了兼容达梦rowid伪列所做功能适配,用来标识数据库基表中每一条记录的唯一键值。本次实现,还新增了数据类型:rowid。该类型是一个大范围整数的数字类型,其范围和功能等同bigint类型。
新增GUC参数:create_as_parallel。该参数的作用:控制在执行create … as或者select … into命令时,数据库可以并行的条件下,是否走并行。如果设置为off,表示为不并行,此时rowid功能正常;设置为on时,表示为走并行,此时无法保证rowid功能正常。该参数默认值为off。

这意味着UXDB的rowid也是个真正存储的一个值,而不是表示物理位置,甚至在同一个表中也还是会出现rowid重复的情况

11.瀚高

环境准备:
centos 7 x86_64

wget https://www.highgo.com/upload/download/20220708/1b73693f2c91fd7f38acbc7d3882cf6c.rpm
rpm -ivh 1b73693f2c91fd7f38acbc7d3882cf6c.rpm
su - highgo
cd $HGDB_HOME/etc
./setup primary #输入密码
pg_ctl -D /opt/HighGo6.0.4-cluster/data -l logfile start
psql #输入密码

建表就报错了,因为不支持number/varchar2类型,手动改成numeric/varchar建表成功。
然后执行匿名块又报错了,因为ORACLE的匿名块语句分隔符是 /,而PG遇到 ;就表示语句结束了。
直接select rowid from tab也是报错,没有rowid这个字段。
在pg_setting中也没有包含 rowid字样的参数。
由于官方文档内容很少,也没有搜索功能,不确认是否支持rowid。

12.TDSQL-PG(ORACLE兼容版)

没有公开下载地址,只能看文档
https://doc.fincloud.tencent.cn/tcloud/Database/TBASE/73365/19613/37492/84290

先开启参数 default_with_rowid,再建表就带rowid字段了,但是为了避免DML的性能问题,没有自动创建rowid的索引,从这个角度上看,也是真实存了数据,不是真正的物理位置。

13.海盒

没有公开下载,需要联系客服,并且没有公开的文档,无法确认。

14.MogDB 5.2.0

环境准备:
【MogDB】MogDB5.2.0重磅发布第二篇-发布了哪些介质,如何获取?

测试记录:

  • 场景一:select * 不显示rowid
MogDB=# create table test_rowid_update(id number,content varchar2(200));
CREATE TABLE
MogDB=# insert into test_rowid_update values (1,'1');
INSERT 0 1
MogDB=# select * from test_rowid_update ;
 id | content
----+---------
  1 | 1
(1 row)

MogDB=# drop table test_rowid_update;
DROP TABLE
MogDB=#
  • 场景二:select rowid 不报错
MogDB=# create table test_rowid_update(id number,content varchar2(200));
CREATE TABLE
MogDB=# insert into test_rowid_update values (1,'1');
INSERT 0 1
MogDB=# select rowid,t.rowid,t.* from test_rowid_update t ;
      rowid       |      rowid       | id | content
------------------+------------------+----+---------
 AAAAAAEAAADsVAAA | AAAAAAEAAADsVAAA |  1 | 1
(1 row)

MogDB=# drop table test_rowid_update;
DROP TABLE
MogDB=#
  • 场景三:通过rowid快速检索数据
MogDB=# create table test_rowid_update(id number,content varchar2(200));
CREATE TABLE
MogDB=# insert into test_rowid_update select id,'id_'||id from generate_series(1,1000) id;
INSERT 0 1000
MogDB=# vacuum test_rowid_update;
VACUUM
MogDB=# vacuum test_rowid_update;
VACUUM
MogDB=# explain analyze select  t.* from test_rowid_update t where rowid = (select rowid from test_rowid_update  where content='id_50') ;
                                                    QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
 Tid Scan on test_rowid_update t  (cost=12.13..16.14 rows=1 width=450) (actual time=0.272..0.273 rows=1 loops=1)
   TID Cond: (rowid = $0)
   InitPlan 1 (returns $0)
     ->  Seq Scan on test_rowid_update  (cost=0.00..12.12 rows=1 width=12) (actual time=0.040..0.261 rows=1 loops=1)
           Filter: ((content)::text = 'id_50'::text)
           Rows Removed by Filter: 999
 Total runtime: 0.407 ms
(7 rows)


MogDB=# drop table test_rowid_update;
DROP TABLE
MogDB=#

可以看到这里使用rowid作为where条件时,使用的是Tid Scan,和使用ctid作为where条件时,执行计划是一样的,也就是通过物理位置进行快速检索。

  • 场景四:对同一个rowid进行多次更新
MogDB=# create table test_rowid_update(id number,content varchar2(200));
CREATE TABLE
MogDB=# insert into test_rowid_update values (1,'1');
INSERT 0 1
MogDB=# declare
MogDB-# i int;
MogDB-# begin
MogDB$# for rec in (select rowid rd from test_rowid_update) loop
MogDB$# update test_rowid_update set content='2' where rowid=rec.rd;
MogDB$# update test_rowid_update set content='3' where rowid=rec.rd;
MogDB$# end loop;
MogDB$# select 1 into i from test_rowid_update where id=1 and content='3';
MogDB$# end;
MogDB$# /
ANONYMOUS BLOCK EXECUTE
MogDB=# select * from test_rowid_update where  id=1 and content='3';
 id | content
----+---------
  1 | 3
(1 row)

MogDB=# drop table test_rowid_update ;
DROP TABLE
MogDB=#

从这里可以看到,该行数据用相同的rowid作为更新条件,可以更新多次。
MogDB的rowid同样也是基于tableoid+ctid分别进行base64编码,在update后也会变,但是仍旧可以用之前的rowid找到变更后的这行记录,所以对一行进行多次更新也不会有问题。

  • 场景五:删除表中重复记录
MogDB=# create table test_rowid_update as select id,'n_'||id as content from generate_series(1,10) id;
INSERT 0 10
MogDB=# insert into test_rowid_update select id,'d_'||id from generate_series(1,5) id;
INSERT 0 5
MogDB=# DELETE FROM test_rowid_update t1
MogDB-# WHERE t1.rowid > (
MogDB(# SELECT MIN(t2.rowid)
MogDB(# FROM test_rowid_update t2
MogDB(# WHERE t1.id= t2.id
MogDB(# );
DELETE 5
MogDB=# 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)

MogDB=#

正确删除了指定字段的重复行

四、测试结果汇总

数据库 是否增加了物理存储字段 select * 不显示rowid select rowid 不报错 通过rowid快速检索数据 对同一个rowid进行多次更新 删除表中重复记录
DM8 N Y Y Y Y Y
崖山 23.2.4 N Y Y Y Y Y
OCEANBASE 4.0 Y Y Y Y Y Y
KINGBASE 8 Y Y Y Y Y Y
POLARDB-O 2.0 Y Y Y Y Y Y
TDSQL-PG(Oracle兼容版) Y Y Y N Y Y
UXDB Y ? ? ? ? ?
GAUSSDB 503.1.0.SPC1700 N Y N N N N
OPENGAUSS 6.0 N Y N N N N
GBASE 8c N Y Y Y N Y
VASTBASE G100 V2.2 BUILD 16 N Y Y Y N Y
神通(openGauss版) ? ? ? ? ? ?
海盒 ? ? ? ? ? ?
瀚高 6.0.4 ? ? ? ? ? ?
MogDB 5.2.0 N Y Y Y Y Y

五、总结

PG/OG系最佳的rowid实现方案,在astore下必然无法实现,所以OG系的ustore应该是当下最佳实现方案,但ustore仍然还需要一段时间催熟。因此MogDB5.2.0在astore下实现的这个rowid,在一众PG/OG系的解决方案里,属于是一种全新的方案了,这种方案不仅没有让DML性能下降,又可以比同样DML性能没有下降的其他数据库支持更多的场景,是PG/OG系中已知的目前唯一一款在astore下没有新增物理字段又同时支持了这5个rowid使用场景的数据库。

声明:本文不涉及任何商业行为,仅对自测结果进行客观陈述,如果觉得有不妥当,请联系本人进行修改 darkathena@qq.com

0
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin
博主关闭了所有页面的评论