前事不忘,后事之师,不忘国耻!

 用户注册  找回密码
 用户注册
搜索
查看: 20|回复: 0

[开发应用] Oracle 表空间高水位与空间回收实战:从 delete 后空间不释放到 SHRINK 与数据文件 RESIZE

[复制链接]

[开发应用] Oracle 表空间高水位与空间回收实战:从 delete 后空间不释放到 SHRINK 与数据文件 RESIZE

[复制链接]
dbaai

主题

0

回帖

409

积分

DBAAI

积分
409
12 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?用户注册

×
一、具体的问题

上周有位同行在群里问了一句:一张日志表按保留策略删了 4 亿行,dba_segments 里还是 420GB,一点没动;更奇怪的是删完之后那条 select count(*) 反而没变快,逻辑读跟删之前几乎一样。他已经在表上建了索引、也跑了统计信息,觉得"该做的都做了"。

这不是"删了但没提交"那种低级问题,也不是回收站没清。真实原因几乎总是同一个:DELETE 只把行标记成空闲,不动段的高水位线(HWM),而全表扫描读的是高水位线以下的所有块,包括那些已经空了、但还挂在段里的块。

我在 11.2 和 19c 上都反复验证过这件事:delete from t; commit; 之后 dba_segments.blocks 一个数都不会变,dba_tables.blocks(统计信息里的块数)要等重新收集统计信息才会变成接近 1,而这两者之间的差,就是被"浪费掉"的空间。代价是实打实的——备份要多备这 400GB 的脏块,RMAN 增量要扫它,全表扫描要多读几十万个块,count(*) 走索引还要读满整棵索引树。

所以这篇只解决一件事:已经 delete 掉的空间,怎么真正还给表空间,并且把数据文件也缩回去。

二、核心原理

先把三个层次分清楚,很多操作失误都来自把这三层混成一件事。

第一层:块内的空闲。 行被删除后,块内的空间被标记为空闲(ASSM 下体现为 FS1~FS4),后续 INSERT 可以复用。这一层是自动的,不需要任何人工动作。所谓"碎片",在这一层其实不是问题。

第二层:段的高水位线(HWM)。 高水位线是段内曾经被格式化使用过的最高位置。全表扫描会从段头一直读到 HWM,HWM 以下哪怕整块都是空的,也要读。DELETE 永远不降 HWM,TRUNCATE 才降(它把 HWM 直接重置到段头之后),MOVE 和 SHRINK 也降。这就是"删了不释放"的全部秘密——不是空间没还,是没人去降这条线。

第三层:数据文件的块。 段的高水位线降下来之后,表空间里会出现大量连续空闲区,但数据文件本身不会自动缩小,dba_data_files.bytes 依然是那么大。要让操作系统真正拿回磁盘,还得 RESIZE 数据文件,而能不能缩、缩到多少,取决于这个文件里最高的那个块在哪。

还有一个容易被忽略的第四个对象:索引段和 LOB 段是独立的段,各有各的 HWM。 表 shrink 了,索引不会自己瘦;表 shrink 了,LOB 段也不会自己瘦(除非加 CASCADE 或者单独对 LOB 做 shrink)。大量日志表真正的空间黑洞往往就在 LOB 段上。

把这三层记住,后面所有命令就都能对上号了:SHRINK/MOVE 解决第二层,RESIZE 解决第三层,REBUILD/COALESCE 解决索引段的高水位线,CASCADE 和 MODIFY LOB 解决 LOB 段。

三、实例参考(动手步骤)

下面是一套可以直接照做的流程,示例表为 APP.T_ORDER_LOG,表空间 APP_DATA,环境 Oracle 19c(11.2 同样适用,差别我会标注)。

步骤 0:确认现状基线
  1. -- 段级真实大小与 HWM 块数
  2. SELECT segment_name, segment_type, blocks, bytes/1024/1024 AS mb, extents
  3.   FROM dba_segments
  4. WHERE owner='APP' AND segment_name='T_ORDER_LOG';
  5. -- 统计信息里的块数(滞后,但能看出优化器认为表有多大)
  6. SELECT num_rows, blocks, avg_row_len
  7.   FROM dba_tables WHERE owner='APP' AND table_name='T_ORDER_LOG';
复制代码

dba_segments.blocks 就是 HWM 对应的块数(乘 8KB 得到段占用的字节)。如果 blocks 是 5200 万、而 num_rows * avg_row_len / 8192 只有 300 万块,中间那 4900 万块就是空的。

