Oracle 空间回收踩坑实录:删了几百G数据,表空间却一点没降下来
做这行十年,DBA 或偏数据库运维的朋友应该都遇到过同一种诡异情形:一张大表按条件 DELETE 掉了几百 GB 的历史数据,业务侧反馈“空间已经清理完了”,你打开 dba_data_files 一看,数据文件大小纹丝不动,剩余空间一点没多出来。更头疼的是,你尝试用ALTER DATABASE DATAFILE ... RESIZE收缩文件,直接给你抛一个ORA-03297: file contains data in use beyond specified RBS high water mark,这就是 Oracle 空间回收里最经典的那个“回收不了”的坑。
这篇文章不聊理论书上的概念,就聊我最近处理的一个真实案例。背景很简单:一套生产库里的订单流水表,数据量从接近 800GB 删到了 120GB 左右,但表空间文件始终停在 810GB 下不去,磁盘告警一直在,业务又不敢重启。整条排查和处理的链路跑下来,我总结出的核心经验是:在 Oracle 里,“数据删了”和“空间能回收”之间,隔着一道叫高水位线(HWM)的墙,还有一堆看起来能回收但根本动不了的特殊表空间类型。下面我把整个排查过程、翻车细节、以及最后怎么把空间真正还给操作系统的完整操作步骤都写出来,希望能帮到正在被同类问题折磨的同行。这套经验同样适用于 Oracle 11g、12c、19c,逻辑基本一致。
1. 先给问题定性:空间“没回收”可能发生在两个层面
很多人一上来就执行RESIZE,报错后又一脸懵。我的习惯是先花十分钟搞清楚一件事:空间到底卡在哪一层。在 Oracle 里,空间回收通常涉及两个完全不同的层面,它们的处理手段甚至原理互相冲突。
1.1 第一步:确认表空间文件大小 vs 段内空间
第一个层面是“操作系统文件层”。你执行ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/PROD/users01.dbf' RESIZE 200G,这是想让 Oracle 把物理文件缩小,把空间还给操作系统。
第二个层面是“段内空间层”。表空间里的表、索引这些段对象内部,存在大量已经被标记为“空闲”但还没有被重新使用的数据块。这些空间 Oracle 自己知道是空的,可以给后续 Insert 用,但你没法直接把它从文件里抠出来还给操作系统。
我处理案例时的第一个动作,就是查这两层分别是什么情况:
-- 查看数据文件大小和当前使用情况 SELECT file_id, file_name, ROUND(bytes / 1024 / 1024 / 1024, 2) AS size_gb, ROUND((bytes - 1024 * blocks) / 1024 / 1024 / 1024, 2) AS used_gb FROM dba_data_files WHERE tablespace_name = 'USERS'; -- 查看表空间里还有多少“空闲”扩展区 SELECT file_id, ROUND(SUM(blocks) * 8 / 1024 / 1024, 2) AS free_mb FROM dba_free_space WHERE tablespace_name = 'USERS' GROUP BY file_id;注意这里dba_free_space统计的其实是段对象里已经释放、可以被重新分配的空间,也就是第二个层面的“段内空闲”。当时我看到的结果是:文件 810GB,dba_free_space里统计出来的空闲也有 400 多 GB。按常识理解,既然空闲这么多,文件为什么不能缩小?
这就牵出了ORA-03297的真相:数据文件末尾附近还残留着数据块(extent),Oracle 不允许你直接把文件尾巴切掉。一个数据文件可以理解成一条长长的街,你要把围墙往后移,但街尾还有几户人家没搬走,你当然动不了。实际生产里,表经过长时间的 DELETE 和 INSERT,那些剩余的数据块会像撒芝麻一样散落在文件的各个位置,文件尾部尤其容易出现“明明是空文件尾,但尾端前面一点还有段在占用”的情况。
1.2 ORA-03297 这条报错到底在说什么
这条报错很多人查文档都查得云里雾里,其实拆开看就一句话:你要把文件收缩到某个大小(RBS,即指定收缩后的 high water mark 位置),但文件里实际数据的使用位置超出了这个目标值。更准确说,在这个目标位置之后仍然存在未被释放的 extent。
我当时的处理是先用一个最长用的查询,看看这个文件里到底是哪些段对象在“阻挠”收缩:
SELECT owner, segment_name, segment_type, extent_id, ROUND(blocks * 8 / 1024, 2) AS extent_mb FROM dba_extents WHERE tablespace_name = 'USERS' ORDER BY file_id, block_id DESC;结果很典型:占着文件尾部的是几张明细表和几个索引,它们虽然已经删了大量数据,但段自身没有收缩过。这时候你基本可以断定,真正的问题出在第二个层面,也就是段对象的高水位线(HWM)没有降下来。顺着这个思路,解决方向就变成了:如何把段的 HWM 降下来,让文件的尾部彻底腾空。
2. 高水位线:DELETE 掉几十 G 空间却分毫未动的真凶
高水位线(High Water Mark,HWM)这个概念,没做过深入调优的 DBA 可能一直只停留在面试题层面。但凡是做过空间回收的人,都会被它狠狠上一课。
2.1 高水位线与低水位线的本质差异
Oracle 的表段里,数据块被分成三种状态:已经格式化并且曾经包含过数据的块(HWM 以下)、从未使用过的块(HWM 以上)、以及 HWM 以下但当前为空、可以复用的块。HWM 就是“这个段曾经插入数据到达过的最远位置”的标记。
我用一个接水杯的例子来解释,一张表就是一个水杯。你往里倒水(插入数据),水面会升到某个位置,这个位置就是 HWM。现在你把水倒掉一半(DELETE 数据),水面下降到某个位置,但杯壁上留下的水渍痕迹(HWM)还停留在原来最高的位置。Oracle 再往这个杯子里加水(新 INSERT),是先加到水渍痕迹以下那些已经空出来的区域,还是直接冲破水渍痕迹往上加?答案是:HWM 以下的空间会被优先复用,HWM 不会自动降低。
那“低水位线”又是怎么回事?在 ASSM(自动段空间管理)下,Oracle 对 HWM 以下的空间也不是一刀切管理,它内部还有一个“低高水位线”(low HWM)的概念。简单理解:段里有一些块是“完全没数据、但已经被格式化过”的块,这些块在低 HWM 和高 HWM 之间,Oracle 知道它们是空的,会优先分配它们。但整段空间的使用水位,依然以高 HWM 为准。
DELETE操作只负责把行数据清掉并标记块为可用,它永远不会去移动 HWM。所以在 Oracle 的视角里,段占用的空间依然那么大,数据文件自然一点都让不出来。
2.2 判断 HWM 上方有多少“可回收空间”
处理案例时我用了 Oracle 官方提供的过程DBMS_SPACE.SPACE_USAGE,它可以精确查出一个段在当前表空间管理模式下已经用到高水位线的程度。这个包在 9i 以后就有,很多 DBA 反而不知道用。
SET SERVEROUTPUT ON DECLARE v_fs1 NUMBER; v_fs2 NUMBER; v_fs3 NUMBER; v_fs4 NUMBER; v_fs5 NUMBER; v_fs6 NUMBER; v_fs7 NUMBER; v_fs8 NUMBER; v_full_blocks NUMBER; v_unformatted_blocks NUMBER; BEGIN DBMS_SPACE.SPACE_USAGE( 'APP', 'ORDER_HIS', 'TABLE', NULL, v_fs1, v_fs2, v_fs3, v_fs4, v_fs5, v_fs6, v_fs7, v_fs8, v_full_blocks, v_unformatted_blocks ); DBMS_OUTPUT.PUT_LINE('FS1(0-25% free): ' || v_fs1); DBMS_OUTPUT.PUT_LINE('FS2(25-50% free): ' || v_fs2); DBMS_OUTPUT.PUT_LINE('FS3(50-75% free): ' || v_fs3); DBMS_OUTPUT.PUT_LINE('FS4(75-100% free): ' || v_fs4); DBMS_OUTPUT.PUT_LINE('FULL blocks: ' || v_full_blocks); DBMS_OUTPUT.PUT_LINE('UNFORMATTED blocks: ' || v_unformatted_blocks); END; /执行完这个脚本,我当时的数字现在还记得:这张表有大量块处于 FS4 状态(75% 以上空间空闲),说明 DELETE 之后块内留下来的空位置多到离谱,但这些空间还是属于这个段的。它们可以被后续 INSERT 复用,却不能直接还给操作系统。
还有一个更简单的判断方式,统计一下表的行数和实际占用,如果逻辑读远大于应有的块数,基本就是 HWM 太高导致的空块太多。不过生产系统别随便做全表扫描,我只在低峰期跑了一次。
2.3 为什么 DELETE 完后看不到效果
到这里你应该明白一个扎心的事实:DELETE 本身根本不是空间回收的手段,它只是“制造可用空间”的手段。真正能实现空间回收的操作只有三种:TRUNCATE、SHRINK SPACE、ALTER TABLE MOVE。TRUNCATE 会直接重建段并把 HWM 拉回零,但它也会清掉全部数据、无法带条件,生产环境大部分场景用不了。
所以 DELETE 完之后你看数据文件大小,它当然纹丝不动,因为 HWM 还停在那儿,段还认为自己占着那么多地盘。我经常跟同事说的一句话是:“DELETE 只是把桌子上的文件收进了抽屉,但桌子还是占着那么多地方。” 你要让桌子变小,得把抽屉里的东西也清掉或者换个桌子——对应到 Oracle,就是下面要说的 SHRINK 和 MOVE。
3. 在线方案:SHRINK SPACE 的正确姿势与翻车风险
SHRINK 是 Oracle 10g 开始提供的段收缩功能,它号称能在不锁表、不影响在线业务的前提下降低 HWM。听起来很美,用起来坑也不少。
3.1 SHRINK 的原理和两个硬性前提
原理上说,SHRINK 分两步走:先在段内部把数据行尽量往 HWM 以下的块里挪动,把空块腾出来;然后 Oracle 直接移动 HWM 到新的位置,那些 HWM 以上的空块就从“段占用的空间”变成了“表空间的空闲空间”。这一步做完,dba_free_space里立马会多出一大批空间,数据文件再收缩就有希望了。
但 SHRINK 有两个硬性前提,缺一个都跑不起来:
- 表空间必须使用 ASSM 管理。也就是创建表空间时要写
SEGMENT SPACE MANAGEMENT AUTO。如果是手动段空间管理(MSSM),SHRINK 直接提示ORA-10635: Invalid segment or tablespace type,因为 SHRINK 依赖 ASSM 的位图状态来定位空块。 - 表本身要开启 ROW MOVEMENT。SHRINK 在块间移动数据行,行的物理地址(rowid)会变化,Oracle 为了安全默认不让你动。必须先执行
ALTER TABLE ... ENABLE ROW MOVEMENT。
第二个前提很多人会犹豫,担心开启 ROW MOVEMENT 会影响业务。实际上这个属性只影响基于 rowid 的查询和物化视图,绝大多数 OLTP 业务不会直接依赖 rowid,开启后并不影响正常 SQL。你只需要让业务方知道这件事,并且知会一下有触发器、依赖 rowid 的极端场景需要额外评估。
3.2 具体操作命令与执行顺序
我当时的操作顺序是这样写的:
-- 1. 开启行移动 ALTER TABLE APP.ORDER_HIS ENABLE ROW MOVEMENT; -- 2. 收缩段,CASCADE 会连同表上的索引一起收缩 ALTER TABLE APP.ORDER_HIS SHRINK SPACE CASCADE; -- 3. 如果空间还是不够,可以把段一次压到最小 ALTER TABLE APP.ORDER_HIS SHRINK SPACE COMPACT;多提一句CASCADE。它会顺带把该表上的普通索引也做收缩,避免你收完表空间,却发现索引的 HWM 没降。但 CASCADE 也意味着所有索引上的行会移动,收缩过程中会产生大量 UNDO 和 REDO,时间也会明显变长。生产环境我的建议是:先只做表收缩,不要带 CASCADE,等表收缩完后重建大索引,这样可以控制每一步的风险。
执行过程中可以从另一个会话观察 V$SESSION_LONGOPS,看收缩任务到哪个阶段了。这步很关键,因为大表的 SHRINK 不是秒级完成的,跑十几分钟很正常,你不盯着心里不踏实。
3.3 翻车风险:函数索引 ORA-10631 和 UNDO 膨胀
SHRINK 最经典的翻车点是:表上存在函数索引(基于函数的索引,如SUBSTR(column, 1, 10)这种)。只要表上有函数索引,执行 SHRINK 大概率会得到:
ORA-10631: SHRINK clause should not be specified for this object为什么?因为函数索引的索引条目是根据函数计算结果生成的,SHRINK 移动行时无法高效维护这种非普通索引结构。Oracle 干脆一刀切:函数索引的表不允许 SHRINK。我那次碰到的表恰好就有个TO_CHAR(create_date, 'YYYYMMDD')的函数索引,第一次执行就撞上了这个错。
两个选择:要么先 drop 函数索引再 SHRINK,回头再重建;要么直接跳过 SHRINK 走下面的 MOVE 方案。因为函数索引往往是为了特定报表存在的,drop 和重建需要业务审批,我那次选了后者。
还有一个容易忽略的问题:SHRINK 过程中数据行大量移动,UNDO 表空间会猛涨。它不是在线 DDL 那种纯字典操作,而是实打实的数据搬移,每一行被挪动都会记录 UNDO。如果你的 UNDO 表空间本来就紧张,SHRINK 可能把 UNDO 撑爆,报出ORA-30036: unable to extend segment by ... in undo tablespace。这属于“为了回收空间反而制造空间危机”的典型事故场景。
3.4 收缩完之后如何验证结果
收缩完成后别急着去 RESIZE 数据文件,先确认两件事:段变小了没有、文件尾部还有没有对象占位。
-- 对比收缩前后的段大小 SELECT segment_name, ROUND(bytes / 1024 / 1024 / 1024, 2) AS size_gb, blocks, extents FROM dba_segments WHERE segment_name = 'ORDER_HIS' AND owner = 'APP';如果段的大小已明显下降,再回文件层面看dba_free_space,你会发现空闲空间也成倍增加了。此时从文件头部开始连续的空闲 extent 已经能覆盖你要收缩到的目标位置,RESIZE 的条件才真正具备。
提醒一下:SHRINK 后建议顺手做一个
ALTER TABLE ... MOVE之外的表分析,也就是DBMS_STATS.GATHER_TABLE_STATS,因为 SHRINK 会改变段内的物理分布,统计信息里关于块数的数据已经不准了,不重新收集会让 CBO 走错执行计划。
4. 离线方案:ALTER TABLE MOVE 的时序细节与重建索引
如果 SHRINK 因为函数索引走不通,或者表特别大、SHRINK 时间太长,那就只能用传统手艺ALTER TABLE MOVE了。这个方案本质上是把表整个重写一遍到新的段里,新段的 HWM 自然是从零开始的,空间回收效果立竿见影,但它离线,要考虑的影响面比 SHRINK 大得多。
4.1 MOVE 和 SHRINK 怎么选
我根据自己的实操体会,把两者放到一张表里对比,选型时直接照着看:
| 对比项 | SHRINK SPACE | ALTER TABLE MOVE |
|---|---|---|
| 在线性 | 在线,不阻塞 DML | 离线,执行期间表不可写 |
| 是否能带条件 | 不能,只能整表 | 不能,只能整表 |
| 函数索引 | 不支持,ORA-10631 | 支持,但重建索引时要想办法 |
| 索引维护 | CASCADE 时自动 | 索引全部失效,必须重建 |
| 空间需求 | 需要少量 UNDO | 需要额外一个完整段大小的空间 |
| 执行速度 | 相对慢,逐块搬 | 相对快,但会锁表 |
| 对统计信息影响 | 需要重收 | 需要重收 |
结论很简单:在线要求高、空间余量充足、没有函数索引的表用 SHRINK;能接受停机、或 SHRINK 遇到硬性阻碍的表用 MOVE。千万别在业务高峰期做 MOVE,哪怕你觉得自己操作很快,一个几 GB 的表复制起来也要好几分钟,这期间所有涉及这张表的应用都会卡死或报错。
4.2 完整操作步骤与索引重建的节奏
我当时对另一张大表PAY_LOG执行了 MOVE,具体操作是这么做的:
-- 1. 记录当前表上的所有索引(后面要重建) SELECT index_name, index_type, tablespace_name FROM dba_indexes WHERE table_owner = 'APP' AND table_name = 'PAY_LOG' ORDER BY index_name; -- 2. 执行表搬迁,这里直接指定搬到另一个空闲表空间 ALTER TABLE APP.PAY_LOG MOVE TABLESPACE DATA_BIG; -- 3. 检查索引状态(此时应该全部 UNUSABLE) SELECT index_name, status FROM dba_indexes WHERE table_owner = 'APP' AND table_name = 'PAY_LOG'; -- 4. 重建所有索引 ALTER INDEX APP.PK_PAY_LOG REBUILD; ALTER INDEX APP.IDX_PAY_LOG_01 REBUILD;注意第 2 步我特意指定了TABLESPACE DATA_BIG,也就是说把表搬到了另一个空间更大的表空间。这是 MOVE 方案很关键的一个技巧:如果你在原表空间内 MOVE,Oracle 需要同时存在新旧两个段,空间稍微不够就会ORA-01658: unable to create INITIAL extent for segment in tablespace。换到一个空间富裕的表空间,相当于把收缩问题转移成了“搬个家”,旧表空间里的原段自然空出来一大片,直接 RESIZE 旧文件就行了。
步骤 4 里重建索引也是有好几个坑的。如果你索引很多、很大,全部重建会花很久,最稳妥的做法是重新创建而不是REBUILD——REBUILD 会在原表空间里新建索引段,如果原表空间已经快满了也可能失败;而CREATE ... INDEX ... TABLESPACE DATA_BIG可以把新索引放到目标表空间,顺便完成索引的物理碎片整理。
4.3 MOVE 过程中最容易忽视的三个小毛病
第一个毛病是MOVE 会改变表的物理布局,但没有自动更新统计信息。上面提到的表分析,MOVE 之后务必执行一遍,否则 CBO 可能基于旧的块数信息做出错误的全表扫描判断。
第二个毛病是MOVE 之后的索引名别搞混。如果表上有主键约束,索引名往往是系统自动生成的那种SYS_C00xxxxx,重建时要先查询出真实索引名,而不是猜。我当时就差点把一个SYS_IL...大对象索引当成普通索引重建,那玩意儿根本不能手动 REBUILD。
第三个毛病是不要忘记处理 LOB 字段。如果表里带 BLOB/CLOB 字段,ALTER TABLE MOVE默认只移动表段,LOB 段是原地不动的。你要用MOVE ... LOB (col_name) STORE AS (TABLESPACE DATA_BIG)这种写法把 LOB 段也一起搬走,否则空间照样收不干净。很多 DBA 在这上面吃过亏,表大小从 800GB 变成 10GB,但表空间文件还是 600GB,一查全是 LOB 段占着位置。
5. 临时表空间与 UNDO 的回收:另一个层面的空间坑
主表的问题解决后,我顺手检查了系统里的其他表空间,发现还有两个“空间回收困难户”——临时表空间(TEMP)和 UNDO 表空间。这俩和普通数据表空间完全两码事,处理不好同样会把你折腾到崩溃。
5.1 TEMP 表空间为什么也卡住
临时表空间是 Oracle 用来做排序、哈希连接、临时表数据存的区域。很多系统里 TEMP 文件设了几十上百 GB,运行一段时间发现它占满磁盘了,你想ALTER DATABASE TEMPFILE ... RESIZE收缩,同样会遇到报错——因为临时文件里还有活动的临时段,或者即便没有活动会话,临时段空间也没有被完整释放回文件尾部。
我的处理流程是这样:
-- 1. 查看当前有哪些会话正在使用临时段 SELECT se.sid, se.username, se.tablespace, se.segtype, se.blocks * 8 / 1024 AS mb FROM v$sort_usage se ORDER BY se.blocks DESC; -- 2. 没有活动会话后,直接收缩临时表空间 ALTER TABLESPACE TEMP SHRINK TEMPFILE '/u01/app/oracle/oradata/PROD/temp01.dbf';ALTER TABLESPACE ... SHRINK TEMPFILE是 11g 以后才有的功能,专门用来收缩临时表空间文件。如果等不及、或者收缩效果不理想,也可以重建临时表空间:新建一个小的 TEMP 表空间,把数据库默认临时表空间切过去,drop 掉旧的。这套操作比普通表空间简单,因为临时表空间里的东西本身不需要持久化,随便搬,但要注意别在业务时间做——切换默认临时表空间会让正在运行的排序操作使用到新建的临时表空间,如果你新文件设太小,反而会引发ORA-01652: unable to extend temp segment。
5.2 UNDO 表空间越界收缩的骚操作
UNDO 表空间更阴险。它不是按数据文件里的 extent 空不空来判断能否收缩的,而是受UNDO_RETENTION控制。Oracle 希望保留足够多的 UNDO 来保证一致性读,所以即便没有活动事务,它也可能觉得自己还“需要”那么大的 UNDO 文件。更坑的是,从 10g 开始 Oracle 会自动调整 UNDO 保留时间(TUNED_UNDORETENTION),你手动设的UNDO_RETENTION=900没什么用,它实际会按查询时长自动调大,于是 UNDO 表空间只涨不缩。
我当时的情况:UNDO 文件 120GB,dba_free_space 显示空闲 90GB,但 RESIZE 就是不让过。查V$UNDOSTAT后看到TUNED_UNDORETENTION被自动拉到了几千万毫秒(等于 Oracle 认为要保留好几个小时的历史数据),只能按下面的步骤强制收缩:
-- 1. 建一个小的新 UNDO 表空间 CREATE UNDO TABLESPACE UNDO_NEW DATAFILE '/u01/app/oracle/oradata/PROD/undo_new01.dbf' SIZE 10G AUTOEXTEND ON; -- 2. 切换默认到新表空间 ALTER SYSTEM SET UNDO_TABLESPACE = UNDO_NEW; -- 3. 等现有事务跑完后,drop 掉旧 UNDO 表空间 DROP TABLESPACE UNDO_OLD INCLUDING CONTENTS AND DATAFILES;这套操作里的关键点是:先用新表空间顶替旧表空间,旧 UNDO 表空间才能被整体 drop。你没法直接在原文件上把它缩到很小,因为 Oracle 不允许你在当前不用的 UNDO 表空间上做太激进的缩小。切换之后如果还担心文件自动增长,记得关掉新表空间的 AUTOEXTEND,或者设一个 MAXSIZE 上限,避免过几个月又涨到满。
6. 复盘整个案例:从接到告警到空间落袋的完整链路
最后做一次复盘,把整个案例从开始到收尾的完整链路串起来,也给后面的空间维护提几个实用建议。一条链路走完,你大概会理解为什么所有人都说“Oracle 空间回收不是删数据那么简单”。
6.1 本次处理的完整时间线
我这次实战的处理顺序,严格来说是下面这样:
- 接到磁盘告警,先查
dba_data_files+dba_free_space,确认空间卡在 USERS 表空间。 - 试图 RESIZE 直接撞上
ORA-03297,再查dba_extents定位是哪些段占着文件尾部。 - 对占尾部的目标表
ORDER_HIS用DBMS_SPACE.SPACE_USAGE判断 HWM 以上空闲块比例,确认是典型的 DELETE 后 HWM 不降问题。 - 方案一执行 SHRINK,因为函数索引撞上
ORA-10631,放弃。 - 方案二对
ORDER_HIS做 MOVE,提前规划好目标表空间 DATA_BIG,MOVE 后重建全索引并收集统计信息。 - 对另一张
PAY_LOG直接走 MOVE,顺带处理了 LOB 段。 - 原 USERS 表空间文件尾部被腾空,重新执行 RESIZE 到目标大小,空间真正还给了操作系统。
- 顺手检查 TEMP 和 UNDO,TEMP 用
SHRINK TEMPFILE解决,UNDO 通过新建/切换/drop 三个动作完成收缩。
整套下来大概花了不到四个小时,如果一开始就闷头 RESIZE,可能折腾一天还在原地打转。
6.2 日常应该怎么盯空间,避免再次走到回收这一步
经历过这次之后,我给自己的维护清单里加了几条强制项:
- 每周做一次段大小排序:查
dba_segments按字节排序,找出增长最快的前十大对象,而不是只看表空间总体使用率。很多空间问题都是“表空间没满,但某个段已经大得离谱”。 - 对大表设置定期归档策略:核心流水表按月分区的话,直接
DROP PARTITION或TRUNCATE PARTITION是低成本的,远好过 DELETE 之后做 SHRINK/MOVE。分区表才是大表数据清理的正道,没有分区的表早晚要被 HWM 卡死。 - 监控 RESIZE 可行性:每次清理完大量数据后,第一时间跑一个
ALTER DATABASE DATAFILE ... RESIZE的预检查,不要等磁盘满了才动手。
6.3 不要“没事就收缩”的忠告
最后一条建议可能会让某些人意外:空间够用的话,不要频繁对表做 SHRINK 或 MOVE。这两种操作本质上都会消耗大量 I/O、UNDO、REDO,还会让热数据在缓存中的命中率下降。收缩一时爽,之后积累的碎片和统计信息滞后可能让业务查询变慢。空间回收是清理动作的收尾,是“不得不做”的修复操作,而不该成为日常习惯。
拿我们这个例子来说,如果一开始表就设计成按月份分区,历史分区到期直接 drop,空间当天就释放了,根本不需要 MOVE。所以真正治本的方案,永远是设计层面的前瞻性。作为一个常年和数据文件打交道的 DBA,我现在看到一张大表还在用 DELETE 清理历史,都会多嘴问一句:你的表分区了吗?这句话,比任何 SHRINK 脚本都值钱。