大家好,我是睿。
表碎片是Oracle DBA日常维护中经常遇到的问题。很多表在经过大量数据插入和删除后,高水位线一直居高不下,即使实际数据已经很少,全表扫描时还是会读很多无效块,导致性能变差。
最近我专门做了一个实验,用同一张表对比了 SHRINK 和 MOVE 两种缩表方式的实际效果,包括操作步骤、资源消耗、整理后的段大小和索引情况等。今天把整个过程完整分享出来,供大家参考。
一、表碎片是怎么产生的?
简单来说,当表频繁INSERT数据后,又执行大量DELETE操作时,高水位线只会上升,不会自动下降。DELETE只会把数据块标记为空闲,但高水位线位置不变。所以全表扫描时,Oracle仍然会扫描到原来的高水位线,产生大量无效逻辑读。
二、两种主流缩表方式
- SHRINK方式(10g引入,支持在线操作)
sql
必须先启用行移动
ALTER TABLE mary.test ENABLE ROW MOVEMENT;
第一步:压缩整理(不释放空间)
ALTER TABLE mary.test SHRINK SPACE COMPACT;
第二步:真正释放空间并处理索引
ALTER TABLE mary.test SHRINK SPACE CASCADE;
- MOVE方式(效果最彻底)
sql
ALTER TABLE mary.test MOVE;
MOVE完成后,所有相关索引都会失效,必须重建:
ALTER INDEX idx_test_id REBUILD ONLINE;
三、如何判断表是否存在碎片?
我常用下面这个SQL快速查看:
sql
set linesize 500
set pagesize 300
col owner for a15
col table_name for a10
SELECT owner, 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"
FROM dba_tables
WHERE owner = 'MARY' AND table_name = 'TEST';
如果想看得更详细,可以使用 show_space 存储过程(适用于ASSM表空间):
show_space完整存储过程
create or replace procedure show_space
( p_segname_1 in varchar2,
p_space in varchar2 default 'MANUAL',
p_type_1 in varchar2 default 'TABLE' ,
p_analyzed in varchar2 default 'N',
p_owner_1 in varchar2 default user)
as
p_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;
procedure p( p_label in varchar2, p_num in number )
is
begin
dbms_output.put_line( rpad(p_label,40,'.') ||
p_num );
end;
begin
p_segname := upper(p_segname_1);
p_owner := upper(p_owner_1);
p_type := p_type_1;
if (p_type_1 = 'i' or p_type_1 = 'I') then
p_type := 'INDEX';
end if;
if (p_type_1 = 't' or p_type_1 = 'T') then
p_type := 'TABLE';
end if;
if (p_type_1 = 'c' or p_type_1 = 'C') then
p_type := 'CLUSTER';
end if;
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 );
if p_space = 'MANUAL' or (p_space <> 'auto' and p_space <> 'AUTO') then
dbms_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 );
end if;
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 */
if p_analyzed = 'Y' then
dbms_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);
end if;
end;
/
使用方法:
sql
SET SERVEROUTPUT ON
EXEC show_space('TEST', 'AUTO', 'TABLE', 'Y', 'MARY');
四、完整实验过程
我新建了两张测试表 mary.test2026、mary.test2026_2,分别插入约500万行数据,然后DELETE全量数据,制造碎片,分别用两种方式处理并观察效果。
实验准备(制造碎片):
创建测试表
sql
CREATE TABLE mary.test2026 (id NUMBER primary key, name VARCHAR2(50), create_time DATE);
插入测试数据
sql
BEGIN
FOR i IN 1..5000000 LOOP
INSERT INTO mary.test2026 VALUES (i, 'test'||i, SYSDATE);
END LOOP;
COMMIT;
END;
/
创建第二张测试表、插入数据、添加主键
sql
create table mary.test2026_2 as select * from mary.test2026;
alter table mary.test2026_2 modify (id NUMBER constraint mary_pk primary key);
收集统计信息
sql
execute dbms_stats.gather_table_stats ('MARY','TEST2026');
execute dbms_stats.gather_table_stats ('MARY','TEST2026_2');
查看两张测试表的段大小
sql
col segment_name for a40
select segment_name,sum(bytes)/1024/1024 as "size_MB" from dba_segments where segment_name in ('TEST2026','TEST2026_2') and owner='MARY' group by segment_name;
SEGMENT_NAME size_MB
TEST2026 168
TEST2026_2 168
查看两张表的高水位线与碎片情况
sql
set linesize 500
set pagesize 300
col owner for a15
col table_name for a10
set linesize 500
set pagesize 300
col owner for a15
col table_name for a10
SELECT owner, 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"
FROM dba_tables
WHERE owner = 'MARY' AND table_name in ('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
删除全部数据
sql
DELETE FROM mary.test2026;
DELETE FROM mary.test2026_2;
COMMIT;
重新收集统计信息
sql
execute dbms_stats.gather_table_stats ('MARY','TEST2026');
execute dbms_stats.gather_table_stats ('MARY','TEST2026_2');
这里我们可以看到,数据量虽然全部删除,但是两张表的数据段大小并没有减少
sql
col segment_name for a40
select segment_name,sum(bytes)/1024/1024 as "size_MB" from dba_segments where segment_name in ('TEST2026','TEST2026_2') and owner='MARY' group by segment_name;
SEGMENT_NAME size_MB
TEST2026 168
TEST2026_2 168
通过以下SQL能够更容易看到实际占用空间与碎片空间的比对。高水位线很高,碎片非常严重。
sql
set linesize 500
set pagesize 300
col owner for a15
col table_name for a10
set linesize 500
set pagesize 300
col owner for a15
col table_name for a10
SELECT owner, 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"
FROM dba_tables
WHERE owner = 'MARY' AND table_name in ('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
sql
ALTER TABLE mary.test2026 ENABLE ROW MOVEMENT;
ALTER TABLE mary.test2026 SHRINK SPACE COMPACT;
ALTER TABLE mary.test2026 SHRINK SPACE CASCADE;
实验2:对mary.test2026_2执行MOVE(重新制造碎片后)
sql
ALTER TABLE mary.test2026_2 MOVE;
重新收集统计信息
sql
execute dbms_stats.gather_table_stats ('MARY','TEST2026');
execute dbms_stats.gather_table_stats ('MARY','TEST2026_2');
观测实验结果:
先检查索引情况:
可以看到SHRINK方式后索引状态正常,MOVE方式则导致MARY_PK索引失效。
sql
set linesize 600
set pagesize 500
col INDEX_NAME for a20
col TABLE_OWNER for a20
col TABLE_NAME for a30
col STATUS for a30
select INDEX_NAME,TABLE_OWNER,TABLE_NAME,STATUS from dba_indexes where TABLE_OWNER='MARY';
INDEX_NAME TABLE_OWNER TABLE_NAME STATUS
SYS_C009491 MARY TEST2026 VALID
MARY_PK MARY TEST2026_2 UNUSABLE
(特殊说明:在原实验中,我此处做出来的结果MARY_PK没有失效,因为所有数据都被清空的原因,但如果表中还有数据,索引一定是会失效的)
重建test2026_2表的失效索引
sql
ALTER INDEX mary.MARY_PK REBUILD ONLINE;
sql
set linesize 500
set pagesize 300
col owner for a15
col table_name for a10
set linesize 500
set pagesize 300
col owner for a15
col table_name for a10
SELECT owner, 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"
FROM dba_tables
WHERE owner = 'MARY' AND table_name in ('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性能优化老司机
专注硬核干货,欢迎一起卷技术!
