跳过正文

Oracle 空间管理深度解析:从逻辑存储结构、HWM 到空间回收策略

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

引言
#

在 Oracle DBA 的日常工作中,空间管理是最常见、也最容易被误解的主题之一。

“表空间满了怎么办?”

“删除了大量数据,为什么磁盘空间没变?”

“碎片到底要不要整理?”

这些问题几乎每个 DBA 都会遇到。很多时候,问题不在于不会操作,而在于没有分清不同层级的“空间”到底指什么:是段内空闲、表空间空闲,还是操作系统可见的磁盘空间?

本文将从 Oracle 空间管理的底层架构讲起,系统解释空间分配与回收机制,并结合实际操作场景说明:什么时候该做、什么时候不该做、做了会产生什么影响。目标不是“把空间整理得很漂亮”,而是帮助你做出正确的、面向业务的决策。


第一章:Oracle 空间架构的层次结构
#

1.1 四层存储模型
#

Oracle 的逻辑存储结构可以分为四层,从宏观到微观依次为:

逻辑存储结构.png
  • 表空间(Tablespace):逻辑存储最高层容器,对应一个或多个数据文件。DBA 日常容量管理的主要对象。
  • 段(Segment):数据库对象的存储实体。表、索引、LOB 等通常都有独立段。
  • 区(Extent):由一组连续数据块组成,是段向表空间申请空间时的分配单位。
  • 数据块(Block):Oracle 数据库以数据块(也称为 Oracle 块或页)为单位,来管理数据库数据文件中的逻辑存储空间。数据块是数据库 I/O 的最小单位。

关键点:很多“空间没释放”的争议,都是因为把这四层混在一起看了。


1.2 表空间管理方式的演进:DMT → LMT
#

Oracle 表空间管理方式经历了明显演进:

管理方式典型时代元数据位置特征
字典管理(DMT)早期版本数据字典表并发竞争高、易产生管理开销
本地管理(LMT)9i+ 主流/默认数据文件头位图分配效率高、管理开销低
ASSM(段空间管理方式)9i+ 主流位图块自动管理块内空闲空间

为什么 LMT 显著减少了“表空间碎片”问题?

在 LMT 中,Oracle 通过位图管理区的分配状态,区大小要么是统一大小(UNIFORM),要么由系统自动分配(AUTOALLOCATE)。这使得传统 DMT 时代那种“空闲区难以合并利用”的问题大幅减少。

更准确的说法是:LMT 显著降低了分配管理层面的碎片问题,但段内空闲、HWM 以下空块等“空间现象”仍然存在,只是含义不同。

示例(统一 extents 的 LMT 表空间):

CREATE TABLESPACE app_data_ts
DATAFILE '/u01/app/oracle/oradata/orcl/app_data01.dbf' SIZE 10G
EXTENT MANAGEMENT LOCAL UNIFORM SIZE 1M
SEGMENT SPACE MANAGEMENT AUTO;

1.3 段空间管理:MSSM vs ASSM
#

MSSM(Manual Segment Space Management)
#

通过 Freelist 跟踪可用块,在高并发 INSERT 场景下可能产生热点争用。

ASSM(Automatic Segment Space Management)
#

通过位图记录块空间使用状态,显著降低并发争用,是现代 Oracle 的标准配置。

ASSM 常见空闲级别(概念上):

  • FS1:0–25% 空闲
  • FS2:25–50% 空闲
  • FS3:50–75% 空闲
  • FS4:75–100% 空闲
  • FULL:块已满或低于 PCTFREE 可用阈值

第二章:高水位线(HWM)的本质
#

2.1 什么是高水位线?
#

高水位线(HWM)可以理解为:段曾经使用到的最高块位置标记

即使你删除了大量数据,这些块变成空块,只要 HWM 没下降,Oracle 在某些访问路径(尤其全表扫描)中仍然可能扫描到这些块。

高水位线.png

2.2 HWM 为什么影响性能?
#

全表扫描(Full Table Scan)通常扫描到 HWM,而不是只扫描“当前有数据的块”。

因此,删除大量数据后你可能会看到:

  • 表看起来“行数少了很多”
  • 但全表扫描耗时变化不明显
  • 段大小也没明显缩小

这并不代表 Oracle “没删掉”,而是因为 DELETE 不会自动降低 HWM


