Oracle存储过程被覆盖找回实战

来源:这里教程网 时间:2026-03-03 22:58:18 作者:

LogMiner 归档日志挖掘深度解析

 

 

  一、问题背景与技术痛点

某医疗客户的核心业务系统中,运维人员执行  `CREATE OR REPLACE PROCEDURE`  语句时,误将业务用户的关键存储过程覆盖,且操作时间点距发现时已超过 10 小时。客户急需找回原始存储过程逻辑,否则将影响后续业务流程。

 

  为何常规方案行不通?

1. DDL 操作的特殊性: `CREATE OR REPLACE`  属于 DDL 语句 ,执行时会隐式提交事务, Oracle 闪回查询( Flashback Query )仅适用于 DML 操作的数据恢复,无法直接恢复被 DDL 覆盖的数据库对象;

2.  时间窗口失效:由于 操作已过去十多个小时 ,基表数据的闪回查询窗口已超出限制,通过查询数据字典历史版本的方式也无法获取原始内容;

3.  场景创新性:团队 以往使用 LogMiner 工具多为追溯数据变更(如误删数据、异常更新),未处理过存储过程等 PL/SQL 对象的版本找回 ,无成熟经验可直接复用。

 

  二、核心原理与猜想推导

 1.  底层存储逻辑拆解

Oracle 中,存储过程的元数据(如对象 ID 、审计信息)存储在  `SYS.PROCEDURE$`  表,而源码则按行存储在 `SYS.SOURCE$`  表(通过  `OBJ#`  关联)。基于这一特性,我提出核心猜想:   

`CREATE OR REPLACE`  覆盖存储过程时 ,底层逻辑应为「删除旧源码 插入新源码」  —— 即先删除 `SYS.SOURCE$`  中该存储过程对应的旧行记录,再插入新版本的源码行 。若猜想成立,归档日志中必然会记录这一系列删除操作,其  `SQL_UNDO`  字段就是恢复原始源码的关键。

 

 2.  测试环境模拟验证

为验证猜想,我搭建了与客户环境一致的测试环境,完整复现存储过程创建 覆盖的全流程:

 

  1 )创建原始存储过程

CREATE OR REPLACE PROCEDURE mary.insert_test_data

IS

    v_ind INTEGER; --  声明循环变量

BEGIN

    v_ind := 0; --  初始化变量

    LOOP

        --  插入数据到mary.tt

        INSERT INTO mary.tt(id, time) 

        VALUES(v_ind, SYSDATE);

        --  每插入1 条提交(原逻辑保留,若想批量提交可调整)

        COMMIT;

        v_ind := v_ind + 1; --  原始自增步长:1

        --  循环终止条件:插入10 万条后退出

        IF v_ind = 100000 THEN

            EXIT; --  替代return PL/SQL 循环中用EXIT 更规范

        END IF;

    END LOOP;

 

EXCEPTION

    WHEN OTHERS THEN

        --  异常时回滚并打印错误信息(便于测试排错)

        ROLLBACK;

        DBMS_OUTPUT.PUT_LINE(' 错误码:' || SQLCODE || ' ,错误信息:' || SQLERRM);

        RAISE; --  可选:重新抛出异常,便于外层捕获

END;

/

 

  2 )模拟误操作覆盖存储过程

修改两个核心常量:将  `v_ind`  自增步长由 1 改为 2 ,循环终止条件由 10 万条改为 30 万条:

CREATE OR REPLACE PROCEDURE mary.insert_test_data

IS

    v_ind INTEGER; --  声明循环变量

BEGIN

    v_ind := 0; --  初始化变量

    LOOP

        --  插入数据到 mary.tt

        INSERT INTO mary.tt(id, time) 

        VALUES(v_ind, SYSDATE);

        --  每插入 1 条提交(原逻辑保留,若想批量提交可调整)

        COMMIT;

        v_ind := v_ind + 2; --  修改后自增步长: 2

        --  循环终止条件:插入 30 万条后退出

        IF v_ind = 300000 THEN

            EXIT; --  替代 return PL/SQL 循环中用 EXIT 更规范

        END IF;

    END LOOP;

 

