🏺 第18章 Oracle 的闪回技术
📄 一句话概括

DROP TABLE 在 Oracle 里默认不删数据,只把表挪进回收站。 所以 drop 之后还有一次后悔的机会。 回收站里的东西照样占表空间。 只 drop 不 purge,盘一点都不会松。

📝 本章和前面几章的关系

18.3 处理的是「行被改错了」。这一篇处理的是「整张表被删了」。 两者的依据完全不同:一个靠 undo,一个靠回收站。 所以闪回丢弃不受 undo_retention 限制——它能退多久,只取决于你有没有 purge。

一、回收站是什么

它不是一张表,也不是一个目录。 它是「被 drop 的对象在表空间里留下的那些段」。 DROP TABLE emp 做了三件事: ① 把段名从 EMP 改成 BIN$...; ② 在数据字典里记一条「它原来叫什么」; ③ 从你的 schema 里摘掉这个名字。 数据块一个都没动。 这就是为什么 drop 一张 100 GB 的表是秒级操作,而 delete 全表要跑几小时。 看回收站有两条路:

SHOW RECYCLEBIN;

SELECT object_name, original_name, type, droptime,
       can_undrop, can_purge, space
  FROM USER_RECYCLEBIN;

SPACE 那一列是它占的块数。 这个视图在 DBA 角度看叫 DBA_RECYCLEBIN,多一列 OWNER。 CAN_UNDROP 和 CAN_PURGE 两列很实用: 表空间做过字典管理的迁移、或者段被部分覆盖,前者会变成 NO,那时候就闪回不了了。

二、drop 之后发生了什么

最容易踩的一个细节:原名立刻被释放。 所以 drop 完马上可以建一张同名表,两张表不冲突。 这就带来一个坑:回收站里可以同时有好几个「emp」。 这时候 FLASHBACK TABLE emp TO BEFORE DROP 会报错——它不知道你要哪一个。 两种指定方式:

-- 按 BIN$ 名精确指定
FLASHBACK TABLE "BIN$abc123==$0" TO BEFORE DROP;

-- 或者闪回时改名
FLASHBACK TABLE emp TO BEFORE DROP RENAME TO emp_bak;

不写 RENAME TO 时,它默认恢复成 ORIGINAL_NAME。 所以如果原名已经被占了,还是会撞。 实践里推荐永远写 RENAME TO。 理由很实在:你要恢复的多半是「旧数据」,而原名上很可能已经有一张「新表」在跑业务。 先恢复成 _bak,比对完再决定谁留,比直接覆盖安全得多。

三、闪回表到 drop 之前

命令就是上面那条,但有几个细节值得单独说。 ① 恢复的是「drop 那一刻」的样子,不是更早。 所以它救不了「上周误删了一批行」——那是 undo 的活儿。 ② 索引、约束、触发器会一起回来。 因为它们也是被 drop 的独立对象,都在回收站里。 ③ 依赖它的视图、存储过程不会回来。 那些对象在 drop 表的时候被标成 INVALID 了,表恢复之后要重新编译。 ④ 外键约束要小心。 如果别的表上有指向它的外键,drop 的时候会被一起删掉,恢复表不会自动恢复外键。 第 ④ 条最容易出事。 表回来了、数据也回来了,你以为搞定了, 结果某个关联表插数据时不再校验——数据一致性从这里开始烂。 所以恢复之后必做一件事:查一遍约束在不在。

SELECT table_name, constraint_name, constraint_type, status
  FROM USER_CONSTRAINTS
 WHERE table_name = 'EMP';

四、purge:把后悔的机会交出去

PURGE 才是真删。它有四种用法,区别在「删哪个范围」。

写法 删什么
PURGE TABLE emp 回收站里那个叫 emp 的
PURGE INDEX idx_name 回收站里的索引
PURGE RECYCLEBIN 当前用户回收站里的全部
PURGE TABLESPACE ts USER u 某表空间里某用户的

还有一种「跳过回收站」的写法,在建表或删表时用:

DROP TABLE emp PURGE;              -- 直接删,不进回收站

CREATE TABLE t (...) PURGE;        -- 以后 drop 它都不进回收站

第二行那个 PURGE 在建表语句里,容易被忽略。 它的意思是「这张表以后被 drop 时,别进回收站」。 用在临时表、中间表上很合适——那些表本来就不需要后悔的机会。 注意 TRUNCATE 和 DROP ... PURGE 还不一样。 TRUNCATE 保留表结构、清空数据,连回收站都不进; DROP ... PURGE 连表一起没了。 两者共同点是:都没有后悔药。