2.3 关于 HWM 观察的一个常见误区
#

很多人会直接看 USER_TABLES.BLOCKS/EMPTY_BLOCKS 判断空间情况,但要注意:

  • 这些字段依赖统计信息(stats),不一定实时;
  • 在不同版本、不同统计采集方式下,参考价值有限;
  • 它们更适合做粗略估算,不是精确诊断。

可先用段大小做快速判断:

SELECT segment_name,
       blocks,
       ROUND(bytes/1024/1024, 2) AS mb
FROM user_segments
WHERE segment_name = 'MY_TABLE';

如果要更精细地分析 ASSM 段内空闲分布,可考虑 DBMS_SPACE(如 SPACE_USAGEUNUSED_SPACE)做进一步诊断。

示例:适用于:表段 / 索引段(ASSM),能看到每个桶里有多少块(Blocks)。

SET SERVEROUTPUT ON;

DECLARE
  l_unformatted_blocks  NUMBER;
  l_unformatted_bytes   NUMBER;

  l_fs1_blocks          NUMBER;  -- 0-25% free
  l_fs1_bytes           NUMBER;
  l_fs2_blocks          NUMBER;  -- 25-50% free
  l_fs2_bytes           NUMBER;
  l_fs3_blocks          NUMBER;  -- 50-75% free
  l_fs3_bytes           NUMBER;
  l_fs4_blocks          NUMBER;  -- 75-100% free
  l_fs4_bytes           NUMBER;

  l_full_blocks         NUMBER;  -- full blocks
  l_full_bytes          NUMBER;
BEGIN
  DBMS_SPACE.SPACE_USAGE(
    segment_owner      => 'SCOTT',
    segment_name       => 'EMP',
    segment_type       => 'TABLE',   -- TABLE / INDEX / CLUSTER / ...
    unformatted_blocks => l_unformatted_blocks,
    unformatted_bytes  => l_unformatted_bytes,
    fs1_blocks         => l_fs1_blocks,
    fs1_bytes          => l_fs1_bytes,
    fs2_blocks         => l_fs2_blocks,
    fs2_bytes          => l_fs2_bytes,
    fs3_blocks         => l_fs3_blocks,
    fs3_bytes          => l_fs3_bytes,
    fs4_blocks         => l_fs4_blocks,
    fs4_bytes          => l_fs4_bytes,
    full_blocks        => l_full_blocks,
    full_bytes         => l_full_bytes
  );

  DBMS_OUTPUT.PUT_LINE('Segment: SCOTT.EMP (TABLE)');
  DBMS_OUTPUT.PUT_LINE('Unformatted Blocks : ' || l_unformatted_blocks);
  DBMS_OUTPUT.PUT_LINE('FS1 (0-25% free)   : ' || l_fs1_blocks);
  DBMS_OUTPUT.PUT_LINE('FS2 (25-50% free)  : ' || l_fs2_blocks);
  DBMS_OUTPUT.PUT_LINE('FS3 (50-75% free)  : ' || l_fs3_blocks);
  DBMS_OUTPUT.PUT_LINE('FS4 (75-100% free) : ' || l_fs4_blocks);
  DBMS_OUTPUT.PUT_LINE('Full Blocks        : ' || l_full_blocks);
END;
/

2.4 关于“碎片”的误解(建议 DBA 必看)
#

在现代 Oracle(LMT + ASSM)环境下,很多人说的“碎片”其实不是一回事:

概念真实影响是否需要处理
表空间分配碎片(LMT)通常较小一般不需要
段内空闲(HWM 以下)可被同段后续重用通常不需要
行迁移/行链接可能影响访问性能视情况处理
索引叶块空洞/不平衡部分场景可能有影响证据驱动处理

核心原则: DELETE 后产生的段内空闲空间,通常可以被该对象后续 DML 重用。不要仅因为“看起来不整齐”就做重组


第三章:DELETE / TRUNCATE / DROP 的空间行为对比
#

这是空间管理里最常被误解的部分。三者都能“让数据消失”,但空间行为完全不同。


3.1 DELETE:删数据,不降 HWM
#

DELETE FROM hr.employees WHERE salary > 30000;