EXCEPTION

    WHEN OTHERS THEN

        --  异常时回滚并打印错误信息(便于测试排错)

        ROLLBACK;

        DBMS_OUTPUT.PUT_LINE(' 错误码: ' || SQLCODE || ' ,错误信息: ' || SQLERRM);

        RAISE; --  可选:重新抛出异常,便于外层捕获

END;

/

 

 

  3 )手工切换归档日志

确保误操作记录写入目标归档日志,便于后续挖掘:

ALTER SYSTEM ARCHIVE LOG CURRENT;

通过  `ARCHIVE LOG LIST`  确认当前归档号为 179 ,目标归档日志文件为 `/oradata/archivelog/1_179_1203889318.dbf`

 

  三、 LogMiner 挖掘实施:从猜想验证到源码提取

 1.  工具准备:匿名块设计思路

为高效挖掘日志,我设计了两个匿名块,分别解决「快速定位操作」和「详细数据分析」的需求:

检查操作类型版:快速统计日志中各对象的操作类型和次数,验证「删除旧源码 插入新源码」的猜想;

建表版:将日志数据持久化到物理表,便于后续筛选、排序和细节分析,避免重复解析日志。

 

  1 )检查操作类型版匿名块

set linesize 5000

SET SERVEROUTPUT ON SIZE UNLIMITED;

BEGIN

  DBMS_LOGMNR.ADD_LOGFILE('/oradata/archivelog/1_179_1203889318.dbf');

  DBMS_LOGMNR.START_LOGMNR(OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);

  FOR rec IN (

    SELECT SEG_OWNER, SEG_NAME, OPERATION ,count(*) as op_count

    FROM V$LOGMNR_CONTENTS

    WHERE 1=1 group by SEG_OWNER, SEG_NAME, OPERATION order by SEG_OWNER, SEG_NAME, OPERATION,op_count

  ) LOOP

    DBMS_OUTPUT.PUT_LINE('SEG_OWNER: '    || RPAD(rec.SEG_OWNER, 10) ||

                         ' | SEG_NAME: '  || RPAD(rec.SEG_NAME, 40) ||

                         ' | OPERATION: ' || RPAD(rec.OPERATION, 10) ||

                         ' | op_count: '  || RPAD(rec.op_count, 10));

  END LOOP;

  DBMS_LOGMNR.END_LOGMNR;

END;

/

 

  2 )建表版匿名块

set linesize 5000

BEGIN

  DBMS_LOGMNR.ADD_LOGFILE('/oradata/archivelog/1_179_1203889318.dbf');

  DBMS_LOGMNR.START_LOGMNR(OPTIONS => DBMS_LOGMNR.DICT_FROM_ONLINE_CATALOG);

  EXECUTE IMMEDIATE '

    CREATE TABLE log202512 AS

    SELECT *  FROM V$LOGMNR_CONTENTS

    WHERE 1=1 

  ';

  DBMS_LOGMNR.END_LOGMNR;

  DBMS_OUTPUT.PUT_LINE('Table created with data from V$LOGMNR_CONTENTS');

EXCEPTION

  WHEN OTHERS THEN

    BEGIN

      DBMS_LOGMNR.END_LOGMNR;

    EXCEPTION

      WHEN OTHERS THEN

        NULL; --  忽略 LogMiner 已关闭的情况

    END;

    DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);

    RAISE;

END;

/

 

 2.  第一步:猜想验证 —— 操作类型统计分析

执行「检查操作类型版」匿名块后,得到关键统计结果(筛选核心信息如下):

SEG_OWNER

SEG_NAME

OPERATION

op_count

分析结论

MARY

