一、问题背景#
在数据库恢复场景中,经常会遇到两个问题:
- 表被
TRUNCATE以后,数据文件里的数据是不是还能找回来? - 表被
DROP以后,数据文件里的数据是不是还在?
从业务视角看,TRUNCATE TABLE 和 DROP TABLE 都会让表数据不可见。但从数据文件物理层看,两者的处理方式并不完全一样。
本文基于一次达梦 DM8 实验,从字典变化、数据文件残留、10002 trace 分析几个角度,验证 TRUNCATE 和 DROP 后数据是否被清理,以及是否具备恢复可能性。
测试环境为达梦 DM8,页大小为:PAGE_SIZE = 8192。测试对象主要包括:USERS1.T_TEST。测试表空间:TBS_TEST
二、实验表准备#
2.1 创建 TBS_TEST 表空间#
使用 32MB数据文件:
CREATE TABLESPACE TBS_TEST
DATAFILE 'TBS_TEST01.DBF'
SIZE 32;确认数据文件路径:
SELECT TABLESPACE_NAME,
FILE_NAME,
BYTES / 1024 / 1024 AS MB
FROM DBA_DATA_FILES
WHERE TABLESPACE_NAME = 'TBS_TEST';结果如下:
SQL> CREATE TABLESPACE TBS_TEST
DATAFILE 'TBS_TEST01.DBF'
SIZE 32;2 3
executed successfully
used time: 13.918(ms). Execute id is 2101.
SQL> SELECT TABLESPACE_NAME,
FILE_NAME,
BYTES / 1024 / 1024 AS MB
FROM DBA_DATA_FILES
WHERE TABLESPACE_NAME = 'TBS_TEST';2 3 4 5
LINEID TABLESPACE_NAME FILE_NAME MB
---------- --------------- ----------------------------- --------------------
1 TBS_TEST /dmdata/DAMENG/TBS_TEST01.DBF 32
used time: 22.791(ms). Execute id is 2102.
SQL>2.2 创建 USERS1 用户#
CREATE USER USERS1 IDENTIFIED BY "Users1@123"
DEFAULT TABLESPACE TBS_TEST;授权:
GRANT RESOURCE TO USERS1;
GRANT PUBLIC TO USERS1;
GRANT SELECT ANY DICTIONARY TO USERS1;结果如下:
SQL> CREATE USER USERS1 IDENTIFIED BY "Users1@123"
DEFAULT TABLESPACE TBS_TEST;2
executed successfully
used time: 4.563(ms). Execute id is 2103.
SQL> GRANT RESOURCE TO USERS1;
GRANT PUBLIC TO USERS1;
GRANT SELECT ANY DICTIONARY TO USERS1;executed successfully
used time: 3.442(ms). Execute id is 2104.
SQL> executed successfully
used time: 1.184(ms). Execute id is 2105.
SQL>
executed successfully
used time: 1.479(ms). Execute id is 2106.
SQL>2.3 创建 T_TEST 并插入测试数据#
切换 USERS1:
CONN USERS1/"Users1@123"@127.0.0.1:5236创建测试表:
CREATE TABLE T_TEST
(
ID INT,
MARKER VARCHAR(200),
PAYLOAD VARCHAR(1000),
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT PK_T_TEST PRIMARY KEY (ID)
)
TABLESPACE TBS_TEST;插入 10000 行测试数据:
INSERT INTO T_TEST(ID, MARKER, PAYLOAD)
SELECT LEVEL,
'DM_TRUNCATE_TEST_20260706_' || TO_CHAR(LEVEL),
'BEFORE_TRUNCATE_20260706_' || TO_CHAR(LEVEL) || '_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
FROM DUAL
CONNECT BY LEVEL <= 10000;
COMMIT;
SELECT CHECKPOINT(100);检查:
SELECT COUNT(*) FROM T_TEST;
SELECT *
FROM T_TEST
WHERE ID <= 5
ORDER BY ID;结果如下:
SQL> CONN USERS1/"Users1@123"@127.0.0.1:5236
Server[127.0.0.1:5236]:mode is normal, state is open
login used time : 2.580(ms)
SQL> CREATE TABLE T_TEST
(
ID INT,
MARKER VARCHAR(200),
PAYLOAD VARCHAR(1000),
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT PK_T_TEST PRIMARY KEY (ID)
)
TABLESPACE TBS_TEST;2 3 4 5 6 7 8 9
executed successfully
used time: 4.815(ms). Execute id is 2401.
SQL> INSERT INTO T_TEST(ID, MARKER, PAYLOAD)
SELECT LEVEL,
'DM_TRUNCATE_TEST_20260706_' || TO_CHAR(LEVEL),
'BEFORE_TRUNCATE_20260706_' || TO_CHAR(LEVEL) || '_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
FROM DUAL
CONNECT BY LEVEL <= 10000;
COMMIT;
SELECT CHECKPOINT(100);2 3 4 5 6 affect rows 10000
used time: 33.157(ms). Execute id is 2402.
SQL> SQL> executed successfully
used time: 6.597(ms). Execute id is 2403.
SQL>
LINEID CHECKPOINT(100)
---------- ---------------
1 0
used time: 24.215(ms). Execute id is 2404.
SQL> SELECT COUNT(*) FROM T_TEST;
LINEID COUNT(*)
---------- --------------------
1 10000
used time: 2.851(ms). Execute id is 2405.
SQL> SELECT *
FROM T_TEST
WHERE ID <= 5
ORDER BY ID;2 3 4
LINEID ID MARKER PAYLOAD CREATE_TIME
---------- ----------- --------------------------- ----------------------------------------------------------------------------- --------------------------
1 1 DM_TRUNCATE_TEST_20260706_1 BEFORE_TRUNCATE_20260706_1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2026-07-06 14:00:08.584210
2 2 DM_TRUNCATE_TEST_20260706_2 BEFORE_TRUNCATE_20260706_2_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2026-07-06 14:00:08.584210
3 3 DM_TRUNCATE_TEST_20260706_3 BEFORE_TRUNCATE_20260706_3_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2026-07-06 14:00:08.584210
4 4 DM_TRUNCATE_TEST_20260706_4 BEFORE_TRUNCATE_20260706_4_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2026-07-06 14:00:08.584210
5 5 DM_TRUNCATE_TEST_20260706_5 BEFORE_TRUNCATE_20260706_5_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2026-07-06 14:00:08.584210
used time: 2.600(ms). Execute id is 2406.
SQL>2.4 TRUNCATE 前记录 T_TEST 的物理位置#
查询段信息:
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';查询所有 extent 信息:
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;查询页大小和数据文件:
SELECT PARA_NAME, PARA_VALUE
FROM V$DM_INI
WHERE PARA_NAME IN ('PAGE_SIZE', 'GLOBAL_PAGE_SIZE');
SELECT TABLESPACE_NAME,
FILE_NAME,
BYTES / 1024 / 1024 AS MB
FROM DBA_DATA_FILES
WHERE TABLESPACE_NAME = 'TBS_TEST';结果如下:
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';2 3 4 5 6 7 8 9 10
LINEID OWNER SEGMENT_NAME TABLESPACE_NAME HEADER_FILE HEADER_BLOCK BLOCKS EXTENTS
---------- ------ ------------ --------------- ----------- ------------ -------------------- --------------------
1 USERS1 T_TEST TBS_TEST 0 16 208 13
used time: 259.236(ms). Execute id is 2407.
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;2 3 4 5 6 7 8 9 10 11
LINEID OWNER SEGMENT_NAME TABLESPACE_NAME FILE_ID EXTENT_ID BLOCK_ID BLOCKS
---------- ------ ------------ --------------- ----------- ----------- ----------- -----------
1 USERS1 T_TEST TBS_TEST 0 1 16 2
used time: 150.731(ms). Execute id is 2408.
SQL>
SQL> SELECT PARA_NAME, PARA_VALUE
FROM V$DM_INI
WHERE PARA_NAME IN ('PAGE_SIZE', 'GLOBAL_PAGE_SIZE');2 3
LINEID PARA_NAME PARA_VALUE
---------- ---------------- ----------
1 GLOBAL_PAGE_SIZE 8192
used time: 6.632(ms). Execute id is 2410.
SQL> SELECT TABLESPACE_NAME,
FILE_NAME,
BYTES / 1024 / 1024 AS MB
FROM DBA_DATA_FILES
WHERE TABLESPACE_NAME = 'TBS_TEST';2 3 4 5
LINEID TABLESPACE_NAME FILE_NAME MB
---------- --------------- ----------------------------- --------------------
1 TBS_TEST /dmdata/DAMENG/TBS_TEST01.DBF 32
used time: 8.243(ms). Execute id is 2411.
SQL>从 DBA_SEGMENTS 可以看到,TRUNCATE 前 USERS1.T_TEST 的段头位于 file 0 block 16,该段记录的空间规模为 208 个块、13 个 extent。页大小为 8192 字节,因此 208 个块对应的理论空间约为:208 × 8192 = 1703936 bytes,约 1.625 MB
这与插入 10000 行测试数据后的表段增长情况是匹配的。
但这里需要注意一个现象:DBA_EXTENTS 只返回了一行,显示 block_id=16, blocks=2。这个结果与 DBA_SEGMENTS 中的 BLOCKS=208, EXTENTS=13 并不完全一致。因此,在本实验中不能简单认为 T_TEST 的全部数据只位于 block 16 开始的 2 个块中。对于 10000 行测试数据来说,2 个 8KB 数据块显然不足以容纳全部行内容。
2.5 TRUNCATE 前用 strings 验证数据在 DBF 文件中#
TRUNCATE 前按 extent 精确截取验证
SELECT 'dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip='
|| HEADER_BLOCK
|| ' count='
|| BLOCKS
|| ' 2>/dev/null | strings -a | grep "DM_TRUNCATE_TEST_20260706_" | head'
AS SCAN_CMD
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY HEADER_FILE, HEADER_BLOCK;对应命令就是:
dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip=16 count=208 2>/dev/null | strings -a | grep "DM_TRUNCATE_TEST_20260706_" | head结果如下:
SQL> SELECT 'dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip='
|| HEADER_BLOCK
|| ' count='
|| BLOCKS
|| ' 2>/dev/null | strings -a | grep "DM_TRUNCATE_TEST_20260706_" | head'
AS SCAN_CMD
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
2 3 4 5 6 7 8 9 10 ORDER BY HEADER_FILE, HEADER_BLOCK;
LINEID SCAN_CMD
---------- ---------------------------------------------------------------------------------------------------------------------------------
1 dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip=16 count=208 2>/dev/null | strings -a | grep "DM_TRUNCATE_TEST_20260706_" | head
used time: 143.555(ms). Execute id is 2412.
SQL> exit
[dmdba@dameng-srv ~]$ dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip=16 count=208 2>/dev/null | strings -a | grep "DM_TRUNCATE_TEST_20260706_" | head
DM_TRUNCATE_TEST_20260706_21
DM_TRUNCATE_TEST_20260706_22
DM_TRUNCATE_TEST_20260706_23
DM_TRUNCATE_TEST_20260706_24
DM_TRUNCATE_TEST_20260706_25
DM_TRUNCATE_TEST_20260706_26
DM_TRUNCATE_TEST_20260706_27
DM_TRUNCATE_TEST_20260706_28
DM_TRUNCATE_TEST_20260706_29
DM_TRUNCATE_TEST_20260706_30
[dmdba@dameng-srv ~]$三、TRUNCATE 实验与结果分析#
3.1 开启 10002 trace 执行 TRUNCATE#
为了更好的了解 TRUNCATE 操作,开启 10002 trace
CONN USERS1/"Users1@123"@127.0.0.1:5236
--开启 10002 trace
ALTER SESSION SET EVENTS '10002 trace name context forever,level 1';
--执行 truncate
TRUNCATE TABLE T_TEST;
--关闭 10002 trace
ALTER SESSION SET EVENTS '10002 trace name context off';
--记录当前 sessid
SELECT SESSID;
--查询 trace 文件路径
SHOW PARAMETER TRACE_PATH;结果如下:
SQL> CONN USERS1/"Users1@123"@127.0.0.1:5236
Server[127.0.0.1:5236]:mode is normal, state is open
login used time : 2.628(ms)
SQL> ALTER SESSION SET EVENTS '10002 trace name context forever,level 1';
executed successfully
used time: 2.777(ms). Execute id is 2801.
SQL> TRUNCATE TABLE T_TEST;
executed successfully
used time: 6.305(ms). Execute id is 2802.
SQL> ALTER SESSION SET EVENTS '10002 trace name context off';
executed successfully
used time: 3.203(ms). Execute id is 2803.
SQL> SELECT SESSID;
LINEID SESSID
---------- --------------------
1 128256296
used time: 2.347(ms). Execute id is 2804.
SQL> SHOW PARAMETER TRACE_PATH;
LINEID para_name para_value
---------- ---------- --------------------
1 TRACE_PATH /dmdata/DAMENG/trace
SQL>3.2 TRUNCATE 后验证 SQL 层面数据#
SELECT COUNT(*) FROM T_TEST;3.3 TRUNCATE 后查看段信息和 extent 信息#
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;3.4 TRUNCATE 后用 strings 验证数据文件残留#
strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "DM_TRUNCATE_TEST_20260706_" | head结果如下:
SQL> SELECT COUNT(*) FROM T_TEST;
LINEID COUNT(*)
---------- --------------------
1 0
used time: 0.788(ms). Execute id is 2806.
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';2 3 4 5 6 7 8 9 10
LINEID OWNER SEGMENT_NAME TABLESPACE_NAME HEADER_FILE HEADER_BLOCK BLOCKS EXTENTS
---------- ------ ------------ --------------- ----------- ------------ -------------------- --------------------
1 USERS1 T_TEST TBS_TEST 0 16 16 1
used time: 45.680(ms). Execute id is 2807.
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;2 3 4 5 6 7 8 9 10 11
LINEID OWNER SEGMENT_NAME TABLESPACE_NAME FILE_ID EXTENT_ID BLOCK_ID BLOCKS
---------- ------ ------------ --------------- ----------- ----------- ----------- -----------
1 USERS1 T_TEST TBS_TEST 0 1 16 2
used time: 32.980(ms). Execute id is 2808.
SQL> exit
[dmdba@dameng-srv ~]$ strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "DM_TRUNCATE_TEST_20260706_" | head
DM_TRUNCATE_TEST_20260706_1
DM_TRUNCATE_TEST_20260706_2
DM_TRUNCATE_TEST_20260706_3
DM_TRUNCATE_TEST_20260706_4
DM_TRUNCATE_TEST_20260706_5
DM_TRUNCATE_TEST_20260706_6
DM_TRUNCATE_TEST_20260706_7
DM_TRUNCATE_TEST_20260706_8
DM_TRUNCATE_TEST_20260706_9
DM_TRUNCATE_TEST_20260706_10
[dmdba@dameng-srv ~]$ 3.5 TRUNCATE 前后段信息变化说明#
本次实验中,TRUNCATE 前 DBA_SEGMENTS 查询结果如下:
HEADER_FILE = 0
HEADER_BLOCK = 16
BLOCKS = 208
EXTENTS = 13TRUNCATE 后 DBA_SEGMENTS 查询结果如下:
HEADER_FILE = 0
HEADER_BLOCK = 16
BLOCKS = 16
EXTENTS = 1从这个变化可以看出:
HEADER_FILE没有变化,仍然是0,说明T_TEST的段头仍然位于同一个数据文件中,也就是/dmdata/DAMENG/TBS_TEST01.DBF。HEADER_BLOCK没有变化,仍然是16,说明TRUNCATE TABLE T_TEST后,表对象本身没有被删除,段头位置仍然保留。也就是说,TRUNCATE不是删除表对象,而是重置表段中的数据和空间状态。BLOCKS从208变成16,说明 TRUNCATE 前由于插入了 10000 行数据,表段已经扩展到较大的空间规模;TRUNCATE 后,表段空间被释放或重置,只保留了较小的初始空间。EXTENTS从13变成1,说明 TRUNCATE 前该表已经分配了多个 extent;TRUNCATE 后,原来用于存放数据的多个 extent 不再属于当前表段,只保留了一个基础 extent。
因此,TRUNCATE 的效果不是逐行删除,而是通过段级操作重置表空间使用状态。SQL 层面看,表仍然存在,但数据行变为 0;空间层面看,原来分配给该表的大量块和 extent 被释放或重新整理。
需要特别说明的是:BLOCKS 和 EXTENTS 变小,只能说明这些块不再属于当前 T_TEST 段,不能直接证明这些块中的历史数据已经被物理清零。原来存放数据的块可能已经回到表空间空闲空间中,在被新对象或新数据覆盖之前,数据文件中仍可能残留旧行内容。
这个结果说明:虽然 T_TEST 在 SQL 层面已经是空表,但 TBS_TEST01.DBF 数据文件中仍然能够看到 TRUNCATE 前插入的测试标记字符串。
3.6 查看 TRUNCATE 的 10002 trace#
找最新 trace:
cd /dmdata/DAMENG/trace
ls -ltrh
ls -lh *你的SESSID*.trc
cat *你的SESSID*.trc结果如下:
[dmdba@dameng-srv ~]$ cd /dmdata/DAMENG/trace
[dmdba@dameng-srv trace]$ ls -ltrh
total 8.0K
-rw-r--r-- 1 dmdba dinstall 4.9K Jul 6 14:21 DMSERVER_0706_1421_128256296.trc
[dmdba@dameng-srv trace]$ cat DMSERVER_0706_1421_128256296.trc
DM Database Server x64 V8[03134284336-20250117-257733-20132], Jan 22 2025 09:16:16 built.
*** 2026-07-06 14:21:20.741000000
*** Start trace 10002 event [SESSION ID:128256296] ***
[DDL SQL]:
TRUNCATE TABLE T_TEST;
[EQUIVALENT DML SQL]:
{
{
INT V_DC_FLAG_LEVEL;
INT ISO_LEVEL = -1;
try{
V_DC_FLAG_LEVEL = SF_RESET_DDLCOMMIT_FLAG();
{
PROCEDURE SP_CHECK_IS_DBMS_AQADM_TABLE()
AS
pkg_exist BIGINT;
tab_exist BIGINT;
BEGIN
SELECT COUNT(*) INTO pkg_exist FROM SYS.SYSOBJECTS WHERE NAME = 'DBMS_AQADM' AND SUBTYPE$ = 'PKG' AND VALID = 'Y' AND SCHID = SF_GET_SCHEMA_ID_BY_NAME('SYS');
IF pkg_exist > 0 THEN
EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM SYS.AQ$_QUEUE_TABLES WHERE OBJNO=1066' INTO tab_exist;
IF tab_exist > 0 THEN
RAISE EXCEPTION(-24005, EXCEPTION_MOUT(-24005));
END IF;
END IF;
END;
SP_CHECK_IS_DBMS_AQADM_TABLE();
}
STRING V_STR;{
SF_LOCK_DICT(1056964608, 3, 0);
PROCEDURE PROC_LOCK_AND_CHECK_TS(TAB_ID VARCHAR, IS_HUGE INT, LOCK_TYPE INT, TAB_LEVEL INT) AS
DECLARE
TYPE OBJ_ID_T IS TABLE OF INT;
TID_ARR OBJ_ID_T;
IND_ARR OBJ_ID_T;
TS_ARR OBJ_ID_T;
HTS_ARR OBJ_ID_T;
BEGIN
IF (TAB_LEVEL = 1) THEN
SELECT ID BULK COLLECT INTO IND_ARR FROM SYS.SYSOBJECTS WHERE PID = TAB_ID AND SUBTYPE$='INDEX';
ELSE
SELECT /*+CNNTB_OPT_FLAG(0)*/ PART_TABLE_ID BULK COLLECT INTO TID_ARR FROM SYS.SYSHPARTTABLEINFO WHERE PART_TABLE_ID > 1 AND LEVEL = TAB_LEVEL - 1 CONNECT BY BASE_TABLE_ID = PRIOR PART_TABLE_ID START WITH BASE_TABLE_ID = TAB_ID;
--获取索引ID
SELECT ID BULK COLLECT INTO IND_ARR FROM SYS.SYSOBJECTS WHERE PID IN (SELECT * FROM TABLE(TID_ARR)) AND SUBTYPE$='INDEX';
END IF;
--表空间加锁
IF (IS_HUGE) THEN
SELECT DISTINCT GROUPID BULK COLLECT INTO TS_ARR FROM SYS.SYSINDEXES WHERE ID IN (SELECT * FROM TABLE(IND_ARR)) AND (XTYPE & 0X1)!=0;
SELECT DISTINCT GROUPID BULK COLLECT INTO HTS_ARR FROM SYS.SYSINDEXES WHERE ID IN (SELECT * FROM TABLE(IND_ARR)) AND (XTYPE & 0X1)=0;
FOR I IN 1..TS_ARR.COUNT LOOP
SF_LOCK_TABLESPACE(TS_ARR(I), LOCK_TYPE, 1);
SP_CHECK_TS_OFFLINE(TS_ARR(I));
END LOOP;
FOR I IN 1..HTS_ARR.COUNT LOOP
--huge表空间改造,上锁不再置上HTS的掩码LOCK_HUGE_TS_MASK
SF_LOCK_TABLESPACE(HTS_ARR(I), LOCK_TYPE, 0);
SP_CHECK_TS_OFFLINE(HTS_ARR(I));
END LOOP;
ELSE
SELECT DISTINCT GROUPID BULK COLLECT INTO TS_ARR FROM SYS.SYSINDEXES WHERE ID IN (SELECT * FROM TABLE(IND_ARR));
FOR I IN 1..TS_ARR.COUNT LOOP
SF_LOCK_TABLESPACE(TS_ARR(I), LOCK_TYPE, 1);
SP_CHECK_TS_OFFLINE(TS_ARR(I));
END LOOP;
END IF;
END;
PROC_LOCK_AND_CHECK_TS(1066, 0, 4, 1);
SF_LOCK_DICT(150995949, 4, 0);
SF_LOCK_DICT_EX2(1066, 2);
SF_TABLE_CHECK(1066, 4);
DELETE SYS.SYSOBJINFOS WHERE ID = 1066 AND TYPE$ = 'TRUNCTIME' AND int_value=0;
INSERT INTO SYS.SYSOBJINFOS(ID, TYPE$, INT_VALUE, STR_VALUE) VALUES(1066, 'TRUNCTIME', 0, TO_CHAR(SYSDATE(),'YYYY-MM-DD HH24:MI:SS'));
SF_TRUNC_TABLE(1066, 0);
SP_DCT_TAB_DDL_REG(1066, 1, 1);
SF_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 0);
SF_LOCK_APPLY_ID();
COMMIT;
}
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
}
catch (exception ex){
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
SF_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 0);
SF_SET_GEN_INS_UREC_FLAG(1);
if (ISO_LEVEL != -1)
SF_SET_TRX_ISO_LEVEL(ISO_LEVEL);
throw;
}
}
}
-------------------------- END --------------------------
*** 2026-07-06 14:21:26.390000000
*** Start trace 10002 event [SESSION ID:128256296] ***
[DDL SQL]:
ALTER SESSION SET EVENTS '10002 trace name context off';
[EQUIVALENT DML SQL]:
{
{
INT V_DC_FLAG_LEVEL;
INT ISO_LEVEL = -1;
try{
V_DC_FLAG_LEVEL = SF_RESET_DDLCOMMIT_FLAG();
SP_SESS_SET_TRACE_EVENT(0, 10002, 1, 1, 0);
COMMIT;
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
}
catch (exception ex){
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
SF_SET_GEN_INS_UREC_FLAG(1);
if (ISO_LEVEL != -1)
SF_SET_TRX_ISO_LEVEL(ISO_LEVEL);
throw;
}
}
}
-------------------------- END --------------------------
[dmdba@dameng-srv trace]$ 3.7 TRUNCATE 的 trace 内容分析#
在本次实验中,TRUNCATE TABLE T_TEST; 的 trace 中可以看到类似核心内容:
DELETE SYS.SYSOBJINFOS WHERE ID = 1066 AND TYPE$ = 'TRUNCTIME' AND int_value=0;
INSERT INTO SYS.SYSOBJINFOS(ID, TYPE$, INT_VALUE, STR_VALUE) VALUES(1066, 'TRUNCTIME', 0, TO_CHAR(SYSDATE(),'YYYY-MM-DD HH24:MI:SS'));
SF_TRUNC_TABLE(1066, 0);
SP_DCT_TAB_DDL_REG(1066, 1, 1);
SF_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 0);
COMMIT;这些信息说明 TRUNCATE 并不是普通的逐行删除,而是一次 DDL 级别的段重置操作:加字典锁/表空间锁 → 检查对象状态 → 记录 TRUNCTIME → 调用内部 SF_TRUNC_TABLE 重置或释放表段空间 → 刷新/移除字典缓存 → commit。但是它不等于对数据文件中的旧行内容做物理清零。只要旧数据块没有被后续新对象或新数据覆盖,仍然可能通过离线扫描方式找到原始数据残留。
3.8 TRUNCATE 实验结论#
通过本次 TRUNCATE 实验,可以得到以下结论:
- 从 SQL 层面看,
TRUNCATE TABLE T_TEST执行后,SELECT COUNT(*) FROM T_TEST返回0,说明表数据已经对数据库用户不可见。 - 从段空间变化看,TRUNCATE 前
DBA_SEGMENTS显示T_TEST占用208个块、13个 extent;TRUNCATE 后变为16个块、1个 extent,说明 TRUNCATE 会重置或释放表段空间。 HEADER_FILE和HEADER_BLOCK在 TRUNCATE 前后保持不变,说明T_TEST表对象本身没有被删除,段头仍然保留。TRUNCATE 的行为不是删除表对象,而是对表段进行重置。BLOCKS和EXTENTS变小,只能说明原来属于该表的大量数据块不再挂在当前T_TEST段下,不能证明这些块中的历史数据已经被物理清零。- 对整个
/dmdata/DAMENG/TBS_TEST01.DBF数据文件执行strings扫描后,仍然可以看到DM_TRUNCATE_TEST_20260706_测试标记,说明 TRUNCATE 后旧行内容仍然残留在数据文件中。 - 10002 trace 中可以看到
TRUNCTIME、SF_TRUNC_TABLE、SP_DCT_TAB_DDL_REG、SF_REMOVE_DICT_OBJ、COMMIT等内部操作,说明 TRUNCATE 是 DDL 级别的段重置操作,不是普通逐行 DELETE。 - trace 中没有体现逐行删除用户数据,也没有体现对数据文件中旧行内容进行物理清零。
因此,本次实验说明:
TRUNCATE TABLE 会让表数据在 SQL 层面立即不可见,并重置或释放表段空间;但它并不等于对数据文件中的历史行内容进行物理清零。只要原数据块没有被后续写入覆盖,仍然可以通过离线扫描方式在 DBF 文件中发现旧数据残留,理论上也存在恢复可能。
四、DROP 实验与结果分析#
前面的 TRUNCATE 实验已经验证:TRUNCATE TABLE 会让表数据在 SQL 层面不可见,并且会重置或释放表段空间;但通过扫描整个数据文件,仍然可以找到 TRUNCATE 前插入的测试标记。这说明 TRUNCATE 不等于立即物理清零数据文件。
下面继续验证 DROP TABLE 的情况。
本节实验继续使用同一个测试环境:
测试用户 :USERS1
测试表 :T_TEST
测试表空间 :TBS_TEST
数据文件 :/dmdata/DAMENG/TBS_TEST01.DBF
页大小 :8192由于前面已经执行过 TRUNCATE TABLE T_TEST,当前 T_TEST 表仍然存在,但表中已经没有数据。因此,DROP 实验前先重新插入一批带有明显标记的数据,便于后续从数据文件中搜索。
4.1 DROP 前重新插入测试数据#
切换到 USERS1 用户:
CONN USERS1/"Users1@123"@127.0.0.1:5236插入 DROP 专用测试数据:
INSERT INTO T_TEST(ID, MARKER, PAYLOAD)
SELECT LEVEL,
'DM_DROP_TEST_20260706_' || TO_CHAR(LEVEL),
'BEFORE_DROP_20260706_' || TO_CHAR(LEVEL) || '_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'
FROM DUAL
CONNECT BY LEVEL <= 10000;
COMMIT;执行 checkpoint,尽量将数据刷入数据文件:
SELECT CHECKPOINT(100);检查数据:
SELECT COUNT(1) FROM T_TEST;
SELECT *
FROM T_TEST
WHERE ID <= 5
ORDER BY ID;结果如下:
SQL> CONN USERS1/"Users1@123"@127.0.0.1:5236
Server[127.0.0.1:5236]:mode is normal, state is open
login used time : 2.585(ms)
SQL> INSERT INTO T_TEST(ID, MARKER, PAYLOAD)
SELECT LEVEL,
'DM_DROP_TEST_20260706_' || TO_CHAR(LEVEL),
'BEFORE_DROP_20260706_' || TO_CHAR(LEVEL) || '_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'
2 3 4 5 FROM DUAL
CONNECT BY LEVEL <= 10000;
6 COMMIT;affect rows 10000
used time: 15.423(ms). Execute id is 5201.
SQL> SQL>
executed successfully
used time: 1.035(ms). Execute id is 5202.
SQL> SELECT CHECKPOINT(100);
LINEID CHECKPOINT(100)
---------- ---------------
1 0
used time: 5.592(ms). Execute id is 5203.
SQL> SELECT COUNT(1) FROM T_TEST;
LINEID COUNT(1)
---------- --------------------
1 10000
used time: 0.562(ms). Execute id is 5204.
SQL> SELECT *
FROM T_TEST
WHERE ID <= 5
ORDER BY ID;2 3 4
LINEID ID MARKER PAYLOAD CREATE_TIME
---------- ----------- ----------------------- ------------------------------------------------------------------------- --------------------------
1 1 DM_DROP_TEST_20260706_1 BEFORE_DROP_20260706_1_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 2026-07-06 15:37:27.706050
2 2 DM_DROP_TEST_20260706_2 BEFORE_DROP_20260706_2_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 2026-07-06 15:37:27.706050
3 3 DM_DROP_TEST_20260706_3 BEFORE_DROP_20260706_3_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 2026-07-06 15:37:27.706050
4 4 DM_DROP_TEST_20260706_4 BEFORE_DROP_20260706_4_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 2026-07-06 15:37:27.706050
5 5 DM_DROP_TEST_20260706_5 BEFORE_DROP_20260706_5_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB 2026-07-06 15:37:27.706050
used time: 0.532(ms). Execute id is 5205.
SQL>4.2 DROP 前记录 T_TEST 的段信息和 extent 信息#
执行 DROP 前,必须先记录 T_TEST 当前的段信息和 extent 信息。因为一旦执行 DROP TABLE T_TEST,表对象会从数据字典中删除,后续就不能再通过 DBA_SEGMENTS 或 DBA_EXTENTS 查询到这个表的物理位置。
查询段信息:
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';查询 extent 信息:
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;查询页大小和数据文件路径:
SELECT PARA_NAME, PARA_VALUE
FROM V$DM_INI
WHERE PARA_NAME IN ('PAGE_SIZE', 'GLOBAL_PAGE_SIZE');
SELECT TABLESPACE_NAME,
FILE_NAME,
BYTES / 1024 / 1024 AS MB
FROM DBA_DATA_FILES
WHERE TABLESPACE_NAME = 'TBS_TEST';结果如下:
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';2 3 4 5 6 7 8 9 10
LINEID OWNER SEGMENT_NAME TABLESPACE_NAME HEADER_FILE HEADER_BLOCK BLOCKS EXTENTS
---------- ------ ------------ --------------- ----------- ------------ -------------------- --------------------
1 USERS1 T_TEST TBS_TEST 0 16 192 12
used time: 74.683(ms). Execute id is 5206.
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;2 3 4 5 6 7 8 9 10 11
LINEID OWNER SEGMENT_NAME TABLESPACE_NAME FILE_ID EXTENT_ID BLOCK_ID BLOCKS
---------- ------ ------------ --------------- ----------- ----------- ----------- -----------
1 USERS1 T_TEST TBS_TEST 0 1 16 2
used time: 62.913(ms). Execute id is 5207.
SQL>
SQL> SELECT PARA_NAME, PARA_VALUE
FROM V$DM_INI
WHERE PARA_NAME IN ('PAGE_SIZE', 'GLOBAL_PAGE_SIZE');2 3
LINEID PARA_NAME PARA_VALUE
---------- ---------------- ----------
1 GLOBAL_PAGE_SIZE 8192
used time: 3.999(ms). Execute id is 5209.
SQL> SELECT TABLESPACE_NAME,
FILE_NAME,
BYTES / 1024 / 1024 AS MB
FROM DBA_DATA_FILES
WHERE TABLESPACE_NAME = 'TBS_TEST';2 3 4 5
LINEID TABLESPACE_NAME FILE_NAME MB
---------- --------------- ----------------------------- --------------------
1 TBS_TEST /dmdata/DAMENG/TBS_TEST01.DBF 32
used time: 4.416(ms). Execute id is 5210.
SQL>这里需要重点说明:DROP 前记录的 HEADER_FILE、HEADER_BLOCK、BLOCKS、EXTENTS 以及 DBA_EXTENTS 中的 BLOCK_ID、BLOCKS,是 DROP 后继续分析数据文件残留的重要依据。DROP 后表对象不存在,普通数据字典已经无法再告诉我们原来的表段在哪里。
4.3 DROP 前验证数据已经写入 DBF 文件#
DROP 前,先验证测试数据确实已经写入 /dmdata/DAMENG/TBS_TEST01.DBF 数据文件。
第一种方式:根据 DBA_EXTENTS 生成精确扫描命令。
SELECT 'dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip='
|| BLOCK_ID
|| ' count='
|| BLOCKS
|| ' 2>/dev/null | strings -a | grep "DM_DROP_TEST_20260706_" | head'
AS SCAN_CMD
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;将输出的命令复制到操作系统执行。例如:
dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip=16 count=2 2>/dev/null | strings -a | grep "DM_DROP_TEST_20260706_" | head如果 DBA_EXTENTS 返回的范围较小,而 DBA_SEGMENTS.BLOCKS 显示表段实际记录的块数较大,也可以结合 HEADER_BLOCK 和 BLOCKS 扩大扫描范围。例如:
dd if=/dmdata/DAMENG/TBS_TEST01.DBF bs=8192 skip=16 count=192 2>/dev/null | strings -a | grep "DM_DROP_TEST_20260706_" | head第二种方式:直接扫描整个表空间数据文件。
strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "DM_DROP_TEST_20260706_" | head结果如下:
[dmdba@dameng-srv ~]$ strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "DM_DROP_TEST_20260706_" | head
DM_DROP_TEST_20260706_21
DM_DROP_TEST_20260706_22
DM_DROP_TEST_20260706_23
DM_DROP_TEST_20260706_24
DM_DROP_TEST_20260706_25
DM_DROP_TEST_20260706_26
DM_DROP_TEST_20260706_27
DM_DROP_TEST_20260706_28
DM_DROP_TEST_20260706_29
DM_DROP_TEST_20260706_30
[dmdba@dameng-srv ~]$说明 DROP 前测试数据已经写入数据文件。
这里的意义是:后续如果 DROP 后仍然能够在同一个 DBF 文件中找到这些字符串,就可以证明 DROP 不等于立即物理清零数据文件。
4.4 开启 10002 trace 执行 DROP TABLE#
为了观察 DROP TABLE 在达梦内部涉及的字典操作和段管理动作,继续开启 10002 trace。
CONN USERS1/"Users1@123"@127.0.0.1:5236
--开启 10002 trace
ALTER SESSION SET EVENTS '10002 trace name context forever,level 1';
--执行 DROP
DROP TABLE T_TEST;
--关闭 10002 trace
ALTER SESSION SET EVENTS '10002 trace name context off';
--记录当前 sessid
SELECT SESSID;
--查询 trace 文件路径
SHOW PARAMETER TRACE_PATH;进入 trace 目录:
cd /dmdata/DAMENG/trace
ls -ltrh
ls -lh *你的SESSID*.trc
cat *你的SESSID*.trc结果如下:
SQL> CONN USERS1/"Users1@123"@127.0.0.1:5236
Server[127.0.0.1:5236]:mode is normal, state is open
login used time : 2.492(ms)
SQL> ALTER SESSION SET EVENTS '10002 trace name context forever,level 1';
executed successfully
used time: 2.449(ms). Execute id is 5601.
SQL> DROP TABLE T_TEST;
executed successfully
used time: 18.135(ms). Execute id is 5602.
SQL> ALTER SESSION SET EVENTS '10002 trace name context off';
executed successfully
used time: 2.722(ms). Execute id is 5603.
SQL> SELECT SESSID;
LINEID SESSID
---------- --------------------
1 140553811567016
used time: 0.520(ms). Execute id is 5604.
SQL> SHOW PARAMETER TRACE_PATH;
LINEID para_name para_value
---------- ---------- --------------------
1 TRACE_PATH /dmdata/DAMENG/trace
SQL> exit
[dmdba@dameng-srv ~]$ cd /dmdata/DAMENG/trace
[dmdba@dameng-srv trace]$ ls -ltrh
total 20K
-rw-r--r-- 1 dmdba dinstall 4.9K Jul 6 14:21 DMSERVER_0706_1421_128256296.trc
-rw-r--r-- 1 dmdba dinstall 11K Jul 6 15:42 DMSERVER_0706_1542_140553811567016.trc
[dmdba@dameng-srv trace]$ cat DMSERVER_0706_1542_140553811567016.trc
DM Database Server x64 V8[03134284336-20250117-257733-20132], Jan 22 2025 09:16:16 built.
*** 2026-07-06 15:42:47.677000000
*** Start trace 10002 event [SESSION ID:140553811567016] ***
[DDL SQL]:
DROP TABLE T_TEST;
[EQUIVALENT DML SQL]:
{
SF_LOCK_APPLY_ID();
{
{
INT V_DC_FLAG_LEVEL;
INT ISO_LEVEL = -1;
try{
V_DC_FLAG_LEVEL = SF_RESET_DDLCOMMIT_FLAG();
{
PROCEDURE SP_CHECK_IS_DBMS_AQADM_TABLE()
AS
pkg_exist BIGINT;
tab_exist BIGINT;
BEGIN
SELECT COUNT(*) INTO pkg_exist FROM SYS.SYSOBJECTS WHERE NAME = 'DBMS_AQADM' AND SUBTYPE$ = 'PKG' AND VALID = 'Y' AND SCHID = SF_GET_SCHEMA_ID_BY_NAME('SYS');
IF pkg_exist > 0 THEN
EXECUTE IMMEDIATE 'SELECT COUNT(*) FROM SYS.AQ$_QUEUE_TABLES WHERE OBJNO=1066' INTO tab_exist;
IF tab_exist > 0 THEN
RAISE EXCEPTION(-24005, EXCEPTION_MOUT(-24005));
END IF;
END IF;
END;
SP_CHECK_IS_DBMS_AQADM_TABLE();
}
{
SP_REPORT_DCT_VERSION(1066, 0, DATETIME '2026-07-06 14:00:02.12');
SF_DELETE_INDEX_MONITOR(150995949, 33555531, 'USERS1', 'INDEX33555531');
SF_DELETE_INDEX_MONITOR_BY_TABID(1066);
SF_LOCK_DICT(1056964608, 3, 0);
SF_LOCK_DICT(150995949, 4, 0);
SP_REPORT_DCT_VERSION(1066, 0, DATETIME '2026-07-06 14:00:02.12');
SF_LOCK_DICT(1066, 2, 0);
SF_REPORT_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 1);
{
INT TMP_CNT;
SELECT COUNT(*) INTO TMP_CNT FROM SYS.SYSOBJECTS WHERE TYPE$='SCHOBJ' AND SCHID = 150994944 AND NAME = 'SYSSTATTABLEIDU';
IF (TMP_CNT == 1) {
EXECUTE IMMEDIATE 'DELETE FROM SYS.SYSSTATTABLEIDU WHERE ID = 1066';
}
SELECT COUNT(*) INTO TMP_CNT FROM SYS.SYSOBJECTS WHERE TYPE$='SCHOBJ' AND SCHID = 150994944 AND NAME = 'SYSSTATPREFS';
IF (TMP_CNT == 1)
EXECUTE IMMEDIATE 'DELETE FROM SYS.SYSSTATPREFS WHERE ID = 1066';
SELECT COUNT(*) INTO TMP_CNT FROM SYS.SYSOBJECTS WHERE TYPE$='SCHOBJ' AND SCHID = 150994944 AND NAME = 'SYSMODIFICATIONS';
IF (TMP_CNT == 1)
EXECUTE IMMEDIATE 'DELETE FROM SYS.SYSMODIFICATIONS WHERE ID = 1066 OR ROOT_ID = 1066 OR BASE_ID = 1066';
SP_MODIFY_AUTO_STAT_OBJ('D', 1066, NULL, NULL);
SP_MODIFY_MON_MODS('D', 1066, -1, -1, NULL);
}
{
PROCEDURE SP_INVALID_DEP_OBJ(OBJID INT)
AS
TYPE REC IS RECORD(SCHID INT, ID INT, TYPE$ VARCHAR(10), SUBTYPE$ VARCHAR(10), NAME VARCHAR(128));
TYPE REC_TAB IS TABLE OF REC;
TYPE INT_ARR_T IS TABLE OF INT;
ID_ARR INT_ARR_T;
OBJ_TAB REC_TAB;
BEGIN
SELECT /*+CNNTB_OPT_FLAG(1)*/ DISTINCT D.ID BULK COLLECT INTO ID_ARR FROM SYS.SYSDEPENDENCIES D CONNECT BY NOCYCLE
PRIOR D.ID = D.REFED_ID AND PRIOR D.TYPE$ NOT IN ('PACKAGE_BODY','CLASS_BODY')
AND D.TYPE$ IN ('VIEW', 'PROCEDURE','FUNCTION','PACKAGE','PACKAGE_BODY','CLASS','CLASS_BODY','TYPE','JCLASS','TRIGGER','OPERATOR','MATERIALIZED VIEW')
START WITH D.REFED_ID = OBJID AND D.TYPE$ IN ('VIEW', 'PROCEDURE','FUNCTION','PACKAGE','PACKAGE_BODY','CLASS','CLASS_BODY','TYPE','JCLASS','TRIGGER','OPERATOR','MATERIALIZED VIEW');
SELECT O.SCHID, O.ID, O.TYPE$, O.SUBTYPE$ ,O.NAME BULK COLLECT INTO OBJ_TAB FROM SYS.SYSOBJECTS O WHERE
O.ID IN (SELECT * FROM TABLE(ID_ARR)) AND SF_IS_SYSTEM_OBJECT(O.ID, O.INFO1, O.SUBTYPE$) = 0;
FOR I IN 1..OBJ_TAB.COUNT LOOP
SF_LOCK_DICT_EX(OBJ_TAB[I].ID, 2);
END LOOP;
UPDATE /*+MULTI_IN_CVT_EXISTS(0) ENABLE_RQ_TO_INV(0)*/ SYS.SYSOBJECTS O SET VALID='N' WHERE (O.TYPE$, O.SCHID, O.NAME) IN (SELECT TYPE$, SCHID, NAME FROM TABLE(OBJ_TAB)) AND O.VALID = 'Y' AND O.SUBTYPE$
IN ('VIEW','PROC','PKG','TRIG','CLASS','TYPE','JCLASS');
FOR I IN 1..OBJ_TAB.COUNT LOOP
SF_REMOVE_DICT_OBJ(OBJ_TAB[I].SCHID, OBJ_TAB[I].ID, OBJ_TAB[I].TYPE$, OBJ_TAB[I].SUBTYPE$, 1);
END LOOP;
END;
SP_INVALID_DEP_OBJ(1066);
}
DELETE FROM SYS.SYSDEPENDENCIES WHERE ID=1066;
{
PROCEDURE SP_REMOVE_MV_DICT_BY_BASE_TID(OBJID INT)
AS
TYPE REC IS RECORD(SCHID INT, ID INT);
TYPE REC_MV IS TABLE OF REC;
OBJ_MV REC_MV;
BEGIN
SELECT B.SCHID, B.ID BULK COLLECT INTO OBJ_MV FROM SYS.SYSDEPENDENCIES A, SYS.SYSOBJECTS B WHERE
A.REFED_ID=OBJID AND A.TYPE$='MATERIALIZED VIEW' AND A.REFED_TYPE$='TABLE' AND A.ID = B.ID AND B.TYPE$ = 'SCHOBJ' AND B.SUBTYPE$ = 'VIEW';
FOR I IN 1..OBJ_MV.COUNT LOOP
SF_REMOVE_DICT_OBJ(OBJ_MV[I].SCHID, OBJ_MV[I].ID, 'SCHOBJ', 'VIEW', 1);
END LOOP;
END;
SP_REMOVE_MV_DICT_BY_BASE_TID(1066);
}
DELETE FROM SYS.SYSDEPENDENCIES WHERE REFED_ID=1066 AND TYPE$='MATERIALIZED VIEW' AND REFED_TYPE$='TABLE';
DELETE FROM SYS.SYSGRANTS WHERE OBJID = 1066;
SP_REMOVE_PCK_CACHE(-1, 1066, 0);
DELETE FROM SYS.SYSINDEXES WHERE ID = 33555530 CHECK LIMIT 1;
DELETE FROM SYS.SYSOBJECTS WHERE NAME = 'INDEX33555530' AND TYPE$ = 'TABOBJ' AND SCHID = 150995949 CHECK LIMIT 1;
DELETE FROM SYS.SYSOBJINFOS WHERE ID = 33555530;
DELETE FROM SYS.SYSINDEXES WHERE ID = 33555531 CHECK LIMIT 1;
DELETE FROM SYS.SYSOBJECTS WHERE NAME = 'INDEX33555531' AND TYPE$ = 'TABOBJ' AND SCHID = 150995949 CHECK LIMIT 1;
DELETE FROM SYS.SYSOBJINFOS WHERE ID = 33555531;
DELETE FROM SYS.SYSOBJINFOS WHERE ID = 1066;
{
UPDATE SYS.SYSCOLUMNS SET NULLABLE$ = 'Y' WHERE ID = 1066 AND COLID = 0;
{
SF_DELETE_INDEX_MONITOR(150995949, 33555531, 'USERS1', 'INDEX33555531');
DELETE FROM SYS.SYSINDEXES WHERE ID = 33555531;
DELETE FROM SYS.SYSOBJECTS WHERE NAME = 'INDEX33555531' AND ID = 33555531 AND SCHID = 150995949 AND TYPE$ = 'TABOBJ' AND SUBTYPE$ = 'INDEX' AND PID = 1066;
DELETE FROM SYS.SYSOBJINFOS WHERE ID = 33555531;
DELETE FROM SYS.SYSSTATS WHERE ID = 33555531 AND COLID = 0 AND T_FLAG = 'I';
}
DELETE FROM SYS.SYSOBJECTS WHERE NAME = 'PK_T_TEST' AND SCHID = 150995949 AND TYPE$ = 'TABOBJ';
DELETE FROM SYS.SYSCONS WHERE ID = 134218808;
}
DELETE FROM SYS.SYSOBJINFOS WHERE ID > 234881024 AND INT_VALUE = 1066 AND TYPE$ = 'CLSREF';
DELETE FROM SYS.SYSTEXTS WHERE ID IN (SELECT ID FROM SYS.SYSOBJECTS INDEX SYSINDEXPIDIDSYSOBJECTS WHERE PID = 1066 AND SUBTYPE$='TRIG');
DELETE FROM SYS.SYSOBJINFOS WHERE ID IN (SELECT ID FROM SYS.SYSOBJECTS INDEX SYSINDEXPIDIDSYSOBJECTS WHERE PID = 1066 AND SUBTYPE$='TRIG');
DELETE FROM SYS.SYSOBJECTS INDEX SYSINDEXPIDIDSYSOBJECTS WHERE PID = 1066 AND SUBTYPE$='TRIG';
{
DELETE FROM SYS.SYSSTATS WHERE ID = 1066;
DELETE FROM SYS.SYSMSTATS WHERE ID = 1066;
DELETE FROM SYS.SYSSTATS WHERE ID = 33555530 AND COLID = 0 AND T_FLAG = 'I';
DELETE FROM SYS.SYSSTATS WHERE ID = 33555531 AND COLID = 0 AND T_FLAG = 'I';
}
DELETE FROM SYS.SYSTABLECOMMENTS WHERE SCHNAME = 'USERS1' AND TVNAME = 'T_TEST';
DELETE FROM SYS.SYSCOLUMNCOMMENTS WHERE SCHNAME = 'USERS1' AND TVNAME = 'T_TEST';
DELETE FROM SYS.SYSOBJECTS WHERE NAME = 'T_TEST' AND TYPE$ = 'SCHOBJ' AND SCHID = 150995949 CHECK LIMIT 1;
DELETE FROM SYS.SYSFCOLDEFAULT WHERE TABID = 1066;
DELETE FROM SYS.SYSCOLUMNS WHERE ID = 1066;
DELETE FROM SYS.SYSCOLINFOS WHERE ID = 1066;
DELETE FROM SYS.SYSCOLCYT WHERE TID = 1066;
DELETE FROM SYS.SYSFECOLS WHERE TID = 1066;
DELETE FROM SYS.SYSCOLBYUSER WHERE TID = 1066;
SF_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 1);
SF_DELETE_SPACELIMIT_NODE(1066);
DELETE FROM SYS.POLICY_GROUPS WHERE OBJID = 1066;
DELETE FROM SYS.POLICIES WHERE OBJID = 1066;
DELETE FROM SYS.POLICY_CONTEXTS WHERE OBJID = 1066;
DELETE FROM SYS.POLICY_COLS WHERE OBJID = 1066;
DELETE FROM SYS.POLICY_MASKS WHERE OBJID = 1066;
SF_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 1);
EXECUTE IMMEDIATE 'SF_DELETE_OBJ_PGRP_NODE(1066, 0)';
DELETE FROM SYSAUDITOR.SYSAUDIT WHERE TVPID = 1066;
{
PROCEDURE SP_REMOVE_STORE_PLN_BY_OBJID(OBJID_IN INT, V_SCHID INT, V_TABNAME VARCHAR)
AS
IS_EXISTS INT;
SQLSTR STRING;
BEGIN
SELECT COUNT(*) INTO IS_EXISTS FROM SYS.SYSOBJECTS WHERE TYPE$='SCHOBJ' AND SCHID = V_SCHID AND NAME = V_TABNAME;
IF (IS_EXISTS != 0) THEN
BEGIN
SQLSTR =
'DECLARE
TYPE ID_ARR IS TABLE OF BIGINT;
TYPE INT_ARR IS TABLE OF INT;
TYPE STR_ARR IS TABLE OF VARCHAR(1000);
PLN_ID_ARR ID_ARR;
SQLSTR_ARR STR_ARR;
SCHID_ARR INT_ARR;
HASH_VALUE_ARR INT_ARR;
BEGIN
SELECT PLN_ID BULK COLLECT INTO PLN_ID_ARR FROM SYS.SYSPLNOBJID WHERE OBJID = '|| OBJID_IN ||';
IF PLN_ID_ARR.COUNT > 0 THEN
SELECT SCHID, HASH_VALUE, SQLSTR BULK COLLECT INTO SCHID_ARR, HASH_VALUE_ARR, SQLSTR_ARR FROM SYS.SYSPLNINFO WHERE PLN_ID IN (SELECT * FROM TABLE(PLN_ID_ARR));
DELETE FROM SYS.SYSPLNINFO WHERE PLN_ID IN (SELECT * FROM TABLE(PLN_ID_ARR));
DELETE FROM SYS.SYSPLNOBJID WHERE PLN_ID IN (SELECT * FROM TABLE(PLN_ID_ARR));
FOR I IN 1..SQLSTR_ARR.COUNT LOOP
SP_REMOVE_STORE_PLN_INNER(SCHID_ARR[I], HASH_VALUE_ARR[I], SQLSTR_ARR[I]);
END LOOP;
END IF;
END;';
EXECUTE IMMEDIATE SQLSTR;
END;
END IF;
END;
SP_REMOVE_STORE_PLN_BY_OBJID(1066, 150994944, 'SYSPLNINFO');
}
SP_DCT_TAB_DDL_REG(1066, 1, 0);
}
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
}
catch (exception ex){
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
SF_SET_GEN_INS_UREC_FLAG(1);
if (ISO_LEVEL != -1)
SF_SET_TRX_ISO_LEVEL(ISO_LEVEL);
throw;
}
}
SF_LOCK_APPLY_ID();
COMMIT;
}}
-------------------------- END --------------------------
*** 2026-07-06 15:42:51.287000000
*** Start trace 10002 event [SESSION ID:140553811567016] ***
[DDL SQL]:
ALTER SESSION SET EVENTS '10002 trace name context off';
[EQUIVALENT DML SQL]:
{
{
INT V_DC_FLAG_LEVEL;
INT ISO_LEVEL = -1;
try{
V_DC_FLAG_LEVEL = SF_RESET_DDLCOMMIT_FLAG();
SP_SESS_SET_TRACE_EVENT(0, 10002, 1, 1, 0);
COMMIT;
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
}
catch (exception ex){
SP_RESTORE_DDLCOMMIT_FLAG(V_DC_FLAG_LEVEL);
SF_SET_GEN_INS_UREC_FLAG(1);
if (ISO_LEVEL != -1)
SF_SET_TRX_ISO_LEVEL(ISO_LEVEL);
throw;
}
}
}
-------------------------- END --------------------------
[root@dameng-srv trace]# 4.5 DROP 后验证 SQL 和字典层面对象是否存在#
DROP 后,先从 SQL 层面验证表是否还存在。
CONN USERS1/"Users1@123"@127.0.0.1:5236
SELECT COUNT(1) FROM T_TEST;查询数据字典:
SELECT OWNER,
TABLE_NAME,
TABLESPACE_NAME
FROM DBA_TABLES
WHERE OWNER = 'USERS1'
AND TABLE_NAME = 'T_TEST';继续查询段信息:
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';继续查询 extent 信息:
SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;结果如下:
SQL> CONN USERS1/"Users1@123"@127.0.0.1:5236
Server[127.0.0.1:5236]:mode is normal, state is open
login used time : 2.422(ms)
SQL> SELECT COUNT(1) FROM T_TEST;
SELECT COUNT(1) FROM T_TEST;
[-2106]:Error in line: 1
Invalid table or view name [T_TEST].
used time: 2.332(ms). Execute id is 0.
SQL> SELECT OWNER,
TABLE_NAME,
TABLESPACE_NAME
FROM DBA_TABLES
WHERE OWNER = 'USERS1'
AND TABLE_NAME = 'T_TEST';2 3 4 5 6
no rows
used time: 12.889(ms). Execute id is 6001.
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
HEADER_FILE,
HEADER_BLOCK,
BLOCKS,
EXTENTS
FROM DBA_SEGMENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST';2 3 4 5 6 7 8 9 10
no rows
used time: 41.757(ms). Execute id is 6002.
SQL> SELECT OWNER,
SEGMENT_NAME,
TABLESPACE_NAME,
FILE_ID,
EXTENT_ID,
BLOCK_ID,
BLOCKS
FROM DBA_EXTENTS
WHERE OWNER = 'USERS1'
AND SEGMENT_NAME = 'T_TEST'
ORDER BY FILE_ID, BLOCK_ID;2 3 4 5 6 7 8 9 10 11
no rows
used time: 24.004(ms). Execute id is 6003.
SQL>这说明:
- DROP TABLE 后,T_TEST 表对象已经从数据字典中删除;
- DBA_TABLES 中已经看不到该表;
- DBA_SEGMENTS 中已经看不到该表段;
- DBA_EXTENTS 中也已经看不到属于该表的 extent 信息;
- 正常 SQL 已经无法再访问 USERS1.T_TEST。
这里需要特别强调:DBA_TABLES、DBA_SEGMENTS、DBA_EXTENTS 查不到 T_TEST,只能说明对象和段映射关系在数据库字典层面已经不存在,不能直接说明数据文件中的旧行内容已经被物理清零。
4.6 DROP 后用 strings 验证数据文件是否仍有残留#
DROP 后,不能再通过 DBA_EXTENTS 查询 T_TEST 的当前位置,因为对象已经不存在。因此,DROP 后验证数据文件残留时,主要依赖两类方式:
第一种方式:扫描整个表空间数据文件。
strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "DM_DROP_TEST_20260706_" | head再扫描 payload:
strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "BEFORE_DROP_20260706_" | head结果如下:
[dmdba@dameng-srv ~]$ strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "DM_DROP_TEST_20260706_" | head
DM_DROP_TEST_20260706_1
DM_DROP_TEST_20260706_2
DM_DROP_TEST_20260706_3
DM_DROP_TEST_20260706_4
DM_DROP_TEST_20260706_5
DM_DROP_TEST_20260706_6
DM_DROP_TEST_20260706_7
DM_DROP_TEST_20260706_8
DM_DROP_TEST_20260706_9
DM_DROP_TEST_20260706_10
[dmdba@dameng-srv ~]$ strings -a /dmdata/DAMENG/TBS_TEST01.DBF | grep "BEFORE_DROP_20260706_" | head
BEFORE_DROP_20260706_1_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_2_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_3_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_4_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_5_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_6_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_7_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_8_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_9_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BEFORE_DROP_20260706_10_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
[dmdba@dameng-srv ~]$DROP TABLE 后,只是删除对象和释放空间,并没有立即清零原数据块。数据文件中仍然残留 DROP 前写入的历史行内容。
4.7 DROP TABLE 的 10002 trace 分析#
本次实验对 DROP TABLE T_TEST; 开启了 10002 trace。从 trace 可以看到,达梦将该 DDL 展开为一系列等价的内部字典操作。
trace 开头明确记录:
[DDL SQL]:
DROP TABLE T_TEST;本次 DROP 操作涉及的核心对象编号如下:
表对象 :T_TEST
表对象 ID :1066
模式 ID :150995949
相关索引 ID :33555530、33555531
主键约束 :PK_T_TEST
约束 ID :134218808从 trace 内容看,DROP TABLE 的内部处理大致可以分为以下几个阶段。
第一,进入 DDL 处理流程并加字典锁:
SF_LOCK_APPLY_ID();
V_DC_FLAG_LEVEL = SF_RESET_DDLCOMMIT_FLAG();
SF_LOCK_DICT(1056964608, 3, 0);
SF_LOCK_DICT(150995949, 4, 0);
SF_LOCK_DICT(1066, 2, 0);这说明 DROP TABLE 是 DDL 级别操作,需要锁定相关字典对象,防止并发 DDL 或对象访问影响删除过程。
第二,检查特殊对象类型,例如是否为队列表:
SP_CHECK_IS_DBMS_AQADM_TABLE();该过程会检查 DBMS_AQADM 以及 SYS.AQ$_QUEUE_TABLES,判断当前对象是否属于高级队列相关表。如果是特殊队列表,需要走专门管理流程。本实验中的 T_TEST 是普通用户表,因此检查后继续执行 DROP。
第三,处理字典版本和对象缓存:
SP_REPORT_DCT_VERSION(1066, 0, DATETIME '2026-07-06 14:00:02.12');
SF_REPORT_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 1);
SF_REMOVE_DICT_OBJ(150995949, 1066, 'SCHOBJ', 'UTAB', 1);这说明 DROP 会报告或更新对象字典版本,并将 T_TEST 从字典缓存中移除。DROP 后,SQL 解析阶段已经无法再通过 USERS1.T_TEST 找到原表对象。
第四,清理统计信息和对象修改监控信息:
DELETE FROM SYS.SYSSTATTABLEIDU WHERE ID = 1066;
DELETE FROM SYS.SYSSTATPREFS WHERE ID = 1066;
DELETE FROM SYS.SYSMODIFICATIONS
WHERE ID = 1066 OR ROOT_ID = 1066 OR BASE_ID = 1066;
SP_MODIFY_AUTO_STAT_OBJ('D', 1066, NULL, NULL);
SP_MODIFY_MON_MODS('D', 1066, -1, -1, NULL);后续还可以看到:
DELETE FROM SYS.SYSSTATS WHERE ID = 1066;
DELETE FROM SYS.SYSMSTATS WHERE ID = 1066;
DELETE FROM SYS.SYSSTATS WHERE ID = 33555530 AND COLID = 0 AND T_FLAG = 'I';
DELETE FROM SYS.SYSSTATS WHERE ID = 33555531 AND COLID = 0 AND T_FLAG = 'I';这些操作说明,DROP 会清理该表及其索引相关的统计信息、统计偏好设置和修改监控记录。
第五,处理依赖对象。trace 中定义并调用了:
PROCEDURE SP_INVALID_DEP_OBJ(OBJID INT)
...
SP_INVALID_DEP_OBJ(1066);该过程会从 SYS.SYSDEPENDENCIES 中查找依赖当前表的视图、过程、函数、包、触发器、物化视图等对象,将相关依赖对象置为无效,并移除字典缓存:
UPDATE SYS.SYSOBJECTS O SET VALID='N'
...
SF_REMOVE_DICT_OBJ(...);随后继续清理依赖关系:
DELETE FROM SYS.SYSDEPENDENCIES WHERE ID=1066;
DELETE FROM SYS.SYSDEPENDENCIES
WHERE REFED_ID=1066
AND TYPE$='MATERIALIZED VIEW'
AND REFED_TYPE$='TABLE';这说明 DROP 表时,不只是删除表本身,还会处理依赖该表的其他数据库对象。
第六,清理对象授权和缓存:
DELETE FROM SYS.SYSGRANTS WHERE OBJID = 1066;
SP_REMOVE_PCK_CACHE(-1, 1066, 0);这表示表对象上的授权信息以及相关包缓存会被删除或失效。
第七,删除索引和主键约束。由于本实验表 T_TEST 创建了主键约束 PK_T_TEST,DROP 时会同时清理约束和相关索引:
DELETE FROM SYS.SYSINDEXES WHERE ID = 33555530 CHECK LIMIT 1;
DELETE FROM SYS.SYSOBJECTS
WHERE NAME = 'INDEX33555530'
AND TYPE$ = 'TABOBJ'
AND SCHID = 150995949 CHECK LIMIT 1;
DELETE FROM SYS.SYSINDEXES WHERE ID = 33555531;
DELETE FROM SYS.SYSOBJECTS
WHERE NAME = 'INDEX33555531'
AND ID = 33555531
AND SCHID = 150995949
AND TYPE$ = 'TABOBJ'
AND SUBTYPE$ = 'INDEX'
AND PID = 1066;
DELETE FROM SYS.SYSOBJECTS
WHERE NAME = 'PK_T_TEST'
AND SCHID = 150995949
AND TYPE$ = 'TABOBJ';
DELETE FROM SYS.SYSCONS WHERE ID = 134218808;这说明 DROP 表时,表上的索引对象、主键约束对象、索引统计信息、索引监控信息也会一并删除。
第八,删除表自身的对象和列定义。trace 中最核心的删除表对象入口语句是:
DELETE FROM SYS.SYSOBJECTS
WHERE NAME = 'T_TEST'
AND TYPE$ = 'SCHOBJ'
AND SCHID = 150995949 CHECK LIMIT 1;随后清理列定义和列属性:
DELETE FROM SYS.SYSFCOLDEFAULT WHERE TABID = 1066;
DELETE FROM SYS.SYSCOLUMNS WHERE ID = 1066;
DELETE FROM SYS.SYSCOLINFOS WHERE ID = 1066;
DELETE FROM SYS.SYSCOLCYT WHERE TID = 1066;
DELETE FROM SYS.SYSFECOLS WHERE TID = 1066;
DELETE FROM SYS.SYSCOLBYUSER WHERE TID = 1066;同时还会清理表注释和列注释:
DELETE FROM SYS.SYSTABLECOMMENTS
WHERE SCHNAME = 'USERS1'
AND TVNAME = 'T_TEST';
DELETE FROM SYS.SYSCOLUMNCOMMENTS
WHERE SCHNAME = 'USERS1'
AND TVNAME = 'T_TEST';这说明 DROP 后,不只是表数据不可见,表对象定义、列定义、约束、索引和注释等元数据也会从数据字典中删除。
第九,清理策略、审计、空间限制和执行计划信息:
SF_DELETE_SPACELIMIT_NODE(1066);
DELETE FROM SYS.POLICY_GROUPS WHERE OBJID = 1066;
DELETE FROM SYS.POLICIES WHERE OBJID = 1066;
DELETE FROM SYS.POLICY_CONTEXTS WHERE OBJID = 1066;
DELETE FROM SYS.POLICY_COLS WHERE OBJID = 1066;
DELETE FROM SYS.POLICY_MASKS WHERE OBJID = 1066;
DELETE FROM SYSAUDITOR.SYSAUDIT WHERE TVPID = 1066;
SP_REMOVE_STORE_PLN_BY_OBJID(1066, 150994944, 'SYSPLNINFO');最后执行 DDL 注册并提交:
SP_DCT_TAB_DDL_REG(1066, 1, 0);
COMMIT;这说明 DROP TABLE 是一次完整提交的 DDL 操作,不能通过普通 ROLLBACK 撤销。
从整个 10002 trace 看,DROP TABLE T_TEST 的主体动作是:
- 加锁;
- 检查对象状态;
- 处理字典版本;
- 清理统计信息;
- 置无效依赖对象;
- 删除依赖关系;
- 删除授权;
- 删除索引;
- 删除主键约束;
- 删除表对象入口;
- 删除列定义;
- 删除注释;
- 删除策略、审计、计划、缓存;
- 注册 DDL;
- COMMIT。
trace 中没有看到对 USERS1.T_TEST 执行逐行删除的动作,也没有看到对 /dmdata/DAMENG/TBS_TEST01.DBF 数据文件进行物理清零的动作。因此,本次 trace 支持如下判断:
- DROP TABLE 是对象级 DDL 操作,核心是删除对象字典入口并释放段空间。
- 它不是普通 DELETE,也不是对数据文件中旧行内容执行物理擦除。
- DROP 后,SQL 和字典层面已经无法访问原表。
- 但只要原数据块没有被后续写入覆盖,数据文件中仍可能残留历史行内容。
4.8 DROP 实验结论#
通过 DROP 实验,可以得到以下结论:
DROP TABLE T_TEST后,正常 SQL 无法再访问USERS1.T_TEST。DBA_TABLES中查不到T_TEST,说明表对象已经从数据库字典中删除。DBA_SEGMENTS中查不到T_TEST,说明表段已经不再作为一个可见段挂在该对象名下。DBA_EXTENTS中查不到T_TEST,说明原来属于该表的 extent 已经无法再通过表名查询。- 这些字典层面的消失,只能说明对象和段映射关系被删除,不能直接证明数据文件中的历史内容被物理清零。
- 如果 DROP 后扫描
/dmdata/DAMENG/TBS_TEST01.DBF仍然可以看到DM_DROP_TEST_20260706_测试标记,说明 DROP 后数据文件中仍有旧数据残留。 - 如果 DROP 后扫描不到测试标记,也只能说明当前扫描范围内没有找到可识别字符串,不能直接推出 DROP 一定会物理清零全部旧数据。
因此,DROP 的本质可以概括为:
- DROP TABLE 删除的是表对象和相关字典映射关系,并释放该对象占用的段空间。
- 但释放空间不等于立即物理清零数据文件。
- 只要旧数据块没有被后续对象或新数据覆盖,数据文件中仍可能残留历史行内容,具备离线恢复的可能。
五、TRUNCATE 与 DROP 对比分析#
通过前面的实验可以看出,TRUNCATE 和 DROP 都会让数据在 SQL 层面不可见,但二者在对象保留、字典变化、段空间处理和恢复条件上存在明显区别。
| 对比项 | TRUNCATE TABLE | DROP TABLE |
|---|---|---|
| SQL 查询结果 | 表仍然存在,但数据行数变为 0 | 表对象不存在,查询时报对象或视图不存在 |
| 表对象是否保留 | 保留 | 不保留 |
DBA_TABLES 中是否可见 | 可见 | 不可见 |
DBA_SEGMENTS 中是否可见 | 通常仍可见 | 不可见 |
DBA_EXTENTS 中是否可见 | 可见,但只反映 TRUNCATE 后当前仍属于该表的 extent | 不可见 |
| 段头位置 | HEADER_FILE、HEADER_BLOCK 通常保持不变 | 表对象删除后,无法再通过表名查询段头 |
| 段空间变化 | BLOCKS、EXTENTS 明显变小,说明段空间被重置或释放 | 原表段映射关系被删除,空间被释放为可重新分配状态 |
| 是否逐行 DELETE | 否 | 否 |
| 是否普通 ROLLBACK 可恢复 | 否 | 否 |
| 10002 trace 重点 | TRUNCTIME、SF_TRUNC_TABLE、SP_DCT_TAB_DDL_REG、COMMIT | 删除 SYSOBJECTS、SYSCOLUMNS、SYSINDEXES、SYSCONS、SYSSTATS、SYSDEPENDENCIES、SYSGRANTS 等字典信息,并 COMMIT |
| 对索引和约束的影响 | 表对象和索引对象通常仍保留,只是数据被清空或段被重置 | 表对象、索引对象、主键约束、列定义等元数据都会被删除 |
| 数据文件是否立即清零 | 不一定,本实验中仍能扫描到 TRUNCATE 前测试字符串 | 不一定,本实验中仍能扫描到 DROP 前测试字符串 |
| 数据文件残留证据 | strings -a /dmdata/DAMENG/TBS_TEST01.DBF 仍可看到 DM_TRUNCATE_TEST_20260706_ | strings -a /dmdata/DAMENG/TBS_TEST01.DBF 仍可看到 DM_DROP_TEST_20260706_ 和 BEFORE_DROP_20260706_ |
| 恢复难度 | 相对较低,因为表结构仍然存在,字段顺序、数据类型等信息还可查 | 相对较高,因为表对象和列定义等字典信息已经删除,需要依赖事前记录、备份、字典残留或人工重建表结构 |
| 恢复关键条件 | 原数据块未被覆盖,表结构仍可识别 | 原数据块未被覆盖,表结构、字段顺序、字段类型等元信息仍可获得或重建 |
| 本质概括 | 保留对象,重置表段 | 删除对象,释放段空间 |
综合来看,TRUNCATE 和 DROP 的共同点是:二者都不是逐行删除,也都不能通过普通 ROLLBACK 恢复;从数据文件层面看,二者都不等价于立即物理清零。
二者的核心区别是:TRUNCATE 保留表对象,只是重置或释放表段空间;DROP 删除表对象及相关字典信息,并释放原表段空间。因此,DROP 后恢复难度通常高于 TRUNCATE,因为不仅要找回数据块,还要解决表结构和字典元数据缺失的问题。
六、用 dmdul 拯救被 TRUNCATE 的表#
前面的实验已经证明,TRUNCATE TABLE 会让表数据在 SQL 层面不可见,但并不会立即物理清零数据文件中的历史行内容。为了进一步验证这些残留数据是否真的具备恢复价值,本节使用自研工具 dmdul 对一张被 TRUNCATE 的测试表进行离线恢复实验。
本节单独创建一张小表 USERS1.T_TRUNCATE_TEST,插入 100 行数据后执行 TRUNCATE,然后使用 dmdul 从数据文件中恢复。
6.1 创建测试表并插入 100 行数据#
使用 USERS1 用户创建测试表:
CONN USERS1/"Users1@123"@127.0.0.1:5236创建表:
CREATE TABLE T_TRUNCATE_TEST
(
ID INT,
MARKER VARCHAR(200),
PAYLOAD VARCHAR(1000),
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT PK_T_TRUNCATE_TEST PRIMARY KEY (ID)
)
TABLESPACE TBS_TEST;插入 100 行带有明显标记的数据:
INSERT INTO T_TRUNCATE_TEST(ID, MARKER, PAYLOAD)
SELECT LEVEL,
'DMDUL_TRUNCATE_RECOVER_' || TO_CHAR(LEVEL),
'DMDUL_BEFORE_TRUNCATE_' || TO_CHAR(LEVEL) || '_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC'
FROM DUAL
CONNECT BY LEVEL <= 100;
COMMIT;执行 checkpoint,将数据尽量刷入数据文件:
SELECT CHECKPOINT(100);检查数据:
SELECT COUNT(*) FROM T_TRUNCATE_TEST;
SELECT *
FROM T_TRUNCATE_TEST
WHERE ID <= 10
ORDER BY ID;实验结果如下:
SQL> CREATE TABLE T_TRUNCATE_TEST
(
ID INT,
MARKER VARCHAR(200),
PAYLOAD VARCHAR(1000),
CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT PK_T_TRUNCATE_TEST PRIMARY KEY (ID)
)
TABLESPACE TBS_TEST;
executed successfully
used time: 31.952(ms). Execute id is 6501.
SQL> INSERT INTO T_TRUNCATE_TEST(ID, MARKER, PAYLOAD)
SELECT LEVEL,
'DMDUL_TRUNCATE_RECOVER_' || TO_CHAR(LEVEL),
'DMDUL_BEFORE_TRUNCATE_' || TO_CHAR(LEVEL) || '_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC'
FROM DUAL
CONNECT BY LEVEL <= 100;
affect rows 100
SQL> COMMIT;
executed successfully
SQL> SELECT CHECKPOINT(100);
LINEID CHECKPOINT(100)
---------- ---------------
1 0
SQL> SELECT COUNT(*) FROM T_TRUNCATE_TEST;
LINEID COUNT(*)
---------- --------------------
1 100查询前 10 行可以看到测试标记:
LINEID ID MARKER PAYLOAD CREATE_TIME
---------- ----------- ------------------------- ----------------------------------------------------------------- --------------------------
1 1 DMDUL_TRUNCATE_RECOVER_1 DMDUL_BEFORE_TRUNCATE_1_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
2 2 DMDUL_TRUNCATE_RECOVER_2 DMDUL_BEFORE_TRUNCATE_2_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
3 3 DMDUL_TRUNCATE_RECOVER_3 DMDUL_BEFORE_TRUNCATE_3_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
4 4 DMDUL_TRUNCATE_RECOVER_4 DMDUL_BEFORE_TRUNCATE_4_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
5 5 DMDUL_TRUNCATE_RECOVER_5 DMDUL_BEFORE_TRUNCATE_5_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
6 6 DMDUL_TRUNCATE_RECOVER_6 DMDUL_BEFORE_TRUNCATE_6_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
7 7 DMDUL_TRUNCATE_RECOVER_7 DMDUL_BEFORE_TRUNCATE_7_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
8 8 DMDUL_TRUNCATE_RECOVER_8 DMDUL_BEFORE_TRUNCATE_8_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
9 9 DMDUL_TRUNCATE_RECOVER_9 DMDUL_BEFORE_TRUNCATE_9_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503
10 10 DMDUL_TRUNCATE_RECOVER_10 DMDUL_BEFORE_TRUNCATE_10_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC 2026-07-06 17:25:15.535503这一步说明,T_TRUNCATE_TEST 在 TRUNCATE 前确实有 100 行业务数据。
6.2 执行 TRUNCATE#
执行 TRUNCATE:
TRUNCATE TABLE T_TRUNCATE_TEST;结果如下:
SQL> TRUNCATE TABLE T_TRUNCATE_TEST;
executed successfully
used time: 6.412(ms). Execute id is 6507.此时,从 SQL 层面看,T_TRUNCATE_TEST 已经被清空。
建议补充执行以下 SQL 确认:
SELECT COUNT(*) FROM T_TRUNCATE_TEST;预期结果为:
COUNT(*) = 06.3 使用 dmdul 扫描数据库文件#
查看 dmdul 版本:
./dmdul version结果如下:
[dmdba@dameng-srv DAMENG]$ ./dmdul version
dmdul v0.2.0 (6875da9)进入 dmdul 交互模式:
./dmdul
[dmdba@dameng-srv DAMENG]$ ./dmdul version
dmdul v0.2.0 (6875da9)
[dmdba@dameng-srv DAMENG]$ ./dmdul
dmdul: Release v0.2.0 - Dameng Data Unloader Tool
Copyright (c) 2026 greatfinish. All rights reserved.
https://github.com/greatfinish/dmdul
Type help; for available commands.
DMDUL> 配置 SYSTEM.DBF 和数据文件目录:
DMDUL> set system /dmdata/DAMENG/SYSTEM.DBF;
DMDUL> set data_dir /dmdata/DAMENG;
DMDUL> set system /dmdata/DAMENG/SYSTEM.DBF;
system = /dmdata/DAMENG/SYSTEM.DBF
DMDUL> set data_dir /dmdata/DAMENG;
data_dir = /dmdata/DAMENG
DMDUL> 执行 bootstrap:
DMDUL> bootstrap;输出如下:
DMDUL> bootstrap;
bootstrap completed
system file: /dmdata/DAMENG/SYSTEM.DBF
control file: /dmdata/DAMENG/dm.ctl
control.dul: /dmdata/DAMENG/control.dul (data files: 6)
dictionary dir: /dmdata/DAMENG/dmdul_dict (users=5 tables=18 columns=92 views=4 sequences=1 routines=10 triggers=2 synonyms=11 tab_privs=25)
page size: 8192 bytes
extent size: 16 pages (u32 @ 0x80)
page count: 9472
charset: UTF-8 (UNICODE_FLAG=1) (SYSTEM.DBF page 4 + 0x2D)
charset parameter: utf-8
objects loaded: 1183
users loaded: 5
tables loaded: 18
columns loaded: 92
views loaded: 4
sequences loaded: 1
routines loaded: 10
triggers loaded: 2
synonyms loaded: 11
tab privileges loaded: 25
DMDUL> 这个结果说明,dmdul 已经成功读取 SYSTEM.DBF、dm.ctl 和数据文件目录,完成字典解析,并识别出当前数据库中的用户、表、列、视图、序列、存储对象、触发器、同义词和对象权限等信息。
6.4 确认 dmdul 能识别被 TRUNCATE 的表#
查看用户列表:
DMDUL> list user;输出如下:
DMDUL> list user;
dictionary source: SYSTEM.DBF
dictionary dir: /dmdata/DAMENG/dmdul_dict
dictionary rows: users=5 tables=18 columns=92 views=4 sequences=1 routines=10 triggers=2 synonyms=11 tab_privs=25 objects=1183
user tables
APP_USER 0
HR_TEST 3
SY 1
SYSDBA 13
USERS1 1
DMDUL> 查看 USERS1 用户下的表:
DMDUL> list table USERS1;输出如下:
DMDUL> list table USERS1;
owner table table_id columns tablespace storage partition
USERS1 T_TRUNCATE_TEST 1067 4 TBS_TEST CLUSTERBTR NO
1 table(s)
DMDUL> 这里可以看到,虽然 T_TRUNCATE_TEST 已经被 TRUNCATE,但表对象仍然存在,dmdul 仍然可以从字典中识别到该表:
owner = USERS1
table = T_TRUNCATE_TEST
table_id = 1067
columns = 4
tablespace = TBS_TEST
storage = CLUSTERBTR
partition = NO这与前面 TRUNCATE 实验的结论一致:TRUNCATE 后表对象仍在,只是表数据在 SQL 层面不可见,表段空间被重置或释放。
6.5 使用 dmdul recover table 恢复数据#
执行恢复命令:
DMDUL> recover table USERS1.T_TRUNCATE_TEST;结果如下:
DMDUL> recover table USERS1.T_TRUNCATE_TEST;
recovery mode: on
ddl output: /dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_ddl.sql
data output: /dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_data.sql
tables exported: 1
rows exported: 100
rows failed: 0
DMDUL> 这个结果非常关键:
tables exported = 1
rows exported = 100
rows failed = 0说明 dmdul 成功从 TRUNCATE 后的数据文件中恢复出了 T_TRUNCATE_TEST 的 100 行历史数据。
6.6 查看 dmdul 恢复出的 DDL#
查看恢复出的 DDL 文件:
cat /dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_ddl.sql结果如下:
-- Generated by dmdul export-ddl. Review before running.
-- Users and roles
-- Password hashes are not exported. Change the placeholder password after import.
CREATE USER USERS1 IDENTIFIED BY "dmdul_default_password" DEFAULT TABLESPACE "TBS_TEST" TEMPORARY TABLESPACE "TEMP";
GRANT PUBLIC TO USERS1;
GRANT RESOURCE TO USERS1;
-- Tables
CREATE TABLE USERS1.T_TRUNCATE_TEST (
ID INT NOT NULL,
MARKER VARCHAR(200),
PAYLOAD VARCHAR(1000),
CREATE_TIME TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP
)
STORAGE(ON "TBS_TEST", CLUSTERBTR);
-- Indexes
-- Constraints
ALTER TABLE USERS1.T_TRUNCATE_TEST ADD CONSTRAINT PK_T_TRUNCATE_TEST PRIMARY KEY (ID);
-- Comments可以看到,dmdul 不仅恢复出了数据,还生成了表结构 DDL,包括:
1. 用户 USERS1;
2. 表 USERS1.T_TRUNCATE_TEST;
3. 字段 ID、MARKER、PAYLOAD、CREATE_TIME;
4. 字段类型;
5. 表空间 TBS_TEST;
6. 存储类型 CLUSTERBTR;
7. 主键约束 PK_T_TRUNCATE_TEST。6.7 查看 dmdul 恢复出的数据#
查看恢复出的数据文件:
cat /dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_data.sql恢复文件中可以看到完整的 INSERT 语句:
-- Generated by dmdul export-data. Review before running.
-- Current decoder targets ordinary in-row heap/cluster/IOT rows.
INSERT INTO USERS1.T_TRUNCATE_TEST (ID, MARKER, PAYLOAD, CREATE_TIME) VALUES (1, 'DMDUL_TRUNCATE_RECOVER_1', 'DMDUL_BEFORE_TRUNCATE_1_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', DATETIME '2026-07-06 17:25:15.535503');
INSERT INTO USERS1.T_TRUNCATE_TEST (ID, MARKER, PAYLOAD, CREATE_TIME) VALUES (2, 'DMDUL_TRUNCATE_RECOVER_2', 'DMDUL_BEFORE_TRUNCATE_2_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', DATETIME '2026-07-06 17:25:15.535503');
INSERT INTO USERS1.T_TRUNCATE_TEST (ID, MARKER, PAYLOAD, CREATE_TIME) VALUES (3, 'DMDUL_TRUNCATE_RECOVER_3', 'DMDUL_BEFORE_TRUNCATE_3_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', DATETIME '2026-07-06 17:25:15.535503');
INSERT INTO USERS1.T_TRUNCATE_TEST (ID, MARKER, PAYLOAD, CREATE_TIME) VALUES (4, 'DMDUL_TRUNCATE_RECOVER_4', 'DMDUL_BEFORE_TRUNCATE_4_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', DATETIME '2026-07-06 17:25:15.535503');
INSERT INTO USERS1.T_TRUNCATE_TEST (ID, MARKER, PAYLOAD, CREATE_TIME) VALUES (5, 'DMDUL_TRUNCATE_RECOVER_5', 'DMDUL_BEFORE_TRUNCATE_5_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', DATETIME '2026-07-06 17:25:15.535503');
...
INSERT INTO USERS1.T_TRUNCATE_TEST (ID, MARKER, PAYLOAD, CREATE_TIME) VALUES (100, 'DMDUL_TRUNCATE_RECOVER_100', 'DMDUL_BEFORE_TRUNCATE_100_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', DATETIME '2026-07-06 17:25:15.535503');从恢复结果可以看到:
1. ID 从 1 到 100 均被恢复;
2. MARKER 字段中的 DMDUL_TRUNCATE_RECOVER_ 标记被完整恢复;
3. PAYLOAD 字段中的 DMDUL_BEFORE_TRUNCATE_ 标记被完整恢复;
4. CREATE_TIME 字段也被恢复为原始插入时间 2026-07-06 17:25:15.535503。也可以通过命令统计恢复出的行数:
grep -c "INSERT INTO USERS1.T_TRUNCATE_TEST" /dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_data.sql结果为:
[dmdba@dameng-srv DAMENG]$ grep -c "INSERT INTO USERS1.T_TRUNCATE_TEST" /dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_data.sql
100
[dmdba@dameng-srv DAMENG]$6.8 dmdul 恢复实验结论#
本次 dmdul 恢复实验进一步验证了前文关于 TRUNCATE 的判断:
TRUNCATE 后,表数据在 SQL 层面已经不可见,但数据文件中的旧行内容并没有立即消失。在本实验中,T_TRUNCATE_TEST 被 TRUNCATE 后,dmdul 仍然成功从数据文件中恢复出 100 行数据,并生成了可执行的 DDL 和 INSERT 脚本:
DDL 文件 :/dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_ddl.sql
数据文件 :/dmdata/DAMENG/USERS1_T_TRUNCATE_TEST_recover_data.sql
恢复行数 :100
失败行数 :0这说明:
- TRUNCATE 并不会立即物理清零数据文件中的旧行内容。
- TRUNCATE 后,只要原数据块尚未被新对象或新数据覆盖,仍然有实际恢复可能。
- 相比 DROP,TRUNCATE 后表对象和字段定义
七、关于 dmdul:一个达梦离线数据抽取小工具#
本文最后的 TRUNCATE 恢复实验使用的是我自己写的开源小工具:dmdul。
项目地址:
https://github.com/greatfinish/dmduldmdul 的定位是一个面向达梦数据库的数据文件离线分析和数据抽取工具。它的目标不是替代数据库官方恢复手段,而是在数据库无法正常打开、表被误操作、或者需要分析 DBF 文件残留数据时,提供一种额外的离线排查和恢复思路。
目前 dmdul 主要支持:
| 功能 | 说明 |
|---|---|
| 离线读取达梦数据文件 | 基于 SYSTEM.DBF、dm.ctl 和用户表空间 DBF 文件进行分析 |
| 解析数据字典 | 可识别用户、表、列、视图、序列、触发器、权限等基础字典信息 |
| 查看用户和表 | 支持 list user、list table <owner> 等交互式命令 |
| 导出表结构 | 可以生成用户表的 DDL 脚本 |
| 导出表数据 | 可以将识别到的数据导出为 INSERT SQL |
| 尝试恢复 TRUNCATE、DROP 后数据 | 在旧数据块未被覆盖的情况下,可尝试从数据文件中恢复被 TRUNCATE、DROP 的历史数据 |
在本文实验中,USERS1.T_TRUNCATE_TEST 被 TRUNCATE 后,数据库 SQL 层面已经无法查询到原来的 100 行数据,但 dmdul 仍然成功从数据文件中恢复出完整的 100 行记录:
tables exported: 1
rows exported: 100
rows failed: 0这说明,只要数据文件中的旧块没有被新数据覆盖,TRUNCATE 后的数据并不一定真的“消失”,仍然存在离线恢复的可能。
当然,任何离线恢复工具都不能保证所有场景 100% 成功。实际恢复效果取决于数据块是否被覆盖、表结构是否还能识别、字段类型是否支持、数据是否涉及 LOB、压缩、加密、分区等复杂场景。因此,建议优先使用数据库官方备份恢复手段,dmdul 更适合作为应急分析、实验验证和补充恢复工具。
如果你也对达梦数据库文件格式、误操作恢复、离线数据抽取感兴趣,欢迎关注、试用或一起完善这个项目:
https://github.com/greatfinish/dmdul也欢迎提交 issue、建议和测试案例,让这个工具在更多真实场景中逐步完善。
八、最终结论#
达梦数据库中,TRUNCATE 和 DROP 都会让数据在 SQL 层面不可见,但并不等于立即物理清零数据文件;只要原数据块没有被覆盖,仍然可能存在离线扫描和恢复的机会。在没有可用备份、且原数据块尚未被覆盖的情况下,也可以尝试使用我的开源工具 dmdul 进行离线扫描和辅助恢复。
