说明:本文整理自书籍《收获,不止Oracle》
Oracle表的分类是多种多样的,除了普通表外,还有全局临时表、外部表、分区表、索引组织表等等具有其他特性的表。虽然普通表基本上可以实现所有的功能,但是这是说功能,而不是说性能。
如果我们善于在合适的场合选择合适的技术,这些“特殊”的表往往能在系统应用设计的性能方面,发挥出巨大的作用。
各种类型表都有优缺点,我们要善于取长补短,灵活利用,本篇文章,我们本着挑剔的态度来探讨普通表的缺点。

如下语句可查询日志量:
select a.name,b.value
from v$statname a,v$mystat b
where a.statistic#=b.statistic#
and a.name='redo size';
为方便后续查询,我们创建如下视图:
--方便后续直接用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';
在进行实验前,我们可以先查询当前日志的大小,如下:

这个值的单位是字节数
现在开始实验,以观察DML操作产生的日志量。
--建表
create table t as select*from dba_objects;

建表产生日志:
(16601608-11396)/1024/1024≈15.8M
--删除表数据
delete from t;
提交前

提交后

删除表数据产生日志:
(70055388-16601608)/1024/1024≈60M
--插入数据
insert into t select*from dba_objects;
提交前

提交后

插入数据产生日志:
(86355360-70055388)/1024/1024≈15.5M
--更新数据
update t set object_id=rownum;
提交前

提交后

更新数据产生日志:
(135263700-86355360)/1024/1024≈46.6M
这三个试验说明了对表的更新操作,无论是删除、插入还是修改,都会产生日志。
那么insert、update、delete三类语句,哪种记录redo log最多,哪种最少?
由上面的实验结果可以看出:delete产生的redo日志最多(60M),其次是update(46.6M),最少的是insert(15.5M)。
--查看执行计划
SQL> set autotrace on
SQL> select count(*) from rui.t;
COUNT(*)
----------
141644
Execution Plan
----------------------------------------------------------
Plan hash value: 2966233522
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 559 (1)| 00:00:07 |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| T | 140K| 559 (1)| 00:00:07 |
-------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
Statistics
----------------------------------------------------------
5 recursive calls
0 db block gets
2079 consistent gets
0 physical reads
0 redo size
528 bytes sent via SQL*Net to client
524 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
--删除数据后再次查看执行计划
SQL> set autotrace off
SQL> delete from rui.t;
141644 rows deleted.
SQL> commit;
Commit complete.
SQL> set autotrace on
SQL> select count(*) from rui.t;
COUNT(*)
----------
0
Execution Plan
----------------------------------------------------------
Plan hash value: 2966233522
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 557 (1)| 00:00:07 |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| T | 1 | 557 (1)| 00:00:07 |
-------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
Statistics
----------------------------------------------------------
4 recursive calls
0 db block gets
2079 consistent gets
0 physical reads
0 redo size
525 bytes sent via SQL*Net to client
524 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
记录数从141644减少到0条记录了,为什么逻辑读还是2079呢?
我们先继续下面的实验:
SQL> truncate table rui.t;
Table truncated.
SQL> select count(*) from rui.t;
COUNT(*)
----------
0
Execution Plan
----------------------------------------------------------
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)
Statistics
----------------------------------------------------------
6 recursive calls
1 db block gets
13 consistent gets
0 physical reads
96 redo size
525 bytes sent via SQL*Net to client
524 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
这里很显然看出:
举个简单的例子,好比我来到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的应用,是不会出现空块过多的情况的。
一张表其实就是一个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个小段比较均匀,我们就可以大致理解为访问量只有原来的三十六分之一,大幅度减少了访问路径,从而高效地提升了性能。
SQL> drop table rui.t purge;
Table dropped.
SQL> create table rui.t as select * from dba_objects where rownum <= 200;
Table created.
SQL> create index idx_obj_id on rui.t(object_id);
Index created.
SQL> set linesize 1000
SQL> set autotrace traceonly
SQL> select * from rui.t where object_id<=10;
9 rows selected.
Execution Plan
----------------------------------------------------------
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)
Statistics
----------------------------------------------------------
23 recursive calls
0 db block gets
40 consistent gets
0 physical reads
0 redo size
2318 bytes sent via SQL*Net to client
524 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
9 rows processed
注意执行计划中有“TABLE ACCESS BY INDEX ROWID”关键字。
一般来说,根据索引来检索记录,会有一个先从索引中找到记录,再根据索引列上的ROWID定位到表中从而返回索引列以外的其他列的动作,这就是TABLE ACCESS BY INDEX ROWID 。
SQL> select object_id from rui.t where object_id<=10;
9 rows selected.
Execution Plan
----------------------------------------------------------
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)
Statistics
----------------------------------------------------------
7 recursive calls
0 db block gets
10 consistent gets
0 physical reads
0 redo size
637 bytes sent via SQL*Net to client
524 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
9 rows processed
执行计划中没有“没有TABLE ACCESS BY INDEX ROWID”关键字了!
因为语句从 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,但是还是可以不回表呢?
普通表是做不到的,能实现这种功能的只有索引组织表。
在对普通表的操作中,我们无法保证在有序插入的前提下就能有序读出。最简单的一个理由就是,如果你把行记录插入块中,然后删除了该行,接下来插入的行会去填补块中的空余部分,这就无法保证有序了。实验如下:
SQL> drop table rui.t purge;
Table dropped.
SQL> create table rui.t (a int,b varchar2(4000)default rpad('*',4000,'*'),c varchar2(3000)default rpad('*',3000,'*'));
Table created.
SQL> insert into rui.t (a) values (1);
1 row created.
SQL> insert into rui.t (a) values (2);
1 row created.
SQL> insert into rui.t (a) values (3);
1 row created.
SQL> select A from rui.t;
A
----------
1
2
3
SQL> delete from rui.t where a=2;
1 row deleted.
SQL> insert into rui.t (a) values (4);
1 row created.
SQL> select A from rui.t;
A
----------
1
4
3
因为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 rui.t;
Execution Plan
----------------------------------------------------------
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)
Statistics
----------------------------------------------------------
0 recursive calls
0 db block gets
7 consistent gets
0 physical reads
0 redo size
581 bytes sent via SQL*Net to client
524 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 rui.t order by A;
Execution Plan
----------------------------------------------------------
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)
Statistics
----------------------------------------------------------
4 recursive calls
0 db block gets
15 consistent gets
0 physical reads
0 redo size
581 bytes sent via SQL*Net to client
524 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
3 rows processed
可以观察到,有排序的操作的统计信息模块有一个1 sorts (memory),表示发生了排序,执行计划中也有SORT ORDER BY的关键字,不过最重要的是,没排序的操作代价为3,有排序的操作代价为4,性能上是有差异的,在大数量时将会非常明显。
关于order by 避免排序的方法有两种思路。
第一种思路是在order by 的排序列建索引。
第二种方法就是,将普通表改造为有序散列聚簇表,这样可以保证顺序插入,order by 展现时无须再有排序动作。