作者:胖头鱼的鱼缸(尹海文) Oracle ACE Pro: Database PostgreSQL ACE Partner 10年数据库行业经验 拥有OCM 11g/12c/19c、MySQL 8.0 OCP、Exadata、CDP等认证 墨天轮MVP,ITPUB认证专家 圈内拥有“总监”称号,非著 名社恐(社交恐怖分子) 公众号:胖头鱼的鱼缸 CSDN:胖头鱼的鱼缸(尹海文) 墨天轮:胖头鱼的鱼缸 ITPUB:yhw1809。 除授权转载并标明出处外,均为“非法”抄袭
各类数据库都是由自己方言的,这些方言在很多场景下成为了数据库迁移的壁垒,近期爱可生发布了SQLShift工具,提供了Oracle向OceanBase的智能SQL改写功能。本期跟随总监一起试用一下SQLShift,这里也感谢爱可生开源社区的长龙同学提供的测试用例。
SQLShift是爱可生开发的企业级SQL方言智能转换平台,作为国内首 个支持Oracle→OceanBase存储过程自动转换的SaaS服务,SQLShift深度融合AI与SQL语法专家模型,精准解决数据库国产化迁移中的隐式转换、逻辑失真等核心痛点,助力企业实现零误差交付。其核心能力为:
在实际数据库迁移过程中帮助解决如SYSDATE兼容适配、分布式系统状态监测逻辑自动重构、动态SQL变量优化与复杂表达式解耦、隐蔽符号差异引发存储过程迁移陷阱、复杂数值类型转换、系统函数迁移等具体问题,使得数据库迁移快速、平滑、简单。
官方网站:
https://sqlshift.cn/
本次测试用例为Oracle向OceanBase 4.2.5 Oracle模式租户的复杂存储过程迁移。
点击首页免费试用并完成登录后。

