[20250501]truncate table恢复实战.txt

[20250501]truncate table恢复实战.txt

--//别人数据库不小心truncate table,需要恢复,本以为3-4个小时可以完成,结果几乎使用1倍的工作时间。
--//往往是节前最容易出错的时候。
--//这类事情虽然以前做过练习,实战的情况几乎没有,在测试环境重新模拟整个的恢复过程。

1.环境:
SCOTT@book01p> @ ver2
==============================
PORT_STRING                   : x86_64/Linux 2.4.xx
VERSION                       : 21.0.0.0.0
BANNER                        : Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
BANNER_FULL                   : Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
BANNER_LEGACY                 : Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
CON_ID                        : 0
PL/SQL procedure successfully completed.

2.测试环境建立:
--//drop table t purge ;

SCOTT@book01p> create table t as select * from all_objects;
Table created.

SCOTT@book01p> create table t_bak as select * from t ;
Table created.

COTT@book01p> select count(*) from t;
  COUNT(*)
----------
     69881

SCOTT@book01p> select count(*) from t_bak;
  COUNT(*)
----------
     69881

SCOTT@book01p> truncate table t ;
Table truncated.
    
SCOTT@book01p> insert into t select * from t_bak where owner='SCOTT';
63 rows created.

SCOTT@book01p> commit ;
Commit complete.

SCOTT@book01p> select rowid from t;
ROWID
------------------
AAAeg8AAMAAAACuAAA
AAAeg8AAMAAAACuAAB
AAAeg8AAMAAAACuAAC
...
AAAeg8AAMAAAACvAAA
AAAeg8AAMAAAACvAAB
63 rows selected.

SCOTT@book01p> @ rowid AAAeg8AAMAAAACuAAA

DATA_OBJECT_ID       FILE      BLOCK        ROW ROWID_DBA            DBA                  TEXT
-------------- ---------- ---------- ---------- -------------------- -------------------- --------------------------------------------------
        124988         12        174          0  0x30000AE           12,174               alter system dump datafile 12 block 174 ;
--//实际上已经插入数据块dba=12,174以及dba=12,175.
--//注:先把这部分数据建立新表保存在别的表空间里面步骤略。

--//注:真实的环境实际上打开归档日志,但是没有rman备份,事发前几天做了expdp的导出。在truncate后已经存在dml操作,覆盖小部
--//分数据块信息,这样比较真实的模拟现实的情况。

3.恢复:
--//确定truncate前数据段号data_object_id.
SYS@book01p> @ o2 scott.t
SYS@book01p> @ pr
==============================
O_OWNER                       : SCOTT
O_OBJECT_NAME                 : T
O_OBJECT_TYPE                 : TABLE
SEG_PART_NAME                 :
O_STATUS                      : VALID
OID                           : 124986
D_OID                         : 124988
CREATED                       : 2025-05-01 10:08:27
LAST_DDL_TIME                 : 2025-05-01 10:15:30
PL/SQL procedure successfully completed.
--//truncate后已经是data_object_id=124988。

SYS@book01p> @ versions obj$ OBJ#,DATAOBJ#,NAME s '' '' obj#=124986
VERSIONS_STARTTIME   VERSIONS_ENDTIME     VERSIONS_STARTSCN VERSIONS_ENDSCN VERSIONS_XID     V       OBJ#   DATAOBJ# NAME
-------------------- -------------------- ----------------- --------------- ---------------- - ---------- ---------- ------------------------------
2025-05-01 10:08:27. 2025-05-01 10:15:28.          36631539        36632608 080009009F1E0000 I     124986     124986 T
2025-05-01 10:15:28.                               36632608                 0A000300331E0000 U     124986     124988 T
--//可以确定truncate后data_object_id=124986。一般以前没有move或者truncate,object_id=data_object_id.
--//也许还有许多情况两者不相等。
--//顺便提一下tab$,seg$表不同通过版本查询,可能原因是cluster table的一部分。
SYS@book01p> @ versions tab$ OBJ#,DATAOBJ#,TS# s '' '' obj#=124986
SELECT versions_starttime
*
ERROR at line 1:
ORA-00600: internal error code, arguments: [ktrvGetChildRwsIdxCtx_not_found], [], [], [], [], [], [], [], [], [], [], []
--//注:可以使用as of scn|timestamp 查询获得以前的记录信息。

--//恢复的方法是通过扫描数据文件,通过rowid读取对应数据块。前提先还原obj$,tab$,seg$为原来的状态。
--//实际上仅仅还原obj$就可以了。