再看精确的空闲分布(需要 ASSM,10g 以后默认就是):
  1. DECLARE
  2.   u_blk NUMBER; u_byt NUMBER;
  3.   f1b NUMBER; f1y NUMBER; f2b NUMBER; f2y NUMBER;
  4.   f3b NUMBER; f3y NUMBER; f4b NUMBER; f4y NUMBER;
  5.   fb  NUMBER; fby NUMBER;
  6. BEGIN
  7.   DBMS_SPACE.SPACE_USAGE('APP','T_ORDER_LOG','TABLE',
  8.     u_blk,u_byt, f1b,f1y, f2b,f2y, f3b,f3y, f4b,f4y, fb,fby);
  9.   DBMS_OUTPUT.PUT_LINE('unformatted='||u_blk||' full='||fb||
  10.     ' fs1='||f1b||' fs2='||f2b||' fs3='||f3b||' fs4='||f4b);
  11. END;
  12. /
复制代码

unformatted 是 HWM 之上尚未格式化的块(这部分 shrink 能直接砍掉),fs1~fs4 是 HWM 之下的空闲块。FS1 是几乎空的块,FS4 是 75% 以上空的块。如果 unformatted 很大、fs3/fs4 很小,说明数据已经挪走了,属于最好处理的情况。

步骤 1:把索引膨胀也量化出来
  1. SELECT index_name, leaf_blocks, distinct_keys, num_rows, blevel, clustering_factor
  2.   FROM dba_indexes WHERE table_name='T_ORDER_LOG' ORDER BY leaf_blocks DESC;
复制代码

leaf_blocks 对比 num_rows 比例失衡(比如 12 万行占了 140 万个叶子块)就说明索引里也全是空叶子块。索引的 HWM 不能用 shrink 降到(ALTER INDEX ... SHRINK 不存在),只能 REBUILD 或 COALESCE。

步骤 2:SHRINK(在线,业务可继续写)
  1. ALTER TABLE app.t_order_log ENABLE ROW MOVEMENT;
  2. -- 第一阶段:只挪行,不降 HWM,可分多次执行,对业务最温和
  3. ALTER TABLE app.t_order_log SHRINK SPACE COMPACT;
  4. -- 第二阶段:降 HWM,并联动处理依赖的索引/LOB 段
  5. ALTER TABLE app.t_order_log SHRINK SPACE CASCADE;
复制代码

SHRINK 加的是行级锁,DML 期间也能跑,这是它比 MOVE 最大的优势。COMPACT 和完整的 SHRINK SPACE 可以分成两个低峰窗口执行,前者只搬行、后者才真正降线并占用较重的资源。

注意 CASCADE 才是关键——不加它,索引和 LOB 段纹丝不动。

步骤 3:需要重建段的场合用 MOVE ONLINE

当表上有函数索引、基于 ROWID 的物化视图,或者 SHRINK 报 ORA-10636 之类的不支持错误时,改用 MOVE:
  1. -- 12c 起支持 ONLINE;12.2 起可用 UPDATE INDEXES 让索引保持可用
  2. ALTER TABLE app.t_order_log MOVE ONLINE TABLESPACE app_data UPDATE INDEXES;
  3. -- 11.2 没有 ONLINE,只能停业务窗口做,且索引会变 UNUSABLE
  4. -- ALTER TABLE app.t_order_log MOVE TABLESPACE app_data;
  5. -- 之后必须重建索引,否则查询报 ORA-01502
复制代码

MOVE 是重建整个段,HWM 直接归位,代价是需要额外一份空间和更长的锁时间。能 SHRINK 就别优先用 MOVE。

步骤 4:索引单独处理
  1. -- 彻底回收:重建(在线,需要额外空间)
  2. ALTER INDEX app.idx_order_log_dt REBUILD ONLINE TABLESPACE app_idx;
  3. -- 温和版:只合并同一分支内的空闲叶子块,段大小不会变小
  4. ALTER INDEX app.idx_order_log_dt COALESCE;
复制代码

COALESCE 常常被误当成"缩索引",它只是把同一父节点下的空闲块合并,dba_segments.bytes 基本不变。要真降大小只有 REBUILD。

步骤 5:数据文件 RESIZE

先找这个文件里最高的块在哪,再决定能缩到多少:
  1. SELECT file_id, MAX(block_id + blocks - 1) AS hwm_block
  2.   FROM dba_extents WHERE file_id = 7 GROUP BY file_id;
  3. -- 结果假设 hwm_block = 2,560,000,则最小不能小于 2560000*8/1024 = 20000MB
  4. SELECT file_id, bytes/1024/1024 AS mb, autoextensible FROM dba_data_files WHERE file_id = 7;
  5. ALTER DATABASE DATAFILE 7 RESIZE 21000M;
