跳过正文

达梦数据库 TRUNCATE / DROP 后数据真的没了吗?一次数据文件残留实验验证

Greatfinish
作者
Greatfinish
记录 Oracle、PostgreSQL、达梦、Linux、存储与生产环境故障处理经验。
目录

一、问题背景
#

在数据库恢复场景中,经常会遇到两个问题:

  1. 表被 TRUNCATE 以后,数据文件里的数据是不是还能找回来?
  2. 表被 DROP 以后,数据文件里的数据是不是还在?

从业务视角看,TRUNCATE TABLEDROP TABLE 都会让表数据不可见。但从数据文件物理层看,两者的处理方式并不完全一样。

本文基于一次达梦 DM8 实验,从字典变化、数据文件残留、10002 trace 分析几个角度,验证 TRUNCATEDROP 后数据是否被清理,以及是否具备恢复可能性。

测试环境为达梦 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      = 13

TRUNCATE 后 DBA_SEGMENTS 查询结果如下:

HEADER_FILE  = 0
HEADER_BLOCK = 16
BLOCKS       = 16
EXTENTS      = 1

从这个变化可以看出:

  1. HEADER_FILE 没有变化,仍然是 0,说明 T_TEST 的段头仍然位于同一个数据文件中,也就是 /dmdata/DAMENG/TBS_TEST01.DBF
  2. HEADER_BLOCK 没有变化,仍然是 16,说明 TRUNCATE TABLE T_TEST 后,表对象本身没有被删除,段头位置仍然保留。也就是说,TRUNCATE 不是删除表对象,而是重置表段中的数据和空间状态。
  3. BLOCKS208 变成 16,说明 TRUNCATE 前由于插入了 10000 行数据,表段已经扩展到较大的空间规模;TRUNCATE 后,表段空间被释放或重置,只保留了较小的初始空间。
  4. EXTENTS13 变成 1,说明 TRUNCATE 前该表已经分配了多个 extent;TRUNCATE 后,原来用于存放数据的多个 extent 不再属于当前表段,只保留了一个基础 extent。

因此,TRUNCATE 的效果不是逐行删除,而是通过段级操作重置表空间使用状态。SQL 层面看,表仍然存在,但数据行变为 0;空间层面看,原来分配给该表的大量块和 extent 被释放或重新整理。

需要特别说明的是:BLOCKSEXTENTS 变小,只能说明这些块不再属于当前 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 实验,可以得到以下结论:

  1. 从 SQL 层面看,TRUNCATE TABLE T_TEST 执行后,SELECT COUNT(*) FROM T_TEST 返回 0,说明表数据已经对数据库用户不可见。
  2. 从段空间变化看,TRUNCATE 前 DBA_SEGMENTS 显示 T_TEST 占用 208 个块、13 个 extent;TRUNCATE 后变为 16 个块、1 个 extent,说明 TRUNCATE 会重置或释放表段空间。
  3. HEADER_FILEHEADER_BLOCK 在 TRUNCATE 前后保持不变,说明 T_TEST 表对象本身没有被删除,段头仍然保留。TRUNCATE 的行为不是删除表对象,而是对表段进行重置。
  4. BLOCKSEXTENTS 变小,只能说明原来属于该表的大量数据块不再挂在当前 T_TEST 段下,不能证明这些块中的历史数据已经被物理清零。
  5. 对整个 /dmdata/DAMENG/TBS_TEST01.DBF 数据文件执行 strings 扫描后,仍然可以看到 DM_TRUNCATE_TEST_20260706_ 测试标记,说明 TRUNCATE 后旧行内容仍然残留在数据文件中。
  6. 10002 trace 中可以看到 TRUNCTIMESF_TRUNC_TABLESP_DCT_TAB_DDL_REGSF_REMOVE_DICT_OBJCOMMIT 等内部操作,说明 TRUNCATE 是 DDL 级别的段重置操作,不是普通逐行 DELETE。
  7. 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_SEGMENTSDBA_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_FILEHEADER_BLOCKBLOCKSEXTENTS 以及 DBA_EXTENTS 中的 BLOCK_IDBLOCKS,是 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_BLOCKBLOCKS 扩大扫描范围。例如:

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_TABLESDBA_SEGMENTSDBA_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 的主体动作是:

  1. 加锁;
  2. 检查对象状态;
  3. 处理字典版本;
  4. 清理统计信息;
  5. 置无效依赖对象;
  6. 删除依赖关系;
  7. 删除授权;
  8. 删除索引;
  9. 删除主键约束;
  10. 删除表对象入口;
  11. 删除列定义;
  12. 删除注释;
  13. 删除策略、审计、计划、缓存;
  14. 注册 DDL;
  15. COMMIT。

