0
点赞
收藏
分享

微信扫一扫

Oracle普通堆表的不足之处

40dba2f2a596 2023-09-02 阅读 12

Oracle表的分类是多种多样的,除了普通表外,还有全局临时表、外部表、分区表、索引组织表等等具有其他特性的表。虽然普通表基本上可以实现所有的功能,但是这是说功能,而不是说性能。

如果我们善于在合适的场合选择合适的技术,这些“特殊”的表往往能在系统应用设计的性能方面,发挥出巨大的作用。

各种类型表都有优缺点,我们要善于取长补短,灵活利用,本篇文章,我们本着挑剔的态度来探讨普通表的缺点。

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博客
举报

相关推荐

0 条评论