大家好,我是睿。
表碎片是Oracle DBA日常维护中经常遇到的问题。很多表在经过大量数据插入和删除后,高水位线一直居高不下,即使实际数据已经很少,全表扫描时还是会读很多无效块,导致性能变差。
最近我专门做了一个实验,用同一张表对比了 SHRINK 和 MOVE 两种缩表方式的实际效果,包括操作步骤、资源消耗、整理后的段大小和索引情况等。今天把整个过程完整分享出来,供大家参考。
一、表碎片是怎么产生的?
简单来说,当表频繁INSERT数据后,又执行大量DELETE操作时,高水位线只会上升,不会自动下降。DELETE只会把数据块标记为空闲,但高水位线位置不变。所以全表扫描时,Oracle仍然会扫描到原来的高水位线,产生大量无效逻辑读。
二、两种主流缩表方式
- SHRINK方式(10g引入,支持在线操作)
必须先启用行移动ALTERTABLEmary.testENABLEROWMOVEMENT;第一步:压缩整理(不释放空间)ALTERTABLEmary.test SHRINK SPACE COMPACT;第二步:真正释放空间并处理索引ALTERTABLEmary.test SHRINK SPACECASCADE;- MOVE方式(效果最彻底)
ALTERTABLEmary.test MOVE;MOVE完成后,所有相关索引都会失效,必须重建:ALTERINDEXidx_test_id REBUILD ONLINE;三、如何判断表是否存在碎片?
我常用下面这个SQL快速查看:
setlinesize500setpagesize300col ownerfora15 col table_namefora10SELECTowner,table_name,num_rows,ROUND((blocks*8)/1024,2)"HIGH_WATER_MB",ROUND((num_rows*avg_row_len/1024/1024),2)"REAL_DATA_MB",ROUND((blocks*8-num_rows*avg_row_len/1024-blocks*8*0.1)/1024,2)"WASTE_MB"FROMdba_tablesWHEREowner='MARY'ANDtable_name='TEST';如果想看得更详细,可以使用 show_space 存储过程(适用于ASSM表空间): show_space完整存储过程createorreplaceprocedureshow_space(p_segname_1invarchar2,p_spaceinvarchar2default'MANUAL',p_type_1invarchar2default'TABLE',p_analyzedinvarchar2default'N',p_owner_1invarchar2defaultuser)asp_segname varchar2(100);p_type varchar2(10);p_owner varchar2(30);l_unformatted_blocks number;l_unformatted_bytes number;l_fs1_blocks number;l_fs1_bytes number;l_fs2_blocks number;l_fs2_bytes number;l_fs3_blocks number;l_fs3_bytes number;l_fs4_blocks number;l_fs4_bytes number;l_full_blocks number;l_full_bytes number;l_free_blks number;l_total_blocks number;l_total_bytes number;l_unused_blocks number;l_unused_bytes number;l_LastUsedExtFileId number;l_LastUsedExtBlockId number;l_LAST_USED_BLOCK number;procedurep(p_labelinvarchar2,p_numinnumber)isbegindbms_output.put_line(rpad(p_label,40,'.')||p_num);end;beginp_segname :=upper(p_segname_1);p_owner :=upper(p_owner_1);p_type :=p_type_1;if(p_type_1='i'orp_type_1='I')thenp_type :='INDEX';endif;if(p_type_1='t'orp_type_1='T')thenp_type :='TABLE';endif;if(p_type_1='c'orp_type_1='C')thenp_type :='CLUSTER';endif;dbms_space.unused_space(segment_owner=>p_owner,segment_name=>p_segname,segment_type=>p_type,total_blocks=>l_total_blocks,total_bytes=>l_total_bytes,unused_blocks=>l_unused_blocks,unused_bytes=>l_unused_bytes,LAST_USED_EXTENT_FILE_ID=>l_LastUsedExtFileId,LAST_USED_EXTENT_BLOCK_ID=>l_LastUsedExtBlockId,LAST_USED_BLOCK=>l_LAST_USED_BLOCK);ifp_space='MANUAL'or(p_space<>'auto'andp_space<>'AUTO')thendbms_space.free_blocks(segment_owner=>p_owner,segment_name=>p_segname,segment_type=>p_type,freelist_group_id=>0,free_blks=>l_free_blks);p('Free Blocks',l_free_blks);endif;p('Total Blocks',l_total_blocks);p('Total Bytes',l_total_bytes);p('Unused Blocks',l_unused_blocks);p('Unused Bytes',l_unused_bytes);p('Last Used Ext FileId',l_LastUsedExtFileId);p('Last Used Ext BlockId',l_LastUsedExtBlockId);p('Last Used Block',l_LAST_USED_BLOCK);/*IF the segment is analyzed */ifp_analyzed='Y'thendbms_space.space_usage(segment_owner=>p_owner,segment_name=>p_segname,segment_type=>p_type,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(rpad(' ',50,'*'));dbms_output.put_line('The segment is analyzed');p('0% -- 25% free space blocks',l_fs1_blocks);p('0% -- 25% free space bytes',l_fs1_bytes);p('25% -- 50% free space blocks',l_fs2_blocks);p('25% -- 50% free space bytes',l_fs2_bytes);p('50% -- 75% free space blocks',l_fs3_blocks);p('50% -- 75% free space bytes',l_fs3_bytes);p('75% -- 100% free space blocks',l_fs4_blocks);p('75% -- 100% free space bytes',l_fs4_bytes);p('Unused Blocks',l_unformatted_blocks);p('Unused Bytes',l_unformatted_bytes);p('Total Blocks',l_full_blocks);p('Total bytes',l_full_bytes);endif;end;/使用方法:
SETSERVEROUTPUTONEXECshow_space('TEST','AUTO','TABLE','Y','MARY');四、完整实验过程
我新建了两张测试表 mary.test2026、mary.test2026_2,分别插入约500万行数据,然后DELETE全量数据,制造碎片,分别用两种方式处理并观察效果。
实验准备(制造碎片):
创建测试表
CREATETABLEmary.test2026(id NUMBERprimarykey,name VARCHAR2(50),create_timeDATE);插入测试数据
BEGINFORiIN1..5000000LOOPINSERTINTOmary.test2026VALUES(i,'test'||i,SYSDATE);ENDLOOP;COMMIT;END;/创建第二张测试表、插入数据、添加主键
createtablemary.test2026_2asselect*frommary.test2026;altertablemary.test2026_2modify(id NUMBERconstraintmary_pkprimarykey);收集统计信息
executedbms_stats.gather_table_stats('MARY','TEST2026');executedbms_stats.gather_table_stats('MARY','TEST2026_2');查看两张测试表的段大小
col segment_namefora40selectsegment_name,sum(bytes)/1024/1024as"size_MB"fromdba_segmentswheresegment_namein('TEST2026','TEST2026_2')andowner='MARY'groupbysegment_name;SEGMENT_NAME size_MB
TEST2026 168
TEST2026_2 168
查看两张表的高水位线与碎片情况
setlinesize500setpagesize300col ownerfora15 col table_namefora10setlinesize500setpagesize300col ownerfora15 col table_namefora10SELECTowner,table_name,num_rows,ROUND((blocks*8)/1024,2)"HIGH_WATER_MB",ROUND((num_rows*avg_row_len/1024/1024),2)"REAL_DATA_MB",ROUND((blocks*8-num_rows*avg_row_len/1024-blocks*8*0.1)/1024,2)"WASTE_MB"FROMdba_tablesWHEREowner='MARY'ANDtable_namein('TEST2026','TEST2026_2');OWNER TABLE_NAME NUM_ROWS HIGH_WATER_MB REAL_DATA_MB WASTE_MB
MARY TEST2026 5000000 166.54 123.98 25.91
MARY TEST2026_2 5000000 166.54 123.98 25.91
删除全部数据
DELETEFROMmary.test2026;DELETEFROMmary.test2026_2;COMMIT;重新收集统计信息
executedbms_stats.gather_table_stats('MARY','TEST2026');executedbms_stats.gather_table_stats('MARY','TEST2026_2');这里我们可以看到,数据量虽然全部删除,但是两张表的数据段大小并没有减少
col segment_namefora40selectsegment_name,sum(bytes)/1024/1024as"size_MB"fromdba_segmentswheresegment_namein('TEST2026','TEST2026_2')andowner='MARY'groupbysegment_name;SEGMENT_NAME size_MB
TEST2026 168
TEST2026_2 168
通过以下SQL能够更容易看到实际占用空间与碎片空间的比对。高水位线很高,碎片非常严重。
setlinesize500setpagesize300col ownerfora15 col table_namefora10setlinesize500setpagesize300col ownerfora15 col table_namefora10SELECTowner,table_name,num_rows,ROUND((blocks*8)/1024,2)"HIGH_WATER_MB",ROUND((num_rows*avg_row_len/1024/1024),2)"REAL_DATA_MB",ROUND((blocks*8-num_rows*avg_row_len/1024-blocks*8*0.1)/1024,2)"WASTE_MB"FROMdba_tablesWHEREowner='MARY'ANDtable_namein('TEST2026','TEST2026_2');OWNER TABLE_NAME NUM_ROWS HIGH_WATER_MB REAL_DATA_MB WASTE_MB
MARY TEST2026 0 166.54 0 149.89
MARY TEST2026_2 0 166.54 0 149.89
实验1:对mary.test2026表执行SHRINK
ALTERTABLEmary.test2026ENABLEROWMOVEMENT;ALTERTABLEmary.test2026 SHRINK SPACE COMPACT;ALTERTABLEmary.test2026 SHRINK SPACECASCADE;实验2:对mary.test2026_2执行MOVE(重新制造碎片后)
ALTERTABLEmary.test2026_2 MOVE;重新收集统计信息
executedbms_stats.gather_table_stats('MARY','TEST2026');executedbms_stats.gather_table_stats('MARY','TEST2026_2');观测实验结果:
先检查索引情况:
可以看到SHRINK方式后索引状态正常,MOVE方式则导致MARY_PK索引失效。
setlinesize600setpagesize500col INDEX_NAMEfora20 col TABLE_OWNERfora20 col TABLE_NAMEfora30 colSTATUSfora30selectINDEX_NAME,TABLE_OWNER,TABLE_NAME,STATUSfromdba_indexeswhereTABLE_OWNER='MARY';INDEX_NAME TABLE_OWNER TABLE_NAME STATUS
SYS_C009491 MARY TEST2026 VALID
MARY_PK MARY TEST2026_2 UNUSABLE
(特殊说明:在原实验中,我此处做出来的结果MARY_PK没有失效,因为所有数据都被清空的原因,但如果表中还有数据,索引一定是会失效的)
重建test2026_2表的失效索引
ALTERINDEXmary.MARY_PK REBUILD ONLINE;setlinesize500setpagesize300col ownerfora15 col table_namefora10setlinesize500setpagesize300col ownerfora15 col table_namefora10SELECTowner,table_name,num_rows,ROUND((blocks*8)/1024,2)"HIGH_WATER_MB",ROUND((num_rows*avg_row_len/1024/1024),2)"REAL_DATA_MB",ROUND((blocks*8-num_rows*avg_row_len/1024-blocks*8*0.1)/1024,2)"WASTE_MB"FROMdba_tablesWHEREowner='MARY'ANDtable_namein('TEST2026','TEST2026_2');OWNER TABLE_NAME NUM_ROWS HIGH_WATER_MB REAL_DATA_MB WASTE_MB
MARY TEST2026 0 .01 0 .01
MARY TEST2026_2 0 0 0 0
(特殊说明:本次实验使用的数据段过于连续,并且是删除所有数据,所以实验结果并不明显,SHRINK方式仅残留了0.01MB的使用空间,在生产环境中剩余的残留空间会更多)
SHRINK完成后观察:
1、段大小明显下降,但仍有少量碎片残留(高水位没有降到最低)
2、索引状态正常,无需重建
3、全表扫描的逻辑读显著减少,但不是最优
MOVE完成后观察:
1、高水位线彻底归零,段大小降到了接近真实数据量 + PCTFREE 的水平
2、碎片清理非常干净,全表扫描逻辑读达到接近TRUNCATE的效果
3、但操作期间需要额外约等于表大小的空间(新旧段同时存在)
4、所有索引失效,必须重建
五、实验结论与选型建议
通过这次对比可以清晰看到:
MOVE在碎片整理效果上明显优于SHRINK,高水位线清理得更彻底,后续全表扫描性能更好,空间释放也更干净。但它需要额外空间,操作时锁表时间相对更长(11g以上可以用ONLINE方式缓解)。
SHRINK操作更灵活,对空间要求低,大部分过程在线,但碎片整理效果稍差,有时还会残留少量碎片。
我的实际建议:
1、如果空间充足、有维护窗口,优先使用MOVE,长远收益更高;
2、如果空间紧张,或者业务不能接受较长时间锁表,就选择SHRINK。
不管哪种方式,都强烈建议在业务低峰期或维护窗口操作。SHRINK的最后一步仍有锁,MOVE更需要提前评估空间。
操作完成后,建议重新收集统计信息:
表碎片整理看似简单,实际操作时需要综合考虑业务窗口、可用空间和性能要求。希望这次完整的实验记录能给大家一些参考。
你在生产环境中主要用哪种方式处理表碎片?欢迎在评论区分享你的经验或遇到的问题。
—— 睿 | Oracle性能优化老司机
专注硬核干货,欢迎一起卷技术!