trace 中没有看到对 USERS1.T_TEST 执行逐行删除的动作,也没有看到对 /dmdata/DAMENG/TBS_TEST01.DBF 数据文件进行物理清零的动作。因此,本次 trace 支持如下判断:

  • DROP TABLE 是对象级 DDL 操作,核心是删除对象字典入口并释放段空间。
  • 它不是普通 DELETE,也不是对数据文件中旧行内容执行物理擦除。
  • DROP 后,SQL 和字典层面已经无法访问原表。
  • 但只要原数据块没有被后续写入覆盖,数据文件中仍可能残留历史行内容。

4.8 DROP 实验结论
#

通过 DROP 实验,可以得到以下结论:

  1. DROP TABLE T_TEST 后,正常 SQL 无法再访问 USERS1.T_TEST
  2. DBA_TABLES 中查不到 T_TEST,说明表对象已经从数据库字典中删除。
  3. DBA_SEGMENTS 中查不到 T_TEST,说明表段已经不再作为一个可见段挂在该对象名下。
  4. DBA_EXTENTS 中查不到 T_TEST,说明原来属于该表的 extent 已经无法再通过表名查询。
  5. 这些字典层面的消失,只能说明对象和段映射关系被删除,不能直接证明数据文件中的历史内容被物理清零。
  6. 如果 DROP 后扫描 /dmdata/DAMENG/TBS_TEST01.DBF 仍然可以看到 DM_DROP_TEST_20260706_ 测试标记,说明 DROP 后数据文件中仍有旧数据残留。
  7. 如果 DROP 后扫描不到测试标记,也只能说明当前扫描范围内没有找到可识别字符串,不能直接推出 DROP 一定会物理清零全部旧数据。

因此,DROP 的本质可以概括为:

  • DROP TABLE 删除的是表对象和相关字典映射关系,并释放该对象占用的段空间。
  • 但释放空间不等于立即物理清零数据文件。
  • 只要旧数据块没有被后续对象或新数据覆盖,数据文件中仍可能残留历史行内容,具备离线恢复的可能。

五、TRUNCATE 与 DROP 对比分析
#

通过前面的实验可以看出,TRUNCATEDROP 都会让数据在 SQL 层面不可见,但二者在对象保留、字典变化、段空间处理和恢复条件上存在明显区别。

