胖头鱼的鱼缸

智能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。
除授权转载并标明出处外,均为“非法”抄袭

3498ff20bcec87e9052f961f06737f3.png
各类数据库都是由自己方言的,这些方言在很多场景下成为了数据库迁移的壁垒,近期爱可生发布了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

image.png
点击首页免费试用并完成登录后。

新建一个项目:

image.png
image.png

新建转换任务

image.png
在源端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;
$$

开始转换

image.png

查看转换结果

image.png
image.png
image.png
image.png

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;
$$ 

转换结果如下:
image.png

实际使用

在完整的测试用例中,还有在OceanBase中创建需要的相关表、存储过程、包和视图的语句,可以验证在OceanBase 4.2.5的实际使用情况。
image.png
image.png
我这里在我之前安装的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

image.png
改写后的存储过程可以正常执行,无报错。

case2

image.png
改写后的存储过程可以正常执行,无报错。

如果大家对测试用例感兴趣,可以联系爱可生开源社区

总结

简单试用下来,SQLShift确实是Oracle到OceanBase的SQL改写神器,对于复杂存储过程也有很好的转换能力,后期还将加入Oracle到PG、DB2到PG、SQL Server到OceanBase以及Oracle到达梦的改写能力。
老规矩,知道写了些啥。