空间行为:

  • 段内空间:释放为可重用(供该段后续使用)
  • 高水位线(HWM):通常不降低
  • 表空间可用空间:通常不增加
  • 操作系统/ASM 可见空间:不会减少文件大小
  • Redo/Undo:大量产生
  • 事务特性:可回滚

适用场景:

  • 需要按条件精确删除
  • 需要回滚能力
  • 需要触发器/审计逻辑生效
  • 小批量或分批删除(批处理)

实务建议: 大批量删除时,不要一次性删完。建议分批提交,控制 Undo/Redo、归档日志、锁持有时间。


3.2 TRUNCATE:快速清空,重置 HWM
#

TRUNCATE TABLE hr.employees;
-- 或
TRUNCATE TABLE hr.employees REUSE STORAGE;

空间行为:

  • 高水位线(HWM):重置
  • 段空间:默认释放多余 extents 给表空间(DROP STORAGE
  • 文件系统/ASM:文件大小通常不变
  • Redo/Undo:相对极少(DDL)
  • 事务特性:不可回滚(DDL 隐式提交)

两种常见模式:

-- 默认行为(等价于 DROP STORAGE)
TRUNCATE TABLE hr.employees DROP STORAGE;

-- 保留已分配空间,适合马上要重新装载数据
TRUNCATE TABLE hr.employees REUSE STORAGE;

适用场景:

  • 清空整表(如临时表、阶段表)
  • ETL 中间层表重置
  • 测试环境快速清空数据

注意:TRUNCATE 不能带 WHERE 条件;且会影响依赖对象/约束场景(需结合外键关系设计)。


3.3 DROP TABLE:删除对象本身
#

DROP TABLE hr.employees;
-- 跳过回收站(如果业务允许)
DROP TABLE hr.employees PURGE;

空间行为:

  • 对象级别:表对象及其依赖对象(索引等)被删除
  • 表空间空间:回收(若进入回收站则可能延迟完全释放)
  • 文件系统/ASM:文件大小通常不变
  • 可恢复性:若进入回收站,可 FLASHBACK TABLE ... TO BEFORE DROP

回收站常用操作:

-- 查看回收站对象
SELECT object_name, original_name, droptime
FROM recyclebin;

-- 恢复
FLASHBACK TABLE hr.employees TO BEFORE DROP;

-- 清空当前用户回收站
PURGE RECYCLEBIN;

-- 清空指定表空间回收站对象(可按需加 USER 子句)
PURGE TABLESPACE users;

3.4 三种操作对比总结
#

操作段内空间重用表空间回收文件系统/ASM 缩小HWM可回滚Redo/Undo
DELETE
TRUNCATE✅(重置后重新分配)✅(默认)
DROPN/A✅(回收站可能延迟)N/A

DROP 本身不可回滚;如进入回收站,可通过 Flashback 恢复对象。


第四章:空间回收的方法与工具
#

这一章建议用“按目标层级”来组织,读者最容易理解。


4.1 目标一:释放段空间(降低 HWM / 提高段内利用率)
#

方法一:Segment Shrink(推荐优先评估)
#

ALTER TABLE hr.employees ENABLE ROW MOVEMENT;

ALTER TABLE hr.employees SHRINK SPACE;
-- 或仅压缩段内数据,不立即降低 HWM
ALTER TABLE hr.employees SHRINK SPACE COMPACT;

-- 连带依赖对象(视版本/对象类型而定)
ALTER TABLE hr.employees SHRINK SPACE CASCADE;

优点:

  • 通常可在线执行(对业务影响较小)
  • 能降低 HWM(非 COMPACT 模式)

注意与限制:

  • 仅适用于 ASSM 表空间
  • 需要 ROW MOVEMENT
  • 会改变 ROWID(对依赖 ROWID 的程序要谨慎)
  • 某些对象类型/版本存在限制(LOB、IOT、分区对象等要单独确认)

方法二:ALTER TABLE … MOVE(重建段)
#

ALTER TABLE hr.employees MOVE;
-- 或移动到新表空间
ALTER TABLE hr.employees MOVE TABLESPACE new_ts;

-- 12c+ 可用在线方式(视版本/对象类型)
ALTER TABLE hr.employees MOVE ONLINE;

特点:

  • 能有效整理段并重建存储布局
  • 常用于迁移表空间、压缩重建

重要注意: 传统 MOVE 后,相关索引通常会变为不可用,需要重建:

ALTER INDEX hr.pk_employees REBUILD;
-- 或在线重建(视版本/许可/对象类型)
ALTER INDEX hr.pk_employees REBUILD ONLINE;

方法三:CTAS(适合大改造/结构优化)
#

CREATE TABLE hr.employees_new AS
SELECT * FROM hr.employees;

RENAME hr.employees TO hr.employees_old;
RENAME hr.employees_new TO hr.employees;

适用场景:

  • 需要同时做结构优化(列调整、压缩、分区改造)
  • 大表离线重构
  • 历史数据治理项目

风险点:

  • 索引、约束、权限、触发器、同义词、统计信息需要重建或迁移
  • 变更窗口要充分评估

4.2 目标二:释放表空间空闲给操作系统 / ASM(缩小数据文件)
#

这是 DBA 常见误区:段里释放了空间 ≠ 数据文件变小

Oracle 一般不会自动缩小数据文件,若要把空间“还给操作系统/ASM”,需要显式操作。

方法一:收缩数据文件(RESIZE)
#

ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/orcl/app_data01.dbf' RESIZE 5G;

前提:

  • 数据文件尾部没有已分配 extents
  • RESIZE 值必须大于文件内最高已用块位置

建议:先评估再收缩(避免 ORA-03297)

  • 先确认数据文件 HWM
  • 预留增长空间(不要收得太极限)
  • 结合 autoextend 策略评估

实务建议:生产库中,宁可保守缩小,也不要频繁“缩了又长”。


方法二:删除空数据文件(特定条件下)
#

ALTER TABLESPACE users DROP DATAFILE '/u01/app/oracle/oradata/orcl/users02.dbf';

前提:

  • 数据文件必须为空(无 extents)
  • 版本、表空间类型、管理方式需满足条件

4.3 索引空间管理:Coalesce vs Rebuild
#

Coalesce(轻量级整理叶块)
#

ALTER INDEX hr.pk_employees COALESCE;
  • 在线
  • 不重建整棵 B-tree
  • 适合局部叶块空洞整理

Rebuild(完全重建)
#

ALTER INDEX hr.pk_employees REBUILD;
-- 在线(视版本/对象类型)
ALTER INDEX hr.pk_employees REBUILD ONLINE;

-- 压缩重建(视场景)
ALTER INDEX hr.pk_employees REBUILD COMPRESS;

经验原则:

  • 先问“有没有性能问题”
  • 再问“是不是索引导致”
  • 最后才决定 COALESCE 还是 REBUILD

不要因为“删除了很多数据”就习惯性重建索引。


第五章:监控与诊断
#

5.1 表空间使用率监控(建议同时看当前值与最大可扩展值)
#

你原文的思路是对的,建议在实际使用时再补一列 maxbytes(若开启 autoextend 更有意义),这样不会误判“快满了”。

示例(简化版,按当前分配容量统计):

SELECT
    df.tablespace_name,
    ROUND(SUM(df.bytes)/1024/1024/1024, 2) AS total_gb,
    ROUND((SUM(df.bytes) - NVL(fs.free_bytes,0))/1024/1024/1024, 2) AS used_gb,
    ROUND(NVL(fs.free_bytes,0)/1024/1024/1024, 2) AS free_gb,
    ROUND((SUM(df.bytes)-NVL(fs.free_bytes,0))/SUM(df.bytes)*100, 2) AS pct_used
FROM dba_data_files df
LEFT JOIN (
    SELECT tablespace_name, SUM(bytes) AS free_bytes
    FROM dba_free_space
    GROUP BY tablespace_name
) fs
ON df.tablespace_name = fs.tablespace_name
GROUP BY df.tablespace_name, fs.free_bytes
ORDER BY pct_used DESC;

如果是 OMF/ASM/autoextend 环境,建议再补充 MAXBYTES 视角,避免只看当前 bytes。


5.2 大段识别
#

SELECT
    owner,
    segment_name,
    segment_type,
    ROUND(bytes/1024/1024/1024, 2) AS size_gb
FROM dba_segments
ORDER BY bytes DESC
FETCH FIRST 20 ROWS ONLY;

这类 SQL 非常适合做巡检、容量治理、空间回收候选对象筛选。


5.3 表统计信息 vs 段占用空间(“是否值得整理”的初筛)
#

SELECT
    t.table_name,
    t.num_rows,
    ROUND(t.blocks * 8 / 1024, 2) AS stats_mb,      -- 假设8KB块,仅粗估
    ROUND(s.bytes / 1024 / 1024, 2) AS segment_mb,
    ROUND((s.bytes / 1024 / 1024) - (t.blocks * 8 / 1024), 2) AS potential_gap_mb
FROM user_tables t
JOIN user_segments s
  ON t.table_name = s.segment_name
WHERE s.segment_type = 'TABLE'
ORDER BY potential_gap_mb DESC;

注意:

  • 这里的 8KB 是估算值,实际请按数据库块大小调整;
  • USER_TABLES.BLOCKS 依赖统计信息,不是实时值;
  • 该查询适合做“候选清单”,不适合直接作为 shrink/move 的唯一依据。

5.4 行迁移/行链接检测(建议改写为现代实践)
#

  • 统计信息:使用 DBMS_STATS
  • 行迁移/行链接专项诊断:使用 ANALYZE ... LIST CHAINED ROWS

示例:

-- 1) 常规统计信息(推荐)
BEGIN
  DBMS_STATS.GATHER_TABLE_STATS(
    ownname        => 'HR',          -- 用户名(Schema 名),替换为你的实际用户名
    tabname        => 'EMPLOYEES',            -- 表名,替换为你的实际表名
    method_opt     => 'FOR ALL COLUMNS SIZE AUTO',  -- 你关注的参数
    estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE, -- 自动采样比例(推荐)
    cascade        => TRUE,             -- 同时收集索引的统计信息(推荐)
    no_invalidate  => FALSE             -- 使已有的执行计划失效,重新生成最优计划
  );
