今天一位同事发现11g环境中一张实验数据表筛选变得很慢,即使为空表。通过EM观察,发现指定sql语句等待时间和执行时间都很长(达到秒级)。
经过进一步询问,确定该表曾经进行批量数据DML操作,多次重复操作。而且数据表包括分区信息。所以初步认为是高水位线HWM过大引起的问题。
在Oracle中,数据表的存储结构是一种类似河道水位的结构。数据表段(segment)就在一系列不连续的区(extents)进行空间的划分,而高水位线就是表示数据表边界的上限标记。
以堆表为例,当一行数据为插入时,首先会在高水位线以下随机寻找一块空闲的空间进行存储。如果在HWM以下不能找到合适的位置,就会提升HWM位置,在新的空间上进行存储。但是,当对数据进行删除的时候,HWM时不会主动降低的,就像河道的最高水位一样,永远是标记河水最高位置。
这个特性在查找的时候,就会带来一些问题。Oracle在筛选数据,特别是进行全表扫描的时候,要从数据段低一直找到HWM的位置,不管中间经过多少空block。如果数据表数据量很少,但是曾经保存过大量数据,那么有时会带来一些性能问题,如select速度慢。
下面通过一个小实验去证明:
1、实验环境获取一下。
SQL> select * from v$version;
BANNER
----------------------------------------------------------------
Oracle Database 10g Enterprise Edition Release 10.2.0.1.0 - Prod
PL/SQL Release 10.2.0.1.0 - Production
CORE 10.2.0.1.0 Production
TNS for 32-bit Windows: Version 10.2.0.1.0 - Production
NLSRTL Version 10.2.0.1.0 – Production
2、建立一张大数据表;
SQL> create table t as select * from dba_objects where 1=0;
Table created
SQL> insert /* +append */ into t select * from dba_objects;
201452 rows inserted
SQL> select count(*) from t;
COUNT(*)
----------
201452
SQL> exec dbms_stats.gather_table_stats('SYS','T',cascade => true);
PL/SQL procedure successfully completed
3、观察数据段分配情况
调用dba_segment视图,可以查看到数据表t的分配情况。
SQL> col segment_name format a10;
SQL> select segment_name, blocks, extents, INITIAL_EXTENT from dba_segments where wner='SYS' and segment_name='T';
SEGMENT_NA BLOCKS EXTENTS INITIAL_EXTENT
---------- ---------- ---------- --------------
T 2816 37 65536
说明:数据表段一共使用了2816个Oracle数据块,分布在37个区extents上。共占用空间2816*8*1024=23068672kb的空间。
收集统计信息并且执行
SQL> exec dbms_stats.gather_table_stats('SYS','T',cascade => true);
PL/SQL procedure successfully completed
SQL> select * from t;
已选择201452行。
执行计划
----------------------------------------------------------
Plan hash value: 1601196873
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 201K| 17M| 622 (3)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 201K| 17M| 622 (3)| 00:00:08 |
--------------------------------------------------------------------------
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
16005 consistent gets
0 physical reads
0 redo size
21626412 bytes sent via SQL*Net to client
148115 bytes received via SQL*Net from client
13432 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
201452 rows processed
4、执行删除后,观察情况
SQL> delete t;
201452 rows deleted
SQL> commit;
Commit complete
分配情况:
SQL> exec dbms_stats.gather_table_stats('SYS','T',cascade => true);
PL/SQL procedure successfully completed
SQL> select segment_name, blocks, extents, INITIAL_EXTENT from dba_segments where wner='SYS' and segment_name='T';
SEGMENT_NA BLOCKS EXTENTS INITIAL_EXTENT
---------- ---------- ---------- --------------
T 2816 37 65536
没有发生变化,如果这时候进行搜索。
SQL> select * from t;
未选定行
执行计划
----------------------------------------------------------
Plan hash value: 1601196873
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 177 | 608 (1)| 00:00:08 |
| 1 | TABLE ACCESS FULL| T | 1 | 177 | 608 (1)| 00:00:08 |
--------------------------------------------------------------------------
统计信息
----------------------------------------------------------
387 recursive calls
0 db block gets
2812 consistent gets
0 physical reads
0 redo size
992 bytes sent via SQL*Net to client
374 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
0 rows processed
结论:当我们手工将数据删除的时候,数据字段中对于表t的描述信息没有发生变化,仍然是37个extents,2816个block,也就意味着HWM没有变化。对比前后两个select的执行计划,发现虽然后者执行返回数据为0(统计信息预估),但是依然花费8s时间,CPU成本也达到608,没有节省时间。消耗CPU在consistent gets上有2812。
5、进行手工处理HWM
方法1:Move
可以使用alter table XX move的方法,对数据表中的数据进行重新排列,调整rowid位置。这样就可以降低HWM。
SQL> alter table t move;
Table altered
SQL> exec dbms_stats.gather_table_stats('SYS','T',cascade => true);
PL/SQL procedure successfully completed
SQL> select segment_name, blocks, extents, INITIAL_EXTENT from dba_segments where wner='SYS' and segment_name='T';
SEGMENT_NA BLOCKS EXTENTS INITIAL_EXTENT
---------- ---------- ---------- --------------
T 8 1 65536
可以发现,数据字段中数据块的个数已经缩小,extents也只有1个。说明大小变化已经落实。
SQL> select * from t;
未选定行
执行计划
----------------------------------------------------------
Plan hash value: 1601196873
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 177 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS FULL| T | 1 | 177 | 2 (0)| 00:00:01 |
--------------------------------------------------------------------------
统计信息
----------------------------------------------------------
387 recursive calls
0 db block gets
52 consistent gets
0 physical reads
0 redo size
992 bytes sent via SQL*Net to client
374 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
0 rows processed
执行成本上,消耗时间只有1s左右,CPU的消耗也只有2。而consistent gets从未调整前的2812下降到了调整后的52。
方法2:shrink space
在10g,推出了收缩空间的方法,也可以起到HWM降低的效果。
SQL> alter table t enable row movement;
SQL> alter t shrink space;
方法3:truncate重建表
如果该表是一个空表,那么最简单的方法就是用truncate对表进行重建,这样HWM,都会重新确立结构。
SQL> truncate table t;
Table truncated
综上所述:Oracle的HWM机制,在管理数据表的方面上是存在一些问题的。对于一些数据量变化巨大的数据表来说,周期性的运维任务可能是最好的解决方法。HWM通常都需要手动的进行手动降低,来提高效率,避免大面积的查找。
但是,对于走索引的搜索而言,由于HWM引起的这种效率的降低应该是有限的。因为索引对应的rowid是没有变化,那么查询所遍历数据块的个数是不会发生变化的。
修改HWM的方法大体有三个:
1、 Move方法,Move是直接对于行进行重新排列的方法,可靠程度比较好。在有分区的时候,还是要指定分区。还可以实现segment对象的定向移动功能。问题在于因为move后,数据的rowid会发生变化,对应的index需要重新rebuild;
2、 Shrink space方法,是10g中新推出的功能,对于segment和tablespace都有尚可的效果。shrink space的操作是有前提的,首先启动了row movement,其次是对象所在数据表空间不能是自动管理段的。;
3、 Truncate命令,直接重新构建数据表;