五、回收站也会撑爆表空间

这是最容易被忽略的运维问题。 一个系统每天 drop 一批中间表,跑了半年。 表空间一路涨,DBA 一直加数据文件,加到没地方加。 最后发现:涨的全是回收站。 怎么防?三条: ① 临时表、中间表建的时候带 PURGE。 ② 定期 PURGE RECYCLEBIN,或者至少监控它的总量。 ③ 给回收站设个上限——不过这个只能靠表空间配额间接控制, Oracle 没有「回收站最多占多少」这个参数。 监控语句:

SELECT SUM(space) * 8192 / 1024 / 1024 AS mb
  FROM USER_RECYCLEBIN;

还有一点要说:表空间真的紧张时,Oracle 会自己清回收站。 顺序是先清最老的。 所以「回收站里的东西一定还在」这个假设不成立——压力大了它自己就没了,而且不通知你。 结论很直接:把回收站当「顺手留的一道保险」,别当「正式的备份手段」。

六、动手 Demo:用真实文件把「挪进回收站」和「挪回来」跑一遍

这一篇的 Demo 全是真的文件操作:shutil.move、os.remove、目录大小统计。 因为「drop 不删数据」这件事,本质就是文件挪了个位置——用文件系统演一遍,比任何比喻都直观。

import os, shutil, tempfile, sqlite3
T = tempfile.gettempdir()
R = os.path.join(T, "recycle_demo")
if os.path.exists(R):
    shutil.rmtree(R)
os.makedirs(os.path.join(R, "RECYCLEBIN"))

p = os.path.join(R, "emp.db")
c = sqlite3.connect(p)
c.execute("create table t (id int primary key, v text)")
c.executemany("insert into t values (?,?)",
              [(i, "v%05d" % i) for i in range(1, 5001)])
c.commit()
c.close()
print("建表后 %.2f MB" % (os.path.getsize(p) / 1048576.0))

# DROP TABLE:文件挪进回收站,数据没删
bn = os.path.join(R, "RECYCLEBIN", "BIN$0001==$0.db")
shutil.move(p, bn)
print("DROP 之后原文件在吗: %s,回收站里 %d 个文件"
      % (os.path.exists(p), len(os.listdir(os.path.join(R, "RECYCLEBIN")))))

# FLASHBACK TABLE ... TO BEFORE DROP:挪回来
shutil.move(bn, p)
c = sqlite3.connect(p)
print("闪回之后读到 %d 行"
      % c.execute("select count(*) from t").fetchone()[0])
c.close()
print("盘上还是 %.2f MB,drop 不释放空间"
      % (os.path.getsize(p) / 1048576.0))

实跑输出:

--------------------------------------------------------------------
第八十二课:闪回丢弃——drop 之后还有一次后悔的机会
--------------------------------------------------------------------

  **`DROP TABLE` 在 Oracle 里默认【不删数据】,只是把表挪进回收站。**
  **这一课用真实文件把「挪进去」和「挪回来」跑一遍,顺便量占多少盘。**

  一、建三张表,量原始大小
  --------------------------------------------------------------------

  表名    行数  文件大小
  ----------------------
  emp82   20000 0.66 MB
  dept82  5000  0.18 MB
  bonus82 3000  0.11 MB
  已选择 3 行
  **三个文件合计 0.95 MB。**

  二、DROP TABLE:数据挪进回收站
  --------------------------------------------------------------------

  回收站里的名字    原名    DROP 时 SCN 大小
  ---------------------------------------------
  BIN$000BC2==$0.db emp82   3010        0.66 MB
  BIN$000BCC==$0.db dept82  3020        0.18 MB
  BIN$000BD6==$0.db bonus82 3030        0.11 MB
  已选择 3 行
  **三张表都不见了,但盘上那份数据还在:回收站占 0.95 MB。**
  **名字变成 `BIN$...` 是重点**——原名被释放了,你可以立刻建一张同名表。
  **在 SQL*Plus 里查 `USER_RECYCLEBIN`(或 `SHOW RECYCLEBIN`)就能看到这四列。**

  三、重名:回收站里可以有多个同名对象
  --------------------------------------------------------------------
  **现在回收站里叫 `emp82` 的有 2 个。**
  **这时候直接 `FLASHBACK TABLE emp82 TO BEFORE DROP` 会报错**,因为它不知道你要哪一个。
  **要么用 `BIN$` 名,要么加 `RENAME TO emp82_old`。**

  四、FLASHBACK TABLE ... TO BEFORE DROP:挪回来
  --------------------------------------------------------------------
  **闪回 SCN 3010 那一份:挪回原名,读到 20000 行。**
  **注意恢复的是【DROP 那一刻】的样子,不是更早。**
  **所以回收站救不了「上周误删了一批行」——那是 undo 的活儿。**

  五、PURGE:真的删掉
  --------------------------------------------------------------------
  **上一步闪回拿走了 0.66 MB,所以现在回收站只剩 0.32 MB。**
  **`PURGE RECYCLEBIN` 之后:从 0.32 MB 降到 0.00 MB。**
  **放掉 0.32 MB。这些数据再没有任何办法找回来,只能靠备份。**

  操作                 数据在哪 还能闪回吗
  ------------------------------------------------
  DROP TABLE           回收站   能
  DROP TABLE ... PURGE 没了     不能
  PURGE RECYCLEBIN     没了     不能
  TRUNCATE TABLE       没了     不能(不进回收站)
  已选择 4 行
  **最后一行最容易吃亏:`TRUNCATE` 不写 undo、不进回收站,它和 `DELETE` 完全是两件事。**
  **空间账也要说清:drop 之后空间【不释放】。要释放得 purge,或者等表空间压力大时被自动挤掉。**