END;
/

-- 2) 链式/迁移行诊断(需预先创建 CHAINED_ROWS 表)
ANALYZE TABLE hr.employees LIST CHAINED ROWS INTO chained_rows;

SELECT COUNT(*) AS chained_row_count
FROM chained_rows
WHERE table_name = 'EMPLOYEES';

说明:CHAIN_CNT 的解释和准确性受统计方式影响,实战中更建议把它当作“提示信号”,结合 SQL 性能表现再决定是否处理。


第六章:实战决策建议
#

很多文章讲完原理就结束了,但 DBA 真正需要的是“怎么选”。

场景1:删除历史数据后,空间没变,是否要整理?
#

先问三件事:

  1. 业务是否会很快再写回这些空间? 如果会,通常不需要整理(留着复用更划算)。

  2. 是否存在明确性能问题(如 FTS 变慢)? 没有证据就不要动。

  3. 你的目标是段内复用、表空间回收,还是 OS/ASM 释放? 目标不同,方法完全不同。


场景2:大表清理后全表扫描变慢
#

优先级建议:

  1. 先确认执行计划是否真在 FTS
  2. 判断是否因 HWM 过高造成无效扫描
  3. 再评估 SHRINK / MOVE / 分区改造

对历史数据治理类大表,长期最优解通常不是反复 shrink,而是分区表 + 生命周期管理


场景3:磁盘告警,想“立刻释放空间”
#

正确路径通常是:

  1. 找出最大段/增长最快对象
  2. 确认是否存在可清理对象(临时表、归档、历史分区、回收站)
  3. 执行对象级回收(如 truncate/drop/shrink/move)
  4. 最后再评估数据文件 RESIZE

不要一上来就收缩数据文件。


总结
#

Oracle 空间管理的核心,不是“把空间变小”,而是准确理解空间所在层级,并选择正确动作

请记住这五条原则:

  1. 先分层级:表空间、段、区、块,含义不同,处理方法不同
  2. 先定目标:是为了性能、复用、回收表空间,还是释放 OS/ASM 空间
  3. 先看证据:有性能问题再整理,而不是出于“强迫症”整理
  4. 先控风险:评估事务、锁、ROWID、索引失效、业务窗口
  5. 长期靠架构:大表历史数据管理优先考虑分区,而不是反复手工回收

数据库管理的目标是服务业务稳定与效率,不是追求“空间看起来完美”。在空间管理上,做正确的事,比做很多事更重要。