智能SQL改写神器SQLShift试玩
数据库管理-第328期 智能SQL改写神器SQLShift试玩(20250523)
作者:胖头鱼的鱼缸(尹海文)
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,这里也感谢爱可生开源社区的长龙同学提供的测试用例。
1 SQLShift
SQLShift是爱可生开发的企业级SQL方言智能转换平台,作为国内首个支持Oracle→OceanBase存储过程自动转换的SaaS服务,SQLShift深度融合AI与SQL语法专家模型,精准解决数据库国产化迁移中的隐式转换、逻辑失真等核心痛点,助力企业实现零误差交付。其核心能力为:
- AI精准解析
:动态学习Oracle与目标库方言差异,自动转换游标循环、异常处理等多种复杂语法。 - 白盒化追溯
:拆解说明存储过程逻辑链路及转换原理,帮助用户降低理解成本。 - 风险预判
:针对系统视图、保留字、LOG ERRORS INTO等显性和隐性不兼容语法,实时生成修复建议及影响评估。
在实际数据库迁移过程中帮助解决如SYSDATE兼容适配、分布式系统状态监测逻辑自动重构、动态SQL变量优化与复杂表达式解耦、隐蔽符号差异引发存储过程迁移陷阱、复杂数值类型转换、系统函数迁移等具体问题,使得数据库迁移快速、平滑、简单。
官方网站:
https://sqlshift.cn/
2 测试用例
本次测试用例为Oracle向OceanBase 4.2.5 Oracle模式租户的复杂存储过程迁移。
2.1 case1
点击首页免费试用并完成登录后。
新建一个项目:
新建转换任务
在源端SQL中输入以下内容,这是一个275行比较复杂的存储过程,用于检查多项数据库信息:
DELIMITER $$
CREATEORREPLACEPROCEDURE"SP_ENTITY_MANAGE_BK1" (
V_BUSIMAIN_CODE INVARCHAR2,
V_ENTITY_CALIBRE INVARCHAR2,
V_ENTITY_CODE INVARCHAR2,
V_CALL_SIGN INVARCHAR2,
V_BUSI_MAINBODY INVARCHAR2,
V_BUSI_CALIBRE INVARCHAR2,
V_ENTITY_TYPE_CODE INVARCHAR2,
V_COMPANY_CALIBRE INVARCHAR2,
V_MANAGER_CODE INVARCHAR2,
V_MANAGER_CALIBRE INVARCHAR2,
V_TRADE_TYPE INVARCHAR2,
V_RETIRE_FLAG INVARCHAR2,
V_ACCMAN_CODE INVARCHAR2,
V_ACCOUNT_CALIBRE INVARCHAR2,
V_FEE_TYPE INVARCHAR2,
V_FEE_SUBJECT INVARCHAR2,
V_SAFEMAN_CODE INVARCHAR2,
V_SAFE_CALIBRE INVARCHAR2,
V_CORP_CALIBRE INVARCHAR2,
V_TEST_CALIBRE INVARCHAR2,
V_COSTMAN_CODE INVARCHAR2,
V_COST_CALIBRE INVARCHAR2,
IS_CORSUR OUT SYS_REFCURSOR
) IS V_FLAG VARCHAR2(20);
BEGIN-- 日志记录
SELECT
open_mode INTO V_FLAG
FROM
v $ database;
IF V_FLAG = 'READ WRITE' THEN P_LOG_EXCEPTION('Starttime:' || 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;
$$
开始转换
查看转换结果
case2
和case1创建方式,SQL改为下面内容:
DELIMITER $$
CREATEORREPLACEPROCEDURE"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 isnullor i.dept_code = i_dept_code)
and (i_operator_id isnullor i.operator_id = i_operator_id)
and (i_inspection_name isnullor (i_inspection_name isnotnulland 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
executeimmediate'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);
endif;
for rec in cur(dat_begin,dat_end) loop
selectcount(1) into int_count from TMP_SYNC_CHECK_DATA i where i.record_id = rec.check_item_id;
if int_count = 0 then
INSERTINTO 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);
endif;
-- 动态字段后缀逻辑保留
selectcase i_insp_level
when'A'thencast(to_char(rec.inspection_date,'dd') asnumber)
when'B'thencast( 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')) asnumber)
when'C'thencast(to_char(rec.inspection_date,'mm') asnumber)
end
into var_suffix
from dual;
executeimmediate
'update TMP_SYNC_CHECK_DATA i
set data_item_'||lpad(var_suffix,2,'0')||' = :1
where i.record_id = :2'
usingcase 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;
endloop;
selectcount(*) into int_count1 from TMP_SYNC_CHECK_DATA where1=1and 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
createusertestidentifiedbytest;
grantconnect,dba totest;
-- 执行用例创建对象语句:略
obclient -h127.0.0.1 -P2883 -utest@orcl -ptest -A
case1
改写后的存储过程可以正常执行,无报错。
case2
改写后的存储过程可以正常执行,无报错。
如果大家对测试用例感兴趣,可以联系爱可生开源社区
总结
简单试用下来,SQLShift确实是Oracle到OceanBase的SQL改写神器,对于复杂存储过程也有很好的转换能力,后期还将加入Oracle到PG、DB2到PG、SQL Server到OceanBase以及Oracle到达梦的改写能力。
老规矩,知道写了些啥。