在源端SQL中输入以下内容,这是一个275行比较复杂的存储过程,用于检查多项数据库信息:
DELIMITER $$
CREATE OR REPLACE PROCEDURE"SP_ENTITY_MANAGE_BK1" (
V_BUSIMAIN_CODE IN VARCHAR2,
V_ENTITY_CALIBRE IN VARCHAR2,
V_ENTITY_CODE IN VARCHAR2,
V_CALL_SIGN IN VARCHAR2,
V_BUSI_MAINBODY IN VARCHAR2,
V_BUSI_CALIBRE IN VARCHAR2,
V_ENTITY_TYPE_CODE IN VARCHAR2,
V_COMPANY_CALIBRE IN VARCHAR2,
V_MANAGER_CODE IN VARCHAR2,
V_MANAGER_CALIBRE IN VARCHAR2,
V_TRADE_TYPE IN VARCHAR2,
V_RETIRE_FLAG IN VARCHAR2,
V_ACCMAN_CODE IN VARCHAR2,
V_ACCOUNT_CALIBRE IN VARCHAR2,
V_FEE_TYPE IN VARCHAR2,
V_FEE_SUBJECT IN VARCHAR2,
V_SAFEMAN_CODE IN VARCHAR2,
V_SAFE_CALIBRE IN VARCHAR2,
V_CORP_CALIBRE IN VARCHAR2,
V_TEST_CALIBRE IN VARCHAR2,
V_COSTMAN_CODE IN VARCHAR2,
V_COST_CALIBRE IN VARCHAR2,
IS_CORSUR OUT SYS_REFCURSOR
) IS V_FLAG VARCHAR2(20);BEGIN -- 日志记录SELECT
open_mode INTO V_FLAGFROM
v $ database;
IF V_FLAG = 'READ WRITE' THEN P_LOG_EXCEPTION('Start time:' || sysdate(), 'SP_ENTITY_MANAGE_BK1');
COMMIT;
END IF;
-- 主查询逻辑
OPEN IS_CORSUR FOR
SELECT
*
FROM
(
SELECT
KK.ROW_NO,
KK.ENTITY_CODE,
KK.ENTITY_NAME,
KK.ENTITY_NAME_EN,
KK.ALT_NAME AS ANOTHER_NAME,
NVL(S1.ORG_NAME, KK.MANAGER_CODE) AS VES_MANAGER,
KK.OWNERSHIP_FLAG_NAME,
KK.ENTITY_CATEGORY,
KK.OPERATION_ZONE_NAME,
KK.MANUFACTURE_LOC,
KK.NATIONALITY,
KK.TOTAL_LENGTH,
KK.WIDTH,
KK.DEPTH,
KK.SPACING_DRAFT,
KK.HOME_PORT,
KK.GROSS_WEIGHT,
KK.NET_WEIGHT,
KK.CALC_LIGHT_WEIGHT,
KK.HULL_SPEED,
KK.CANAL_WEIGHT_A,
KK.CANAL_NET_A,
KK.POWER_OUTPUT,
KK.BUILD_DATE,
TRUNC(MONTHS_BETWEEN(SYSDATE, KK.BUILD_DATE) / 12) AS CREATE_YEAR,
KK.COMMISSION_DATE,
KK.RETIREMENT_DATE,
KK.IDENTIFIER AS CALL_SIGN,
KK.UNIQUE_ID AS IMO_NO
FROM
(
SELECT
ROW_NUMBER() OVER(
ORDER BY
MB.ENTITY_CODE
) AS ROW_NO,
MB.ENTITY_CODE,
MB.ENTITY_NAME,
MB.ENTITY_NAME_EN,
MB.ALT_NAME,
GET_ENTITY_MANAGEMENT(MB.ENTITY_ID, SYSDATE(), '1') AS MANAGER_CODE,
NVL(
MB.OWNERSHIP_FLAG,
(
SELECT
C.DISPLAY_NAME
FROM
CODE_DICTIONARY C
WHERE
C.DICT_TYPE = 'SHIP_OWNER_FLAG'
AND C.CODE_KEY = MB.OWNERSHIP_FLAG
AND ROWNUM = 1
)
) AS OWNERSHIP_FLAG_NAME,
MB.ENTITY_CATEGORY,
NVL(
MB.OPERATION_ZONE,
(
SELECT
C.DISPLAY_NAME
FROM
CODE_DICTIONARY C
WHERE
C.DICT_TYPE = 'NAVIGATION_ZONE'
AND C.CODE_KEY = MB.OPERATION_ZONE
)
) AS OPERATION_ZONE_NAME,
MB.MANUFACTURE_LOC,
NVL(
MB.NATIONALITY,
(
SELECT
CTRY_NAME
FROM
COUNTRY_MASTER
WHERE
CTRY_CODE = MB.NATIONALITY
)
) AS NATIONALITY,
MB.TOTAL_LENGTH,
MB.WIDTH,
MB.DEPTH,
MB.SPACING_DRAFT,
NVL(
MB.HOME_PORT,
(
SELECT
LOCATION_NAME
FROM
PORT_MASTER
WHERE
LOCATION_CODE = MB.HOME_PORT
)
) AS HOME_PORT,
MB.GROSS_WEIGHT,
MB.NET_WEIGHT,
MB.CALC_LIGHT_WEIGHT,
MB.HULL_SPEED,
MB.CANAL_WEIGHT_A,
MB.CANAL_NET_A,
MB.POWER_OUTPUT,
MB.BUILD_DATE,
MB.COMMISSION_DATE,
MB.RETIREMENT_DATE,
MB.IDENTIFIER,
MB.UNIQUE_ID
FROM
GENERIC_ENTITY MB
LEFT JOIN (
SELECT
LISTAGG(V.TEST_SIZE, ',') WITHIN GROUP (
ORDER BY
V.STAT_ID
) AS TEST_CALIBRE,
LISTAGG(V.FEE_TYPE, ',') WITHIN GROUP (
ORDER BY
V.STAT_ID
) AS FEE_TYPE,
LISTAGG(V.FEE_SUBJECT, ',') WITHIN GROUP (
ORDER BY
V.STAT_ID
) AS FEE_SUBJECT,
LISTAGG(V.CORP_SIZE, ',') WITHIN GROUP (
ORDER BY
V.STAT_ID
) AS CORP_CALIBRE,
LISTAGG(V.COMPANY_SIZE, ',') WITHIN GROUP (
ORDER BY
V.STAT_ID
) AS COMPANY_CALIBRE,
V.STAT_ID
FROM
ENTITY_STATS V
GROUP BY
V.STAT_ID
) VV ON MB.ENTITY_ID = VV.STAT_ID
WHERE
(
V_BUSIMAIN_CODE IS NULL
OR GET_ENTITY_MANAGEMENT(MB.ENTITY_ID, SYSDATE, '2') = V_BUSIMAIN_CODE
)
AND (
V_ENTITY_CALIBRE IS NULL
OR GET_ENTITY_CALIBRE(MB.ENTITY_ID, SYSDATE, '1') = V_ENTITY_CALIBRE
)
AND (
V_ENTITY_CODE IS NULL
OR MB.ENTITY_CODE = V_ENTITY_CODE
)
AND (
V_CALL_SIGN IS NULL
OR MB.IDENTIFIER = V_CALL_SIGN
)
AND (
V_BUSI_MAINBODY IS NULL
OR GET_ENTITY_MANAGEMENT(MB.ENTITY_ID, SYSDATE, '2') = V_BUSI_MAINBODY
)
AND (
V_BUSI_CALIBRE IS NULL
OR GET_ENTITY_CALIBRE(MB.ENTITY_ID, SYSDATE, '2') = V_BUSI_CALIBRE
)
AND (
V_ENTITY_TYPE_CODE IS NULL
OR MB.ENTITY_TYPE_CODE = V_ENTITY_TYPE_CODE
)
AND (
V_COMPANY_CALIBRE IS NULL
OR VV.COMPANY_CALIBRE = V_COMPANY_CALIBRE
)
AND (
V_MANAGER_CODE IS NULL
OR GET_ENTITY_MANAGEMENT(MB.ENTITY_ID, SYSDATE, '1') = V_MANAGER_CODE
)
AND (
V_MANAGER_CALIBRE IS NULL
OR GET_ENTITY_CALIBRE(MB.ENTITY_ID, SYSDATE, '3') = V_MANAGER_CALIBRE
)
AND (
V_TRADE_TYPE IS NULL
OR MB.TRADE_TYPE = V_TRADE_TYPE
)
AND (
V_RETIRE_FLAG IS NULL
OR MB.RETIRE_FLAG = V_RETIRE_FLAG
)
AND (
V_ACCMAN_CODE IS NULL
OR GET_ENTITY_MANAGEMENT(MB.ENTITY_ID, SYSDATE, '4') = V_ACCMAN_CODE
)
AND (
V_ACCOUNT_CALIBRE IS NULL
OR GET_ENTITY_CALIBRE(MB.ENTITY_ID, SYSDATE, '4') = V_ACCOUNT_CALIBRE
)
AND (
V_FEE_TYPE IS NULL
OR VV.FEE_TYPE = V_FEE_TYPE
)
AND (
V_FEE_SUBJECT IS NULL
OR VV.FEE_SUBJECT = V_FEE_SUBJECT
)
AND (
V_SAFEMAN_CODE IS NULL
OR GET_ENTITY_MANAGEMENT(MB.ENTITY_ID, SYSDATE, '5') = V_SAFEMAN_CODE
)
AND (
V_SAFE_CALIBRE IS NULL
OR GET_ENTITY_CALIBRE(MB.ENTITY_ID, SYSDATE, '5') = V_SAFE_CALIBRE
)
AND (
V_CORP_CALIBRE IS NULL
OR VV.CORP_CALIBRE = V_CORP_CALIBRE
)
AND (
V_TEST_CALIBRE IS NULL
OR VV.TEST_CALIBRE = V_TEST_CALIBRE
)
AND (
V_COSTMAN_CODE IS NULL
OR GET_ENTITY_MANAGEMENT(MB.ENTITY_ID, SYSDATE, '6') = V_COSTMAN_CODE
)
AND (
V_COST_CALIBRE IS NULL
OR GET_ENTITY_CALIBRE(MB.ENTITY_ID, SYSDATE, '6') = V_COST_CALIBRE
)
) KK
LEFT JOIN ORGANIZATION S1 ON S1.ORG_CODE = KK.MANAGER_CODE
AND NVL(S1.IS_DELETED, '0') <> '1'
) A;
EXCEPTION
WHEN NO_DATA_FOUND THEN NULL;
WHEN OTHERS THEN RAISE;
END SP_ENTITY_MANAGE_BK1;
$$