INSERT_TEST_DATA

DDL

2

对应存储过程的「新建」和「覆盖修改」两次 DDL  操作

SYS

PROCEDURE$

INSERT

1

新建存储过程时,向元数据表插入元数据

SYS

PROCEDURE$

UPDATE

1

覆盖存储过程时,更新元数据表的元数据(如审计信息)

SYS

SOURCE$

DELETE

28

覆盖时删除原始源码,共  28  行(与存储过程源码行数一致)

SYS

SOURCE$

INSERT

56

新建插入  28  行源码  +  覆盖时插入  28  行新源码,总数 56  行,与  DELETE  次数呈 2:1  关系

 

核心发现: `SYS.SOURCE$`  表的「 DELETE 28  + INSERT 56 次」完全印证了最初的猜想 —— 存储过程覆盖的底层逻辑是「删除旧源码 插入新源码」 ,这为后续提取原始源码提供了关键依据。

 

 3.  第二步:源码提取 —— 精准筛选关键记录

执行「建表版」匿名块后,日志数据已持久化到  `log202512`  表。接下来通过多轮查询,逐步定位原始源码:

 

  1 )第一轮查询:直接查询业务用户对象

最初尝试直接筛选  `SEG_OWNER='MARY'`   的记录,期望获取存储过程的创建语句:

SELECT

    SCN, TIMESTAMP, OPERATION, SQL_REDO, SQL_UNDO

FROM

    sys.log202512

WHERE SEG_OWNER='MARY';

 

结果分析: `SQL_REDO`  字段确实包含两次  `CREATE OR REPLACE PROCEDURE`  语句,但  `SQL_UNDO`  均为空。 真实环境中归档日志通常不会保留存储过程新建的完整记录,仅靠业务用户对象查询无法获取旧版本。

 

  2 )第二轮查询:挖掘系统表关键操作

既然业务用户对象查询无果, 转而聚焦系统表  `SYS.SOURCE$`   `SYS.PROCEDURE$` ,筛选有有效 `SQL_UNDO`  的记录:

SELECT

    SCN, TIMESTAMP, XIDUSN, XIDSQN, OPERATION,

    SEG_OWNER, SEG_NAME, SQL_REDO, SQL_UNDO

FROM

    sys.log202512

WHERE 

    SQL_UNDO IS NOT NULL

    AND SQL_UNDO <> 'Unsupported'

    AND SEG_OWNER = 'SYS'

    AND SEG_NAME IN ('SOURCE$', 'PROCEDURE$');

 

  3 )关键记录解析:还原原始源码

