Oracle表的分类是多种多样的,除了普通表外,还有全局临时表、外部表、分区表、索引组织表等等具有其他特性的表。虽然普通表基本上可以实现所有的功能,但是这是说功能,而不是说性能。
如果我们善于在合适的场合选择合适的技术,这些“特殊”的表往往能在系统应用设计的性能方面,发挥出巨大的作用。
各种类型表都有优缺点,我们要善于取长补短,灵活利用,本篇文章,我们本着挑剔的态度来探讨普通表的缺点。
1. 表更新日志开销较大
如下语句可查询日志量:
select a.name,b.value
from v$statname a,v$mystat b
where a.statistic#=b.statistic#
and a.name='redo size';
创建测试用户:
sqlplus "/ as sysdba"
grant all on v_$mystat to ljb;
grant all on v_$statname to ljb;
connect ljb/ljb
为方便后续查询,我们创建如下视图:
--方便后续直接用select * from v$redo_size进行查询
create or replace view v$redo_size as
select a.name,b.value
from v$statname a,v$mystat b
where a.statistic#=b.statistic#
and a.name='redo size';
在进行实验前,我们可以先查询当前日志的大小,如下:
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 30416
这个值的单位是字节数
现在开始实验,以观察DML操作产生的日志量。
1.1 建表
--建表
create table t as select * from dba_objects;
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 1869132
SQL> select (1869132-30416)/1024/1024 as MB from dual;
MB
----------
1.75353622
建表产生日志: (1869132-30416)/1024/1024≈1.75M
1.2 删除表数据
--删除表数据
delete from t;
--提交前
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 8198296
--提交后
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 8198532
SQL> select (8198532-1869132)/1024/1024 as MB from dual;
MB
----------
6.03618622
删除表数据产生日志: (8198532-1869132)/1024/1024≈6.04M
1.3 插入数据
--插入数据
insert into t select * from dba_objects;
--提交前
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 9955752
--提交后
SQL> commit;
提交完成。
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 9955960
SQL> select (9955960-8198532)/1024/1024 as MB from dual;
MB
----------
1.67601395
插入数据产生日志: (9955960-8198532)/1024/1024≈1.68M
1.4 更新数据
--更新数据
SQL> update t set object_id=rownum;
--提交前
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 14456528
--提交后
SQL> commit;
提交完成。
SQL> select * from v$redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 14456764
SQL> select (14456764-9955960)/1024/1024 as MB from dual;
MB
----------
4.29230118
更新数据产生日志: (14456764-9955960)/1024/1024≈4.29M
这三个试验说明了对表的更新操作,无论是删除、插入还是修改,都会产生日志。
那么insert、update、delete三类语句,哪种记录redo log最多,哪种最少? 由上面的实验结果可以看出:delete产生的redo日志最多(6.04M),其次是update(4.29M),最少的是insert(1.68M)。
2. delete无法释放空间
--观察未删除表时产生的逻辑读
SQL> drop table t purge;
SQL> create table t as select * from dba_objects ;
SQL> set autotrace on
SQL> select count(*) from t;
COUNT(*)
----------
259216
执行计划
----------------------------------------------------------
Plan hash value: 2966233522
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 1177 (3)| 00:00:15 |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| T | 6679K| 1177 (3)| 00:00:15 |
-------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
5923 consistent gets
0 physical reads
0 redo size
349 bytes sent via SQL*Net to client
471 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
--观察delete删除t表所有记录后,居然逻辑读不变都是5923
SQL> set autotrace off
SQL> delete from t ;
已删除259216行。
SQL> commit;
提交完成。
SQL> set autotrace on
SQL> select count(*) from t;
COUNT(*)
----------
0
执行计划
----------------------------------------------------------
Plan hash value: 2966233522
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 1145 (1)| 00:00:14 |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| T | 1 | 1145 (1)| 00:00:14 |
-------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
5923 consistent gets
0 physical reads
0 redo size
346 bytes sent via SQL*Net to client
471 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
记录数从259216减少到0条记录了,为什么逻辑读还是5923呢?
我们先继续下面的实验:
SQL> truncate table t;
表被截断。
SQL> select count(*) from t;
COUNT(*)
----------
0
执行计划
----------------------------------------------------------
Plan hash value: 2966233522
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| T | 1 | 2 (0)| 00:00:01 |
-------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
6 recursive calls
1 db block gets
12 consistent gets
0 physical reads
96 redo size
346 bytes sent via SQL*Net to client
471 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
这里很显然看出:
- delete 删除并不能释放空间,虽然delete将很多块的记录删除了,但是空块依然保留,Oracle 在查询时依然会去查询这些空块。
- truncate 是一种释放高水平位的动作,这些空块被回收,空间也就释放了。
举个简单的例子,好比我来到XX大楼统计里面的人数,我从1楼找到20楼,每层的房间都打开去检查了一下,发现实际情况是一个人都没有。我很后悔自己累得半死却得出没人的结论,但问题是,你不打开房间,怎么知道没人呢,这就类似delete后空块的情况。而与truncate有些类似的生动例子就是,我想统计 XX 大楼里的人数,结果发现,XX 大楼被铲平了,啥房间都没有了,于是我飞快地得出结论,XX大楼里没有人。
不过truncate显然不能替代delete,因为truncate是一种DDL操作而非DML操作,truncate后面是不能带条件的,truncate table t where…是不允许的。 但是如果表中这些where条件能形成有效的分区,Oracle是支持在分区表中做truncate分区的,命令大致为 alter table t truncate partition ‘分区名’,如果where条件就是分区条件,那等同于换角度实现了truncate table t where…的功能。
当大量delete 删除再大量insert插入时,Oracle会去这些delete的空块中首先完成插入(直接路径插入除外),所以频繁delete又频繁insert的应用,是不会出现空块过多的情况的。
3. 表记录太大检索较慢
一张表其实就是一个SEGMENT,一般情况下我们都需要遍历该SEGMENT的所有BLOCK来完成对该表进行更新查询等操作,在这种情况下,表越大,更新查询操作就越慢!
有没有什么好方法能提升检索的速度呢?主要思路就是缩短访问路径来完成同样的更新查询操作,简单地说就是完成同样的需求访问BLOCK的个数越少越好。Oracle为了尽可能减少访问路径提供了两种主要技术,一种是索引技术,另一种则是分区技术。
我们先来说说索引技术: 当我们建成了一个索引,在SQL查询时我们首先会访问索引段,然后通过索引段和表段的映射关系,迅速从表中获取行列的信息并返回结果。
再来说说分区技术: 分区技术就是把普通表T表改造为分区表,比如以select * from t where created>= xxx and created <=xxx 这个简单的SQL语句为例进行分析。 如果以created这个时间列为分区字段,比如从2010年1月到2012年12月按月建36个分区。早先的T表就一个T段,现在情况变化了,从1个大段分解成了36个小段,分别存储了2010年1月到2012年12月的信息,此时假如created>= xxx and created <=xxx 这个时间跨度正好是落在2012年11月,那Oracle的检索就只要完成一个小段的遍历即可,假设这36个小段比较均匀,我们就可以大致理解为访问量只有原来的三十六分之一,大幅度减少了访问路径,从而高效地提升了性能。
4. 索引回表读开销很大
SQL> drop table t purge;
表已删除。
SQL> create table t as select * from dba_objects where rownum<=200;
表已创建。
SQL> create index idx_obj_id on t(object_id);
索引已创建。
SQL> set linesize 1000
SQL> set autotrace traceonly
SQL> select * from t where object_id<=10;
已选择9行。
执行计划
----------------------------------------------------------
Plan hash value: 134201588
------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 9 | 1863 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| T | 9 | 1863 | 2 (0)| 00:00:01 |
|* 2 | INDEX RANGE SCAN | IDX_OBJ_ID | 9 | | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("OBJECT_ID"<=10)
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
23 recursive calls
0 db block gets
40 consistent gets
0 physical reads
0 redo size
1943 bytes sent via SQL*Net to client
471 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
9 rows processed
SQL>
注意执行计划中有“TABLE ACCESS BY INDEX ROWID”关键字。
一般来说,根据索引来检索记录,会有一个先从索引中找到记录,再根据索引列上的ROWID定位到表中从而返回索引列以外的其他列的动作,这就是TABLE ACCESS BY INDEX ROWID 。
观察如果消除TABLE ACCESS BY INDEX ROWID的开销情况
SQL> select object_id from t where object_id<=10;
已选择9行。
执行计划
----------------------------------------------------------
Plan hash value: 188501954
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 9 | 117 | 1 (0)| 00:00:01 |
|* 1 | INDEX RANGE SCAN| IDX_OBJ_ID | 9 | 117 | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("OBJECT_ID"<=10)
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
7 recursive calls
0 db block gets
10 consistent gets
0 physical reads
0 redo size
430 bytes sent via SQL*Net to client
471 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
9 rows processed
SQL>
执行计划中没有“没有TABLE ACCESS BY INDEX ROWID”关键字了,逻辑读由40变为10,代价也由2变为1了!
因为语句从 select * from t where object_id<=10 改写为 select object_id from t where object_id<=10 了,不用从索引中回到表中获取索引列以外的其他列了。
可以发现性能有所提升。
避免回表从而使性能提升这是一个很简单的道理,少做事性能当然提升了。只是select * from t; 和select object_id from t;毕竟不等价,有没有什么方法可以实现写法依然是select * from t,但是还是可以不回表呢?
普通表是做不到的,能实现这种功能的只有索引组织表。
5. 有序插入却难有序读出
在对普通表的操作中,我们无法保证在有序插入的前提下就能有序读出。最简单的一个理由就是,如果你把行记录插入块中,然后删除了该行,接下来插入的行会去填补块中的空余部分,这就无法保证有序了。实验如下:
SQL> drop table t purge;
表已删除。
SQL> create table t (a int, b varchar2(4000) default rpad('*',4000,'*'), c varchar2(3000) default rpad('*',3000,'*') );
表已创建。
SQL> insert into t (a) values (1);
已创建 1 行。
SQL> insert into t (a) values (2);
已创建 1 行。
SQL> insert into t (a) values (3);
已创建 1 行。
SQL> select A from t;
A
----------
1
2
3
SQL> delete from t where a=2;
已删除 1 行。
SQL> insert into t (a) values (4);
已创建 1 行。
SQL> commit;
提交完成。
SQL> select A from t;
A
----------
1
4
3
SQL>
因为BLOCK大小默认是8KB,所以这里特意用rpad(‘‘,4000,’’), rpad(‘‘,3000,’’)来填充B、C字段,这样可以保证一个块只插入一条数据,方便做试验分析跟踪。
我们在查询数据时,如果想有序地展现,就必须使用order by ,否则根本不能保证顺序展现,而order by 操作是开销很大的操作,实验如下:
--order by 操作是开销很大的操作
SQL> set linesize 1000
SQL> set autotrace traceonly
SQL> select A from t;
执行计划
----------------------------------------------------------
Plan hash value: 1601196873
--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 39 | 3 (0)| 00:00:01 |
| 1 | TABLE ACCESS FULL| T | 3 | 39 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
7 consistent gets
0 physical reads
0 redo size
374 bytes sent via SQL*Net to client
471 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
3 rows processed
SQL> select A from t order by A;
执行计划
----------------------------------------------------------
Plan hash value: 961378228
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 39 | 4 (25)| 00:00:01 |
| 1 | SORT ORDER BY | | 3 | 39 | 4 (25)| 00:00:01 |
| 2 | TABLE ACCESS FULL| T | 3 | 39 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
4 recursive calls
0 db block gets
15 consistent gets
0 physical reads
0 redo size
374 bytes sent via SQL*Net to client
471 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
3 rows processed
SQL>
可以观察到,有排序的操作的统计信息模块有一个1 sorts (memory),表示发生了排序,执行计划中也有SORT ORDER BY的关键字,不过最重要的是,没排序的操作代价为3,有排序的操作代价为4,性能上是有差异的,在大数量时将会非常明显。
关于order by 避免排序的方法有两种思路。 第一种思路是在order by 的排序列建索引。 第二种方法就是,将普通表改造为有序散列聚簇表,这样可以保证顺序插入,order by 展现时无须再有排序动作。
6.参考资料
- 《收获,不止Oracle》
- Oracle表格分类浅析1——普通堆表_oracle 堆表_夜光小兔纸的博客-CSDN博客