第一张表:三张表 20000/5000/3000 行,文件 0.66/0.18/0.11 MB,合计 0.95 MB。 这个数是后面所有对比的基线。 第二张表是 drop 之后的样子。 注意四列:名字变成 BIN$000BC2==$0.db 这种,原名还在,SCN 记着是什么时候 drop 的, 大小一点没变。 「大小一点没变」这句是重点。 drop 之后盘上那份数据完好无损,只是名字换了、并且从你的 schema 里摘掉了。 第四段是闪回:从 BIN$000BC2==$0.db 挪回 emp82.db,读到 20000 行。 注意恢复的是 SCN 3010 那一份,也就是【drop 那一刻】的样子。 第五段那个对比最能说明问题:purge 之前回收站 0.32 MB,purge 之后 0.00 MB。 放掉的这 0.32 MB 再没有任何办法找回来。 最后那张四行表要背下来。 只有第一行「DROP TABLE」还能闪回,其余三种(包括 TRUNCATE)都是真没了。 还有一个数字容易被忽略:闪回之后「盘上还是 0.66 MB」。 说明整个过程磁盘占用从来没变过——drop 不释放空间,闪回也不额外占空间。 边界要说清:真实 Oracle 的回收站是「表空间里的段」,不是文件。 这里用文件模拟,能演对「挪位置」这层语义,但演不出「段被新数据覆盖」这种情况。

30 秒实验

  1. 把回收站目录改成两层(按日期分目录),再跑一次。 体会一下 Oracle 为什么不需要这么做——它靠数据字典记元信息,不靠目录结构。
  2. 在 shutil.move(p, bn) 之后再加一次 shutil.move,把第二个同名表也挪进去。 看闪回时怎么区分两个 BIN$ 名。
  3. 把 os.remove(bn) 换成「先写满一个新文件再删」, 看文件系统层面「删除」到底做了什么——这也是 Oracle PURGE 和「数据被覆盖」的差别。

常见误区

  1. 「DROP TABLE 会把数据删掉」。 默认不删,只改段名、记字典,数据块一个不动。
  2. 「drop 之后空间就释放了」。 不释放,要 PURGE 或者等表空间紧张时被自动挤掉。
  3. 「回收站能找回上周误删的行」。 不能。它只保存 drop 那一刻的整表快照,行级的要靠 undo。
  4. 「恢复表之后一切照旧」。 外键约束不会自动恢复,视图和存储过程要重新编译。
自测三问(点开对答案) 1. `DROP TABLE` 到底删了什么? —— **只删了「名字」和「字典里的登记」。** 段名改成 `BIN$...`, **数据块原封不动,这就是它能秒级完成的原因。** 2. 回收站里有两个同名表,怎么闪回指定那一个? —— **用 `BIN$` 名精确指定**,或者**`FLASHBACK TABLE emp TO BEFORE DROP RENAME TO emp_bak`。** 3. 为什么建议闪回时总带 `RENAME TO`? —— **因为原名多半已经被新表占了。** 先恢复成 `_bak`、**比对完再决定谁留,比直接覆盖安全。** 4. 只 drop 不 purge,表空间会怎样? —— **一直涨,一点不松。** 而且表空间真紧张时, **Oracle 会自己从最老的开始清回收站,还不通知你。**

下一篇收尾:闪回版本查询、闪回事务查询、闪回数据归档。 前两个是「查清是谁改的、怎么改回去」,第三个是「把历史存到 undo 管不到的地方」。

相关笔记