和case1创建方式,SQL改为下面内容:
DELIMITER $$CREATE OR REPLACE PROCEDURE "SP_PMS_SYNC_ROUTINE_CHECK" ( i_asset_code varchar2, i_insp_level varchar2, i_dept_code varchar2, i_operator_id varchar2, i_inspection_name varchar2, i_check_date varchar2 -- 检查日期,yyyy-mm、yyyy) is/*** Created : 2013.08.15 Purpose : 日常检查历史数据同步 **/ cursor cur(i_begin_date date,i_end_date date) is select EQUIPMENT_NAME,i.EQUIP_CODE,inspection_name,dept_name_zh,i.dept_code,operator_name_zh,i.operator_id, result_code,i.result_name_zh,i.insp_level,i.inspection_date, i.asset_code,i.asset_name,i.check_item_id,i.comments, i.created_by,i.created_dept,i.created_time,i.create_tz,i.MODIFIED_BY, i.MODIFIED_DEPT,i.MODIFIED_TIME,i.MODIFY_TZ,i.org_code,i.VERSION,i.resp_group_code from vw_maint_check_info i where i.insp_level = i_insp_level and i.asset_code = i_asset_code and i.inspection_date >= i_begin_date and i.inspection_date <= i_end_date and (i_dept_code is null or i.dept_code = i_dept_code) and (i_operator_id is null or i.operator_id = i_operator_id) and (i_inspection_name is null or (i_inspection_name is not null and i.inspection_name like '%'||i_inspection_name||'%')) and i.data_type = 'S'; int_count integer; int_count1 integer; var_suffix varchar2(2); dat_begin date; dat_end date;begin execute immediate 'truncate table TMP_SYNC_CHECK_DATA'; -- 日期处理逻辑保留 if length(i_check_date) = 4 then dat_begin := to_date(i_check_date||'-01-01','yyyy-mm-dd'); dat_end := to_date(i_check_date||'-12-31','yyyy-mm-dd'); else dat_begin := to_date(i_check_date||'-01','yyyy-mm-dd'); dat_end := last_day(dat_begin); end if; for rec in cur(dat_begin,dat_end) loop select count(1) into int_count from TMP_SYNC_CHECK_DATA i where i.record_id = rec.check_item_id; if int_count = 0 then INSERT INTO GCTEST.TMP_SYNC_CHECK_DATA ( record_id, EQUIPMENT_NAME, EQUIPMENT_CODE, INSPECTION_ITEM, dept_name, dept_code, operator_name, operator_id, insp_level, insp_date, asset_code, asset_name, org_code, created_by, created_dept, created_time, create_tz, MODIFIED_BY, MODIFIED_DEPT, MODIFIED_TIME, MODIFY_TZ, VERSION, resp_group_code) Values (rec.check_item_id,rec.EQUIPMENT_NAME,rec.equip_code,rec.inspection_name,rec.dept_name_zh,rec.dept_code, rec.operator_name_zh,rec.operator_id,rec.insp_level,trunc(rec.inspection_date,'mm'), rec.asset_code,rec.asset_name, rec.org_code, rec.created_by, rec.created_dept, rec.created_time, rec.create_tz, rec.MODIFIED_BY, rec.MODIFIED_DEPT, rec.MODIFIED_TIME, rec.MODIFY_TZ, rec.VERSION, rec.resp_group_code); end if; -- 动态字段后缀逻辑保留 select case i_insp_level when 'A' then cast(to_char(rec.inspection_date,'dd') as number) when 'B' then cast( to_char( pkg_date_util.get_1st_m(rec.inspection_date, decode(length(i_check_date),4,'yy','mm')), decode(length(i_check_date),4,'ww','w')) as number) when 'C' then cast(to_char(rec.inspection_date,'mm') as number) end into var_suffix from dual; execute immediate 'update TMP_SYNC_CHECK_DATA i set data_item_'||lpad(var_suffix,2,'0')||' = :1 where i.record_id = :2' using case rec.result_code when '0' then '√' when '1' then '×' when '2' then 'O' when '3' then '—' end || substr(rec.comments,1,50), rec.check_item_id; end loop; select count(*) into int_count1 from TMP_SYNC_CHECK_DATA where 1=1 and asset_code='0336'; commit; exception when others then rollback; dbms_output.enable(10000); dbms_output.put_line(sqlerrm);end sp_pms_sync_routine_check; $$
转换结果如下:

在完整的测试用例中,还有在OceanBase中创建需要的相关表、存储过程、包和视图的语句,可以验证在OceanBase 4.2.5的实际使用情况。
我这里在我之前安装的OB 4.2.5单机版上进行两个用例的测试:
obclient -h127.0.0.1 -P2883 -usys@orcl -pObdj_123 -A
create user test identified by test;grant connect,dba to test;-- 执行用例创建对象语句:略
obclient -h127.0.0.1 -P2883 -utest@orcl -ptest -A
改写后的存储过程可以正常执行,无报错。
改写后的存储过程可以正常执行,无报错。
如果大家对测试用例感兴趣,可以联系爱可生开源社区
简单试用下来,SQLShift确实是Oracle到OceanBase的SQL改写神器,对于复杂存储过程也有很好的转换能力,后期还将加入Oracle到PG、DB2到PG、SQL Server到OceanBase以及Oracle到达梦的改写能力。
老规矩,知道写了些啥。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31466763/viewspace-3085325/ ,如需转载,请注明出处,否则将追究法律责任。