本文知识点和理论来源于《收获不止Oracle》一书,并对书中内容作出整理,以共同学习
文章整体结构: https://www.edrawsoft.cn/viewer/public/s/36020647239118
1、体系结构
学习oracle首先要了解其物理体系结构,也可以理解为oracle的整体架构。结合整理的思维导图,看出其体系结构可对应分为oracle实例和数据库文件。
1.1 实例instance
对图做下说明:
- 1、oracle由实例和数据库组成,上半部分为实例instance,下半部分为数据库Datebase
- 2、实例是由一个开辟的共享内存区SGA(system global area)和一系列后台进程组成。其中SGA主要被划分为共享池(共享缓冲区)、数据缓冲区和日志缓冲区,后台进程包括图示PMON、SMON、DBWR、LGWR、CKPT、LCKn、RECO、ARCH等一系列进程。
- 3、数据库是由数据文件,参数文件日志文件控制文件归档日志(重做)文件等系列文件组成,其中的归档日志最终可能会被转移到新的存储介质,用于数据备份和恢复。
- 4、图中有个PGA区,这也是一个开辟出来的内存区,和SGA最明显的区别在于,PGA不是共享内存区,是私有不共享的,S可以理解为共享的首字母。首先对于用户来讲,用户发起的无论查询还是更新的任何操作,都是在PGA先预处理,接下来才进入实例区域,由SGA和后台进程共同完成用户发起的请求。
PGA的具体作用主要三点:a) 保存用户连接信息,如会话属性,绑定变量
b) 保存用户权限等重要信息,当用户进程与数据库建立连接会话时,系统会将这个户的相关权限查询出来,保存在PGA区。
c) 当发起的指令需要排序时,就在PGA内存区内完成,如果放不下,超出部分就会在临时表空间中排序。也就是在磁盘中排序。
关于后台进程的解释,实际上oracle后台进程远不止图上列举的几个,这里列举的只是比较常见且比较重要的进程,下面依次介绍。
POMN
POMN含义为Process Monitor,是进程监视器。如果你在执行某更新语句,未提交时进程崩溃了,这个时候POMN会自动回滚操作,无须人工执行ROLLBACK,除此之外还可以干预后台进程,比如RECO进程异常失败了,此时POMN会重启RECO进程,如果遇得LGWR进程失败这样严重问题,POMN会做出终止实例这个激烈操作,用于防止数据错乱。
SMON
SMON 含义为System Monitor 理解为系统监视器。与POMN不同的是SMON关注的是系统级的进程而非单个进程,重点工作在于intance recovery,除此之外还有清理临时表空间,清理回滚表空间,合并空闲表空间,等。
LCKn
LCKn仅适用于RAC数据库,最多可以有10个进程(LCK0,LCK1,…,LCK9),用于实例间的封锁。
RECO
RECO用于分布式数据库的恢复,适用于两阶段提交下的应用场景。比如有多个数据库A,B,C,在发起的过程中A,B,C,都要成功,事务才会成功,只要有一个失败,就必须全部回滚。这里跟LCKn一样,使用场景比较特殊。
CKPT
CKPT由ORACLE的FAST_START_MTTR_TARGET参数控制,用于触发DBWR从数据缓存区写数据到磁盘,CKPT执行越频繁,DBWR写出就越频繁,这样数据库批量性能就越低,但是在数据库异常恢复的时候,会越迅速。
DBWR
DBWR是oracle最核心的进程之一,负责把数据从数据缓存区写入到磁盘,和CKPT进程相辅相成,因为是CKPT促进DBWR去写数据。不过DBWR也和LGWR也密切相关,因为DBWR将数据写到磁盘的时候,必须停止LGWR先完成日志缓冲区写到磁盘后,方才开工。
LGWR
这个进程的目的很简单,就是把日志缓冲区的数据从内存写到磁盘的redo文件中,完成数据库对象的创建,更新数据库操作过程的记录,redo文件的作用非同小可,可以用来对数据库的异常恢复,只要保护好redo文件和后续的对应归档文件,理论上计时数据库文件全部删光了,还是可以让数据库根据这些日志记录,把所有在数控中曾经发生的一切事情全部重做一遍,从而保证了数据库的安全。
因为日志文件如此重要,所以LGWR也和DBWR一样是数据库核心进程。
另外记录的日志需要保证有序,这样才能在数据恢复的时候才有意义,试想用户原本先执行创建表对象,再插入数据到表中去。在数据恢复的时候,因为日志没有保证顺序,导致先插入数据到表中,可是表还没有创建… >.<
为了解决这个问题,LGWR有5条严格的制度来执行:
- 每隔三秒钟,LGWR运行一次
- 任何commit触发,LGWR运行一次
- DBWR把数据从缓存写到磁盘,LGWR运行一次
- 日志缓冲区满三分之一或记录满1M,LGWR运行一次
- 联机日志文件切换,也触发LGWR
ARCH
ARCH是写日志写到需要覆盖重写的时候(日志缓冲区就那么大,写完会覆盖重写),触发ARCH进程去转移日志文件,复制出去形成归档日志文件,保存在磁盘,以防止日志丢失。
·
1.2 数据库
说到数据库,不得不涉及表空间(tablespace)、段(segment)、区(extent)、块(block)。ORACLE正是通过表空间,段,块,区控制磁盘的合理使用,如图:
可以看出,数据库由若干表空间组成,表空间由若干段组成,段由若干区组成,区又有oracle最小单元块组成。也就是说,一系列连续的块组成了区,一个或多个区组成了段,一个或多个段组成了表空间,而一个或多个表空间组成了DataBase(一个DataBase要想存在,必须至少需要有SYSTEM和UNDO表空间)。其中,表空间包含系统表空间,临时表空间,用户表空间,回滚表空间。除了用户表空间外,其他三个表空间各有各自用途,不可随意更改。
结合我们的思维导图(https://www.edrawsoft.cn/viewer/public/s/36020647239118 ),接下来要从最小的数据块说起。
我们知道所有数据在文件系统最小物理存储单位是字节,操作系统也有一个类似Oracle的块容量的参数block size,但是Oracle总是访问整个Oracle Block,而不是按照操作系统的block size来访问的。一般情况下,操作系统的块容量为512字节大小的整数倍,但是数据库块大小却一般默认为8Kb(也可以设置为2kb,4kb,16kb,32kb等其他大小),值为操作系统块容量的整数倍,因为可以减少IO操作。
至于怎么减少IO,举个列子,假如IO的大小设置为512字节(0.5kb),如果DB的Block设置为1kb,正好需要两个IO系统块才可以容纳下,但是如果DB的block设置为0.8kb,必须要2个操作系统块区容纳,相当于1kb大小的空间只用了0.8kb,这样就浪费了0.2kb空间。
另外Oracle数据块并不是简单的往里插入数据,插满了就下一个块这么简单,除此之外他还额外提供了一定管理功能。数据块组成又细分为数据块头、表目录区、行目录区、可用空间区、行数据区五个部分。如图
对数据块5个部分的说明:
- 数据块头(header)中包含了次数据块的概要信息,列如块地址(block address)及此数据块所属的段(segment)的类型(比如到底是索引还是表)。
- 表目录区,只要有一行数据插入到数据块,那改行数据所在的表的信息将被存储在这个区域。
- 行目录区是什么呢?其实就是你存放你插入的行的地址。
- 可用空间区,就是块中的空余空间,这个空余空间多少由oracle的PCTFREE参数来设置,是可控制的。如果是10,表示该块将会空10%左右的空间。此外如果是表或者索引,该区域还会存储事务条目,大致有23字节左右的开销。
- 而行数据区就更简单理解了,就是存储具体的行的信息或者索引的信息,这部分占用了数据块的绝大部分空间。
对于块(block)的理解到位了,那么把连续的块组合到一起,就形成了区,Oracle中被称为EXTENT的数据逻辑存储分配单位就是这么形成的。EXTENT是Oracle的分配空间的最小单位,注意分配这个词。
接下来,当用户创建了一张T表,实质上也就是创建了一个Segment,只要segment创建成功,数据库就一定为其分配了若干数据块(data block)的初始数据扩展(initial extent),即便表中没有数据,但是这些初始数据扩展中的数据块已经为即将插入的数据源准备做好了。
每个segment的定义中都包含了数据扩展(extent)的存储参数(storage parameter),存储参数适用于各种类型的数据段。例如用户可以在create table语句中使用STORAGE 字句设定存储参数,来决定创建表时为其数据段(data segment)分配多少初始空间,或者限定一个表最多可以包含多少个数据扩展,如果用户没有指定存储参数,那么在创建表示使用表空间的默认存储参数。
若干个段(segment)又组成了表空间,根据表空间的功能,又可以划分为系统表空间,回滚表空间,临时表空间和用户表空间。
说了这么久,我们来用脚本试验一下,实际体会一下
以下查询说明数据块的大小是8kb(8192/1024),这是oracle的最小存储单位
也可以通过以下语句查询
表空间的创建:
-
普通数据表空间
-
临时表空间
注意 TEMPORARY和TEMPFILE关键字 -
回滚表空间
注意关键字UNDO
-
系统表空间
系统表空间和用户表空间都属于永久保留内容的表空间。
2、表的设计
根据思维导图的整理,我们知道oracle表可以分为普通堆表、全局临时表、分区表、索引组织表和簇表。可以说各种类型的表有各种类型的功能,这里我的理解为在遇到不同的业务场景下,该选则什么样的表才是最好的,即在什么场合选择什么技术。
2.1 普通堆表
2.11 更新开销大
先给出结论,普通堆表在更新操作时产生日志开销大。下面我们来验证:
首先准备分析数据库产生了多少日志脚本,如下:
SQL> SELECT a.name,b.value FROM v$statname a,v$mystat b WHERE a.STATISTIC#=b.STATISTIC# AND a.NAME='redo size';
NAME VALUE
---------------------------------------------------------------- ----------
redo size 820
该脚本是利用 v$statname 和v $mystat两个动态性能来跟踪当前session操作产生的日志,使用方法简单:首先执行该脚本,查看日志大小,随即执行的的更新语句,再执行该脚本返回的日志大小,两者相减,就是你更新语句产生的日志大小。实验如下:
--首先该视图需要sys授权之后才可以使用执行。
SQL> grant all on v_$mystat to wmj;
授权成功。
SQL> grant all on v_$statname to wmj;
授权成功。
--创建测试表
SQL> CREATE TABLE t AS SELECT * FROM Dba_Objects;
Table created
-- 创建视图方便直接用SELECT * FROM v_redo_size查询;
SQL> CREATE OR REPLACE VIEW v_redo_size AS
2 SELECT A.NAME, B.VALUE
3 FROM V$STATNAME A, V$MYSTAT B
4 WHERE A.STATISTIC# = B.STATISTIC#
5 AND A.NAME = 'redo size';
View created
SQL>
--下面执行删除操作
SQL> SELECT * FROM v_redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 93904
SQL> DELETE FROM t;
72512 rows deleted
SQL> SELECT * FROM v_redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 26911740
SQL>
观察上图,可以看到删除语句产生了 26911740-93904=26817836的日志量,因为这个单位是字节,所以大概25M的日志。然后继续试验
SQL> INSERT INTO t SELECT * FROM dba_objects;
72513 rows inserted
SQL> SELECT * FROM v_redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 35155776
SQL>
插入语句35155776-26911740=8244036,大概7M的日志数据,接下来看修改更新
SQL> UPDATE t SET t.object_id = ROWNUM;
72513 rows updated
SQL> SELECT * FROM v_redo_size;
NAME VALUE
---------------------------------------------------------------- ----------
redo size 47153872
SQL>
更新产生了47153872-35155776=11998096,大概11M数据。
以上代码说明无论删除更新还是插入,都会产生日志,这些日志用于数据库的备份和恢复。但是从性能来讲,写日志越多代表行动能越低。
2.12 删除无法释放空间
删除大家都知道怎么用,从以上看到删除産的日志最大,因为删除产生UNDO最多,而UNDO有需要REDO来保护,所以删除产生的redo 也最多,所以也有不少性能问题是和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(*)
----------
72514
执行计划
----------------------------------------------------------
Plan hash value: 2966233522
-------------------------------------------------------------------
| Id | Operation | Name | Rows | Cost (%CPU)| Time |
-------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 290 (1)| 00:00:04 |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| T | 68280 | 290 (1)| 00:00:04 |
-------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
28 recursive calls
0 db block gets
1104 consistent gets
1033 physical reads
0 redo size
529 bytes sent via SQL*Net to client
519 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
以上是为删除表示产生的物理读 1104 consistent gets,然后执行DELETE FROM t; ,在查看,推测物理读应该是减少。
SQL> DELETE FROM t;
已删除72514行。
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 | 290 (1)| 00:00:04 |
| 1 | SORT AGGREGATE | | 1 | | |
| 2 | TABLE ACCESS FULL| T | 1 | 290 (1)| 00:00:04 |
-------------------------------------------------------------------
Note
-----
- dynamic sampling used for this statement (level=2)
统计信息
----------------------------------------------------------
0 recursive calls
0 db block gets
1038 consistent gets
0 physical reads
0 redo size
526 bytes sent via SQL*Net to client
519 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
对比发现 物理读 1038 consistent gets 并没有减少,为什么的呢?明明数据已经被删了,物理读却没有减少。下面在看代码
SQL> TRUNCATE TABLE t ;
表被截断。
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 | 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)
统计信息
----------------------------------------------------------
20 recursive calls
1 db block gets
10 consistent gets
0 physical reads
96 redo size
526 bytes sent via SQL*Net to client
519 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed
SQL>
这时发现物理读 10 consistent gets大幅度下降,这说明什么?delete操作不能释放空间!虽然记录被删除了,但是这些空块依然存在,Oracle在查询时,依然会去查询这些空快,而TRUNCATE 是一种释放的高水平动作,直接把空块也回收掉,空间也就释放了。
2.13 表记录太大检索较慢
新建一张表,也就是一个SEGMENT,一般情况,对表查询,就是对SEGMENT所有的块进行遍历,依次找到我们的数据,但是数据越多,遍历的块也就越多,查询性能也就越慢。如何解决?Oracle提供两种技术,索引和分区。下面看代码
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 t.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):