查询结果中,两条核心事务记录揭示了完整流程:

 

  事务 1 新建存储过程( SCN=5179831 TIMESTAMP=2025-12-14 00:46:58

首先向  `SYS.PROCEDURE$`  插入元数据:

  INSERT INTO "SYS"."PROCEDURE$"("OBJ#","AUDIT$","STORAGESIZE","OPTIONS")

  VALUES ('88608','--------------------------------------',NULL,'0');

  

随后向  `SYS.SOURCE$`  插入 28 行源码(按行号排序),例如:

  INSERT INTO "SYS"."SOURCE$"("OBJ#","LINE","SOURCE") VALUES ('88608','1','PROCEDURE     insert_test_data¶');

  INSERT INTO "SYS"."SOURCE$"("OBJ#","LINE","SOURCE") VALUES ('88608','2','IS¶');

  INSERT INTO "SYS"."SOURCE$"("OBJ#","LINE","SOURCE") VALUES ('88608','3','    v_ind INTEGER; --  声明循环变量 ¶');

  ...

  INSERT INTO "SYS"."SOURCE$"("OBJ#","LINE","SOURCE") VALUES ('88608','14','        v_ind := v_ind + 1;¶');

  

   这部分记录完整呈现了原始存储过程的源码结构, `OBJ#=88608`  是该存储过程的唯一标识。

 

  事务 2 存储过程覆盖修改( SCN=5179853 TIMESTAMP=2025-12-14 00:47:16

首先更新  `SYS.PROCEDURE$`  的元数据:

  UPDATE "SYS"."PROCEDURE$"

  SET "AUDIT$" = '--------------------------------------', "OPTIONS" = '0' 

  WHERE "OBJ#" = '88608' AND ROWID = 'AAAADdAABAAAVH8AAi';

  

随后执行 28  `DELETE`  语句,删除  `SYS.SOURCE$`   `OBJ#=88608`  的旧源码(即原始存储过程),例如:

  DELETE FROM "SYS"."SOURCE$" 

  WHERE "OBJ#" = '88608' AND "LINE" = '14' AND "SOURCE" = '        v_ind := v_ind + 1;¶' AND ROWID = 'AAAADgAABAAAWZjAAY';

  

   关键亮点:这些 DELETE 语句的  `SQL_UNDO`  字段,正是恢复原始源码的核心 —— 每一条 DELETE SQL_UNDO 都是一条 INSERT 语句,对应原始源码的一行。

最后执行 28  `INSERT`  语句,插入修改后的新源码(例如  `v_ind := v_ind + 2;` )。

 

 4.  最终提取:原始源码还原

将事务 2  `SYS.SOURCE$`  表的 28  `DELETE`  操作对应的  `SQL_UNDO`  字段提取出来,按  `LINE`  行号排序,去除换行符  `¶`  并格式化,即可完整还原原始存储过程的逻辑:

CREATE OR REPLACE PROCEDURE mary.insert_test_data

IS

    v_ind INTEGER; --  声明循环变量

BEGIN

    v_ind := 0; --  初始化变量

    LOOP

        --  插入数据到 mary.tt

        INSERT INTO mary.tt(id, time) 

        VALUES(v_ind, SYSDATE);

        --  每插入 1 条提交(原逻辑保留,若想批量提交可调整)

        COMMIT;

        v_ind := v_ind + 1;

        --  循环终止条件:插入 10 万条后退出

        IF v_ind = 100000 THEN

            EXIT; --  替代 return PL/SQL 循环中用 EXIT 更规范

        END IF;

    END LOOP;

 

EXCEPTION

    WHEN OTHERS THEN

        --  异常时回滚并打印错误信息(便于测试排错)

        ROLLBACK;

        DBMS_OUTPUT.PUT_LINE(' 错误码: ' || SQLCODE || ' ,错误信息: ' || SQLERRM);

        RAISE; --  可选:重新抛出异常,便于外层捕获

END;

/

 

  三、关键分析与经验总结

猜想验证闭环:从「存储过程覆盖 = 删除旧源码 + 插入新源码」的猜想,到 LogMiner 统计结果的印证,再到系统表记录的精准提取,形成完整的技术逻辑闭环;

系统表的关键作用: `SYS.SOURCE$`   `SYS.PROCEDURE$`  作为 Oracle 存储 PL/SQL 对象的核心字典表,是本次恢复的关键突破口,其操作记录完整保留了对象的版本变更轨迹;

- SQL_UNDO 的价值: DDL 操作的  `SQL_UNDO`  虽在业务用户对象中为空,但在系统表的底层操作中依然有效,这是本次恢复成功的核心技术点。

 

  四、总结

本次案例通过「猜想 验证 提取」的全流程,成功利用 LogMiner 工具从归档日志中找回了被覆盖的 Oracle 存储过程原始源码,解决了 DDL 操作无法通过常规闪回恢复的技术难题。核心价值不仅在于成功恢复数据,更在于形成了一套可复用的 PL/SQL 对象版本找回方案 —— 关键在于理解 Oracle 底层存储逻辑,聚焦系统字典表的操作轨迹,善用 LogMiner  `SQL_UNDO`  字段。

 

相关推荐