ITPub博客

首页 > 数据库 > Oracle > 记一次 Oracle 存储过程被覆盖找回案例分析

记一次 Oracle 存储过程被覆盖找回案例分析

原创 Oracle 作者:龙山游龙 时间:2026-02-09 15:40:07 0 删除 编辑

1 、故障描述

某次某用户,误操作执行 `CREATE OR REPLACE PROCEDURE` 语句时,误将业务用户的关键存储过程覆盖,且操作时间点距发现时已超过数小时。用户急需找回原始存储过程逻辑,否则将影响后续业务流程。

 

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` 。

( 4 )、创建两个匿名块

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

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

检查匿名块

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;

/

建表匿名块

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;

/

 

3 、解决方案

原存储过程 DDL 语句提取:

执行「建表」匿名块后,日志数据已持久化到 `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 )最终提取:原始 DDL 语句还原

将事务 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;

/

 


来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/30310891/viewspace-3110295/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
传统商业、开源和国产据库领域十从业经验,TB级高并发数据库的管理经验。对于数据库内部原理、体系结构,数据页/块结构原理有着深刻认识;对于数据库新特性、分布式高可用架构和性能调优有着丰富的的实战经验。擅长故障诊断以及数据库异常打开及数据灾难挽救。

注册时间:2015-06-28

  • 博文量
    178
  • 访问量
    334662