1. ORA-01654 到底卡在哪一步
ORA-01654: 索引 XXX 无法通过 128 (在表空间 XXX 中) 扩展,这条报错我在生产库上处理过不下二十次,几乎每次都伴随业务方的电话:「订单提交不了」「批量任务卡住了」。它属于典型的空间类报错,跟 ORA-01653(表无法扩展)、ORA-01652(临时段无法扩展)是同一个家族,区别只在于卡住的对象不一样——这次是索引段在伸手要空间,表空间没给。
先把结论摆前面:这条报错不是数据库坏了,而是索引所在的段(segment)在插入、更新、分裂过程中需要申请一个新的区(extent),表空间里凑不出这么一整块连续的、大小符合要求的空闲空间,于是一次分配动作失败,前端语句直接抛错中断。数字128后面省略了单位,实际是128KB,代表段头记录里的下一区大小(next extent)。这个数字看着小,恰恰说明问题不一定出在「表空间总量不够」,而很可能是碎片或者文件的扩展上限被顶住了。
适合谁看这个问题?日常要接 DBA 工单的应用运维、刚接手 Oracle 库的后端开发、以及需要自己兜底小公司数据库的同学。本文按「搞清机制 → 定位定性 → 选方案 → 完整实操 → 避坑」的顺序走,最后给出的 SQL 都可以直接贴到 SQL*Plus 或任意客户端里跑。
1.1 报错文本里的四个信息位
这条报错格式固定,逐字段拆开看,每一段都对应一个决策点。
索引 XXX:这里会带上 schema 前缀,形如APP.IDX_ORDER_CUST。第一件事就是确认这个索引属于哪张表、什么类型(普通 B 树、唯一索引、位图索引、函数索引、分区索引的某个分区),因为不同类型处置手法不一样。位图索引重建代价和普通索引差很远,分区索引的局部索引可以只重建单个分区。
无法通过 128:这是请求分配的新区大小,单位 KB。为什么偏偏是 128?两种典型来源——表空间用了UNIFORM SIZE 128K的本地管理方式,所有区都整齐地是 128KB;或者这是个从老版本升上来的字典管理表空间,索引建的时候带了STORAGE (NEXT 128K)。如果是AUTOALLOCATE的本地管理表空间,段小于 1MB 时区大小是 64KB,超过 1MB 后变 1MB,所以报错里的数字通常是 64、1024、8192、65536 这一档,出现 128 反而不多见。看到 128,基本可以反推表空间的区管理方式。
在表空间 XXX 中:锁定战场。所有后续的空间查询、扩容操作都围绕这个表空间展开。
隐含的第四个信息是「时间点」——报错开始出现的时刻。这个信息报错本身不带,得靠告警日志(alert log)和业务反馈拼。如果是凌晨批量跑批时集中出现,多半是数据量增长顶到了水位;如果是全天零散出现,碎片或者文件达到 MAXSIZE 的可能性更大。
注意:不要一看到 ORA-01654 就去加数据文件。我见过不少案例,表空间总量还有几十 G,纯粹是连续空闲块凑不出来,加文件虽然能临时缓解,但碎片问题会持续恶化,几周后同样的报错卷土重来。
1.2 索引段扩张的物理过程与 next_extent
要理解这条报错,得先明白索引是怎么「长大」的。Oracle 的索引是一棵 B 树,叶子块里存的是「键值 + rowid」的排序结果。当你往表里插数据,对应的索引叶子块会逐渐填满,达到PCTFREE阈值后发生块分裂(split),老块里的数据被分一半到新块里——这就要向表空间申请新的数据块。
分配不是一块一块要的,Oracle 以「区」为单位整块申请。段的头部(segment header)维护着NEXT_EXTENT这个属性,告诉系统「我下次要多大的一块」。申请流程是:段头提出需求 → 表空间找一块大小足够、连续的空闲区 → 找到就分配,段的区数量 +1;找不到就报 ORA-01654。
关键就在「连续」两个字。表空间层面的空闲空间统计是按区(extent)粒度记录的,DBA_FREE_SPACE里每一行就是一个空闲区。一个表空间可能合计有 3GB 空闲,但分成八百个互不相邻的小块,最大的一块只有 512KB。这时候索引张口要 1MB,系统找不到 1MB 的连续块,直接拒绝。这就是碎片型 ORA-01654 的本质。
在AUTOALLOCATE的本地管理表空间里,Oracle 按段的当前大小阶梯式决定区大小:段小于 1MB 时每区 64KB,1MB 到 64MB 之间每区 1MB,64MB 到 1GB 之间每区 8MB,超过 1GB 后每区 64MB。所以大索引的NEXT_EXTENT往往已经涨到 64MB,对连续空间的要求很苛刻,碎片一旦形成就容易出事——这也是为什么这类报错在数据量大的库上更常见。
1.3 和邻近几个 ORA 错误的分工
很多人把这几个错误混为一谈,其实各有分工,判断错了方向就会白忙活。
| 错误码 | 受影响对象 | 本质原因 | 典型处置 |
|---|---|---|---|
| ORA-01653 | 表(table)段 | 表空间给不出需要的区 | 扩表空间、治碎片 |
| ORA-01654 | 索引(index)段 | 同上,只是对象是索引 | 扩表空间、重建索引、迁移 |
| ORA-01652 | 临时段 | 排序、哈希、全局临时表需要 temp 空间 | 扩临时表空间、优化 SQL |
| ORA-01688 | 回滚段 / UNDO | undo 表空间不足 | 扩 undo、调整 undo 保留策略 |
| ORA-01950 | 用户配额 | 用户在该表空间的配额用尽 | 调 quota 或给 UNLIMITED |
| ORA-01631 | 表的最大区数 | MAXEXTENTS限制 | 调大 maxextents(本地管理已少见) |
这张表值得贴在工位上。同一个表空间报 ORA-01653 和 ORA-01654 是常事,因为表和索引混放;但如果只报 01654 而表插入正常,说明索引单独占了一个紧张的表空间,这在「表一个表空间、索引另一个表空间」的规范部署里非常常见——索引表空间往往扩容不及时,成了短板。
2. 先定性再动手:三组查询把问题锁死
处置之前必须先判断「到底是哪种类型的空间不足」,这是整个流程里最容易被跳过、也最容易出错的环节。凭经验乱加文件,轻则浪费空间,重则掩盖了真正的问题。我习惯用三组查询快速定性,整个过程两分钟内能跑完。
2.1 第一组:表空间总账与数据文件水位
先看这个表空间有几个文件、多大、能不能自动扩、扩到哪为止。
SELECT file_id, file_name, ROUND(bytes/1024/1024, 2) AS 当前MB, autoextensible AS 自动扩展, ROUND(maxbytes/1024/1024, 2) AS 上限MB, ROUND(increment_by * 8192 / 1024 / 1024, 2) AS 每次扩MB, status, online_status FROM dba_data_files WHERE tablespace_name = 'TS_BIZ' ORDER BY file_id;这里有个经常被忽略的点:INCREMENT_BY的单位是数据块,不是字节也不是 MB,所以必须乘以块大小(默认 8KB)才能换算成 MB。上面这条 SQL 里我直接乘了 8192,如果你的库块大小是 16KB 或 32KB,要改成对应值,用SHOW PARAMETER db_block_size确认一下。
重点看两个数:AUTOEXTENSIBLE=NO的文件,bytes就是它的天花板,扩不动了就得RESIZE或者加新文件;AUTOEXTENSIBLE=YES的文件要看当前MB是否已经贴住上限MB,如果两者相等,说明自动扩展已经失效,这时候表面上看它是「自动扩展的」,实际上已经满了,特别容易误判。我就吃过这个亏:查autoextensible是 YES 就放心了,结果bytes和maxbytes一模一样,白折腾半小时。
再看文件总量:
SELECT tablespace_name, ROUND(SUM(bytes)/1024/1024, 2) AS 总大小MB, ROUND(SUM(GREATEST(maxbytes - bytes, 0))/1024/1024, 2) AS 还能自动扩MB FROM dba_data_files GROUP BY tablespace_name ORDER BY 总大小MB DESC;「还能自动扩 MB」这一列如果接近 0,说明这个表空间的自动扩展能力已经耗尽,只能手工介入。顺带在主机上跑一下df -h,确认文件系统还有空间——数据库层面允许扩,操作系统没地方写,一样会失败,而且报错信息会变得很隐晦。
2.2 第二组:目标索引段的家底与下一区大小
接下来聚焦报错的那个索引。
SELECT i.owner, i.index_name, i.table_name, i.index_type, i.status, i.tablespace_name, ROUND(i.initial_extent/1024, 2) AS 初始区KB, ROUND(i.next_extent/1024, 2) AS 下一区KB, ROUND(s.bytes/1024/1024, 2) AS 段大小MB, s.extents AS 区数量, s.blocks AS 占用块数 FROM dba_indexes i JOIN dba_segments s ON i.owner = s.owner AND i.index_name = s.segment_name WHERE i.index_name = 'IDX_ORDER_CUST';下一区KB就是报错文本里那个数字的来源,对照一下是不是 128 或 65536 之类。区数量和段大小MB能看出段的体量:一个 50MB 的索引和 5GB 的索引,处置策略完全不同,前者直接重建成本很低,后者得挑窗口。
想更精确地拿到「下一次分配到底要多大」,用DBMS_SPACE包最稳,它直接读段头,不依赖统计信息新鲜度:
SET SERVEROUTPUT ON DECLARE v_total_blocks NUMBER; v_total_bytes NUMBER; v_unused_blocks NUMBER; v_unused_bytes NUMBER; v_fid NUMBER; v_bid NUMBER; v_bno NUMBER; BEGIN DBMS_SPACE.UNUSED_SPACE( segment_owner => 'APP', segment_name => 'IDX_ORDER_CUST', segment_type => 'INDEX', total_blocks => v_total_blocks, total_bytes => v_total_bytes, unused_blocks => v_unused_blocks, unused_bytes => v_unused_bytes, last_used_extent_file_id => v_fid, last_used_extent_block_id => v_bid, last_used_block => v_bno); DBMS_OUTPUT.PUT_LINE('段总字节: ' || v_total_bytes); DBMS_OUTPUT.PUT_LINE('未使用字节: ' || v_unused_bytes); END; /顺带提醒一个坑:DBA_INDEXES.NEXT_EXTENT这个字段在某些版本上对分区索引、特别是全局索引的显示可能不准确,只有主段信息。分区索引出问题时,要去DBA_IND_PARTITIONS里逐个分区看,别被主表的空值误导。
2.3 第三组:碎片度与连续可用块
这是定性的核心。
SELECT tablespace_name, COUNT(*) AS 空闲块数, ROUND(SUM(bytes)/1024/1024, 2) AS 空闲合计MB, ROUND(MAX(bytes)/1024/1024, 2) AS 最大连续MB, ROUND(AVG(bytes)/1024, 2) AS 平均块KB FROM dba_free_space WHERE tablespace_name = 'TS_BIZ' GROUP BY tablespace_name;看「最大连续MB」。如果它小于索引的下一区大小,那根因就确认了——不是没空间,是空间碎了。举例:空闲合计 1.5GB,最大连续 32MB,而索引下一区 64MB,必然失败。这时候往表空间里加一个 4GB 的数据文件,能马上缓解(因为新文件的空闲空间是完整的一整块),但如果不解决碎片,几个月后又会在新文件上重现。
还要注意DBA_FREE_SPACE的粒度是区,不是块,所以「最大连续」实际上是「最大的那个空闲区」,它代表了单次能分配的上限。另外,本地管理表空间的位图里,如果相邻空闲区能被合并,Oracle 会自动合并——COALESCE的作用就在这儿。
顺便查一下DBA_TABLESPACE_USAGE_METRICS拿整体水位(这个视图在 10g 之后才有,且只对本地管理表空间准确):
SELECT tablespace_name, ROUND(used_space * 8192 / 1024 / 1024, 2) AS 已用MB, ROUND(tablespace_size * 8192 / 1024 / 1024, 2) AS 总MB, ROUND(used_space * 100 / tablespace_size, 2) AS 使用率 FROM dba_tablespace_usage_metrics WHERE tablespace_name = 'TS_BIZ';2.4 一张判别表定方向
三组查询跑完,把结果往这张表里套,方向基本就出来了。
| 现象组合 | 判定 | 首选动作 |
|---|---|---|
| 总大小 ≈ 已用,文件无自动扩展 | 真满 | 加数据文件 / RESIZE |
| 有自动扩展,但 bytes = maxbytes | 扩展上限顶住 | 调大 MAXSIZE 或 RESIZE |
| 空闲合计充足,最大连续 < 下一区 | 碎片 | 加文件(应急)+ 重建/迁移(治本) |
| 空闲充足,文件也未满,仍报错 | 用户配额或段属性异常 | 查 dba_ts_quotas、查 maxextents |
| 只在跑批时段报错 | 增长型压力 | 评估增长率后扩容 |
| 报错对象是临时表上的索引 | 检查临时表空间 | 扩 temp、查 dba_temp_free_space |
「用户配额」这一行容易被漏掉。虽然配额不足通常报 ORA-01950,但在某些场景下(比如通过 definer's rights 过程间接插入),错误可能被包装转化。查一下配额不费事:
SELECT username, tablespace_name, ROUND(bytes/1024/1024, 2) AS 已用MB, ROUND(max_bytes/1024/1024, 2) AS 配额MB FROM dba_ts_quotas WHERE tablespace_name = 'TS_BIZ';max_bytes = -1表示无限制(UNLIMITED TABLESPACE)。看到具体数字就要留意了。
3. 六种处置手段及其适用边界
方案没有优劣,只有匹配。下面这几种我按「应急 → 治本 → 预防」的顺序排,实际处置时经常是组合拳。
3.1 扩表空间:加文件与 RESIZE 的参数怎么算
最直接的手段。加文件:
ALTER TABLESPACE TS_BIZ ADD DATAFILE '/u01/app/oracle/oradata/ORCL/ts_biz04.dbf' SIZE 4G AUTOEXTEND ON NEXT 512M MAXSIZE 30G;SIZE 4G是初始大小,不要给太小,否则刚建完又要扩;NEXT 512M是每次自动扩展的增量,我一般设成 256M 到 1G 之间。设太小(比如 64M)会导致频繁触发扩展,扩展动作本身要短暂持有文件级锁,批量写入时会有感知;设太大则在文件系统上瞬间吃掉大片空间,留给其他库的余量不好控。
MAXSIZE要结合文件系统剩余空间和单文件理论上限一起算。8KB 块的小文件表空间,单个数据文件的理论上限是 4194304 个块 × 8KB ≈32GB;大文件表空间(bigfile)则是 4G 个块,理论能到 32TB。所以给普通表空间设MAXSIZE UNLIMITED其实是一种误导,实际到 32GB 就封顶了。稳妥做法是把 MAXSIZE 设在 30GB 以内,留出余量,或者干脆用大文件表空间规划大表。
扩已有文件:
ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/ORCL/ts_biz01.dbf' RESIZE 8G;RESIZE只能往大了改,不能小于当前已用空间(会报 ORA-03297)。改之前必须确认文件系统有余量,这条在测试环境无所谓,在生产环境改废了只能从备份恢复,属于高危操作。
扩容大小怎么定?别拍脑袋。看一眼索引所在表的近期增长:假设这个索引每周增长 2GB,上一次扩容在 3 周前,那么合理余量是「4 到 6 周的增量」,也就是 8GB 到 12GB。再留 20% 的碎片缓冲,最终给 12GB 到 15GB 比较踏实。这个算法没有任何玄学,就是「增长率 × 观察窗口 + 安全系数」。
注意:如果表空间是
AUTOALLOCATE且已经严重碎片化,新加的文件虽然能解决眼下的报错,但由于新文件会优先被使用,很快又会被写碎。加完文件后一定要安排一次碎片治理,别把它当成一劳永逸。
3.2 治碎片:REBUILD、SHRINK、COALESCE 怎么选
这三个命令都能改变索引段的物理分布,但作用机理完全不同,用错了没效果还会白耗资源。
ALTER INDEX ... COALESCE做的是「合并同一区内相邻的空闲块」,不解锁新的连续空间。它只对字典管理表空间或者UNIFORM分配的老式表空间有明显效果,对AUTOALLOCATE的索引基本是聊胜于无。执行代价低、不阻塞 DML(只短暂锁段头),可以作为低峰期的常规维护,但别指望靠它解决 ORA-01654。
ALTER INDEX APP.IDX_ORDER_CUST COALESCE;ALTER INDEX ... SHRINK SPACE是把段尾部的高水位线下移,把腾出来的空间还给表空间,效果立竿见影。但它有两个硬性前提:表空间必须启用 ASSM(自动段空间管理),也就是SEGMENT SPACE MANAGEMENT AUTO;索引不能是某些特殊类型(比如基于函数的位图索引组合等)。执行时建议分两步,先COMPACT在线做,再短暂锁住做SHRINK SPACE,把对业务的影响压到最小。
-- 第一步:在线整理,业务基本无感 ALTER INDEX APP.IDX_ORDER_CUST SHRINK SPACE COMPACT; -- 第二步:正式回收 HWM,短暂持锁 ALTER INDEX APP.IDX_ORDER_CUST SHRINK SPACE;ALTER INDEX ... REBUILD是最彻底的方案,整段重建,逻辑上等同于「新建一个索引 + 删掉旧的」。它天然解决了碎片问题,还能顺带更新统计信息、改变存储属性。代价也最大:需要一个与索引大小相当的空间放新段,还要有足够的排序空间(PGA 不够就落盘到临时表空间),以及对应的 UNDO 来支撑回滚。
ALTER INDEX APP.IDX_ORDER_CUST REBUILD ONLINE TABLESPACE TS_IDX PARALLEL 4 COMPUTE STATISTICS;关于参数的选择:ONLINE允许重建期间继续 DML,但代价是维护一张日志表,重建末尾要「回放」,总耗时可能翻倍;NOLOGGING能显著减少 REDO 生成,加速重建,但重建完成后的第一次 DML 仍会补写 REDO,且在 DataGuard 环境下可能导致备库块损坏(因为 NOLOGGING 操作不传日志),主备架构下要慎用;PARALLEL 4加速但有 CPU 和 IO 代价,而且结束后往往需要把并行度降回来,否则后续 DML 也会以并行方式跑,反而更慢。
ALTER INDEX APP.IDX_ORDER_CUST NOPARALLEL;这一句经常被忘。执行完并行重建不降并行度,索引的DEGREE一直挂着,后续单条插入可能变成并行执行计划,属于典型的「埋雷」。
3.3 挪位置:把索引迁到宽松表空间
如果原表空间短期内没法清理(比如还混着大量业务表),把索引挪走是性价比最高的做法。
ALTER INDEX APP.IDX_ORDER_CUST REBUILD TABLESPACE TS_IDX;一句命令同时完成「重建」和「迁移」,重建期间原表空间只承担旧段的读取,新段的分配全部落在目标表空间。前提是目标表空间的空间要算够:新段大小 ≈ 原段大小 × (1 + PCTFREE 调整带来的膨胀系数),一般按原大小的 1.1 到 1.3 倍预留。
遇到分区索引要分开处理。局部索引(LOCAL)可以逐分区搬,业务影响小:
ALTER INDEX APP.IDX_ORD_PART REBUILD PARTITION P_2026_01 TABLESPACE TS_IDX;全局索引(GLOBAL)跟着表分区走,需要整体重建或者配合UPDATE GLOBAL INDEXES操作。分区表做DROP PARTITION时如果不加这个子句,全局索引会整段变成 UNUSABLE,后续查询直接走全表扫描,性能断崖式下跌——这个问题我见过太多次,一定要在变更脚本里写死。
ALTER TABLE APP.T_ORDER DROP PARTITION P_2025_01 UPDATE GLOBAL INDEXES;注意:
UPDATE GLOBAL INDEXES会让 DROP 操作变慢(要同步维护索引),但换来的是索引始终可用。凌晨窗口够长就用它,窗口紧张可以拆成「DROP → 重建全局索引」两步,但中途索引不可用,要评估业务能否容忍。
3.4 改属性:next_extent、MAXSIZE、限额
修改索引的STORAGE子句在本地管理表空间下绝大多数情况无效,这是新手最容易踩的坑。执行ALTER INDEX ... STORAGE (NEXT 1M)系统可能不报错,但实际分配行为仍由表空间的区管理策略决定,NEXT_EXTENT该多大还是多大。只有在字典管理表空间里,STORAGE子句才真正生效。所以看到「网上说改 STORAGE 就行」的文章,先确认你的表空间管理方式:
SELECT tablespace_name, extent_management, allocation_type, segment_space_management FROM dba_tablespaces WHERE tablespace_name = 'TS_BIZ';EXTENT_MANAGEMENT = LOCAL且ALLOCATION_TYPE = SYSTEM(即 AUTOALLOCATE)时,改存储参数没意义;ALLOCATION_TYPE = UNIFORM时,区大小固定,也改不了;只有DICTIONARY才是传统的那套规则。
真正能改的是数据文件的MAXSIZE:
ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/ORCL/ts_biz01.dbf' AUTOEXTEND ON NEXT 512M MAXSIZE 20G;这条命令适合「文件已到上限但磁盘还有空间」的场景,是成本最低的修复方式之一。执行前用df -h确认文件系统余量,别让操作系统层面的空间写满,那会引发比 ORA-01654 严重得多的问题。
用户配额:
ALTER USER APP QUOTA UNLIMITED ON TS_BIZ; -- 或者给具体值 ALTER USER APP QUOTA 10G ON TS_BIZ;生产环境我倾向给具体值而不是 UNLIMITED,原因很简单——限制一个失控的批处理任务把整个表空间撑爆,比事后清理要省事得多。配额这层「保险丝」,加不加取决于团队对上游代码的信任程度。
3.5 减体积:分区维护与历史数据瘦身
有些表空间的紧张是历史数据堆出来的。订单表三年没清过,索引自然跟着膨胀。这种情况下扩容只是把问题往后推,真正的解法是归档。
分区表用DROP PARTITION或TRUNCATE PARTITION最干脆,注意配合全局索引维护(见上一节)。非分区表可以走「在线重定义」把表改造成分区表,再用ALTER TABLE ... MOVE PARTITION把冷数据搬到低速表空间,索引重建时自然落到不同的位置。
-- 冷分区搬到历史表空间,同时释放原表空间 ALTER TABLE APP.T_ORDER MOVE PARTITION P_2024_12 TABLESPACE TS_HIS; -- 之后必须重建该分区的索引,否则索引处于 UNUSABLE ALTER INDEX APP.IDX_ORD_PART REBUILD PARTITION P_2024_12;MOVE PARTITION之后局部索引会失效,这一点必须写进变更检查清单。至于清理策略(保留多少个月、归档到哪、是否压缩存储),属于业务决策,技术上能做的就是把这些动作做成可重复执行的脚本,别每次手工敲。
4. 一次完整的现场处置记录(模拟案例)
下面这个案例是把前面几节串起来的一个完整走位,人物和库名都是代称,数字做了调整,但每一步的思路和真实场景一致。
4.1 现象与第一手信息
某日 09:12 开始,业务系统陆续报「订单创建失败」。日志里刷屏的是:
ORA-01654: unable to extend index APP.IDX_ORDER_CUST by 65536 in tablespace TS_ORDER注意这里的数字是 65536,即 64MB,说明这个索引段已经超过 1GB,处在 64MB 一区的档位。第一反应是「表空间满了」,但先别急,按流程走。
第一组查询结果:
TS_ORDER共 4 个数据文件,合计 64GB- 三个文件
AUTOEXTENSIBLE = YES,MAXSIZE都已设 20GB,且bytes = maxbytes,全部顶格 - 第四个文件 4GB,
AUTOEXTENSIBLE = NO
第二组查询结果:IDX_ORDER_CUST段大小 3.2GB,区数量 51,下一区 64MB。
第三组查询结果:空闲合计 1.8GB,最大连续块仅 24MB。
到这里结论很清楚了:总量上 4GB 空闲(64GB 总量减去已用),但最大连续块 24MB,凑不出 64MB,属于碎片型 + 扩展上限顶格的复合问题。
4.2 一次无效的尝试和它的原因
现场有人第一时间执行了加数据文件:
ALTER TABLESPACE TS_ORDER ADD DATAFILE '.../ts_order05.dbf' SIZE 2G;执行成功,但报错没停。原因很直接:新文件虽然提供了 2GB 的完整连续空间,看起来够了,但索引是在原文件上「继续长」的——段的数据块并不要求全部落在同一个文件里,新增的区会优先落到新文件,理论上确实能缓解。问题是2GB 对一个 3.2GB 且在高速增长的索引来说太小,两小时内又被吃干,再次触发同样的报错。
这个教训很典型:应急扩容的尺寸必须覆盖「当前缺口 + 一段时间内的增长」,不能只解决眼前那一次分配。如果当时直接给 16GB,配合自动扩展,至少能撑两周,把窗口留到碎片治理上。
4.3 分步处置
确认根因后,处置分三步走,全程盯业务监控。
第一步,补齐自动扩展能力,先稳住:
ALTER DATABASE DATAFILE '.../ts_order01.dbf' AUTOEXTEND ON NEXT 1G MAXSIZE 24G; ALTER DATABASE DATAFILE '.../ts_order02.dbf' AUTOEXTEND ON NEXT 1G MAXSIZE 24G; ALTER DATABASE DATAFILE '.../ts_order03.dbf' AUTOEXTEND ON NEXT 1G MAXSIZE 24G;把三个文件的 MAXSIZE 从 20GB 提到 24GB,前提是文件系统还有 12GB 以上余量(df -h显示余量 40GB,安全)。这一步立刻让报错停止,因为 64MB 的连续空间请求可以由自动扩展补上。
第二步,重建索引碎片,从根上解决:
-- 先确认临时表空间够不够,重建大索引需要排序空间 SELECT tablespace_name, ROUND(tablespace_size * 8192 / 1024 / 1024, 2) AS 总MB, ROUND(free_space * 8192 / 1024 / 1024, 2) AS 空闲MB FROM dba_temp_free_space;临时表空间剩 18GB,索引 3.2GB,按 1.5 倍预留约 4.8GB,空间充足。于是挑业务低谷执行:
ALTER INDEX APP.IDX_ORDER_CUST REBUILD ONLINE PARALLEL 4 COMPUTE STATISTICS; ALTER INDEX APP.IDX_ORDER_CUST NOPARALLEL;重建耗时 22 分钟,其间业务正常读写,CPU 峰值到 60%,在可接受范围。重建后段大小从 3.2GB 降到 2.1GB,区数量从 51 降到 34,回收了约 1.1GB。
第三步,把散落的空闲空间收拢。重建后表空间里多出一些零散的 64MB 区块,用DBA_FREE_SPACE复核:
SELECT COUNT(*) AS 空闲块数, ROUND(SUM(bytes)/1024/1024, 2) AS 合计MB, ROUND(MAX(bytes)/1024/1024, 2) AS 最大连续MB FROM dba_free_space WHERE tablespace_name = 'TS_ORDER';最大连续块升到 3.2GB,问题解除。
4.4 回归验证
处置之后要做三件事,缺一不可。
一是确认索引状态正常:
SELECT owner, index_name, status, tablespace_name, last_analyzed FROM dba_indexes WHERE index_name = 'IDX_ORDER_CUST';STATUS必须是VALID。重建期间如果并发事务有冲突,可能留下UNUSABLE,一旦如此必须立刻再重建一次。
二是确认统计信息已更新,否则优化器可能仍按旧数据选错执行计划。COMPUTE STATISTICS会同步更新,但如果用了NOLOGGING或者别的方式,记得手工补:
EXEC DBMS_STATS.GATHER_INDEX_STATS('APP', 'IDX_ORDER_CUST');三是盯三天的业务指标。包括:订单创建的成功率、该表的 DML 响应时间、表空间的日增量。第三项最关键,用它反推下一次扩容的时点,写进运维日历。
5. 排查速查表与踩坑心得
5.1 常见问题速查表
| 现象 | 可能原因 | 验证方式 | 处置 |
|---|---|---|---|
| 报错数字是 128 | 表空间为 UNIFORM 128K 或字典管理 | 查dba_tablespaces.allocation_type | 加空间或迁移到 AUTOALLOCATE 表空间 |
| 空闲合计大,仍报错 | 碎片,最大连续块不足 | dba_free_space的 MAX(bytes) | 加文件应急 + 重建索引 |
| 加文件后很快复发 | 新文件被写碎 / 尺寸给太小 | 观察dba_free_space变化 | 一次给足,配合碎片治理 |
| 文件显示可自动扩但仍报错 | bytes 已到 maxbytes | 对比bytes与maxbytes | 调大 MAXSIZE 或 RESIZE |
| 只在夜间批处理时报错 | 增长型压力,白天空闲 | 对比报错时间与批处理时间 | 评估增量后扩容 |
| 重建时报 ORA-01652 | 临时表空间不足 | dba_temp_free_space | 扩 temp 或减少并行度 |
| 重建后索引 UNUSABLE | 并发 DML 冲突 | dba_indexes.status | 重新执行 REBUILD |
| 报错对象是分区索引 | 需定位到具体分区 | dba_ind_partitions.status | 单分区重建 |
| 改 STORAGE 无效果 | 本地管理表空间不认 | 查extent_management | 改用扩容/迁移方案 |
| 分区 DROP 后查询变慢 | 全局索引失效 | dba_indexes.status | 重建或加 UPDATE GLOBAL INDEXES |
5.2 那些文档里不写的坑
坑一:AUTOEXTEND 的「假在线」。前面提过一次,这里再强调——DBA_DATA_FILES.AUTOEXTENSIBLE显示 YES,不代表还能扩。必须同时比对BYTES和MAXBYTES。我见过巡检脚本只查autoextensible就报「正常」,结果库炸了才发现文件早到顶。巡检 SQL 里一定要加这个条件。
坑二:重建大索引的排序空间被低估。索引 10GB,临时表空间只剩 8GB,REBUILD跑到 90% 报 ORA-01652,前面的时间全白费。经验值是按索引大小的 1.2 到 1.5 倍预留临时空间,并且PARALLEL越高,每个并行进程都要独立的排序区,总需求会成倍上涨。并行度 4 的重建,临时空间按 2 倍留都不算多。
坑三:ONLINE 重建的「尾部卡顿」。ONLINE模式下,重建主体完成后要把期间捕获的 DML 日志回放到新索引上,这段时间表现为「进度条不动」,持续时间取决于重建时长和并发 DML 量。业务高峰期做这个操作,回放阶段可能比主体还长,而且会持有更重的锁。我的习惯是必须在业务低谷做,别看它叫 ONLINE 就以为真的随时随地。
坑四:COALESCE被当成万能药。网上很多文章说「索引碎片用 COALESCE 就行」,在 AUTOALLOCATE + ASSM 的环境里,它的效果微乎其微。判断标准很简单:如果DBA_FREE_SPACE的最大连续块远小于下一区,COALESCE 救不了你,只有 REBUILD 或加文件。
坑五:忘记降并行度。前面写过,这里再点一次。并行重建后索引的DEGREE会保留,后续单条 INSERT 都可能走并行,CPU 飙高、锁竞争加剧,问题极难排查。
坑六:在备库/灾备环境忽略 NOLOGGING。NOLOGGING重建不产生完整 REDO,主库重建后备库对应数据块可能损坏。主备架构下这个操作要么禁用,要么重建后立刻做一次全备。这个坑不常遇到,遇到一次就是事故。
坑七:RESIZE缩小文件。有些人想「先把文件缩小再加」,释放文件系统空间。RESIZE小于当前 HWM 会报 ORA-03297,而且缩小操作本身风险极高,除非你明确知道高水位在哪。生产环境我基本不做缩小,宁可加新文件。
坑八:只盯表空间不看文件系统。数据库层面所有检查都通过,ADD DATAFILE执行时报操作系统错误,因为/u01满了。每次扩容前跑一次df -h,两秒钟的事,能省掉大量来回。
提示:所有 DDL 类操作(REBUILD、RESIZE、DROP PARTITION)在 executing 之前,先把执行计划、预估耗时、回滚方案写进变更单。这类操作一旦中断,恢复成本远高于多做一次预演。
5.3 日常监控:把阈值报警做在前面
ORA-01654 是可以被提前发现的那类错误,前提是监控做到位。
第一条监控,表空间使用率:
SELECT tablespace_name, ROUND(used_space * 100 / NULLIF(tablespace_size, 0), 2) AS 使用率, ROUND((tablespace_size - used_space) * 8192 / 1024 / 1024, 2) AS 剩余MB FROM dba_tablespace_usage_metrics WHERE used_space * 100 / NULLIF(tablespace_size, 0) > 80 ORDER BY 使用率 DESC;阈值定 80% 告警、90% 严重告警。注意这个视图算的是「已分配空间内的使用率」,它不包含文件还能自动扩展的部分,所以要配合第二条一起看。
第二条监控,自动扩展余量:
SELECT tablespace_name, ROUND(SUM(GREATEST(maxbytes - bytes, 0)) / 1024 / 1024, 2) AS 剩余可扩MB, COUNT(*) FILTER (WHERE autoextensible = 'YES' AND bytes >= maxbytes) AS 顶格文件数 FROM dba_data_files GROUP BY tablespace_name ORDER BY 剩余可扩MB;顶格文件数 > 0就要处理了,哪怕当前使用率只有 70%,因为增长是不可控的。(FILTER子句是较新版本支持,老版本改成SUM(CASE WHEN ... THEN 1 ELSE 0 END)。)
第三条监控,碎片指数:
SELECT tablespace_name, ROUND(MAX(bytes) / 1024 / 1024, 2) AS 最大连续MB, ROUND(SUM(bytes) / 1024 / 1024, 2) AS 空闲合计MB, ROUND(MAX(bytes) * 100 / NULLIF(SUM(bytes), 0), 2) AS 连续度百分比 FROM dba_free_space GROUP BY tablespace_name HAVING SUM(bytes) > 0 ORDER BY 连续度百分比;「连续度百分比」低于 10% 就该警惕了,说明空闲空间极度分散。这个指标比单纯看使用率更能提前预警碎片型故障。
把这三条做成每日巡检脚本,结果推送到值班群,配合一个「表空间增长趋势」的周报(记录每周的表空间总量与增量),扩容决策就有数据支撑,不用等报错来催。我在实际维护中把这三条跑了两年多,ORA-01654 从「突然爆发」变成了「提前一周就知道要加空间」,处置节奏完全不一样。
最后分享一个自己的习惯:每次处理完这类空间问题,我都会把这个表空间的「当前总量、周增量、本次扩容后的预计可用周数」记到一张维护台账里。这张台账价值极高——下次再报错时,翻开就知道上次加到多少、撑了多久、增长斜率有没有变化。比起每次从零开始查,效率差着数量级。