SYS@book01p> select * from obj$ where obj#=124986
  2  @ pr
==============================
OBJ#                          : 124986
DATAOBJ#                      : 124988
OWNER#                        : 109
NAME                          : T
NAMESPACE                     : 1
SUBNAME                       :
TYPE#                         : 2
CTIME                         : 2025-05-01 10:08:27
MTIME                         : 2025-05-01 10:15:30
STIME                         : 2025-05-01 10:08:27
STATUS                        : 1
REMOTEOWNER                   :
LINKNAME                      :
FLAGS                         : 0
OID$                          :
SPARE1                        : 6
SPARE2                        : 1
SPARE3                        : 109
SPARE4                        :
SPARE5                        :
SPARE6                        :
SIGNATURE                     : 570DD59CAB4634BF17253AE92B1920B0
SPARE7                        : 134233583
SPARE8                        : 0
SPARE9                        : 0
DFLCOLLID                     : 16382
CREAPPID                      :
CREVERID                      :
CREPATCHID                    :
MODAPPID                      :
MODVERID                      :
MODPATCHID                    :
SPARE10                       :
SPARE11                       :
SPARE12                       :
SPARE13                       :
SPARE14                       :
PL/SQL procedure successfully completed.

SYS@book01p> update (select * from obj$ where obj#=124986 and DATAOBJ#=124988) set DATAOBJ#=124986;
1 row updated.

SYS@book01p> commit ;
Commit complete.

--//确定扫描数据文件最大块号。
SCOTT@book01p> select * from dba_DATA_FILES where file_id=12
  2  @ pr
==============================
FILE_NAME                     : /u01/oradata/BOOK/book01p/users01.dbf
FILE_ID                       : 12
TABLESPACE_NAME               : USERS
BYTES                         : 267386880
BLOCKS                        : 32640
STATUS                        : AVAILABLE
RELATIVE_FNO                  : 12
AUTOEXTENSIBLE                : YES
MAXBYTES                      : 34359721984
MAXBLOCKS                     : 4194302
INCREMENT_BY                  : 160
USER_BYTES                    : 266338304
USER_BLOCKS                   : 32512
ONLINE_STATUS                 : ONLINE
LOST_WRITE_PROTECT            : OFF
PL/SQL procedure successfully completed.
--//最大块号 32640。
--//$ cd bbed ;
--//$ rm  log.bbd
--//我的bbed取了别名并且定义为函数rlbbed。在parfile=bbed.par加入spool=Y,这样执行的输出记录在log.bbd文件里面。
$ type rlbbed
rlbbed is a function
rlbbed ()
{
    cd /home/oracle/bbed;
    $RLWRAP -s 9999 -c -r -i -f /usr/local/share/rlwrap/bbed $BBED parfile=bbed.par cmdfile=cmd.par
}

$ seq 128 1 32640 | xargs -IQ echo p /d dba 12,Q ktbbhsid.ktbbhsg1 | rlbbed  > /dev/null
--//输出有点长。

--//确定那些数据块的段号等于124986。
$ grep -B1  "  124986$" log.bbd | grep ktbbhsid.ktbbhsg1 | head -4
BBED> p /d dba 12,176 ktbbhsid.ktbbhsg1
BBED> p /d dba 12,177 ktbbhsid.ktbbhsg1
BBED> p /d dba 12,178 ktbbhsid.ktbbhsg1
BBED> p /d dba 12,179 ktbbhsid.ktbbhsg1

--//将需要扫描的数据块保存在文本scan.txt文件中。
$ grep -B1 124986$ log.bbd | grep ktbbhsid.ktbbhsg1 > scan.txt

SCOTT@book01p> @ seg2 t ''
    SEG_MB OWNER                SEGMENT_NAME                   SEGMENT_TYPE         SEG_TABLESPACE_NAME                BLOCKS     HDRFIL     HDRBLK
---------- -------------------- ------------------------------ -------------------- ------------------------------ ---------- ---------- ----------
         0 SCOTT                T                              TABLE                USERS                                   8         12        170

SCOTT@book01p> select * from dba_extents where segment_name='T'
  2  @ pr
==============================
OWNER                         : SCOTT
SEGMENT_NAME                  : T
PARTITION_NAME                :
SEGMENT_TYPE                  : TABLE
TABLESPACE_NAME               : USERS
EXTENT_ID                     : 0
FILE_ID                       : 12
BLOCK_ID                      : 168
BYTES                         : 65536
BLOCKS                        : 8
RELATIVE_FNO                  : 12
PL/SQL procedure successfully completed.

--//段头在12,170.而实际上truncate的插入已经导致前面数据块做个格式化,数据块168到175已经标识为段号124988。
--//通过查看log.bbd文件内容也可以确定.
BBED> p /d dba 12,168 ktbbhsid.ktbbhsg1
BBED-00400: invalid blocktype (32)

BBED> p /d dba 12,169 ktbbhsid.ktbbhsg1
BBED-00400: invalid blocktype (33)

BBED> p /d dba 12,170 ktbbhsid.ktbbhsg1
BBED-00400: invalid blocktype (35)

BBED> p /d dba 12,171 ktbbhsid.ktbbhsg1
ub4 ktbbhsg1                                @24       124988

BBED> p /d dba 12,172 ktbbhsid.ktbbhsg1
ub4 ktbbhsg1                                @24       124988

BBED> p /d dba 12,173 ktbbhsid.ktbbhsg1
ub4 ktbbhsg1                                @24       124988

BBED> p /d dba 12,174 ktbbhsid.ktbbhsg1
ub4 ktbbhsg1                                @24       124988

BBED> p /d dba 12,175 ktbbhsid.ktbbhsg1
ub4 ktbbhsg1                                @24       124988

BBED> p /d dba 12,176 ktbbhsid.ktbbhsg1
ub4 ktbbhsg1                                @24       124986

--//尝试通过rowid访问数据块看看。
SCOTT@book01p> select OWNER,OBJECT_NAME from t where rowid = dbms_rowid.rowid_create(1,124986,12,176,0);
OWNER                          OBJECT_NAME
------------------------------ ------------------------------
SYS                            SQLOBJ$PLAN
--//OK,说明通过rowid方式取数据没有问题。

SYS@book01p> create table scott.scanblock ( file_id number,block_id number  )  tablespace TSP_AUDIT;
Table created.

SYS@book01p> create table scott.bak_t tablespace TSP_AUDIT as select * from scott.t where 0=1;
Table created.
--//注意建立的新表一定不能使用原来的表空间,避免覆盖。

$ awk '{print $5}' scan.txt |  sed 's/^/insert into scanblock values (/;s/$/);/' > scan1.txt
$ head -2 scan1.txt ; tail -2 scan1.txt
insert into scanblock values (12,176);
insert into scanblock values (12,177);
insert into scanblock values (12,24614);
insert into scanblock values (12,24615);
--//执行@scan1.txt.注意提交。

--//从网上找的脚本我修改仅仅扫描scott.scanblock表。实际上8k数据块最多736条记录,对应这样表取200行号已经足够,不然扫描有点
--//慢,正常不会遗漏。
$ cat truncT.txt
declare
   v_fno number;
   v_s_bno number;
   v_e_bno number;
   v_rowid rowid;
   v_owner varchar2(100):='&&1';
   v_table varchar2(100):='&&2';
   v_o_owner varchar2(100):='&&3';
   v_o_table varchar2(100):='&&4';
   v_dataobj number;
   v_sql varchar2(4000);
   v_tablespace varchar2(100);
   nrows number;
begin
   nrows:=0;
   select data_object_id into v_dataobj from dba_objects where owner=v_owner and object_name=v_table;
--  select tablespace_name into v_tablespace from dba_tables where owner=v_owner and table_name=v_table;
   for i in (select file_id,block_id from scott.scanblock) loop
   v_fno:=i.file_id;
   v_s_bno:=i.block_id;
   v_e_bno:=i.block_id+1-1;
       for j in v_s_bno .. v_e_bno loop
        begin
         for x in 0 .. 200 loop
           v_rowid:=dbms_rowid.rowid_create(1,v_dataobj,v_fno,j,x);
           v_sql:='insert into '||v_o_owner||'.'||v_o_table||' select * from '||v_owner||'.'||v_table||' where rowid=:1';
                   execute immediate v_sql using v_rowid;
           if sql%rowcount = 1 then nrows:=nrows+1; end if;
               if (mod(nrows,10000)=0) then commit; end if;
         end loop;
        exception
          when others then
            null;
        end;
        commit;
       end loop;
    end loop;
end;
/

SYS@book01p> @ truncT.txt SCOTT T SCOTT BAK_T
PL/SQL procedure successfully completed.
--//注意大写owner,表名。

SCOTT@book01p> select count(*) from bak_t ;
  COUNT(*)
----------
     69567
--//丢失了69881-69567 = 314。

SCOTT@book01p> select * from bak_t minus select * from t_bak;
no rows selected
--//说明恢复的数据没有任何问题。

4.还原现场:
SYS@book01p> update (select * from obj$ where obj#=124986 and DATAOBJ#=124986) set DATAOBJ#=124988;
1 row updated.

SYS@book01p> commit ;
Commit complete.

SYS@book01p> alter system flush shared_pool;
System altered.

SYS@book01p> select count(*) from scott.t;
  COUNT(*)
----------
        63
--//顺便提一下数据块171到175已经标识为段号124988,已经做了格式化,虽然插入仅仅在块174,175,但是其他数据块里面的数据已经
--//无法看到。
SCOTT@book01p> @ bbvi 12 171
BVI_COMMAND
------------------------------------------------------------------------------------------------------------------------
bvi -b 1400832 -s 8192 /u01/oradata/BOOK/book01p/users01.dbf
xxd -c16 -g 2 -s 1400832 -l 8192 /u01/oradata/BOOK/book01p/users01.dbf
dd if=/u01/oradata/BOOK/book01p/users01.dbf bs=8192 skip=171 count=1 of=12_171.dd conv=notrunc 2>/dev/null
od -j 1400832 -N 8192 -t x1 -v /u01/oradata/BOOK/book01p/users01.dbf
hexdump -s 1400832 -n 8192 -C -v /u01/oradata/BOOK/book01p/users01.dbf
alter system dump datafile '/u01/oradata/BOOK/book01p/users01.dbf' block 171;

alter session set events 'immediate trace name set_tsn_p1 level 6';
alter session set events 'immediate trace name buffer level 50331819';

9 rows selected.

$ xxd -a -c16 -g 2 -s 1400832 -l 8192 /u01/oradata/BOOK/book01p/users01.dbf
0156000: 06a2 0000 ab00 0003 f5f8 2e02 0000 0104  ................
0156010: 7139 0000 0100 0000 3ce8 0100 f5f8 2e02  q9......<.......
0156020: 0080 0000 0200 3200 a800 0003 0000 0000  ......2.........
0156030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
0156060: 0000 0000 0000 0000 ffff 0e00 981f 8a1f  ................
0156070: 8a1f 0000 0000 0000 0000 0000 0000 0000  ................
0156080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0156090: a900 0003 1000 0000 0000 0000 0000 0000  ................
01560a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
01560b0: 0000 0000 0000 0000 0000 0000 a002 0000  ................
01560c0: 3ae8 0100 55f3 2e02 0000 0000 0003 0003  :...U...........
01560d0: 4000 0000 0000 0000 0000 0000 0000 0000  @...............
01560e0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
0156180: 0000 0000 0000 0000 0000 0000 1111 1111  ................
0156190: 1111 1111 1111 1111 1111 1111 1111 1111  ................
01561a0: 1111 1111 1111 1111 1111 1111 0000 0000  ................
01561b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
0156390: 0000 0000 d3f3 2e02 0000 0000 0000 0000  ................
01563a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
01567a0: 0000 0000 0000 0000 0000 0000 4141 4141  ............AAAA
01567b0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
01567c0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
01567d0: 4141 4141 4141 4141 4141 4141 4141 4141  AAAAAAAAAAAAAAAA
01567e0: 4141 4141 4141 4141 4141 4141 0000 0000  AAAAAAAAAAAA....
01567f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
*
0157ff0: 0000 0000 0000 0000 0000 0000 0106 f5f8  ................
--//原始的数据信息完成清除了。通过这样的方式已经无法恢复,即使我修改段号等于124986。

5.顺便贴上原始的恢复脚本:
--//链接来自:http://www.minniebaby.tech/2021/10/25/truncate-table%e6%81%a2%e5%a4%8d-%e8%84%9a%e6%9c%ac/,做一些修改。
--//顺便贴上原始的恢复脚本,使用它扫描范围有点大,实际的环境非常慢,不如我先确定扫描那些块要快一些。
--//行号取到999有点多,可以根据需要修改。另外我加入mod(nrows,10000)=0提交,我扫描时打开另外窗口发现实际上记录增加并不是按
--//照10000量增加,也许pl/sql有点不同,有点多余。

SCOTT@book01p> select count(*) from bak_t ;
  COUNT(*)
----------
     46647

SCOTT@book01p> select count(*) from bak_t ;
  COUNT(*)
----------
     47168

SCOTT@book01p> select count(*) from bak_t ;
  COUNT(*)
----------
     47771

$ cat trunc.txt
declare
   v_fno number;
   v_s_bno number;
   v_e_bno number;
   v_rowid rowid;
   v_owner varchar2(100):='&&1';
   v_table varchar2(100):='&&2';
   v_o_owner varchar2(100):='&&3';
   v_o_table varchar2(100):='&&4';
   v_dataobj number;
   v_sql varchar2(4000);
   v_tablespace varchar2(100);
   nrows number;
begin
   nrows:=0;
   select data_object_id into v_dataobj from dba_objects where owner=v_owner and object_name=v_table;
   select tablespace_name into v_tablespace from dba_tables where owner=v_owner and table_name=v_table;
   for i in (select relative_fno,block_id,blocks
               from dba_extents
              where owner=v_owner and segment_name=v_table and extent_id=0
             union all
             select relative_fno,block_id,blocks
               from dba_free_space
              where tablespace_name=v_tablespace
             union all
             select relative_fno,block_id,blocks from (
               select relative_fno,block_id,blocks,row_number()over(partition by owner,segment_name,partition_name order by extent_id desc) rn
               from dba_extents
              where tablespace_name=v_tablespace and extent_id>0) where rn=1) loop
   v_fno:=i.relative_fno;
   v_s_bno:=i.block_id;
   v_e_bno:=i.block_id+i.blocks-1;
       for j in v_s_bno .. v_e_bno loop
        begin
         for x in 0 .. 999 loop
           v_rowid:=dbms_rowid.rowid_create(1,v_dataobj,v_fno,j,x);
           v_sql:='insert into '||v_o_owner||'.'||v_o_table||' select * from '||v_owner||'.'||v_table||' where rowid=:1';
                   execute immediate v_sql using v_rowid;
           if sql%rowcount = 1 then nrows:=nrows+1; end if;
               if (mod(nrows,10000)=0) then commit; end if;
         end loop;
        exception
          when others then
            null;
        end;
        commit;
       end loop;
    end loop;
end;
/


$ cat versions.sql
-- Copyright 2023 lfree. All rights reserved.
-- Licensed under the Apache License, Version 2.0. See LICENSE.txt for terms and conditions.
------------------------------------------------------------------------------------------------------------
--
-- File name:   versions.sql
-- Purpose:     display table record of dml history
--
-- Author:      lfree
--
-- Usage:
--     @ versions <table_name> <col1,..,colN> <scn|s|t|time|timestamp> <scn1|time1> <scn2|time2> <filter>
--
-------------------------------------------------------------------------------------------------------------
set term off

column 2 new_value 2
column 3 new_value 3
column 4 new_value 4
column 5 new_value 5
column 6 new_value 6
column cols new_value v_cols
column st   new_value v_st
column s1   new_value v_s1
column s2   new_value v_s2
column filter  new_value v_filter

select null "2" , null "3" , null "4" , null "5" ,null "6" from dual where 1=2;

select decode('&2',NULL,'&1..*','*','&1..*','&2') cols
       ,decode(lower('&3'),null,'scn','s','scn','scn','scn','t','timestamp','time','timestamp','timestamp','timestamp','scn') st
       ,decode('&4',null,'minvalue','&4') s1
       ,decode('&5',null,'maxvalue','&5') s2
from dual ;

select decode('&6',null,'1=1','&6') "6" from dual ;

set term on

--set echo on verify on

SELECT versions_starttime
        ,versions_endtime
        ,versions_startscn
        ,versions_endscn
        ,versions_xid
        ,versions_operation
        ,&v_cols
       FROM &1 VERSIONS BETWEEN &v_st &v_s1 AND &v_s2
      where ( &6 )
      ORDER BY VERSIONS_STARTSCN nulls first;
      -- FROM &1 VERSIONS BETWEEN SCN MINVALUE AND MAXVALUE
      -- FROM &1 VERSIONS BETWEEN TIMESTAMP MINVALUE AND MAXVALUE
      -- FROM &1 VERSIONS BETWEEN SCN &3 AND &4
      -- FROM &1 VERSIONS BETWEEN TIMESTAMP &&3 and &4

set echo off verify off

作者:lfree原文地址:https://www.cnblogs.com/lfree/p/18859471

%s 个评论

要回复文章请先登录注册