对比项TRUNCATE TABLEDROP TABLE
SQL 查询结果表仍然存在,但数据行数变为 0表对象不存在,查询时报对象或视图不存在
表对象是否保留保留不保留
DBA_TABLES 中是否可见可见不可见
DBA_SEGMENTS 中是否可见通常仍可见不可见
DBA_EXTENTS 中是否可见可见,但只反映 TRUNCATE 后当前仍属于该表的 extent不可见
段头位置HEADER_FILEHEADER_BLOCK 通常保持不变表对象删除后,无法再通过表名查询段头
段空间变化BLOCKSEXTENTS 明显变小,说明段空间被重置或释放原表段映射关系被删除,空间被释放为可重新分配状态
是否逐行 DELETE
是否普通 ROLLBACK 可恢复
10002 trace 重点TRUNCTIMESF_TRUNC_TABLESP_DCT_TAB_DDL_REGCOMMIT删除 SYSOBJECTSSYSCOLUMNSSYSINDEXESSYSCONSSYSSTATSSYSDEPENDENCIESSYSGRANTS 等字典信息,并 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_
恢复难度相对较低,因为表结构仍然存在,字段顺序、数据类型等信息还可查相对较高,因为表对象和列定义等字典信息已经删除,需要依赖事前记录、备份、字典残留或人工重建表结构
恢复关键条件原数据块未被覆盖,表结构仍可识别原数据块未被覆盖,表结构、字段顺序、字段类型等元信息仍可获得或重建
本质概括保留对象,重置表段删除对象,释放段空间

综合来看,TRUNCATEDROP 的共同点是:二者都不是逐行删除,也都不能通过普通 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(*) = 0

6.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.DBFdm.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

这说明:

  1. TRUNCATE 并不会立即物理清零数据文件中的旧行内容。
  2. TRUNCATE 后,只要原数据块尚未被新对象或新数据覆盖,仍然有实际恢复可能。
  3. 相比 DROP,TRUNCATE 后表对象和字段定义

七、关于 dmdul:一个达梦离线数据抽取小工具
#

本文最后的 TRUNCATE 恢复实验使用的是我自己写的开源小工具:dmdul

项目地址:

https://github.com/greatfinish/dmdul

dmdul 的定位是一个面向达梦数据库的数据文件离线分析和数据抽取工具。它的目标不是替代数据库官方恢复手段,而是在数据库无法正常打开、表被误操作、或者需要分析 DBF 文件残留数据时,提供一种额外的离线排查和恢复思路。

目前 dmdul 主要支持:

功能说明
离线读取达梦数据文件基于 SYSTEM.DBFdm.ctl 和用户表空间 DBF 文件进行分析
解析数据字典可识别用户、表、列、视图、序列、触发器、权限等基础字典信息
查看用户和表支持 list userlist table <owner> 等交互式命令
导出表结构可以生成用户表的 DDL 脚本
导出表数据可以将识别到的数据导出为 INSERT SQL
尝试恢复 TRUNCATE、DROP 后数据在旧数据块未被覆盖的情况下,可尝试从数据文件中恢复被 TRUNCATE、DROP 的历史数据

在本文实验中,USERS1.T_TRUNCATE_TESTTRUNCATE 后,数据库 SQL 层面已经无法查询到原来的 100 行数据,但 dmdul 仍然成功从数据文件中恢复出完整的 100 行记录:

tables exported: 1
rows exported: 100
rows failed: 0

这说明,只要数据文件中的旧块没有被新数据覆盖,TRUNCATE 后的数据并不一定真的“消失”,仍然存在离线恢复的可能。

当然,任何离线恢复工具都不能保证所有场景 100% 成功。实际恢复效果取决于数据块是否被覆盖、表结构是否还能识别、字段类型是否支持、数据是否涉及 LOB、压缩、加密、分区等复杂场景。因此,建议优先使用数据库官方备份恢复手段,dmdul 更适合作为应急分析、实验验证和补充恢复工具。

如果你也对达梦数据库文件格式、误操作恢复、离线数据抽取感兴趣,欢迎关注、试用或一起完善这个项目:

https://github.com/greatfinish/dmdul

也欢迎提交 issue、建议和测试案例,让这个工具在更多真实场景中逐步完善。

八、最终结论
#

达梦数据库中,TRUNCATEDROP 都会让数据在 SQL 层面不可见,但并不等于立即物理清零数据文件;只要原数据块没有被覆盖,仍然可能存在离线扫描和恢复的机会。在没有可用备份、且原数据块尚未被覆盖的情况下,也可以尝试使用我的开源工具 dmdul 进行离线扫描和辅助恢复。