
1. 为什么要在达梦上做数据自动迁移数据迁移这件事做过一次的人都知道最怕的不是迁移本身而是迁移完之后业务方追着你问昨天的数据怎么没同步过来。尤其是用达梦数据库的场景很多团队是从Oracle或者MySQL转过来的习惯了原来那套迁移工具链到了达梦这边发现要么工具不趁手要么权限卡得死要么就是跨库访问的性能跟预期差了一大截。我接手过好几个达梦的数据迁移项目有同实例跨Schema的有跨实例的还有从Oracle往达梦迁的。这些项目里最让我头疼的不是迁移逻辑本身有多复杂而是迁移任务的调度和容错。你写一个存储过程把数据从A表搬到B表逻辑再复杂也就那样但你怎么保证它每天凌晨准时跑跑失败了怎么重试跑了一半断了怎么续这些才是真正消耗精力的地方。达梦数据库本身提供了DBMS_SCHEDULER这个包功能上对标Oracle的同名包可以创建定时任务来周期性执行存储过程。这套组合拳打下来基本上可以做到设定好规则剩下的交给数据库自己。我实测下来在达梦8上这套方案跑了大半年每天定时迁移几百万行数据稳定性是靠谱的。这篇文章适合谁看如果你是达梦数据库的运维或者开发手头有定期数据迁移的需求又不想引入额外的调度中间件比如什么XXL-JOB、Quartz之类的那这套存储过程DBMS_SCHEDULER的方案就是为你准备的。不需要额外部署服务不需要写Java代码纯SQL就能搞定。当然如果你对达梦的存储过程语法还不太熟建议先补一下基础不然有些地方可能会卡住。下面我会从整体设计思路开始把方案选型的考量、存储过程的编写要点、定时任务的配置细节、以及我踩过的坑一个一个拆开来讲。2. 整体方案设计与选型考量2.1 为什么选存储过程而不是外部程序做数据迁移摆在面前的路无非几条写Java/Python程序用JDBC连数据库搬数据、用ETL工具比如Kettle、DataX、或者在数据库内部用存储过程搞定。我先说为什么没选外部程序。用Java写迁移逻辑你得处理连接池、事务边界、批量提交、异常重试代码量不小。而且一旦迁移逻辑有变化你得重新打包部署走一遍CI/CD流程。更麻烦的是如果迁移涉及多张表的关联清洗在应用层做意味着要把数据拉到内存里处理数据量一大就容易OOM。ETL工具呢Kettle和DataX确实能干活但引入它们意味着你要多维护一套工具链而且达梦的JDBC驱动跟这些工具的兼容性偶尔会出幺蛾子。我之前用Kettle连达梦就遇到过驱动版本不匹配导致中文乱码的问题排查了半天。存储过程方案的核心优势在于数据不动、逻辑动。数据全程在数据库内部流转不需要通过网络传输到应用层再传回来。对于大批量数据的迁移这个性能差异是数量级的。而且存储过程天然支持事务控制你可以精确控制每批提交多少行失败了从哪个断点续传。注意存储过程方案适合的是数据库内部或数据库之间的数据迁移。如果你的源数据在文件系统、消息队列或者第三方API里那还是得用外部程序。2.2 DBMS_SCHEDULER相比操作系统的crontab强在哪有人可能会想我用Linux的crontab定时调用一个脚本去执行SQL不就行了何必用DBMS_SCHEDULER这个思路不是不行但有几个明显的短板。第一crontab调用的脚本需要明文存储数据库密码这是个安全隐患。第二crontab的执行日志跟数据库日志是分离的出了问题你得两头查。第三crontab没法感知数据库的状态比如数据库正在做备份、或者某个关键表被锁了它照样会触发任务。DBMS_SCHEDULER是数据库原生的调度器任务定义、执行日志、状态查询都在数据库内部完成。你可以通过USER_SCHEDULER_JOB_LOG和USER_SCHEDULER_JOB_RUN_DETAILS这两个视图直接查到每次执行的详细情况包括开始时间、结束时间、执行结果、错误信息。排查问题的时候一条SQL就能定位。另外DBMS_SCHEDULER支持基于事件的调度。比如你可以定义一个任务让它在一个特定的自定义事件被触发时才执行而不是死板地按时间跑。这个在复杂的迁移场景里很有用。2.3 整体架构长什么样这套方案的整体结构其实很简单就三层数据层源表和目标表可能在同一实例的不同Schema下也可能在不同实例上通过DBLINK访问。逻辑层存储过程负责具体的数据抽取、转换、加载逻辑包含批次控制、异常处理、断点记录。调度层DBMS_SCHEDULER创建的定时任务负责按计划触发存储过程并记录执行日志。这三层各司其职逻辑层和调度层是解耦的。也就是说你可以单独测试存储过程的逻辑确认没问题了再挂到调度器上。反过来如果迁移逻辑需要调整你只需要修改存储过程调度任务不用动。我一般会在方案里额外加一张迁移日志表用来记录每次迁移的批次、时间范围、影响行数、状态。这张表是存储过程自己维护的跟DBMS_SCHEDULER的日志互补。DBMS_SCHEDULER告诉你任务有没有跑迁移日志表告诉你跑了多少数据、跑到哪了。3. 存储过程编写的核心细节3.1 迁移日志表的设计在写存储过程之前先把日志表建好。这张表是整个迁移方案的基础设施没有它出了问题你两眼一抹黑。CREATE TABLE MIG_LOG ( LOG_ID BIGINT IDENTITY(1,1) PRIMARY KEY, PROC_NAME VARCHAR(100) NOT NULL, BATCH_NO INT NOT NULL, START_TIME TIMESTAMP DEFAULT SYSDATE, END_TIME TIMESTAMP, MIN_ID BIGINT, MAX_ID BIGINT, ROW_COUNT INT DEFAULT 0, STATUS VARCHAR(20) DEFAULT RUNNING, ERR_MSG VARCHAR(2000), REMARK VARCHAR(500) );字段说明一下。PROC_NAME标识是哪个存储过程写的日志方便多个迁移任务共用一张日志表。BATCH_NO是批次号每次迁移递增。MIN_ID和MAX_ID记录这一批处理的主键范围断点续传的时候靠它。STATUS有三个状态RUNNING、SUCCESS、FAILED。实操心得日志表本身也会越来越大建议加一个定期清理的机制。我一般会在迁移存储过程里顺手加一段逻辑每次执行时删除30天前的日志记录。别小看这个日志表膨胀到几千万行的时候查询会明显变慢。3.2 分批迁移的核心逻辑一次性把几百万行数据从一个表搬到另一个表最大的风险是undo表空间爆掉和锁等待超时。达梦的undo机制跟Oracle类似一个大事务产生的undo数据如果超过阈值会直接报错回滚。所以必须分批。分批的策略通常是按主键范围切每批处理固定行数。下面是一个典型的分批迁移存储过程骨架CREATE OR REPLACE PROCEDURE PROC_MIG_DATA( P_BATCH_SIZE IN INT DEFAULT 5000 ) AS V_MIN_ID BIGINT; V_MAX_ID BIGINT; V_CUR_ID BIGINT; V_ROW_CNT INT; V_BATCH_NO INT; V_TOTAL INT : 0; BEGIN -- 获取当前批次号 SELECT NVL(MAX(BATCH_NO), 0) 1 INTO V_BATCH_NO FROM MIG_LOG WHERE PROC_NAME PROC_MIG_DATA; -- 获取待迁移数据的主键范围 SELECT MIN(ID), MAX(ID) INTO V_MIN_ID, V_MAX_ID FROM SRC_TABLE WHERE MIG_FLAG N; IF V_MIN_ID IS NULL THEN INSERT INTO MIG_LOG(PROC_NAME, BATCH_NO, STATUS, REMARK) VALUES(PROC_MIG_DATA, V_BATCH_NO, SUCCESS, 无待迁移数据); COMMIT; RETURN; END IF; V_CUR_ID : V_MIN_ID; WHILE V_CUR_ID V_MAX_ID LOOP -- 插入目标表 INSERT INTO TGT_TABLE(ID, NAME, AMOUNT, CREATE_TIME) SELECT ID, NAME, AMOUNT, CREATE_TIME FROM SRC_TABLE WHERE ID V_CUR_ID AND ID V_CUR_ID P_BATCH_SIZE AND MIG_FLAG N; V_ROW_CNT : SQL%ROWCOUNT; -- 更新源表标记 UPDATE SRC_TABLE SET MIG_FLAG Y WHERE ID V_CUR_ID AND ID V_CUR_ID P_BATCH_SIZE AND MIG_FLAG N; -- 记录日志 INSERT INTO MIG_LOG(PROC_NAME, BATCH_NO, MIN_ID, MAX_ID, ROW_COUNT, STATUS, END_TIME) VALUES(PROC_MIG_DATA, V_BATCH_NO, V_CUR_ID, V_CUR_ID P_BATCH_SIZE - 1, V_ROW_CNT, SUCCESS, SYSDATE); COMMIT; V_TOTAL : V_TOTAL V_ROW_CNT; V_CUR_ID : V_CUR_ID P_BATCH_SIZE; END LOOP; -- 更新最终状态 UPDATE MIG_LOG SET REMARK 总计迁移 || V_TOTAL || 行 WHERE PROC_NAME PROC_MIG_DATA AND BATCH_NO V_BATCH_NO AND STATUS SUCCESS; COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; INSERT INTO MIG_LOG(PROC_NAME, BATCH_NO, MIN_ID, MAX_ID, STATUS, ERR_MSG, END_TIME) VALUES(PROC_MIG_DATA, V_BATCH_NO, V_CUR_ID, V_CUR_ID P_BATCH_SIZE - 1, FAILED, SQLERRM, SYSDATE); COMMIT; RAISE; END;这段代码有几个关键点值得展开说。批次大小怎么定。P_BATCH_SIZE默认5000这个值不是拍脑袋来的。太小了比如100那提交次数太多每次提交都有开销整体速度上不去。太大了比如50000单次事务的undo量可能超过限制。我的经验是如果单行数据不大几百字节5000到10000是比较合适的。如果单行数据有几KB那就降到1000到2000。你可以先跑一批试试看看undo表空间的使用情况再调整。为什么先插入再更新标记。有人可能会想能不能先更新标记再插入不行。如果先更新标记然后插入失败了那这批数据就被标记成已迁移但实际没迁过去数据就丢了。先插入再更新最坏的情况是插入成功但更新标记失败那下次会重复插入。重复插入的问题可以通过目标表的唯一约束或者MERGE语句来解决但数据丢失是没法补救的。异常处理里的ROLLBACK。注意异常块里先ROLLBACK再写日志。如果不回滚当前批次的部分数据可能已经写入但标记没更新会造成状态不一致。回滚之后写FAILED日志然后RAISE把异常抛出去让调度器知道任务失败了。3.3 断点续传的实现上面的代码其实已经隐含了断点续传的能力。因为每次循环都COMMIT而且源表的MIG_FLAG字段标记了哪些数据已经迁过。如果存储过程在中间某次循环失败了下次重新执行时SELECT MIN(ID) FROM SRC_TABLE WHERE MIG_FLAG N会自动从上次失败的位置继续。但这里有个细节要注意如果失败发生在INSERT成功但UPDATE标记之前那这批数据会被重复迁移。解决方法是给目标表加唯一约束或者在INSERT时用WHERE NOT EXISTS做去重。INSERT INTO TGT_TABLE(ID, NAME, AMOUNT, CREATE_TIME) SELECT S.ID, S.NAME, S.AMOUNT, S.CREATE_TIME FROM SRC_TABLE S WHERE S.ID V_CUR_ID AND S.ID V_CUR_ID P_BATCH_SIZE AND S.MIG_FLAG N AND NOT EXISTS (SELECT 1 FROM TGT_TABLE T WHERE T.ID S.ID);加了NOT EXISTS之后性能会稍微下降一点但换来的幂等性是值得的。尤其是迁移任务需要反复重跑的场景这个代价完全可以接受。3.4 跨实例迁移的处理如果源表和目标表不在同一个达梦实例上就需要用到DBLINK。达梦支持通过CREATE LINK创建数据库链接语法跟Oracle类似CREATE LINK LINK_TO_REMOTE CONNECT DM WITH USERNAME REMOTE_USER IDENTIFIED BY PASSWORD USING 192.168.1.100:5236;创建好之后就可以在SQL里用表名LINK名的方式访问远程表了。INSERT INTO TGT_TABLE(ID, NAME, AMOUNT) SELECT ID, NAME, AMOUNT FROM SRC_TABLELINK_TO_REMOTE WHERE ID V_CUR_ID AND ID V_CUR_ID P_BATCH_SIZE;注意跨实例迁移的性能瓶颈通常在网络。如果源表和目标表之间的网络延迟较高建议把批次大小调小比如降到1000减少单次网络传输的数据量。另外DBLINK的查询是在远程实例上执行的所以WHERE条件里的索引能不能用上取决于远程表的索引情况。跨实例迁移还有一个坑字符集。如果两个实例的字符集不一致中文可能会出现乱码。达梦的字符集在实例创建时就确定了后期修改很麻烦。所以在建库之前就要规划好迁移之前先用SELECT UNICODE()或者查V$INSTANCE确认两边的字符集是否一致。4. DBMS_SCHEDULER定时任务的配置4.1 创建定时任务的完整步骤存储过程写好了接下来就是让DBMS_SCHEDULER按时触发它。达梦的DBMS_SCHEDULER用法跟Oracle基本一致分三步创建Program、创建Schedule、创建Job。先创建Program定义要执行什么BEGIN DBMS_SCHEDULER.CREATE_PROGRAM( PROGRAM_NAME PROG_MIG_DATA, PROGRAM_TYPE STORED_PROCEDURE, PROGRAM_ACTION PROC_MIG_DATA, ENABLED TRUE, COMMENTS 数据迁移存储过程 ); END; /然后创建Schedule定义什么时候执行BEGIN DBMS_SCHEDULER.CREATE_SCHEDULE( SCHEDULE_NAME SCHED_MIG_DAILY, START_DATE SYSTIMESTAMP, REPEAT_INTERVAL FREQDAILY; BYHOUR2; BYMINUTE0; BYSECOND0, END_DATE NULL, COMMENTS 每天凌晨2点执行 ); END; /最后创建Job把Program和Schedule绑在一起BEGIN DBMS_SCHEDULER.CREATE_JOB( JOB_NAME JOB_MIG_DATA, PROGRAM_NAME PROG_MIG_DATA, SCHEDULE_NAME SCHED_MIG_DAILY, ENABLED TRUE, COMMENTS 每日数据迁移任务 ); END; /这三步做完任务就会在每天凌晨2点自动执行PROC_MIG_DATA存储过程。4.2 REPEAT_INTERVAL的语法详解REPEAT_INTERVAL用的是日历表达式语法Calendaring Expression Syntax跟Oracle的DBMS_SCHEDULER完全兼容。常用的几种写法需求表达式每天凌晨2点FREQDAILY; BYHOUR2; BYMINUTE0; BYSECOND0每小时执行一次FREQHOURLY; BYMINUTE0; BYSECOND0每30分钟执行一次FREQMINUTELY; INTERVAL30每周一至周五早上6点FREQWEEKLY; BYDAYMON,TUE,WED,THU,FRI; BYHOUR6每月1号凌晨3点FREQMONTHLY; BYMONTHDAY1; BYHOUR3实操心得如果迁移任务比较重建议避开业务高峰期。我一般会跟业务方确认一个低峰时段比如凌晨2点到4点之间。另外如果迁移任务可能跑超过24小时那就要考虑用FREQDAILY会不会导致任务重叠。达梦的DBMS_SCHEDULER默认不允许同一个Job的多个实例并行执行所以重叠的问题不用担心但你要确保任务能在下一次触发之前跑完。4.3 任务状态的查询与监控任务创建好之后怎么知道它有没有在跑、跑得怎么样达梦提供了几个视图-- 查看Job的基本信息和状态 SELECT JOB_NAME, ENABLED, STATE, LAST_START_DATE, NEXT_RUN_DATE, RUN_COUNT, FAILURE_COUNT FROM USER_SCHEDULER_JOBS WHERE JOB_NAME JOB_MIG_DATA; -- 查看Job的执行日志 SELECT LOG_ID, JOB_NAME, LOG_DATE, STATUS, ACTUAL_START_DATE, RUN_DURATION, ADDITIONAL_INFO FROM USER_SCHEDULER_JOB_LOG WHERE JOB_NAME JOB_MIG_DATA ORDER BY LOG_DATE DESC; -- 查看Job执行的详细信息包括错误信息 SELECT LOG_ID, JOB_NAME, STATUS, ERROR#, ACTUAL_START_DATE, RUN_DURATION, ADDITIONAL_INFO FROM USER_SCHEDULER_JOB_RUN_DETAILS WHERE JOB_NAME JOB_MIG_DATA ORDER BY LOG_DATE DESC;USER_SCHEDULER_JOB_RUN_DETAILS里的ADDITIONAL_INFO字段会记录错误信息如果任务失败了这里能看到具体的错误码和错误描述。我一般会写一个监控脚本每天早上检查一下前一天的执行状态如果有FAILED的就发告警。4.4 任务的修改与删除需要调整执行时间或者修改调用的存储过程时不用删了重建直接改就行-- 修改Job的调度频率 BEGIN DBMS_SCHEDULER.SET_ATTRIBUTE( NAME JOB_MIG_DATA, ATTRIBUTE REPEAT_INTERVAL, VALUE FREQDAILY; BYHOUR3; BYMINUTE0; BYSECOND0 ); END; / -- 暂停Job BEGIN DBMS_SCHEDULER.DISABLE(JOB_MIG_DATA); END; / -- 启用Job BEGIN DBMS_SCHEDULER.ENABLE(JOB_MIG_DATA); END; / -- 删除Job BEGIN DBMS_SCHEDULER.DROP_JOB(JOB_MIG_DATA, TRUE); END; /DROP_JOB的第二个参数TRUE表示如果Job正在运行也强制删除。一般建议先DISABLE等当前执行完了再DROP。5. 实操过程中踩过的坑与排查技巧5.1 权限问题DBMS_SCHEDULER用不了这是新手最容易遇到的问题。创建Job的时候报错权限不足或者DBMS_SCHEDULER不存在。原因通常是当前用户没有执行DBMS_SCHEDULER的权限。达梦里需要单独授权-- 用SYSDBA执行 GRANT CREATE JOB TO YOUR_USER; GRANT EXECUTE ON DBMS_SCHEDULER TO YOUR_USER;另外如果存储过程里涉及跨Schema访问表还需要相应的SELECT/INSERT/UPDATE权限。达梦的权限体系跟Oracle类似但有些细节不一样比如达梦默认的PUBLIC权限比Oracle要小很多在Oracle里能直接用的东西在达梦需要显式授权。5.2 任务执行了但数据没迁过去这种情况一般是存储过程内部出了异常但异常被吞掉了。检查USER_SCHEDULER_JOB_RUN_DETAILS的ADDITIONAL_INFO字段如果显示SUCCESS但数据没动那可能是存储过程里的异常处理块把错误吃掉了。我建议在存储过程的异常处理里除了写日志表还要用RAISE把异常重新抛出去。这样DBMS_SCHEDULER才能感知到失败在Job日志里标记为FAILED。如果只是写日志不抛异常调度器会认为任务成功执行了。5.3 迁移速度越来越慢刚开始跑的时候很快跑着跑着速度就降下来了。这个通常是因为源表的MIG_FLAG字段没有索引。每次查询WHERE MIG_FLAG N都要全表扫描数据量越大越慢。CREATE INDEX IDX_SRC_MIG_FLAG ON SRC_TABLE(MIG_FLAG, ID);注意索引的顺序MIG_FLAG在前ID在后。因为查询条件是MIG_FLAG N AND ID ? AND ID ?把等值条件放在前面范围条件放在后面索引的选择性更好。还有一个可能的原因是目标表的索引太多。每次INSERT都要维护索引索引越多写入越慢。如果目标表是专门用来做迁移的中间表可以考虑先禁用索引迁移完成后再重建。5.4 常见问题速查表问题现象可能原因排查方法解决方案Job创建失败报权限错误缺少CREATE JOB权限查USER_SYS_PRIVSGRANT CREATE JOB TO userJob执行成功但数据未迁移存储过程异常被吞查MIG_LOG表的ERR_MSG异常处理中加RAISE迁移速度逐渐变慢源表MIG_FLAG无索引EXPLAIN查看执行计划建复合索引(MIG_FLAG, ID)中文乱码源和目标字符集不一致查V$INSTANCE的字符集建库时统一字符集单批次undo超限批次大小设置过大查undo表空间使用率调小P_BATCH_SIZE跨实例迁移超时网络延迟高或DBLINK配置不当测试网络延迟调小批次检查LINK配置Job日志显示FAILED但无详细信息错误信息被截断查ADDITIONAL_INFO字段扩大ERR_MSG字段长度重复迁移同一批数据断点续传逻辑不完善对比MIG_LOG和TGT_TABLEINSERT加NOT EXISTS5.5 一个真实的排查案例有一次客户反馈说迁移任务连续三天都失败了错误信息是-3236: 无效的游标状态。这个错误号在达梦的文档里描述得很模糊网上也搜不到太多有用的信息。我上去排查先看了USER_SCHEDULER_JOB_RUN_DETAILS发现失败时间都在凌晨2点03分左右也就是任务刚启动不久。然后查了MIG_LOG表发现最后一条SUCCESS日志的MAX_ID是某个值之后的批次都是FAILED。进一步分析发现源表在那个时间点正好有一个批量删除操作在跑删除了大量MIG_FLAG Y的历史数据。存储过程在循环过程中游标指向的数据被其他会话删除了导致游标状态失效。解决方案有两个一是调整迁移任务的执行时间避开数据清理窗口二是在存储过程里加一个异常捕获遇到游标失效时重新打开游标继续。我选了第一个方案因为改时间最简单而且从业务逻辑上讲迁移和数据清理本来就不应该同时跑。这个案例告诉我们迁移任务的时间窗口规划很重要。你要清楚在这个时间段内还有哪些任务在操作同一批表。如果有冲突要么调整时间要么在存储过程里做好并发控制。6. 性能调优与进阶技巧6.1 批量提交的优化前面代码里用的是INSERT INTO ... SELECT的方式这是达梦里批量插入最快的方式之一。但如果你需要更精细的控制比如在插入前做一些数据转换可以用BULK COLLECT加FORALLDECLARE TYPE T_ID IS TABLE OF SRC_TABLE.ID%TYPE; TYPE T_NAME IS TABLE OF SRC_TABLE.NAME%TYPE; V_IDS T_ID; V_NAMES T_NAME; CURSOR C_DATA IS SELECT ID, NAME FROM SRC_TABLE WHERE MIG_FLAG N AND ROWNUM 5000; BEGIN OPEN C_DATA; LOOP FETCH C_DATA BULK COLLECT INTO V_IDS, V_NAMES LIMIT 5000; EXIT WHEN V_IDS.COUNT 0; FORALL i IN 1..V_IDS.COUNT INSERT INTO TGT_TABLE(ID, NAME) VALUES(V_IDS(i), V_NAMES(i)); COMMIT; END LOOP; CLOSE C_DATA; END;BULK COLLECT一次性把数据拉到PL/SQL的内存集合里FORALL批量执行DML。这种方式比逐行处理快很多但内存消耗也更大。5000行的集合大概占几MB内存一般没问题。6.2 并行度的考虑达梦支持并行查询和并行DML。如果服务器CPU核数多可以在存储过程里开启并行ALTER SESSION FORCE PARALLEL DML PARALLEL 4; ALTER SESSION FORCE PARALLEL QUERY PARALLEL 4;但并行不是万能的。如果源表和目标表在同一个磁盘上并行读写反而会因为I/O竞争导致性能下降。另外并行DML会占用更多的undo和临时表空间。我一般只在数据量特别大千万级以上且服务器资源充足的情况下才开并行。6.3 迁移任务的幂等性设计幂等性是指同一个任务执行多次结果是一样的。这在迁移场景里非常重要因为任务可能因为各种原因重跑。除了前面提到的NOT EXISTS去重还有一种更彻底的方式在目标表上建唯一索引然后用MERGE语句MERGE INTO TGT_TABLE T USING (SELECT ID, NAME, AMOUNT FROM SRC_TABLE WHERE ID V_CUR_ID AND ID V_CUR_ID P_BATCH_SIZE AND MIG_FLAG N) S ON (T.ID S.ID) WHEN MATCHED THEN UPDATE SET T.NAME S.NAME, T.AMOUNT S.AMOUNT WHEN NOT MATCHED THEN INSERT (ID, NAME, AMOUNT) VALUES(S.ID, S.NAME, S.AMOUNT);MERGE的好处是既能插入新数据又能更新已存在的数据。如果你的迁移场景是增量同步而不是一次性搬迁MERGE是更好的选择。6.4 监控与告警的集成DBMS_SCHEDULER本身不提供告警功能但你可以通过查询Job日志来实现。我一般会写一个独立的监控存储过程每天定时检查前一天的迁移状态CREATE OR REPLACE PROCEDURE PROC_MIG_MONITOR AS V_FAIL_CNT INT; BEGIN SELECT COUNT(*) INTO V_FAIL_CNT FROM USER_SCHEDULER_JOB_RUN_DETAILS WHERE JOB_NAME JOB_MIG_DATA AND STATUS FAILED AND LOG_DATE TRUNC(SYSDATE) - 1; IF V_FAIL_CNT 0 THEN -- 这里可以插入告警表或者调用发邮件/短信的接口 INSERT INTO MIG_ALERT(ALERT_TIME, ALERT_MSG) VALUES(SYSDATE, 数据迁移任务失败 || V_FAIL_CNT || 次); COMMIT; END IF; END;然后给这个监控存储过程也创建一个定时任务每天早上8点跑一次。这样即使半夜迁移失败了第二天上班也能第一时间知道。6.5 大数据量迁移的分区策略如果源表是分区表迁移的时候可以利用分区裁剪来提升性能。比如按时间分区的表可以每次只迁移一个分区的数据-- 查看分区信息 SELECT TABLE_NAME, PARTITION_NAME, HIGH_VALUE FROM USER_TAB_PARTITIONS WHERE TABLE_NAME SRC_TABLE ORDER BY PARTITION_POSITION; -- 按分区迁移 INSERT INTO TGT_TABLE SELECT * FROM SRC_TABLE PARTITION(P202401); COMMIT;按分区迁移的好处是每次处理的数据量可控而且如果某个分区迁移失败不影响其他分区。缺点是分区多了之后存储过程里的循环逻辑会复杂一些。实操心得达梦的分区表语法跟Oracle高度兼容但有些细节不一样。比如达梦不支持INTERVAL分区自动分区需要手动添加分区。另外达梦的分区索引类型LOCAL/GLOBAL的选择也会影响迁移性能LOCAL索引在分区迁移时维护成本更低。7. 一些个人体会这套方案我在三个项目里实际用过最长的跑了将近一年每天迁移200万到500万行数据没有出过数据丢失的问题。中间因为业务调整改过几次迁移逻辑都是只改存储过程调度任务没动过维护成本很低。如果你刚开始接触达梦的存储过程和DBMS_SCHEDULER我的建议是先在测试环境把整个流程跑通。建两张测试表写一个最简单的迁移存储过程创建一个每分钟执行一次的Job观察日志表的记录情况。确认没问题了再把批次大小、执行频率这些参数调到生产环境的值。另外达梦的文档虽然不如Oracle的详细但基本的语法和示例都有。遇到报错的时候先查达梦的错误码手册大部分问题都能找到答案。实在找不到的去达梦的社区论坛搜一下或者直接提工单他们的技术支持响应速度还可以。最后说一个容易被忽略的点迁移完成后的数据校验。不要以为存储过程跑完了就万事大吉一定要写一个校验脚本对比源表和目标表的行数、关键字段的汇总值比如SUM、COUNT DISTINCT。我一般会在迁移存储过程的最后加一段校验逻辑如果行数对不上就写一条WARNING日志。这个习惯帮我发现过好几次数据不一致的问题都是因为源表在迁移过程中有并发写入导致的。