[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
--//别人数据库不小心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