Oracle表空间满排查与治理:从ORA-01653应急扩容到空间回收

Oracle表空间满排查与治理:从ORA-01653应急扩容到空间回收 1. 先把表空间满这件事的概念对齐做 Oracle 运维的人大概都有过这种凌晨告警应用侧报错ORA-01653: unable to extend table ... in tablespace ...值班群里一句话甩过来——XX 表空间满了快处理一下。作为 DBA表空间使用率告警和空间不足处置属于日常最频繁、也最容易做错的一类工单处理慢了业务写不进去处理糙了留下一堆隐患比如数据文件加了几十个没人管、临时表空间重建后默认没切回来、UNDO 扩到几个 T 却还天天报ORA-30036。这篇笔记按我自己处理这类故障的实际顺序来写先把概念和口径对齐再用几条 SQL 把现状摸清然后给应急止血的手段最后聊怎么从天天扩容变成真正治理。内容基于常见生产实践整理脚本在 11g、12c、19c 上都跑过细节上会把版本差异标出来。适合刚接手 Oracle 库的运维、需要自己盯库的后端同学以及被表空间告警折腾过几次想彻底搞明白的 DBA 参考。1.1 表空间、数据文件、段是三层结构别混着看很多人一开始被绕晕是因为把表空间满当成了一个整体概念。实际上 Oracle 的存储是三层套娃表空间tablespace是逻辑容器它自己不占磁盘数据文件datafile是物理载体磁盘上的.dbf就是它段segment是对象在表空间里的具体占用一张表、一个索引、一个 LOB 字段在表空间里都对应一个或多个段。这三层的关系决定了满有三种完全不同的形态。第一种是数据文件真的写满了磁盘还开不了自动扩展这是物理层的满第二种是表空间里所有数据文件的可用空间加起来不够分配下一个区extent这是逻辑层的满第三种是段本身能扩展但分配不到连续的 extent或者因为配额quota被卡住。三种情况的外在表现都是应用报错但处置动作完全不一样。理解这一点的实际价值在于当你看到告警说表空间 95%第一步不应该去加文件而是先确认是逻辑水位高还是物理文件真的没空间了。逻辑水位高但数据文件还有几个 T 的 MAXBYTES 没释放那是统计口径的问题加文件纯属浪费。反过来物理磁盘都写穿了你还在调整段参数那就是南辕北辙。1.2 三种满的形态与对应报错生产上遇到的表空间类报错其实就那么几个能准确对应到形态排查就成功了一半。ORA-01653无法为某个表扩展空间。这是永久表空间最典型的报错说明目标表空间没有可用的连续空间了。ORA-01654无法为索引扩展。索引段和表段是分开算的常见于索引表空间先满。ORA-01658无法为段创建初始区。通常出现在新建表、新建索引或者分区表新增分区的时候。ORA-01652无法在临时表空间中扩展临时段。这是临时表空间被打爆的典型报错一般伴随大排序、大哈希连接或并行查询。ORA-30036无法在 UNDO 表空间中扩展段。UNDO 写满紧接着往往就是ORA-01555 snapshot too old。ORA-1691表空间无法扩展时更友好的一个提示通常会带着后面的具体报错一起出现。把这几个错误码和形态对应起来值班时的判断速度会快很多。我的习惯是看到 1653/1654/1658 就去查永久表空间的水位和段排行看到 1652 立刻去查临时表空间的占用会话看到 30036 就直接去看 UNDO 区使用和运行时间最长的那几条 SQL。方向对了后面的动作就是体力活。1.3 空间不足的本质增长快于预期或者回收没做做了这么多年我把表空间出问题的原因归成两类。第一类是增长超出规划比如上线时按日均 500MB 估的容量实际业务跑起来日均 5GB几个月就把预留水位吃穿又比如某次批量导入没做分区一次性写进来几百 GB。第二类是该回收的没回收典型的就是回收站recyclebin里的对象没清理、历史分区没归档、临时表空间里的临时段没释放、UNDO 被一个跑了几小时的查询拖住不回收。第二类其实更常见也更冤——磁盘上明明有空间只是 Oracle 没法用。所以每次处理表空间告警我都会问自己两个问题这个空间是被真实业务数据占住的还是被临时状态占住的如果是后者扩容只是把问题往后推真正的解法是把空间还回来。2. 十分钟摸清现状排查脚本与口径告警响了之后最忌讳的就是直接ADD DATAFILE。加文件是秒级动作但它掩盖了为什么满这个核心问题。我自己的固定动作是跑四组查询大概十分钟就能把整件事看清楚。2.1 第一眼全库表空间水位一览第一个查询看整体水位用DBA_TABLESPACE_USAGE_METRICS配合DBA_TABLESPACES换算成 MBSELECT m.tablespace_name, ROUND(m.tablespace_size * t.block_size / 1024 / 1024, 2) AS total_mb, ROUND(m.used_space * t.block_size / 1024 / 1024, 2) AS used_mb, ROUND(m.used_percent, 2) AS used_pct FROM dba_tablespace_usage_metrics m, dba_tablespaces t WHERE m.tablespace_name t.tablespace_name ORDER BY m.used_percent DESC;这个视图从 10g 就有颗粒细、跑得快适合第一眼定位。但它的口径有个坑必须知道USED_SPACE是按段的高水位线HWM以下部分统计的段里那些已分配但高水位线以上的块并不计入。所以在一张刚做过大量DELETE的表所在表空间里这个视图给出的使用率会偏乐观。反过来回收站里的对象、没有被清理的临时段它也不一定算得准。因此第二个口径必须交叉验证用DBA_DATA_FILES和DBA_FREE_SPACE手工算SELECT d.tablespace_name, ROUND(d.bytes / 1024 / 1024, 2) AS total_mb, ROUND(NVL(f.bytes, 0) / 1024 / 1024, 2) AS free_mb, ROUND((d.bytes - NVL(f.bytes, 0)) / d.bytes * 100, 2) AS used_pct FROM (SELECT tablespace_name, SUM(bytes) bytes FROM dba_data_files GROUP BY tablespace_name) d, (SELECT tablespace_name, SUM(bytes) bytes FROM dba_free_space GROUP BY tablespace_name) f WHERE d.tablespace_name f.tablespace_name() ORDER BY used_pct DESC;注意这里用的是外连接()因为一个完全写满的表空间在DBA_FREE_SPACE里可能查不到任何行内连接会把它漏掉——这恰恰是最需要关注的那个。提示两个查询结果差得比较多时优先信DBA_FREE_SPACE的口径因为它反映的是下一个区能不能分配出去这才是报错的直接原因。在 12c 及以上的多租户环境里要注意上面这些视图是容器相关视图。如果你连的是 CDB 根查出来的是所有 PDB 汇总正确做法是切到目标 PDB 再执行或者在查询里加上CON_ID过滤。这一条在 19c 上踩过坑第一次看到数据翻了好几倍以为出了大事其实是查错层了。2.2 第二眼数据文件还剩多少可增长空间水位高不等于没救关键看数据文件还能不能长。这条 SQL 是判断要不要立刻加文件的核心依据SELECT d.tablespace_name, d.file_name, ROUND(d.bytes / 1024 / 1024, 2) AS cur_mb, CASE WHEN d.autoextensible YES THEN ROUND((d.maxbytes - d.bytes) / 1024 / 1024, 2) ELSE 0 END AS growable_mb, d.autoextensible FROM dba_data_files d ORDER BY growable_mb ASC, d.tablespace_name;重点看growable_mb还能长多少和autoextensible是否开了自动扩展。这里有两个非常典型的坑第一MAXBYTES为 0 不代表能无限长。在AUTOEXTENSIBLENO的时候MAXBYTES往往显示为 0有人会误读成没有上限。判断的时候一定要结合autoextensible字段一起看。第二开了自动扩展不代表一定能长。MAXBYTES只是逻辑上限它受文件系统剩余空间、ASM 磁盘组剩余空间、甚至操作系统单个文件大小限制约束。数据文件的MAXBYTES写着 32G但挂载点只剩 5G那实际能长 5G 就到头了。所以每季度我都会顺带确认一下磁盘组的剩余水位这一步比看数据库视图更重要。如果你还想看表空间级别总共还剩多少增长余量可以这样汇总SELECT tablespace_name, ROUND(SUM(bytes) / 1024 / 1024, 2) AS current_mb, ROUND(SUM(CASE WHEN autoextensible YES THEN maxbytes ELSE bytes END) / 1024 / 1024, 2) AS ceiling_mb FROM dba_data_files GROUP BY tablespace_name ORDER BY tablespace_name;ceiling_mb除以current_mb就是理论上的增长倍数配合日均增长量能直接算出还能撑几天。2.3 第三眼谁在吃空间——段级排行确定表空间确实快满了接下来必须回答一个问题空间被谁占了。段级排行是必跑的一条SELECT * FROM ( SELECT owner, segment_name, segment_type, partition_name, ROUND(bytes / 1024 / 1024, 2) AS mb FROM dba_segments WHERE tablespace_name APP_DATA ORDER BY bytes DESC ) WHERE ROWNUM 20;这里刻意用了ROWNUM而不是FETCH FIRST因为在 11g 上FETCH FIRST语法不支持写成子查询两边都能跑。另外一定要带上partition_name分区表按分区显示占用一眼就能看出是哪个时间段的分区最胖——这对后面做分区归档非常关键。看完段排行通常会遇到三种结论。第一种是某个业务表真的膨胀了比如日志表、流水表这时候要考虑分区或者归档第二种是索引比表还大常见于低选择性的索引或者大量失效索引没清理这时候ALTER INDEX ... REBUILD或者直接删掉无用索引第三种是 LOB 段DBA_SEGMENTS里显示为LOBSEGMENT和LOBINDEX一张表带几个 CLOB 字段就能吃掉几百 G这种情况要单独处理。还要补一条索引和表分在不同表空间时两边的告警要分开看。我遇到过索引表空间 98% 而表空间只有 40% 的情况应用报的却是ORA-01654不熟悉的人容易查错方向。2.4 临时与 UNDO 要换另一套口径查永久表空间的脚本对临时表空间完全不适用——临时表空间在DBA_FREE_SPACE里查不到数据。临时表空间要用DBA_TEMP_FREE_SPACESELECT tablespace_name, ROUND(tablespace_size / 1024 / 1024, 2) AS total_mb, ROUND(allocated_space / 1024 / 1024, 2) AS allocated_mb, ROUND(free_space / 1024 / 1024, 2) AS free_mb FROM dba_temp_free_space;这里的ALLOCATED_SPACE是当前被临时段占用的部分FREE_SPACE是文件里还没分配的。如果ALLOCATED_SPACE接近TABLESPACE_SIZE而FREE_SPACE很小说明临时表空间确实被撑满了。接着查是谁在占SELECT s.sid, s.serial#, s.username, s.sql_id, u.tablespace, u.segtype, ROUND(u.blocks * 8192 / 1024 / 1024, 2) AS mb FROM v$tempseg_usage u, v$session s WHERE u.session_addr s.saddr ORDER BY u.blocks DESC;V$TEMPSEG_USAGE老版本里是V$SORT_USAGE能看到每个会话占了多少临时空间、是排序段还是哈希段。拿到SQL_ID之后配合V$SQL和DBMS_XPLAN看执行计划多半是某个 SQL 走了全表扫描加哈希连接或者并行度过高导致临时段暴涨。UNDO 表空间则是另一套SELECT begin_time, end_time, undoblks, maxquerylen, tuned_undoretention FROM v$undostat ORDER BY begin_time DESC FETCH FIRST 20 ROWS ONLY;MAXQUERYLEN是最长查询的秒数TUNED_UNDORETENTION是系统自动算出来的保留时长。如果MAXQUERYLEN突然飙到几千甚至上万秒UNDO 就会被一直按住不回收这时候光扩 UNDO 文件是治标真正要处理的是那条长查询。另外也可以用V$ROLLSTAT联合V$ROLLNAME看每个 UNDO 段的状态和大小SELECT s.name AS segname, r.status, ROUND(r.rssize / 1024 / 1024, 2) AS mb FROM v$rollstat r, v$rollname s WHERE r.usn s.usn ORDER BY r.rssize DESC;STATUS如果是ONLINE并且段都很胖说明 UNDO 在使用中如果已经切成OFFLINE那这些空间很快就能回收。2.5 几个常被忽略的空间黑洞回收站、LOB、索引这一小节值得单独拎出来讲因为这几个是数据删了空间却没还的元凶也是我认为最容易坑人的地方。回收站recyclebin。DROP TABLE默认不真删对象被扔进回收站空间照占。查一下SELECT owner, original_name, type, ts_name, space FROM dba_recyclebin ORDER BY space DESC;SPACE单位是块数乘 8K 就是占用 MB。清理方式有两种单个对象用PURGE TABLE BIN$xxxx整体清空用PURGE DBA_RECYCLEBIN;。要特别提醒的是PURGE DBA_RECYCLEBIN会清掉所有用户的回收站执行前确认没有人在等着闪回误删的表。LOB 段。带有 CLOB/BLOB 的表在表空间里会额外产生LOBSEGMENT存数据和LOBINDEX存定位这两块在段排行里通常排名很靠前。尤其是一些把大 JSON 或大文本塞进 CLOB 的设计删掉主表记录后如果不做LOB的整理空间回收会滞后很多。失效索引和重复索引。业务迭代几年之后经常留下一些和主键功能重叠的索引或者某个统计任务临时建的索引没人删。我见过的一个库里单个索引 200 多 G一问是两年前做报表临时建的。定期跑一遍索引使用情况统计V$OBJECT_USAGE或者按业务侧的表访问日志分析再清理能省下大量空间。3. 应急止血不动业务前提下的扩容与迁移现状查清楚之后进入处置环节。原则很简单先保证业务能写进去再做治理动作。下面这几招按优先级排列基本覆盖了 90% 的现场。3.1 加数据文件还是开自动扩展最直接的止血动作是加数据文件ALTER TABLESPACE APP_DATA ADD DATAFILE /u02/oradata/orcl/app_data02.dbf SIZE 10G AUTOEXTEND ON NEXT 512M MAXSIZE 30G;这里每一个参数都有讲究。SIZE 10G是初始大小如果只是应急可以给 2G 先顶上避免一次性占满磁盘NEXT 512M是每次扩展的步长太小会导致频繁扩展每次扩展都要更新控制文件有轻微开销太大则可能一次吃掉十几 GMAXSIZE 30G是上限下面单独说为什么是 30G 而不是更大。另一种做法是给已有的数据文件开自动扩展ALTER DATABASE DATAFILE /u02/oradata/orcl/app_data01.dbf AUTOEXTEND ON NEXT 512M MAXSIZE 30G;这两种做法怎么选我的经验是已经有很多小文件、磁盘空间充足的库优先开自动扩展省心文件数量已经很多超过 30 个或者需要精细控制 I/O 分布的库手工加固定大小的文件更合适。因为数据文件太多会带来几个问题db_files参数上限、打开文件句柄增多、DBWn写多文件的调度压力以及最烦人的——备份文件列表变长RMAN 的备份恢复时间跟着变长。另外ALTER DATABASE DATAFILE ... RESIZE这个语法一般不用来做扩容它主要用于缩小文件而且受 HWM 限制经常缩不动别把它和ADD DATAFILE搞混。3.2 单文件 32G 上限这件事必须说清楚MAXSIZE为什么建议写 30G因为小文件表空间smallfile tablespace里单个数据文件最多只能有 4194304 个数据块。标准库的DB_BLOCK_SIZE是 8K乘一下4194304 × 8192 字节 32GB。也就是说你不设MAXSIZE上限让它一 Letter直长长到 32GB 也会报错停下报错信息通常类似ORA-03206: maximum file size of ... blocks in autoextend clause。这个数字直接决定了容量规划的方式。如果表空间预计要 100G你不能指望一个文件长到 100G得拆成至少 4 个文件每个设 30G 上限留点余量。反过来说如果你用的是大文件表空间BIGFILE单文件上限就变成 4M 个块乘以块大小对应的更大容量8K 块约 32TB一个文件就能吃下整个表空间。BIGFILE 的管理更简单但代价是不能往一个表空间里加第二个文件扩容只能靠自动扩展或者用RESIZE扩大文件——灵活性差一些。顺带说个实操细节不要用ALTER TABLESPACE ... ADD DATAFILE往 BIGFILE 表空间加文件会直接报错。分不清的话查一下SELECT tablespace_name, bigfile FROM dba_tablespaces WHERE tablespace_name APP_DATA;BIGFILE为YES就是大文件表空间。3.3 表空间搬家把冷数据从热表空间挪走如果段排行显示某个冷表或历史数据占了很大比例把它挪到便宜的表空间或者干脆归档掉比一味扩容更划算。移动普通表ALTER TABLE app_user.log_history MOVE TABLESPACE ARCH_DATA STORAGE (INITIAL 64M NEXT 64M);这条语句有几个必须记住的副作用。第一表移动之后所有索引都会变成UNUSABLE必须重建ALTER INDEX app_user.idx_log_time REBUILD TABLESPACE ARCH_IDX;第二表移动后统计信息会失效业务如果依赖 CBO 选执行计划一定要重新收集BEGIN DBMS_STATS.GATHER_TABLE_STATS(APP_USER, LOG_HISTORY, cascade TRUE); END; /第三表移动过程中会短暂加锁对正在写入的大表要挑窗口期做或者考虑在线重定义。如果这张表是分区表ALTER TABLE ... MOVE是对整表操作分区表要一个分区一个分区地移动ALTER TABLE app_user.log_history MOVE PARTITION P202401 TABLESPACE ARCH_DATA;分区移动同样会让该分区对应的全局索引失效需要重建全局索引或者使用UPDATE GLOBAL INDEXES子句老版本对MOVE PARTITION支持有限稳妥起来还是手工重建。这里我踩过一次坑分区表移动了 12 个分区之后忘了重建全局索引第二天业务查询报ORA-01502: index ... or partition of such index is in unusable state排查了半天才反应过来。对于绝对不能停业务的核心表还有一条路是DBMS_REDEFINITION在线重定义能在业务持续写入的情况下把表迁到新表空间。它灵活但步骤繁琐需要建中间表、启动重定义、同步增量、切表、清理中间表五大步且对表的类型有要求不能带某些特殊数据类型。一般表空间告急的应急场景用不到它这里先记个位置。3.4 临时表空间被打爆的应急重建临时表空间满了最怕的是无法收缩也无法删——因为它可能还挂着默认临时表空间的身份。处置顺序必须严格走错了会报错。第一步先建一个新的临时表空间CREATE TEMPORARY TABLESPACE TEMP02 TEMPFILE /u02/oradata/orcl/temp02_01.dbf SIZE 4G AUTOEXTEND ON NEXT 512M MAXSIZE 20G;第二步把默认临时表空间切过去ALTER DATABASE DEFAULT TEMPORARY TABLESPACE TEMP02;第三步确认老临时表空间没有活跃占用SELECT COUNT(*) FROM v$tempseg_usage WHERE tablespace TEMP;第四步等确认没有会话在用了一般要等业务低谷或者把占用的会话找出来让业务方处理再删DROP TABLESPACE TEMP INCLUDING CONTENTS AND DATAFILES;注意如果老临时表空间还是默认的直接DROP会报ORA-12906: cannot drop default temporary tablespace。一定要先切默认再删。还有一个更好用的做法是临时表空间组temporary tablespace group把多个临时表空间归到一个组里默认临时表空间设成组名这样多个会话会自动分散到不同临时表空间既缓解单文件压力也分散了 I/OALTER TABLESPACE TEMP02 TABLESPACE GROUP TEMPGRP; ALTER DATABASE DEFAULT TEMPORARY TABLESPACE TEMPGRP;临时表空间被打爆的部分原因在 SQL 侧处理完之后一定要回头找那条 SQL。常见的三种问题大表关联没走索引变成哈希连接、ORDER BY无索引导致大规模排序、并行度设得太高PARALLEL 16在大表上能把临时表空间秒穿。找到之后要么加索引要么调并行度要么把这条 SQL 挪到专门的报表库跑。3.5 UNDO 表空间告急的正确处置顺序UNDO 的处置比临时表空间更讲究因为它和事务一致性直接相关操作不当会造成实例级问题。标准做法也是新建、切换、等待、删除四步CREATE UNDO TABLESPACE UNDO02 DATAFILE /u02/oradata/orcl/undo02_01.dbf SIZE 4G AUTOEXTEND ON NEXT 512M MAXSIZE 30G RETENTION NOGUARANTEE; ALTER SYSTEM SET undo_tablespace UNDO02 SCOPE BOTH;在 RAC 环境下参数需要加SID*。切完之后老的 UNDO 段不会立刻释放需要等它们全部变成OFFLINESELECT segment_name, status FROM dba_rollback_segs WHERE tablespace_name UNDO01;STATUS全部变成OFFLINE之后才能安全删除DROP TABLESPACE UNDO01 INCLUDING CONTENTS AND DATAFILES;如果想缩小已有的 UNDO 数据文件ALTER DATABASE DATAFILE ... RESIZE 4G是可以用的但有个前提文件里高水位线以下不能有活动的 UNDO 段。通常要等负载低谷或者让 UNDO 段自然过期回收之后再操作不然会报ORA-03297: file contains used data beyond requested RESIZE value这个报错本身没什么危害就是操作失败。关于RETENTION GUARANTEE我必须提醒一句生产上慎用。开启之后 Oracle 会强制保留指定时长的 UNDOUNDO 空间不够时不会覆盖旧数据而是让 DML 操作失败。看起来是保护查询不报 1555实际效果往往是把一个偶发问题变成大面积写失败。除非有明确合规要求否则让它保持NOGUARANTEE用UNDO_RETENTION参数来调节期望值就够了。4. 从止血到治理让表空间不再天天报警扩容是止痛药吃多了有副作用。想真正减少这类告警得从碎片回收、数据生命周期、监控预警三个方向下手。4.1 高水位线与碎片删了数据空间为什么没还这是被问得最多的一个问题明明删了几千万行数据表空间使用率一点没降。原因是Oracle 的段空间释放和行删除是两回事。DELETE只是把数据块标记为空闲块还在段里段的高水位线也没降表空间自然认为这块空间还被占着。只有整段释放或者做段收缩空间才真正还回去。对支持自动段空间管理ASSM的表空间可以用收缩ALTER TABLE app_user.log_history ENABLE ROW MOVEMENT; ALTER TABLE app_user.log_history SHRINK SPACE COMPACT; ALTER TABLE app_user.log_history SHRINK SPACE;COMPACT阶段只做行迁移不降 HWM对业务影响小可以放在业务时段做第二步不写COMPACT才会真正降低高水位线耗时更长建议放窗口期。注意这招只对 ASSM 表空间有效如果表空间是手工段空间管理MSSMSHRINK SPACE会直接报错那种情况只能用MOVE重建表。还要注意SHRINK之后索引状态。带COMPACT的收缩一般不会让索引失效但为了保险做完之后确认一下SELECT index_name, status FROM dba_indexes WHERE table_name LOG_HISTORY AND status VALID;如果是索引碎片导致的索引比表大用ALTER INDEX ... REBUILD或者COALESCEALTER INDEX app_user.idx_log_time COALESCE;COALESCE是合并同一 B 树分支内的空闲块不改变索引结构、不需要额外空间、可以在线执行比REBUILD温和得多适合做日常维护。REBUILD更彻底但需要额外的临时空间大约等于索引大小的 1.2 倍而且只对建立在普通堆表上的索引有效。我的习惯是日常用COALESCE一年一次的维护窗口用REBUILD。4.2 分区滚动与数据生命周期如果一张表是持续增长的流水/日志类数据再怎么收缩都是治标最终方案一定是分区加滚动归档。这也是我在容量规划里最愿意推荐的做法。设计上通常按时间做范围分区比如按月或按天。分区的好处不只是查询裁剪更重要的是归档和清理变成元数据操作秒级完成几乎不产生 UNDO 和 REDO-- 先把老分区交换成独立表 ALTER TABLE app_user.log_history EXCHANGE PARTITION P202312 WITH TABLE app_user.log_history_202312; -- 确认数据没问题后直接干掉分区 ALTER TABLE app_user.log_history DROP PARTITION P202312;DROP PARTITION会让全局索引失效新版本支持加UPDATE GLOBAL INDEXES子句在线维护老版本则要准备重建全局索引。如果表上全是本地索引LOCAL INDEX那DROP PARTITION顺带就把索引一起删了完全不用额外操作——这也是我一直建议流水表用本地索引的原因维护成本低分区操作几乎无感。滚动的落地形式可以很简单一个存储过程每月初把超过保留期的分区逐个交换出去、导出到一个归档表空间或直接expdp到文件、然后删分区再配合DBMS_SCHEDULER定时跑。保留期怎么定不要拍脑袋跟业务方确认查询的最远时间范围一般留 12 到 24 个月。定完之后容量需求就变成可计算的了单分区大小 × 分区数量 表空间需求一目了然。4.3 预警脚本与值班规范真正省心的库问题都是在 80% 水位就被处理掉的而不是等 100% 报错再来救火。预警脚本我一般按分级 看增长速率两个维度做。第一级是水位告警阈值用 85%只提醒不升级第二级是 92%推给当天值班的 DBA第三级是数据文件可增长余量为 0 或磁盘剩余空间低于 100G这个是真正的紧急级别。判断脚本可以参考SELECT t.tablespace_name, ROUND(t.used_pct, 2) AS used_pct FROM (SELECT m.tablespace_name, m.used_percent AS used_pct FROM dba_tablespace_usage_metrics m) t WHERE t.used_pct 85 ORDER BY t.used_pct DESC;比水位更重要的是增长速率。同样 85% 的两个表空间一个每天涨 100MB一个每天涨 5GB紧急程度完全不同。我习惯每天定时把水位快照写进一张自建的监控表然后算 7 日平均增长量SELECT tablespace_name, ROUND(AVG(used_mb), 2) AS avg_used_mb, ROUND((MAX(used_mb) - MIN(used_mb)) / GREATEST(COUNT(*) - 1, 1), 2) AS daily_growth_mb FROM dba_ts_monitor WHERE collect_time SYSDATE - 7 GROUP BY tablespace_name ORDER BY daily_growth_mb DESC;有了日均增长和剩余可增长空间就能算出预计多少天后满这个数字比单纯的水位百分比有说服力得多跟业务方申请扩容预算时也好解释。执行层面用DBMS_SCHEDULER建个定时任务每小时跑一次检查超过阈值就用UTL_MAIL发邮件或者写到一张告警表里由监控平台捞取。关键是要有闭环告警推给谁、多久没响应要升级、处置完了怎么记录这些流程上的东西比脚本本身更重要。我见过太多库告警是有的但没人看最后还是要等应用报错。4.4 常见问题速查表与避坑清单上面讲的都是方法这里把高频问题和对应处置收成一张速查表值班的时候可以直接照着走。现象最可能原因第一步动作ORA-01653表无法扩展表空间无可用连续空间查段排行 数据文件增长余量ORA-01654索引无法扩展索引表空间满或索引膨胀查索引段大小考虑 REBUILD 或清理无用索引ORA-01658无法创建初始区新建对象时空间不足或配额限制查用户配额DBA_TS_QUOTAS和表空间水位ORA-01652临时表空间无法扩展大排序/哈希/并行导致临时段暴涨V$TEMPSEG_USAGE定位会话和 SQL_IDORA-30036UNDO 无法扩展长查询按住 UNDO 不回收或 UNDO 太小查V$UNDOSTAT的MAXQUERYLEN使用率不降但数据已删段 HWM 未降或对象在回收站查DBA_RECYCLEBIN做SHRINK SPACE自动扩展开了但还是满MAXBYTES到顶或磁盘没空间查文件系统/ASM 剩余空间检查是否撞 32G 上限几条从实际操作里总结出来的避坑经验我认为比脚本本身更值钱扩容前先看磁盘不要只看数据库。数据文件的MAXSIZE只是一个数字物理磁盘写满了照样报错而且磁盘满的影响比表空间满严重得多会牵连到归档日志、备份、甚至实例挂起。改动前记录改动后核对。每次加文件、切默认表空间我都会把变更命令记进变更单改完立刻复查一遍相关视图。有一次在 RAC 上只在一个节点改了 UNDO 参数另一个节点没生效切换后出现跨实例的异常事后才知道参数要带SID*。不要在告警最密集的时候做治理动作。表空间告急了先加文件让业务恢复漂移、迁移、重建这些动作放窗口期做。应急和治理混在一起很容易在压力下出错。慎碰隐藏参数。网上流传过一些和 UNDO 自动调节相关的隐藏参数调整方法我个人的态度是除非有明确的问题证据且经过测试环境验证否则生产上不要动。隐藏参数没有官方支持升级之后行为可能变化出问题很难排查。扩容完了要回头找根因。这一条最重要。每次扩容之后都问一句为什么它长这么快是业务真的在长还是某个 SQL 或某个程序在疯狂写。前者是容量规划问题后者是代码问题两者的处理方式差得很远。我见过一个库表空间一年扩了八次最后发现是某个采集程序把调试日志写进了生产表加了 20 个文件都压不住把日志级别调低之后半年没再告警。这套排查思路和处理手段在永久表空间、临时表空间、UNDO 表空间上都够用。分区滚动和在线重定义那套更细的操作涉及交换分区后的约束处理、全局索引重建策略、以及DBMS_REDEFINITION的分步实操展开讲篇幅会很长。