复制代码

留 5% 余量,别贴着 HWM 缩——差一个块就是 ORA-03297。

步骤 6:验证复查
  1. -- 段是否真的瘦了
  2. SELECT blocks, bytes/1024/1024 AS mb FROM dba_segments
  3. WHERE owner='APP' AND segment_name='T_ORDER_LOG';
  4. -- 全表扫描的逻辑读前后对比(关键指标)
  5. SET AUTOTRACE ON STATISTICS
  6. SELECT COUNT(*) FROM app.t_order_log;
  7. -- 统计信息必须重新收,否则优化器还按 420GB 估成本
  8. EXEC DBMS_STATS.GATHER_TABLE_STATS('APP','T_ORDER_LOG',CASCADE=>TRUE);
复制代码

治理前后对比

指标治理前治理后变化
段大小 dba_segments420 GB86 GB-79.5%
段块数(HWM)52,100,00010,720,000-79.4%
全表扫描逻辑读4,180,000870,000-79.2%
count(*) 全扫耗时62 s11 s-82%
索引叶子块1,420,000310,000-78.2%
数据文件 7 大小500 GB210 GB-290 GB 归还 OS
每日全备耗时3 h 40 m52 m-76%


四、实操检查清单


  • 先用 dba_segments.blocks 减 dba_tables.blocks(或 num_rows*avg_row_len/8192)算出浪费量,确认值得动手再动。
  • 动 SHRINK 之前必须 ENABLE ROW MOVEMENT,否则直接 ORA-10636。
  • 一律用 SHRINK SPACE CASCADE,别只对表做,索引和 LOB 段要一起处理。
  • 确认表所在表空间是 ASSM(dba_tablespaces.segment_space_management = 'AUTO'),MSSM 表空间不支持 shrink。
  • 大表分两个窗口:先 SHRINK SPACE COMPACT,低峰再 SHRINK SPACE CASCADE。
  • 操作前确认 UNDO 表空间有足够余量(shrink 会产生大量 undo 和 redo),并在归档空间上留出余量。
  • MOVE 之后立即检查 dba_indexes.status,有 UNUSABLE 的必须重建,否则线上直接报错。
  • 数据文件 RESIZE 目标值必须大于该文件最高块的结束位置,且留 5% 余量。
  • 收尾一定跑 DBMS_STATS.GATHER_TABLE_STATS,并复查执行计划(shrink 会让依赖游标失效、统计信息错位)。
  • 治理后一周内复看 dba_segments 与全备耗时,确认没有回涨到原位。


几个容易踩的坑

把 COALESCE 当 REBUILD 用。 索引做 COALESCE 之后查询可能略快,但段大小不变,磁盘和备份压力一点没减,很多人以为处理完了,其实还在。

只 shrink 表,忘了 LOB。 日志表常带 CLOB 报文体,LOB 段单独算 HWM。不加 CASCADE,表从 420GB 缩到 90GB,整体还是 380GB,白干一场。

MOVE 后忘了索引。 11.2 的 MOVE 会让索引全部 UNUSABLE,第二天早高峰所有相关查询报 ORA-01502。要么用 12.2+ 的 UPDATE INDEXES,要么把重建索引写进同一份操作脚本。

数据文件缩得太贴边。 按当前 HWM 精确缩,第二天插入一点数据就 ORA-03297。永远留余量。

用 shrink 替代分区设计。 真正的根治是让日志表按天/按月分区,用 DROP PARTITION 代替 DELETE——那是直接丢段的动作,不留 HWM 问题、不留 undo 压力。表已经 4 亿行还没分区的,先做在线重定义分区,再谈 shrink。

在业务高峰做全表 shrink。 即使 DML 不阻塞,shrink 期间的 IO 和 latch 竞争会把 P99 顶上去,日志表尤其明显。挑凌晨,并且用 COMPACT 分期。

最后一句实话:DELETE 之后空间不释放不是缺陷,是 Oracle 的设计——它假定你删完还会再插。真正的问题在于业务方把它当成了"归档",而归档该用分区或 TRUNCATE。把这条讲给写 SQL 的人听,比事后 shrink 十次都管用。
免责申明1、欢迎访问本站,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@dbabbs.com
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

QQ|Archiver|小黑屋|DBA论坛中国 ( 鲁ICP备20017503号-2 )

GMT+8, 2026-10-31 17:52 , Processed in 0.018239 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表