数据库技术丛书

Oracle 11g数据库管理员指南

刘宪军　著

ISBN：978-7-111-30935-2

本书纸版由机械工业出版社于2010年出版，电子版由华章分社（北京华章图文信息有限公司）全球范围内制作与发行。

版权所有，侵权必究

客服热线：+ 86-10-68995265

客服信箱：service@bbbvip.com

官方网址：www.bbbvip.com

新浪微博 @言商书局BBBVIP

腾讯微博 @bbb-vip

前言

目前全世界发展势头最猛的IT巨头可能要数Oracle公司了。几年前，Oracle收购了BEA公司，成功地将Weblogic这个优秀的应用服务器软件收归己有。不久前，Oracle又将SUN公司纳入自己旗下，从此有了自己的小型机和UNIX操作系统，这无疑为Oracle插上了腾飞的双翼。然而，Oracle公司最引人注目的还是它的数据库产品。

在15年前，大家可能想不到Oracle公司会有今天的发展成果，当时的数据库市场是Sybase的天下，Oracle只是一个名不见经传的小公司。如今Oracle数据库产品以其出色的安全性、稳定性和优异的性能稳居数据库产品的榜首，占领了数据库市场的大部分份额，在银行、保险、通信、政府等应用领域具有绝对的话语权。

在一个企业应用系统中，核心部分是数据。对一个企业而言，数据就是生命。要管理重要的数据库，就需要高水平的数据库管理员。无论在国内还是国外，数据库管理员的待遇都是很丰厚的。然而Oracle软件毕竟是一个非常复杂的数据库产品，如果只掌握它的皮毛，你只能做一些初级的技术工作，根本无法管理数据库，你就失去了升职、深造的机会。不仅如此，如果不小心把数据搞丢了，还要承担法律责任。

想必大家都听过“庖丁解牛”的故事吧？庖丁在解牛的时候，手所接触的地方，肩所靠的地方，脚所踩的地方，膝盖所顶的地方，都发出皮骨相离声，刀子刺进去时响声更大，这些声音没有不合乎音律的。正当你在欣赏这种音律时，牛肉已经被干净利索地解了下来，而庖丁“提刀而立，为之四顾，为之踌躇满志，善刀而藏之”。究其原因，只有“依乎天理”，才能做到“以神遇而不以目视，官知止而神欲行”，从而“批大，道大，因其固然”。一般的厨工每月换一把刀，是因为他们用刀子去砍骨头，而庖丁的刀已经用了19年，还像新的一样。

学习Oracle也是一样的，只有掌握Oracle的脉络，那么在解决实际问题的时候才能像庖丁那样，做到得心应手，游刃有余。在客户现场，当几个工程师（可能包括原厂工程师）正在为解决一个问题争得面红耳赤的时候，如果你三下五除二帮客户解决了问题，你也能体会到庖丁那种“踌躇满志”的感觉。

本书的编写目的，就是试图使读者掌握Oracle的脉络，而不是教大家怎样学SQL语句。本书既不是对英文资料的翻译，也不是对Oracle产品用法的直白描述，而是对作者多年从事技术支持和培训工作的经验总结。书中列举了很多实际的例子，都是作者在客户现场遇到过并亲手解决的实际案例，希望这些例子对大家有所帮助。

读者在学习Oracle时，可以先学习怎样在UNIX/Linux系统中安装Oracle软件，然后学习怎样创建数据库，接着应该学习SQL和PL/SQL。接下来要重点学习的内容是Oracle的体系结构，只有掌握了这部分内容，才有可能对数据库进行管理、备份与恢复以及性能优化。以后如果有机会配置RAC集群环境，或者利用DataGuard配置数据库的异地容灾，读者就能体会到，一旦掌握了Oracle体系结构，对自己的帮助是多么大。

现在的企业应用系统都越来越复杂，涉及好多种技术。要想管理好数据库，只掌握少数的两三种技术是不够的。就数据库而言，目前的运行环境一般都是UNIX/Linux，管理员应该至少掌握一种UNIX，还应该了解Java、网络和一些存储方面的知识。

在本书编写之前不久，Oracle公司发布了Oracle 11g的最新版本的产品11.2。根据以往的情况来看，Oracle每个版本的产品都有两个次版本，其中第二个次版本是最稳定、用户使用时间最长的产品。如Oracle 10g有两个次版本10.1和10.2，目前很多企业用户还在使用10.2这个版本。本书的内容力求体现Oracle的最新特点。

面对技术发展的迅猛势头，作者也感觉才疏学浅。本书难免有疏漏和不足的地方，敬请读者朋友批评指正。

作者

2010年5月

第一部分　Oracle使用基础

第1章　SQL语言基础

SQL是结构化查询语言（Structured Query Language）的缩写，它是目前关系数据库系统中通用的标准语言。

SQL最早在20世纪70年代由Boyce和Chamberlin提出，并首先在IBM公司的数据库管理系统System R上实现，随后又在IBM的DB2上实现，并获得了巨大的成功。后来美国标准化组织和国际标准化组织先后将SQL作为关系数据库系统的标准语言，从此，SQL得到了发展的机会。到目前为止，包括Oracle、Sybase、Informix等在内的几乎所有大型数据库系统都支持SQL。

SQL在字面上虽然称为结构化查询语言，实际上它还包括数据操纵、数据定义、事务控制、安全控制等一系列命令。SQL操作的基本对象是表，也就是关系。它可以对表中的数据进行查询、增加、删除、修改等常规操作，还可以维护表中数据的一致性、完整性和安全性，能够满足从单机到分布式系统的各种应用需求。

SQL是一种非过程化的语言，用户在使用SQL操作数据时，只需要告诉系统做什么，而不需要关心怎么做，系统会根据用户的意图自动完成相应的操作。由于SQL的这一特点，它被人们称为“第四代语言”（4GL），以区别于面向过程的高级语言。

用SQL语言编写的SQL语句有两种执行方式，一种是联机交互方式，SQL语句在一定的平台上执行，例如数据库管理系统提供的实用程序。这个执行平台将SQL语句提交给数据库服务器，并将从数据库服务器返回的执行结果显示给用户。另一种方式是嵌入方式，用户在用C/C++、Java等高级语言编写应用程序时，可能需要操作数据库中的数据，这时SQL作为一种嵌入式语言，嵌入到高级语言程序中，通过数据库接口如ODBC、JDBC访问数据库中的数据。

SQL包括一系列命令，可以满足对数据的各种访问。按照通用的分类标准，SQL命令分为以下几种类型：

·查询命令　包括SELECT命令

·DML命令　包括INSERT、DELETE、UPDATE命令

·DDL命令　包括CREATE、DROP、ALTER、RENAME、TRUNCATE命令

·事务控制命令　包括COMMIT、ROLLBACK、SAVEPOINT命令

·DCL命令　包括GRANT、REVOKE命令

命令和相关的参数一起构成了SQL语句。下面将对这些命令分别进行详细的介绍。

1.1　查询语句

查询语句是使用最为频繁的数据库访问语句，对应的SQL命令是SELECT。虽然只有一条命令，但是由于它有灵活多样的形式，以及功能强大的子句，可以组成各种复杂的查询语句，能够完成各种复杂的查询。

SELECT语句可以根据用户的要求查询数据库中的数据，并且可以对它们进行简单的计算和统计。最简单的SELECT语句只有一个FROM子句，格式如下：

SELECT表达式

FROM表名

其中SELECT之后引导一个或多个列名，或者表达式，用来指定需要查询的列，或者对数据所进行的计算。在FROM子句指定一个或多个表名，用来指定本次查询所涉及的表。查询的结果是返回一行或多行数据，每行由一个或多个列的列值组成。

完整的SELECT语句包括WHERE、ORDER、GROUP等子句。格式如下：

SELECT表达式

FROM表名

WHERE条件

GROUP BY列名

HAVING条件

ORDER BY表达式

SELECT语句最灵活的用法体现在WHERE子句中的查询条件，这个条件用来指定查询什么样的数据。在以下各节中，我们将分别介绍SELECT语句的各个组成部分。

1.1.1　查询语句的基本用法

如果要查询某个表中一个或多个列的数据，需要在SELECT命令之后指定列名，并在FROM子句中指定查询所涉及的表。格式如下：

SELECT列1，列2……

FROM表名

查询的结果是从指定的表中将指定列的数据显示出来。例如，要查询dept表中的deptno和loc列，对应的SELECT语句为：

SELECT deptno, loc FROM dept；

这样的语句可以在Oracle提供的实用工具SQL*Plus中执行，也可以在其他实用工具或应用程序中执行。SQL*Plus的提示符是“SQL＞”，在本书中介绍的SQL语句基本上是在SQL*Plus中执行的。在操作系统的终端窗口中输入sqlplus命令，并指定用户名和口令，即可登录数据库服务器。例如（其中$为UNIX/Linux系统的SHELL提示符）：

$sqlplus scott/tiger

SQL语句中除字符串外，各个部分是大小写不敏感的。如果在SQL*Plus中执行SQL语句，还要在语句末尾加上一个分号。分号并不是SQL语句的一部分，只是语句结束的标志。一条句可以在一行中书写，也可以分行书写。关于SQL*Plus的详细用法，请参阅第2章。这条命令的执行结果为：

DEPTNO LOC

10 NEW YORK

20 DALLAS

30 CHICAGO

40 BOSTON

如果要查询表中的所有的列，可以用“*”符号代替所有的列名。例如：

SQL＞SELECT*FROM dept；

如果不了解表的结构，可以在SQL*Plus中执行命令DESCRIBE（简写为DESC），查看表的结构。这个命令的参数是表名，或者其他对象名。注意，这条命令不是SQL命令，而是SQL*Plus中的命令。

为了演示SQL语句的用法，在本章中大部分SQL语句都以Oracle提供的模板模式中的表emp和dept为操作对象。只要以用户名scott和口令TIGER登录数据库服务器，就能直接访问这两个表。其中emp表是员工的信息表，dept是部门信息表，这两个表的结构如下所示（其中各列的意义是作者标注的）：

在默认情况下，在显示数据时，各列的标题就是列的名称。在SELECT语句中可以定义列的别名，这样在显示数据时，列的标题就是这个别名，在整个SQL语句中都可以使用这个别名。使用别名的SELECT语句格式为：

SQL＞SELECT列1 AS别名1，列2 AS别名2……

或者在列名后直接指定别名，省略AS关键字。例如：

SQL＞SELECT deptno AS部门编号，loc地址FROM dept；

这条命令的执行结果为：

部门编号　地址

10 NEW YORK

20 DALLAS

30 CHICAGO

40 BOSTON

在查询结果中如果有重复行，可以使用DISTINCT关键字去掉重复行的显示。重复行是指在SELECT语句中涉及的所有列的列值完全相同的行。例如，要查询员工所分布的部门，可以用DISTINCT关键字去掉其中的重复行：

SQL＞SELECT DISTINCT deptno AS部门编号FROM emp；

实际上多名员工在同一部门上班的情况是存在的，但是这条命令执行的结果去掉了重复的部门编号的显示。命令执行的结果为：

部门编号

10

20

30

SELECT语句不仅可以进行简单的查询，还可以对查询的列进行简单的计算，也可以在两个列之间进行计算，或者将某个列与其他表达式，或者两个表达式进行计算。表1.1中列出了在SELECT语句可以使用的运算符。

使用||运算符可以将两个数据连接起来。无论是数字型还是日期型数据，在进行这种运算时，都可以看做是字符型数据。通过||运算符，用户可以设计自己喜欢的数据显示方式，如将两个列的值连接起来，也可以将列的值与其他文字连接起来。连接以后所得的数据可以当做一个列来显示。例如，可以将dept表中的deptno和loc列以及其他文字连接起来，相应的SELECT语句为：

SQL＞SELECT'部门'||deptno||'的地址为：'||loc AS部门地址FROM dept；

这条命令的执行结果为：

部门地址

-------------------------------------

部门10的地址为：NEW YORK

……

如果在SQL语句中使用了字符串，必须用一对单引号将字符串限定，并且字符串中的字符是大小写敏感的。

加减乘除四则运算在SELECT语句中比较简单，需要注意的是空值的计算。空值与其他数据进行四则运算时，结果将得到空值，而不管它与什么样的数据运算。例如，要在emp表中查询员工的工资与奖金之和，由于部分员工的奖金为空，致使查询的结果与我们希望的结果不符。查询语句为：

SQL＞SELECT sal+comm AS总收入FROM emp；

这条语句的执行结果为：

总收入

----------

1900

1750

……

已选择12行。

在emp表中共有12名员工，每名员工都有工资。如果奖金为空，对应的计算结果就为空。空值与0或者空格是不同的。空值就是没有数据，而0或者空格是实实在在的数据，就像考试没有成绩和得了0分是不一样的。为了解决空值的计算问题，SQL提供了一个函数，这个函数是NVL，它的功能是把空值转换为其他可以参加运算的数据。这个函数的调用格式是：

NVL（表达式，替代值）

当表达式的结果为空时，这个函数就把表达式的值用指定的值代替。有了这个函数，我们就可以在奖金为空时把它用0或者其他数据代替。改进后的查询工资和奖金之和的语句为：

SQL＞SELECT sal+nvl（comm，0）AS总收入FROM emp；

总收入

----------

800

1900

1750

2975

……

已选择12行。

SELECT命令还可以用来计算一个普通表达式的值，这个表达式可能与表没有任何关系，如3*5这样的表达式。例如：

SQL＞SELECT 3*5，3+5 FROM dept；

不过这样的查询语句所得的结果却不是我们希望的。这条SELECT语句执行的结果为：

3*5 3+5

--------------------

15 8

15 8

15 8

15 8

原来，查询的结果是把表达式的值重复了若干次。因为SELECT语句必须通过FROM子句指定一个或多个表，而表达式与这些表是无关的，所以，SELECT语句简单地根据可以查询到的行数，将表达式的值重复若干次。为了解决这个问题，Oracle提供了一个特殊的表dual，这个表的结构为：

SQL＞DESC dual

名称是否为空?类型

--------------------------------------------------------

DUMMY VARCHAR2（1）

通过查询这个表，发现表中只有一行数据：

SQL＞SELECT*FROM dual；

D

-

X

可见，dual表只有一个列，而且中有一行数据。所以，在进行与具体的表无关的运算时，可以在FROM子句中指定dual表，这样可以保证计算的结果只显示一次。例如：

SELECT 3*5，3+5 FROM dual；

1.1.2　查询语句中的条件

在前面所列举的查询中，由于没有限制条件，所以查询的结果是将表中的所有行都显示出来。如果希望只查询一部分行，那么可以通过WHERE子句指定条件。WHERE子句的作用是通过指定条件，使SELECT语句仅仅查询符合条件的行，如部门10的员工数据，或者工资大于2000元的员工数据等。在更多情况下，都需要根据指定的条件对数据进行查询。

WHERE子句指定的条件是一个关系表达式，如果关系表达式的结果为真，则条件成立，否则条件不成立。关系表达式用于比较两个表达式的大小，或者进行模糊匹配，或者将一个表达式的值与一个集合中的元素进行匹配。表1.2列出了常用的关系运算符。

例如，要在表dept中查询部门10的员工姓名和工资信息，对应的SELECT语句为：

SQL＞SELECT ename, sal FROM emp WHERE deptno=10；

下面的SELECT语句用于查询员工KING的基本情况：

SQL＞SELECT empno, ename, sal, comm FROM emp WHERE ename='KING'；

LIKE运算符通常用来进行字符串的模糊匹配，而“=”运算符只能对字符串进行精确比较。在LIKE指定的关系表达式中可以使用两个通配符：%和_，其中%可以代替多个字符，_可以代替一个字符。例如，要查询包含字符串“AR”的员工姓名，构造的SELECT语句为：

SQL＞SELECT ename FROM emp WHERE ename LIKE'%AR%'；

又如要查询这样的员工，姓名中第一个字符是任意字符，第二个是“A”，然后是若干任意字符，这时构造的SELECT语句为：

SQL＞SELECT ename FROM emp WHERE ename LIKE'_A%'；

注意，%用来代替多个连续的字符，包括空字符串，而_只能用来代替一个字符，不包括空字符。

IN运算符用来与一个集合中的元素进行比较。SELECT语句将指定的表达式与集合中的元素一一比较，只要与其中一个相等，则条件成立。如果没有任何一个元素与表达式的值相等，则条件不成立。例如，下面的SELECT语句用于查询其姓名在指定集合之中的员工：

SQL＞SELECT ename FROM emp WHERE ename IN（'SMITH'，'FORD'，'HELLO'）；

BETWEEN运算符用于将表达式的值与两个指定数据进行比较，如果表达式的值在这两个数据之间，则条件成立。这两个数据和表达式必须能够比较大小，而且后一个数据必须大于前一个数据。例如，下面的SELECT语句用于查询工资在1000到2000之间的员工：

SQL＞SELECT ename FROM emp WHERE sal BETWEEN 1000 AND 2000；

如果用包含“＞”等运算符的表达式改写上述SQL语句，则对应的SELECT语句为：

SQL＞SELECT ename FROM emp

WHERE sal＞=1000 AND sal＜=2000；

在复杂的查询语句中，可能需要多个条件，这些条件通过AND或OR运算符连接。多个条件表达式连接起来以后，就构成一个逻辑表达式。逻辑表达式的结果要么为真，要么为假，它是与两个关系表达式的值和所使用的连接运算有关的。假设X和Y是两个关系表达式，表1.3列出了两个关系表达式的运算规则。

例如，要查询在部门10工作，且工资在1000和2000之间的员工姓名，相应的SELECT语句为：

SQL＞SELECT ename FROM emp

WHERE deptno=10 AND sal BETWEEN 1000 AND 2000；

NOT运算符的作用是对关系表达式的值取反。它的用法是在关系表达式之前加上NOT运算符。例如，要查询工资不大于1000的员工姓名，相应的SELECT语句为：

SQL＞SELECT ename FROM emp WHERE NOT sal＜1000；

这条语句等价于：

SQL＞SELECT ename FROM emp WHERE sal＞=1000；

在默认情况下，NOT运算符只对最近的一个关系表达式取反，如果要对已经通过AND或OR连接的多个关系表达式同时取反，则要用一对圆括号将多个关系表达式限定。例如，要对下列SELECT语句中的两个条件同时取反：

SELECT ename FROM emp WHERE sal＞1000 AND sal＜2000；

对两个条件同时取反以后的SELECT语句为：

SELECT ename FROM emp WHERE NOT（sal＞1000 AND sal＜2000）；

这条语句等价于：

SELECT ename FROM emp WHERE sal＜=1000 OR sal＞=2000；

在WHERE子句中构造条件时，还要注意空值的运算。空值与任何数据进行赋值运算、四则运算以及关系运算时，结果都为空值。例如，下列的SELECT语句本意是要查询姓名不为空的所有员工的工资，查询的结果应该是所有员工的工资，但是结果却为空。

SQL＞SELECT sal FROM emp WHERE ename！=NULL；

原因是ename列与空值进行了关系运算，结果为空，于是整个条件表达式的结果为假。判断某个表达式是否为空值的运算符是“IS NULL”，判断是否不为空值的运算符是“IS NOT NULL”。例如，用运算符“IS NOT NULL”重新构造上述SELECT语句，将得到我们希望的结果。这个SELECT语句为：

SQL＞SELECT sal FROM emp WHERE ename IS NOT NULL；

1.1.3　查询语句中的单行函数

在SELECT语句中不仅可以对数据进行前面讲述的各种运算，还可以把数据作为函数的参数，进行其他的计算。所谓单行函数，就是分别作用于查询结果中的每一行，对于每一行，分别得到一个计算结果。这些函数都是Oracle提供的系统函数，用户可以在SELECT语句或其他SQL语句中直接使用它们。Oracle提供的函数很多，这里仅仅把最常用的函数进行分类介绍。在函数中处理的数据可以是某个列的列值，也可以是某个表达式，在举例时不再单独说明。

1.字符串处理函数

顾名思义，这类函数以字符串为处理对象，处理的结果是另一字符串或者一个数字。

（1）CONCAT函数

这个函数的参数是两个字符串，计算的结果是将两个字符串连接在一起，生成一个新的字符串。例如，concat（‘Hello，’，‘World’）的结果为Hello, World。

（2）CHR与ASCII函数

CHR函数的参数是一个正整数，它将这个正整数作为ASCII码，返回对应的字母。例如chr（65）的结果为A。ASCII函数的作用正好相反，它以一个字符为参数，返回这个字符对应的ASCII码。例如ASCII（‘A’）的结果为65。

（3）INSTR函数

这个函数在一个字符串中查找另一个字符串，如果找到，则返回出现的位置，否则返回0，位置的编号从1开始。这个函数的语法格式为：

instr（字符串，子字符串，start, occurrence）

其中前两个参数是必需的，这个函数在第一个参数中查找第二个参数，得到的结果是第二个参数在第一个参数中出现的位置，如果没有找到，则返回0。后两个参数是可选的，参数start指定从第一个参数的什么位置开始查找，默认从1开始，即第一个字符。参数occurrence指定查找子字符串的第几次出现。例如，要在部门名称中查找字符串‘TI’，相应的SELECT语句为：

SQL＞SELECT dname, instr（dname，'TI'）AS location FROM dept；

DNAME LOCATION

------------

ACCOUNTING 7

RESEARCH 0

SALES 0

OPERATIONS 6

（4）LENGTH函数

这个函数的作用是求得一个字符串的长度。例如length（‘Hello’）的结果为5。

（5）LOWER和UPPER函数。

这两个函数的作用是进行字符串的大小写转换，它们的参数都是一个字符串。其中LOWER函数将字符串中的字母转换为对应的小写字母，UPPER函数将字符串中的字母转换为对应的大写字母。例如，函数lower（‘Hello’）的结果为hello，而函数upper（‘Hello’）的结果为HELLO。

（6）LPAD与RPAD函数

这两个函数的作用是在字符串中填充指定的字符，使字符串达到指定的长度。LPAD函数从左边填充，RPAD函数从右边填充，处理的结果是得到一个新的字符串。这两个函数的语法格式为：

LPAD（字符串，长度，填充字符）

RPAD（字符串，长度，填充字符）

这两个函数在字符串中填充指定的字符，使其达到指定的长度，默认是填充空格。如果指定的长度比字符串本来的长度小，则将字符串截断，只保留新的长度。例如，对部门名称分别进行左填充和右填充，使其长度为10，填充的字符分别是“*”和“#”，对应的SELECT语句为：

SQL＞SELECT lpad（dname，12，'*'）AS左填充，rpad（dname，12，'#'）AS右填充FROM dept；

这条语句执行的结果为：

左填充右填充

------------------------

**ACCOUNTING ACCOUNTING##

****RESEARCH RESEARCH####

*******SALES SALES#######

**OPERATIONS OPERATIONS##

（7）LTRIM、RTRIM函数和TRIM函数

这三个函数的作用是去掉字符串左边或右边连续的空格，并得到一个新的字符串。例如，函数ltrim（‘Hello’）的结果为Hello, rtrim（‘Hello’）的结果为Hello。

TRIM函数的作用是同时去掉字符串左边和右边的连续空格，它相当于对字符串先执行LTRIM函数，再执行RTRIM函数，或者先执行RTRIM函数，再执行LTRIM函数。

（8）REPLACE函数

这个函数的作用是在一个字符串中查找另一个字符串，并将找到的字符串用第三个字符串代替。这个函数的语法格式为：

replace（字符串，子字符串，替换字符串）

如果在字符串没有找到子字符串，则不做任何处理，如果找到，则用替换字符串代替。如果没有指定替换字符串，则将找到的子字符串从原字符串中删除。例如，假设要在部门名称中查找字符串TI，并将其替换为Hello，为了进行对比，我们将替换前后的字符串都显示出来。对应的SELECT语句为：

SQL＞SELECT dname, replace（dname，'TI'，'Hello'）AS new_str FROM dept；

字符串替换的结果为：

DNAME NEW_STR

------------------------

ACCOUNTING ACCOUNHelloNG

RESEARCH RESEARCH

SALES SALES

OPERATIONS OPERAHelloONS

（9）SUBSTR函数

这个函数的作用是根据指定的开始位置和长度，返回一个字符串的子字符串。它的语法格式为：

substr（字符串，开始位置，长度）

位置编号从1开始。开始位置可以是正整数或负整数。如果是负整数，则从字符串的右边开始数。长度是可选的，如果缺省，则返回从开始位置到字符串末尾的所有字符。例如：

substr（'Hello'，2，3）的结果为ell

substr（'Hello'，-3，2）的结果为ll

substr（'Hello'，-3）的结果为ello

2.数学函数

数学函数的处理对象是数字型数据，处理的结果一般也是数字型数据。

（1）ABS函数

这个函数的作用是求得一个数字的绝对值。

（2）CEIL和FLOOR函数

这两个函数都以一个数字为参数，CEIL函数返回大于或等于这个数字的最小整数。FLOOR函数返回小于或等于这个数字的最大整数。例如：

ceil（-15.6）的结果是-15，floor（-15.6）的结果是-16。

ceil（15.6）的结果是16，floor（15.6）的结果是15。

（3）MOD函数

这个函数有两个参数，结果是两个数相除所得的余数。

（4）ROUND和TRUNC函数

ROUND函数的作用是对数据进行四舍五入计算。这个函数的语法结构为：

round（数据，舍入位置）

其中舍入位置可以是正整数，也可以是负整数。如果是正整数，则从小数点开始向右数，一直到舍入位置，从这一位开始四舍五入。如果是负整数，则从小数点开始向左数，然后进行四舍五入。例如：

round（49.456，2）的结果为49.46

round（89.456，-1）的结果为90

round（89.456，-2）的结果为100

TRUNC函数的用法与ROUND函数类似，只不过它的功能是对数据进行截取运算，只舍不入，也就是把一个数据的指定位之后的数字全部舍去。例如：

trunc（49.456，2）的结果为49.45

trunc（49.456，-1）的结果为40

3.日期型函数

日期型函数的处理对象是日期型数据，处理的结果一般也是日期型数据。

（1）ADD_MONTHS函数

这个函数在某个日期的基础上，加上一个指定的月数，返回一个新的日期。它的格式为：

ADD_month（日期，月数）

这个函数是在指定的日期上加上若干个整月数，所以日期中的日应该保持不变。但是指定的日期如果是当月的最后一天，函数会做相应的调整，以保证返回的日期也是当月的最后一天。例如：

ADD_months（to_date（'12-05-10'，'dd-mm-yy'），6）的结果为"12-11月-10"；

ADD_months（to_date（'31-01-10'，'dd-mm-yy'），1）的结果为"28-2月-10"

ADD_months（to_date（'28-02-10'，'dd-mm-yy'），1）的结果为"31-3月-10"

在ADD_MONTHS函数中，不能直接使用类似“12-05-10”这样的日期型数据，因为Oracle把这样的数据是当做字符串来处理的，所以首先要调用to_date函数将它转换为真正的日期型数据。上述第一个例子容易理解。在第二个例子中，因为31日是2010年1月的最后一天，所以加上一个月后，得到2月的最后一天，即28日。同理，在2010年2月28日上加上一个月后，得到3月的最后一天，即3月31日。函数返回的结果可能因系统的不同而显示不同的格式，因为不同系统采用的日期格式可能不同。

（2）LAST_DAY函数

这个函数返回指定日期所在月份的最后一天。例如：

last_day（to_date（'01-02-10'，'dd-mm-yy'））的结果为"28-2月-10"。

（3）MONTHS_BETWEEN函数

这个函数有两个参数，都是日期型数据，返回的结果是两个日期之间相差的月数。这个函数的语法格式为：months_between（日期1，日期2）。例如：

months_between（to_date（'01-05-10'，'dd-mm-yy'），to_date（'01-05-09'，'dd-mm-yy'））

结果为12

months_between（to_date（'01-05-10'，'dd-mm-yy'），TO_DATE（'12-05-09'，'dd-mm-yy'））结

果为11.6451613

如果两个日期中的日相同，或者都是当月的最后一天，则返回结果是一个整数，否则将返回一个小数。第一个参数如果比第二个参数小，则返回的结果为负数。

（4）NEXT_DAY函数

这个函数有两个参数，一个是日期，一个是与星期几对应的整数，返回的结果是这个日期之后最近的星期几所对应的日期。函数的调用格式为：

next_day（日期，整数）

例如，假设2010年4月28日是星期三，则最近的星期五应该是2010年4月30日，最近的星期二应该是2010年5月27日。注意星期的编号方法是：星期天为1，星期一为2，依此类推。例如：

next_day（to_date（'28-04-10'，'dd-mm-yy'），6）的结果为"30-4月-10"

next_day（to_date（'28-04-10'，'dd-mm-yy'），3）的结果为"04-5月-10"

（5）ROUND函数

这个函数对指定的日期进行四舍五入处理。它有两个参数，一个是日期，一个是表示日期某个组成部分的格式字符串。函数的语法格式为：

round（日期，格式字符串）

其中格式字符串用于指定从日期的哪一部分开始四舍五入。Oracle支持的格式字符串包括yy（或yyyy）、mm、dd、hh，分别表示从年、月、日、时位进行四舍五入。例如：

round（to_date（'28-08-10'，'dd-mm-yy'），'mm'）的结果为"01-9月-10"

round（to_date（'12-08-10 13：50'，'dd-mm-yy hh24：mi'），'dd'）结果为"13-8月-10"

在第一个例子中，指定从月开始四舍五入，由于28日已经过了该月的一半，故该位向前进位。在第二个例子中，指定从日开始四舍五入，由于13时已经超过当天中午12时，故向前进位，使12日变为13日。

（6）SYSDATE函数

这是个很常用的函数，它用来获得系统当前时间。在有些日志操作中，常常需要记录当前时间，使用这个函数是很方便的。这个函数没有任何参数。这个函数返回的时间可以精确到秒，但在显示时可能只显示到日，根据系统的不同显示格式有所变化。如果希望得到时、分、秒，则需要通过to_char函数进行数据类型转换。例如：

SQL＞SELECT sysdate FROM dual；

SYSDATE

----------

26-4月-10

（7）TRUNC函数

这个函数的功能类似于ROUND函数，但是它不进行四舍五入，而是从指定位开始，截断其后面的部分，它的语法格式与ROUND函数相同。例如：

trunc（to_date（'28-08-10 23：50'，'dd-mm-yy hh24：mi'），'dd'）结果为"28-8月-10"

trunc（to_date（'28-08-10'，'dd-mm-yy'），'mm'）结果为"01-8月-10"

4.类型转换函数

在进行数据处理时，常常需要对数据进行类型转换。数据类型转换主要涉及字符型、数字型和日期型数据之间的相互转换，涉及的函数有to_char、to_date和to_number。

（1）to_char

to_char函数的作用是将一个日期型或者数字型数据转换为字符串。如果操作对象是日期型数据，这个函数的语法格式为：

TO_CHAR（日期，格式字符串）

其中格式字符串是由日期格式元素和隔离符号组成的字符串，用来规定转换的格式。例如，下列语句将员工表中员工的受聘日期按照指定的格式转换为字符串：

SELECT ename, to_char（hiredate，'yyyy-mm-dd day'）FROM emp WHERE empno=7902；

ENAME TO_CHAR（HI

--------------------

FORD 1981-12-03星期四

又如，下列SELECT语句获取当前系统时间，然后按照指定的格式进行转换：

SELECT to_char（sysdate，'yyyy-mm-dd hh24：mi：ss'）FROM dual；

TO_CHAR（SYSDATE，'YY

-------------------

2010-05-26 12：17：25

常用的日期格式元素如表1.4所示。

如果没有指定格式字符串，则按照当前系统默认的时间格式转换为字符串。

如果操作对象是数字型数据，to_char函数的语法格式为：

to_char（数字，格式字符串）

其中格式字符串是由数字格式元素和小数点、分隔符组成的字符串，用来控制转换的格式。这种转换主要用在财务报表中。例如：

to_char（5432，'$9，999.99'）的结果为'$5，432.00'

常用的数字格式元素如表1.5所示。

（2）to_date函数

这个函数的作用是把一个字符串转换为一个日期型数据，它有两个参数，处理的结果是一个日期型数据。这个函数的语法格式为：

to_date（字符串，格式字符串）

其中格式字符串由表1.4中的格式元素组成，它的作用是把字符串中用分隔符分开的不同部分解释成一个日期的不同部分。例如：

to_date（'12-09-2010'，'dd-mm-yyyy'）结果为"12-9月-10"

通过格式字符串，把字符串中的12解释为日，09解释为月，2010解释为年，然后将这个字符串转换为一个日期型数据。格式字符串中的每部分对字符串中的每部分是一一对应地进行解释的，所以日期字符串中的每一部分对于格式字符串中的对应部分来说，必须是合法的数据。例如：

to_date（'12-02-2010'，'dd-mon-yyyy'）

将得到一个错误的结果，原因是格式字符串中的mon试图将字符串中的02解释为某个月份的缩写，而02并不是某个月份缩写。还要注意的是，转换后得到的日期格式与函数指定的格式并不一定相同，因为格式字符串是用来解释字符串中的不同部分的，而日期的显示格式依赖于当前系统的日期格式。

（3）to_number

这个函数的作用是把一个字符串转换为数字，它有两个参数，处理的结果是一个数字型数据。这个函数的语法格式为：

to_number（字符串，格式字符串）

其中格式字符串包含表1.5中所介绍的格式元素，它把字符串中的$以及用逗号、小数点分开的不同部分分别进行解释，它的分隔方式与字符串不同部分的分隔方式应该是一致的。例如：

to_number（'$1，000.50'，'$999，999.99'）的结果为1000.5

to_number（'$1000.50'，'$999，999.99'）的结果将出错

第二个例子出错的原因是字符串中的数字字符并没有每三位逗号隔开，而格式字符串却要按这种方式解释它。另外，格式字符串中的9或0的位数不应少于字符串中数字字符的位数。

1.1.4　分组函数与分组统计

1.分组函数

分组函数又称为聚集函数，是一种多行函数。之所以称为多行函数，是与单行函数对应的，因为这种函数对多行数据一起进行计算，只返回一个结果，而不是每行都返回一个结果。

聚集函数主要用来进行数据的统计，常用的聚集函数有以下几个：

·AVG求平均值

·MIN求最小值

·MAX求最大值

·SUM求和

·COUNT计数

（1）AVG函数

AVG函数用来求指定列上的平均值，它将自动忽略列上的空值。如果要去掉重复值的计算，可在列名前加上DISTINCE选项。例如，要求部门30的员工的平均工资，构造的SELECT语句如下：

SQL＞SELECT avg（sal），avg（distinct sal）FROM emp WHERE deptno=30；

为了观察重复值对这个函数的影响，在SELECT语句中进行了两种形式的函数调用，其中第二次调用去掉了重复值，对重复值只计算一次。这条语句的执行结果为：

AVG（SAL）AVG（DISTINCTSAL）

--------------------------

1566.66667 1630

（2）MIN与MAX函数

MIN函数的作用是求指定列的最小值，MAX函数的作用是求指定列的最大值。这两个函数都自动忽略空行。例如，要求部门30的员工的最低工资和最高工资，构造的SELECT语句如下：

SQL＞SELECT min（distinct sal），max（sal）FROM emp WHERE deptno=30；

这条SELECT语句的执行结果为：

MIN（DISTINCTSAL）MAX（SAL）

---------------------------

950 2850

（3）COUNT函数

COUNT函数用来计算数据的行数。在默认情况下，这个函数不计算空行。如果要计算空行，可以用“*”代替列名。如果要去掉重复值的计算，可在列名前加上DISTINCE选项，这样如果遇到重复值，只计算一次。例如，要计算公司中领取工资的人数，构造的SELECT语句如下：

SQL＞SELECT count（sal），count（distinct sal）FROM emp；

为了观察DISTINCE选项的作用，在SELECT语句中进行了两次函数调用。如果有两个员工的工资相同，只按一个人计算。这条语句的执行结果为：

COUNT（SAL）COUNT（DISTINCTSAL）

----------------------------

12 11

（4）SUM函数

SUM函数的作用是对指定列求和，它将自动忽略空值。如果要去掉重复值的计算，可在列名前加上DISTINCE选项。例如，要求部门30的员工工资总和，构造的SELECT语句如下所示：

SQL＞SELECT sum（sal）FROM emp WHERE deptno=30；

SUM（SAL）

----------

9400

现在，为了说明这些函数的用法，把它们综合起来，构造一个SELECT语句，求部门30的员工的平均工资、最高工资、最低工资、工资总和以及总人数。这条SELECT语句如下所示：

SQL＞SELECT avg（sal）AS平均工资，min（sal）AS最低工资，

max（sal）AS最高工资，sum（sal）AS工资总和

FROM emp

WHERE deptno=30；

这条SELECT语句的执行结果为：

平均工资　最低工资　最高工资　工资总和

----------------------------------------

1566.66667　950　2850　9400

2.分组统计

在上面最后一个例子中。我们对部门30的员工的工资进行了统计，用这种方法也可以统计其他部门的数据。但是每进行一次统计，都需要单独构造一条SELECT语句，如果表中的部门很多，或者部门数很难确定，用这种方法很难满足用户的查询要求。解决这个问题的一个办法是使用GROUP子句。

分组函数最常见的用法是与GROUP子句一起使用，用来对表中的数据进行分组统计。为了统计表中各个部门员工的工资，只要一条语句就可以完成。GROUP子句的语法格式为：

GROUP BY列1，列2……

GROUP子句根据指定的列对数据进行分组统计。首先根据第一个列进行分组统计，第一个列相同时再进一步根据第二个列进行分组统计。例如，要对公司各部门的员工工资进行统计，包括各部门的平均工资、最高工资、最低工资、工资总和和总人数，构造的SELECT语句如下所示：

SQL＞SELECT deptno AS部门号，avg（sal）AS平均工资，min（sal）AS最低工资，

max（sal）AS最高工资，sum（sal）AS工资总和

FROM emp

GROUP BY deptno；

这样我们就可以用一条语句完成所有部门的统计。这条SELECT语句的执行结果为：

部门号　平均工资　最低工资　最高工资　工资总和

--------------------------------------------

10　2916.66667　1300　5000　8750

20　2258.33333　800　3000　6775

30　1566.66667　950　2850　9400

与GROUP子句一起使用的还有一个子句，即HAVING子句。这个子句是可选的，它不能单独使用，只能配合GROUP子句使用，作用是对GROUP子句设置条件，对统计后的结果进行限制。例如，对于上述统计，我们只希望显示最低工资在900元以上，并且工资总和在7000元以上的部门的统计信息，相应的SELECT语句为：

SQL＞SELECT deptno AS部门号，avg（sal）AS平均工资，min（sal）AS最低工资，

max（sal）AS最高工资，sum（sal）AS工资总和

FROM emp

GROUP BY deptno

HAVING min（sal）＞900 AND sum（sal）＞7000

这样，部门20因为统计信息不满足设置的条件，就不被显示。这条SELECT语句的执行结果为：

部门号　平均工资　最低工资　最高工资　工资总和

--------------------------------------------------

10　2916.66667　1300　5000　8750

30　1566.66667　950　2850　9400

HAVING子句中的关系表达式必须使用分组函数，可以是在SELECT语句中已经出现的分组函数，也可以是没有出现的函数。虽然HAVING子句和WHERE子句都是用来设置条件的，但是WHERE子句设置的条件是在查询时起作用的，它决定查询什么样的数据，如果要进行统计，这样的条件是在统计之前就已经起作用了。而HAVING子句设置的条件只有在进行统计后才起作用，它决定了对于统计产生的数据，哪些需要显示给用户。

1.1.5　数据的排序

SELECT语句可以使用的最后一个子句是ORDER子句。以前在查询数据时，数据显示的顺序是不可预知的。如果要对数据进行某种方式的排序，就要借助于ORDER子句。ORDER子句的语法格式为：

ORDER BY列1排序方式，列2排序方式……

ORDER子句对查询到的数据按照指定列的大小排序。如果指定了多个排序列，则首先按照第一个排序列排序，如果这个列的值相同，则再按照第二个排序列继续排序。排序方式包括ASC和DESC，分别表示升序排序和降序排序，二者可选其一，默认的排序方式是升序排序。如果指定了多个排序列，可以为每个排序列单独指定排序方式。

例如，要对公司各部门的工资统计情况进行排序，要求是按照工资总和从大到小排序，如果工资总和相同，再按照部门号从小到大排序。相应的SELECT语句为：

SQL＞SELECT deptno AS部门号，avg（sal）AS平均工资，min（sal）AS最低工资，

max（sal）AS最高工资，sum（sal）AS工资总和

FROM emp

GROUP BY deptno

ORDER BY sum（sal）desc, deptno asc

这条SELECT语句的执行结果为：

部门号

平均工资

最低工资

最高工资

工资总和

--------------------------------------------------

30

1566.66667 950

2850

9400

10

2916.66667 1300

5000

8750

20

2258.33333 800

3000

6775

ORDER子句中的排序列可以是列名，可以是列的别名，也可以是其他的表达式，还可以是它在SELECT语句中的排列序号。例如上述SELECT语句中的第一个排序列就是一个函数，这个函数可以用前面定义的别名“工资总和”来代替，也可以用它的排列序号5来代替。上面的ORDER子句可以改为等价的形式：

ORDER BY 5 desc，部门号asc

如果在SELECT语句中用到了所有的子句，那么将构成一条复杂的SQL语句。这些子句的使用顺序是：WHERE子句、GROUP子句、HAVING子句、ORDER子句。现在我们再来看一个综合的例子，在这个例子中用到了SELECT语句的所有子句。假设要求按照部门号对员工的工资进行统计，参加统计的员工工资必须大于1000元，将统计结果中凡满足最低工资在900元以上，并且工资总和在7000元以上的部门统计信息显示出来，显示时按照工资总和从大到小排序，如果工资总和相同，再按照部门号从小到大排序。相应的SELECT语句为：

SQL＞SELECT deptno AS部门号，avg（sal）AS平均工资，min（sal）AS最低工资，

max（sal）AS最高工资，sum（sal）AS工资总和

FROM emp

WHERE sal＞1000

GROUP BY deptno

HAVING min（sal）＞900 AND sum（sal）＞7000

ORDER BY 5 desc，部门号asc

统计的结果如下所示。与以前的统计结果相比，这次的结果不同，原因是这次统计时设置了WHERE子句中的条件，如果不满足这个条件，就不会被查询到，当然就没有机会参加统计了。

部门号　平均工资　最低工资　最高工资　工资总和

--------------------------------------------------

10　2916.66667　1300　5000　8750

30　1690　1250　2850　8450

1.1.6　多表查询

以前讲述的查询语句都只涉及一个表的数据。在很多情况下，需要查询的数据往往涉及多个表，这时需要对多个表进行连接查询。例如，如果既要查询员工的信息，又要查询员工所在部门的信息，这就涉及emp和dept两个表。

表间的连接关系有相等连接、非相等连接、外连接和子连接等多种形式，其中最常用的连接形式是相等连接。相等连接体现在WHERE子句中指定的条件上，在条件中要指定两个表通过哪些列进行连接。一般情况下进行连接查询的两个表是通过主键和外键进行关联的，所以最简单的条件是一个表的外键与另一个表的主键相等。例如，下面的SELECT语句从EMP表中查询员工的姓名和工资，同时在dept表中查询员工所在部门的名称：

SQL＞SELECT ename, sal, dname FROM emp, dept WHERE emp.deptno=dept.deptno；

其中DEPTNO是DEPT表中的主键，同时它又是emp表中的外键，在这个查询语句中连接的条件是它们相等，条件emp.deptno=dept.deptno的意思是在emp表中查询每个员工所在的部门号，然后根据部门号在dept表中查询对应的部门名称，凡是不满足这个条件的部门名称都将被过滤掉。

如果一个列在两个表中同时存在，那么在SELECT语句中要用表名进行限定（表名.列名或者表的别名.列名），否则系统将无法确定是哪个表中的列。上面的SELECT语句可以改为下面的等价的语句：

SQL＞SELECT ename, sal, dname FROM emp a, dept b WHERE a.deptno=b.deptno；

在这个SELECT语句中，为了书写方便，为表emp和dept分别定义了别名a和b，这样在其他子句中就可以使用这样的别名了。在构造查询语句时，首先要仔细分析这个查询要涉及哪些表，以及这些表通过哪些列进行连接，然后在SELECT语句中指定所有涉及的表，在WHERE子句中指定连接条件。下面我们再考察一个涉及三个表的查询。除了emp和dept两个表以外，第三个表salgrade也是Oracle提供的模板表，这个表记录了工资级别的规定，它的结构为：

SQL＞desc salgrade

名称　是否为空?类型

--------------------------------------------------

GRADE NUMBER

LOSAL NUMBER

HISAL NUMBER

这三个列的意义分别是级别编号、工资下限和工资上限。

现在要查询部门10和20中每个员工的姓名、工资、工资级别以及所在部门的名称，相应的SELECT语句为：

SQL＞SELECT ename AS姓名，sal AS工资，grade AS工资级别，dname AS部门名称

FROM emp a, dept b, salgrade c

WHERE a.deptno=b.deptno

AND（a.deptno=10 or a.deptno=20）

AND（sal＞=c.losal and sal＜=c.hisal）

这条查询语句的执行结果为：

姓名　工资　工资级别　部门名称

SMITH 800 1 RESEARCH

MILLER 1300 2 ACCOUNTING

JONES 2975 4 RESEARCH

CLARK 2450 4 ACCOUNTING

KING 5000 5 ACCOUNTING

外连接是一种特殊的连接方式。假设有两个表A和B，用相等连接查询可以返回表A中的所有行，而表B中的部分行因为不满足相等条件，所以是不会被查询到的，但是利用外连接可以返回表B中的所有行。对于表A和B来说，外连接的条件表达式的格式为：

WHERE A.列名（+）=B.列名

如果要显示表B中所有行，包括使用相等连接无法显示的行，则在表A的列名之后指定外连接的标志“（+）”。例如，对于表emp和dept来说，利用相等连接可以查询所有员工的信息以及员工所在部门的信息。如果某个部门没有员工，那么该部门的信息是查询不到的，因为这样的部门不满足相等条件。但是如果使用外连接，可以保证它们同样被查询出来。完成这个查询功能的SELECT语句为：

SQL＞SELECT ename, dname FROM emp a, dept b WHERE a.deptno（+）=b.deptno；

这条SELECT语句的执行结果为：

ENAME DNAME

……

MARTIN SALES

WARD SALES

OPERATIONS

其中最后一个部门OPERATIONS为空，在表emp中没有与它的编号相等的员工，在相等连接查询中它是不会被显示出来的，但是利用外连接，可以保证这样的数据也被查询出来。

自连接是一种特殊的相等连接。相等连接一般涉及多个不同的表，自连接也涉及多个表，但是它们是同一个表。例如，在表emp中，每个员工都有一个顶头上司的编号，而这个上司同时也是该公司的员工。如果要查询每个员工的上司姓名，首先要确定上司的编号，然后根据这个编号再查询emp表，利用相等连接确定上司的姓名，这就相当于两个表的连接。能够完成这个查询的SELECT语句为：

SQL＞SELECT a.ename, b.ename AS manager

FROM emp a, emp b

WHERE a.mgr=b.empno；

这条语句的执行结果为：

ENAME MANAGER

SMITH FORD

ALLEN BLAKE

……

由于要把同一个表看成两个不同的表进行连接，所以在FROM子句中要为emp表定义两个不同的别名，而SELECT之后的两个ENAME列就分别是这两个表中的列，因此要用别名进行限定。在emp表中，员工KING是最高领导，他没有上司，所以在上述查询结果中并没有显示。为了在查询中将所有员工姓名都列出来，可以在上述查询的基础上再使用外连接。用于完成这个查询的SELECT语句为：

SQL＞SELECT a.ename, b.ename AS manager

FROM emp a, emp b

WHERE a.mgr=b.empno（+）；

1.1.7　子查询

子查询就是嵌套在另一个SELECT语句中的查询。在SELECT语句中，WHERE子句或者HAVING子句中的条件往往不能用一个确定的表达式来确定，而要依赖于另一个查询，这个被嵌套使用的查询就是子查询，它在形式上是被一对圆括号限定的SELECT语句。在子查询中还可以再嵌套子查询。

例如，要查询所有在部门RESEARCH工作的员工姓名。如果使用常规的查询方法，要进行两次查询，首先查询dept表，确定该部门的部门号，然后根据这个部门号在emp表中查询属于这个部门的员工。也就是说，需要两条SELECT语句：

SQL＞SELECT deptno FROM dept WHERE dname='RESEARCH'；

SQL＞SELECT ename FROM emp WHERE deptno=20；（部门RESEARCH的部门号）

连接这两条SELECT语句的纽带是中间结果—部门号（deptno）。要完成这样的查询，不得不需要人工干预，在两条SELECT语句中传递参数。如果利用子查询，这个问题就迎刃而解。能够完成这个查询功能的一条SELECT语句为：

SQL＞SELECT ename FROM emp

WHERE deptno=（SELECT deptno FROM dept WHERE dname='RESEARCH'）；

这种复杂的SELECT语句的执行过程为：首先执行子查询，将执行的结果返回给主查询，然后再根据条件执行主查询。

子查询一般出现在SELECT语句的WHERE子句或HAVING子句中，作为条件表达式的一部分。子查询的结果是返回一行或多行数据，可以被看做一个集合。条件表达式就是要将某个表达式与这个集合中的元素进行某种比较运算，根据运算的结果是真或是假来决定是否执行上一层查询。常用的运算符如表1.6所示。

其中EXISTS运算符测试子查询的返回结果，只要结果不为空，条件就为真，而主查询和子查询之间可能没有直接关系。例如，在下面的查询语句中，因为子查询返回的结果为空，条件为假，所以上主查询也返回空。

SQL＞SELECT ename FROM emp

WHERE exists（SELECT deptno FROM dept WHERE deptno=0）；

IN运算符将某个列的值与子查询的返回结果进行比较，只要与其中的一个结果相等，条件即为真。例如，要查询所有出现在emp表中的部门名称，即至少有一名员工的部门，构造的SELECT语句为：

SQL＞SELECT dname FROM dept

WHERE deptno IN（SELECT distinct deptno FROM emp）；

“=”运算符号比较特殊，它将某个列的值与集合中的元素进行精确匹配。如果子查询只返回单行结果，那么将这个列与这一行进行比较。如果子查询返回多行结果，那么必须用ANY或ALL进行限定，否则将出错。ANY运算符的作用是，只要列值与返回结果中的任何一个相等，条件即为真。ALL运算符的作用是，列值要与返回结果中的所有行都要进行比较。例如，要查询所有在emp表中出现的部门名称，即至少有一名员工的部门，也可以使用下面的SELECT语句：

SQL＞SELECT dname FROM dept

WHERE deptno=any（SELECT distinct deptno FROM emp）；

运算符＞、＜、＞=和＜=与=的用法相似，子查询可以返回单行结果，也可以返回多行结果。如果是多行结果，必须用ANY或ALL进行限定。下面我们再考察几个例子，比较ANY和ALL之间的区别。

如果要查询这样的员工姓名，他的工资高于部门30中的每个员工，相应的SELECT语句为：

SQL＞SELECT ename FROM emp

WHERE sal＞all（SELECT sal FROM emp WHERE deptno=30）；

如果要查询这样的员工姓名，他的工资不低于部门30中的最低工资。也就是说，工资高于部门30中其中任何一个员工即可。相应的SELECT语句为：

SQL＞SELECT ename FROM emp

WHERE sal＞any（SELECT sal FROM emp WHERE deptno=30）；

在子查询中还可以使用分组函数。例如，要查询所有比公司全部员工平均工资高的员工姓名，构造的SELECT语句为：

SQL＞SELECT ename FROM emp

WHERE sal＞（SELECT avg（sal）FROM emp）；

如果要查询这样的部门名称，它的平均工资高于其他部门的平均工资，这样的查询需要使用两次子查询。首先查询其他部门的平均工资，然后根据查询的结果查询其平均工资高于这个结果的部门号，最后根据这个部门号查询它的部门名称。相应的SELECT语句如下所示：

SELECT dname FROM dept

WHERE deptno=（SELECT deptno FROM emp a

GROUP BY deptno

HAVING avg（sal）＞all（SELECT avg（sal）FROM emp

WHERE deptno！=a.deptno

GROUP BY deptno））

在这条SELECT语句中，最后一个子查询最先执行，用来求得其他部门的平均工资。然后执行上一个子查询，返回平均工资高于其他部门平均工资的部门号。最后执行最上层的查询，返回这个部门的名称。

1.2　DML语句

DML是“数据操纵语言”（Data Manipulation Language）的简写。如果说SELECT语句对数据进行的是读操作，那么DML语句对数据进行的是写操作。DML语句的操作对象是表中的行，这样的语句一次可以影响一行或多行数据。DML包括三种操作：插入（INSERT）、删除（DELETE）、修改（UPDATE）。

1.2.1　INSERT语句

INSERT语句的作用是往表中插入一行，它的语法格式为：

INSERT INTO表（列1，列2……）VALUES（表达式1，表达式2……）；

在向表中插入一行时，INSERT语句将表达式的值作为对应列的值，列的排列顺序、数据类型和数量应该与表达式一致，否则可能会出错。如果没有指定某个列，那么在插入数据时这个列的值将为空。在表达式中，字符串类型数据的大小写是敏感的，日期型数据的格式在不同系统中是有区别的。例如，要往dept表中插入一行，部门号为50，部门名称为NETWORK，部门地址为BEIJING，相应的INSERT语句为：

SQL＞INSERT INTO dept（deptno, dname, loc）VALUES（50，'NETWORK'，'BEIJING'）；

在INSERT语句中如果指定了列名，那么它们的顺序可以随意，只要与VALUES子句中的表达式一一对应即可。如果要为所有的列都提供数据，则可以省略列名，但是VALUES子句中表达式的顺序、数据类型和数量必须与表中列的定义一致。例如，上面的INSERT语句为所有的三个列都提供了数据，所以可以简写为：

SQL＞INSERT INTO dept VALUES（50，'NETWORK'，'BEIJING'）；

在INSERT语句中为各列指定数据时，可以指定一个常量，或者指定一个表达式，如函数、算术运算表达式等。例如，当公司新来一名员工时，可以将当前时间作为它的受聘日期，作为表emp中列hiredate的值：

SQL＞INSERT INTO emp（empno, ename, deptno, sal, hiredate）

VALUES（9999，'Hello'，30，1000，sysdate）；

利用INSERT语句中还可以从另一个表中复制数据，这时要在INSERT语句中使用子查询，对应的语法格式为：

INSERT INTO表1（列1，列2……）

SELECT列1，列2……FROM表2 WHERE条件表达式

这里的SELECT子句实际上是一个子查询。执行这样的语句时，首先执行SELECT子句，将返回的查询结果作为指定列的值，插入到表1中。用这种方法可以一次向表中插入多行，但是需要注意的是，表1的指定各列要与SELECT子句中的各列在排列顺序、数据类型和数量上保持一致。例如，假设有一个表emp1，它的结构与emp相同，现在希望从表emp中将部门10和部门20的员工数据复制到表emp1中，相应的SELECT语句为：

SQL＞INSERT INTO emp1（empno, ename, deptno, sal, hiredate）

SELECT empno, ename, deptno, sal, hiredate FROM emp

WHERE deptno=10 or deptno=20；

1.2.2　DELETE语句

DELETE语句用来从表中删除指定的行，它一次可以删除一行，也可以删除多行。DELETE语句的语法格式为：

DELETE FROM表名

WHERE条件

在默认情况下，DELETE语句可以不使用WHERE子句，这时将删除表中的所有行。例如，下面的DELETE语句将删除表emp中的所有行：

SQL＞DELETE FROM emp；

如果希望只删除表中的一部分数据，需要通过WHERE指定条件。例如，要从表emp删除中部门30的工资低于1000元的员工数据，相应的DELETE语句为：

SQL＞DELETE FROM emp WHERE deptno=30 AND sal＜1000；

在DELETE语句的WHERE子句也可以使用子查询，子查询与SELECT语句中的子查询用法相同。例如，要从表DEPT中删除这样的部门数据，它在表emp中没有所属的员工，即空部门，相应的DELETE语句为：

DELETE FROM dept

WHERE deptno NOT in（SELECT distinct deptno FROM emp）；

1.2.3　UPDATE语句

UPDATE语句的作用是对表中已经存在的数据进行修改。它可以一次修改一行，也可以修改多行。这条语句的语法格式为：

UPDATE表名

SET列1=表达式1，列2=表达式2……

WHERE条件

UPDATE语句通过SET子句为指定列指定新值，将列值修改为指定的表达式。在SET子句中指定所有需要修改的列。在默认情况下，UPDATE语句不需要WHERE子句，这时UPDATE语句将修改表中的所有行。例如，下面的UPDATE语句将把emp表中所有员工的工资增加10%，奖金增加100元：

SQL＞UPDATE emp

SET sal=sal*1.1，comm=nvl（comm，0）+100；

如果通过WHERE子句指定了条件，那么UPDATE语句只修改满足条件的行。例如，如果要为部门10和20中工资高于2000元的员工增加工资和奖金，增加幅度与上一条UPDATE语句相同。相应的UPDATE语句为：

SQL＞UPDATE emp

SET sal=sal*1.1，comm=nvl（comm，0）+100；

WHERE deptno=10 OR deptno=20 AND sal＞2000；

在UPDATE语句的WHERE子句中，也可以使用子查询。这时的条件并不是一个确定的条件，而是依赖于对另一个表的查询。例如，要对与员工BLAKE同在一个部门的员工增加工资和奖金，增加幅度与上一条UPDATE语句相同。相应的UPDATE语句为：

SQL＞UPDATE emp

SET sal=sal*1.1，comm=nvl（comm，0）+100

WHERE deptno=（SELECT deptno FROM emp WHERE ename='BLAKE'）；

1.3　事务控制语句

事务是对数据库操作的逻辑单位，在一个事务中可以包含一条或多条DML（数据操纵语言）、DDL（数据定义语言）和DCL（数据控制语言）语句，这些语句组成一个逻辑整体。事务的执行只有两种结果：要么全部执行，把数据库带入一个新的状态；要么全部不执行，对数据库不做任何修改。对事务的操作有两个：提交（COMMIT）和回滚（ROLLBACK）。提交事务时，对数据库所做的修改便永久写入数据库。回滚事务时，对数据库所做的修改全部撤销，数据库恢复到操作前的状态。事务可用于操作数据库的任何场合，包括应用程序、存储过程、触发器等。

我们拿现实生活中的一个例子来说明事务的概念。在两个银行账号之间转账时，首先从第一个账号中减去转账金额，然后在第二个账号中加上相等数量的金额，这两个操作必须作为一个整体来完成，不允许只进行第一个操作而不进行第二个操作，也不允许只进行第二个操作而不进行第一个操作。如果这两个操作都顺利完成，那么这个事务可以提交，这次转账成功。否则如果有一个操作失败，那么这个事务必须回滚，这次转账失败，两个账号的状态都恢复到操作以前的状态。在数据库系统中，类似这样可以看做一个整体的多个操作就是一个事务。

事务具有四个属性，这四个属性的英文单词首字母合在一起就是ACID。这四个属性是：

·原子性（Atomicity）：事务要么全部执行，要么全部不执行，不允许部分执行。

·一致性（Consistency）：事务把数据库从一个一致状态带入另一个一致状态。

·独立性（Isolation）：一个事务的执行不受其他事务的影响。

·持续性（Durability）：一旦事务提交，就永久有效，不受关机等情况的影响。

一个事务中可以包含多条DML语句，或者包含一条DDL语句，或者包含一条DCL语句。事务开始于第一条SQL语句，在下列之一情况下结束：

·遇到COMMIT或ROLLBACK命令。

·遇到一条DDL或者DCL命令。

·系统发生错误、退出或者崩溃。

总之，事务是一系列可以把系统带入一个新的状态的操作，如果事务被提交，则数据库进入一个新的状态，否则数据库恢复到事务以前的状态。在数据库中使用事务的好处是首先可以确保数据的一致性，其次在对数据做永久修改之前可以预览以前的数据改变，还可以将逻辑上相关的操作进行分组。

控制事务的方式有两种，一种是隐式控制，数据库管理系统根据实际情况决定提交事务还是回滚事务；另一种方式是显式控制，在事务的最后放置一条COMMIT或ROLLBACK命令，将事务提交或回滚。

如果是隐式控制，那么事务在遇到一条DDL命令，如CREATE，或者遇到一条DCL命令，如GRANT，或者从SQL*Plus正常退出，即使没有发出COMMIT或ROLLBACK命令，这个事务将被自动提交。如果从SQL*Plus非正常退出或发生系统崩溃，那么系统将自动回滚事务。

如果是显式控制，那么在事务的最后就要通过COMMIT命令提交事务，或者通过一条ROLLBACK命令回滚事务。

如果事务被提交，那么对数据库所作的修改将写入数据库。如果回滚事务，一般情况下将回滚到事务的开始，即对数据库不做任何修改。在Oracle中，允许部分回滚事务，即可以将事务有选择地回滚到中间的某个点。部分回滚是通过设置保存点（SAVEPOINT）来实现的。在事务中可以通过SAVEPOINT命令设置若干个保存点，这样可以将事务有选择地回滚到某一个保存点。图1.1表示对事务的提交、回滚和保存点操作。

图　1.1　事务控制

在图1.1所示的事务中，有一条INSERT语句，一条UPDATE语句和一条DELETE语句，并设置了两个保存点。如果在事务的最后执行了COMMIT命令，则这三条语句都将对数据库产生影响。如果在事务的最后执行了ROLLBACK命令，则事务回退到事务的开始，这三条语句都数据库不产生任何影响。如果执行了命令ROLLBACK TO B，则事务回退到保存点B，这时仅有INSERT和UPDATE语句对数据库产生影响，DELETE语句的执行结果被撤销。如果继续执行命令ROLLBACK TO A，则事务回退到保存点A，这时UPDATE和DELETE语句的执行结果被撤销。如果继续执行COMMIT命令，则只有INSERT语句的执行结果被写入数据库。

用户访问数据库时，数据库中的数据是放在缓冲区中的，当前用户可以通过查询操作，浏览对数据操作的结果。如果没有提交事务，其他用户是看不到事务的修改结果的。当一个用户修改表中的数据时，将对被修改的数据加锁，其他用户无法在此期间对该行数据进行修改，直到这个用户提交或回滚这个事务。

如果在事务的最后执行了COMMIT命令，则对数据的修改将被写入数据库，以前的数据将永久丢失，无法恢复，其他用户都可以浏览修改后的结果，在数据上加的锁被释放，其他用户可以对数据执行新的修改，在事务中设置的所有保存点将被删除。

下面的语句序列演示了在SQL*Plus中执行一条DML语句，然后执行COMMIT命令的情况，并显示了执行的结果。

SQL＞INSERT INTO dept VALUES（60，'HHHHH'，'HHHHHH'）；

已创建1行。

SQL＞COMMIT；

提交完成。

如果在事务的最后执行了ROLLBACK命令，那么所有未提交的修改将被丢弃，对数据所做的修改将被取消，数据恢复到修改以前的状态，在行上加的锁被释放，其他用户可以对这样的数据进行新的修改。

下面的语句序列演示了在SQL*Plus中执行一条DML语句，然后执行ROLLBACK命令的情况，并显示了执行的结果。

SQL＞DELETE FROM dept WHERE deptno=60；

已删除1行。

SQL＞ROLLBACK；

回退已完成。

如果在事务中设置了保存点，并且在事务的最后执行ROLLBACK命令回滚到某个保存点，那么在此保存点之后的DML语句所做的修改将被丢弃，但是在此保存点之前的DML语句所做的修改仍然没有写入数据库，还可以进行提交或回滚。

下面的语句序列是在SQL*Plus中执行的两条DML语句，以及在两条DML语句之间设置的保存点，然后是回滚到这个保存点，最后提交这个事务的情况。

SQL＞DELETE FROM dept WHERE deptno=60；

已删除1行。

SQL＞SAVEPOINT a；

保存点已创建。

SQL＞DELETE FROM dept WHERE deptno=40；

已删除1行。

SQL＞ROLLBACK TO a；

回退已完成。

SQL＞COMMIT；

提交完成。

1.4　DDL语句

DDL是“数据定义语言”（Data Defination Language）的缩写，它包含CREATE、ALTER、DROP、RENAME、TRUNCATE等命令，用来对数据库对象进行创建、修改、删除、重命名等操作。其中CREATE、ALTER和DROP命令的功能十分强大，几乎可以对所有的数据库对象进行管理，例如表、视图、索引、存储程序等。在本节中仅介绍与表的操作有关的DDL命令，与其他数据库对象有关的DDL命令在以后的章节中将陆续介绍。

1.4.1　表的创建

CREATE命令可用来创建表。创建表时要确定表的结构，即确定表中各列的名字和类型。在关系数据库中，表被看做是一个关系，表中的每个列是关系中的属性，是一个不可再分割的基本单位。表中的行对应关系中的一个元组。表的结构确定下来以后，就可以通过INSERT语句向表中插入数据了。用来创建表的CREATE命令格式为：

CREATE TABLE表名（

列1数据类型，

列2数据类型，

……

）；

如果已经存在同名的表，则应该先删除原来的表，然后再创建。表名是代表这个数据库对象的名称，对表名的要求是必须以字母开头，长度为1～30个字符，而且只能包含A～Z、a～z、0～9、_、$和#等字符，不能使用Oracle的保留字，在同一个用户模式中不能拥有两个同名的表。

表中的每个列都有一个名字，在同一个表中不能有同名的列。列的数据类型可以是系统预定义的类型，也可以是用户自定义类型。表1.7列出了系统预定义的数据类型。

在创建表时，可以通过DEFAULT关键字为列指定一个默认值，这样当用INSERT语句插入一行时，如果没有为该列指定值，就以默认值填充，而不是插入空值。

例如，创建一个学生表，表中包括学号、姓名、性别、出生日期、所在学校等列，可以用下列的CREATE语句：

SQL＞CREATE TABLE student（

sno number（8），

sname char（8），

birthday date，

school varchar（40））；

如果要验证表的结构是否与期望的结果一致，可以在表创建之后通过DESC命令查看表的结构。这个命令只能列出表中各列的列名、数据类型以及是否为空等属性。

在创建表时，还可以以另一个表为模板确定当前表的结构。一般情况下，可以从一个表复制它的结构，从而快速创建一个表。复制表的结构是通过子查询来实现的，即在CREATE语句中可以嵌套SELECT语句。这时的CREATE语句格式为：

CREATE TABLE表名

AS SELECT语句

CREATE语句将根据SELECT子句中指定的列，确定当前表的结构，然后将子查询返回的数据插入到当前表中，这样在创建表的同时向表中插入了若干行。例如，现在要根据表emp的结构创建表emp_1，仅复制表emp中的empno、deptno和sal三个列，同时复制部门30的数据。相应的CREATE语句为：

SQL＞CREATE TABLE emp_1

AS SELECT empno, deptno, sal

FROM emp

WHERE deptno=30；

一般情况下，在通过这种方式创建的表中，列名和列的定义与原来的表一致。如果希望在创建一个新表时指定与原来的表不同的列名，可以在CREATE语句中的表名之后指定新的列名。如果只希望拷贝表的结构，而不复制表中的数据，可以将SELECT子句中的条件指定为一个永远为假的条件。例如，现在希望根据表emp创建表emp_2，为复制的三个列指定新的列名，并且不复制表emp中的数据，相应的CREATE语句为：

SQL＞CREATE TABLE emp_2（empno_2，deptno_2，sal_2）

AS SELECT empno, deptno, sal

FROM emp

WHERE 1＜0；

实际上，创建表的语句是非常复杂的，在表上可以定义约束，可以指定存储参数等属性。

在这里，顺便向读者介绍两个概念，一个是模式（Schema），一个是数据字典（Data dictionary）。模式指的是一个用户所拥有的所有数据库对象的逻辑集合。在创建一个新用户时，同时创建了一个同名的模式，这个用户创建的所有数据库对象都位于这个模式中。用户在自己的模式中创建表，需要具有CREATE TABLE系统权限，如果需要在别人的模式中创建表，则需要具有CREATE ANY TABLE权限。在访问其他用户的数据库对象时，要指定对方的模式名称，例如，通过SCOTT.EMP引用SCOTT用户的emp表。

数据字典是一些视图，从这些视图中可以查看一些重要的系统系统数据，如数据库中的表、索引、权限、表空间等信息。这些视图是在创建数据库时自动创建的，它们的内容也是由数据库服务器自动维护的。用户可以查看以下几种形式的数据字典视图：

·以USER_开始的视图；

·以ALL_开始的视图；

·以DBA_开始的视图。

从USER_视图中可以查看当前用户所创建的某类数据库对象，如在USER_TABLES中可以查看当前用户所创建的任何一个表的信息。从ALL_视图中可以查看当前用户有权限访问的某类数据库对象，如在ALL_TABLES中可以查看当前用户有权限的任何一个表的信息。在DBA_视图中可以查看当前数据库中所有的某类对象的信息，如在DBA_TABLES中可以查看当前数据库中所有的表。DBA_视图只能由数据库管理员（一般是SYS用户）查看。

1.4.2　如何修改表的结构

表在创建以后，如果在使用的过程中发现表的结构不合理，可以通过执行ALTER命令修改它的结构。修改表结构的操作包括增加列、修改某个列的定义、删除列、增加约束、修改约束、表的重命名等。

ALTER命令可以使用若干个子句，通过这些子句可以完成修改表结构的操作。可以使用的子句包括ADD、DROP、MODIFY和RENAME等。

如果要在表中增加一个列，通过ADD子句指定一个列的定义，至少要包括列名和列的数据类型。增加列的语法结构为：

ALTER TABLE表名

ADD（列1，数据类型，

列2，数据类型，

……）；

例如，要在student表中增加两个列，一个是性别，另一个是家庭地址，对应的ALTER语句为：

SQL＞ALTER TABLE student

ADD（gender char（2）DEFAULT'男'，

address varchar2（50））；

需要注意的是，如果一个表中已经有数据，这时增加一个列时，不能将该列约束为“非空（NOT NULL）”，因为我们不能一方面要求该列必须有数据，而另一方面又无法在增加列的同时向该列插入数据。例如，要向dept表中增加一个非空的列，语句的执行将出错：

ALTER TABLE dept

ADD（alias char（20）NOT NULL）

这条语句执行的结果为：

ALTER TABLE dept

*

ERROR位于第1行：

ORA-01758：要添加法定（NOT NULL）列，则表必须为空

也就是说，要往一个表中增加一个非空列时，这个表必须是空的。如果表中已经有数据，单独使用“NOT NULL”约束是不行的。一种好的解决办法是为该列指定默认值，这样在增加一个非空列的同时，为这个列填充了指定的默认值。

例如，上面的ALTER语句如果修改为下面的形式，就可以保证这个列能够顺利加入到表中：

ALTER TABLE dept

ADD（alias char（20）default'NOT KNOWN'NOT NULL）；

利用ALTER语句还可以从表中删除一个列。用来完成这个操作的子句是DROP。用于删除列的ALTER语句格式为：

ALTER TABLE表名DROP COLUMN列名

例如，要删除表student中的gender列，对应的ALTER语句为：

ALTER TABLE student DROP COLUMN gender；

删除一个列时，这个列将从表的结构中消失，这个列的所有数据也将从表中被删除。原则上可以删除任何列，但是一个列如果作为表的主键，而且另一个表已经通过外键在两个表之间建立了关联关系，这样的列是不能被删除的。例如，如果要删除表dept中的列deptno，系统将会发生错误：

ALTER TABLE dept DROP COLUMN deptno

*

ERROR位于第1行：

ORA-12992：无法删除父项关键字列

如果要修改表中一个列的定义，可以使用ALTER语句的MODIFY子句。通过MODIFY子句可以修改列的长度、非空等属性。使用了MODIFY子句的ALTER语句格式为：

ALTER TABLE表名

MODIFY（列1，新数据类型非空属性，

列2，新数据类型非空属性，

……）；

例如，要将表student中的gender列由原来的char（2）改为char（4），非空，将address列由原来的varchar2（50）改为char（40），相应的ALTER语句为：

SQL＞ALTER TABLE student

MODIFY（gender char（4）NOT NULL，

address char（40））；

需要注意的是，如果表中目前没有数据，那么可以将一个列的长度增加或减小，也可以将一个列指定为非空。如果表中已经有数据，那么只能增加列的长度，如果该列有空值，不能将该列指定为非空。

通过ALTER语句，还可以为表进行重命名，也就是将表的名字改为另一个名字。为表重命名的ALTER语句格式为：

ALTER TABLE表名RENAME TO新表名

例如，要将表student重命名为stu，相应的ALTER语句为：

SQL＞ALTER TABLE student RENAME TO stu；

1.4.3　其他DDL命令

除了CREATE和ALTER两条主要的命令外，DDL还包括DROP、RENAME和TRUNCATE等几条命令。其中DROP命令的功能是删除一个对象，通过这条命令几乎可以删除任何类型的数据库对象。用来删除表的DROP命令的格式为：

DROP TABLE表名

例如，要删除表stu，相应的DROP语句为：

SQL＞DROP TABLE stu；

数据库对象删除后，它的有关信息就从相关的数据字典中删除。

RENAME命令的作用是对数据库对象重新命名。对表进行重新命名的命令格式为：

RENAME表名TO新表名；

例如，要将表STU重新命名为STU_1，相应的RENAME语句为：

SQL＞RENAME stu TO stu_1；

表被重新命名以后，它的信息就会在相关的数据字典中更新。

TRUNCATE命令的作用是删除表中的数据。与DELETE语句不同的是，TRUNCATE命令将删除表中的所有数据，不需要指定任何条件，而且数据被删除后无法再恢复。这条命令的语法格式为：

TRUNCATE TABLE表名；

例如，要删除表student中的所有数据，可以执行下面的语句：

SQL＞TRUNCATE TABLE student；

TRUNCATE命令作用的结果是删除所有的数据，而且不可恢复，所以这条命令要慎用。从执行结果来看，一条TRUNCATE语句相当于下列两条语句的组合：

DELETE FROM表名；

COMMIT；

1.5　约束

约束是加在表上的一种强制性的规则，是保证数据完整性的一种重要手段。当向表中插入数据，或修改表中的数据时，必须满足约束所规定的条件。例如，员工的性别必须是“男”或“女”，部门号只能是已经存在的部门的编号等。在设计表的结构时，应该充分考虑在表上需要施加的约束。

保证数据完整性的方法大致有三种，即应用程序代码、触发器和约束。其中约束是一种更为灵活的方式，它不仅维护更加方便，而且性能也比较高，是保证数据完整性的最佳选择。

约束可以在创建表时指定，也可以在表创建之后再指定。如果在表创建之后再指定约束，可能会因为表中已经存在一些数据不满足这个条件而使得约束无法施加。

1.5.1　约束的类型

在表上可以施加以下五种约束：

·NOT NULL　非空约束

·UNIQUE　唯一性约束

·PRIMARY KEY　主键约束

·FOREIFN KEY　外键约束

·CHECK　检查约束

NOT NULL约束规定一个列上的值不能为空。当使用INSERT语句向表中插入一行数据，或者使用UPDATE语句修改一行数据时，必须为该列指定值，不能使其为空。例如，在表dept中，在列deptno上施加了NOT NULL约束，其余各列都没有，所以在向这个表中插入数据时，必须为这个列指定数据。向这个表中插入数据的最简INSERT语句为：

SQL＞INSERT INTO dept（deptno）VALUES（70）；

UNIQUE约束规定一个列上的数据必须唯一，不能有重复值，但是允许为空值。例如在表dept中，可以在部门名称dname列上施加UNIQUE约束，这样可以保证部门的名称不会重复。当在某个列上指定了UNIQUE约束时，在该列上将自动生成一个唯一性索引。

PRIMARY KEY约束是主键约束。主键用来唯一地标识表中的一行数据，它规定在主键列上的数据不能重复，并且不能为空。每个设计合理的表都应该有一个主键。主键可以是一个列，也可以是多个列的组合。如果在某个列上指定了主键约束，那么就不需要在该列上再指定NOT NULL约束和UNIQUE约束了。

在一个表上只能创建一个主键。当创建主键时，在主键列上将自动建立一个唯一性索引，索引的名字与约束的名字相同。

FOREIFN KEY为外键约束。外键用来与另一个表建立关联关系。两个表之间的关联关系是通过主键和外键来维持的。外键规定该列中的数据必须是另一个与之关联的表中的主键列中的数据。外键可以是一个列，也可以是多个列的组合。在一个表中只能有一个主键，但是可以有多个外键。例如，在表dept中deptno列是主键列，在表emp中，empno列是主键列，deptno列是外键列，这个外键规定deptno列的数据必须是表dept中deptno列中的数据。假设要往表emp中插入一行数据，其中deptno列的值为100，而在表dept的deptno列中根本就不存在这样的数据，所以这行数据就违反了外键约束。

检查约束是一个关系表达式，它规定了一个列必须满足的条件。例如员工的性别只能是“男”或“女”，员工的工资必须在1000～8000等。当向表中插入一行，或者修改某一行时，都要检查指定列的值是否满足这个条件，如果满足，这个操作才能成功执行。

1.5.2　如何在创建表时指定约束

约束可以在创建表的同时指定，也可以在表创建之后再指定。如果与表同时创建，那么在创建表的CREATE语句中通过CONSTRAINT关键字指定约束的名称和约束类型。同时创建表和约束的CREATE语句格式为：

CREATE TABLE表名（

列1　数据类型　CONSTRAINT　约束名1　约束类型，

列2　数据类型　CONSTRAINT　约束名2　约束类型，

……

）；

其中约束名是为约束指定的唯一的名称。约束名可以由用户自己指定，也可以自动产生。如果省略关键字CONSTRAINT和约束名称，那么约束名称将自动产生。如果约束名称是自动产生的，那么根据这样的名称无法判断约束所在的表以及约束类型。如果用户自己指定约束名称，则可以在名称中包含表名、约束类型等有用信息。

如果在一个列的定义之后指定该列上的约束时，这种定义约束的方法称为列级约束。例如，下面的CREATE语句用来创建一个表，名为student，在各个列上都指定了约束。

SQL＞CREATE TABLE student（

sno number（8）PRIMARY KEY，

sname char（8）NOT NULL，

gender char（2）CHECK（gender in（'男'，'女'）），

birthday date，

school varchar（40））；

在创建表student时指定了三个约束，第一个约束指定学号sno列为主键，第二个约束指定姓名sname列不为空，第三个约束指定性别gender列的值只能是“男”或“女”，这些约束都是列级约束。在创建表时没有为这几个约束指定名字，系统将自动为它们指定各自的名字。下面的CREATE语句在创建表时指定约束，并为每个约束指定了名字。

SQL＞CREATE TABLE student（

sno number（8）CONSTRAINT pk_sno PRIMARY KEY，

sname char（8）CONSTRAINT nn_sname NOT NULL，

gender char（2）CONSTRAINT gen_check CHECK（gender in（'男'，'女'）），

birthday date，

school varchar（40））；

约束可以在每个列的定义之后分别指定，也可以在所有列的定义之后一起指定。如果一个约束在表定义的最后才指定，这样的约束定义方法称为表级约束。如果一个约束涉及多个列的组合，那么就不能在每个列之后指定约束，而只能定义为表级约束。例如，若表student的主键列不是sno列，而是sno列和sname列的组合，这样的约束就不能在sno列或sname列之后指定，而只能在所有列的定义之后再指定。定义表级约束的CREATE语句格式为：

CREATE TABLE　表名（

列1　数据类型，

列2　数据类型，

……

CONSTRAINT　约束名1　约束类型（列名），

CONSTRAINT　约束名2　约束类型（列名），

……）；

在五种约束中，NOT NULL约束只能以列级约束的形式定义，其余四种既可以以列级约束的形式定义，也可以以表级约束的形式定义。因为表级约束是在所有列之后定义的，而不是在某个具体的列之后定义，所以在表级约束中要指定相关的列名。

例如，上面的创建表的CREATE语句也可以改为下面的形式：

CREATE TABLE student（

sno number（8），

sname char（8）CONSTRAINT nn_sname NOT NULL，

gender char（2），

birthday date，

school varchar（40），

CONSTRAINT pk_sno PRIMARY KEY（sno），

CONSTRAINT gen_check CHECK（gender in（'男'，'女'））

）；

外键约束的定义形式比较复杂，因为外键要与另一个表的主键进行关联，所以不仅要指定约束的类型和有关的列，还要指定与哪个表的哪个列进行关联。

如果在列级定义外键约束，定义的格式为：

CONSTRAINT约束名REFERENCES表名（列名）

其中约束名是为这个外键约束起的名字。FOREIGN KEY为约束类型，即外键约束。REFERENCES关键字指定与哪个表的哪个列进行关联。例如，在表emp中，外键列为deptno，它与表dept的deptno列进行关联。这个外键的定义语句为：

CONSTRAINT fk_deptno REFERENCES dept（deptno）

这条代码放置在emp表的deptno列定义之后。如果要在表级定义外键约束，那么外键的定义代码放置在所有列的定义之后，它的格式为：

CONSTRAINT约束名FOREIGN KEY（外键列）REFERENCES表名（列名）

例如，在表emp中的deptno列上施加的外键约束也可以通过下面的形式定义：

CONSTRAINT fk_deptno FOREIGN KEY（deptno）REFERENCES dept（deptno）

约束作为一种附加在表上的数据库对象，它的信息也被记录在数据字典中。与约束有关的数据字典有两个，一个是user_constraints；另一个是user_cons_columns。其中在数据字典user_constraints中记录当前用户所拥有的约束的信息，如约束名、约束类型、约束所在的表、约束的状态等。如果是外键，还记录了与之关联的主键名称。例如，下面的SELECT语句用来查询表student上的约束信息：

SQL＞SELECT constraint_name AS名称，constraint_type AS约束类型，status AS状态

FROM user_constraints

WHERE table_name='STUDENT'；

这条SELECT语句的执行结果为：

约束名　约束类型　状态

NN_SNAME C ENABLED

GEN_CHECK C ENABLED

PK_SNO P ENABLED

如果要进一步查询约束施加在哪个列上，就需要查询另一个数据字典了，它就是USER_CONS_COLUMNS。例如，下面的SELECT语句查询表student中的各个约束是在哪个列上定义的：

SQL＞SELECT constraint_name, table_name, column_name

FROM user_cons_columns WHERE table_name='STUDENT'；

这条SELECT语句的执行结果为：

CONSTRAINT_NAME TABLE_NAME COLUMN_NAME

NN_SNAME STUDENT SNAME

PK_SNO STUDENT SNO

GEN_CHECK STUDENT GENDER

1.5.3　如何在创建表之后指定约束

约束既可以在创建表的同时定义，也可以在表创建之后再添加。不过在表创建之后再添加约束可能会带来这样的问题，如果表中已经有数据，而这样的数据不满足将要添加的约束，那么约束是无法添加的。例如要为表的某个列指定NOT NULL约束，但是这个表中这个列本来就有很多空值，这种情况导致这个约束无法添加。因此最好的做法是在创建表之前充分考虑需要什么样的约束，在创建表的同时定义约束。

添加约束实际上也是对表结构的修改，因此添加约束也是通过执行ALTER语句完成的。因为表的结构已经确定，所以我们无法采用列级约束的形式，在某个列名之后指定约束，而只能采用表级约束的形式。添加约束的ALTER语句格式为：

ALTER TABLE表名ADD（CONSTRAINT约束名约束类型（列名））

其中CONSTRAINT关键字和约束名是可省的，如果没有为约束指定名称，那么名称将自动产生。如果要添加多个约束，在ADD子句的括号中指定多个用逗号分隔的约束就可以了。现在假设表student上没有任何约束，我们为这个表添加几个约束，具体的语句为：

SQL＞ALTER TABLE student

ADD（CONSTRAINT pk_sno PRIMARY KEY（sno），

CONSTRAINT gen_check CHECK（gender in（'男'，'女'）））；

除NOT NULL约束以外，其余四种约束都可以通过ADD子句添加。NOT NULL约束约束比较特殊，只能通过ALTER命令的MODIFY子句来添加。添加NOT NULL约束的语法格式为：

ALTER TABLE表名MODIFY（列名CONSTRAINT约束名NOT NULL）；

同样，如果要为多个列施加NOT NULL约束，那么在MODIFY子句之后的括号中指定多个约束项，相互之间用逗号分隔开即可。假设表student中的sname列上没有NOT NULL约束，可以通过下面的ALTER语句在该列上增加NOT NULL约束：

SQL＞ALTER TABLE student MODIFY（sname CONSTRAINT nn_sname NOT NULL）；

1.5.4　约束的维护

如果希望去掉表上的某个约束，可以将其删除，也可以使其无效。约束是不能被修改的，如果在表上已经建立了一个约束，现在希望把它改为另一类型的约束，或者希望把它施加在另一个列上，只能先将这个约束删除，然后重新创建。

删除约束是通过执行ALTER命令的DROP子句来完成的。删除约束的ALTER命令的语法格式为：

ALTER TABLE表名DROP CONSTRAINT约束名；

例如，要删除表student上的约束gen_check，可以执行下面的ALTER命令：

ALTER TABLE student DROP CONSTRAINT gen_check；

如果要删除一个主键约束，首先要考虑这个主键列是否已经被另一个表的外键列关联，如果没有关联，那么这个主键约束可以被直接删除，否则不能直接删除。例如，在表dept中，在列deptno上定义了主键，在表emp中的deptno上定义外键约束，两个表之间通过主键和外间建立了关联，那么主键约束是不能被直接删除的。要删除主键约束，必须使用CASCADE关键字，连同与之关联的外键约束一起删除。删除主键的ALTER命令语法格式为：

ALTER TABLE表名DROP CONSTRAINT主键约束名CASCADE；

例如，要删除表dept上的主键约束pk_dept，可以执行下面的ALTER语句：

SQL＞ALTER TABLE dept DROP CONSTRAINT pk_dept CASCADE；

约束被删除后，加在表上的限制条件被取消，在此之后写入的数据或修改的数据再也不用考虑这些限制条件。例如，将表student上的约束gen_check删除后，员工的性别可以是‘男’和‘女’以外的其他数据。

在表中建立主键约束或UNIQUE约束时，在相关的列上将自动建立唯一性索引。当从表中删除主键约束或UNIQUE约束时，与它们相关的索引也被一起删除。

如果一个表被删除了，那么依附于它的约束也就没有意义了，这个表上的约束也将被一起删除。

如果希望一个约束暂时不起作用，可以使其无效。使约束无效的操作是通过ALTER命令的DISABLE子句实现的。使约束无效的ALTER命令格式为：

ALTER TABLE表名DISABLE CONSTRAINT约束名；

例如，要使表student上的约束nn_sname无效，相应的语句为：

SQL＞ALTER TABLE student DISABLE CONSTRAINT nn_sname；

一个约束无效后，它的状态就变为DISABLED，这时通过查询数据字典，可以了解约束的当前状态。例如，下面的SELECT语句查询表STUDENT上的约束及其状态：

SQL＞SELECT constraint_name AS约束名，constraint_type AS约束类型，status AS状态

FROM user_constraints

WHERE table_name='STUDENT'；

这条SELECT语句的执行结果为：

约束名　约束类型　状态

NN_SNAME C DISABLED

GEN_CHECK C ENABLED

PK_SNO P ENABLED

当一个约束无效后，这个约束并没有从数据库中被删除，只是暂时不起作用。这时要向表中插入数据或修改已有数据时，就不必满足这个约束条件了。如果希望一个约束重新有效，可以执行带ENABLE子句的ALTER命令。这时ALTER命令的格式为：

ALTER TABLE表名ENABLE CONSTRAINT约束名；

例如，要使刚才已经无效的约束nn_sname重新有效，可以执行下面的ALTER语句：

ALTER TABLE student ENABLE CONSTRAINT nn_sname；

约束重新有效后，它在数据字典中的状态就变为ENABLED。需要注意的是，在一个约束无效到重新有效的这段时间内，用户可能向表中插入了一些数据，或者修改了一些数据，这些数据可能恰好不满足这个约束条件，这样在执行上面的ALTER语句时就会出错，约束将无法重新有效，出错信息类似于这样：“无法验证（SCOTT.NN_SNAME）-违反检查约束条件”。

1.6　视图

视图是一种非常重要的数据库对象，它的形式类似于普通表，我们可以从视图中查询数据。实际上它是建立在表上的一种虚表，在视图中并不存储真正的数据，而是仅仅保存一条SELECT语句，对视图的访问将被转化为对表的访问。视图所基于的表称为基表，而视图可以认为是对基表的一种查询操作。

使用视图的主要目的是为了方便用户访问基表，以及保证用户对基表的安全访问。

对用户而言，往往要对一个表进行大量的查询操作，如果查询操作比较复杂，并且需要频繁地进行，那么可以为这个查询定义一个视图。假设用户需要经常执行下面的查询：

SELECT dname FROM dept

WHERE deptno=（SELECT deptno FROM emp a

GROUP BY deptno

HAVING avg（sal）＞all（SELECT avg（sal）FROM emp

WHERE deptno！=a.deptno GROUP BY deptno））

如果为这个查询定义一个视图，那么用户只要执行一条简单的SELECT语句，对这个视图进行查询，那么实际的操作就是对基表dept执行了上面的查询。

需要注意的是，在视图中并不保存对基表的查询结果，而仅仅保存一条SELECT语句。只有当访问视图时，数据库服务器才去执行视图中的SELECT语句，从基表中查询数据。虽然我们对视图没有做过任何修改，但是对视图的多次访问可能得到不同的结果，因为基表中的数据可能随时被修改。所以视图中并不存储静态的数据，而是从基表中动态查询的。

从另外一个角度来看，视图可以保证对基表的安全访问。在设计表时，我们一般是从整体的角度来考虑表的结构的，而不是从每个用户的角度来确定表结构以及定义允许的操作。对于同一个表，不同的用户可以进行不同的操作，可以访问不同的数据。这样我们就可以为不同的用户定义不同的视图，从而保证用户只能进行允许的操作，访问特定的数据。

例如，对于员工表emp，公司经理可以浏览所有的数据，但是不能修改数据；人事部门可以查看和修改员工的职务、部门等信息，也可以增加一个新员工；财务部门可以查看、修改员工的工资和奖金；而对于普通员工，只能查看其他员工的部门和职务等信息。如果为每一类用户分别定义一个视图，就可以保证他们对同样的数据进行不同的访问。

1.6.1　视图的创建、修改和删除

用户可以在自己的模式中创建视图，只要具有CREATE VIEW这个系统权限即可。如果希望在其他用户的模式中创建视图，则需要具有CREATE ANY VIEW系统权限。如果一个视图的基表是其他用户模式中的对象，那么当前用户需要具有对这个基表的SELECT权限。

创建视图的命令是CREATE VIEW，这条命令的格式为：

CREATE VIEW视图名

AS SELECT语句

WITH READ ONLY

WITH CHECK OPTION；

其中最后两个选项是可选的，其中“WITH READ ONLY”限定对视图只能进行查询操作，不能进行DML操作。“WITH CHECK OPTION”限定DML操作必须满足一定的条件。

例如，下面的语句创建视图view_1，它所代表的操作是查询员工表中部门30的员工姓名、工资和奖金。

SQL＞CREATE VIEW view_1 AS

SELECT ename, sal, comm FROM emp

WHERE deptno=30；

视图view_2所代表的操作是查询部门20和30中工资大于2000元的员工姓名、工资和奖金。创建这个视图的CREATE语句为：

SQL＞CREATE VIEW view_2 AS

SELECT ename, sal, comm FROM emp

WHERE（deptno=30 or deptno=20）and sal＞2000

视图被创建之后，可以通过DESC命令查看视图的结构。查看视图结构的方法与查看表的方法相同，查看的结果是列出视图中各列的定义。

视图的结构是在执行CREATE VIEW语句创建视图时确定的，在默认情况下，列的名称与SELECT之后基表的列名相同，数据类型和是否为空也继承了基表中的相应列。如果希望视图中的各列使用不同的名字，那么在创建视图时，在视图的名称之后应该指定各列的名称。例如，下面的语句重新创建视图view_1，并为这个视图指定了不同的名称。

CREATE VIEW view_1（name, salary, comm1）AS

SELECT ename, sal, comm FROM emp

WHERE deptno=30；

如果执行DESC命令查看视图view_1的结构，我们将发现视图中各列的名称就是在CREATE VIEW语句中指定的名称，而数据类型和是否为空继承了基表中的对应列。下面是执行DESC命令查看视图view_1结构的结果：

SQL＞DESC view_1；

名称　是否为空?类型

NAME NULL VARCHAR2（10）

SALARY NULL NUMBER（7，2）

COMM1 NULL NUMBER（7，2）

视图作为一种数据库对象，它的相关信息被存储在数据字典中。与当前用户的视图有关的数据字典是USER_VIEWS，查询这个数据字典，可以获得当前用户的视图的相关信息。例如，需要查询视图VIEW_2中的相关信息，可以执行下面的SELECT语句：

SELECT text FROM user_views WHERE view_name='VIEW_2'；

在列TEXT中存储的是创建视图时使用的SELECT语句。另外，在数据字典ALL_VIEWS存储的是当前用户可以访问的所有视图的信息，在数据字典DBA_VIEWS存储的是系统中的所有视图的信息，这个数据字典只有DBA可以访问。

如果发现视图的定义不合适，可以对其进行修改。实际上视图中的SELECT语句是不能直接修改的，所以修改视图的一种方法是先删除视图，再重新创建，另一种方法是在创建视图的CREATE语句中使用OR REPLACE选项。带OR REPLACE选项的CREATE语句格式为：

CREATE OR REPLACE VIEW视图名

AS SELECT语句

WITH READ ONLY

WITH CHECK OPTION；

这样在创建视图时，如果视图不存在，则创建它。如果已经存在一个同名的视图，那么先删除这个视图，然后再根据SELECT语句创建新视图，用这个新视图代替原来的视图。

视图在不需要时，可以将其从数据库中删除。删除视图的命令是DROP VIEW。用户可以直接删除自己创建的视图，如果希望删除其他用户创建的视图，则需要具有DROP ANY VIEW这个系统权限。DROP VIEW命令的格式为：

DROP VIEW视图名；

例如，要删除视图view_1，可以执行下面的语句：

DROP VIEW view_1；

视图被删除后，相关的信息也被从数据字典中删除。

1.6.2　如何对视图进行访问

对视图的访问包括查询和受限制的DML操作。访问视图的方法与访问表的方法基本相同。例如，要查询视图view_1，可以执行下面的SELECT语句：

SELECT*FROM view_1；

如果要向视图view_1中插入一行，可以执行下面的INSERT语句：

INSERT INTO view_1 VALUES（'MARY'，1000，200）

在访问视图时，这种访问被转化为对基表的访问，所以在视图上执行DML操作时，也要遵守基表上的约束。上述INSERT语句在执行时系统将会出错，错误信息是：ORA-01400：无法将NULL插入（“SCOTT”.“EMP”.“EMPNO”），发生错误的原因是这行数据违反了基表的主键约束。在这一行数据中只提供了姓名、工资和奖金三列的值，而主键列deptno没有对应的数据，所以就违反了基表上的主键约束。

现在我们来考察对视图的DML操作进行限制的情况的情况。假设我们以下面的语句创建了视图view_2：

CREATE OR REPLACE VIEW view_2 AS

SELECT ename, sal, comm FROM emp

WHERE（deptno=30 or deptno=20）AND sal＞2000；

对这个视图进行查询操作时，将得到一些结果：

SQL＞SELECT*FROM VIEW_2；

ENAME SAL COMM

JONES 2975

BLAKE 2850

AAAA 3000

如果再对视图view_2进行一次UPDATE操作，那么再次查询的结果将有所不同：

SQL＞UPDATE view_2 SET sal=sal-900；

已更新3行。

SQL＞SELECT*FROM VIEW_2；

ENAME SAL COMM

JONES 2075

AAAA 2100

查询的结果表明，执行UPDATE语句之后，有一行数据因为sal列的数据不满足创建视图时WHERE子句中指定的条件，从而在视图中消失了。为了防止这种情况，我们可以在创建视图时使用WITH CHECK OPTION，例如，下面的语句重新创建视图view_2：

CREATE OR REPLACE VIEW view_2 AS

SELECT ename, sal, comm FROM emp

WHERE（deptno=30 or deptno=20）AND sal＞2000

WITH CHECK OPTION；

那么现在再执行以前的UPDATE语句时，系统将会出错，错误信息为：

ORA-01402：视图WITH CHECK OPTION违反WHERE子句

这就相当于为视图增加了一个约束，当对视图进行DML操作时，数据必须满足WHERE子句中指定的条件。

1.6.3　复杂视图

以前在创建视图时，在CREATE VIEW语句的SELECT子句中只涉及一个表的操作，并且只是对基表中的列进行简单的查询，并没有出现多个基表，或者对基表中的列进行表达式运算或者函数运算的情况，这种视图被称为简单视图。对简单视图不仅可以进行查询操作，还可以进行DML操作。

复杂视图是这样的视图，视图中的列是从基表中的列经过表达式或函数运算而来，或者是对基表进行了DISTINCT查询，或者涉及多个表的操作。总而言之，如果在用CREATE VIEW语句创建视图时，在SELECT之后的列名中使用了表达式、函数，或者使用了DISTINCT关键字，或者对多个表进行了连接查询，这样的视图都是复杂视图。

创建复杂视图仍然是通过执行CREATE VIEW命令完成的，只不过因为在SELECT子句中使用了表达式或者函数，这样的运算式不能作为视图中的列名，所以在创建复杂视图时必须为每个列指定列名。例如，下面的语句创建视图view_3：

SQL＞CREATE OR REPLACE VIEW view_3（deptno, max_sal, min_sal, sum_sal）AS

SELECT deptno, max（sal），min（sal），sum（sal）

FROM emp

GROUP BY deptno；

视图view_3中的max_sal、min_sal和sum_sal三个列是通过对基表中的列SAL分别经过三个函数的运算而来的，所以必须为这三个列明确地指定列名。

下面的语句用来创建视图view_4，这个视图涉及两个表的操作，并且对其中的一个列进行了表达式运算。

SQL＞CREATE OR REPLACE VIEW view_4（empno, ename, dname, sal, comm）AS

SELECT empno, ename, dname, sal*1.1，comm

FROM emp, dept

WHERE emp.deptno=dept.deptno；

对复杂视图，允许的操作只有查询，大部分视图不允许DML操作。例如，如果通过下面的INSERT语句向视图view_4中插入一行，系统将出错。

SQL＞INSERT INTO view_4 VALUES（9999，'Kate'，10，2000，200）；

相应的错误信息为：

ORA-01776：无法通过连接视图修改多个基表

现在我们总结一下在什么样的视图上可以执行DML操作，在什么样的视图不允许执行DML操作。

对于简单视图，如果基表中的所有列都被包含在视图中，或者至少主键列和所有不允许为空的列都被包含在视图中，并且在创建视图的CREATE VIEW语句的SELECT语句中最多只使用了WHERE子句，对这样的视图是可以进行插入、删除、修改操作的。如果在创建这样的简单视图时使用了WITH CHECK OPTION选项，那么执行DML操作时要遵守一定的约束条件。

对于涉及多个基表的复杂视图，如果其中至少一个表的所有列都被包含在视图中，或者至少一个表的主键列和所有不允许为空的列都被包含在视图中，并且在创建视图的CREATE VIEW语句的SELECT语句中最多只使用了WHERE子句，这样的视图是允许进行插入、删除、修改操作的。对这样的视图进行DML操作时，只有其中一个表可以被修改，并且被修改的列只能映射到一个表中。

对于只涉及一个基表的复杂视图，如果视图中的列是对基表的列经过某种运算而来，包括表达式、AVG等函数，或者在创建视图的CREATE VIEW语句的SELECT语句中使用了DISTINCT关键字、GROUP子句，对这样的视图是不能进行DML操作的。

对于涉及多个表的视图，如果视图中的列没有包含其中一个表中的所有主键列和所有不为空的列，或者视图中的部分列是经过对基表中的列经过某种运算而来的，或者在创建视图时在SELECT子句中使用了DISTINCT关键字、GROUP子句，这样的视图也是不能进行DML操作的。

1.7　索引

查询是在表上进行的最频繁的访问。在查询数据时，很少有用户愿意查询表中的所有数据，除非要对整个表进行处理。一般情况下用户总是查询表中的一部分数据。在SELECT语句中，通常需要通过WHERE子句指定查询条件，以获得满足该条件的所有数据。如果能够在很小的范围内查询需要的数据，而不是在全表范围内查询，那么将减少很多不必要的磁盘I/O，查询的速度无疑会大大加快。提供这种快速查询的方法就是索引。

1.7.1　索引的基本概念

索引是一种建立在表上的数据库对象，它主要用于加快对表的查询操作。合理使用索引可以大大减少磁盘访问的次数，从而大大提高数据库的性能。

使用索引的主要目的是加快查询速度，另外，索引也可以作为唯一性约束。如果在表的一个列上建立了唯一性索引，那么系统将自动在这个列上建立唯一性约束，这样可以保证插入这个列的数据是唯一的。

索引究竟是怎样加快查询速度的呢？原来，索引是建立在表中的某个列或几个列上的，这样的列称为索引列。在创建索引时，数据库服务器将对索引列的数据进行排序，并将排序的结果存储在索引所占用的存储空间中。在查询数据时，数据库服务器首先在索引中查询，然后再到表中查询。因为索引中的数据事先进行了排序，所以只需要很少的查找次数就可以找到需要的数据。

例如，假设要执行下面的查询语句：

SQL＞SELECT empno, ename, sal FROM emp WHERE empno=7902；

假设在表emp中有1000行数据，如果没有创建索引，那么系统将不得不在全表范围内查询，查询的次数为1000。但是如果使用了索引进行查询，那么只需要很少的几次查询就可以找到编号为7902的员工的数据。

图1.2为索引和表之间的关系示意。

图　1.2　索引与表的关系

从上图可以看到，在索引中，不仅存储了索引列上的数据，而且还存储了一个ROWID的值。ROWID是表中的一个伪列，是数据库服务器自动添加的，表中的每一行数据都有一个ROWID值，它代表这一行的标识，即一行数据在存储空间的物理位置。在访问表中的数据时，都要根据这个伪列的值找到数据的实际存储位置，然后再进行访问。由于索引列上的数据已经进行了排序，在索引中很快就能找到这行数据，然后根据ROWID就能直接到表中找到这行数据了。

需要注意的是，表是独立于索引的，无论对在表上建立了多少索引，无论索引对表中的数据进行什么样的排序，表中的数据都不会有任何变化。

在查询一行数据时，首先在索引中查询该行的行标识，然后根据这个行标识找到表中的数据。因为索引中的数据是经过排序的，所以采用了折半查找法查找数据，以达到快速查找的目的。图1.3表示利用索引查找数据的过程。

图　1.3　索引的工作原理

利用折半查找法在索引中查找数据的过程类似于遍历一棵二叉树，首先与根节点比较，如果与查找的数据相同，则一次访问就完成查询。如果要查找的数据小于根节点，则在根节点的左子树中查找，否则在右子树中查找，这样查找的范围将缩小一半。按照这种方法，每次将查找范围缩小一半，然后在剩下的节点中继续查找，直到找到所需的数据。如果利用上述索引在表emp中查找员工7777，访问一次磁盘就可以得到结果，要查找员工7902，第三次就可以得到结果。

按照索引列的值是否允许重复，索引可以分为唯一性索引和非唯一性索引，其中唯一性索引可以保证索引列的值是唯一的。按照索引列中列的数目，索引可以分为单列索引和复合索引。按照索引列的数据的组织方式，索引可以分为B*树索引、位图索引、反向索引和基于函数的索引，这里仅介绍B*树索引的用法。

合理地使用索引固然可以大大提高数据库的查询性能，但是不合理的索引反而会降低数据库的性能，尤其是在进行DML操作时。在创建索引时，表中的数据将被排序，如果对表进行了DML操作，表中的数据发生了变化，这时索引中的数据也将被重新排序，如果在表上建立了多个索引，那么每个索引中的数据都要被重新进行排序。这种排序的开销是很大的，尤其是表非常大时。

索引是关系型数据库系统用来提高性能的有效方法之一，索引的使用可以减少磁盘访问的次数，从而大大提高了系统的性能。但是在设计索引时必须全面考虑在表上所进行的操作，如果在表上进行的主要操作是查询操作，那么可以考虑在表上建立索引，如果在表上要进行频繁的DML操作，那么索引反而会引起更多的系统开销。一般来说，创建索引要遵循以下原则：

·如果每次查询仅选择表中的少量行，应该建立索引。

·如果在表上需要进行频繁的DML操作，不要建立索引。

·尽量不要在有很多重复值的列上建立索引。

·不要在太小的表上建立索引。在一个小表中查询数据时，速度可能已经足够快，如果建立索引，对查询速度不仅没有多大帮助，反而需要一定的系统开销。

1.7.2　索引的创建、修改和删除

索引可以自动创建，也可以手工创建。如果在表的一个列或几个列上建立了主键约束或者唯一性约束，那么数据库服务器将自动在这些列上建立唯一性索引，这时索引的名字与约束的名字相同。

手工创建索引需要执行SQL命令，创建索引的命令是CREATE INDEX。一个用户可以在自己的模式中创建索引，只要这个用户具有CREATE INDEX这个系统权限。如果希望在其他用户的模式中创建索引，那么需要具有CREATE ANY INDEX这个系统权限。

CREATE INDEX命令的语法格式为：

CREATE INDEX索引名

ON表名（列1，列2……）；

例如，如果要在表emp的empno列上建立索引，可以执行以下SQL语句：

SQL＞CREATE INDEX idx_1

ON emp（empno）；

在这个索引中，索引列只有一个，这样的索引称为单列索引。如果要建立复合索引，则要指定多个列。例如，下面的语句在表emp上创建了一个复合索引：

SQL＞CREATE INDEX idx_2

ON emp（empno, deptno）；

在默认情况下，创建的索引是非唯一的，也就是说，在表中的索引列上允许存在重复值。如果要创建唯一性索引，那么需要使用关键字UNIQUE。例如：

SQL＞CREATE UNIQUE INDEX idx_3

ON emp（ename, deptno）；

这时在表emp的ename和deptno列上创建了唯一性索引inx_3，这样可以保证表emp的ename和deptno列组合没有重复值，也就是说，没有两个员工名字相同，并且在同一个部门。

在创建一个索引时，需要指定一个或多个列，那么到底指定哪些列呢？一个重要原则是选择经常用在WHERE子句中使用的列。例如，如果要经常根据列empno的值查询员工的数据，那么可以考虑将列empno作为索引的索引列。

如果在查询时要在WHERE子句中指定多个查询条件，那么可以在涉及的多个列上分别创建索引。例如，对于条件WHERE A=7788 AND B＞2000，可以在列A和列B上创建两个索引，但是在查询时也要涉及两个索引的查询。如果能够将两个索引合并为一个，那么查询的次数也会减少。如果在一个索引建立在两个或多个列上，这样的索引就是复合索引。复合索引主要用于多个条件的查询语句中。

一般情况下，在指定索引中的列时，要遵循以下原则：

·在WHERE子句中经常使用的列上创建索引。

·尽量不要在具有大量重复值的列上创建索引。

·具有唯一值的列是建立索引的最佳选择，但是究竟是否在这个列上建立索引，还要看是否对这个列经常进行查询。

·如果WHERE子句中的条件涉及多个列，可以考虑在这些列上创建一个复合索引。

正如前面所说，合理设计的索引将提高系统的性能，而不合理的索引反而会降低系统性能。所以，在数据库的运行过程中，要经常利用SQL Trace检查索引是否被使用，检查索引是否像期望的那样提高了数据库的性能。如果一个索引并没有被频繁地使用，或者一个索引对数据库性能的提高只有微小的帮助甚至没有帮助，这时可以考虑删除这个索引。

1.7.3　索引信息的查询

与索引有关的数据字典有两个：user_indexes和user_ind_columns。

例如，要查询索引的类型、所基于的表、是否唯一性索引，以及状态等信息，可以执行以下查询语句：

SQL＞SELECT index_type, table_name, uniqueness, status FROM user_indexes

WHERE index_name='IDX_1'；

下面的查询语句用来获得索引所基于的表和表上的列：

SQL＞SELECT table_name, column_name FROM user_ind_columns

WHERE index_name='IDX_1'；

1.8　序列

序列是一种数据库对象，用来自动产生一组唯一的序号。序列是一种共享式的对象，多个用户可以共同使用序列中的序号。一般将序列应用于表的主键列，这样当向表中插入数据时，主键列就使用了序列中的序号，从而保证主键列的值不会重复。用这种方法可以代替在应用程序中产生主键值的方法，可以获得更可靠的主键值。

1.8.1　序列的创建、修改和删除

默认情况下，用户可以在自己的模式中创建序列。如果希望在其他用户的模式中创建序列，则必须具有CREATE ANY SEQUENCE这个系统权限。创建序列的命令为CREATE SEQUENCE，它的完整语法格式为：

CREATE SEQUENCE序列名INCREMENT BY n

START WITH n

MAXVALUE n|NOMAXVALUE

MINVALUE n|NOMINVALUE

CYCLE|NOCYCLE

CACHE n|NOCACHE

在这个命令的语法格式中，除序列名以外，其余各选项都是可选的。各选项中的n是一个整数。其中START WITH选项指定序列中的序号从哪个数字开始，默认情况下从它的最小值开始。INCREMENT选项指定了序列中序号递增的幅度，也就是后一个序号比前一个序号大多少。序号可以递增，也可以递减，所以INCREMENT选项中的数字n可以是正整数，也可以是负整数。

MAXVALUE用来指定序列中序号的最大值。如果没有最大值，可用NOMAXVALUE选项代替这个选项。同样，MINVALUE用来指定序列中序号的最小值，序列中的最小值必须小于或等于它的开始值。

如果为序列指定了最大值，那么当序列中的序号被消耗完时，用户将无法从这个序列中取得序号。选项CYCLE使得序列中的序号可以循环使用。当用户正在使用序列中的最大值时，下一个可以使用的序号就是它的开始值。

用户每使用序列一次，都要对序列进行一次查询。如果把序列中的序号放在内存中进行缓冲，那么用户获得序号的速度将大大加快。选项CACHE的作用就是将序列中接下来的n个序号在内存中进行缓冲。如果不希望进行缓冲，可以用NOCACHE选项代替它。

例如，下面的语句使用默认值创建了一个序列seq1：

SQL＞CREATE SEQUENCE seq1；

下面的语句创建了一个序列seq2，它的开始值是100，增幅是2，最大值为10000，序列中的序号不在内存中进行缓冲。

SQL＞CREATE SEQUENCE seq2

START WITH 10

INCREMENT BY 2

MAXVALUE 10000

NOCACHE；

序列的信息可以从数据字典user_sequences中获得。例如，下面的SELECT语句用于查询序列seq2的最小值、最大值、增幅、下一个可用序号、是否循环等信息：

SQL＞SELECT min_value, max_value, increment_by, last_number, cycle_flag

FROM user_sequences

WHERE sequence_name='SEQ2'

MIN_VALUE MAX_VALUE INCREMENT_BY LAST_NUMBER C

1　10000　2　10　N

序列在创建之后，在使用的过程中，可以对其进行修改。比如修改它的最大值、最小值、增幅等，但是不能修改开始值。需要注意的是，如果已经有部分序号被使用，那么对序列的修改只影响以后的序号，对以前已经使用的序号不起作用。

修改序列的命令是ALTER SEQUENCE。用户可以修改自己的序列，如果希望修改其他用户的序列，则需要具有ALTER ANY SEQUENCE这个系统权限。ALTER SEQUENCE命令的用法与CREATE SEQUENCE命令的用法基本相同。例如，下面的语句修改序列seq2的最小值、最大值、增幅，并使其中的序号可循环使用：

SQL＞ALTER SEQUENCE seq2

MINVALUE 5

MAXVALUE 50000

INCREMENT BY 3

CYCLE；

现在重新执行上面的SELECT，查询这个序列的信息，查询结果为：

MIN_VALUE MAX_VALUE INCREMENT_BY LAST_NUMBER C

5　50000　3　11　Y

删除序列的命令是DROP SEQUENCE。用户可以删除自己创建的序列，如果要删除其他用户的序列，则要具有DROP ANY SEQUENCE系统权限。序列被删除后，它的相关信息就被从数据字典中删除。例如，可用下面的语句删除序列seq2：

SQL＞DROP SEQUENCE seq2；

1.8.2　序列的使用

对用户而言，序列中的可用资源是其中包含的序号。用户可以通过SELECT命令获得可用的序号，也可以将序号应用于DML语句和表达式中。如果要使用其他用户的序列，则必须具有对该序列的SELECT权限。

序列提供了两个伪列，即NEXTVAL和CURRVAL，用来访问序列中的序号。其中NEXTVAL代表下一个可用的序号，CURRVAL代表当前的序号。序列可以认为是包含了一系列序号的一个指针。序列刚被创建时，这个指针位于第一个序号之前，以后每获得一个序号，指针就向后移动一个位置，这时就可以用CURRVAL访问序列中的当前序号，用NEXTVAL访问下一个序号。在第一次使用序列中的序号时，必须首先访问NEXTVAL伪列，使指针指向第一个序号。图1.4为序列中的序号和指针的示意。

图　1.4　序列中的序号

通过SELECT语句可以从序列中获得一个可用的序号。例如，对于已经创建的序列SEQ1，可以执行下面的SELECT语句：

SQL＞SELECT seq1.nextval FROM dual；

这条语句的执行结果为：

NEXTVAL

----------

1

其中seq1.nextval表示序列seq1的NEXTVAL伪列。这时如果再利用SELECT语句访问这个序列的CURRVAL伪列，应该返回它的当前序号，即1。在SELECT语句中使用表dual是必要的，因为SELECT语句将根据表中数据的行数返回若干个序号，并且每访问一次NEXTVAL伪列，指针就向后移动一个序号。假设把SELECT语句中的表名用dept代替，那么执行结果为：

NEXTVAL

----------

2

3

4

5

因为表dept中有四行，所以SELECT语句返回接下来的连续四个序号。

CURRVAL伪列代表序列中的当前序号，访问这个伪列时指针并不向后移动。CURRVAL伪列的引用方法与NEXTVAL伪列相同，引用格式为：序列名.currval。

序列还可应用于SELECT语句的其他形式。例如，在下面的SELECT语句中，序列seq1为每行数据提供了一个编号：

SELECT seq1.nextval, deptno, dname, loc FROM dept；

这条语句的执行结果类似于以下形式（这里假定第一次使用序列seq1）：

NEXTVAL DEPTNO DNAME LOC

1 10 ACCOUNTING NEW YORK

2 20 RESEARCH DALLAS

3 30 SALES CHICAGO

在更多情况下序列的作用为表中的主键列或其他列提供一个唯一的序号。例如，要往表emp中插入一行时，可以利用序列为每个员工指定唯一的员工号。下面的INSERT语句向表emp中插入一行，其中empno的值为序列seq1中的下一个序号：

SQL＞INSERT INTO emp（empno, ename, mgr, hiredate, deptno）

VALUES（seq1.nextval，'GOOD'，7902，sysdate，20）；

序列是一种共享式的数据库对象，用户可以直接使用自己创建的序列，其他用户也可以访问当前用户的序列，只要具有对这个序列的SELECT权限即可。如果一个序号被某个用户获得，那么其他用户就不能再获得这个序号了。也就是说，序列是可以共享的，但序列中的序号却是不能共享的。

对序列中序号的访问操作是作为一个单独的事务实现的，这个事务的执行与其他事务的执行成功与否无关。如果包含一条DML语句的事务被回滚了，那么对序列的操作是无法回滚的。假设序列seq1的当前序号是10，考虑下面的语句：

SQL＞INSERT INTO dept VALUES（seq1.nextval，'NETWORK'，'BEIJING'）；

SQL＞ROLLBACK；

SQL＞SELECT seq1.nextval FROM dual；

如果上面的语句都执行成功，最后一条语句的执行结果是12。这是为什么呢？因为INSERT语句获得了序列seq1的下一个序号11，虽然这个事务被回滚了，但是序列中的指针还是向后移动了，序号11再也无法使用了，下一个可以使用的序号是12。

在访问序列中的序号时，可能会发生序号不连续的情况，不连续的原因可能是事务发生了回滚，或者多个用户共同访问同一个序列。

一个用户要访问其他用户的序列时，不仅要具有对这个序列的SELECT权限，在访问时还要在序列的名称前以用户名进行限定。例如，下面的SELECT语句是当前用户访问用户scott的序列seq1的情况：

SELECT scott.seq1.nextval FROM dual；

如果要将一个序列的SELECT权限授予其他用户，相应的GRANT命令格式为：

GRANT SELECT ON序列名TO用户名；

1.8.3　序列信息的查询

序列作为一种数据库对象，它的相关信息也存储在数据字典中。与序列相关的数据字典有三个：USER_SEQUENCES、DBA_SEQUENCES和ALL_SEQUENCES。

其中数据字典USER_SEQUENCES的各列及意义如下所示：

名称　是否为空?类型　意义

SEQUENCE_NAME NOT NULL VARCHAR2（30）序列名称

MIN_VALUE NULL NUMBER　最小值

MAX_VALUE NULL NUMBER　最大值

INCREMENT_BY NOT NULL NUMBER　增加幅度

CYCLE_FLAG NULL VARCHAR2（1）是否循环使用

ORDER_FLAG NULL VARCHAR2（1）是否按顺序

CACHE_SIZE NOT NULL NUMBER　是否缓冲

LAST_NUMBER NOT NULL NUMBER　下一个可用序号

例如，要了解序列SEQ2的相关信息，可以执行下面的SELECT语句：

SQL＞SELECT sequence_name, min_value, max_value, increment_by, last_number

FROM user_sequences

WHERE sequence_name='SEQ2'；

这条语句的执行结果如下：

sequence_name min_value max_value Increment_by last_numbe

------------------------------------------------------------

SEQ2　1　10000　2　10

1.9　同义词

同义词是一种数据库对象，它是为一个数据库对象定义的别名，使用同义词的主要目的是为了简化SQL语句的书写。

1.9.1　同义词的概念和类型

利用同义词可以为用户的一个对象，或者其他用户的一个对象定义别名，从而简化命令或程序的书写，在命令或程序中可以直接使用同义词代替原来的对象。

可以为表、视图、存储程序、序列等对象建立同义词，也可以为一个同义词再建立同义词，甚至可以为一个不存在的对象建立同义词，系统仅仅在使用同义词时才验证它所代表的对象是否存在。

同义词本身并不包含原对象中的数据或代码，它的作用仅仅相当于一个指针。在使用同义词时，系统根据同义词的定义查找它所指向的对象，将对同义词的访问转化为对原对象的访问。同义词的作用仅仅是为了方便用户操作数据库对象。

Oracle支持两种类型的同义词，即私有同义词和公共同义词。私有同义词由普通用户创建，在默认情况下只能由用户本人使用。公有同义词一般由DBA创建，可以由所有用户使用。

公共同义词的意义在于它代表了一个大家都可以访问的对象。例如，程序包DBMS_OUTPUT是属于特权用户SYS的，但是任何一个用户都可以直接使用它，而不用指定它所在的模式。普通用户能够使用的名称DBMS_OUTPUT显然是一个公共同义词。

用户可以随意为其他用户的一个对象创建一个同义词，但这并不意味着这个这个用户就具有了访问其他用户的对象的权限。如果要通过同义词访问其他用户的对象，还需要具有相应的权限。只有在使用同义词时，系统才验证用户是否有相应的访问权限。

1.9.2　同义词的创建与删除

用户可以在自己的模式中创建同义词，这需要具有CREATE SYNONYM这个系统权限。如果希望在其他用户的模式中创建同义词，则需要具有CREATE ANY SYNONYM这个系统权限。普通用户创建的同义词一般都是私有同义词，公有同义词一般由DBA创建，普通用户如果希望创建同义词，则需要CREATE PUBLIC SYNONYM这个系统权限。

创建私有同义词的命令是CREATE SYNONYM，它的语法规则为：

CREATE SYNONYM同义词FOR用户名.对象名；

例如，假设当前用户为了方便地访问scott用户的表dept，可以执行下面的CREATE语句创建同义词：

CREATE SYNONYM sy_dept FOR scott.dept；

这样在具有相应权限的情况下，当前用户就可以通过这个同义词代替原来的表，在SQL语句中通过访问同义词来对原来的表进行操作。例如：

SELECT deptno, dname, loc FROM sy_dept；

创建公有同义词的命令也是CREATE SYNONYM，只是要使用PUBLIC关键字进行限定。创建公有同义词的命令格式为：

CREATE PUBLIC SYNONYM同义词FOR用户名.对象名；

例如，为了让大家都能方便地访问scott用户的表dept, DBA可以通过执行下面的语句创建一个公共同义词：

SQL＞CREATE PUBLIC SYNONYM pub_dept FOR scott.dept；

在具有相应权限的情况，任何用户都可以通过这个同义词访问SCOTT用户的表dept，例如，可以向表中写入一行数据：

SQL＞INSERT INTO pub_dept（deptno, dname, loc）

VALUES（100，'NETWORK'，'BEIJING'）；

用户如果不使用同义词时，可以将其删除。删除同义词的命令是DROP SYNONYM。这条命令的语法格式为：

DROP SYNONYM同义词；

一个用户可以删除自己创建的同义词，如果要删除其他用户创建的同义词，则要具有DROP ANY SYNONYM系统权限。DBA可以删除所有的公共同义词，普通用户需要具有DROP PUBLIC SYNONYM系统权限，才能删除公共同义词。同义词被删除以后，它的相关信息也将从数据字典中删除。

1.9.3　同义词信息的查询

同义词作为一种数据库对象，它的相关信息被存储在数据字典中。与同义词有关的数据字典有三个：user_sysnonyms、all_synonyms、dba_synonyms。

其中在数据字典user_synonyme中记录了当前用户所拥有的同义词。这个表的个列定义及其意义如下所示：

名称　是否为空?类型　意义

SYNONYM_NAME NOT NULL VARCHAR2（30）同义词的名称

TABLE_OWNER VARCHAR2（30）所指向的对象属主

TABLE_NAME NOT NULL VARCHAR2（30）所指向的对象名称

DB_LINK VARCHAR2（128）数据库链接

如果要查询当前用户创建了哪些同义词，它们各代表哪个用户的哪个对象，可以执行下面的SELECT语句进行查询：

SQL＞SELECT synonym_name, table_name, table_owner FROM user_synonyms；

在数据字典all_synonyms中记录了当前用户所能使用的所有同义词，包括私有同义词和公共同义词。

在数据字典dba_synonyms中记录了数据库中所有的同义词，包括每个用户创建的私有同义词和DBA创建的公共同义词。这个视图只有DBA能够访问，它的结构除了包含数据字典user_synonyms的所有列外，还有一个列owner代表同义词的创建者。

如果要在整个数据库范围内查询某个同义词的信息，可以对数据字典dba_synonyms进行查询。例如，要查询用户scott所创建的所有同义词，可以执行下面的SELECT语句：

SQL＞SELECT synonym_name, table_name, table_owner FROM dba_synonyms

WHERE owner='SCOTT'；

如果要查询用户scott的表dept具有哪些同义词，可以执行下面的SELECT语句：

SQL＞SELECT synonym_name, owner FROM dba_synonyms

WHERE table_owner='SCOTT'AND table_name='DEPT'；

如果要查询系统中所有的公共同义词，可以执行下面的SELECT语句：

SQL＞SELECT synonym_name, table_name, table_owner FROM dba_synonyms

WHERE owner='PUBLIC'；

第2章　SQL*Plus用法指南

在第1章中我们介绍了SQL的基本用法。SQL是一种命令式的语言，它是用户操作数据库的最重要的一种方式。SQL语句在执行时，需要有一个执行环境。开发人员在开发软件时，最终的结果是形成一个应用程序。在应用程序中可以嵌入SQL语句，利用这些SQL语句可以访问数据库。这时应用程序作为数据库服务器的客户，与数据库服务器以客户/服务器模式工作。把SQL语句嵌入到应用程序中的好处是使最终用户无需了解SQL的工作原理，也不需要知道SQL命令的使用方法，只要能够正确操作应用程序，就能够与数据库服务器进行交互。

然而对于数据库管理员，应用程序远远不能达到他们的目的，访问数据库最直接的方式是利用数据库软件提供的实用程序。Oracle提供了一个功能强大的实用工具—SQL*Plus。SQL*Plus是Oracle提供的一个客户端工具，也是远程客户访问数据库的一种重要途径。在SQL*Plus中可以执行任一条SQL语句，可以执行一个PL/SQL块，也可以执行SQL*Plus本身的命令，而且几乎所有的管理任务都可以在SQL*Plus中完成。

2.1　SQL*Plus的基本用法

如果要使用SQL*Plus与数据库服务器进行交互，首先要登录到数据库服务器上，这时在SQL*Plus进程和数据库服务器之间将建立一条连接，它们以客户/服务器模式工作。

2.1.1　登录与退出

启动SQL*Plus的命令是sqlplus，在操作系统提示符下输入sqlplus，系统将显示版本、日期、版权等信息，然后提示用户输入用户名和口令。如果输入正确，则在SQL*Plus和数据库服务器之间将建立一条连接，并显示服务器的信息，最后出现SQL*Plus的提示符“SQL＞”，在这个提示符下就可以输入需要执行的命令了。例如：

$sqlplus

SQL*Plus：Release 11.2.0.1.0 Production on Sun Apr 11 16：21：57 2010

Copyright（c）1982，2009，Oracle.All rights reserved.

请输入用户名：scott

请输入口令：*****

连接到：

Oracle Database 11g Enterprise Edition Release 11.2.0.1.0-64bit Production

With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL＞

如果以sys用户身份登录，在用户名之后还应该指定身份“as sysdba”或“as sysoper”。例如：

$sqlplus

SQL*Plus：Release 11.2.0.1.0 Production on Sun Apr 11 16：21：57 2010

Copyright（c）1982，2009，Oracle.All rights reserved.请

输入用户名：sys as sysdba

请输入口令：*****

连接到：

启动SQL*Plus时，还可以在sqlplus命令之后直接指定用户名和口令，这时系统将不再提示输入用户名和口令，而是直接登录到数据库服务器。格式是：用户名/口令。例如：

$sqlplus scott/tiger

$sqlplus sys/1234 as sysdba

如果在一台计算机上安装了多个数据库服务器，为了登录到正确的数据库，在进入SQL*Plus之前，先要设置环境变量ORACLE_SID，将其值设置为要登录的数据库。例如：

c：\＞SET ORACLE_SID=ORCL

如果数据库服务器运行在UNIX下，通过以下方式设置环境变量：

ORACLE_SID：$ORACLE_SID=ORCL

$export ORACLE_SID

在默认情况下，SQL*Plus和Oracle服务器位于同一台计算机上，启动SQL*Plus时，它将自动登录到本机的数据库服务器上。如果SQL*Plus和Oracle服务器不在同一台计算机上，在登录时还要指定所使用的网络服务名。例如：

C：\＞sqlplus scott/tiger@netsvr

其中netsvr是已经定义的网络服务名。这时SQL*Plus和Oracle服务器将通过Oracle Net进行通信。

无论采用哪种登录方式，登录成功后将出现SQL*Plus的提示符“SQL＞”。SQL*Plus是一个基于字符界面的工具，所有的命令都需要手工输入。在提示符之后输入的命令以分号结束。如果命令太长，可以输入回车，在换行之后继续输入，这时在每行之前将自动出现当前的行号。在命令的最后输入分号，然后回车，这条命令将提交给数据库服务器执行。需要注意的是，分号并不是SQL命令的一部分，而是一条SQL命令结束的标志。例如：

SQL＞SELECT empno, sal

2 FROM emp

3 WHERE deptno=10；

退出SQL*Plus时，在提示符之后输入命令QUIT或EXIT即可。如果要在不退出SQL*Plus的情况下断开与数据库服务器的连接，则输入DISCONNECT命令。如果要重新连接，或者在已经连接的情况下以另一个用户的身份连接，则使用CONNECT命令。这条命令的格式为：

CONNECT用户名/口令as sysdba

如果是SYS用户，则使用“as sysdba”或者“as sysoper”参数。如果是远程登录，还要在用户名和口令之后输入网络服务名。

在SQL*Plus中还可以执行操作系统命令。host命令使得用户可以在不退出SQL*Plus的情况下执行操作系统命令。在SQL*Plus提示符下执行host命令后，将进入操作系统提示符，在这里可以执行操作系统命令。在操作系统提示符下输入exit命令，将返回SQL*Plus。

SQL*Plus的UNIX版本还提供了一个“！”命令，它使得用户可以在SQL*Plus提示符下直接执行UNIX命令。UNIX命令执行结束后重新显示SQL*Plus提示符。例如：

SQL＞！vi/etc/host.conf

2.1.2　如何获取帮助信息

如果在使用SQL*Plus命令时有困难，可以使用HELP或?命令获得帮助信息。首先，可以获得帮助索引，命令的格式为：

SQL＞HELP INDEX

命令的执行结果为：

@COPY PAUSE SHUTDOWN

@@DEFINE PRINT SPOOL

/DEL PROMPT SQLPLUS

ACCEPT DESCRIBE QUIT START

APPEND DISCONNECT RECOVER STARTUP

……

上述命令都属于SQL*Plus，也就是说，这些命令只能在SQL*Plus中执行。读者在学习Oracle的过程中，一定要搞清楚哪些是SQL*Plus命令，哪些是SQL命令。通过HELP命令可以进一步获得每条命令的详细帮助信息。获取帮助的命令格式为：

HELP命令

例如：

SQL＞HELP CONNECT

CONNECT

-------

Connects a given username to Oracle.

CONN[ECT][{logon|/}[AS{SYSOPER|SYSDBA}]]

WHERE logon has the following syntax：username[/password][@CONNECT_identifier]

In iSQL*Plus you must always include your username and password.

2.1.3　如何修改SQL*Plus的设置信息

在SQL*Plus中有两类相关的设置信息，一类是SQL*Plus本身的设置信息，这类信息主要控制SQL*Plus的输出格式；另一类是数据库服务器的设置信息，这类信息主要来自实例的参数文件。

显示SQL*Plus设置信息的命令是show，例如，显示当前登录用户的命令为：

SQL＞show user

USER为"SCOTT"

如果要显示SQL*Plus的所有设置信息，执行“show all”命令，命令执行的结果类似于以下形式：

SQL＞show all

appinfo为OFF并且已设置为"SQL*Plus"

arraysize 15

autocommit OFF

autoprint OFF

autorecovery OFF

……

如果要显示某个具体的设置信息，可以在show命令之后跟上相关的关键字，例如：

SQL＞show timing

timing OFF

表2.1列出了SQL*Plus主要的设置信息及其意义。

如果要显示数据库服务器的参数设置信息，可以使用“show parameter”命令，并在命令之后指定要显示的参数名称。由于这些信息是从参数文件中读取的，因此只有特权用户可以查看这样的信息。例如，要查看当前数据库的名称，执行如下命令：

SQL＞show parameter db_name

NAME TYPE VALUE

db_name string ORCL

在命令执行的结果中包含参数的名称、类型和参数值。

由于参数名都是字符串，在显示参数时，可以只指定参数名称的一部分，这样，系统将显示所有包含这个字符串的参数。例如，要显示所有包含字符串“db_block”的参数设置信息，可以执行如下命令：

SQL＞show parameter db_block

NAME TYPE VALUE

db_block_buffers integer 0

db_block_checking boolean FALSE

db_block_checksum boolean TRUE

db_block_size integer 8192

修改SQL*Plus设置信息的命令是SET，这样可以灵活控制SQL*Plus的显示格式。例如，SQL*Plus的默认提示符是“SQL＞”，如果要将提示符改为“SQL*Plus＞＞”，可以执行以下命令：

SQL＞set sqlprompt"SQL*Plus＞＞"

需要注意的是，改变后的设置信息只对SQL*Plus的当前启动起作用。如果要经常修改这些设置信息，通常的做法是编写一个脚本文件，在脚本文件中指定这些设置信息，然后在SQL*Plus中执行脚本文件，具体的例子请参阅2.4节。

数据库服务器的参数设置信息来自参数文件，有关参数文件的信息，请参阅第4章。

2.1.4　实例的启动与关闭

Oracle的实例是一组内存结构和后台进程的集合，通过实例，应用程序才可以访问数据库中的数据。Oracle服务器是由实例和数据库组成的，一个实例只能打开一个数据库。

启动Oracle服务器的命令为startup，这条命令只有SYS用户才可以执行。执行startup命令时，显示的信息为：

SQL＞startup

ORACLE例程已经启动。

Total System Global Area 409194496 bytes

Fixed Size 2176248 bytes

Variable Size 301992712 bytes

Database Buffers 96468992 bytes

Redo Buffers 8556544 bytes

数据库装载完毕。

数据库已经打开。

在Windows系统中，数据库服务器表现为一个系统服务。所以，只要启动对应的系统服务，即可启动数据库服务器。系统服务的命名规则为：OracleService+实例名称。例如，在Windows系统的DOS窗口中，可以用命令行来启动Oracle服务器，例如：

C：＞net start OracleServiceORCL

OracleServiceORCL服务正在启动……

OracleServiceORCL服务已经启动成功。

更简单的方法是在控制面板的管理工具中打开“服务”组件，查找Oracle服务器所对应的系统服务，使其启动，如图2.1所示。

关闭数据库服务器的过程与启动过程正好相反，首先关闭数据库，然后卸载数据库，最后关闭实例。关闭Oracle服务器对应的命令为shutdown，这条命令也是只能由SYS用户执行。Oracle服务器关闭时显示的信息为：

SQL＞shutdown

数据库已经关闭。

已经卸载数据库。

ORACLE例程已经关闭。

在Windows系统中，Oracle服务器同样可以作为一个系统服务来关闭。例如：

C：＞net stop OracleServiceORCL

同样，Oracle服务器也可以通过图2.1所示的“服务”组件进行关闭。

图　2.1　Windows中的系统服务

2.2　SQL*Plus中的缓冲区

SQL*Plus提供了一个命令缓冲区，用来保存最近执行的一条SQL语句，或者一个PL/SQL块。用户可以反复执行缓冲区中的内容，也可以对缓冲区中的内容进行编辑。

2.2.1　执行缓冲区中的内容

执行缓冲区中内容的命令有两个：“/”和run。

“/”命令的作用是执行缓冲区中刚刚输入的或者已经执行内容。如果是一条SQL语句，它的结束标志是“；”，遇到分号，这条SQL语句就会执行。如果在SQL语句执行后输入/命令，这条SQL语句将再执行一次。如果是PL/SQL块，结束标志仍然是“；”，只是在输入结束后还必须输入/命令，这个块才能执行。如果再次输入“/”命令，这个块将再次执行。例如：

SQL＞CREATE TABLE t2（id number）；

表已创建。

SQL＞/

create table t2（id number）

*

ERROR位于第1行：

ORA-00955：名称已由现有对象使用

首先在SQL*Plus中执行CREATE命令创建表T2，然后输入“/”命令再次执行这条SQL语句。由于这个表已经创建，所以出现了错误信息。

run命令与“/”命令一样，也是再次执行缓冲区中的内容，只是在执行之前首先显示缓冲区中的内容。例如，在刚才执行了CREATE语句后，再执行run命令，结果如下所示：

SQL＞run

1*create table t2（id number）

create table t2（id number）

*

ERROR位于第1行：

ORA-00955：名称已由现有对象使用

2.2.2　编辑缓冲区的内容

缓冲区真正的意义在于用户可以编辑其中的内容，这样如果语句执行出错，用户可以很方便地进行修改，特别是长的、复杂的SQL语句或者PL/SQL块。

使用最频繁的编辑命令是edit（或ed）。这条命令的作用是打开默认的编辑器（在Windows环境中为记事本），并将缓冲区中的内容放在编辑器中。用户可以在编辑器中修改缓冲区中的内容，修改完后保存并退出编辑器，然后在SQL*Plus中输入/命令，修改后的内容将在SQL*Plus中执行。图2.2显示的是当一条SQL语句执行出错时，用edit命令打开编辑器的情况。

图　2.2　缓冲区内容的编辑

执行edit命令时，SQL*Plus在操作系统当前目录中建立了一个临时文件，用来保存当前缓冲区的内容。这个文件的默认文件名为“afiedt.buf”。需要注意的是，在这个临时文件中并不保存所有已经执行的SQL语句或者PL/SQL块，仅当执行edit命令时，才将当前缓冲区中的内容写入这个文件，文件中以前的内容将被覆盖。

如果要显示缓冲区中的内容，可以执行list（或者l）命令。list命令以分行的形式显示缓冲区的内容，并在每一行前面显示行号。如果要显示某一行的内容，可以在list命令之后指定行号，这样只显示指定的一行，并使这一行成为当前行，而不是显示所有内容。例如，假设在缓冲区中已经有一条SQL语句，我们可以以不同的形式执行list命令：

SQL＞list

1 SELECT ename

2 FROM emp

3*WHERE deptno=10

SQL＞l 2

2*FROM emp

SQL＞l3

3*WHERE deptno=10

还有一种简单的方法用来显示某一行的内容。在SQL*Plus提示符下直接输入一行的行号，结果与将行号作为参数的list命令是等价的。

append命令（或者a）的作用是在缓冲区中当前行的末尾追加文本。在默认情况下，最后一行是当前行。如果以某一行的行号作为参数执行了list命令，那么指定的行将成为当前行。append命令的格式为：

append文本

append将把指定的文本追加到当前行的末尾。注意追加的文本不需要用引号限定，否则引号将作为文本的一部分一起被追加。例如，对于前面的SELECT语句，如果希望在刚才的基础上再检索sal列的值，那么可以在第一行的末尾追加文本“，sal”。追加的步骤为：

SQL＞l1

1*SELECT ename

SQL＞append, sal

1*SELECT ename, sal

这样，缓冲区中第一行的内容由原来的“SELECT ename”变为“SELECT ename, sal”，SELECT语句执行的结果将检索ename和sal两个列的值。

append命令的作用是在当前行的末尾追加文本。如果要在缓冲区中增加一行，就要使用input命令。input命令（或者i）的作用是在当前行之后追加一行或者多行。在默认情况下，input命令在最后一行之后追加文本。如果要在某一行之后追加，应该先执行list命令使该行成为当前行，然后再追加。

使用input命令追加文本时，可以只追加一行，这时input命令的格式为：

input文本

如果要追加多行，则输入不带参数的input命令并回车，这时行号将变成ni的形式，其中n是从当前行号的下一个数字开始的整数，表示该行内容是追加到缓冲区中的。追加结束后以一个空行和回车符结束。例如，假设当前缓冲区中有一条不完整的SQL语句，现在希望把它补充完整。当前缓冲区显示的结果为：

SQL＞list

1 SELECT ename

2*AND sal＞1000

在第一行之后增加FROM子句和WHERE子句，操作步骤如下：

SQL＞list 1

1*SELECT ename

SQL＞input

2i FROM emp

3i WHERE deptno=10

4i

SQL＞

当前缓冲区中的内容为：

SQL＞list

1 SELECT ename

2 FROM emp

3 WHERE deptno=10

4*AND sal＞1000

注意，在追加多行时，input命令为追加的新行重新显示了行号，即上面的2i、3i等。输入结束后，在下一行直接回车，这时重新显示SQL*Plus提示符，追加操作便告结束。

如果发现缓冲区中内容有错误，可以用edit命令打开编辑器，在编辑器中进行修改。还有一种修改方法，就是change命令。change（或者c）命令的作用是在缓冲区中当前行上用新的字符串代替旧的字符串。这条命令的格式为：

change/新字符串/旧字符串

例如，要把上面修改后的SELECT语句中的最后一个条件“sal＞1000”改为“comm is not null”，操作的步骤为：

SQL＞list 4

4*and sal＞1000

SQL＞change/sal＞1000/comm is not null/

4*and comm is not null

重新显示的结果表明这一行的内容已经被修改。

如果要清空缓冲区中的内容，可以执行del命令。当缓冲区被清空后，就不能再执行edit命令进行编辑，也不能再执行list命令进行显示了。如果只删除缓冲区中的一部分内容，则通过edit和list命令可以显示剩下的内容。

在默认情况下，del命令删除缓冲区中当前行的全部内容。但是通过指定参数，del命令可以删除指定的一行或者多行。del命令的格式有以下形式：

·del开始行号结束行号删除开始行号和结束行号之间的行。

·del开始行号*删除开始行号和当前行之间的行。

·del*结束行号删除当前行和结束行号之间的行。

·del last删除最后一行。

其中开始行号和结束行号是指定的行号，开始行号必须小于结束行号。符号“*”用来代表当前行，标识符last代表最后一行。

2.2.3　如何对操作系统文件进行读写

在SQL*Plus中可以对操作系统中的文本文件进行简单的读写访问。例如，事先将SQL语句或者PL/SQL块的代码存放在文本文件中，再把文本文件调入缓冲区中，使之执行。或者把当前缓冲区中的内容保存到一个文件中，或者把SQL语句、PL/SQL块的执行结果保存到文件中。

读文件涉及的命令包括@、get、start等命令。

@命令的作用是将指定的文本文件的内容读到缓冲区中，并执行它。文本文件可以是本地文件，也可以是远程服务器上的文件。如果是本地文件，@命令的命令的执行格式为：

@文件名

这里的文件名要指定完整的路径，默认的扩展名是.sql，如果脚本文件使用了默认的扩展名，则在@命令中可以省略扩展名。如果是远程文件，必须将它存放到一个web服务器上，并以HTTP或FTP方式访问。这时@命令的命令的执行格式为（以HTTP为例）：

@http：//web服务器/文件名

使用@命令读取文件时，文件中可以包含多条SQL语句，每条语句以分号结束；或者可以包含一个PL/SQL块。文件被读入缓冲区中以后，SQL*Plus将按顺序执行文件中的代码，并将执行结果输出到显示器上。例如，假设在/home/oracle目录下有一个文件，名为a.sql，文件的内容为：

SELECT ename FROM emp WHERE empno=7902；

SELECT dname FROM dept WHERE deptno=10；

现在希望通过@命令将这个文件读到缓冲区中，命令执行的执行格式如下：

SQL＞@/home/oracle/a

@命令还有一个用法，就是在启动SQL*Plus的同时，将指定的文件读入缓冲区并执行它。这时@命令和文件名一起作为SQL*Plus的命令行参数，格式如下：

sqlplus用户名/口令@文件名

注意，这种格式与以前提到的使用网络服务的格式是很相似的，但是仍然有区别，请注意观察：

sqlplus用户名/口令@网络服务名

由于文件名和网络服务名都表现为字符串，所以单纯从名字上无法区分到底使用了文件名还是网络服务名。二者的区别在于第一种格式中在用户名/口令之后有一个空格，这时将把后面的参数解释为一个文件，并把这个文件加载到缓冲区中。在第二种格式中，用户名/口令之后没有空格，这时将后面的参数解释为网络服务名。

get命令的作用与@命令相似，但是它只是把文件加载到缓冲区中，并不直接执行。get命令的的执行格式为：

get文件名选项

其中文件名的默认扩展名为.sql，在get命令中可以省略。目前get命令只支持本地的操作系统文件。可以使用的选项有两个：LIST和NOLIST。其中LIST选项指定将文件的内容读到缓冲区的同时，还要在显示器上输出，这是默认选项。选项NOLIST使得文件的内容不在显示器上输出。

使用get命令时还要注意，在文本文件中只能包含一条SQL语句，而且不能以分号结束。也可以只包含一个PL/SQL块，块以分号结束。在使用@和get命令时要注意这些格式上的差别。例如，假设在/home/oracle目录下有一个文件，名为b.sql，文件的内容为：

SELECT ename FROM emp WHERE empno=7902

现在先通过get命令把它读入缓冲区，然后执行/命令使之执行：

SQL＞get/home/oracle/b

1*SELECT ename FROM emp WHERE empno=7902

SQL＞/

ENAME

----------

FORD

start命令与@命令是等价的，这里不再赘述。

写文件涉及的命令包括save和spool。其中save命令用于将当前缓冲区中的内容写入一个操作系统文件，而spool命令用于将命令的执行结果输出到一个操作系统文件。

save命令的格式为：

SQL＞save文件名选项

其中选项指定以什么样的方式写文件。可以使用的选项有以下三个：

·CREATE如果文件不存在，则创建。否则，命令执行失败。

·APPEND如果文件不存在，则创建。否则，在文件末尾追加。

·REPLACE如果文件不存在，则创建。否则删除原文件，重新创建。

如果不指定完整的路径，则在当前目录下产生这个文件，文件的默认扩展名是.sql。例如，假设当前缓冲区中有一条SELECT语句，使用save命令可以将这条语句写入文件：

SQL＞list

1*SELECT*FROM emp

SQL＞save/home/oracle/aa

spool命令利用假脱机技术，用于将SQL*Plus的输出写入到文件中，它有以下几种用法：

·spool得到当前spool的状态，默认为不可用。

·spool文件名启动spool，并打开指定的文件。

·spool off关闭spool，并将SQL*Plus的输出写入文件中。

·spool out关闭spool，将SQL*Plus的输出写入文件中，并同时送往打印机。

如果在SQL*Plus中以命令行的方式执行spool命令，那么从执行spool命令并打开文件开始，此后的所有输出，包括错误信息，以及用户的键盘输入，都将写入指定的文件，直到遇到“spool off”或者“spool out”。但是这些信息的写入是一次性完成，即在执行“spool off”或者“spool out”的一瞬间，这些信息才一次全部写入文件，包括最后执行的“spool off”或者“spool out”命令本身。文件的默认扩展名为.LST，默认的路径是当前目录。

spool命令通常的用法是生成报表。首先将精心设计的SQL语句存放在一个文件中，在产生输出的语句前后加上spool命令，然后将这个文件读到缓冲区中执行。这样在写入的文件中只有命令执行的结果，而不包括SQL语句本身。

例如，假设当前目录下有一个文件，名为c.sql，它的内容为：

spool cc

SELECT ename, sal FROM emp WHERE deptno=10；

spool off

现在将这个文件读到缓冲区中，并使之执行，执行结果如下：

SQL＞@c

ENAME SAL

CLARK 2450

KING 5000

MILLER 1300

文件中SQL语句的执行结果显示在屏幕上，同时在当前目录下生成了文件cc.LST，文件的内容与屏幕上显示的结果完全一致。

2.3　如何在SQL*Plus中使用变量

为了使数据处理更加灵活，在SQL*Plus中可以使用变量。SQL*Plus中的变量在SQL*Plus中的整个启动期间一直有效，这些变量可以用在SQL语句、PL/SQL块以及文本文件中。在执行这些代码时，先将变量替换为变量的值，然后再执行。

2.3.1　用户自定义的变量

用户可以根据需要，自己定义变量。有两种类型的自定义变量，第一类变量不需要定义，可以直接使用，在执行代码时SQL*Plus将提示用户输入变量的值。第二类变量需要事先定义，并且需要赋初值。

第一类变量不需要事先定义，在SQL语句、PL/SQL块以及脚本文件中可以直接使用。这类变量的特点是在变量名前面有一个“＆”符号。当执行代码时，如果发现有这样的变量，SQL*Plus将提示用户逐个输入变量的值，当用变量值代替变量后，才执行代码。例如，假设用户构造了一条SELECT语句，在语句中使用了两个变量：

SELECT ename, sal FROM＆table_name WHERE ename='＆name'；

这条语句的执行过程为：

输入table_name的值：emp

输入name的值：SMITH

原值　1：SELECT ename, sal FROM＆table_name WHERE ename='＆name'

新值　1：SELECT ename, sal FROM emp WHERE ename='SMITH'

ENAME SAL

SMITH 800

其中字符串“emp”和“SMITH”是用户输入的变量值。在SQL*Plus中首先用变量值代替变量，生成一个标准的SQL语句，然后再执行这条语句。当为所有的变量都提供了变量值后，这条语句才能执行。在构造这样的SQL语句时要注意，使用变量和不使用变量的语句在形式上是一致的。例如，ename列的值为字符型，应该用一对单引号限定，使用了变量以后，仍然要用一对单引号限定。

上述语句如果需要再次执行，系统将提示用户再次逐个输入变量的值。为了使用户在每次执行代码时不需要多次输入变量的值，可以在变量名前加上“＆＆”符号。使用这种形式的变量，只需要在第一次遇到这个变量时输入变量的值，变量值将保存下来，以后就不需要不断输入了。例如，假设把上述SELECT语句该为以下形式：

SELECT ename, sal FROM＆＆table_name WHERE ename='＆＆name'

那么在第一次执行时，像以前一样需要输入变量的值，而再次执行时，就不再需要输入变量的值了，直接使用以前提供的变量值。以下是第二次以后的执行情况：

SQL＞/

原值　1：SELECT ename, sal FROM＆＆table_name WHERE ename='＆＆name'

新值　1：SELECT ename, sal FROM emp WHERE ename='SMITH'

……

在SQL*Plus中可以使用的第二类自定义变量需要事先定义，而且需要提供初值。定义变量的命令是DEFINE。定义变量的格式是：

define变量名=变量值

变量经定义后，就可以直接使用了。实际上，用DEFINE命令定义的变量和使用“＆”的变量在本质上是一样的。用DEFINE命令定义变量以后，由于变量已经有值，所以在使用变量时不再提示用户输入变量的值。

如果执行不带参数的DEFINE命令，系统将列出所有已经定义的变量，包括系统定义的变量和用“＆”定义的变量，以及即将提到的参数变量。例如：

SQL＞define

DEFINE_CONNECT_IDENTIFIER="ORCL"（CHAR）

DEFINE_SQLPLUS_RELEASE="1001000200"（CHAR）

DEFINE_EDITOR="NOTEPAD"（CHAR）

DEFINE_O_VERSION="Oracle10g Enterprise Edition Release 10.1.0.2.0-Production

With the OLAP and Oracle Data Mining options

JServer Release 10.1.0.2.0-Production"（CHAR）

DEFINE_O_RELEASE="1001000200"（CHAR）

DEFINE_RC="1"（CHAR）

DEFINE TABLE_NAME="emp"（CHAR）

DEFINE NAME="SMITH"（CHAR）

其中最后两个变量就是刚才我们用“＆”定义的变量。

现在，让我们看一个用DEFINE命令定义变量的例子。

SQL＞define col_name=ename

SQL＞define salary=3000

在这里定义了两个变量，然后在SQL语句中就可以直接使用这两个变量了。在使用变量时，仍然用“＆变量名”的形式来引用变量的值。例如：

SQL＞SELECT＆col_name FROM emp WHERE sal＞＆salary；

在执行这条语句时，用ename代替变量col_name，用3000代替变量salary，生成一条标准的SQL语句。这条语句的执行结果为：

原值　1：SELECT＆col_name FROM emp WHERE sal＞＆salary

新值　1：SELECT ename FROM emp WHERE sal＞3000

ENAME

KING

当一个变量不再使用时，可以将其删除。undefine命令用于取消一个变量的定义。删除一个变量的命令格式为：

undefine变量名

2.3.2　参数变量

在SQL*Plus中，除了用户自定义的变量外，还有一类变量，这就是参数变量。参数变量在使用时不需要事先定义，可以直接使用。

前面我们讲述了get和@命令的用法。这两个命令的作用是将一个文本文件加载到缓冲区中，使之执行。因为文本文件的内容是固定的，在执行期间不能被修改，所以只能执行固定的代码，这就为灵活的数据操作带来了一定的困难。例如，要查询某部门中员工的工资情况。部门号事先不确定，而是根据实际情况临时确定的。这样在文本文件的SELECT语句中就不能将部门号指定为一个固定值。

解决这个问题的一个办法是使用参数变量。由于部门号是不确定的，所以在执行文本文件时可以将实际的部门号作为一个参数，在SELECT语句中通过参数变量引用这个参数。参数在SQL*Plus的命令行中指定的格式为：

@文件名参数1参数2参数3……

这样在文本文件中可以用参数变量＆1、＆2、＆3分别引用参数1、参数2、参数3……

例如，要查询某部门中工资大于某个数值的员工姓名，在构造SELECT语句时就不能将部门号和工资这两个列的值指定为固定值，而是分别用一个参数变量代替。假设我们在目录/home/oracle中建立了一个文本文件d.sql，文件的内容为：

SELECT ename FROM emp WHERE deptno=＆1 and sal＞＆2；

在执行这个文本文件时，需要为参数变量＆1和＆2指定实际的参数值。参数值是在用get或者@命令加载文本文件时指定的。例如，要查询部门10中工资大于2000的员工，执行文件d.sql的命令格式为：

SQL＞@/home/oracle/d 10 2000

这条命令执行的情况为：

原值　1：SELECT ename FROM emp WHERE deptno=＆1 and sal＞＆2

新值　1：SELECT ename FROM emp WHERE deptno=10 and sal＞2000

……

从命令的执行结果可以看出，在SQL*Plus中首先用实际参数10代替参数变量＆1，用参数2000代替参数变量＆2，生成一条标准的SQL语句，然后才执行这条SQL语句。

2.3.3　与变量有关的交互式命令

SQL*Plus还提供了几条交互式命令，主要包括prompt、accept和pause。这几条命令主要用在文本文件中，用来完成灵活的输入输出。

prompt命令用来在屏幕上显示指定的字符串。这条命令的格式为：

prompt字符串

注意这里的字符串不需要单引号限定，即使是用空格分开的几个字符串。prompt命令只是简单地把其后的所有内容在屏幕上显示。例如：

SQL＞prompt I'm a programmer

I'm a programmer

accept命令的作用是接收用户的键盘输入，并把用户输入的数据存放到指定的变量中，它一般与prompt命令配合使用。accept命令的格式为：

accept变量名变量类型prompt提示信息选项

其中变量名是指存放数据的变量，这个变量不需要事先定义，可直接使用。变量类型是指输入的数据的类型，目前SQL*Plus只支持数字型、字符型和日期型数据的输入。prompt用来指定在输入数据时向用户显示的提示信息。选项指定了一些附加的功能，可以使用的选项包括：hide和default。hide功能使用户的键盘输入不在屏幕上显示，这在输入保密信息时非常有用。default为变量指定默认值，在输入数据时如果直接回车，则使用该默认值。

例如，希望从键盘输入一个数字型数据到变量xyz，在输入之前显示指定的提示信息，还为变量指定默认值，这样如果在输入数据时直接回车，那么变量的值就是这个默认值。对应的accept命令的形式为：

SQL＞accept xzy number prompt请输入变量xyz的值：default 0

请输入变量xyz的值：100

pause命令的作用是使当前的执行暂时停止，在用户输入回车键后继续。一般情况下pause命令用在文本文件的两条命令之间，使第一条命令执行后出现暂停，待用户输入回车键后继续执行。pause命令的格式为：

pause文本

其中文本是在暂停时向用户显示的提示信息。

现在，我们构造一个文本文件，演示这几条命令的用法。文本文件e.sql的功能是统计某个部门的员工工资，部门号需要用户从键盘输入。文本文件的内容如下：

prompt工资统计现在开始

accept dno number prompt请输入部门号：default 0

pause请输入回车键开始统计……

SELECT ename, sal FROM emp WHERE deptno=＆dno；

这个脚本文件的执行过程为：

SQL＞@/home/oracle/e

工资统计现在开始

请输入部门号：10

请输入回车键开始统计……

原值　1：SELECT ename, sal FROM emp WHERE deptno=＆dno

新值　1：SELECT ename, sal FROM emp WHERE deptno=10

ENAME SAL

--------------------

CLARK 2450

KING 5000

MILLER 1300

如果希望生成一个报表，那么可以在SELECT前后分别加上spool命令，将统计的结果写到一个文件中，或者发往打印机。

2.4　SQL*Plus的报表功能

SQL*Plus有一个强大的功能，就是能够根据用户的设计生成美观的报表。实际上，利用本章中前面介绍的知识已经能够生成一个简单的报表了，但是如果要生成规范的、美观的报表，还要学习SQL*Plus的其他一些功能。

SQL*Plus的报表功能是利用它的命令来实现的。首先，用户要根据自己的意图，设计报表的显示格式，这包括报表的标题、各列的显示格式等。然后构造查询语句，决定要对哪些数据进行显示。最后还要决定把报表仅仅显示在屏幕上，还是存放在文本文件中，或者送往打印机。

一般情况下，生成一个报表需要许多条命令，如果每次在生成报表时都输入这么多的命令，是很麻烦的事情。通常的做法是把这些命令放到一个文本文件中，在需要时只要把这个文本文件读到缓冲区中，并使其执行即可。

2.4.1　报表的标题设计

报表的标题是利用SQL*Plus的两个命令来设计的，即TTITLE和BTITLE。其中TTITLE命令用来设计报表的头部标题，而BTITLE用来设计报表的尾部标题。

TTITLE命令设计的头部标题显示在报表每页的顶部。设计头部标题时，要指定显示的信息和显示的位置，还可以使标题分布在多行之中。TTITLE命令有以下几种执行格式：

·ttitle on|off：打开或关闭头部标题的显示，默认为on。

·ttitle头部标题信息：设计头部标题信息。

·ttitle：获得当前的标题设置信息。

其中ttitle on用来打开头部标题的显示功能，而ttitle off用来关闭这个功能，在默认情况下，这个功能是可用的。

用ttitle命令设计头部标题的操作是比较复杂的。这条命令的格式为：

ttitle format　显示格式　显示位置　显示信息

其中format参数用来规定标题的显示格式，这个参数是可选的。显示位置规定标题在一行中的位置，可选的位置有三个：CENTER（中间）、LEFT（左边）和RIGHT（右边）。显示信息指定了标题的内容。一般情况下，标题可以指定为以下内容：

·指定的文本

·SQL.LNO当前的行号

·SQL.PNO当前的页号

·SQL.RELEASE当前Oracle的版本号

·SQL.USER当前登录的用户名称

例如，设计一个显示在正中的标题，命令格式为：

ttitle center　蓝天集团公司员工工资统计表

如果在标题中要分开显示多条信息，例如制表人、当前页号等，可以在title命令中分别设置不同信息的显示格式、显示位置和显示内容。如果这些信息要在多行中显示，可以在两条信息之间使用SKIP选项。这个选项使后面的信息跳过指定的行数再显示，它需要一个整型参数，单位是行数。

例如，在刚才设计的标题的基础上，增加制表人和当前页号，作为副标题。副标题在主标题之下两行处显示。如果命令太长，一行容纳不下时，可以用“-”符号分行，将命令分为多行书写。满足上述要求的命令格式为：

ttitle center蓝天集团员工工资统计表skip 2 left-

制表人：sql.user right页码：sql.pno

这个标题的显示结果为：

蓝天集团员工工资统计表

制表人：SCOTT　页码：2

BTITLE命令的用法与TTITLE命令是一样的，区别在于BTITLE命令用来设计尾部标题，显示的位置在报表每页的底部。

2.4.2　报表显示格式的设计

报表的显示格式是指报表中数据的显示格式。有两条命令用来设计报表的显示格式，它们是COLUMN和BREAK。COLUMN命令用来设计某一列数据的显示格式，而BREAK命令使数据根据某个标准分组显示。

COLUMN命令的作用是设计某一列的显示格式，包括列标题的文字和对齐方式、列数据的宽度和显示格式等。这条命令是比较复杂的，主要表现为它有许多可以使用的选项。读者应该多练习它的使用，注意观察命令执行的结果，从中总结经验。COLUMN命令的格式为：

column列名选项

COLUMN命令的主要选项有以下几个：

·heading：指定列标题的显示文字。

·format：指定列数据的显示格式。

·justify：指定列标题的对齐方式，包括左（left）、居中（center）、右（right）。

·null：当列数据为空时，将显示指定的文本。

·wrapped|truncated：规定当列标题或数据超出规定的宽度时，如何显示。其中wrapped为默认值表示换一行继续显示。Truncated表示截断余下的数据。

其中heading选项用来规定列的标题。默认情况下，列的标题就是列的名字。用户可以定制自己喜欢的列标题。如果列标题中有空格，要用双引号限定。还可以把列标题中的文字分成两行显示，格式是：“第一行文字|第二行文字”。例如，通过下面的命令为ename列定义标题为“姓名”，为sal列定义标题为“工资”。

column ename heading姓名

column sal heading工资

那么，在执行下列SELECT语句时：

SELECT ename, sal FROM emp WHERE empno=7902；

显示的结果为：

姓名　工资

--------------------

FORD 3000

format选项指定数据的显示格式，主要用来设置字符型、数字型和日期型数据的格式。常用的格式字符串如表2.2所示。

例如，如果通过下列命令为sal列设置了显示格式：

column sal heading　工资　format$999，999.00

那么刚才执行的SELECT语句现在的执行结果为：

姓名　工资

----------------------

FORD$3，000.00

Justify选项用来指定列标题的对齐方式，可选的对齐方式有左对齐、居中和右对齐三种方式。注意这种对齐方式仅对列标题起作用，并不影响列的数据的对齐方式。

Null选项用来指定当列的数据为空时，应该显示什么样的数据。例如，在显示奖金信息时，如果没有奖金，可以显示为0。

在制作报表时，我们希望属于同一部门的员工数据集中在一起显示，这样我们可以将部门号作为分组的标准，将数据分组显示。如果部门号变化了，可以跳过几行或一页，继续显示另一部门的数据。

BREAK命令的作用就是根据指定的列作为分组标准，将数据分组显示。例如将同一部门的员工集中在一起显示。BREAK命令的格式为：

BREAK ON　列名　措施

其中列名就是被指定为分组标准的列，凡是该列数据相同的数据集中在一起显示。可以选择的措施有以下两个：

·skip行数|PAGE

·noduplicates|duplicates

其中skip选项规定当指定列的值发生变化时，怎样显示后面的数据。可以跳过指定的行数，或者跳过一页，继续显示后面的数据。当把一个正整数作为skip的参数时，跳过这么多行。如果把PAGE作为skip的参数，跳过一页，二者可选其一。

noduplicates（或nodup）和duplicates（或dup）选项规定了是否显示重复的列值。当所有的行以指定的列为标准分组显示时，这个列有许多重复的值。如果使用了nodup选项，将不显示重复值，这是默认的选项，如果使用了dup，将显示重复值。例如，如果以部门号为标准进行分组，那么所有的部门号为10的行集中在一起显示，在这些行中，可以只在第一行显示部门号10，其余行均不显示。当部门号为10的行显示完后，跳过若干行后或一页后，继续显示部门号为20的数据。在所有部门号为20的行中，仍然只在第一行中显示部门号20，其余行均不显示，依此类推。

现在考察下面的例子。我们希望检索工资大于2000的员工，并且以部门号为标准分组显示。构造的BREAK命令和SELECT语句如下所示：

SQL＞break on deptno skip 1 nodup

SQL＞SELECT deptno, ename, sal FROM emp

WHERE sal＞2000

ORDER BY deptno

这条SELECT语句执行的结果为：

deptno ename sal

-------------------------------

10 CLARK 2，450.00

KING 5，000.00

20 JONES 2，975.00

FORD 3，000.00

30 BLAKE 2，850.00

应该注意的是，当使用BREAK命令指定了分组标准后，系统是按照被检索的顺序对行进行分组显示的，而不是将数据按照分组标准集中在一起后再显示。所以在SELECT语句中附加ORDER BY子句对行进行排序是必要的。

现在，让我们把COLUMN和BREAK命令综合起来，制作一个比较复杂的报表。制作报表的代码如下：

ttitle center蓝天集团员工工资统计表skip 2 left-

制表人：sql.user right页码：sql.pno

btitle skip 1 center"内部资料概不对外"

column dname heading部门名称

column ename heading姓名

column sal format$999，999.00 heading工资justify center

column comm heading奖金

column loc format a10 heading部门地址

break on dname skip 1 nodup

SELECT dname, ename, sal, comm, loc

FROM emp, dept

WHERE emp.deptno=dept.deptno

ORDER BY emp.deptno；

现在把这段代码存放在一个文本文件中，假设为/home/oracle目录下的report.sql，然后把这个文件加载到缓冲区中，并使之执行，将得到我们希望的报表。生成的报表如下所示：

SQL＞@report

蓝天集团员工工资统计表

制表人：SCOTT　页码：1

部门　名称　姓名　工资　奖金　部门地址

-------------------------------------------------------

ACCOUNTING CLARK$2，450.00 NEW YORK

KING$5，000.00 NEW YORK

MILLER$1，300.00 NEW YORK

RESEARCH SMITH$800.00 DALLAS

FORD$3，000.00 DALLAS

JONES$2，975.00 DALLAS

内部资料　概不对外

蓝天集团员工工资统计表

制表人：SCOTT　页码：2

部门名称　姓名　工资　奖金　部门地址

------------------------------------------------------------

SALES ALLEN$1，600.00 300 CHICAGO

JAME S$950.00 CHICAGO

TURNER$1，500.00 0 CHICAGO

BLAKE$2，850.00 CHICAGO

MARTIN$1，250.00 1400 CHICAGO

WARD$1，250.00 500 CHICAGO

内部资料　概不对外

已选择12行。

以上生成的报表将在显示器上输出。如果希望把报表保存到文件中，或者直接送到打印机上进行打印，可在SELECT语句之前加上spool命令，以一个文本文件名作为它的参数。在SELECT语句之后再加上一个spool命令，并且以off或out作为它的参数。

2.4.3　如何对特定列进行统计

以前介绍的报表仅仅从数据库中检索数据，然后按照指定的格式显示出来。如果要对报表中的数据进行统计，就要借助于SQL*Plus的另外一条命令：COMPUTE。

一般情况下，对报表的统计不外乎两种形式，即水平统计和垂直统计。水平统计是把一行中的几个列的值进行计算，例如，求公司中每个员工的工资和奖金之和。这种统计比较简单，通过SELECT语句就可以实现。例如，下列SELECT语句将计算每个员工的工资和奖金之和：

SELECT sal+nvl（comm，0）AS收入FROM emp；

垂直统计是对报表中某个特定列的值进行某种计算。例如公司所有员工的工资总和，或者某个部门中所有员工的奖金之和。这种统计是比较复杂的，通过SQL*Plus的COMPUTE命令可以实现这种统计。

COMPUTE命令的格式为：

compute函数label标签文字of列名on列名

其中函数指定对数据进行什么样的统计。COMPUTE命令可以实现的统计有SUM（求和）、AVG（求平均值）、MIN（求最小值）、MAX（求最大值）和COUNT（计数）。

Label选项指定了一个字符串，用来在计算所得的数据之前显示。例如，对某列进行SUM统计的数据之前可以显示“总计”，在AVG统计的数据之前可以显示“平均”等。默认情况下显示的信息是所使用的函数名称，如sum、avg。

Of选项指定了一个列名，COMPUTE命令对这个列的数据进行计算，计算的结果显示在这个列的正下方。需要注意的是，这个列的数据类型必须能够进行指定的计算。

On选项之后也指定了一个列名。COMPUTE命令根据这个列对数据进行分组统计。COMPUTE命令通常是和BREAK命令配合使用的，BREAK命令对数据进行分组显示，而COMPUTE命令对分组后的数据分别进行计算。例如，我们可以将员工的数据按照部门号进行分别显示，然后按照分组的结果对各部门分别进行统计。例如，要对各部门员工的工资分别求和，相应的COMPUTE命令为：

compute sum label总计of sal on depnto

计算的结果显示在每个部门员工数据之后，被计算列的正下方。

现在，我们把与报表有关的几个重要命令综合起来，制作一个完整的报表。假设制作报表的所有命令存放在文本文件report.sql中，这个文件的内容为：

ttitle center蓝天集团员工工资统计表skip 2 left-

制表人：sql.user format 99 right页码：sql.pno

btitle skip 1 center"内部资料概不对外"

column dname heading部门名称

column ename heading姓名

column sal format$999，999.00 heading工资justify center

column comm format$9999.00 heading奖金

column收入format$999，999.00

break on dname skip 1 nodup

compute sum label总计of sal on dname

compute sum of comm on dname

compute sum of收入on dname

spool wwww

SELECT dname, ename, sal, comm, sal+nvl（comm，0）as收入

FROM emp, dept

WHERE emp.deptno=dept.deptno

order by emp.deptno；

spool off

这样在将这个文件读到缓冲区中并执行时，将生成希望的报表，并将报表存放到当前目录下的文件www.lst中。报表的内容为：

蓝天集团员工工资统计表

制表人：SCOTT　页码：1

部门名称　姓名　工资　奖金　收入

-------------------------------------------------------

ACCOUNTING CLARK$2，450.00$2，450.00

KING$5，000.00$5，000.00

MILLER$1，300.00$1，300.00

RESEARCH SMITH$800.00$800.00

FORD$3，000.00$3，000.00

JONES$2，975.00$2，975.00

*************---------------------------------

总计$15，525.00$15，525.00

内部资料概不对外

蓝天集团员工工资统计表

制表人：SCOTT　页码：2

部门名称　姓名　工资　奖金　收入

----------------------------------------------------------

SALES ALLEN$1，600.00$300.00$1，900.00

JAMES$950.00$950.00

TURNER$1，500.00$.00$1，500.00

BLAKE$2，850.00$2，850.00

MARTIN$1，250.00$1400.00$2，650.00

WARD$1，250.00$500.00$1，750.00

*************--------------------------------------

总计$9，400.00$2，200.00$11，600.00

内部资料　概不对外

第3章　PL/SQL编程

本章介绍PL/SQL有关的内容，主要包括匿名块、存储过程和存储函数三种形式的PL/SQL块，以及在PL/SQL中如何使用变量、类型、流控制语句、游标、触发器、异常等内容，重点介绍如何利用PL/SQL块访问数据库中的数据。

3.1　PL/SQL概述

如果说SQL是一种标准的数据库访问语言，那么PL/SQL则是Oracle公司开发的一种编程语言，它是对SQL的扩充。在PL/SQL程序中，可以定义变量、数据类型、函数以及过程，可以使用流控制语句，可以包含SQL语句，还可以进行错误处理。利用PL/SQL程序，可以对数据库进行复杂的访问。

与SQL相比，PL/SQL有很多优点，例如，PL/SQL体现了模块化的程序设计思想，在PL/SQL程序中可以定义函数和过程，用来完成不同的功能。PL/SQL程序还可以在不同平台、不同计算机之间方便地移植。另外，利用PL/SQL程序可以大大提高访问数据库的效率，因为PL/SQL程序可以在SQL*Plus中执行，也可以在客户端的应用程序中调用执行，只要知道PL/SQL程序的名字，就可以调用它。图3.1说明了这一点。

图　3.1　PL/SQL程序与传统SQL执行方式的比较

从图3.1中可以看出，传统的SQL命令是单独执行的，客户端每执行一条SQL语句，都要向数据库服务器发出一次请求，并从服务器返回一次结果。而PL/SQL程序是作为一个整体执行的，客户端只需发出一次请求，服务器执行后只返回一个结果。

PL/SQL是一种结构化编程语言，程序的基本单元是块，主要的块形式有函数、过程和匿名块。一个PL/SQL块由以下三部分组成：

DECLARE

变量声明部分

BEGIN

可执行部分

EXCEPTION

异常处理部分

END

其中变量声明部分用来定义变量、类型、游标、子程序、触发器、异常等。被声明的元素在本块范围内有效。这一部分在PL/SQL块中是可选的。

可执行部分是PL/SQL块的主体，它包含了PL/SQL块的可执行代码，是必不可少的部分。PL/SQL块的功能主要体现在这一部分。

异常处理部分用来处理PL/SQL块在执行过程中发生的错误。如果块执行正常，则块正常结束，否则从出现错误的语句开始，转至异常处理部分开始执行，即进行异常处理。这部分在PL/SQL块中也是可选的。

最简单的PL/SQL块仅包含由BEGIN和END限定的可执行部分。例如：

BEGIN

DBMS_OUTPUT.PUT_LINE（'Hello, World'）；

END；

3.2　PL/SQL中的变量

在PL/SQL块中可以定义变量和数据类型，这使得PL/SQL块对数据的处理更加灵活。变量和类型的定义放在PL/SQL块的变量声明部分。

3.2.1　变量的定义与使用

变量的定义有两种格式，分别为：

变量名　类型[约束][DEFAULT默认值]

变量名　类型[约束][：=初始值]

其中用方括号限定的部分是可选的。约束用来规定变量必须满足的条件，比如“NOT NULL”约束指定变量不能为空值，这样在定义变量的时候就要为其指定初始值或默认值。

变量名要遵守一定的命名规则。变量名必须以字母开头，包含数字、字母、下划线以及$、#符号，长度不能超过30字符，并且不能与Oracle关键字相同。变量名与大小写是无关的。

变量的类型可以是PL/SQL提供的数据类型，也可以是用户自定义的类型。表3.1列出了基本的数据类型。

除了上述基本类型外，PL/SQL还定义了一些子类型。在PL/SQL块中既可以使用这些基本数据类型，也可以使用它们的子类型，但数据的最终类型仍然是它的基本类型。表3.2列出了部分数据类型的子类型。

变量在定义时可以指定默认值或初始值，在PL/SQL块的运行过程中还可以为其赋值。赋值的格式为：

变量名：=表达式

如果需要输出变量的值，则要调用DBMS_OUTPUT程序包中的过程PUT_LINE，这个过程的参数是要输出的变量或表达式。除了变量的声明外，变量的赋值、输出等操作都要放在PL/SQL块的可执行部分。下面的代码演示了变量的声明、赋值和输出操作。

DECLARE

id integer NOT NULL DEFAULT 100；

name varchar2（20）：='SMITH'；

birthday date DEFAULT SYSDATE；

BEGIN

id：=200；

dbms_output.put_line（'id的值为：'||id）；

dbms_output.put_line（'name的值为：'||name）；

dbms_output.put_line（'birthday的值为：'||birthday）；

END；

上述代码的执行结果与下面类似：

SQL＞/

id的值为：200

name的值为：SMITH

birthday的值为：03-9月-04

在输出变量之前，要确保SQL*Plus的输出是打开的，否则将得不到输出结果。也就是说，首先要在SQL*Plus中对参数SERVEROUTPUT进行设置：

SQL＞SET SERVEROUTPUT ON

PL/SQL块的代码可以直接在“SQL＞”提示符下输入。输入结束后，执行命令“/”即可使该PL/SQL块开始执行。如果发现代码执行时有错误，可以输入“ed”命令进行修改。通过“ed”命令将打开一个文本编辑器，对缓冲区中的内容进行编辑。编辑完后重新输入命令“/”，可以再次执行PL/SQL块。

在定义变量时，除了直接为变量指定类型外，还可以通过%TYPE属性为变量指定类型，%TYPE用于获得另一个变量或者表中某个列的类型，使得新定义的变量与该变量或该列的类型完全一致。%TYPE属性的用法为：

变量名　另一变量%TYPE

变量名　表.列%TYPE

使用%TYPE属性的一个好处是当原来的变量或列的类型被修改后，不需要修改新变量的类型。另一个好处是，我们只希望一个变量与另一个变量的类型相同，但是我们可以不关心它到底是什么类型。例如：

id integer DEFAULT 100；

no id%TYPE；

name emp.ename%TYPE；

在上述例子中，首先定义了一个变量id，然后定义了一个变量no，通过%TYPE属性使变量no的类型与id变量的相同。最后又定义了变量name，它的类型与emp表中的列ename的类型一致。

3.2.2　如何在PL/SQL中定义类型

用户可以在PL/SQL块中根据需要定义数据类型，然后利用这个类型定义变量。常用的自定义类型包括记录类型和集合类型，它们都是复合数据类型。

记录类型允许在一个类型中包含若干类型不同的字段，字段类型可以是基本数据类型，也可以是另一个复合数据类型。记录类型的定义格式为：

TYPE类型名IS RECORD

（字段1定义，

字段2定义，

……

）；

其中每个字段的定义都与变量定义的方法完全相同，即包括字段名、类型、约束、默认值或初始值等几部分。

例如，要存储学生的信息，可以定义一个记录类型，包括姓名、年龄、学号、成绩等字段。该记录的定义为：

TYPE student is RECORD

（name char（10），

age integer DEFAULT 20，

no char（10），

score number（5，2）

）；

定义了记录类型后，现在就可以定义该类型的变量了。例如，下面的代码定义了两个student类型的变量，分别表示两个学生。

stu1 student；

stu2 student；

在使用记录类型变量时，要单独引用它的每个字段，引用的方法为：

变量.字段

记录型变量中字段的使用方法与普通变量基本相同，可以为其赋值，也可以输出它的值。下面的代码是在定义上述类型和变量的基础上，某个PL/SQL块的可执行部分：

BEGIN

stu1.name：='smith'；

stu1.no：='0203001'；

stu1.score：=97；

dbms_output.put_line（stu1.name）；

dbms_output.put_line（stu1.no）；

dbms_output.put_line（stu1.score）；

END；

由于student记录类型中的age字段使用了默认值，因此当没有为变量stu1中的age字段赋值时，该字段的值就为默认值。程序的运行结果为：

SQL＞/

smith

0203001

97

定义记录类型变量的另外一个简便方法是使用表的%ROWTYPE属性。%ROWTYPE属性可以取得表中各个字段的定义，使得记录类型变量的结构与表中一行的结构完全一致。例如，根据emp表的结构可以定义记录类型变量employee：

employee emp%ROWTYPE；

这样就定义了一个记录类型变量employee，它所包含的字段及其类型、长度与表emp的各列完全相同。在PL/SQL块中可以直接使用这个变量的各个字段，例如：

BEGIN

employee.empno：=100；

employee.ename：='SMITH'；

dbms_output.put_line（employee.empno）；

dbms_output.put_line（employee.ename）；

END；

使用%ROWTYPE属性的好处是可以根据表的结构直接定义结构类型变量，而不需要事先知道这个表的结构，而且当表的结构发生改变时，PL/SQL块中的变量定义不需要修改。

记录类型变量中包含若干类型不同的数据，而集合类型变量中包含多个相同类型的元素。要创建一个集合，先要定义一个集合类型，然后再定义该类型的集合变量。定义集合类型的格式如下：

TYPE类型名IS TABLE OF类型；

其中类型名是要创建的集合类型的名字；类型是指集合中每个元素的类型，每个元素的类型都相同。元素类型可以是基本的数据类型，也可以是使用%TYPE属性取得的另一个变量的类型，更复杂的情况是表中的一行，即集合中的每个元素是表中的一行数据。

例如，下面的语句定义了一个集合类型，元素类型是整数。

TYPE IntSet IS TABLE OF integer

在定义集合类型变量时，需要调用集合的构造函数，对集合变量进行初始化，为集合指定初始的元素，或者将其初始化为一个空集合。例如：

intSet1 IntSet：=IntSet（10，20，30）；

intSet2 IntSet：=IntSet（）；

在上述代码中定义了两个集合变量intSet1和intSet2，并分别调用构造函数进行初始化，intSet1中包含三个元素，intSet2是一个空集合，不包含任何元素。

集合类型类似于面向对象技术中的类，除了提供构造函数外，还提供了一些方法，可以用来对集合进行操作。表3.3列出了集合的部分方法。

通过这种方法定义的集合类型变量中的元素个数是确定的，通过EXTEND方法可以对变量中的元素进行扩展。在使用集合变量时，一般是单独使用其中的每个元素，可以对某个元素赋值，也可以输出某个元素的值。集合元素可以通过集合变量名和下标来引用，引用的方法为：

集合变量（下标）

下面的例子演示了集合的使用方法：

DECLARE

TYPE IntSet IS TABLE OF integer；

intSet1 IntSet：=IntSet（10，20，30）；

intSet2 IntSet：=IntSet（）；

BEGIN

dbms_output.put_line（'集合intSet1中元素的个数为：'||intSet1.count）；

intSet1.extend（2，3）；

dbms_output.put_line（'扩充后集合intSet1中元素的个数为：'||intSet1.count）；

for i in 1..intSet1.count loop

dbms_output.put_line（'第'||i||'个元素的值为：'||intSet1（i））；

end loop；

intSet2.extend（3）；

intSet2（1）：=100；

intSet2（2）：=200；

intSet2（3）：=300；

dbms_output.put_line（'集合intSet2中元素为：'）；

for i in 1..intSet2.count loop

dbms_output.put_line（'第'||i||'个元素的值为：'||intSet2（i））；

end loop；

END；

上述代码执行的结果为：

SQL＞/

集合intSet1中元素的个数为：3

扩充后集合intSet1中元素的个数为：5

第1个元素的值为：10

第2个元素的值为：20

第3个元素的值为：30

第4个元素的值为：30

第5个元素的值为：30

集合intSet2中元素为：

第1个元素的值为：100

第2个元素的值为：200

第3个元素的值为：300

实际上，还有一种定义记录类型的方法，定义的格式为：

TYPE 类型名 IS TABLE OF 类型 INDEX BY BINARY_INTEGER；

在定义集合类型之后，就可以定义集合类型变量了。通过这种方法定义的集合类型变量不需要进行初始化，在PL/SQL块的可执行部分可以任意地为第n个元素赋值，但是不能通过EXTEND方法对变量进行扩展。元素的实际数量就是被赋值的元素的个数。下面的代码是使用这种定义方法的例子：

DECLARE

TYPE xyz IS TABLE OF integer INDEX BY BINARY_INTEGER；

intSet3 xyz；

BEGIN

intSet3（1）：=100；

intSet3（2）：=200；

intSet3（10）：=1000；

dbms_output.put_line（'集合intSet3中元素的个数为：'||intSet3.count）；

END；

这个PL/SQL块的执行结果为：

集合intSet3中元素的个数为：3

3.3　PL/SQL中的流控制

PL/SQL提供了丰富的流控制语句，用来对程序的执行流程进行控制。通过流控制语句，我们可以编写更复杂的PL/SQL块。流控制语句分为两类，即条件判断语句和循环语句。

3.3.1　IF语句

IF语句是一种条件判断语句，它根据条件判断的结果执行不同的代码。最简单的IF语句格式为：

IF 条件 THEN

代码

END IF

如果条件成立，就执行指定的代码，否则执行IF语句后面的代码。如果还要求在条件不成立时执行另外的代码，则可以使用下面的形式：

IF 条件 THEN

代码1

ELSE

代码2

END IF

那么当条件成立时执行代码1，条件不成立时执行代码2。

在更复杂的情况下，要先后判断多个条件，这时要用到下面的形式：

IF 条件1 THEN

代码1

ELSIF条件2

代码2

……

ELSE

代码n

END IF

在执行上面的IF语句时，首先判断条件1。当条件1成立时执行代码1，否则继续判断条件2，如果成立则执行代码2，否则继续判断下面的条件。如果前面的条件都不成立，则执行ELSE后面的代码n。

上面提到的IF形式每次只进行一个条件的判断，如果这个条件成立，则执行相应的代码，否则继续判断下面的条件。有时候需要一次判断多个条件，根据多个条件的综合情况执行相应的代码，这就要用到条件的联合。

条件的联合有“与”和“或”两种形式。“与”运算通过AND运算符连接多个条件，只有当所有条件都成立时，整个条件判断才算成立。如果有一个条件不成立，则整个条件判断不成立。“与”运算的格式为：

条件1 AND 条件2 AND 条件3……

例如，要计算1+2+3+……+100的值，当和大于500时停止，求正好使和大于500的最后一个整数。这时可以构造两个条件，其中一个条件要求求和的数据小于等于100，另一个条件要求求和的结果小于等于500，只要这两个条件同时满足，就可以继续求和。只要其中一个条件不满足，则求和停止。用于求和的PL/SQL块的代码如下所示。在块中用到了LOOP循环。

DECLARE

i INTEGER：=0；

total INTEGER：=0；

BEGIN

LOOP

IF i＜=100 and total＜=500 THEN--如果两个条件同时满足，则继续循环

i：=i+1；

total：=total+i；

ELSE--只要有一个条件不满足，则退出循环，计算结束

EXIT；

END IF；

END LOOP；

dbms_output.put_line（i）；

END；

3.3.2　LOOP语句

LOOP是一种循环语句，它使一部分代码反复执行。LOOP语句的基本格式为：

LOOP

循环体

END LOOP

如果不做特殊处理，LOOP中的代码将无限制地执行。一般可以用EXIT WHEN或者EXIT语句结束LOOP循环。EXIT WHEN指定一个条件，当条件满足时退出循环。EXIT语句使循环结束，一般与IF语句结合使用。例如，要计算1+2+3+……的值，当和大于500时停止，求正好使和大于500的最后一个整数，代码如下：

DECLARE

i INTEGER：=0；

total INTEGER：=0；

BEGIN

LOOP

i：=i+1；

total：=total+i；

EXIT WHEN total＞500；

END loop；

dbms_output.put_line（i）；

END；

在上述例子中，语句EXIT WHEN total＞500使得当求和结果大于500时停止循环。这条语句可以用EXIT语句代替，这时需要用IF语句判断和是否大于500：

IF total＞500 THEN

EXIT；

END IF；

3.3.3　WHILE语句

WHILE语句的功能是根据条件判断的结果循环执行一部分代码，只要条件成立，则反复执行这段代码。WHILE语句的格式为：

WHILE 条件 LOOP

循环体

END LOOP

在执行WHILE循环时，首先判断条件是否成立，如果成立，则执行循环体。然后再判断条件，如果条件成立，接着执行循环体，直到条件不成立时，循环结束。例如，下面的代码用来求表达式1+2+3+……+100的值。

DECLARE

i INTEGER：=1；

total INTEGER：=0；

BEGIN

while i＜=100 LOOP

total：=total+i；

i：=i+1；

END LOOP；

dbms_output.put_line（total）；

END；

3.3.4　FOR语句

FOR语句的功能是指定一个起始值，一个终止值，在这个范围内反复执行一段代码，并由一个循环变量控制循环的执行。循环变量从起始值开始，每执行一次循环，循环变量自动加1或减1，直到与终止值相等时，循环结束。FOR语句的格式为：

FOR 循环变量 IN[REVERSE]起始值..终止值 LOOP

循环体

END LOOP

在FOR语句中，在IN的后面，从起始值开始到终止值之间的整数构成一个集合，集合中的元素依次加1。在执行FOR语句时，循环变量先取集合中的第一个元素，执行一次循环体，然后依次取集合中的每个元素，分别执行一次循环体，直到把集合中的元素都取一次。如果使用REVERSE，则按照相反的顺序取集合中的元素，即先取最后一个元素，然后依次取前面的元素，直到第一个元素。

例如，上述求表达式1+2+3+……+100的代码可以改用FOR语句实现，代码如下：

DECLARE

total INTEGER：=0；

BEGIN

FOR i IN 1..100 LOOP

total：=total+i；

END LOOP；

dbms_output.put_line（total）；

END；

从上面的例子可以看出，在FOR循环中，循环变量不需要定义，也不需要显式地将集合中的元素赋给它，也不需要进行条件判断，所以用FOR语句编写的代码比较简洁。需要注意的是，循环变量只能在FOR循环内使用。

3.4　PL/SQL如何访问数据库

编写PL/SQL块的主要目的是对数据库进行访问，因此，在PL/SQL块中可以包含SELECT语句、DML语句，还可以包含DCL语句。需要注意的是，在PL/SQL块中不能直接包含DDL语句，如果要利用PL/SQL块完成诸如创建表、修改表结构等操作，需要通过其他方法。

通过SQL语句以及流控制语句，可以编写复杂的PL/SQL块，对数据库进行复杂的访问。由于PL/SQL块一般是在应用程序中调用执行，而不是以交互方式执行，所以在PL/SQL块中的SQL语句与一般的SQL语句有所不同，这一点在学习时要注意。

3.4.1　如何对数据进行查询

在PL/SQL块中通过SELECT语句从数据库中检索数据。由于要对数据进行查询以及处理，而不仅仅是显示出来，所以SELECT语句需要采用下面的特殊格式：

SELECT列1，列2……INTO变量1，变量2……

FROM表

……

与一般的SELECT语句相比，PL/SQL块中的SELECT语句使用了INTO子句，其余部分是相同的。利用INTO子句，把查询到的数据存放在变量中，以进行相应的处理。例如，下面的例子首先从部门表中检索部门30的名称、地址等信息，分别存放在变量d_name和d_location中，然后从雇员表中查询该部门中员工的工资、奖金总和，分别存放在变量total_sal和变量total_comm中，最后打印出这些信息。为了使程序更加灵活，首先定义了一个变量dno，用来存放部门编号，这样在SELECT语句中就可以使用这样的变量。

DECLARE

dno integer；

d_name dept.dname%type；

d_location dept.loc%type；

total_sal number；

total_comm number；

BEGIN

dno：=30；

SELECT dname, loc INTO d_name, d_location

FROM dept

WHERE deptno=dno；

dbms_output.put_line（'名称：'||d_name||'位置：'||d_location）；

SELECT sum（sal），sum（nvl（comm，0））INTO total_sal, total_comm

FROM emp

WHERE deptno=dno；

dbms_output.put_line（'工资总和：'||total_sal||'奖金总和：'||total_comm）；

END；

程序执行的结果为：

SQL＞/

名称：SALES　位置：CHICAGO

工资总和：9400　奖金总和：2200

需要注意的是，在SELECT语句中，需要查询的列与INTO子句中的变量在数目、类型上要一致，否则会发生错误。

在执行SELECT语句时，可能会发生两种例外情况，一是没有查询到满足条件的数据，二是存在多行满足条件的数据，这就是通常所说的异常。Oracle预定义了一些异常，例如在第一种情况下发生的是异常NO_DATA_FOUND，在第二种情况下发生的是异常TOO_MANY_ROWS。对于这样的异常必须做出处理，否则会影响PL/SQL块的正确执行。

为了使程序简洁，在INTO子句中可以使用一个记录型变量，以代替多个单独的变量，把查询的数据存储在这个记录型变量中。例如，对上面的PL/SQL块进行改造，首先定义一个记录类型department以及一个该类型的变量depart。然后定义一个记录类型employee以及一个该类型的变量employ。在变量depart中存放需要查询的部门的信息，在变量employ中存放员工的总工资和总奖金。

DECLARE

dno integer；

TYPE department is RECORD

（d_no integer，

d_name dept.dname%type，

d_location dept.loc%type

）；

depart department；

TYPE employee is RECORD

（total_sal number，

total_comm number

）；

employ employee；

BEGIN

dno：=30；

SELECT deptno, dname, loc INTO depart

FROM dept

WHERE deptno=dno；

dbms_output.put_line（'名称：'||depart.d_name||'位置：'||depart.d_location）；

SELECT sum（sal），sum（nvl（comm，0））INTO employ

FROM emp

WHERE deptno=dno；

dbms_output.put_line（'工资总和：'||employ.total_sal||'奖金总和：'||employ.total_comm）；

END；

在使用记录型变量时要注意，SELECT之后的列要与记录型变量各个字段在数目与类型上保持一致，否则程序执行时会发生错误。

3.4.2　如何使用DML语句

在PL/SQL块中可以包含INSERT、DELETE、UPDATE语句，用于对数据库中的表进行增、删、改等操作。在这些语句中，可以使用数字、字符串等形式的常量，也可以使用变量，还可以使用记录型变量。例如，下面的程序向dept表中插入两行：

DECLARE

dno integer；

d_name dept.dname%type；

d_location dept.loc%type；

BEGIN

INSERT INTO dept VALUES（50，'NETWORK'，'NOWHERE'）；

dno：=60；

d_name：='DEVELOP'；

d_location：='NOWHERE'；

INSERT INTO dept VALUES（dno, d_name, d_location）；

END；

两条INSERT语句都是将三个常量作为三个列的值，将其插入表中。

在使用记录型变量时，可以将各个字段的值作为表中各列的值，向表中插入一行，在INSERT语句的VALUES子句中必须指定记录型变量各个字段的值。例如：

DECLARE

TYPE department is RECORD

（d_no integer，

d_name dept.dname%type，

d_location dept.loc%type

）；

depart department；

BEGIN

depart.d_no：=70；

depart.d_name：='MAINTAIN'；

depart.d_location：='NOWHERE'；

INSERT INTO dept（deptno, dname, loc）

VALUES（depart.d_no, depart.d_name, depart.d_location）；

END；

在删除表中的数据时，在DELETE语句的WHERE子句中也可以使用变量，例如，以下例子要从dept表中删除某个部门的信息，部门编号由变量dno指定。

DECLARE

dno number；

BEGIN

dno：=70；

DELETE FROM dept WHERE deptno=dno；

END；

在修改表中的数据时，在UPDATE语句的SET子句和WHERE子句中可以使用变量，也可以使用记录型变量。如果是记录型变量，则要单独使用该变量的各个字段。例如，下面的例子要修改某部门的信息，部门编号由变量dno指定，该部门的信息存放在记录型变量depart中。

DECLARE

dno number；

TYPE department is RECORD

（

d_name dept.dname%type，

d_location dept.loc%type

）；

depart department；

BEGIN

dno：=60；

depart.d_name：='MONITOR'；

depart.d_location：='WASHINGTON'；

UPDATE dept

SET dname=depart.d_name, loc=depart.d_location

WHERE deptno=dno；

END；

3.5　子程序设计

利用PL/SQL可以进行模块化程序设计。在一个PL/SQL块中，可以定义若干个子程序。把一些功能相对独立、需要经常执行的代码定义为一个子程序，在需要时根据子程序的名字进行调用。这样不仅便于程序设计和编码，而且利于程序的调试。

PL/SQL有两种形式的子程序，即过程和函数。在子程序中也可以定义变量、类型、游标等，也可以进行异常处理。在调用子程序时，可以向子程序传递参数。过程与函数的区别在于函数具有返回值，可以向调用者返回执行结果，而过程没有返回值。

3.5.1　如何使用过程

子程序的定义出现在PL/SQL块的声明部分，而它的调用则出现在PL/SQL块的可执行部分。过程的定义格式如下：

PROCEDURE 过程名（参数1定义，参数2定义……）

IS

变量声明部分

BEGIN

可执行部分

EXCEPTION

异常处理部分

END；

在过程中可以定义参数，在调用该过程时，可以向过程传递实际参数。如果没有参数，则过程名后面的圆括号及参数列表可以省略。参数的定义形式为：

参数名　参数传递模式　数据类型：=默认值

其中参数名和数据类型是必不可少的部分，其他两部分是可以省略的。参数传递模式包括IN、OUT以及IN OUT三种形式，其中IN是默认的传递模式，如果没有指定，则默认为IN，它指的是从调用者向过程中传递一个实际参数。

OUT是指从过程中向调用者传递参数，如果要使用这种传递模式，则需要明确指定。在调用过程时，过程的执行情况会影响这个变量的值。

“IN OUT”是一种双向传递模式，一方面从调用者向过程传递参数，另一方面从过程向调用者传递结果，如果要使用这种形式，则需要明确指定。

三种参数传递模式的比较如表3.4所示。

参数默认值的作用是在调用过程时，如果没有提供实际参数，则将此默认值作为实际参数传递给过程。数据类型用来指定参数的类型，在参数定义中不能指定对参数的约束条件，即不能指定参数的长度和是否为空等属性。

在下面的PL/SQL块中定义了两个过程，其中prompt过程用于对某部门的员工增加工资和奖金，而total_income过程用于计算某部门员工的总收入和应缴的税。

DECLARE

dno number；

procedure promption（salary IN integer, commiss IN integer, d_no IN integer：=0）

is

BEGIN

if d_no=0 then--表示所有部门

UPDATE emp

set sal=sal+salary, comm=comm+commiss；

else--仅表示指定的部门

UPDATE emp

set sal=sal+salary, comm=comm+commiss

WHERE deptno=d_no；

END if；

END；

procedure total_income（d_no IN integer：=0）

is

empno integer；

total number；

tax number；

BEGIN

if d_no=0 then--表示所有部门

SELECT sum（sal+nvl（comm，0）），sum（sal*0.03）INTO total, tax FROM emp；

else--仅表示指定的部门

SELECT sum（sal+nvl（comm，0）），sum（sal*0.03）INTO total, tax

FROM emp

WHERE deptno=d_no；

END if；

dbms_output.put_line（'总收入：'||total||'总税款：'||tax）；

END；

BEGIN--PL/SQL块的可执行部分

dno：=10；

promption（100，0，dno）；

total_income（dno）；

END；

程序的执行结果为：

总收入：9050 总税款：271.5

在过程promption中定义了三个参数，其中参数d_no带有默认值，这样在调用该过程时，如果没有为该参数提供实际参数，则使用默认值。例如，在上述例子中，为部门10中的员工增加了100元工资。如果采用下面的调用形式，则为所有部门的员工增加100元工资：

promption（100，0）；

在调用过程时，需要为过程中的参数提供实际参数，它们在顺序上是对应的。为了保证将参数正确地传给过程，要求在定义过程时，将所有带默认值的参数集中放在参数列表的右边。因为只有这样才能将其他实际参数一对一地赋给前面的不带默认值的参数。

如果过程有多个参数，在调用过程时，也可以不按照参数列表的顺序提供实际参数，这时需要两种参数之间的对应关系。例如，过程promption可以采用以下调用形式：

promption（d_no=＞dno, commiss=＞0，salary=＞100）；

过程total_income中的参数d_no也带有默认值，这样在调用该过程时，如果提供了实际参数，则计算指定部门的员工总收入和总税款，如果采用以下调用形式：

total_income（）；

则计算所有部门的员工总收入和总税款。

在上述两个过程中，所有参数的传递模式都是IN，即把实际参数从调用者传递给过程。这种形式是默认的，可以省略IN关键字。如果需要把过程的执行情况反映给调用者，则需要使用OUT形式，或者“IN OUT”形式。例如，为了计算员工总收入和总税款，并把结果反映到主程序中，对过程total_income进行了一些改造，增加了两个参数，传递模式都是OUT。改造后的PL/SQL块代码如下：

DECLARE

dno number；

abcd number；

xyz number；

procedure total_income（d_no IN integer, total OUT number, tax OUT number）

is

empno integer；

BEGIN

if d_no=0 then--表示所有部门

SELECT sum（sal+nvl（comm，0）），sum（sal*0.03）INTO total, tax FROM emp；

else--仅表示指定的部门

SELECT sum（sal+nvl（comm，0）），sum（sal*0.03）INTO total, tax

FROM emp

WHERE deptno=d_no；

END if；

END；

BEGIN--PL/SQL块的可执行部分

dno：=10；

total_income（dno, abcd, xyz）；

dbms_output.put_line（'总收入：'||abcd||'总税款：'||xyz）；

END；

程序的执行结果为：

总收入：8750 总税款：262.5

在调用过程total_income时，提供了三个实际参数，其中参数abcd和xyz没有实际的值，即使有，在这里也不起任何作用，因为这两个参数的传递模式是OUT。过程在执行时，将参数total和tax的值传分别赋给abcd和xyz，这样就将过程中的数据传给了调用者。

3.5.2　如何使用函数

函数是另一种形式的子程序，它不仅可以像过程那样定义数据和类型，传递参数，还可以向调用者返回执行结果。函数的定义语法格式如下：

FUNCTION 函数名（参数1，参数2……）RETURN 数据类型

IS

声明部分

BEGIN

可执行部分

EXCEPTION

异常处理部分

END；

其中参数的定义、传递模式都与在过程中的情况是相同的。例如，下面这个函数的功能是根据参数n的值，计算1+2+3+……+n的值，最后将结果返回。

FUNCTION total（n IN integer）RETURN integer

IS

result integer：=0；

i integer；

BEGIN

FOR i in 1..n LOOP

result：=result+i；

END LOOP；

RETURN result；

END；

从函数中应当向调用者传递一个返回值。在函数参数列表之后的RETURN语句规定了函数的返回值类型，它可以是简单类型，也可以是记录、集合等复杂类型。在函数的可执行部分应当至少包含一个RETURN语句，用于向调用者返回执行结果。任何一条RETURN语句的执行都将导致函数的执行结束，并返回调用者。

除了具有返回值外，函数在其他方面的用法与过程基本相同。在调用函数时，可以将函数的返回值赋给一个变量，变量的类型必须与函数的返回值相同。例如，考虑下面的PL/SQL块的代码：

DECLARE

total_income number；--PL/SQL块中的变量

total_tax number；

function income（d_no emp.deptno%type）RETURN number--函数income

is

total number；

BEGIN--函数income的可执行部分

SELECT sum（sal+nvl（comm，0））INTO total

FROM emp

WHERE deptno=10；

RETURN total；

END；

function tax（d_no emp.deptno%type）RETURN number--函数tax

is

total number；

BEGIN--函数tax的可执行部分

SELECT sum（sal+nvl（comm，0））*0.03 INTO total

FROM emp

WHERE deptno=10；

RETURN total；

END；

BEGIN--PL/SQL块的可执行部分

total_income：=income（10）；

dbms_output.put_line（'总收入为：'||total_income）；

total_tax：=tax（10）；

dbms_output.put_line（'总共应交税：'||total_tax）；

END；

这个块的执行结果为：

SQL＞/

总收入为：8750

总共应交税：262.5

在这个PL/SQL块中定义了两个函数income和tax，分别用于计算指定部门的员工的总收入和应当交纳的所得税。两个函数都使用了一个参数，代表需要处理的部门号，由于没有指定参数的传递模式，故采用默认的IN方式。在两个函数中经过计算后返回计算结果。

在PL/SQL块的可执行部分，分别调用了这两个函数，并将函数的返回值赋给了变量total_income和total_tax，然后进行打印。

在PL/SQL块的主程序和函数中都可以定义变量，主程序的变量定义在该块的声明部分，变量的作用范围是整个块，包括各个函数。函数的变量定义在函数的声明部分，其作用范围仅限于该函数。如果PL/SQL块的变量与函数的变量同名，那么主程序的变量在该函数中不起作用。这个规则也适用于过程。

为了说明PL/SQL块的变量和函数变量的关系，我们再看看下面的PL/SQL块：

DECLARE

total number：=100；

procedure fun1

is

total number：=0；

BEGIN

dbms_output.put_line（'在函数fun1中total的值为：'||total）；

END；

procedure fun2

is

BEGIN

dbms_output.put_line（'在函数fun2中total的值为：'||total）；

END；

BEGIN

total：=total+100；

fun1；

fun2；

dbms_output.put_line（'在块中total的值为：'||total）；

END；

程序执行的结果为：

SQL＞/

在函数fun1中total的值为：0

在函数fun2中total的值为：200

在块中total的值为：200

在块的声明部分定义了一个变量total，并赋初值为100。在函数fun1中也定义了一个同名的变量，并赋初值为0，在该函数中起作用是自己的变量，所以调用函数fun1时变量total的值为0。在函数fun2中没有定义同名的变量，该函数中的变量total就是主程序中的变量，在调用该函数之前已经使这个变量增加了100，所以调用函数fun2时变量total的值为200。在主程序中当然使用的是自己的变量，而不可能是函数中的变量。

3.5.3　函数与过程的重载

函数与过程是一段具有特定功能的程序段，在PL/SQL块中根据它们的名字进行调用。函数和过程的名字原则上可以由用户任意指定，只要满足命名规则即可，但是最好能够体现这段代码的功能。

在调用函数或过程时，根据它们的定义可能需要提供实际参数。如果实际参数的类型或数目与形式参数不一致，子程序将出现错误信息，并停止执行。如果需要对不同类型或不同数目的参数进行相似的处理，我们可以定义多个名字相同的函数，这就是子程序的重载。

重载子程序的名字相同，但是参数的类型或数目不同，返回值也可能不同。如果完全相同，就不是重载了，而是重复定义，这是不允许的。

在调用重载子程序时，主程序将根据实际参数的类型和数目，自动确定调用哪个子程序。例如，要对整数和浮点数分别求整数次幂，可以编写两个重载子程序，参数的类型分别是整数和浮点数。这样在调用时，如果提供的实际参数是浮点数，则自动调用对浮点数求幂的子程序。如果提供的实际参数是整数，则自动调用对整数求幂的子程序。例如，在下面的代码中，定义了两个重载函数，分别对整数8和浮点数8.8求10次幂。

DECLARE

m integer；

int_number integer；

int_result integer；

float_number float；

float_result float；

function power（x integer, n integer）RETURN integer--对整数x求n次幂

is

result integer：=1；

BEGIN

for i in 1..n loop

result：=result*x；

END loop；

RETURN result；

END；

function power（x float, n integer）RETURN float--对浮点数x求n次幂

is

result float：=1；

BEGIN

for i in 1..n loop

result：=result*x；

END loop；

RETURN result；

END；

BEGIN

m：=10；

int_number：=8；

int_result：=power（int_number, m）；

dbms_output.put_line（'对整数求幂的结果为：'||int_result）；--调用第一个power函数

float_number：=8.8；

float_result：=power（float_number, m）；--调用第二个power函数

dbms_output.put_line（'对浮点数求幂的结果为：'||float_result）；

END；

上述PL/SQL块的执行结果为：

SQL＞/

对整数求幂的结果为：1073741824

对浮点数求幂的结果为：2785009760.0940212224上面的PL/SQL块提供了两个重载函数，在调用时根据参数的类型不同自动确定调用哪个函数。下面再看一个例子，根据参数的数目不同自动确定调用哪个函数。

DECLARE

procedure increase_salary（d_no emp.deptno%type, amount float）

is

BEGIN

UPDATE emp set sal=sal+amount WHERE deptno=d_no；

END；

procedure increase_salary（amount float）

is

BEGIN

UPDATE emp set sal=sal+amount；

END；

BEGIN

increase_salary（10，100.50）；--调用第一个increase_salary过程

increase_salary（200）；--调用第二个increase_salary过程

END；

在这个例子中定义了两个重载过程，用于为员工增加工资。第一个过程有两个参数，分别是部门号和增加的额度。第二个过程只有一个参数，即增加的额度。在调用过程时，如果提供了部门号和增加额度两个实际参数，则调用第一个increase_salary过程，为指定部门的员工增加工资。如果只提供了增加额度这一个参数，则调用第二个increase_salary过程，为所有员工增加工资。

3.5.4　函数与过程的递归调用

子程序定义好以后，需要在主程序或其他子程序中调用后才能执行，执行完后返回到调用者。在有些情况下，子程序在执行过程中还可能要调用自己，调用结束后返回当前调用的地方。子程序自己调用自己的现象称为递归调用。

考虑求整数n的阶乘的情况。n！的值为n*（n-1）！，为了求n的阶乘，首先要求出（n-1）！。同样，要计算（n-1）！，首先要计算（n-2）！的值，一直到1的阶乘，而1的阶乘的值是已知的。如果编写一个函数fact，这个函数可以求得任何整数的阶乘，那么这个函数就是一个递归函数。下面是求阶乘的递归过程：

fact（n）=n*fact（n-1）

=n*（n-1）*fact（n-2）

=……

=n*（n-1）*（n-2）*……*fact（1）

在调用函数fact求n的阶乘时，首先要求n-1的阶乘，这时需要调用函数自己，不过这次传递的参数是n-1。同样，求n-1的阶乘时，需要再次调用函数本身，求得n-2的阶乘，这次传递的参数是n-2。依此类推，最后要调用fact函数求1的阶乘，而1的阶乘是已知的，这是递归返回的条件。求得1的阶乘后，便可返回到调用fact（1）的地方，求得2的阶乘。求得2的阶乘后再返回到调用fact（2）的地方，求得3的阶乘。这样每返回一次，就可求得上一个数的阶乘，直到求得n的阶乘。下面是一个求整数m的阶乘的PL/SQL块。

DECLARE

m integer；

result integer；

function fact（n integer）

RETURN integer

is

BEGIN

if n=1 then

RETURN 1；

else

RETURN n*fact（n-1）；--递归调用

END if；

END；

BEGIN

m：=10；

result：=fact（m）；--调用函数，求整数10的阶乘

dbms_output.put_line（m||'的阶乘为：'||result）；

END；

这个PL/SQL块的执行结果为：

SQL＞/

10的阶乘为：3628800

让我们再来看一个递归调用的例子。表emp中存放的是公司员工的信息，其中包括员工号、员工姓名以及经理编号等信息。以下是表emp中这三个列的数据：

SQL＞SELECT empno, ename, mgr FROM emp

EMPNO ENAME MGR

7369 SMITH 7902

7566 JONES 7839

7839 KING

7902 FORD 7566

从查询的结果可以看出，除员工KING外，其他人都有一个经理，而经理同时也是一个员工，其中KING是公司的最高领导。如果指定任何一个员工号，希望得到这个员工的经理，以及这个经理的经理，一直到最高领导这样的垂直、直接领导关系。借助于子程序的递归调用，可以完成这样的要求。以下是用过程递归的方法编写的一个PL/SQL块：

DECLARE

procedure manager（employee_no emp.empno%type）

is

name emp.ename%type；--员工姓名

manager_no emp.empno%type；--员工经理的编号

manager_name emp.ename%type；--员工经理的姓名

BEGIN

SELECT ename, mgr INTO name, manager_no

FROM emp WHERE empno=employee_no；

if manager_no is not null then--如果员工的经理编号不为空，则查询其姓名

SELECT ename INTO manager_name FROM emp WHERE empno=manager_no；

dbms_output.put_line（name||'-＞'||manager_name）；

manager（manager_no）；--递归调用，查询该经理的经理

else--如果员工的经理编号为空，说明该员工即为最高领导

dbms_output.put_line（name||'是最高层领导'）；

END if；

END；

BEGIN

manager（7369）；

EXCEPTION

when NO_DATA_FOUND then

dbms_output.put_line（'没有这样的员工'）；

END；

这个PL/SQL块的执行结果为：

SMITH-＞FORD

FORD-＞JONES

JONES-＞KING

KING是最高层领导

过程manager以一个员工号为参数，首先查询该员工的经理编号。如果经理编号为空，则说明该员工为公司的最高领导，这时打印相应的信息，并结束过程的执行。否则查询该经理的姓名，并打印他们之间的领导关系，然后递归调用过程本身，以该经理的编号作为参数，继续查询他的经理的信息。

从程序的运行结果可以看出，编号为7369的员工姓名为SMITH，他的经理为FORD，而FORD的经理是JONES，这样可以一直向上追溯到最高领导KING。为了防止在程序中指定一个不存在的员工号而导致程序执行出错，在程序中增加了异常处理，如果没有查询到任何信息，则打印相应的出错信息。

3.6　存储过程与存储程序

在前面的部分我们介绍了PL/SQL块的基本编写方法。在SQL*plus中编写PL/SQL程序，并在SQL*plus中执行它，PL/SQL块的代码就存放在SQL*plus的缓冲区中。如果在SQL*plus中执行了其他的SQL语句或PL/SQL块，缓冲区中就会存放新的代码，原来的PL/SQL块就会被从缓冲区中清除出去。这种没有名称只是临时存放在缓冲区中的PL/SQL块叫做匿名块。

匿名块就是没有名字的PL/SQL块，它仅存放在缓冲区中，只能在当前SQL*plus环境中执行。如果希望PL/SQL块能随时被调用执行，并且能被数据库用户共享，就需要创建存储程序。存储程序是有名字的PL/SQL块，用户可以根据它的名字进行多次调用。存储程序在创建时经过了编译与优化，被存放在数据库中，任何用户只要有适当的权限，就可以调用它。而且在调用时无需再进行编译，因此能以很快的速度执行。

与匿名块相比，存储程序是作为数据库对象存储在数据库中的，因此，首先要在数据库中创建存储程序。

存储程序的调用可以在SQL语句中、应用程序中、SQL*plus中以及其他PL/SQL块中进行。在第一次被调用时，存储程序的代码被装载到系统全局区的共享池中，以后再次调用时直接从共享池中取出代码即可执行。

存储程序与前面介绍的子程序的区别在于子程序是完成某个特定功能的程序段，它本身并不能单独执行，只能作为一个模块，在一个PL/SQL块内部被调用执行。而存储程序是一个可单独执行的程序，它可以包含多个子程序，可以在SQL语句中、应用程序中、SQL*plus中以及其他PL/SQL块中被调用执行。

存储程序的形式包括：存储过程、存储函数、触发器和程序包等。

3.6.1　存储过程

如果用户要在自己的模式中创建存储过程，需要具有CREATE PROCEDURE系统权限，如果要在其他用户的模式中创建存储过程，则需要具有CREATE ANY PROCEDURE系统权限。创建存储过程的语法为：

CREATE OR REPLACE PROCEDURE 过程名（参数1，参数2……）

AUTHID CURRENT_USER|DEFINER

AS

声明部分

BEGIN

可执行部分

EXCEPTION

异常处理部分

END；

其中OR REPLACE选项的作用是当同名的存储过程存在时，首先将其删除，再创建新的存储过程。当然，条件是当前用户具有删除原存储过程的权限。存储过程在创建过程中已经进行了编译和优化。如果需要对存储过程进行修改，不能直接修改它的源代码，只能执行CREATE命令重新创建。存储过程、存储程序、程序包都是这样的情况。

存储过程可以带有参数，这样在调用存储过程时就需要指定相应的实际参数。如果没有参数，过程名后面的圆括号和参数列表就可以省略了。每个参数的定义格式为：

参数名　参数传递模式　数据类型：=默认值

参数各定义中各部分的用法与子程序中的参数完全相同，详细信息请参阅本章“子程序设计”部分。

AUTHID选项用来规定存储过程执行时的权限。这个选项有两个可选值，即CURRENT_USER和DEFINER，二者只能选择其中一个。过程的执行者和创建者可能不是同一个用户，如果使用CURRENT_USER创建存储过程，那么在调用时，该过程以当前登录用户的身份执行。为此，过程的创建者必须授予当前用户执行该过程的权限。如果以DEFINER创建存储过程，那么在调用时，该过程将以创建者身份执行，这是创建存储过程时默认的选项。

在存储过程中可以定义变量、类型、子程序、游标等元素，定义的方法与在匿名块中完全相同，这里不再详细描述。存储过程的声明部分开始于关键字AS，结束于关键字BEGIN，而且不需要使用关键字DECLARE。

存储过程的可执行部分是它的主要部分，它可以包含SQL语句和流控制语句，是存储过程功能的集中体现。异常处理部分用来处理存储过程在执行过程中可能出现的错误。例如，下面的代码用来创建存储过程total_income，它的功能是计算某部门员工的总收入。这个过程有一个参数，代表部门编号，并指定了默认值。这样，在调用时，如果提供了参数，则计算指定部门的数据，否则将计算所有员工的数据。

CREATE OR REPLACE PROCEDURE total_income（d_no IN integer：=0）

AUTHID DEFINER

AS

total number；

BEGIN

if d_no=0 then--表示所有部门

SELECT sum（sal+nvl（comm，0））INTO total FROM emp；

else--仅表示指定的部门

SELECT sum（sal+nvl（comm，0））INTO total

FROM emp

WHERE deptno=d_no；

END if；

dbms_output.put_line（'总收入：'||total）；

END；

存储过程创建以后，就可以随时调用执行了。在SQL*plus中调用存储过程的命令是EXECUTE，命令的使用格式为：

EXECUTE过程名（实际参数）

例如，要计算部门10的员工总收入和应缴的税，则可以以下形式调用刚才创建的存储过程total_income：

SQL＞EXECUTE total_income（10）

如果要在一个PL/SQL块中调用存储过程，则不需要EXECUTE命令，只要通过过程名和实际参数就可以调用，调用的格式为：

过程名（实际参数）；

每个用户都可以执行自己创建的存储过程，如果要执行其他用户的存储过程，则需要具有对该存储过程的EXECUTE权限。为此，存储过程的所有者要将EXECUTE权限授予这个用户。授予EXECUTE权限的语句格式为：

GRANT EXECUTE ON过程名TO用户

例如，存储过程total_income的所有者要将它的执行权限授予用户lxj，则可以执行下面的SQL语句：

SQL＞GRANT EXECUTE ON total_income TO lxj

如果要删除一个存储过程，可以执行DROP命令，这个命令的格式为：

DROP PROCEDURE 过程名

3.6.2　存储函数

存储函数也是一种存储程序，它被创建后便存储在数据库中，用户可以直接调用。存储函数与存储过程的区别在于，存储函数必须向调用环境返回一个执行结果。一般情况我们是把存储函数作为一个表达式来使用的，它可用于普通表达式能够使用的场合，这是因为每个函数都有一个返回值，在调用存储函数时，这个返回值便是存储函数的执行结果。例如，可以将存储函数赋给一个变量，或者将这个函数与另一个表达式进行计算等。

创建存储函数的语法格式为：

CREATE OR REPLACE FUNCTION 函数名（参数1，参数2……）RETURN 返回类型

AUTHID CURRENT_USER|DEFINER

AS

声明部分

BEGIN

可执行部分

RETURN 表达式；

EXCEPTION

异常处理部分

END；

可以看出，创建存储函数的格式与创建存储过程的格式大致相同，只有三个不同的地方，第一，用FUNCTION关键字代替了PROCEDURE关键字，以表明创建的对象是存储函数；第二，在参数列表之后用RETURN关键字规定了存储函数返回值的类型；第三，在存储函数的可执行部分至少有一条RETURN语句，将执行结果返回给调用者。

在存储函数的可执行部分中，可能会出现多条RETURN语句，用于向调用者返回不同的数据，但是经过逻辑处理后，只能有一条RETURN语句被执行，保证从存储函数中返回一个确定的数据，这样就符合了程序的“单出口”的原则。

如果用户要在自己的模式中创建存储函数，需要具有CREATE FUNCTION的系统权限，如果要在其他模式中创建存储函数，则需要具有CREATE ANY FUNCTION的系统权限。

例如，下面的存储函数用来计算每个员工的总收入。这个函数有两个参数，即工资和奖金，它的功能是求出工资和奖金之和，然后将结果返回。创建这个函数的语句为：

CREATE OR REPLACE FUNCTION total_income（sal number, comm number）

RETURN number

AS

result number：=0；

BEGIN

result：=sal+nvl（comm，0）；

RETURN result；

END；

如果要利用这个存储函数求员工的总收入，可以将这个函数用在SELECT语句中，作为SELECT语句的一个表达式，并且向它传递实际参数，最后得到它的计算结果。例如：

SQL＞SELECT ename, total_income（sal, comm）as total FROM emp；

再比如，下面的存储函数用于计算员工应缴的个人所得税，这个函数以部门号为参数，计算该部门中全部员工的所得税总和。假设税率为3%，该函数用SUM函数计算全体员工的工资总和，然后乘以3%，并将最后的结果返回。函数的代码如下所示：

CREATE OR REPLACE FUNCTION tax_per_depart（dno integer）RETURN number

AS

result number：=0；

BEGIN

SELECT sum（sal）*0.03 INTO result FROM emp

WHERE deptno=dno

GROUP BY deptno；

RETURN result；

END；

与其他存储函数一样，这个函数可以用在SELECT语句中，也可以在其他匿名块、存储过程、存储函数中调用执行。例如，在下面的匿名块中调用了该函数，计算部门20的所得税。

DECLARE

dno integer；

total_tax number；

BEGIN

dno：=20；

total_tax：=tax_per_depart（dno）；

dbms_output.put_line（'Total tax of department'||dno||'is：'||total_tax）；

END；

每个用户都可以直接调用自己创建的存储函数，如果要调用其他用户的存储函数，则需要具有对相应存储函数的EXECUTE权限。为此，存储函数的所有者要将EXECUTE权限授予适当的用户。授予EXECUTE权限的语句格式为：

GRANT EXECUTE ON函数名TO用户

例如，存储函数total_income的所有者要将它的执行权限授予用户lxj，则可以执行下面的SQL语句：

SQL＞GRANT EXECUTE ON total_income TO lxj

如果要删除一个存储函数，可以执行DROP命令，这个命令的格式为：

DROP FUNCTION 过程名

总之，存储过程和存储函数都是存储程序，它们的区别在于存储过程没有返回值，只能被单独调用执行，在功能上类似于一条命令，而存储函数有返回值，可以用在SELECT语句和运算表达式中，它的作用相当于一个普通的表达式。在存储过程和存储函数中都可以定义子程序，这里把重点放在了存储过程和存储函数本身的使用上，对子程序在存储过程和存储函数中的用法没有进行描述，实际上这也是很简单的，详细的使用方法请参阅3.5节的相关内容。

3.6.3　程序包

程序包是一种Oracle数据库对象，它是一组逻辑上相关的数据类型、变量、过程、函数和游标等的集合。程序包被创建后，存储在数据库中，用户可以直接使用包中的数据类型和变量，也可以直接调用包中的过程和函数。

程序包有两种形式，一种是用户根据需要创建的程序包，一种是系统预定义的程序包。这里介绍自定义程序包的创建、使用、删除等操作，以及预定义程序包的使用方法。

用户可以根据需要创建自己的程序包。在程序包中可以定义数据类型、变量、过程、函数、异常和游标等元素，这些元素具有全局的特性，可以在程序包中使用，也可以在程序包之外使用。

一个程序包由两部分组成：程序包的头部和包体。其中头部用来定义类型、变量、异常、声明游标、过程和函数，它的作用相当于程序包的接口。在包体中可以利用头部的类型定义变量，定义过程、游标和函数的代码。

在创建程序包时，头部和包体是分别创建的，并且头部必须在包体之前创建。程序包创建之后，如果要对其功能进行修改，这时只需修改包体的代码即可，不用修改头部，仅当需要改变参数类型、参数个数等信息时，才需要修改程序包的头部。

创建程序包头部的命令是CREATE PACKAGE，这条命令的语法格式为：

CREATE[OR REPLACE]PACKAGE包名

AUTHID CURRENT_USER|DEFINER

AS

类型的定义；

变量的定义；

子程序的声明；

游标的声明；

异常的声明；

END；

其中OR REPLACE选项的作用是当指定的包已经存在时重新创建它。AUTHID选项用来规定程序包以哪个用户的身份执行。这个选项有两个可选值，即CURRENT_USER和DEFINER，二者只能选择其中一个。

子程序的声明就是定义过程和函数的原型，即子程序的名称、参数和返回值，不包含它的代码部分。类型定义部分允许用户根据需要创建自己的数据类型。

例如，要对部门员工的总收入和所得税进行统计，为此需要编写一个程序包。在程序包中首先定义了一个记录类型total，然后声明了一个函数tax_per_depart，用来统计某个部门的所得税，过程total_per_depart用来统计各个部门的员工总收入。最后还定义了一个游标c1。需要注意的是，在程序包的头部定义游标时需要指定它的返回类型。以下是创建程序包employee头部的代码：

CREATE OR REPLACE PACKAGE employee

AS

type total is record（

dno emp.deptno%type，

total_income number

）；

function tax_per_depart（dno integer）RETURN number；

procedure total_per_depart；

cursor c1 RETURN total；

END；

程序包的包体是对头部的实现，主要用来定义过程和函数的可执行代码。创建包体的命令是CREATE PACKAGE BODY，这条命令的语法格式为：

CREATE[OR REPLACE]PACKAGE BODY包名AS

游标的实现；

子程序的实现；

END；

其中包名与创建头部时使用的名字完全相同。游标的实现是指定游标中所使用的SELECT语句。子程序的实现是写出过程和函数的代码，过程和函数的编写方法与以前介绍的方法完全相同。以下是创建程序包employee的包体的代码。

CREATE OR REPLACE PACKAGE BODY employee

AS

CURSOR c1 RETURN total is--定义游标

SELECT deptno, sum（sal）FROM emp GROUP BY deptno；

function tax_per_depart（dno integer）--定义函数tax_per_depart

RETURN number

as

result number；

BEGIN

SELECT sum（sal）*0.03 INTO result FROM emp

WHERE deptno=dno

GROUP BY deptno；

RETURN result；

END；--函数tax_per_depart结束

procedure total_per_depart--定义过程total_per_depart

as

depart total；

BEGIN

open c1；

fetch c1 INTO depart；--利用取出游标中的数据

while c1%found loop

dbms_output.put_line（'部门'||depart.dno||'总收入：'||depart.total_income）；

fetch c1 INTO depart；

END loop；

close c1；

END；--过程total_per_depart结束

END；--包体结束

定义了程序包employee后，用户就可以在PL/SQL块或者SQL*Plus中使用这个包中的类型、游标、变量、过程和函数了，使用的方法为：

包名.元素名

例如，要利用程序包employee中的过程total_per_depart统计各个部门员工的总收入，可以在SQL*Plus中调用这个过程：

SQL＞EXEC employee.total_per_depart

部门10 总收入：8750

部门20 总收入：6775

部门30 总收入：9400

再比如，在一个匿名块中调用程序包employee中的函数tax_per_depart，计算部门20的所得税，这个匿名块的代码为：

DECLARE

dno integer；

total_tax number；

t1 employee.total--利用程序包employee中的类型total定义一个变量，

BEGIN

t1.dno：=10；

t1.total_income：=1000；

dno：=20；

total_tax：=employee.tax_per_depart（dno）；

dbms_output.put_line（'Total tax of department'||dno||'is：'||total_tax）；

END；

如果一个程序包不再需要，我们可以将其从数据库中删除。删除程序包时，可以选择只删除包体，或者删除整个包。删除整个程序包的命令是DROP PACKAGE，它的格式为：

DROP PACKAGE包的名字；

这样，程序包的头部和包体都将从数据库中被删除。

如果只删除包体，相应的命令为DROP PACKAGE BODY，它的格式为：

DROP PACKAGE BODY包的名字；

3.6.4　系统预定义程序包

Oracle提供了一些预定义的程序包，利用这些包可以完成一些复杂的操作。这些程序包提供了一些常用的类型、变量、过程和函数，用户可以在PL/SQL块和应用程序中直接使用它们。正确地使用这些预定义的程序包，可以使开发工作达到事半功倍的效果。常用的预定义程序包及其用途如下所示：

DBMS_OUTPUT　实现基本的输入输出功能

UTL_FILE　对操作系统文件进行读、写等操作

DBMS_SQL　执行DDL语句

DBMS_PIPE　用于在两个进程间以管道方式进行通信

DBMS_JOB　管理数据库中的作业

下面将对最常用的程序包DBMS_OUTPUT、UTL_FILE和DBMS_SQL做简单的介绍。

1.DBMS_OUTPUT程序包

DBMS_OUTPUT包的功能是将PL/SQL块的执行结果显示在屏幕上，这种输出操作是通过缓冲区来完成的。SQL*Plus为存储程序、PL/SQL块、触发器的执行提供了一个缓冲区，用于存放程序执行期间所产生的数据，这个缓冲区以“先进先出”的方式管理其中的数据。

在默认情况下，PL/SQL块的执行结果是输出到缓冲区里的，如果进行一些特殊的设置，缓冲区中的数据就会输出到屏幕上，然后从缓冲区中清除。DBMS_OUTPUT包提供了对缓冲区进行设置、读和写等操作的功能，它提供了一系列的过程和函数，分别对缓冲区进行设置、读和写等操作。用户利用DBMS_OUTPUT包中的过程或函数可以向缓冲区中写入数据，也可以从缓冲区中读数据。

缓冲区的设置操作主要包括使其可用和不可用等操作。使缓冲区不可用的过程是DISABLE，这个过程可以在SQL*Plus中以如下形式执行：

SQL＞EXEC dbms_output.disable；

如果要在存储程序、PL/SQL块和触发器中调用这个过程，则不需要EXEC命令，可以直接调用执行。

与DISABLE相对的操作是ENABLE过程，它可以使缓冲区可用，并且可以设置缓冲区的大小。它的调用形式为：

enable（缓冲区的大小）

如果在调用这个过程时不指定任何参数，则结果是使缓冲区可用，并将其大小设置为默认大小，即20 000字节。例如，要将缓冲区的大小设置为1024字节，这个过程的调用形式为：

SQL＞EXEC dbms_output.enable（1024）；

缓冲区的写操作指的是向缓冲区中写入数据，目前允许的数据类型有数字型、字符串型和日期型。写操作涉及的过程有以下几个：

·PUT（参数）：将指定的参数写入缓冲区。

·PUT_LINE（参数）：将指定的参数写入缓冲区，并在行末写一个换行符。

·NEW_LINE：在缓冲区中当前位置处写一个换行符。

缓冲区中的数据是以行的形式组织的，每行最多存储255个字符，一行写满时，自动从下一行开始继续写。由于缓冲区的大小有限，写数据的原则是“先进先出”，当缓冲区写满时，如果还要继续写，那么最先写入缓冲区中的数据就会被从缓冲区中清除出去，以便腾出空间容纳新数据。

PUT和PUT_LINE过程的作用都是向缓冲区当前位置处写入一行数据，它们之间的区别是，PUT_LINE在写完数据后在当前行的末尾写入一个换行符，而PUT过程不写入换行符。过程NEW_LINE的作用仅仅是在缓冲区当前位置处写入一个换行符。实际上，调用一次过程PUT_LINE，相当于先调用一次过程PUT，然后再调用一次过程NEW_LINE。

如果要使缓冲区中的数据显示在显示器上，必须使选项SERVEROUTPUT有效，这个选项的作用就是使缓冲区中的数据可以输出到屏幕上。为了使这个选项有效，在SQL*Plus中执行SET命令：

SQL＞SET SERVEROUTPUT ON

这个选项的另一个可选值是OFF，它的作用正好与ON相反。为了说明这几个过程的用法，让我们首先观察下面这个PL/SQL块的执行情况：

DECLARE

data1 integer：=100；

data2 varchar2（10）：='Hello'；

data3 date DEFAULT sysdate；

BEGIN

dbms_output.put（data1）；

dbms_output.put_line（data2）；

dbms_output.put_line（data3）；

END；

这个块的执行结果为：

SQL＞/

100Hello

28-5月-10

缓冲区的读操作是指将缓冲区中的数据以行的形式读出来。与缓冲区的读操作有关的过程有两个：

·GET_LINE：从缓冲区中读一行。

·GET_LINES：从缓冲区中读多行。

过程GET_LINE的作用是将目前缓冲区中最先写入的一行数据读出，并将这一行数据从缓冲区中删除。它的调用形式为：

GET_LINE（变量，状态）

其中变量用于存放从缓冲区中读出的数据，它的类型必须与要读的数据一致。状态也是一个变量，用来表示本次读操作是否成功，它的传递模式为OUT。在这个过程执行结束后，如果状态变量的值为0，表示成功，如果为1，则表示缓冲区中没有数据。

过程GET_LINES的作用是将目前缓冲区中最先写入的几行数据读出，并将它们从缓冲区中删除。它的调用形式为：

GET_LINES（变量，行数）

其中变量是一个集合类型变量，用来存放读到的几行数据。行数也是一个变量，在读操作之前，这个参数用于指定需要读的行数，在读操作之后，这个参数表示实际读到的数据行数。下面再通过一个例子说明读操作和写操作的综合应用。

DECLARE

data integer；--表示数据的变量

stat integer；--表示状态的变量

BEGIN

dbms_output.put（100）；

dbms_output.put_line（200）；

dbms_output.get_line（data, stat）；

dbms_output.put_line（'缓冲区中的数据：'||data）；

dbms_output.put_line（'状态：'||stat）；

END；

这个块的执行结果为：

SQL＞/

缓冲区中的数据：100200

状态：0

在上述PL/SQL块中，第一次向缓冲区中写100时使用了过程PUT，写入数据后没有换行。第二次向缓冲区中写200时使用了过程PUT_LINE，这样100和200被写在了同一行。在读数据时使用了过程GET_LINE，将刚才写入的一行数据读到变量data中，于是变量data的值为100 200。而变量stat用来表示本次读操作是否成功，其值为0，表示读操作成功。由此可见，在从缓冲区中读数据时，是以行为单位进行的，而不是以数据为单位。

实际上，DBMS_OUTPUT程序包本身并没有输入输出的功能，它所能做的就是对缓冲区进行读写操作。如果使SERVEROUTPUT选项有效，则缓冲区的内容就被输出到屏幕上，PUT和PUT_LINE过程只需要把数据写入缓冲区中就可以了，这就相当于完成了输出工作。而GET_LINE的功能是从缓冲区中读一行数据，如果缓冲区中有数据，则它把当前缓冲区中最先写入的数据读出，这就相当于完成了输入工作。

2.UTL_FILE程序包

UTL_FILE程序包功能是对本地操作系统的文件进行访问。在PL/SQL块中访问文件的能力是有限的，主要包括文件的打开、关闭、读、写等操作。在访问文件之前，必须先打开文件，这时系统将返回一个文件标识，对文件的读、写等操作都是通过这个文件标识进行的。文件访问完后，还应该及时关闭文件。

UTL_FILE程序包中与文件的打开和关闭操作有关的函数和过程有：

·FOPEN

·IS_OPEN

·FCLOSE

·FCLOSE_ALL

在访问文件之前，首先要用函数FOPEN打开文件。这个函数的调用格式为：

FOPEN（目录，文件名，打开模式）

其中目录为文件所在的位置，它文件名一起确定了要访问的文件。打开模式是指以什么样的方式打开文件。UTL_FILE包规定了三种打开模式：

·r：只读方式，用于读出文件的内容。

·w：写方式，用于向文件中写入数据。

·a：追加方式，用于在文件末尾写入数据。

UTL_FILE程序包访问文件的功能很有限，并不是所有的文件都可以访问。利用这个程序包只能访问指定目录中的文本文件。为了访问某个目录中的文件，必须通过初始化参数指定这个目录，指定的格式为：

UTL_FILE_DIR=目录

例如，为了访问目录/home/oracle中的文件，需要在参数文件中添加一下内容：

UTL_FILE_DIR=/home/oracle

如果没有初始化参数指定目录，那么在访问一个目录中的文件时，Oracle将抛出一个预定义的异常UTL_FILE.INVALID_PATH。如果要访问所有目录中的文件，可以用“*”代表任何目录，代替上面某个具体目录，但这种作法是不提倡的，因为这将带来安全隐患。

如果文件打开成功，FOPEN函数将返回一个FILE_TYPE类型的文件标识，以后对文件的访问就是通过这个文件标识进行的。FILE_TYPE类型是在UTL_FILE包中定义的类型，用户可以直接使用。

函数IS_OPEN用于判断一个文件是否已经被打开，它只有一个参数，就是FOPEN函数返回的文件标识。如果文件已经被打开，函数IS_OPEN将返回真值，否则返回假值。

文件访问结束后，应该调用过程FCLOSE关闭文件。这个过程只有一个参数，就是FOPEN函数返回的文件标识。如果打开了多个文件，可以调用过程FCLOSE_ALL关闭所有文件，这个过程没有任何参数。

与文件的读操作有关的过程为GET_LINE，它的调用格式为：

GET_LINE（文件标识，变量）

其中文件标识就是用函数FOPEN打开文件时的返回值。变量是一个字符串类型的变量，用于存放从文件中读到的数据。因为对文件的读操作是以行为单位进行的，所以这个变量要能够存放文件中的一行数据。如果一个文件是空的，或者当前已经读到了文件末尾，这时系统将抛出异常NO_DATA_FOUND。下面的例子演示了文件的打开、读和关闭操作。文件打开后将它的第一行数据读出，并输出到显示器上。

DECLARE

fp UTL_FILE.FILE_TYPE；

line varchar2（100）；

BEGIN

fp：=utl_file.fopen（'/home/oracle'，'sqlnet.log'，'r'）；

utl_file.get_line（fp, line）；

dbms_output.put_line（line）；

utl_file.fclose（fp）；

END；

对文件的写操作涉及的过程较多，这里仅介绍用得最多的过程PUT_LINE。这个过程以行的形式将数据写入文件，每写入一行，就在行的末尾添加一个换行符，它的调用形式为：

PUT_LINE（文件标识，变量）

这个过程将变量中的数据写入文件标识所代表的文件中。在下面的例子中，首先以只读方式打开第一个文件，然后以追加方式打开第二个文件。从第一个文件中读一行数据到变量中，在这行数据的前后各添加一个“#”后再写入第二个文件，最后关闭两个文件。

DECLARE

fp1 UTL_FILE.FILE_TYPE；

fp2 UTL_FILE.FILE_TYPE；

line varchar2（100）；

BEGIN

fp1：=utl_file.fopen（'/home/oracle'，'sqlnet.log'，'r'）；

fp2：=utl_file.fopen（'/home/oracle'，'sqlnet.bak'，'a'）；

utl_file.get_line（fp1，line）；

utl_file.put_line（fp2，'#'||line||'#'）；

utl_file.fclose_all；

END；

3.DBMS_SQL包

在PL/SQL块中我们可以利用SELECT命令从数据库中检索数据，也可以利用INSERT、DELETE和UPDATE语句对数据库中的数据进行增加、删除、修改等操作。但是像创建表、删除表、修改表结构这样的操作在PL/SQL块中是不能直接完成的，也就是说，在PL/SQL块中不能直接执行CREATE、DROP、ALTER这样的DDL命令。如果要在PL/SQL块中进行这样的操作，就要借助于Oracle提供的程序包—DBMS_SQL。

DBMS_SQL包使得在PL/SQL包中执行DDL命令成为可能。利用DBMS_SQL包执行DDL命令时，首先要打开一个游标，然后通过这个游标执行DDL命令，最后关闭这个游标。

DBMS_SQL包提供了一系列的过程和函数，利用这些过程和函数可以完成所需的操作。用来打开游标的函数是OPEN_CURSOR，这个函数没有任何参数。如果游标打开成功，这个函数将返回一个整数，这个整数就是游标的标识。以后执行SQL语句就是通过这个游标的标识进行的。

对SQL语句进行分析的过程是PARSE，这个过程对SQL语句进行语法分析，将其与打开的游标进行关联，然后执行这条SQL语句。这个过程的调用格式为：

PARSE（游标标识，SQL语句，语言标志）

其中游标标识就是打开游标时的返回值。SQL语句是需要执行的DDL命令的完整形式。语言标志指定该过程以什么样的方式处理SQL语句，这个参数有三个可选值：

·DBMS_SQL. V6：采用Oracle6的方式处理SQL语句。

·DBMS_SQL. V6：采用Oracle7的方式处理SQL语句。

·DBMS_SQL. NATIVE：采用一般方式处理SQL语句。

SQL语句执行结束后，应该及时关闭游标。关闭游标的过程是CLOSE_CURSOR，这个过程只有一个参数，就是通过函数OPEN_CURSOR打开的游标的标识。

例如，在下面的PL/SQL块中，首先打开一个游标，游标的标识为cur_1，然后利用这个游标执行一条SQL语句，创建表t1，这个表有两个列，id和name。最后关闭这个游标。这个块的执行结果是在当前用户的模式中创建了一个表t1。

DECLARE

cur_1 integer；

str varchar2（100）；

BEGIN

str：='CREATE table t1（id integer, name varchar2（10））'；

cur_1：=dbms_sql.open_cursor；

dbms_sql.parse（cur_1，str, DBMS_SQL.V7）；

dbms_sql.close_cursor（cur_1）；

END；

如果已经有一个同名的表存在，上述PL/SQL块执行时将出错。为了向用户报告出错的情况，我们可以在PL/SQL块中捕捉错误，并进行异常处理，将出错的情况报告给用户，这样的PL/SQL块才算是一个完整的、健壮的程序。下面是增加了异常处理的PL/SQL块：

DECLARE

cur_1 integer；

str varchar2（100）；

already_exists EXCEPTION；

PRAGMA EXCEPTION_INIT（already_exists，-00955）；

BEGIN

str：='CREATE table t1（id integer, name varchar2（10））'；

cur_1：=dbms_sql.open_cursor；

dbms_sql.parse（cur_1，str, DBMS_SQL.V7）；

dbms_sql.close_cursor（cur_1）；

EXCEPTION

WHEN already_exists THEN

dbms_output.put_line（'需要创建的表已经存在'）；

END；

在这个块中定义了一个异常already_exists，然后将它与错误号-00955关联起来。错误号-00955代表的错误情况是指定的名称已经被其他对象使用。这样当发生这个错误时，系统将抛出异常already_exists。在块的最后，进行的异常的处理，将错误的情况显示给用户。

实际上，在PL/SQL块中还有一种执行DDL和DCL语句的方法，那就是把这样的语句作为EXECUTE IMMEDIATE命令的参数。EXECUTE IMMEDIATE命令的功能是执行动态的SQL语句，它的参数可以是一个变量或一个表示SQL语句的字符串，还可以是用“||”符号连接在一起的若干字符串等。例如：

sql_stmt：='CREATE TABLE t1（id number, name char（10））'；

EXECUTE IMMEDIATE sql_stmt；

EXECUTE IMMEDIATE'GRANT select, update on t1 to user1'；

EXECUTE IMMEDIATE'DROP TABLE t1'；

3.6.5　与存储程序有关的数据字典

在数据库中，存储过程、存储函数以及程序包的信息是存放在数据字典中的。与存储程序有关的数据字典有：

·user_procedures

·user_objects

·user_source

·user_errors

在数据字典user_procedures中存放的是当前用户所拥有的存储过程和存储函数信息。例如，要想查看当前用户所拥有的存储过程和存储函数，执行下面的SELECT语句：

SQL＞SELECT object_name, object_type, authid FROM user_procedures；

OBJECT_NAME OBJECT_TYPE AUTHID

EMPLOYEE PACKAGE DEFINER

EMPLOYEE PACKAGE DEFINER

TAX_PER_DEPART FUNCTION DEFINER

TOTAL_INCOME PROCEDURE DEFINER

数据字典user_objects用来存放当前用户所拥有的所有类型的数据库对象，包括表、视图、触发器、序列、存储过程、存储函数以及程序包等。如果要了解当前用户所拥有的数据库对象类型，可以执行下面的SELECT语句：

SQL＞SELECT distinct object_type FROM user_objects；

OBJECT_TYPE

------------------

FUNCTION

INDEX

PACKAGE

PACKAGE BODY

PROCEDURE

TABLE

从执行结果可以看出，在当前用户所拥有的模式中，有索引、表、存储过程、存储函数和程序包五种数据库对象。程序包的头部和包体的类型分别为PACKAGE和PACKAGE BODY。如果要查看某个数据库对象的详细信息，同样可以执行相应的SELECT语句。例如，以下SELECT语句用来查看对象“total_income”的详细信息：

SQL＞SELECT object_name, object_type, created, status FROM user_objects

WHERE object_name='TOTAL_INCOME'；

OBJECT_NAM OBJECT_TYPE CREATED STATUS

---------------------------------------------

TOTAL_INCOME FUNCTION 28-5月-10 VALID

数据字典user_source用来存放存储过程、存储函数和程序包的源代码。当然，这个视图的目的只是为了查看源代码，PL/SQL程序的执行并不是从这里开始的，因为程序在创建时已经经过了编译，在数据库中以二进制形式存储。因此，试图通过修改这个数据字典而达到修改存储程序的功能是行不通的。Oracle在创建PL/SQL程序时，将按照用户在编写时的自然格式，以行的形式存储程序代码，并记录每行的行号，所有代码行合起来就是该程序的源代码。例如，要查看函数tax_per_depart的源代码，可以执行下列SELECT语句：

SQL＞SELECT line, text FROM user_source WHERE name='TAX_PER_DEPART'；

LINE TEXT

------------------------------------------------------------

1 function tax_per_depart（dno integer）

2 RETURN number

3 as

4 result number；

5 BEGIN

6 SELECT sum（sal）*0.03 INTO result FROM emp

7 WHERE deptno=dno

8 GROUP BY deptno；

9 RETURN result；

10 END；

如果在创建存储过程、存储函数或者程序包时发生了语法错误，SQL*Plus将把错误信息在屏幕上显示，同时Oracle把错误信息记录在数据字典中。数据字典user_errors就是用来存放当前用户在创建存储程序时发生的错误的。例如，在创建存储函数total_income时，错把SELECT语句中的“WHERE deptno=dno”写成了“WHERE deptno=ddno”，于是发生了错误：

CREATE OR REPLACE FUNCTION total_income（dno emp.deptno%type）

RETURN number

as

result number；

BEGIN

SELECT sum（sal）INTO result FROM emp

WHERE deptno=ddno

GROUP BY deptno；

RETURN result；

END；

SQL＞/

警告：创建的函数带有编译错误。

为了确定发生的所有错误的位置，执行下列查询语句：

SQL＞SELECT sequence, line, position FROM user_errors

WHERE name='TOTAL_INCOME'；

SEQUENCE LINE POSITION

------------------------------------------------------------------------

1 7 14

2 6 1

可以看出，发生了两个错误，第一个位于第7行第14个字符处，第二个位于第6行第1个字符处。为了查看第一个错误的详细信息，需要检索TEXT列的数据：

SQL＞SELECT text FROM user_errors WHERE SEQUENCE=1；

TEXT

--------------------------------------------------------------------

PLS-00103：出现符号"END"在需要下列之一时：

BEGIN case DECLARE exit for

goto if loop mod null pragma raise RETURN SELECT UPDATE while

with＜an identifier＞＜a double-quoted delimited-identifier＞

＜a bind variable＞＜＜close current DELETE fetch lock INSERT

open rollback savepoint set sql execute commit forall merge

＜a single-quoted SQL string＞pipe

PL/SQL：ORA-00904："DDNO"：无效的标识符

根据这些错误信息很快便可以确定错误的原因，从而进行纠正。在很多情况下，发生的多个错误是由同一个原因引起的，只要修改了出现错误的程序代码，多个错误可能一起消失。这需要用户在编写程序的过程中不断积累经验。

SQL*Plus还提供了一种查看错误信息的简便方法，用show errors命令可以查看当前发生的错误，而不需要了解数据字典的详细结构。这个命令的用法为：

SQL＞show errors

命令执行的结果为：

FUNCTION TOTAL_INCOME出现错误：

LINE/COL ERROR

------------------------------------------------

6/1 PL/SQL：SQL Statement ignored

7/14 PL/SQL：ORA-00904："DDNO"：无效的标识符

或者在查看错误信息时指定发生错误的对象的类型和名称，如：

SQL＞show errors function total_income

在这种情况下，命令show errors的格式为：

SQL＞show errors　对象类型　对象名称

3.7　异常处理

用户编写的PL/SQL块在执行过程中不可避免地要发生一些错误。这里涉及的错误并不是由于程序的语法错误引起的，而是因为处理的数据超出了处理的范围而引发的错误。如果给这样的错误起一个名字，这就是异常。当PL/SQL块在执行过程中检测到一个错误时，就会抛出相应的异常。在块中应当处理这样的异常，否则会引起应用程序运行停止。

3.7.1　异常处理程序

异常一般是在PL/SQL程序执行错误时由数据库服务器抛出，也可以在PL/SQL块中由程序员在一定的条件下显式抛出。无论是哪种形式的异常，都可以在PL/SQL块的异常处理部分编写一段程序进行处理，如果不做任何处理，异常将被传递到调用者，由调用者统一处理。图3.2表示两种不同的异常处理方式。

图　3.2　异常处理和异常传递

如果要在PL/SQL块中对异常进行处理，就需要在异常处理部分编写处理程序。异常处理程序的形式如下：

EXCEPTION

WHEN 异常1 OR 异常2 THEN

异常处理程序1；

WHEN 异常3 OR 异常4 THEN

异常处理程序2；

WHEN OTHERS THEN

异常处理程序n

END

异常处理程序以关键字EXCEPTION开始，结束于关键字END。在这部分可以对多个异常分别进行不同的处理，也可以进行相同的处理。如果没有列出所有异常，可以用关键字OTHERS代替其他的异常，在异常处理程序的最后加上一条WHEN OTHERS子句，用来处理前面没有列出的所有异常。

如果PL/SQL块执行出错，或者遇到显式抛出异常的语句，则程序立即停止执行，转去执行异常处理程序。异常被处理结束后，整个PL/SQL块的执行便告结束。所以一旦发生异常，则在PL/SQL块的可执行部分中，从发生异常的地方开始，以后的代码将不再执行。

在PL/SQL块中有三种类型的异常，即预定义的异常、非预定义的异常和用户自定义的异常。下面分别介绍这几种异常的使用方法。

3.7.2　预定义的异常

Oracle把一些常见的错误定义为有名字的异常，这就是预定义的异常。Oracle有许多预定义的异常，在进行处理时不需要再定义，只需要编写相应的异常处理程序即可。当PL/SQL块执行发生错误时，数据库服务器将自动抛出相应的异常，并执行编写的异常处理程序。表3.5列出了部分预定义的异常。

其中前面的两个异常是最常见的异常。下面的代码演示如何处理这两个异常。

DECLARE

name emp.ename%type；

BEGIN

SELECT ename INTO name FROM emp WHERE deptno=100；--其实没有编号为100的部门

EXCEPTION--这里将引发NO_DATA_FOUND异常

WHEN NO_DATA_FOUND THEN

dbms_output.put_line（'没有满足条件的数据'）；

WHEN TOO_MANY_ROWS THEN

dbms_output.put_line（'太多的数据'）；

END；

这个块的执行结果为：

没有满足条件的数据

因为编号为100的部门不存在，所以PL/SQL程序在执行到这条SELECT语句时引发了NO_DATA_FOUND异常。但是如果对一个存在的部门进行查询，可能返回多行数据。例如，如果将where子句的条件改为“deptno=10”，因为部门10有多个员工，这时将返回多行数据，从而引发TOO_MANY_ROWS异常。由此可见，在PL/SQL程序通过传统的SELECT命令只能查询一行数据，如果查询0行或多行数据，都会引发异常。如果要对0行或多行数据的情况进行处理，就要用到游标了。

在向表的主键列上写入一个重复的值时将引发异常DUP_VAL_ON_INDEX。例如，在部门表中列deptno是主键列，这就要求这个列上的值不能重复。如果已经存在部门10，再向这个表插入一行数据，部门编号也为10，这时将引发异常DUP_VAL_ON_INDEX。例如：

BEGIN

INSERT INTO dept VALUES（10，'network'，'nowhere'）；

EXCEPTION

WHEN DUP_VAL_ON_INDEX THEN

dbms_output.put_line（'主键列上的值重复'）；

END；

这个块的执行结果为：

主键列上的值重复

在PL/SQL块的异常处理部分，由WHEN引导的代码即为异常处理程序。一般在一个PL/SQL块中有多个异常处理程序，分别用于处理不同的异常。但是一般只可能执行其中一段异常处理程序，因为当发生一个异常时，PL/SQL块的执行立即从可执行部分转入异常处理部分，当处理完异常后PL/SQL块的执行便宣告结束，这时将不会有别的异常出现。

一般针对一个异常可以编写一段单独的异常处理程序，也可以对多个异常编写同一段异常处理程序，如果发生不同的异常，可以进行同样的处理。这样在WHEN子句中可以指定多个异常的名字，相互之间用OR分隔。例如：

WHEN NO_DATA_FOUND OR TOO_MANY_ROWS THEN

dbms_output.put_line（'SELECT语句出错'）；

……

PL/SQL提供了两个函数，SQLCODE用于返回发生的错误的代码，SQLERRM用于返回错误的原因。有了这两个函数，就可以编写通用的异常处理程序，处理所有的异常。例如，将错误代码和错误信息显示给用户。例如：

BEGIN

INSERT INTO dept VALUES（10，'network'，'nowhere'）；

EXCEPTION

WHEN others THEN

dbms_output.put_line（'错误代码：'||SQLCODE）；

dbms_output.put_line（'错误原因：'||SQLERRM）；

END；

这个块的执行结果为：

错误代码：-1

错误原因：ORA-00001：违反唯一约束条件（SCOTT.PK_DEPT）

在上面的异常处理的例子中，我们仅仅把发生错误的信息显示出来。如果希望把所有发生的错误记录下来，可以创建一个表，在PL/SQL的异常处理部分把错误的情况写入这个表，生成日志信息。例如，在数据库中创建表err_info，它的结构如下：

列名　类型　为空　描述

err_time DATE NO　错误发生的时间

err_user VARCHAR（30）YES　因执行PL/SQL而引发错误的用户

err_code INTEGER YES　错误代码

err_message VARCHAR（100）YES　错误原因

这样在处理异常时，就可以直接将异常的情况写入这个表，而不用显示给用户了。如果要对所有的异常进行相同的处理，那么在异常处理部分就不需要分别列出每个异常，只要用OTHERS代替就可以了。例如：

DECLARE

name emp.ename%type；

err_code integer；

err_message varchar（100）；

BEGIN

SELECT ename INTO name FROM emp WHERE deptno=100；

EXCEPTION--这里将引发NO_DATA_FOUND异常

WHEN OTHERS THEN

err_code：=SQLCODE；

err_message：=SQLERRM；

INSERT INTO err_info VALUES（SYSDATE, USER, err_code, err_message）；

COMMIT；

END；

这个块的执行结果是将异常的信息记录在表error_info中。

引发异常的一个重要原因是处理数时发生错误。统计表明，SELECT语句、DML语句以及游标操作语句更容易引发异常。编写PL/SQL块的主要目的是处理数据，而PL/SQL块在逻辑上与数据是分开的，程序员根本无法预料数据的变化。例如，要查询部门10的员工，程序员根本不知道这个部门中有没有员工，有一个还是有多个员工。所以在编写程序时，程序员应该考虑各种可能出现的异常，在程序中编写这些异常的处理代码，这样的程序才能经受各种错误的考验。

3.7.3　非预定义异常

在PL/SQL中还有一类会经常遇到的错误。每个错误都有相应的错误代码和错误原因，但是由于Oracle没有为这样的错误定义一个名称，因而不能直接进行异常处理。在一般情况下，只能在PL/SQL块执行出错时查看其出错信息。

编写PL/SQL程序时，应该充分考虑到各种可能出现的异常，并且都作出适当的处理，这样的程序才是健壮的。对于这类非预定义的异常，由于它也被自动抛出的，因而只需要定义一个异常，把这个异常的名称与错误的代码关联起来，然后就可以像处理预定义异常那样处理这样的异常了。非预定义异常的处理过程如图3.3所示。

图　3.3　非预定义异常的处理过程

异常的定义在PL/SQL块的声明部分进行，定义的格式为：

异常名称 EXCEPTION

其中异常名称是用户自己定义的一个名字，此时它仅仅是一个符号，没有任何意义。只有把这个名称与某个错误代码关联起来以后，这个异常才代表这个错误。把异常的名称与错误代码进行关联的格式是：

PRAGMA EXCEPTION_INIT（异常名，错误代码）

这种关联也是在PL/SQL块的声明部分进行。这样这个异常的名字就代表这个特定的错误了，当PL/SQL程序在执行的过程中发生这个错误时，这个异常将被自动抛出，这时就可以对其进行处理了。

例如，错误代码-02292的含义是违反了关联完整性。如果两个表通过主键和外键建立了关联关系，这时要从主表中删除一行，就可能会违反它们之间的关联完整性。例如员工表emp与部门表dept通过列deptno建立了关联关系，如果要从部门表dept删除一行，必须保证EMP表中没有这个部门的员工，否则就违反了它们之间的关联完整性。下面的代码演示了试图删除部门10的数据时发生的错误，因为在表emp中还有部门10的员工。

SQL＞DELETE FROM dept WHERE deptno=10；

DELETE FROM dept WHERE deptno=10

*

ERROR位于第1行：

ORA-02292：违反完整约束条件（SCOTT.FK_DEPTNO）-已找到子记录日志

如果这时定义一个异常，把这个异常与错误代码-02292关联起来，就可以在PL/SQL块的异常处理部分对其进行处理了。下面是处理这个异常的代码。

DECLARE

reference_err EXCEPTION；

PRAGMA EXCEPTION_INIT（reference_err，-02292）；

BEGIN

DELETE FROM dept WHERE deptno=10；

EXCEPTION

WHEN reference_err THEN

dbms_output.put_line（'您所进行的操作违反了关联完整性'）；

END；

这个PL/SQL块的执行结果为：

您所进行的操作违反了关联完整性

3.7.4　用户自定义的异常

除了Oracle定义的两种异常外，在PL/SQL中还可以自定义异常。程序员可以把一些特定的状态定义为异常。这样的异常一般由程序员自己决定，在一定的条件下抛出，然后利用PL/SQL的异常机制进行处理。

对于用户自定义的异常，有两种处理方法。第一种方法是先定义一个异常，并在适当的时候抛出，然后在PL/SQL块的异常处理部分进行处理。用户自定义的异常一般在一定的条件下抛出，于是这个条件就成为引发这个异常的原因；第二种方法是向调用者返回一个自定义的错误代码和一条错误信息。

这里先介绍第一种方法。异常的定义在PL/SQL块的声明部分进行，定义的格式为：

异常名称 EXCEPTION

异常名称这时仅仅是一个符号，仅当在一定条件下抛出时，这个异常才有意义。抛出异常的命令是RAISE，异常的抛出在PL/SQL块的可执行部分进行。RAISE命令的格式为：

RAISE 异常名称

异常一般在一定的条件下抛出，因此RAISE语句通常跟在某个条件判断的后面，这样就把这个异常与这个条件关联起来了。抛出异常的原因可能是数据出错，也可能是满足了某个自定义的条件，处理自定义异常的方法与处理前两种异常的方法相同。

例如，编写一个PL/SQL程序，求1+2+3+……100的值。在求和的过程中如果发现结果超出了1000，则抛出异常，并停止求和。这个块的代码如下：

DECLARE

out_of_range EXCEPTION；--定义异常

result integer：=0；

BEGIN

for i in 1..100 loop

result：=result+i；

if result＞1000 then

RAISE out_of_range；--抛出异常

END if；

END loop；

EXCEPTION

WHEN out_of_range THEN--处理异常

dbms_output.put_line（'当前的计算结果为'||result||'，已超出范围'）；

END；

这个块的执行结果为：

当前的计算结果为1035，已超出范围

用RAISE命令不仅可以抛出一个自定义的异常，也可以抛出一个预定义异常和非预定义异常。例如，在上面求和的例子中，当计算结果超过1000时可以抛出异常VALUE_ERROR。修改后的PL/SQL块代码如下：

DECLARE

result integer：=0；

BEGIN

for i in 1..100 loop

result：=result+i；

if result＞1000 then

RAISE VALUE_ERROR；--当条件满足时抛出一个预定义的异常

END if；

END loop；

EXCEPTION

WHEN VALUE_ERROR THEN

dbms_output.put_line（'当前的计算结果为'||result||'，已超出范围'）；

END；

这个块的执行结果为：

当前的计算结果为1035，已超出范围

现在再来介绍自定义异常处理的第二种方法。当PL/SQL块的执行满足一定的条件时，可以向PL/SQL程序返回一个错误代码和一条错误信息。错误代码的范围是-20000到-20999，这个范围的代码是Oracle保留的，本身没有任何意义。程序如果把一个错误代码与某个条件关联起来，那么在条件满足时系统将引发这样的错误。当然这是人为制造的一种错误，并不表示程序或数据真正出现了错误。

PL/SQL提供了一个过程，用于向PL/SQL程序返回一个一个错误代码和一条错误信息。这个过程是RAISE_APPLICATION_ERROR，过程的调用格式为：

RAISE_APPLICATION_ERROR（错误代码，错误信息）

例如，对上面求和的例子加以修改，当计算结果大于1000时，PL/SQL程序便得到一个错误代码-20001和一条错误信息。修改后的代码如下：

DECLARE

result integer：=0；

BEGIN

for i in 1..100 loop

result：=result+i；

if result＞1000 then

RAISE_APPLICATION_ERROR（-20001，'当前的计算结果为'||result||'，已超出范围'）；

END if；

END loop；

END；

这个块的执行结果为：

DECLARE

*

ERROR位于第1行：

ORA-20001：当前的计算结果为1035，已超出范围

ORA-06512：在line 7

从程序运行的结果来看，程序的执行过程确实发生了错误，返回了指定的错误代码和错误信息。在这一点上用户自定义的异常与非预定义异常是相似的。只不过非预定义异常是由数据库服务器自动抛出的，并且错误代码和错误信息都是由数据库服务器指定的，而用户自定义的异常是由程序员抛出的，错误代码和错误信息都是由程序员指定的。

在处理非预定义异常时，我们为每个错误代码指定了一个异常名称，然后就可以根据这个名称进行异常处理。既然用户自定义的异常也可以向调用者返回错误代码和错误信息，那么我们也可以采用同样的方法处理这样的异常。

首先定义一个异常，然后把这个异常与某个错误代码关联起来。这两步都在PL/SQL块的声明部分进行。然后在PL/SQL程序的可执行部分根据一定的条件，抛出这个异常。最后在PL/SQL块的异常处理部分捕捉并处理这个命名的异常。例如，用这种方法重新处理上述求和的例子中的异常，代码如下：

DECLARE

result integer：=0；

out_of_range EXCEPTION；

PRAGMA EXCEPTION_INIT（out_of_range，-20001）；

BEGIN

for i in 1..100 loop

result：=result+i；

if result＞1000 then

RAISE_APPLICATION_ERROR（-20001，'当前的计算结果为'||result||'，已超出范围'）；

END if；

END loop；

EXCEPTION

WHEN out_of_range THEN

dbms_output.put_line（'错误代码：'||SQLCODE）；

dbms_output.put_line（'错误信息：'||SQLERRM）；

END；

这个块的执行结果为：

错误代码：-20001

错误信息：ORA-20001：当前的计算结果为1035，已超出范围

从上述PL/SQL块可以看出，我们首先在声明部分定义了一个异常out_of_range，然后把这个异常与错误代码-20001关联起来，一旦程序在运行过程中发生了这个错误，就是抛出了异常out_of_range。在块的可执行部分，如果在累加的过程中变量result的值超过了1000，则返回错误代码-20001以及相应的错误信息。这样在异常处理部分就可以捕捉并处理异常out_of_range了。

在处理用户自定义的异常时，也可以使用函数SQLCODE和SQLERRM，这两个函数分别用于返回指定的错误代码和错误信息。从程序的运行结果可以看出，这两个函数确实返回了指定的错误代码和错误信息。这样的错误代码和错误信息是在可执行部分通过过程RAISE_APPLICATION_ERROR指定的。

3.7.5　异常的传递

如果PL/SQL程序在执行的过程中发生了错误，则转去执行相应的异常处理程序，然后结束块的执行。如果没有定义相应的异常处理程序，那么PL/SQL程序将向调用者返回出错的相关信息，也就是把异常传递到程序的调用者，然后结束程序的执行。如果这个程序是在SQL*Plus中执行的，那么异常就会传递到SQL*Plus环境，从而把错误信息显示在屏幕上。例如，下面的块在检索数据时引发了TOO_MANY_ROWS异常，并把异常传递到SQL*plus中。

DECLARE

name emp.ename%type；

BEGIN

SELECT ename INTO name FROM emp WHERE deptno=10；

EXCEPTION--这里将引发TOO_MANY_ROWS异常

when NO_DATA_FOUND then

dbms_output.put_line（'没有满足条件的数据'）；

END；

这个块的执行结果为：

DECLARE

*

ERROR位于第1行：

ORA-01422：exact fetch RETURNs more than requested number of rows

ORA-06512：at line 4

从程序的执行结果可以看出，由于在程序中没有处理异常TOO_MANY_ROWS，所以这个异常被传递到程序的调用者—SQL*Plus中。

在PL/SQL块中可以定义过程、函数等形式的子程序，在每个子程序中也可以分别定义异常处理程序。这样当子程序执行出现错误时，就转去执行相应的异常处理程序。然后子程序的执行便告结束，PL/SQL块接着从子程序调用处的下一条语句开始执行。如果子程序对出现的异常进行了处理，就可以认为子程序的执行正常结束。例如，再来考虑子程序重载的这个例子。在这个块中定义了两个重载过程increase_salary，用来对员工增加工资。第一个过程有两个参数，分别是部门编号和增加的额度，用于对指定的部门的员工增加工资。第二个过程带有一个参数，即增加的额度，用于对所有员工增加工资。

这里在第一个过程中添加了处理异常NO_DATA_FOUND的程序，还添加了一条SELECT语句。如果在调用过程时指定了一个不存在的部门，那么在查询该部门信息时将引发NO_DATA_FOUND异常，这个过程的执行流程就会转到异常处理部分。

DECLARE

procedure increase_salary（d_no emp.deptno%type, amount float）

is

d_name dept.dname%type；

BEGIN

SELECT dname INTO d_name FROM dept--其实部门100不存在

WHERE deptno=d_no；

UPDATE emp set sal=sal+amount WHERE deptno=d_no；

EXCEPTION

WHEN NO_DATA_FOUND THEN

dbms_output.put_line（'这个部门不存在'）；

END；

procedure increase_salary（amount float）

is

BEGIN

UPDATE emp set sal=sal+amount；

END；

BEGIN

increase_salary（100，100.50）；--调用第一个increase_salary过程

increase_salary（200）；--调用第二个increase_salary过程

END；

这个部门不存在

从块的执行结果可以看出，当调用第一个重载过程时，因为传递了一个不存在的部门编号，所以引发了NO_DATA_FOUND异常。这个过程在处理异常后便执行结束，PL/SQL块接着执行第二条调用语句，调用第二个过程。第一个过程因为处理了出现的异常，所以可以认为是正常结束，它并不会影响块整个程序中其他语句的执行。

对异常的处理应当遵循“不扩散”的原则。在子程序中发生的错误应该在子程序中进行处理，不要扩散到主程序中。同样，在PL/SQL块的可执行部分出现的错误应该在块中进行处理，不要扩散到调用该块的SQL*Plus或应用程序中。

如果在子程序中没有处理出现的错误，情况会怎么样呢？再来考虑上面的例子，我们取消了第一个increase_salary过程中的异常处理部分。为了便于测试，我们在两条调用语句中间添加了一条输出语句。修改后的代码如下：

DECLARE

procedure increase_salary（d_no emp.deptno%type, amount float）

is

d_name dept.dname%type；

BEGIN

SELECT dname INTO d_name FROM dept

WHERE deptno=d_no；

UPDATE emp set sal=sal+amount WHERE deptno=d_no；

END；

procedure increase_salary（amount float）

is

BEGIN

UPDATE emp set sal=sal+amount；

END；

BEGIN

increase_salary（100，100.50）；--调用第一个increase_salary过程

dbms_output.put_line（'第一个过程执行结束'）；

increase_salary（200）；--调用第二个increase_salary过程

END；

这个块的执行结果为：

DECLARE

*

ERROR位于第1行：

ORA-01403：no data found

ORA-06512：at line 6

ORA-06512：at line 16

在调用第一个increase_salary过程时，由于指定了一个不存在的部门编号，所以引发了异常NO_DATA_FOUND。在子程序中没有处理这个异常，所以过程非正常结束。从程序的执行结果可以看出，我们指定的输出并没有产生，可以断定，第一条调用语句以下的所有语句都没有得到执行。

如果子程序没有处理出现的错误，那么异常就被传递到它的调用者，即PL/SQL主程序，从而在主程序中也会产生错误。所以主程序将在调用子程序的地方停止执行，而去处理这个异常。但是因为主程序也没有定义异常处理程序，所以这个异常又被传递到块的调用者—SQL*Plus，从而在屏幕上显示出错的信息。

从子程序中传递到PL/SQL主程序中的异常，能不能在主程序中进行处理呢？答案是肯定的。如果在主程序中定义了异常处理程序，那么异常被从子程序传递到主程序中后，就像在主程序中产生的异常一样进行处理。这样我们可以在主程序中编写统一的异常处理程序，无论异常是在主程序中抛出的，还是在子程序中抛出的，都可以得到同样的处理。这种做法虽然是可行的，但是它不符合“不扩散”原则。如果程序出现了异常，不容易确定是什么地方出现了错误，也无法对程序的不同部分产生的异常进行单独的处理。例如，把上述例子中第一个increase_salary过程的异常处理放在PL/SQL块中。如果调用过程时引发了异常，便可以进行处理。修改后的代码如下（这里去掉了第二个过程）：

DECLARE

procedure increase_salary（d_no emp.deptno%type, amount float）

is

d_name dept.dname%type；

BEGIN

SELECT dname INTO d_name FROM dept

WHERE deptno=d_no；

UPDATE emp set sal=sal+amount WHERE deptno=d_no；

END；

BEGIN

increase_salary（100，100.50）；

EXCEPTION

WHEN NO_DATA_FOUND then

dbms_output.put_line（'这是在过程中产生的异常'）；

END；

这个块的执行结果为：

这是在过程中产生的异常

从上述执行结果可以看出，从子程序中传递到主程序的异常确实可以在主程序中进行处理。但是在主程序中也可能产生同样的异常，这时如果输出同样的信息就不合适了。

从上面的例子可以看出，在主程序、各个子程序之中可能会因为不同的原因引发同一个异常。为了方便地确定异常产生的原因，应该在PL/SQL程序的每部分都定义异常处理程序。

3.8　游标的应用

游标是一种私有的工作区，用于保存SQL语句的执行结果。在执行一条SQL语句时，数据库服务器会打开一个工作区，将SQL语句的执行结果保存在这里。

在Oracle数据库中有两种形式的游标：隐式游标和显式游标。隐式游标是由数据库服务器定义的，显式游标是用户根据需要自己定义的。

3.8.1　隐式游标

隐式游标是数据库服务器定义的一种游标。在执行一条DML语句或SELECT语句时，数据库服务器将自动打开一个隐式游标，存放该语句的执行结果。在一个PL/SQL块中可能有多条DML或SELECT语句，隐式游标始终存放最近一条语句的执行结果。

隐式游标有几个很有用的属性，可以帮助我们了解游标的信息。表3.6列出了隐式游标的几个常用属性。

其中SQL%FOUND属性值为布尔值，表示是否找到了满足条件的数据，如果找到了相应的数据，其值为TRUE，否则为FALSE。SQL%ROWCOUNT属性表示某个操作影响的数据行数，对于UPDATE语句，表示修改的行数；对于INSERT语句，表示插入的行数；对于DELETE语句，表示被删除的行数。例如，可以在每一条DML语句之后输出该操作影响的行数。

BEGIN

UPDATE emp set sal=sal+100；

if SQL%FOUND then

dbms_output.put_line（'被修改的行数：'||SQL%ROWCOUNT）；

END if；

DELETE FROM emp；

if SQL%FOUND then

dbms_output.put_line（'被删除的行数：'||SQL%ROWCOUNT）；

END if；

INSERT INTO dept VALUES（70，'aaa'，'aaaa'）；

INSERT INTO dept VALUES（80，'bbb'，'bbbb'）；

if SQL%FOUND then

dbms_output.put_line（'最近插入的行数：'||SQL%ROWCOUNT）；

END if；

ROLLBACK；

END；

这个块的执行结果为：

SQL＞/

被修改的行数：14

被删除的行数：14

最近插入的行数：1

PL/SQL 过程已成功完成

从这个例子可以看出，当有多条DML语句时，隐式游标只记录最近一条DML语句的执行情况。如果是SELECT语句，情况则比较特殊。因为当SELECT语句没有检索到满足条件的数据时，将引发NO_DATA_FOUND异常，而当检索到多行满足条件的数据时，将引发TOO_MANY_ROWS异常。所以只有当SELECT语句正好检索到一行数据时，才可以使用隐式游标的这些属性。如果要处理这两种特殊情况，就需要借助于显式游标。

3.8.2　显式游标

对于PL/SQL块中的SELECT语句，可以用显式游标来处理。显式游标是一个打开的工作区，在这个工作区里保存SELECT语句的执行结果。用显式游标可以处理返回0行、一行、多行等各种情况，并且在返回0行或多行数据这两种特殊情况下，不会引发NO_DATA_FOUND和TOO_MANY_ROWS异常。使用游标处理SELECT语句的步骤如下：

1）声明游标

2）打开游标

3）逐行取出游标中的行，并分别进行处理

4）关闭游标

游标的工作过程如图3.4所示。

图　3.4　游标的工作过程

游标的声明在PL/SQL块的声明部分进行。声明的语法格式为：

DECLARE

CURSOR游标名IS

SELECT语句；

例如，下面的语句声明了一个名为cur_1的游标：

DECLARE

CURSOR cur_1 IS

SELECT*FROM emp；

由于SELECT语句的执行结果将存放在工作区中，因此不需要使用INTO子句将返回的数据赋给变量。

为了处理游标中的数据，首先要打开游标。打开游标意味着将指定的SELECT语句交给数据库服务器执行，并将返回结果存放在工作区中。打开游标的命令是OPEN，其语法格式为：

OPEN游标名

例如，打开游标cur_1的语句为：

OPEN cur_1；

游标打开后，就可以取出游标中的数据，并对其进行处理了。从游标中取出数据的命令是FETCH。FETCH命令一次取出一行数据，并将其赋给指定的变量。FETCH命令的格式为：

FETCH 游标 INTO 变量1，变量2，……

游标中的数据只有在取出后才能进行处理。为此，需要在FETCH语句中用INTO子句指定多个变量，分别存放一行数据中各个列的值。FETCH命令将一行数据中各列的值依次赋给指定的变量。需要注意的是，变量的类型、数目要与游标中一行的各列相对应。

在用FETCH命令取出游标中的数据时，可以设想有一个指针，指向游标中的一行数据。当游标刚刚打开时，指针指向第一行，以后每取出一行，指针自动指向下一行，直到将所有的数据都取出为止。用FETCH命令取数据的过程如图3.5所示。

游标在使用完后，应该及时关闭，以释放它所占用的内存空间。关闭游标的命令是CLOSE，其语法格式为：

CLOSE 游标名

当游标关闭后，不能再从游标中获取数据。如果需要，可以再次打开游标。

考虑下面的游标。从emp表中检索员工7902的姓名、工资、工作时间。由于SELECT命令仅返回一行数据，所以处理的过程很简单。

图　3.5　用FETCH命令取出游标中的数据的过程

DECLARE

name emp.ename%type；

salary emp.sal%type；

hire_date emp.hiredate%type；

CURSOR cur_1 IS

SELECT ename, sal, hiredate FROM emp WHERE empno=7902；

BEGIN

open cur_1；

fetch cur_1 INTO name, salary, hire_date；

dbms_output.put_line（'姓名：'||name||'工资：'||salary||'工作时间：'||hire_date）；

close cur_1；

END；

这个块的执行结果为：

姓名：FORD　工资：3000　工作时间：03-DEC-81

为了使程序更加简洁，在PL/SQL块中可以使用记录变量。首先声明一个记录变量，它的结构与游标的结构相同。然后可以使用FETCH语句将游标中的一行数据取出后存放在记录变量中，接下来就可以对这个记录变量进行处理了。例如，对上面的PL/SQL块进行修改，在PL/SQL块中使用记录变量。修改后的代码如下：

DECLARE

CURSOR cur_2 IS

SELECT ename, sal, hiredate FROM emp WHERE empno=7902；

e cur_2%rowtype；

BEGIN

open cur_2；

fetch cur_2 INTO e；

dbms_output.put_line（'姓名：'||e.ename||'工资：'||e.sal||'工作时间：'||e.hiredate）；

close cur_2；

END；

这个块的执行结果与使用简单变量时的结果完全相同。

在上面的块中通过一个简单的游标，处理一行数据。在使用游标时，必须考虑各种特殊情况。如果SELECT语句没有返回结果，游标是空的，这时FETCH语句将取不到数据。如果SELECT语句返回多行数据，这时用一条FETCH语句仅能取到游标中的一行数据。利用游标的属性可以了解游标当前的状态，防止各种意外情况的发生。表3.7列出了显式游标的若干属性。

在下面的PL/SQL块中使用了显式游标的属性，使得PL/SQL块能够处理各种例外情况，比如没有取到合适的数据，或者取到多行数据。

DECLARE

CURSOR cur_3 IS

SELECT ename, sal, hiredate FROM emp WHERE deptno=20；

e cur_3%rowtype；

BEGIN

if not cur_3%ISOPEN then--如果游标没有打开，则打开它

open cur_3；

END if；

fetch cur_3 INTO e；--取出第一行数据

while cur_3%found loop

dbms_output.put_line（'姓名：'||e.ename||'工资：'||e.sal||'工作时间：'||e.hiredate）；

fetch cur_3 INTO e；

END loop；

dbms_output.put_line（'员工总数：'||cur_3%rowcount）；--获取的总行数

close cur_3；--关闭游标

END；

这个块的执行结果为：

姓名：SMITH　工资：800　工作时间：17-12月-80

姓名：JONES　工资：2975　工作时间：02-4月-81

姓名：SCOTT　工资：3000　工作时间：19-4月-87

姓名：ADAMS　工资：1100　工作时间：23-5月-87

姓名：FORD　工资：3000　工作时间：03-12月-81

员工总数：5

这个PL/SQL块的功能是查询部门20的所有员工的姓名、工资和工作时间。如果这个部门不存在，则不显示任何员工的信息，仅显示“员工总数：0”的信息。如果该部门有一个或多个员工，则显示他们的信息，并打印该部门员工总数。

在块中首先用游标的%ISOPEN属性判断游标是否打开，如果没有打开，则打开它。然后用FETCH语句取出第一行，并用游标的%FOUND属性判断是否取到数据。如果游标是空的，则这个属性的值为FALSE，这样就不用继续取数据了。如果取到了数据，则处理这行数据，并试图取下一行。这样通过循环的方式，每取到一行数据，就试图再取下一行，然后判断是否取到数据，直到将所有数据取出。

在游标的四个属性中，%ISOPEN属性用于测试游标的状态。其他三个属性用来测试FETCH命令的执行结果，%FOUND和%NOTFOUND属性用来测试最近的一次FETCH是否取到数据，%ROWCOUNT属性表示自游标打开以来，到目前为止，用FETCH命令获取的行数。

下面的例子用另外一种形式的循环处理游标。

DECLARE

CURSOR cur_4 IS

SELECT ename, sal, hiredate FROM emp WHERE deptno=20；

e cur_4%rowtype；

BEGIN

if not cur_4%ISOPEN then--如果游标没有打开，则打开它

open cur_4；

END if；

loop

fetch cur_4 INTO e；

EXIT WHEN cur_4%NOTFOUND；--如果最近一次FETCH没有取到数据，则退出循环，否则，对取到的数据进行处理

dbms_output.put_line（'姓名：'||e.ename||'工资：'||e.sal||'工作时间：'||e.hiredate）；

END loop；

dbms_output.put_line（'员工总数：'||cur_4%rowcount）；--获取的总行数

close cur_4；--关闭游标

END；

游标中的数据一般是通过循环方式来处理的。在上面两个例子中定义了两个游标cur_3和cur_4，并用常规的循环方法进行处理。PL/SQL提供了一种更简便的方法处理游标，这种方法利用FOR循环，逐行处理游标中的行。FOR语句的格式为：

FOR变量IN游标名LOOP

处理变量

END LOOP；

这里把游标中的数据当做一个集合，一次从中取出一行，赋给一个记录类型变量，然后就可以处理这个变量了。这个变量在使用之前不需要定义，在循环开始时自动产生，在FOR语句中可以直接使用，这个变量的结构与游标的结构完全相同。

利用FOR循环从游标中取数据时，不需要用OPEN命令打开游标。当循环开始执行时，游标被自动打开。游标在使用完后，也不需要执行CLOSE命令关闭。

FOR循环的循环体每执行一次，就会自动取出游标中的一行数据，赋给记录类型变量，然后指针自动往下移动，所以不需要通过FETCH命令获取游标中的数据。例如，下面的PL/SQL块利用FOR循环处理游标中的数据。

DECLARE

CURSOR cur_5 IS

SELECT ename, sal, hiredate FROM emp WHERE deptno=20；

e_count integer：=0；

BEGIN

for e in cur_5 loop

dbms_output.put_line（'姓名：'||e.ename||'工资：'||e.sal||'工作时间：'||e.hiredate）；

e_count：=e_count+1；

END loop；

dbms_output.put_line（'员工总数：'||e_count）；

END；

由于在循环执行结束时，游标已经关闭，因此无法再用游标的%rowcount属性统计获取的数据行数。在块中声明了一个变量e_count，每进行一次循环，变量e_count加1，这样就可以统计出获取的总行数。

从上面的例子可以看出，利用FOR循环可以大大简化游标的处理过程。

需要注意的是，为了重点说明游标的用法，在与游标有关的例子中，对于从游标中取出的数据，仅仅显示在屏幕上。读者可以根据需要，对这些数据进行其他的处理，比如写入其他表中。

3.8.3　带参数的游标

在前面介绍游标的例子中，SELECT语句都没有WHERE子句，或者用WHERE子句指定了一个固定的条件，这样每次都查询同样的数据。在更多的情况下，可能要根据实际情况查询不同的数据。为了通过游标对数据进行更加灵活的处理，可以为游标定义参数，这些参数可以用在WHERE子句中。在打开游标时，指定实际的参数值，这样游标在每次打开时，可以根据不同的实际参数值，返回所需的不同数据。

定义带参数的游标的语法格式为：

DECLARE

CURSOR 游标名（参数1，参数2，……）

IS

SELECT语句；

其中参数的定义方法与子程序中的参数定义完全相同，可以指定默认值，指定参数传递模式。默认的参数传递模式为IN，如果要使用OUT或者“IN OUT”模式，就需要明确指定。由于游标一般不需要通过参数向调用者传递数据，所以OUT模式在游标中没有什么实际用处。

在用OPEN命令打开游标时，要向游标提供实际参数，游标根据提供的参数值，查询符合条件的数据。打开游标的语法格式为：

OPEN游标名（实际参数1，实际参数2……）；

例如，考虑在下面定义的游标。

DECLARE

CURSOR cur_6（d_no IN emp.deptno%type, min_sal IN emp.sal%type：=1000）

IS

SELECT ename, sal, hiredate FROM emp

WHERE deptno=d_no and sal＞=min_sal；

e cur_6%rowtype；

BEGIN

if not cur_6%ISOPEN then--如果游标没有打开，则打开它

open cur_6（20，2000）；

END if；

fetch cur_6 INTO e；--取出第一行数据

while cur_6%found loop

dbms_output.put_line（'姓名：'||e.ename||'工资：'||e.sal||'工作时间：'||e.hiredate）；

fetch cur_6 INTO e；

END loop；

dbms_output.put_line（'员工总数：'||cur_6%rowcount）；--获取的总行数

close cur_6；--关闭游标

END；

这个块的执行结果为：

姓名：JONES　工资：2975　工作时间：02-4月-81

姓名：SCOTT　工资：3000　工作时间：19-4月-87

姓名：FORD　工资：3000　工作时间：03-12月-81

员工总数：3

在这个例子中，用传统的循环方法处理游标。首先定义了一个带参数的游标，参数d_no表示部门编号，min_sal表示最低工资，两个参数的传递模式都是IN。游标的功能是查询属于指定部门并且工资不低于指定值的所有员工。

在打开游标时，指定了两个实际参数20和2000，这样，检索出来的数据就是属于部门20，并且工资不低于2000的所有员工。如果再次以“open cur_6（10，3000）”的形式打开游标，那么检索到的数据就是属于部门10，并且工资不低于3000的员工。由此可见，带参数的游标在查询数据时更加灵活。

如果要用FOR循环处理游标中的数据，可以按照同样的方法定义游标。由于没有使用OPEN命令打开游标，所以实际参数在FOR语句中指定。这时FOR语句的格式为：

FOR 变量 IN 游标（实际参数1，实际参数2……）LOOP

……

END LOOP；

这样，在循环开始执行时，游标自动打开，并根据指定的实际参数查询数据。例如，用FOR循环处理带参数的游标，对上面的PL/SQL块进行修改，代码如下：

DECLARE

CURSOR cur_7（d_no emp.deptno%type, min_sal emp.sal%type）

IS

SELECT ename, sal, hiredate FROM emp

WHERE deptno=d_no and sal＞=min_sal；

e_count integer：=0；

BEGIN

FOR e IN cur_7（10，3000）LOOP

dbms_output.put_line（'姓名：'||e.ename||'工资：'||e.sal||'工作时间：'||e.hiredate）；

e_count：=e_count+1；

END LOOP；

dbms_output.put_line（'员工总数：'||e_count）；

END；

这个块的执行结果为：

姓名：KING　工资：5000　工作时间：17-11月-81

员工总数：1

3.8.4　如何通过游标修改表中的数据

游标的主要作用是查询数据，并对数据逐行进行处理。对于游标中的数据还可以根据需要进行修改，例如修改某个列的值，或者删除某一行。在定义游标时，需要把游标定义为可以修改的形式，定义格式如下：

DECLARE

CURSOR 游标名（参数1，参数2……）

IS

SELECT语句

FOR UPDATE；

游标可以带参数，或者不带参数。SELECT语句中的FOR UPDATE子句的作用是加锁，它的功能是把游标中的数据锁定，这样可以防止其他用户同时修改这些数据。由于在并发环境中许多用户可能同时访问数据库，如果把游标打开后不希望其他用户同时修改这些数据，就需要对游标加锁，否则就会导致数据的不一致。SELECT语句中的FOR UPDATE子句就是为了在游标打开后对它进行加锁，待游标关闭时再释放锁，用这种方式可以保证用户对数据的正确访问。

对游标中的数据是逐行处理的，每次处理指针当前指向的行。在修改游标中的数据时，也是对当前行进行修改，然后将修改后的结果写入数据库。PL/SQL提供了一种修改游标当前行的机制，如果在UPDATE、DELETE语句中使用WHERE CURRENT OF子句，可以保证只对游标当前行进行修改。WHERE CURRENT OF子句将修改操作限定在游标的当前行。

例如，某部门要为员工增加工资，增加的幅度为10%，但只限于工资最低的5名员工。如果本部门员工总数不足5人，则为所有员工都增加工资。考虑用下面的PL/SQL块实现这个操作。

DECLARE

CURSOR cur_8（d_no emp.deptno%type）

IS

SELECT ename, sal, hiredate FROM emp

WHERE deptno=d_no

ORDER BY sal--保证员工按照工资从低到高的顺序排列

FOR UPDATE；

e_count integer：=0；

e cur_8%rowtype；

BEGIN

open cur_8（30）；--打开游标，将部门编号30作为参数

fetch cur_8 INTO e；

while cur_8%FOUND loop

exit when e_count＞=5；

UPDATE emp set sal=sal*1.1 WHERE CURRENT OF cur_8；

e_count：=e_count+1；

fetch cur_8 INTO e；

END loop；

dbms_output.put_line（'增加工资的员工人数：'||e_count）；

close cur_8；

END；

这个块的运行结果为：

增加工资的员工人数：5

再来考虑下面的PL/SQL块，它的功能同样是为某部门工资最低的5名员工增加工资。不过现在要用FOR循环来处理游标中的行。

DECLARE

CURSOR cur_9（d_no emp.deptno%type）

IS

SELECT ename, sal, hiredate FROM emp

WHERE deptno=d_no

ORDER BY sal--保证员工按照工资从低到高的顺序排列

FOR UPDATE；

e_count integer：=0；

BEGIN

for e in cur_9（30）loop

exit when e_count＞=5；

UPDATE emp set sal=sal*1.1 WHERE CURRENT OF cur_9；--修改员工工资

e_count：=e_count+1；

END loop；

dbms_output.put_line（'增加工资的员工人数：'||e_count）；

END；

通过WHERE CURRENT OF子句，不仅可以修改游标的当前行，还可以删除游标的当前行，实际的结果是从数据库中删除了当前行。例如，要删除游标cur_9的当前行时，可以使用以下语句：

DELETE FROM emp WHERE CURRENT OF cur_9；

3.9　触发器

触发器是一种特殊的存储过程，它在创建后就存储在数据库中。触发器的特殊性在于它是建立在某个具体的表之上的，而且是自动激发执行的，如果用户在这个表上执行了某个DML操作（UPDATE、INSERT、DELETE），触发器就被激发执行。

触发器常用于自动完成一些数据库的维护工作。例如，触发器可以具有以下功能：

·可以对表自动进行复杂的安全性、完整性检查。

·可以在对表进行DML操作之前或之后进行其他处理。

·进行审计，可以对表上的操作进行跟踪。

·实现不同节点间数据库的同步更新。

3.9.1　触发器的使用

触发器是依附于某个具体的表的特殊存储过程，它在某个DML操作的激发下自动执行。在创建触发器时应该仔细考虑如它的相关信息。具体地说，应该考虑以下几个方面的问题：

1）触发器应该建立在哪个表之上。

2）触发器应该对什么样的DML操作进行响应。

3）触发器在指定的DML操作之前激发还是在之后激发。

4）对每次DML响应一次，还是对受DML操作影响的每一行数据都响应一次。

在确定了触发器的实现细节后，现在就可以创建触发器了。创建触发器的语法格式为：

CREATE[OR REPLACE]TRIGGER触发器

BEFORE|AFTER|INSTEAD OF

DELETE|INSERT|UPDATE[OF列名]

ON表名

[FOR EACH ROW[WHEN条件]]

BEGIN

PL/SQL语句；

END；

用户如果要在自己的模式中创建触发器，需要具有CREATE TRIGGER系统权限。如果希望能够在其他用户的模式中创建触发器，需要具有CREATE ANY TRIGGER系统权限。

在创建触发器的语法结构中，用方括号限定的部分是可选的，可以根据需要选用。创建触发器的命令是CREATE TRIGGER，根据指定的名字创建一个触发器。OR REPLACE子句的作用是如果已经存在同名的触发器，则删除它，并重新创建。

触发器可以是前激发的（BEFORE），也可以是后激发的（AFTER）。如果是前激发的，则触发器在DML语句执行之前激发。如果是后激发的，则触发器在DML语句执行之后激发。用BEFORE关键字创建的触发器是前激发的，用AFTER关键字创建的触发器是后激发的，这两个关键字只能使用其一。INSTEAD OF子句仅用于视图上的触发器。

触发器可以被任何DML命令激发，包括INSERT、DELETE和UPDATE。如果希望其中的一种、两种或三种命令能够激发该触发器，则可以指定它们之间的任意组合，两种不同命令之间用空格分开。如果指定了UPDATE命令，还可以进一步指定当表中的哪个列受到UPDATE命令的影响时激发该触发器。

当在指定的表上执行指定的DML命令时，将会激发触发器，触发器将对这样的操作进行必要的响应。触发器可能对每次单独的DML操作响应一次，也可能对每次DML操作所影响的每一行数据响应一次。如果对每次单独的DML操作响应一次，触发器执行的次数与受影响的行数无关，这样的触发器叫做语句级触发器。如果对受影响的每一行数据都响应一次，那么触发器执行的次数等于受影响的行数，这样的触发器叫做行触发器。

FOR EACH ROW子句的作用是指定创建的触发器为行触发器。如果没有这样的子句，则创建的触发器为语句级触发器。

由关键字BEGIN和END限定的部分是触发器的代码，也就是触发器被激发时所执行的代码。代码的编写方法与普通PL/SQL块的编写方法相同。在触发器中可以定义变量，也可以进行异常处理，如果发生异常，就执行相应的异常处理程序。

例如，下面创建的触发器是为了监视用户对表EMP中的数据所进行的删除操作。如果有这样的访问，则打印相应的信息。

CREATE OR REPLACE TRIGGER del_trg

BEFORE DELETE

ON emp

BEGIN

dbms_output.put_line（'您正在对表emp进行删除操作'）；

END；

如果在表emp上进行DELETE操作，则激发这个触发器，例如：

SQL＞DELETE FROM emp；

您正在对表emp进行删除操作

已删除14行。

从触发器的执行情况可以看出，无论用户通过DELETE命令删除0行、1行或者多行数据，这个触发器只对每次DELETE操作激发一次，所以这是一个典型的语句级触发器。

如果一个触发器不再使用，那么可以删除它。删除触发器的语法为：

DROP TRIGGER　触发器

例如，要删除刚才创建的触发器del_trg，使用的语句为：

SQL＞DROP TRIGGER del_trg

触发器的创建者和数据库管理员可以使触发器失效。触发器失效后将暂时不起作用，直到再次使它有效。使触发器失效的命令格式为：

ALTER TRIGGER触发器DISABLE

触发器失效后只是暂时不起作用，它仍然存在于数据库中，使用命令可以使它再次起作用。使触发器再次有效的命令格式为：

ALTER TRIGGER触发器ENABLE

例如，下面的两条命令先使触发器del_trg失效，然后使其再次有效：

SQL＞ALTER TRIGGER del_trg DISABLE

SQL＞ALTER TRIGGER del_trg ENABLE

3.9.2　语句级触发器

如果一个触发器在用户每次进行DML操作时被激发而且执行一次，而不管这个DML操作影响了多少行数据，这个触发器就是语句级触发器。

语句级触发器有前激发和后激发两种形式。前激发触发器是在DML操作执行之前被激发执行，后激发触发器是在DML操作执行之后被激发执行。无论是哪种形式，触发器都将执行一次。

例如，我们创建一个前激发触发器emp_trg，当用户对表EMP的DEPTNO列进行UPDATE操作时该触发器将被激发。创建该触发器的语句为：

CREATE TRIGGER emp_trg

BEFORE UPDATE OF deptno

ON emp

BEGIN

dbms_output.put_line（'您正在修改表emp的deptno列'）；

END；

如果我们执行下面的UPDATE语句：

SQL＞UPDATE emp set deptno=10 WHERE deptno=30；

那么可能得到如下结果：

您正在修改表emp的deptno列

已更新6行。

从UPDATE语句的执行结果可以看出，实际受影响的有6行数据，但是触发器只执行了一次。实际上不管这条语句影响了一行、两行还是多行，或者没有影响任何行，这个触发器都将在UPDATE语句执行之前被激发执行一次。再考虑下面的UPDATE语句，相信我们会对语句级触发器的执行有更深的理解。这次我们修改一个根本不存在的部门编号3。

SQL＞UPDATE emp SET deptno=10 WHERE deptno=3；

您正在修改表emp的deptno列

已更新0行。

如果我们更关心用户对表所实施的访问本身，而不是该次访问影响的数据行数，这时我们可以在表上创建语句级触发器。

在触发器中可以使用三个条件谓词，这三个谓词用来判断当前所执行的操作。它们是：

·INSERTING：如果激发触发器的操作是INSERT，则结果为真，否则为假

·UPDATING：如果激发触发器的操作是UPDATE，则结果为真，否则为假

·DELETING：如果激发触发器的操作是DELETE，则结果为真，否则为假

这三个条件谓词通常作为IF语句的条件，用来判断用户当前所进行的操作。

如果要对用户在表emp上进行的所有DML操作进行监视，可以在这个表上创建一个触发器，将用户的所有DML操作作为日志记录下来。为此，我们先创建一个表emp_log，它的结构及各列的意义如表3-8所示。

然后，在表emp上创建一个后激发的触发器emp_dml_trg。只要用户在表上进行DML操作，这个触发器就会将执行这个操作的用户以及操作类型和操作时间记录在表emp_log中，而不管这样的操作影响了多少行数据。

CREATE OR REPLACE TRIGGER emp_dml_trg

AFTER

INSERT OR UPDATE OR DELETE

ON emp

DECLARE

dml_type char（10）；

BEGIN

if INSERTING then

dml_type：='INSERT'；

elsif UPDATING then

dml_type：='UPDATE'；

elsif DELETING then

dml_type：='DELETE'；

END if；

INSERT INTO emp_log

VALUES（user, dml_type, to_char（sysdate，'yyyy-mm-dd hh：mi：ss'））；

END；

如果用户在表emp上进行了以下DML操作：

SQL＞DELETE FROM emp WHERE deptno=20；

SQL＞UPDATE emp set sal=sal+100；

SQL＞INSERT INTO emp（empno, ename, job, deptno）VALUES（9999，'SMITH'，'SALES'，20）；

那么这三个操作的信息都将被记录在表emp_log中。查询表emp_log，将得到类似如下的结果：

SQL＞SELECT*FROM emp_log；

OPER_USER OPER_TYPE OPER_TIME

SCOTT DELETE 2010-05-16 09：52：30

SCOTT UPDATE 2010-05-16 09：52：50

SCOTT INSERT 2010-05-16 09：53：50

3.9.3　行触发器

如果在某个表上创建了一个触发器，在对这个表进行DML操作时，每影响一行数据，该触发器都将被激发执行一次，那么这个触发器就是行触发器。首先看一个非常简单的触发器的例子：

CREATE OR REPLACE TRIGGER emp_trg

AFTER DELETE

ON emp

FOR EACH ROW

BEGIN

dbms_output.put_line（'1行已经被删除'）；

END；

这个触发器是后触发的。如果对表emp进行DELETE操作，将激发这个触发器的执行。在创建触发器时使用了FOR EACH ROW子句，因此这个触发器是行触发器，例如，如果执行了以下的DELETE语句，执行结果为：

SQL＞DELETE FROM emp WHERE deptno=20；

1行已经被删除

1行已经被删除

1行已经被删除

1行已经被删除

1行已经被删除

已删除5行。

执行结果中的最后一行是SQL*Plus的统计信息，表明有5行受到DELETE语句的影响。从触发器执行的输出信息可以看出，触发器确实执行了5次。也就是说，触发器执行的次数等于受DML操作影响的数据行数。

如果DML操作没有影响到任何一行数据，那么触发器将不执行。例如，如果要根据一个不存在的deptno列的值从emp表中删除行，结果如下：

SQL＞DELETE FROM emp WHERE deptno=100；

已删除0行。

可见，如果DML语句没有影响到任何行，触发器将不会执行。

如果我们更关心DML语句对每行数据的访问情况，而不是DML操作本身的信息，那么在表上创建行触发器更合适。

在创建行触发器时，我们可以指定一些条件，这样只有当特定的数据受到DML语句影响时，触发器才被激发执行。创建触发器时，可以在FOR EACH ROW子句之后使用WHEN子句指定条件。

例如，重新考虑上面的简单的行触发器。如果只对部门30进行监视，只有当从表emp中删除deptno列值为30的行时，才激发触发器。这个触发器可以这样创建：

CREATE OR REPLACE TRIGGER emp_trg

AFTER DELETE

ON emp

FOR EACH ROW WHEN（OLD.deptno=30）

BEGIN

dbms_output.put_line（'1行已经被删除'）；

END；

如果执行两次DELETE操作，分别删除deptno为30和20的行，执行结果为：

SQL＞DELETE FROM emp WHERE deptno=30；

1行已经被删除

……

已删除6行。

SQL＞DELETE FROM emp WHERE deptno=20；

已删除5行。

可见，当从表emp中删除部门30的员工时，触发器被激发，而删除其他部门的员工时，没有激发触发器。

在行触发器中，同样可以使用条件谓词INSERT、UPDATING和DELETING，以判断当前所进行的DML操作。

行触发器通常用于对用户的DML操作进行合法性检查，使得用户修改数据的操作必须按照一定的规则进行。

为了能够比较修改前和修改后的数据，在触发器的可执行代码中，可以使用两个关联行—NEW和OLD。它们分别表示触发器被激发时，当前行的原数据和新数据。

NEW表示修改后的行，通过NEW可以引用新行中的各个列的值，如NEW.ename。OLD表示修改前的行，通过OLD可以引用原来的各个列的值，如OLD.ename。

对于UPDATE命令，OLD表示原来的行，NEW表示修改后的行。对于INSERT命令，OLD没有意义，NEW表示新写入的一行。对于DELETE命令，NEW没有意义，OLD表示被删除的一行。

在触发器的可执行代码中，如果要通过OLD和NEW引用某个列的值，要在前面加上“：”，在其他地方，则不用使用“：”。

现在，我们要创建一个比较复杂的触发器，这个触发器对表emp上的DML操作进行监视。这个触发器需要满足以下要求：

1）在DML操作执行之前进行合法性检查。

2）如果要从表emp中删除一行数据，不能删除部门30的员工。如果是其他部门的员工，则删除这行数据，并把这一行数据在另一个表中进行备份。

3）如果要写入一行数据，要保证这个员工的工资高于1000。

4）如果修改表emp的sal列，应保证新工资比原工资要高，并把员工的工资变化情况记录在另一个表中。

为此，我们需创建两个表，一个是del_action，用来保存从表emp中删除的行，它的结构与表emp相同。可以使用下面的语句创建一个空表del_action：

CREATE TABLE del_action

AS

SELECT*FROM emp

WHERE deptno=0；

第二个表是update_action，用来记录员工工资的变化情况。这个表中的各个字段定义如表3-9所示。

现在我们可以根据上面的要求创建触发器了。创建这个触发器的语句为：

CREATE OR REPLACE TRIGGER emp_dml_trg

BEFORE

INSERT OR DELETE OR UPDATE OF sal

ON emp

FOR EACH ROW

BEGIN

if INSERTING then

if：new.sal is null or：new.sal＜1000 then

dbms_output.put_line（'新员工的工资不得低于1000元'）；

END if；

END if；

if UPDATING then

if：new.sal＜=：old.sal then

dbms_output.put_line（'员工'||：old.empno||'的工资没有增加'）；

else

INSERT INTO update_action VALUES（

：old.empno，：old.ename，：old.sal，：new.sal, to_char（sysdate，'yyyy-mm-dd hh：mi：ss'），user）；

END if；

END if；

if DELETING then

if：old.deptno=30 then

dbms_output.put_line（'部门'||：old.deptno||'的员工不得删除'）；

else

INSERT INTO del_action VALUES（

：old.empno，：old.ename，：old.job，：old.mgr，：old.hiredate，：old.sal，：old.comm，：old.deptno）；

END if；

END if；

END；

在触发器中指定的规则与表上的约束不同。约束是一种强制性的规则，如果DML操作违反了约束，那么这次DML操作是无效的，对数据将不做任何修改。如果DML操作违反了触发器的规则，触发器将按照既定的方法进行响应，但是它并不阻止DML语句的执行。

例如，如果用户执行下面的DELETE语句，从表emp中删除数据：

SQL＞DELETE FROM emp WHERE deptno=30；

这条语句的执行结果为：

部门30的员工不得删除

1行已经被删除

部门30的员工不得删除

1行已经被删除

……

已删除6行。

一方面，触发器按照既定的方法进行响应，结果是正确的。另一方面，DELETE语句却顺利执行了。这是因为，触发器只能对用户的DML操作进行合法性检查，但并不能阻止DML操作的执行，它并不是一种强制性的规则。

如果触发器在检查到不合规定的DML操作时，抛出一个异常，那么这次DML操作对数据所做的修改是无效的，也就是说，触发器阻止了DML语句的执行。例如，把上面的触发器emp_dml_trg重新进行定义，则可以保证不合规定的DML操作不会影响任何数据。创建新的触发器的代码如下：

CREATE OR REPLACE TRIGGER emp_dml_trg

BEFORE

INSERT OR DELETE OR UPDATE OF sal

ON emp

FOR EACH ROW

BEGIN

if INSERTING then

if：new.sal is null or：new.sal＜1000 then

raise_application_error（-2 0000，'新员工的工资不得低于1000元'）；

END if；

END if；

if UPDATING then

if：new.sal＜=：old.sal then

raise_application_error（-20001，'员工'||：old.empno||'的工资没有增加'）；

else

INSERT INTO update_action VALUES（

：old.empno，：old.ename，：old.sal，：new.sal, to_char（sysdate，'yyyy-mm-dd hh：mi：ss'），user）；

END if；

END if；

if DELETING then

if：old.deptno=30 then

raise_application_error（-20002，'部门'||：old.deptno||'的员工不得删除'）；

else

INSERT INTO del_action VALUES（

：old.empno，：old.ename，：old.job，：old.mgr，：old.hiredate，：old.sal，：old.comm，：old.deptno）；

END if；

END if；

END；

这样，如果用户执行了不合规定的DML操作，触发器将抛出相应的异常，并终止触发器的执行，用户的DML操作不会对表中的数据造成任何改变。例如，如果用户试图修改员工的工资，使每个人的工资比原来少100，UPDATE语句的执行结果为：

SQL＞UPDATE emp set sal=sal-100；

UPDATE emp set sal=sal-100

*

ERROR位于第1行：

ORA-20001：员工7369的工资没有增加

ORA-06512：在"SCOTT.EMP_DML_TRG"，line 9

ORA-04088：触发器'SCOTT.EMP_DML_TRG'执行过程中出错

3.9.4　视图上的触发器

视图是建立在一个或多个表之上的虚表。视图中的数据来自对基表的查询。对于简单视图，可以对其进行DELETE、UPDATE和INSERT等DML操作。如果是复杂视图，则不允许对其执行DML操作。

在视图上也可以建立触发器。建立视图触发器的语法为：

CREATE[OR REPLACE]TRIGGER 触发器

INSTEAD OF

DELETE|INSERT|UPDATE[OF列名]

ON 视图

[FOR EACH ROW]

BEGIN

PL/SQL语句；

END；

与表上的触发器相比，视图上的触发器既不是前触发（BEFORE）的，也不是后触发（AFTER）的，而是“INSTEAD OF”类型的。“INSTEAD OF”的意思是当在视图上进行DML操作时，不去执行指定的DML语句，而是执行触发器的代码，这样对视图进行的DML操作根本就不会执行，取代它执行的是触发器的代码。

视图上的触发器都是行触发器，因此，在创建触发器时，“FOR EACH ROW”子句可以省略。对视图进行DML操作时，受影响的每一行数据都会激发触发器执行一次。在触发器中也可以使用关联行NEW和OLD。

如果是简单视图，可以对其进行DML操作。在简单视图上建立触发器的目的一般是为了进行安全校验，例如，我们可以在简单视图上建立触发器，禁止某些DML操作，或者将一些DML操作转化为其他的操作。

现在，我们首先用下面的SQL语句在表emp上建立一个简单视图view_1。

CREATE OR REPLACE VIEW view_1

AS

SELECT ename, empno, sal, deptno FROM emp

接下来，我们在视图view_1上建立一个触发器trg_emp_view，如果对这个视图进行DELETE或者UPDATE操作时，将激发这个触发器。

CREATE OR REPLACE TRIGGER trg_emp_view

INSTEAD OF DELETE OR UPDATE

ON view_1

FOR EACH ROW

BEGIN

if deleting then

if：old.deptno=30 then

raise_application_error（-20001，'部门30的员工不能删除'）；

END if；

END if；

if updating then

if：new.sal＜=：old.sal then

raise_application_error（-20002，'该员工的工资没有增加'）；

END if；

END if；

END；

这个触发器的目的是当删除部门30的员工时抛出异常，或者减少员工的工资时抛出异常。如果执行下面的DML语句：

SQL＞DELETE FROM view_1 WHERE deptno=30；

执行结果为：

DELETE FROM view_1 WHERE deptno=30

*

ERROR位于第1行：

ORA-20001：部门30的员工不能删除

ORA-06512：在"SCOTT.TRG_EMP_VIEW"，line 4

ORA-04088：触发器'SCOTT.TRG_EMP_VIEW'执行过程中出错

如果执行下面的DML语句：

SQL＞DELETE FROM view_1 WHERE deptno＜＞30；

执行结果为：

已删除8行

执行结果表明最后一条DML语句似乎已经顺利执行，但是查询视图view_1的结果表明这条DML语句并没有执行。这是因为当对视图执行规定的DML命令时，将激发触发器，DML操作不会真正执行，取代它执行的是触发器的代码。在上面的触发器中，仅仅对删除部门30的操作进行判断，而对其他部门的删除操作没有做任何进一步的处理，所以会得到数据已被删除的假象。这一点与表上的触发器是完全不同的，使用时要特别小心。

如果对上述视图进行UPDATE操作，将得到同样的结果。如果减少员工的工资，将产生一个异常。如果增加员工工资，表面上看UPDATE操作已经执行成功，实际上工资并没有修改。

如果是复杂视图，则不能在视图上进行DML操作。复杂视图是建立在多个表之上的视图，或者是使用了分组函数的视图。

考虑用以下SQL语句创建的视图：

CREATE OR REPLACE VIEW view_2

AS SELECT emp.ename, emp.empno, emp.sal, emp.deptno, dept.dname FROM emp, dept

WHERE emp.deptno=dept.deptno

如果要在这个视图上执行DML操作，将会发生错误。例如：

SQL＞INSERT INTO view_2 VALUES（'SMITH'，7799，2000，10，'SALES'）；

INSERT INTO view_2 VALUES（'SMITH'，7799，2000，10，'ACCOUNTING'）

*

ERROR位于第1行：

ORA-01776：无法通过连接视图修改多个基表

如果在这个视图上创建一个触发器，那么当在这个视图上进行DML操作时，将激发触发器的执行。如果在触发器中指定了其他操作，DML语句就会以其他方式执行，这样就把用户的DML语句转化为其他的SQL语句了。

例如，对于视图view_2上的INSERT操作，可以将其转化为另一条INSERT语句，用于向emp表中插入一行。当然，如果在写入数据时指定的部门号在表dept中不存在，触发器的执行就会发生错误。对于DELETE操作，可以将其转化为另一条DELETE语句，仅从EMP表中删除相关部门的员工。创建这个触发器的语句为：

CREATE OR REPLACE TRIGGER trg_1

INSTEAD OF INSERT OR DELETE

ON view_2

FOR EACH ROW

BEGIN

if INSERTING then

INSERT INTO emp（ename, empno, sal, deptno）

VALUES（：new.ename，：new.empno，：new.sal，：new.deptno）；

END if；

if DELETING then

DELETE FROM emp WHERE empno=：old.empno；

END if；

END；

创建触发器后，视图上的DML语句就可以顺利执行了。当然，DML语句本身并没有执行，而是转化为另外的DML语句执行了。例如，再执行刚才的INSERT语句：

SQL＞INSERT INTO view_2 VALUES（'SMITH'，7799，2000，10，'SALES'）；

已创建1行。

在视图上进行的DML操作还得遵守基表上的完整性约束。例如，如果在视图view_2上执行INSERT操作，如果指定的部门号在dept表中不存在，或者指定的员工号在表emp中已经存在，都将违反完整性约束。例如：

SQL＞INSERT INTO view_2 VALUES（'PANDA'，8800，2000，90，'DEVELOP'）

INSERT INTO view_2 VALUES（'PANDA'，8800，2000，90，'DEVELOP'）

*

ERROR 位于第1行：

ORA-02291：违反完整约束条件（SCOTT.FK_DEPTNO）-未找到父项关键字

ORA-06512：在"SCOTT.TRG_1"，line 3

ORA-04088：触发器'SCOTT.TRG_1'执行过程中出错

发生错误的原因是在表dept中不存在部门90，从而违反了两个表间的关联完整性。

3.9.5　与触发器有关的数据字典

触发器是一种特殊的存储程序，从被创建之时起，触发器就被存储在数据库中，直到被删除。触发器与一般存储过程或者存储函数的区别在于触发器可以自动执行，而一般的存储过程或者存储函数需要调用才能执行。

与触发器有关的数据字典有：

·USER_TRIGGERS：存储当前用户所拥有的触发器。

·DBA_TRIGGERS：存储管理员所拥有的触发器。

·ALL_TRIGGERS：存储所有的触发器。

·USER_OBJECTS：存储当前用户所拥有的对象，包括触发器。

·DBA_OBJECTS：存储管理员所拥有的对象，包括触发器。

·ALL_OBJECTS：存储数据库中所有的对象，包括触发器。

例如，要想了解触发器TRG_1的类型、触发事件、所基于的对象类型和名称、状态等信息，可以查询视图user_triggers。

SQL＞column triggering_event format a20

SQL＞column table_name format a10；

SQL＞SELECT trigger_type, triggering_event, base_object_type，

TABLE_NAME, status

FROM user_triggers

WHERE trigger_name='TRG_1'；

TRIGGER_TYPE TRIGGERING_EVENT TABLE_NAME STATUS

---------------------------------------------------------------------

INSTEAD OF INSERT OR DELETE VIEW_2 ENABLED

触发器中的代码可以从数据字典视图user_triggers的trigger_body列中获得，如果想查看触发器的代码，可以对这个列进行检索。例如：

SQL＞SELECT trigger_body FROM user_triggers WHERE trigger_name='TRG_1'；

查询的结果为：

TRIGGER_BODY

BEGIN

if INSERTing then

INSERT INTO emp（ename, empno, sal, deptno）

VALUES（：new.ename，：new.empno，：new.sal，：new.deptno）；

END if；

if deleting then

DELETE FROM emp WHERE empno=：old.empno；

END if；

END；

触发器的信息也可以从其他视图中获得。例如，视图user_objects记录了当前用户所拥有的所有对象，其中包括触发器。在这个视图中，触发器是object_type列的值为TRIGGER的对象。

第二部分　Oracle DBA

第4章　Oracle体系结构

作为一个关系数据库产品，Oracle目前占据了关系数据库市场的最大份额。与同类产品比较，Oracle提供了一套完整的机制，保证用户快速、安全地访问数据库，同时保证数据库安全、稳定地运行。在用户眼中，Oracle具有性能稳定、运行高效等优点。

Oracle的优点是显而易见的，然而它的体系结构却是非常复杂的。作为一名数据库管理员，为了使数据库安全高效、安全地运行，并在数据库出现故障时能够快速进行恢复，了解Oracle的体系结构是非常必要的。

数据库中的数据是以文件的形式存储在磁盘上的，人们平常所说的数据库就是指这些存储数据库的数据文件，它是一个静态的概念。然而对数据库的访问则是一个动态的过程，必须通过数据库服务器来进行。数据库服务器不仅包括数据文件，还包括一组用来访问数据文件的内存结构和后台进程，这些内存结构和后台进程叫做实例。

实例是Oracle体系结构中最重要的概念。除此之外，读者还应该了解数据在逻辑上和物理上的组织形式。本章主要包括以下内容：

·实例：介绍实例中内存结构和后台进程。

·物理结构：介绍数据文件、控制文件和参数文件等。

·逻辑结构：介绍表空间、区、段、块等存储结构。

图4.1为Oracle数据库服务器的体系结构。

图　4.1　Oracle的体系结构

4.1　实例的体系结构

当用户希望访问数据库时，需要在操作系统中运行相关的应用程序，如SQL*Plus，启动用户进程。用户进程通过实例访问数据库。实例和数据库组成了数据库服务器，一个数据库服务器中至少有一个实例。在单机环境中，实例和数据库是一一对应的，一个实例只能和一个数据库建立关联关系，一个数据库也只能被一个实例加载。在RAC环境中，一个数据库可以对应多个实例，用户进程可以通过任何一个实例访问数据库。

4.1.1　实例的概念

实例（Instance）是一组内存结构和后台进程的集合。当用户访问数据库时，在数据库服务器端首先要启动一个实例，在内存中分配一定的存储空间，并启动一些后台进程。内存空间的作用是存储与用户访问有关的重要数据，后台进程的功能是监视系统的运行状态，并负责在实例和数据库之间交换数据。在支持线程的操作系统中，这些后台进程以线程的方式运行。

用户访问数据库的操作是通过实例来完成的。实例通过后台进程与数据库中的文件进行交互，将用户修改过的或新增加的数据写入文件，而用户对数据的所有访问都是在实例的内存结构中进行的。引入实例的好处是显而易见的：数据位于内存中，用户读写内存的速度要比直接读写磁盘快得多，而且内存中的数据可以在多个用户之间共享，从而提高了数据访问的并发性。

单机版的数据库（如FoxPro）没有实例的概念，用户对数据的访问是通过直接访问数据文件来完成的，这样在性能上大大低于Oracle。而且因为数据无法在多个用户之间共享，多个用户只能以串行的方式访问数据。可想而知，在一个大型的应用系统中，如果同时有成千上万个用户同时访问数据库时，采用性能低下的数据库是件非常可怕的事情。由此可见，Oracle的实例对于数据库的性能是多么的重要。

4.1.2　实例的组成

当数据库服务器启动时，首先启动实例，然后加载并打开数据库。当用户访问数据库时，数据库服务器便为用户进程启动一个服务器进程，负责处理用户进程的所有请求，例如将用户访问的数据从数据文件读到内存中。

在计算机的内存中不仅要存储数据库中的数据，还要存储数据字典的信息、重做日志以及经过解析的SQL代码等。实例中的这部分内存结构叫做系统全局区（SGA, System Global Area）。

SGA是实例中最重要的组成部分，一个实例只有一个SGA。SGA中的数据可以在多个用户进程之间共享。SGA由若干个缓存和缓冲池组成，不同类型的数据存储在不同的缓存和缓冲池中。SGA的大小可以定制，通过在参数文件中为各个缓存和缓冲池分别指定大小，可以确定SGA的大小。

用户对数据的操作实际上是在SGA中进行的，当启动数据库服务器时，首先启动实例，然后数据库被打开。当用户进程向服务器进程发出请求时，服务器进程将用户请求的数据读到SGA中，用户对数据的所有访问直接在SGA中完成，其他用户进程也可以在SGA中访问相同的数据。当关闭实例时，未保存的数据写入数据文件中，SGA被撤销，所有的数据从SGA中清除。

当用户访问数据库时，实例为用户进程启动一个服务器进程，并分配一段内存区，用来保存用户进程的私有信息和控制信息，这段内存区叫做进程全局区（PGA, Process Global Area）。

SGA是所有用户进程共享的，只要实例被启动，无论是否有用户访问数据库，SGA都存在。而PGA是用户进程私有的，当用户进程向数据库服务器发出请求时，实例为用户进程分配PGA，当用户进程结束时，PGA自动释放。

由此可见，实例中的内存结构包括SGA和PGA两部分。SGA是所有用户共享的，它在实例的运行过程中一直存在。严格地说，PGA并不属于实例，它是服务器进程的一部分，是用户进程私有的，是一种临时的内存结构。

Oracle允许成千上万个用户同时访问数据库，并提供了一种巧妙的机制来保证用户对数据的安全、高效访问。在Oracle实例中包含一组后台进程，它们负责完成复杂的数据访问和维护工作。在Oracle实例中可以启动以下后台进程：SMON, DBWR, PMON, CKPT, LGWR, ARCH, RECO。其中有些进程是必须启动的，而另外一些是可选的。在默认情况下实例将启动SMON、DBWR、PMON、CKPT和LGWR等五个后台进程。

实例的组成如图4.2所示。在接下来的几节中，我们将分别对实例的内存结构和后台进程进行详细的介绍。

图　4.2　实例的组成

4.2　实例的内存结构

当实例启动时，系统为实例分配了一段内存空间，并启动若干后台进程。内存空间分成不同的部分，分别用来存储不同的信息。具体来说，在这段内存空间中存储以下信息：

·程序代码：Oracle的可执行代码。

·缓冲数据：用户要访问的数据、重做日志等。这部分内存叫做SGA。

·与会话有关的信息。

·与进程间通信有关的信息，如加锁的信息。

在上述内存区域中，最重要的是SGA。SGA是由多个缓存和缓冲池组成的，在这些内存结构中存储不同类型的数据。根据存储数据的类型，SGA中主要包含以下类型的内存结构：

·数据库高速缓存

·重做日志缓冲区

·共享池

·Java池

·大池

其中数据库高速缓存由许多缓冲区组成，共享池由数据字典缓存和库缓存两部分组成。在这里之所以使用了缓存和缓冲区两个概念，是因为它们来自不同的英文单词，缓存是从单词cache翻译来的，而缓冲区来自单词buffer。

当实例运行时，可以通过命令查看SGA的大小。SGA是由不同的内存结构组成的，所以查看的结果是分别列出了不同组成部分的大小。例如，在SQL*Plus中执行下面的SHOW命令可以查看当前SGA的大小：

SQL＞SHOW SGA

Total System Global Area 135338868 bytes

Fixed Size 453492 bytes

Variable Size 109051904 bytes

Database Buffers 25165824 bytes

Redo Buffers 667648 bytes

SGA的更详细的信息可以从动态性能视图v$sga、v$sgainfo和v$sgastat中获得。从这些视图中可以获得每种缓冲区和缓存的大小信息。

4.2.1　数据库高速缓存

数据库高速缓存是SGA中的一段存储区域，用来存放用户最近访问的数据。当用户访问数据文件中的数据时，服务器进程首先查看这样的数据是否已经存在于数据库高速缓存中。如果是，则直接在数据库高速缓存对数据进行访问，并将处理结果返回给用户，这次数据访问叫做“命中”，这样的读操作称为“逻辑读”。否则，服务器进程将数据从数据文件的数据块中读到数据库高速缓存中，然后在数据库高速缓存对数据进行访问，这次数据访问叫做“未命中”，这样的读操作称为“物理读”。显然，如果直接在数据库高速缓存访问数据，要比从数据文件中读数据快得多。所以，访问数据的命中率越高，数据库的性能就越高。对数据库进行性能优化的一个重要方面就是提高逻辑读在所有读操作中所占的比例。

数据库高速缓存的大小通过初始化参数DB_CACHE_SIZE来指定。提高数据访问命中率的最直接方法是增加数据库高速缓存的大小，但它的大小不能无限制地增加，它要受到物理内存大小的限制。

用户访问的数据都存储在数据文件中，数据文件被划分为许多大小相同的数据块。数据块是Oracle进行读写的基本单位，也就是说，即使用户只希望访问一个字节的数据，那么数据所在的整个数据块也将被读到数据高速缓存中。同样，在写数据时，也是以数据块为单位的。这样做的好处是提高了数据库服务器的吞吐量。

数据库高速缓存是由一个个的缓冲区组成的，数据从数据文件中被读到数据库高速缓存中之后，就放在这些缓冲区中。缓冲区的大小与数据块的大小一致，一个数据块的内容恰好放在一个缓冲区中。数据块的大小由初始化参数DB_BLOCK_SIZE指定，并且在数据库创建后不能被修改，那么缓冲区的大小也由这个参数决定。所有缓冲区大小的总和由初始化参数DB_CACHE_SIZE指定。假设在参数文件中定义了以下两个初始化参数：

DB_BLOCK_SIZE=8192

DB_CACHE_SIZE=25165824

这说明每个缓冲区的大小为8192字节，即8KB，而所有缓冲区的总的大小为25165824字节，即24MB，这也是整个数据库高速缓存的大小。由此可以计算组成整个数据库高速缓存的缓冲区个数，即25 165 824/8192=3072个。

前面已经提到，提高数据访问命中率的一个有效方法是增加数据库高速缓存的大小。但是由于受到物理内存的限制，数据库高速缓存不可能无限大。如果将数据库高速缓存设置得过大，操作系统可以使用的内存将减少，这样就要使用虚拟内存，反而会降低系统性能。所以数据访问的命中率不可能达到100%。如果用户访问的数据不在数据库高速缓存中，就需要把数据读到某个缓冲区中。一个数据块的内容到底放在哪个缓冲区中，如果没有足够的空闲缓冲区该怎么办呢？我们有必要分析一下缓冲区的使用情况。

根据缓冲区的使用情况，我们可以把缓冲区分为空闲缓冲区、脏缓冲区和忙缓冲区三种类型。

如果一个缓冲区中存放的是由SELECT命令检索的数据，而且这样的数据没有被修改过，这样的缓冲区就是空闲缓冲区。如果缓冲区中存放的是由DML命令处理过的数据，而且这样的数据已经被写入了数据块中，这样的缓冲区也是空闲缓冲区。空闲缓冲区的内容与对应数据块中的内容是完全一致的，这样的缓冲区可用来存放用户即将访问的数据。

如果用户执行了INSERT、UPDATE或者DELETE命令，相应的访问将在缓冲区中进行。如果数据被修改后还没有写入数据块，这时缓冲区中的内容与数据块不一致，这样的缓冲区就是脏缓冲区。脏缓冲区中的数据必须写入数据文件的数据块中，这个任务是由后台进程DBWR完成的。脏缓冲区中的数据被写入数据块后，脏缓冲区又成为空闲缓冲区。

忙缓冲区是指正在被访问的缓冲区。两个用户进程不能同时访问同一个忙缓冲区。

如果用户进程要访问的数据不在缓冲区中，服务器进程将把对应数据块中的数据读入数据库高速缓存的空闲缓冲区中，这个缓冲区中以前的数据将被覆盖。现在考虑一种情况：假设一个缓冲区中的内容被覆盖，恰在这时，另一个用户进程要再一次访问这个缓冲区中以前的数据，服务器进程将不得不把数据所在的数据块重新从数据文件读到另一个缓冲区中，这种情况显然会降低数据库的性能。为了保证数据访问的命中率，Oracle采用了LRU（最近最少使用Least Recently Used）算法，确定每次使用的空闲缓冲区。LRU算法的思想是基于这么一个假设：在最近一段时间内使用最少的缓冲区，在以后的一段时间内也将使用最少，而最近一段时间被频繁访问的缓冲区，在以后的一段时间也将被频繁访问，这个缓冲区中的数据就不应该被覆盖。基于这个思想，每次将数据读到缓冲区中时，服务器进程总是选择那些最近访问次数最少的空闲缓冲区。这种方法虽然不能完全杜绝，但是可以尽量减少上述情况的发生。

在实例中维护了一个LRU队列，在这个队列中记录了各个缓冲区的使用情况。队列的操作遵循“先进先出”的原则，那些最近访问最频繁的缓冲区位于队列尾部，而那些最近最少被访问的缓冲区则位于队列头部。

如果用户访问的数据恰好在缓冲区中，则该缓冲区被标志为“最近访问”，并被移动到队列尾部。如果用户访问的数据不在缓冲区中，服务器进程将在队列头部寻找合适数量的空闲缓冲区，将数据读到这些缓冲区中，并将它们标志为“最近访问”，然后将它们移动到队列尾部。

如果在搜索LRU队列的过程中遇到一个忙缓冲区，服务器进程将忽略它。如果找到一个脏缓冲区，服务器进程将这个脏缓冲区写入另外一个“脏队列”中，然后继续查找LRU队列，直到找到足够数量的空闲缓冲区。在脏队列中记录了数据库高速缓存中的脏缓冲区，实例中的DBWR后台进程在一定的时机下将这个队列中的缓冲区写入数据文件中。

上面我们考虑了缓冲区的一种使用情况，即在数据库高速缓存总有足够数量的空闲缓冲区。一种经常发生的情况是，在搜索LRU队列时没有找到足够数量的空闲缓冲区，这时服务器进程将激活实例中的DBWR后台进程，将脏队列中的脏缓冲区内容写入到数据文件中，这些脏缓冲区重新成为空闲缓冲区，它们将被从脏队列中清除，而重新被写入LRU队列，服务器进程将继续在LRU队列中搜索。

当用户进程执行事务时，将在数据库高速缓存中产生脏缓冲区，这些脏缓冲区并不是立刻被写入数据文件，而是在一定的时机下，由DBWR进程一起写入，这样做的好处是减少了磁盘I/O，从而提高了数据库的性能。

前面说过，数据块的大小由初始化参数DB_BLOCK_SIZE指定，而且不能改变。设置数据块大小的一个基本原则是：如果在数据库中主要执行SELECT语句，如在数据仓库中，这个参数可以设置得大一些，如果主要执行DML语句，这个参数可以设置得小一些。但是在一个数据库中，可能会对一部分数据主要执行SELECT语句，对另外一部分数据主要执行DML语句。在这种情况下，我们可以在数据库中定义不同的数据块大小，根据数据的不同访问要求，把它们放在不同的数据块中，从而从整体上提高数据库的性能。

由初始化参数DB_BLOCK_SIZE指定的数据块称为标准块，在数据库在中还可以定义其他大小的非标准数据块，非标准数据块的大小可以是2KB、4KB、8KB、16KB、32KB等。为了访问非标准块中的数据，在SGA中也需要为它们定义相应的数据库高速缓存，而缓存中缓冲区大小与非标准块的大小也是一致的。Oracle提供了一套新的初始化参数DB_nK_CACHE_SIZE，用于定义与nKB的非标准数据块对应的数据库高速缓存大小：

DB_2K_CACHE_SIZE　为2KB的数据块定义缓存大小，缓存由2KB的缓冲区组成

DB_4K_CACHE_SIZE　为4KB的数据块定义缓存大小，缓存由4KB的缓冲区组成

DB_8K_CACHE_SIZE　为8KB的数据块定义缓存大小，缓存由8KB的缓冲区组成

……

例如，假设在参数文件中有以下初始化参数：

DB_BLOCK_SIZE=8192

DB_CACHE_SIZE=25165824

DB_2K_CACHE_SIZE=48M

DB_16K_CACHE_SIZE=56M

这说明标准数据块的大小为8KB，由8KB的缓冲区组成的数据库高速缓存为24MB，同时定义了两种大小的非标准块和对应的缓存，由2KB的缓冲区组成的数据库高速缓存大小为48MB，由16KB的缓冲区组成的数据库高速缓存大小为56MB，总的数据库高速缓存大小为三者之和。

注意，在上面这种情况下使用初始化参数DB_8K_CACHE_SIZE是非法的，因为标准块的大小为8KB。

为了使用非标准数据块，首先需要定义对应的数据库高速缓存，然后在创建表空间时为数据文件指定数据块大小。当用户访问标准数据块中的数据时，数据将被读入与标准块大小一致的缓冲区中，同样，非标准数据块中的数据将被读入与非标准块大小一致的缓冲区中。

在SQL*Plus中可以通过SHOW命令查看数据块的大小和每种缓冲区的大小，默认缓冲区的大小与标准数据块的大小一致。例如，下面的命令将显示标准数据块的大小：

SQL＞SHOW PARAMETER DB_BLOCK_SIZE

NAME TYPE VALUE

db_block_size integer 8192

下面的命令将显示16kB缓冲区的大小：

SQL＞SHOW PARAMETER DB_16K_CACHE_SIZE

NAME TYPE VALUE

db_16k_cache_size big integer 58720256

4.2.2　重做日志缓冲区

重做日志是对用户事务所产生的记录，通过重做日志能够重新产生数据，它是保证数据安全的一种重要方法。当用户执行DML（INSERT、UPDATE和DELETE）或者DDL（CREATE、DROP、ALTER等）操作时，服务器进程首先将这些操作记录在重做日志缓冲区中，然后才去修改相应的数据。重做日志缓冲区中的内容在一定的时机下，被LGWR后台进程写入重做日志文件。如果数据库系统出现了故障，可以根据重做日志文件中的重做日志对数据库进行恢复。

引入重做日志缓冲区的好处是显而易见的：将日志记录在重做日志缓冲区中，比直接写入重做日志文件要快得多。另外，LGWR进程并不是在每次用户访问数据之后，都要将重做日志缓冲区中的日志写入重做日志文件，而是将最近一段时间产生的重做日志一起写入，这样可以减少访问磁盘的次数，从而提高系统的性能。

重做日志缓冲区的大小由初始化参数LOG_BUFFER指定。在一定的时机下，后台进程LGWR会将重做日志缓冲区中的内容写入重做日志文件。例如，在重做日志缓冲区被消耗了1/3时。由此可见，重做日志缓冲区越大，就可以记录越多的用户操作，写重做日志文件的次数也就越少，这样也可以提高数据库的性能。当然，重做日志缓冲区的大小是受物理内存大小的限制的。

4.2.3　共享池

数据库中的数据是以表的形式组织在一起的。当用户访问数据时，数据库服务器首先检查对应的表是否存在，然后检查指定的列是否存在，还要检查权限和加锁等信息。当这些检查都通过时，数据库服务器将对用户的命令进行分析，产生分析代码和执行计划，然后按照这样的执行计划访问数据，并将执行结果返回给用户。

为了提高数据库的性能，Oracle在SGA中开辟了一个共享池，用于存放与SQL语句的执行有关的信息。共享池主要由三部分组成，即数据字典高速缓存、库高速缓存和服务器结果缓存。共享池的大小由初始化参数SHARED_POOL_SIZE指定。总的来说，共享池几乎和数据库中的所有操作都有关。

当用户访问数据库中的数据时，数据库服务器首先要查询相关的数据字典，确定要访问的对象是否存在，如表、视图等，以及表和视图上的列等。然后检查权限等信息，最后才执行这样的命令。数据字典信息存储在SYSTEM表空间中，也就是说，存储在磁盘上的数据文件中。如果数据库服务器进程每次都要从磁盘上读数据字典信息，那么SQL语句的执行效率是很低的。

为了加快这个SQL语句的执行速度，Oracle在共享池中开辟了数据字典缓存，用来存放最近访问的数据字典的信息。这样查询相关的数据字典时，可以直接在数据字典缓存中进行。

库高速缓存用于存放最近执行的SQL命令的相关信息。我们知道，数据库服务器在执行SQL命令时，首先要对SQL命令进行解析，产生分析代码，并生成执行计划，然后才按照执行计划执行SQL命令，最后把命令的执行结果返回给用户。在整个执行过程中，分析阶段所用的时间是最长的。如果能减少分析所用的时间，那么整个SQL命令的执行效率将大大提高。Oracle的做法是：在执行一条SQL命令时，服务器进程对它进行分析，然后把SQL命令文本、解析结果和执行计划就存储在库高速缓存中。

服务器进程在执行一条SQL命令时，首先要到库高速缓存中查看是否存在这条SQL命令的信息，如果发现SQL语句相关信息已经存储在库高速缓存中，就直接取出执行计划并执行它，这样就省去了分析所用的时间，可以大大加快SQL命令的执行速度。否则，服务器进程需要按部就班地对SQL命令进行解析，然后生成执行计划，并将这些信息存储在库高速缓存中。

当两条SQL命令的文本完全相同时，我们认为它们是同一条命令，这时它们可以共享库高速缓存中的信息。如果命令文本中的大小写、空格个数或参数的数值不同，就认为是两条不同的命令，它们在执行时需要单独进行分析，最终将生成不同的分析代码和执行计划。例如，考虑以下两条命令：

SELECT ename, sal FROM emp WHERE eno=7902

SELECT ENAME, SAL FROM EMP WHERE ENO=7902

这两条命令看似相同，实际上是有差别的。首先，命令中的大小写不同。其次，两个列之间的空格个数不同，所以它们将生成不同的分析代码和执行计划。由此可见，命令的书写风格对命令的执行结果没有什么影响，但对命令的执行效率却大有影响。

在这里，给程序员一个建议：开发程序时要考虑编程的风格和程序的执行效率。虽然每个程序员都会编程，但是不同的程序员编写的程序的执行效率是大不一样的。实践证明，在很多生产系统中，程序员编写的程序就是整个系统性能的瓶颈，这样的系统在提交客户后，管理员对此是无能为力的。

4.2.4　Java池

Java池是SGA中一段可选的存储区域。如果要在数据库中运行Java应用程序，那么对用户的每个会话来说，都需要一个单独的Java虚拟机。实际情况是，每个Java虚拟机仅仅需要很小的一段内存空间，大约是35KB。Java虚拟机为什么能在这么小的内存空间中运行呢？

在实例启动时，可以为在SGA中分配一个Java池，用来存放运行Java所必需的共享代码和共享数据。多个Java应用程序可以共享Java池中的代码和数据。在有些情况下，每个用户的Java会话信息也存储在Java池中。

Java池的大小由初始化参数JAVA_POOL_SIZE指定，默认大小为20MB，在运行Java应用程序时，每个类大约需要4KB～8KB的Java池空间，这样我们可以根据Java应用程序中类的个数来大概估计一下所需的Java池空间。

另外，我们还可以通过查询动态性能视图v$sgastat来了解Java池的使用情况。例如：

SQL＞SELECT*FROM v$sgastat WHERE pool='java pool'；

POOL NAME BYTES

---------------------------------------------------

java pool free memory 4194304

4.2.5　PGA

PGA（进程全局区，Program Global Area）是内存中一段特殊的区域，它包含了服务器进程的数据和控制信息，它是一段非共享的内存区域。当服务器进程启动时，数据库服务器为它分配一段PGA，这个PGA只能由当前服务器进程访问。实际上，PGA并不属于实例，而是属于服务器进程私有的。

PGA包括两个部分：私有SQL区和会话内存区。

在私有SQL区中保存了SQL语句的绑定信息和运行时内存结构。当用户执行一条SQL语句时，服务器进程即为这条语句分配一段私有SQL区。如果两个用户执行了相同的SQL语句，那么这两段私有SQL区就被映射为一个共享的SQL区。

当用户执行SQL语句时，将显式或隐式地使用游标，每个游标都有一段私有SQL区。私有SQL区由持久区和运行时区组成，其中持久区保存SQL语句的绑定信息，仅当游标关闭时它才被释放。运行时区是在服务器进程接收到SQL语句的执行请求时才产生的，在语句执行结束时被释放。

私有SQL区的位置与会话的连接方式有关。如果会话是以专用方式与数据库服务器连接的，那么它位于服务器进程的PGA中。如果会话以共享方式连接数据库服务器，那么它将位于SGA中。

会话内存区保存会话变量和其他会话信息。对于共享服务器，这部分内存区是共享的，而不是私有的，这些信息被所有的共享服务器进程所共享。

对于复杂的查询操作，私有SQL区中的运行时区大部分被用作排序、位图的创建、位图的合并等特殊操作，这部分特殊区域叫做“SQL工作区”。例如，用户执行排序操作时，要用到排序区，排序区就位于SQL工作区中。

SQL工作区的大小是可以控制的。这部分内存区域越大，数据库的性能就越高。如果这部分内存区域的大小不足以执行排序等操作，那么将使用临时表空间中的临时段。

在以前Oracle版本中，用于排序、位图索引的创建等操作的内存区域分别由初始化参数SORT_AREA_SIZE、CREATE_BITMAP_AREA_SIZE、BITMAP_MERGE_AREA_SIZE等指定。在Oracle 11g中，可以对这部分内存区域进行自动管理。首先通过设置初始化参数WORKAREA_SIZE_POLICY，将SQL工作区的管理方式设置为自动方式，然后设置初始化参数PGA_AGGREGATE_TARGET，指定SQL工作区的大小。用户在进行排序等操作时，使用的内存区域的总和不能超过SQL工作区的大小，服务器进程将根据用户操作的需求，自动分配所需的内存区域。

初始化参数WORKAREA_SIZE_POLICY的值有两个：AUTO和MANUAL。如果设置为AUTO，则SQL工作区的管理是自动进行的，这时就不需要通过SORT_AREA_SIZE等初始化参数为不同的操作分别指定内存区域了，只要通过初始化参数PGA_AGGREGATE_TARGET指定PGA的大小就可以了。如果设置为MANUAL，那么需要分别为用户的各种操作指定所需内存区域的大小。

4.3　实例中的后台进程

在一个大型数据库系统中，每时每刻都可能处理大量的用户请求，在数据库服务器中需要执行非常复杂的处理。例如，将脏缓冲区中的内容写入数据文件，将重做日志缓冲区中的重做日志写入重做日志文件，发出检查点以维护数据文件、控制文件和重做日志文件间的一致状态，在数据库服务器重新启动时进行实例恢复，进行重做日志归档等，这些任务都由实例中的后台进程来完成。

当实例启动时，这些后台进程将自动启动。每个进程都有特定的功能，同时，这些进程之间也会相互协作。例如，LGWR进程用于把重做日志缓冲区中的重做日志写入重做日志文件，当重做日志文件被写满后，LGWR进程将向ARCH进程发信号，由ARCH进程对重做日志文件进行归档。

需要注意的是，并不是所有的后台进程都需要启动。常用的后台进程及其功能如表4.1所示。

实例启动后，可以查看正在运行的后台进程。例如，在SQL*Plus中查询动态性能视图V$BGPROCESS，可以获得正在运行的后台进程：

SQL＞SELECT name FROM v$bgprocess WHERE paddr＜＞'00'；

NAME

PMON

DBW0

LGWR

CKPT

SMON

RECO

其中条件paddr＜＞'00'限定了“正在运行的后台进程”。

实例中的后台进程与SGA、数据文件、控制文件和重做日志文件的关系如图4.3所示。

图　4.3　后台进程与SGA及数据库文件的关系

4.3.1　DBWR进程

DBWR进程的功能是将数据库高速缓存中的脏缓冲区内容写入数据文件中的数据块。当用户执行DML命令时，服务器进程在数据库高速缓存中的缓冲区中修改数据，并将修改后的缓冲区标志为“脏缓冲区”。DBWR进程将在一定的条件下将脏缓冲区的内容写入数据文件。

用户执行了DML命令后，被修改的缓冲区并不是被立即写入数据文件，而是保留一段时间，即使用户执行了COMMIT操作。DBWR进程开始工作时，它将把一批脏缓冲区的内容一块写入数据文件。这样做的好处有两点，一是减少了写磁盘的次数，因为多次写磁盘的操作被合并为一次写操作。二是减少了读磁盘的次数，因为如果另外一个用户正好也要对同样的数据进行处理，便可直接在脏缓冲区中进行。如果脏缓冲区被写入了数据文件，它将成为空闲缓冲区，并且可能马上被其他的访问所使用。一旦空闲缓冲区被再次使用，那么当另外一个用户要访问这个缓冲区中以前的内容时，只好重新从数据文件中读取。

用户访问数据库时，服务器进程如果发现需要的数据不在数据库高速缓存中，它将把数据从数据文件读到空闲缓冲区中，在此之前，服务器进程要在LRU队列中搜索合适数量的空闲缓冲区，在搜索过程中如果遇到一个脏缓冲区，服务器进程将把它记录在脏队列中，然后继续搜索。如果遇到忙缓冲区，将忽略它。DBWR进程工作时，将扫描脏队列，把那些位于脏队列中，并且最近很少被访问的脏缓冲区写入数据文件。而那些虽位于脏队列，但最近仍被频繁访问的脏缓冲区，或者那些还没有被记录在脏队列中的脏缓冲区，仍然可以保持“脏”状态，直到被写入数据文件。

在一个实例中可以启动多个DBWR进程，在默认情况下只启动一个。如果用户的事务很频繁，那么在数据高速缓存中将瞬间产生大量的脏缓冲区，对于一个DBWR进程来说，要把这些脏缓冲区写入数据文件，负载是很重的。这时候可以考虑启动额外的DBWR进程，以提高写数据的效率。DBWR进程的数目由初始化参数DB_WRITER_PROCESSES指定，最多可以启动20个（DBW0-DBW9以及DBWa-DBWj）。

DBWR进程并不是越多越好，一个基本的原则是，这个进程的数目不要超过CPU的数目，如果计算机中只有一个CPU，多个DBWR进程在CPU中只能以串行方式运行。而且计算机中如果只有一个硬盘，对数据库的写操作也只能以串行方式进行，如果多个DBWR进程同时写数据文件，将发生磁盘访问冲突。

DBWR进程在以下几种情况执行写操作：

·固定的时间间隔（如每隔3秒）。

·当数据库服务器发出检查点时。

·当脏队列中的缓冲区数目达到一定值时，也就是说在数据库高速缓存中不能有太多的脏缓冲区。

·当用户执行了某操作，需要在数据库高速缓存中搜索一定数量的空闲缓冲区时，空闲缓冲区的数量不能满足要求，这时DBWR进程将把脏队列中的一部分脏缓冲区写入数据文件，这部分脏缓冲区将重新成为空闲缓冲区。

4.3.2　LGWR进程

LGWR进程的功能是将重做日志缓冲区中的重做日志写入重做日志文件，这是Oracle保证数据库一致性的一种重要手段。

当用户执行DML或DDL命令时，服务器进程首先在重做日志缓冲区中生成重做日志，然后才修改数据库高速缓存中相应的缓冲区。在一定的条件下，LGWR进程将重做日志缓冲区中的重做日志写入重做日志文件，而DBWR进程也将在一定的条件下，把数据库高速缓存中的脏缓冲区写入数据文件，这两个进程并不是同步的。

LGWR进程被启动执行的时机有以下几种情况：

·固定的时间间隔（如每隔3秒）。

·用户执行了COMMIT操作。

·重做日志缓冲区已经有1/3的空间被写满。

·DBWR将脏缓冲区写入数据文件之前。

由此可见，如果脏缓冲区中的数据被写入了数据文件，那么重做日志一定被写入了重做日志文件。而用户数据还没有被写入数据文件时，重做日志也可能被写入了重做日志文件，也可能没有。

SGA中的重做日志缓冲区是一段可循环使用的存储区域。一方面，服务器进程将生成的重做日志写入重做日志缓冲区，占用一部分空间。另一方面，LGWR进程将重做日志写入重做日志文件，释放一部分空间。由于LGWR进程将重做日志写入重做日志文件的速度高于服务器生成重做日志的速度，所以在任何情况下重做日志缓冲区中始终都有足够的空闲空间。重做日志缓冲区的使用情况如图4.4所示。

图　4.4　重做日志缓冲区的使用

当用户执行事务时，服务器进程一方面在重做日志缓冲区中产生重做日志；另一方面，在数据高速缓存中修改数据。对重做日志缓冲区和重做日志文件是按顺序写的，效率很高，而对数据高速缓存和数据文件都是随机写的，因为数据的分布并没有什么规律，所以效率较低。如果写日志和写数据是同步进行的，那么整个数据库的性能就不可能很高。

为了提高处理事务的效率，Oracle采取了“快速提交”的机制。当用户执行一个事务时，这个事务将获得一个SCN，同时产生重做日志，重做日志被写入重做日志缓冲区。在此之后，服务器进程才在数据高速缓存中修改数据。如果用户提交了事务，SCN也将被写入重做日志缓冲区，然后LGWR进程立即开始工作，将这个事务的重做日志和SCN一起写入重做日志文件，这个事务就算成功地执行结束了。而与这个事务有关的脏缓冲区这时并没有写入数据文件，而是由DBWR进程在适当的时候写入。

在这里向大家介绍一个概念，即SCN（系统改变号，System Change Number）。SCN是用于记录数据库变化的一个数字，它是一个正整数。当一个事务执行之后，便获得一个新的SCN，每个SCN在整个数据库中都是唯一的，而且所有SCN的值是递增的，随着数据库的运行，SCN不断增大，并且永远不会重复，也没有消耗完的时候。SCN被同时记录在数据文件、控制文件和重做日志文件中，如果这三个文件的SCN完全一致，数据库就达到了一个完全一致的状态。

现在我们考虑一个非常复杂的问题，Oracle如何利用重做日志保证数据库的一致性？如果发生了系统断电，用户执行的事务是否有效？Oracle如何利用重做日志进行数据库的恢复？

假设用户执行了一条DML命令，如UPDATE。假设在执行这条命令之前数据库的SCN值为2000，在执行这条DML命令之后，这个事务获得的SCN是2010。现在我们来分析两种常见的情况。

首先我们考虑用户没有提交事务的情况。根据DBWR进程LGWR进程的工作时机可知，这次事务带来的变化可能已经被写入了数据文件和重做日志文件，也可能没有。但是无论是否写入，这次事务都是无效的，因为新的SCN没有被写入数据文件、控制文件和重做日志文件，这三种文件中记录的还是原来的SCN值，即2000。如果此刻系统突然断电，那么数据库服务器在重新启动时，将检查这三种文件中记录的SCN，并且回退最后一个SCN之后的所有事务。也就是说，用户执行了一条DML命令，但是因为没有提交该事务，系统发生断电等故障时，数据将丢失，无法恢复。

现在再考虑事务被提交的情况。当用户执行COMMIT命令提交事务时，重做日志缓冲区中的重做日志和新的SCN将被写入重做日志文件。尽管这个事务所产生的脏缓冲区可能还没有被DBWR进程写入数据文件，但这个事务已经有效。考虑上面提到的UPDATE命令，这时重做日志文件记录的是新的SCN值，即2010，但是这个SCN值还没有被写入数据文件和控制文件。如果这三个文件的SCN不一致，那么重做日志文件中记录的SCN一定大于数据文件和控制文件中的SCN。如果此刻系统突然断电，那么数据库服务器在重新启动时，将检查这三种文件中记录的SCN，并且重新执行两个SCN之间的所有事务，然后将其中未提交的事务回滚，这样三种文件中的SCN将达到一致，然后才打开数据库，这时数据库达到了一致的状态。数据库的这种操作称为“实例恢复”，实例恢复是根据重做日志文件的内容进行的，这个过程由后台进程SMON完成。SMON进程将重做日志文件中记录的事务重新执行一次，从而保证了数据不会因为系统故障而丢失。

那么在系统运行过程中，三种文件中的SCN能不能达到一致呢？答案是肯定的，这要依赖于后台进程CKPT。当CKPT进程开始工作时，SCN将被写入数据文件和控制文件，使三种文件的SCN完全一致。同时CKPT进程通知DBWR进程开始工作，把脏缓冲区中的数据写入数据文件。这时数据库将达到完全一致的状态，系统断电时数据库将不受任何影响，也不需要进行实例恢复。

由此可见，只有当事务被提交后，才有可能进行实例恢复，用户的数据才不至于丢失。如果事务没有提交，数据是无法进行恢复的。

4.3.3　CKPT进程

CKPT进程的功能是发出检查点。检查点是一种数据库事件，当数据库服务器发生检查点时，SCN将被写入数据文件和控制文件，而且数据库高速缓存中的脏缓冲区将被DBWR进程写入数据文件，这时数据库达到完全一致的状态。

我们知道，数据库服务器是依靠SCN来维护数据文件、控制文件和重做日志文件之间的一致状态的。如果一个事务没有提交，脏缓冲区及重做日志缓冲区中的内容可能已经被写入了数据文件和重做日志文件。这时如果数据库服务器发出检查点，三种文件的SCN达到一致，数据库达到一种一致状态。但这三种文件中记录的是该事务之前的SCN。数据库服务器如果重新启动，这个事务将被回滚，因为它处于最后一个SCN之后。

如果事务被提交，新的SCN将首先被写入重做日志文件，这时数据库服务器如果发出检查点，新的SCN被写入数据文件和控制文件，三种文件的SCN完全相同，数据库达到一致状态，在这种情况下，数据库服务器在重新启动时就不需要进行实例恢复。相反，如果没有发出检查点，那么在数据文件和控制文件中保持原来的SCN。如果系统断电，数据库服务器在重新启动时，将进行实例恢复，两个SCN之间的所有事务将被重新执行一次，使数据库达到一致状态。

CKPT进程的任务有两个：一是通知DBWR进程，将数据库高速缓存中所有脏缓冲区写入数据文件，二是发出检查点，将SCN的值写入数据文件和控制文件的头部。可见，当数据库服务器发出检查点时，将带来大量的磁盘I/O，因此，应该尽量减少检查点的发生。

CKPT进程在以下几种情况下被启动执行：

·正常关闭数据库服务器时。

·进行日志切换时。

·手工发出检查点（执行ALTER SYSTEM CHECKPOINT命令）。

·由初始化参数LOG_CHECKPOINT_TIMEOUT和LOG_CHECKPOINT_INTERVAL指定的时机来到时。

与检查点的发生时机有关的两个初始化参数为：

·LOG_CHECKPOINT_TIMEOUT：用于指定两个检查点之间的时间间隔（以秒为单位）。Oracle 11g中这个参数的默认值为1800，即30分钟。如果设置为0，将取消固定时间间隔方式的检查点。

·LOG_CHECKPOINT_INTERVAL：这个参数指定了一个操作系统块的数目，可以看做是检查点在空间上发生的间隔。当往重做日志缓冲区中写入指定块数的重做日志时，将发出检查点。

在管理数据库时，应该为检查点设置合理的时间、空间间隔。如果间隔太大，对数据文件的写操作次数将减少，数据文件与重做日志文件的SCN将相差较大，在系统断电时，进行实例恢复的时间将加长。反之，如果间隔太小，磁盘写操作将被频繁执行，数据库的性能将降低，但这样做的好处是，在系统断电时实例恢复的时间将被缩短。

现在以一个不合理的检查点间隔的例子来说明如何消除不必要的检查点。假设操作系统块的大小为1024字节，初始化参数LOG_CHECKPOINT_INTERVAL的值为4608，重做日志文件的大小为10MB。那么每当向重做日志文件中写入1024×4608=4.5MB的重做日志时，数据库服务器将发出检查点。这样在两个检查点之后，重做日志文件将被写满9MB。如果再写入1MB，就会进行日志切换，并自动发出一个检查点。这样最后两个检查点的间隔仅为1MB，显然间隔太短。所以在设置LOG_CHECKPOINT_INTERVAL参数时要考虑重做日志文件的大小，尽量将一些检查点与切换日志时发出的检查点合并。

4.3.4　SMON进程

SMON是系统监视进程，它的功能是监视数据库服务器的运行状况，并执行一些必要的清理工作。在数据库服务器启动时，SMON进程将检查数据文件、控制文件和重做日志文件，并根据三个文件的SCN值进行实例恢复，或者回滚未提交的事务。

另外，SMON进程还可以对数据库的存储空间进行一些常规的管理。具体的功能如下：

·回收临时表空间中不再使用的临时段。

·在字典管理表空间中合并相邻的空闲存储空间。

在数据库服务器重新启动时，SMON进程负责进行实例恢复，并对数据库的存储空间进行管理。在实例正常运行时，SMON进程也会经常工作，或者被其他进程调用，这时它的功能仅限于对存储空间进行管理。

4.3.5　PMON进程

PMON进程的功能是定期检查用户进程，并进行回收资源的操作。

当用户访问数据库时，用户进程与服务器进程建立连接。当用户进程断开连接时，PMON进程负责回收为其服务的服务器进程所占用的资源。

如果由于某种原因使连接非正常断开，如用户进程异常终止，或网络发生故障，用户进程所占用的资源还没有释放。PMON进程负责检查所有用户进程的状态，清除非正常终止的用户进程，并回收它们所占用的资源，如锁、存储区域等，终止相应的子进程，并释放对应服务器进程所占用的资源。

PMON进程还有一个功能，就是将实例和调度器注册到网络监听器中。当实例启动时，PMON进程负责将实例的信息注册到监听器中。如果监听器没有启动，PMON进程将周期性地察看它的状态。

4.3.6　ARCH进程

ARCH进程的功能是对重做日志文件进行归档。

数据库的重做日志记录在重做日志文件中。在一个数据库中往往需要若干组重做日志文件，这些文件是循环使用的。当一组重做日志文件被写满时，数据库服务器自动进行日志切换，重做日志接着被写入下一组重做日志文件。当数据库服务器再一次使用一组重做日志文件时，文件中以前的内容将被覆盖，而且永远无法恢复。

随着数据库服务器的运行，重做日志文件不断被覆盖。可以想象，如果系统发生故障，造成数据丢失，这时候要对数据进行恢复。数据恢复需要用到对数据库所进行的备份，以及重做日志。如果重做日志缺失，那么最后一段时间的数据将无法恢复。为了防止这种情况的发生，Oracle提供了一种归档日志模式。在这种模式下，数据库服务器将自动对重做日志文件进行归档。如果数据库处于归档日志模式下，ARCH进程将自动启动，当一个重做日志文件被写满时，要进行日志切换，ARCH进程将对刚刚写满的重做日志文件进行归档，将重做日志文件的内容保存在归档日志文件中。

有了归档日志文件，数据库管理员再也不用担心重做日志文件被覆盖的事情发生了。由于日志归档是自动进行的，所以数据库服务器可以顺利地进行日志切换，无需人工干预。

如果数据库处于非归档日志模式下，重做日志文件是不会被归档的。只有数据库处于归档日志模式下，并且启动了ARCH进程，数据库服务器才会自动对重做日志进行归档。在一个实例中可以启动多个ARCH进程，默认情况只启动4个。

与ARCH进程有关的初始化参数有以下几个：

·LOG_ARCHIVE_MAX_PROCESSES：指定可以启动的ARCH的最大数目。

·LOG_ARCHIVE_FORMAT：指定归档日志文件的名称。

·LOG_ARCHIVE_DEST_n：指定归档日志文件的存储路径，其中n的取值范围为1～31。

·LOG_ARCHIVE_DEST_STATE_n：指定对应的归档路径是否可用。

如果数据库服务器写重做日志文件的速度比日志归档的速度要快，那么将发生数据库服务器被阻塞的现象。所以在服务器写重做日志的操作非常频繁的情况下，可以启动多个ARCH进程，以加快日志归档的速度。

4.4　实例的内存结构管理

内存结构管理主要涉及对SGA中的各种缓冲区进行最佳设置，使实例对内存的利用率达到最高，从而提高数据库的性能。从Oracle 11g开始，可以对SGA和PGA进行完全的自动管理，也就是说，只要分别指定SGA和PGA的最大大小，实例将自动确定每种缓冲区的大小，以及每个服务器进程的PGA大小，随着数据库服务器的运行，实例可以根据需要随时调整这些内存区域的大小。

有三种方法可以对实例的内存结构进行管理：

·自动内存管理。

·自动共享内存管理。

·手工共享内存管理。

4.4.1　自动内存管理

这是一种完全自动的内存管理方式，只要通过初始化参数MEMORY_TARGET和MEMORY_MAX_TARGET指定实例可用的最大内存大小，实例就可以自动确定SGA和PGA的大小，SGA中的每种缓冲区大小也根据需要自动确定。随着数据库服务器的运行，实例将根据需要，在指定的范围内自动调整SGA和PGA的大小。

MEMORY_TARGET参数可以动态修改，管理员可以随时修改这个参数的值，而不用重新启动实例。MEMORY_MAX_TARGET是不能动态修改的，它的作用是为实例指定了可用内存的上限，也就是说，指定了MEMORY_TARGET参数的最大可取值。设置初始化参数MEMORY_MAX_TARGET的目的是防止管理员将MEMORY_TARGET参数设置过大，从而给操作系统留下太少的内存。

为了使用自动内存管理方法，需要对以下初始化参数进行适当的设置：

SGA_TARGET　应该设置为0

PGA_AGGREGATE_TARGET　应该设置为0；

LOCK_SGA　应该设置为false

如果希望了解SGA和PGA目前的实际大小，以及SGA中每种缓冲区的实际大小，可以查询动态性能视图v$memory_dynamic_components。例如，下面的SQL语句用于查看PGA以及SGA中每种缓冲区目前的大小、以及可以调整的范围：

SQL＞SELECT component, current_size, min_size, max_size

FROM v$memory_dynamic_components；

4.4.2　自动共享内存管理

自动共享内存管理用来对SGA进行自动设置。只要通过初始化参数SGA_TARGET和SGA_MAX_SIZE指定SGA的最大大小，那么SGA中的各种缓冲区大小就可以根据需要自动确定，可以说，这是一种半自动化的内存管理方法。需要注意的是，重做日志缓冲区、非标准块对应的高速缓存、KEEP缓冲池和RECYCLE缓冲区的大小仍需自己设置。

为了使用自动共享内存管理，首先为初始化参数SGA_TARGET和SGA_MAX_SIZE指定适当的大小，然后把以下初始化参数的值设为0。

MEMORY_MAX_TARGET

MEMORY_TARGET

SHARED_POOL_SIZE

LARGE_POOL_SIZE

JAVA_POOL_SIZE

DB_CACHE_SIZE

STREAMS_POOL_SIZE

而以下初始化参数仍然需要进行适当的设置：

LOG_BUFFER

DB_KEEP_CACHE_SIZE

DB_RECYCLE_CACHE_SIZE

DB_nK_CACHE_SIZE

如果需要了解SGA中每种缓冲区的实际大小，可以查询动态性能视图v$sgainfo。

4.4.3　手工共享内存管理

这种内存管理方法很好理解，SGA中每种缓冲区的大小都需要手工设置，SGA的总大小就是这些缓冲区之和。如果SGA设置过大，操作系统可用的内存将减少，这时将使用交换空间，把SGA的一部分数据放到交换空间中，这样会降低数据库的性能。可以把初始化参数LOCK_SGA的值设置为TRUE，这样就可以把SGA锁定在物理内存中。

为了使用手工共享内存管理，需要把以下初始化参数的值设置为0。

MEMORY_MAX_TARGET

MEMORY_TARGET

SGA_TARGET

PGA_AGGREGATE_TARGET

以下参数需要根据实际情况进行适当的设置：

SHARED_POOL_SIZE　用于设置共享池的大小

DB_CACHE_SIZE　用于设置数据库高速缓存的大小

LOG_BUFFER　用于设置重做日志缓冲区的大小

LARGE_POOL_SIZE　用于设置大池的大小

JAVA_POOL_SIZE　用于设置Java池的大小

DB_KEEP_CACHE_SIZE　用于设置KEEP缓冲池的大小

DB_RECYCLE_CACHE_SIZE　用于设置RECYCLE缓冲池的大小

DB_nK_CACHE_SIZE　用于为nK的非标准数据块设置高速缓存大小

4.5　数据库的连接模式

当用户需要访问数据库时，在客户端要建立一个用户进程，在服务器端，需要为用户进程分配一个服务器进程，两个进程之间建立连接，服务器进程处理用户进程的请求。客户端应用程序可以是SQL*Plus、EM、RMAN，或者用户自己开发的Java应用程序。

用户进程必须通过服务器进程才能访问数据库实例。服务器进程接受用户进程的请求，解析并执行用户进程发送的SQL命令，然后检查数据库高速缓存。如果用户访问的数据恰好就存储在缓冲区中，那么服务器进程直接在缓冲区中对数据进行处理，并将执行结果返回给用户进程。否则，服务器进程将从数据文件中读取所需的数据，并将数据复制到数据库高速缓存中，然后在数据库高速缓存对数据进行处理。

一个服务器进程可以仅为一个用户进程服务，也可以同时为多个用户进程服务。根据用户进程是否可以共享服务器进程，Oracle提供了两种数据库连接模式：专用数据库连接模式和共享数据库连接模式。

4.5.1　专用数据库连接模式

在专用数据库连接模式下，服务器进程与用户进程是一一对应的。每当有一个用户进程试图与数据库服务器建立连接时，数据库服务器便启动一个服务器进程为用户进程服务。而当用户进程访问结束，断开连接时，服务器进程也自动终止。图4.5表示数据库连接模式中的专用连接模式。在这里我们要注意区分数据库、数据库服务器和服务器进程的概念。

在专用数据库连接模式下，各个服务器进程之间完全独立，它们之间并不共享数据。当用户进程连接数据库服务器时，服务器进程便被启动，而且在一直运行，直到用户进程断开连接。

图　4.5　专用连接模式

对于单个用户进程而言，专用连接模式的效率是很高的，因为一个服务器进程只为一个用户进程服务。但是对一个大型的数据库系统而言，这种连接模式未必合适，一方面，在服务器端需要启动大量的服务器进程，以处理众多的用户进程的请求，这对整个系统来说，负载是很重的。另一方面，当用户进程和服务器进程建立连接后，服务器进程的大部分处理时间都在等待用户的输入输出，处理用户的请求只需要很短的时间。

如果用户进程数目较少，或者用户进程需要对数据库进行大量的访问，那么可以考虑将数据库连接模式设置为专用模式。

4.5.2　共享数据库连接模式

专用数据库连接模式在处理大批量用户连接时效率并不高，因为需要为每个用户进程启动一个服务器进程，这样会消耗大量的系统资源。如果用户进程的大部分连接时间都处于空闲状态，服务器的资源就会被白白浪费。比如在银行储蓄系统中，计算机处理一次存取款的时间为几毫秒，而工作人员输入相关信息却需要几分钟的时间。

Oracle提供了一种共享数据库连接模式，这种模式允许一个服务器进程同时为多个用户进程服务，多个用户进程共享一个服务器进程。当一个用户进程处于空闲状态时，比如正在等待用户输入数据，服务器进程就可以处理其他用户进程的请求。在这种情况下，服务器进程的工作效率得到了很大的提高。

在数据库服务器中可以启动多个共享服务器进程，每个服务器进程都可以处理多个用户请求，在数据库服务器中只需要少量的服务器进程就可以处理大量的用户请求。

Oracle推荐在以下情况采用共享数据库连接模式：

·在联机事务处理（OLTP）环境中访问数据库。可能有大量用户进程需要连接到数据库服务器，这种模式可以使用户进程有效地使用系统资源。

·计算机的内存有限。与专用服务器相比，当用户进程数量增加时，共享服务器的数量并不需要增加，或者增加很少，这就减少了对内存的要求。

·如果需要使用Oracle Net的特性，如连接共享、连接集中和负载均衡。共享数据库连接模式的工作原理如图4.6所示。

图　4.6　共享数据库连接模式

在图4.6中，调度器、共享服务器进程、请求队列和响应队列共同完成共享数据库连接模式的功能。其中请求队列和响应队列是SGA的一部分，为了更清楚地表示这几部分之间的关系，在图4.6中我们将这两个队列从SGA中分离出来。

调度器负责接收用户的请求，并将用户请求传递给适当的共享服务器进程。在一个数据库服务器中可以启动多个调度器，而且至少要为Oracle支持的每一种网络协议启动一个调度器（如TCP/IP、IPX/SPX、命名管道等）。可以通过初始化参数DISPATCHERS和MAX_DISPATCHERS来指定调度器的数目。其中参数DISPATCHERS指定为某类网络协议启动的调度器，而MAX_DISPATCHERS用来设置数据库服务器中可以启动的最大调度器的数目。例如：

DISPATCHERS=（PROTOCOL=TCP）（DISPATCHERS=3）

MAX_DISPATCHERS=20

上面的两个参数规定为TCP/IP协议启动3个调度器，而总的调度器数目为20。

当用户进程向数据库服务器发出请求时，监听器在指定的端口监听用户的请求，并将请求传递给一个负载较轻的调度器，调度器将这个请求以及自己的ID一起放到请求队列中。

共享服务器进程从请求队列中取出一个请求，并对其中的SQL命令进行解析和执行，然后将执行的结果放到响应队列中。解析和执行的过程与专用服务器相同。

在一个数据库服务器中可以启动多个共享服务器进程，这些进程可以处理任何一个用户进程发出的请求，而不是处理一个确定的请求。共享服务器进程的数目由初始化参数SHARED_SERVERS和MAX_SHARED_SERVERS指定，其中参数SHARED_SERVERS指定在初始情况下需要启动的共享服务器进程数目，而参数MAX_SHARED_SERVERS指定了允许启动的最大共享服务器进程数目。当用户请求较多时，数据库服务器将启动更多的共享服务器进程，而当用户请求较少时，服务器将关闭不必要的共享服务器进程，但在任一时刻，共享服务器进程的数目将保持在两个参数SHARED_SERVERS和MAX_SHARED_SERVERS的值之间。

调度器除了负责将用户进程的请求传递给共享服务器进程外，还负责将共享服务器进程处理的结果返回给用户进程。调度器周期性地检查响应队列，如果发现与自己ID一致的处理结果，调度器将取出这个结果，并将它返回给用户进程。这个处理结果就是在此之前，由该调度器传递给共享服务器进程的某个用户请求的处理结果，调度器根据其中的ID判断它是否属于自己。

需要注意的是，在共享数据库连接模式下也可以启动专用服务器进程。有些用户请求是不适合以共享数据库连接模式处理的。例如，当用户以DBA身份登录数据库服务器时，用户进程无法与调度器通信。当数据库服务器监听到这样的用户请求时，它将为用户进程启动一个专用服务器进程，以处理该用户的所有操作。

4.5.3　如何设置共享连接模式

要将数据库服务器的连接模式从专用模式改为共享模式其实是很简单的，只要将初始化参数SHARED_SERVERS的值设置为一个大于0的整数即可，其余几个初始化参数都不是必须设置的。在共享模式中至少需要一个调度器，因此，当把连接模式改为共享模式之后，在数据库服务器中将自动启动一个调度器。

如果把初始化参数SHARED_SERVERS的设置放在参数文件中，那么当数据库服务器启动时就处于共享连接模式。如果在参数文件中没有对这个参数进行设置，但是同通过初始化参数DISPATCHERS至少设置了一个调度器，那么数据库服务器启动时也处于共享连接模式。

如果数据库是通过DBCA创建的，在数据库中将有一个为XDB协议配置的调度器，而且初始化参数SHARED_SERVERS值为1。只要用户访问Oracle的XML DB服务，就连接到一个共享服务器进程上。这并不意味着数据库服务器目前就使用了共享连接模式，这时还需要设置更多的共享服务器进程和调度器。

Oracle推荐的共享服务器进程数目设置原则为：每10个客户端进程对应一个服务器进程。数据库管理员可以根据客户端进程的大致数目，设置共享服务器进程的数目，而且在数据库服务器运行的过程中，可以根据实际情况，随时调整服务器进程的数目。例如：

SQL＞ALTER SYSTEM SET shared_servers=5；

共享服务器进程的相关信息可以从动态性能视图v$shared_server中获得。服务器的命名规则是Snnn，其中nnn是从000开始的三位整数。例如，下面的SELECT语句用于查询共享服务器进程的名称和状态：

SQL＞SELECT name, status FROM v$shared_server；

NAME STATUS

S000 WAIT（COMMON）

S001 WAIT（COMMON）

S002 WAIT（COMMON）

S003 WAIT（COMMON）

S004 WAIT（COMMON）

如果将数据库服务器的连接模式设置为共享模式，那么至少需要设置一个调度器。在默认情况下，数据库服务器将为TCP协议设置一个调度器。根据客户端与服务器端通信的实际情况，需要为每种通信协议设置一个调度器。对于TCP/IP协议，需要指定调度器所监听的IP地址和端口号。例如，下面的语句将使用TCP/IP协议的调度器的数目设置为3：

SQL＞ALTER SYSTEM SET DISPATCHERS=

"（ADDRESS=（PROTOCOL=TCP）（HOST=192.168.1.100））（DISPATCHERS=3）"；

调度器的相关信息可以从动态性能视图V$DISPATCHER中获得。调度器的命名规则是Dnnn，其中nnn是从000开始的三位整数。例如，下面的SELECT语句用于查询调度器的名称、所用协议、状态等信息：

SQL＞SELECT name, network, status FROM v$dispatcher；

NAME NETWORK STATUS

D000（ADDRESS=（PROTOCOL=tcp）（HOST=sunday）（PORT=49157））WAIT

D001（ADDRESS=（PROTOCOL=TCP）（HOST=192.168.1.100））WAIT

D002（ADDRESS=（PROTOCOL=TCP）（HOST=192.168.1.100））WAIT

D003（ADDRESS=（PROTOCOL=TCP）（HOST=192.168.1.100））WAIT

4.6　数据库的逻辑结构

前面已经提到，数据库服务器包括实例和数据库两部分。其中数据库是用来存储数据的，而实例是用来访问数据库中的数据的。实例包括一组内存结构和后台进程，而数据库的结构需要从逻辑结构和物理结构两个方面来理解。数据库的逻辑结构是指数据的逻辑组织形式，是Oracle内部用来管理数据的机制。数据库的物理结构是从用户角度感觉到的结构，是在操作系统中存储和管理数据的机制。

从逻辑结构上来讲，一个数据库包含若干个表空间，不同类型的数据存储在不同的表空间中，如系统数据、用户数据、临时数据和回滚数据等分别存储在不同的表空间中。表空间中包含若干个段，同一个表空间中的数据又可进一步根据类型不同而存储在不同的段中，如数据段、索引段、临时段等。一个段中又可以包含若干个区，区是Oracle分配存储空间和回收存储空间的基本单位。区是由连续的多个数据块组成的，数据块是Oracle读写数据库的基本单位。表空间、段、区和数据块组成了数据库的逻辑结构。

逻辑结构是指数据的组织形式，而从物理形式上讲，数据是以数据文件的形式存储在磁盘上的。在操作系统中，能使用户真正感觉到数据库存在的是数据文件。数据文件是存储在磁盘上的，它需要占用若干个操作系统块。

逻辑结构和物理结构并非毫无关系，而是紧密联系的。表空间中的数据是存放在数据文件中的，因此一个表空间对应一个或多个数据文件。数据块是Oracle中存储数据的基本单位，一个数据块对应若干个操作系统块，数据最终是存储在操作系统块中的，Oracle对数据块的访问最终转化为对操作系统块的访问。

数据库的逻辑结构与物理结构之间的关系如图4.7所示。

图　4.7　数据库的物理逻辑结构和物理结构

4.6.1　表空间

表空间是数据库中数据的逻辑组织形式，一个数据库在逻辑上由多个表空间组成。表空间用于将不同类型的数据组织在一起，就像一个公司里的员工是按照不同部门组织在一起一样。表空间中的数据在物理上是存储在数据文件中的，一个表空间对应一个或多个数据文件，就像一个部门对应若干个办公室一样。当表空间中的存储空间紧张时，可以向表空间中添加数据文件，一个数据文件只能属于一个表空间。

如果表空间中只包含一个数据文件，那么所有数据都将存储在这个数据文件中。如果表空间对应多个数据文件，那么数据将被分割成几部分，分别存放在这些数据文件中。表空间与数据文件的关系如图4.8所示。

图　4.8　表空间与数据文件

在数据库中引入表空间的好处有以下几点：

·将系统数据和用户数据分开，有利于保护重要的数据。

·可以限制用户对磁盘存储空间的使用。

·将临时数据与用户数据分开，从而减少用户数据存储区的碎片，提高数据库的性能。

·能够将不同类型的数据分别存放在不同的磁盘上，以减少磁盘的读写冲突。可以将访问频繁的数据存储在速度相对较快的磁盘上，从而在整体上提高数据库的性能。

·各个表空间可以被单独设置为联机或脱机状态，这样可以在数据库正常运行的情况下，将单个表空间置于脱机状态，并对其进行备份或恢复。

在一个数据库中有五种类型的表空间，即SYSTEM表空间、SYSAUX表空间、UNDO表空间、临时表空间和普通表空间。其中前四种表空间是必不可少的，在创建数据库时就需要创建它们，普通表空间是根据需要才创建的。

1.SYSTEM表空间

SYSTEM表空间是数据库中一个必需的表空间。在创建数据库时，SYSTEM表空间将被自动创建。在SYSTEM表空间中存储着数据库的系统信息，如数据字典，数据库对象的定义、PL/SQL存储程序的代码、SYSTEM回滚段等。

2.SYSAUX表空间

SYSAUX表空间也是数据库中一个必需的表空间，它是在创建数据库时自动被创建的。SYSAUX表空间是对SYSTEM表空间的辅助表空间，以前存储在SYSTEM表空间中的数据现在存储在SYSAUX表空间中，从而减轻了SYSTEM表空间的负担。另外，许多以前需要单独表空间的数据现在都可以存储在SYSAUX表空间中，从而减少了需要维护的表空间的数目。

3.UNDO表空间

UNDO表空间是用来存储回滚数据的。回滚数据是被事务修改的数据，例如，假设用户执行语句“DELETE FROM emp WHERE empno=7902”。被DELETE命令访问的数据就是回滚数据。在事务尚未提交之时，这一行数据被存放在UNDO表空间中。此时假设另一个用户执行语句“SELECT*FROM emp WHERE empno=7902”，那么他将得到从UNDO表空间中返回的这一行数据。如果事务被回滚，UNDO表空间中的数据被写回原来的存储空间，就好像原来的DML操作没有被执行一样。

由此可见，UNDO表空间是为了回滚事务而设计的。在以前版本的数据库中，回滚数据只能存放在回滚段中。回滚段位于某一个表空间中，或者某些特定的表空间中。回滚段的管理比较复杂，需要数据库管理员手工执行繁琐的命令。目前版本的数据库都使用UNDO表空间来管理回滚数据。在UNDO表空间中只能存放回滚段，而不能存放其他类型的段，如数据段、索引段等。使用UNDO表空间好处是对回滚数据进行自动管理，从而减轻了数据库管理员的负担。

4.临时表空间

临时表空间用于存放用户访问数据库时所产生的临时数据。例如，当用户执行语句“SELECT*FROM emp ORDER BY empno”时，将对表中的数据进行排序并产生排序结果。排序操作一般是在PGA的排序区中进行的。如果排序区的大小不足以容纳这些数据，将使用临时表空间。在临时表空间中只能建立临时段。临时段也不是永久存在的，当用户第一次在数据库中执行排序等操作时，临时段将自动产生，而当数据库关闭时临时段的空间将被释放。正是由于这个原因，在临时表空间不允许创建永久性的数据库对象，如表、索引等。

在一个数据库中可以创建多个临时表空间。如果没有临时表空间，那么用户在执行排序等操作时可能需要使用SYSTEM表空间存储临时数据。如果在SYSTEM表空间中频繁地存储临时数据，将产生大量的存储碎片，从而降低数据库的性能。

在使用CREATE USER命令创建用户时，可以通过TEMPORARY TABLESPACE子句为该用户指定临时表空间。用户在创建之后，也可以通过ALTER USER命令为其指定临时表空间。这样用户访问数据库时产生的临时数据将被存储在指定的临时表空间中。

5.普通表空间

普通表空间是用户真正关心的表空间，在数据库中可以创建多个普通表空间。普通表空间用来存放用户的数据。

4.6.2　段

表空间将数据按照类型从逻辑上分离开来，如将用户数据与系统数据分别组织在一个表空间中。在同一个表空间中，可能存在不同类型的数据库对象，如表、索引。Oracle将不同数据库对象中的数据以段的形式组织在一起。一个表空间包含多个段，但一个段只能属于一个表空间。

当用户在数据库中创建一个数据库对象时，在表空间中将自动创建一个段，以存储该对象的数据。比如，在默认情况下，一个表对应一个表段，一个索引对应一个索引段。段中存储空间的分配是以区为单位进行的。在一个段中包含若干个区。在分配段时需要指定初始的区个数。随着段中数据的增加，数据服务器将会扩充该段，为段分配所需的区，而当段中数据被删除时，空闲的区可以被回收。

在表空间中主要可以创建四种主要类型的段，它们是数据段、索引段、临时段和回滚段。在每个段中存储不同的数据库对象。

数据段用来保存表中的数据，默认情况下一个表对应一个表段。在一个表段中只能存储一个表中的数据。当用户在表空间中创建一个表时，数据库服务器将自动在这个表空间中为该表创建一个段，段的名字与表的名字相同。

索引段用来存储索引中的数据，当用户为一个表创建索引时，数据服务器将自动为该索引创建一个索引段，索引段与索引的名字相同，并且它们间是一一对应的。当在表上创建主键约束或唯一性约束时，也将产生相应的索引段。

临时段用于存放临时数据，当用户执行排序等操作时，将产生大量的临时数据，这些临时数据存储在临时段中。当然，临时数据是优先存储在PGA的排序区中的，这样可以提高排序的速度。如果排序区的大小不足以存放这些临时数据，才会用到临时段。临时段不是必需的，如果没有创建专门的临时段，用户的排序操作将使用SYSTEM表空间中的临时段。由于SYSTEM表空间存储着重要得系统数据，频繁地使用SYSTEM表空间将产生大量的碎片，从而降低数据库的性能，所以Oracle建议尽量创建专门的临时段，并将它存放在专门的临时表空间中。

回滚段用于存储回滚数据。当用户执行DML语句时，数据库服务器将修改后的数据存储在表段中，而将修改前的数据作为回滚数据存储在回滚段中。当用户回滚事务时，数据库服务器将回滚段中的数据重新写入表段，该事务所做的修改将取消。当用户提交事务时，回滚段中的数据将变为无效，这时用户将无法回滚该事务。在数据库中可以创建多个回滚段。当用户执行DML操作时，数据库服务器将自动为当前事务指定一个回滚段，用户也可以通过命令指定一个回滚段。在创建数据库时，系统在SYSTEM表空间中自动创建一个SYSTEM回滚段，SYSTEM回滚段用于维护Oracle内部的事务。数据库管理员可以通过命令创建其他的回滚段。

需要注意的是，Oracle 11g提供了两种管理回滚数据的方法，一种是手工管理方式，这种方法利用回滚段维护事务。另一种方法称为自动管理方式，这种方式利用专门的UNDO表空间管理回滚数据。由于回滚段的管理太复杂，Oracle建议大家使用自动管理方式，在以后的Oracle版本中，可能会取消手工管理方式。

4.6.3　区

区是Oracle分配存储空间的最小单位，一个段由多个区组成，一个区由若干个连续的数据块组成，区的大小是数据块大小的整数倍。在创建一个数据库对象时，数据库服务器为该对象分配若干个区，以存储该对象的数据。数据库对象至少占用一个区，随着数据的增加，数据库服务器将不断为该对象分配所需的区，这些区的大小可能相等，也可能不相等。

4.6.4　数据块

数据块是Oracle中的最小存储单位，也是数据库服务器读写数据的最小逻辑单位。数据库服务器在为段分配空间或回收存储空间时，是以区为单位进行的，而在读写数据时，是以数据块为单位进行的。

数据库中的数据最终是存储在硬盘上的，所以数据块与操作系统中的块必然有着密切的联系。一个数据块由若干个操作系统块组成，它的大小是操作系统块的整数倍。数据库服务器在读写数据时以数据块为单位进行，这种访问最终转化为对操作系统块的读写。

在Oracle 11g中，有两种形式的数据块，一种是标准块，另一种是非标准块。标准块的大小由初始化参数DB_BLOCK_SIZE指定，所有标准块的大小都相同。非标准块的大小可以有多种情况，如2KB、4KB、8KB、16KB、32KB（但是不能与标准块的大小相同），等等。在数据库中可以使用一系列初始化参数DB_nK_CACHE_SIZE，其中n为2、4、8、16、32等。这些初始化参数系列用来为非标准块指定数据库高速缓存大小。如果在数据库中定义了非标准块，那么必须在SGA中为它定义相应的数据库高速缓存。无论是哪种数据块，它的大小在数据库创建之后就不能再修改。

数据库服务器在读写数据时，数据块中的数据将首先被调入SGA的数据库高速缓存中，在缓存中必须为每一种大小的数据块定义缓冲区，缓存区的大小与数据块相同。当用户访问数据库时，数据块的内容被读写到与之大小相同的缓冲区中。

4.7　数据库的物理结构

数据库中的数据在逻辑结构上是以表空间、段、区、数据块的形式组织的，而在物理上则表现为储存在磁盘上的文件。数据库的物理结构是指数据在操作系统中的的存储方式，是对用户可见的组织形式。数据库的物理结构包括数据文件、控制文件和重做日志文件，这三种文件是数据库正常运行所必需的。另外，数据库中还包括口令文件、参数文件、警告文件和跟踪文件等。

4.7.1　数据文件

顾名思义，数据文件是用来存储数据的。在数据文件中存储所有数据库对象的结构和数据，包括表、视图、索引、触发器、存储程序等数据库对象。用户对数据库的访问实际上就是对数据文件的访问，只不过这些文件不能由操作系统直接访问，而必须通过数据库服务器才能访问。

数据文件与一般的文件有所不同。一般的操作系统文件的初始大小可能比较小，随着文件中内容的增加，它的大小也随着增加，同样，它的大小也将随着内容的减少而减少，在任何时候，它的大小与其内容的多少是一致的。数据文件更像是一个空的容器，在创建数据文件时，就按照指定的大小分配了存储空间。在文件被写满之前，无论文件中包含多少数据，它的大小总是固定不变的。

既然数据文件是一种操作系统文件，它在磁盘上必然占用一定数量的操作系统块。在数据库服务器内部，数据读写的基本单位是数据块，一个数据块对应多个操作系统块。在数据库中读写一个数据块时，在操作系统中对应着对多个操作系统块的读写。

数据文件在逻辑上属于表空间，一个表空间可以包含一个或多个数据文件，而一个数据文件只能属于一个表空间。用户在创建数据库对象时，只能指定所属的表空间，而不能指定存储在哪个数据文件上。数据库对象被创建后，它的结构和数据就存储在一个或多个数据文件中。

随着数据库的运行，数据文件中的数据可能越来越多，并最终耗尽数据文件的存储空间。为了存储更多的数据，数据文件的存储空间必须能够扩展。Oracle提供了三种扩展数据文件存储空间的方法。第一种方法是在当前表空间中增加新的数据文件，新数据将被存储在新的数据文件中。第二种方法是手工扩展表空间中的数据文件，在原来的存储空间的基础上，增加一定数量的存储空间。第三种方法是激活数据文件的自动扩展功能，数据库服务器将自动为数据文件分配新的存储空间。

4.7.2　控制文件

控制文件是数据库中另一种重要的文件，它的功能是记录数据库的结构和状态。这是一个二进制文件，用户无法查看和修改文件的内容。数据库在启动时需要根据控制文件的内容，查找数据文件并打开它们。在数据库运行的过程中，对数据库结构所做的任何修改都将记录在控制文件中。

在启动数据库服务器时，首先启动实例，然后才能打开数据库。数据库服务器是通过控制文件在实例和数据库之间建立对应关系的。在控制文件中记录了数据文件的路径、重做日志文件的路径、当前日志序列号、SCN等信息。

如果控制文件丢失或损坏，数据库服务器将无法正常运行。由于控制文件的特殊重要性，对它的存储有特殊的要求。在一个数据库中至少需要一个控制文件，Oracle建议至少创建两个控制文件，并将它们分别存储在两个磁盘上，这两个文件互相镜像，如果一个文件损坏，数据库服务器可以使用另外一个文件。在正常情况下，这两个文件的内容是完全一样的，数据库服务器只需要从其中一个文件中读取信息，但是对数据库结构所做的任何修改都必须同时写入两个控制文件。

4.7.3　重做日志文件

重做日志文件是保证数据库安全的一种重要手段。在重做日志文件中记录的是用户对数据库所做的修改，即一条条的DML和DDL命令。当数据库服务器发生故障时，数据库管理员可以根据重做日志的内容对数据进行恢复，从而保证数据不会因为故障而丢失。

用户在执行DML或DDL操作时，实际的数据处理是在SGA中进行的。服务器进程首先生成一条重做日志，并将它存储在重做日志缓冲区中，然后在数据库高速缓存中修改相应的缓冲区。

在一定的时机下，DBWR进程将数据库高速缓存中脏缓存区的内容写入数据文件中，LGWR将重做日志缓冲区中的内容写入重做日志文件。在一般情况下，LGWR总是先于DBWR进程将重做日志写入重做日志文件。

在重做日志文件中不仅记录重做日志，还记录SCN。如果用户提交事务，SCN将随着重做日志一起被LGWR进程写入重做日志文件。这样如果数据库服务器发生了故障，数据库管理员可以将数据库恢复到最后一个SCN处。如果用户没有提交事务，重做日志也可能被写入重做日志文件，但如果这个事务被回滚，对数据库将不产生任何影响。

在数据库中至少需要两个重做日志文件，数据库服务器将以循环的方式将重做日志写入这些文件。当第一个文件写满后，数据库服务器自动进行日志切换，将重做日志写入下一个文件。当最后一个文件被写满时，数据库服务器重新将重做日志写入第一个文件。如此循环往复进行，重做日志文件中以前的内容将被覆盖。

由于重做日志文件的特殊重要性，Oracle建议为每一个重做文件至少建立一个镜像文件。互为镜像的重做日志文件归为一个日志组，同一个日志组中所有文件的内容和大小完全相同。这些文件应该尽量分布在不同的磁盘上，以免磁盘发生故障时丢失所有的重做日志。数据库服务器在写重做日志时，必须将重做日志同时写入同一个日志组的所有文件。

为了保证重做日志的安全，应该及时对重做日志文件进行归档，产生归档日志文件。归档日志文件可以被备份到磁盘等存储介质上。归档操作必须在数据库的归档模式下进行，并且需要启动一个或多个ARCH进程。在LGWR进程向一个重做日志文件写入重做文件之前，该文件必须被归档，否则LGWR进程将被挂起，所有的事务都停止执行。

4.7.4　跟踪文件和警告文件

当数据库服务器运行不正常时，将在警告文件和跟踪文件产生一些有用的信息，数据库管理员可以查看这些文件的内容，根据其中记录的信息判断故障发生的原因。跟踪文件用于记录服务器进程和后台进程发生的内部错误信息，每个服务器进程和后台进程都有自己的跟踪文件。后台进程的跟踪文件名为＜SID＞＜process＞.TRC，其中SID为实例名称，process为后台进程名称。例如，实例TEST中的LGWR进程的跟踪文件名为“TESTLGWR.TRC”。服务器进程的跟踪文件名称为＜SID＞_＜PID＞_ORA.TRC，其中SID为实例名称，PID为服务器进程的进程号。根据文件名称，数据库管理员就可以判断跟踪文件是由哪个进程产生的。

警告文件用于记录实例内部的错误消息以及数据库管理员对数据库所做的维护，另外，在警告文件中还记录非默认的初始化参数。警告文件的名称为ALERT_＜SID＞.LOG，其中SID为实例名称。

跟踪文件和警告文件都有默认的存储路径，它们的路径由初始化参数指定。如初始化参数USER_DUMP_DEST指定了服务器进程跟踪文件的存储路径，而初始化参数BACKGROUND_DUMP_DEST指定了后台进程跟踪文件和警告文件的存储路径。

4.8　特权用户与口令文件

在创建数据库时，自动创建了一个特殊的用户SYS，这个用户就是我们平常所说的数据库管理员，它具有两种特殊系统权限SYSDBA和SYSOPER，对整个数据库具有所有权限。对数据库的管理基本上是以这个用户的身份来完成的。

SYS用户在登录实例时，需要指定SYSDBA或者SYSOPER权限。例如，通过SQL*Plus以以下方式登录：

sqlplus sys/1234 as sysdba

或者：

sqlplus sys/1234 as sysoper

其中“1 2 3 4”是SYS用户的口令。在登录之前需要在系统中设置一个环境变量ORACLE_SID。在Windows中，只有当一个系统中运行多个实例时，才需要设置这个变量。在UNIX/Linux中设置环境变量的命令是（其中orcl是实例的名称）：

ORACLE_SID=orcl

export ORACLE_SID

在Windows中设置环境变量的命令是：

set ORACLE_SID=orcl

在UNIX/Linux中安装Oracle软件之前，需要在系统中创建一个oracle用户，这个用户属于dba用户组（在Windows中这个用户一般是系统管理员administrator，属于ora_dba用户组），以后对数据库的所有管理任务，都是以oracle用户登录到系统中来完成的。只要以oracle用户登录到系统中，就能够以下面的方式登录到数据库实例中，而不用提供SYS用户的口令：

sqlplus/as ssydba

这时候实际上是把系统中的oracle用户映射为数据库中的SYS用户。在Oracle中把这种验证方式称为操作系统验证。实际上，在操作系统中任何用户只要属于dba用户组，就能以上面的方式登录到数据库实例中。同样，任何系统用户只要数据oper用户组（或者dba用户组），就能够以下面的方式登录数据库实例：

sqlplus/as sysoper

如果SYS用户以远程方式登录实例，如通过EM，就需要提供口令。SYS用户的口令一方面同普通用户一样，存储在数据库中，另一方面，SYS用户的口令存储在一个操作系统文件中，这个文件就是口令文件。在UNIX/Linux中，口令文件存放在Oracle安装目录的dbs子目录下，文件命名规则是：orapw＜sid＞.ora，其中＜sid＞指的是实例名称。在Windows中，口令文件存储在database子目录下，文件的命名规则是：PWD＜sid＞.ora。

SYS用户的远程登录，是通过口令文件进行验证的。如果口令文件丢失，或者口令输入错误，验证就会失败。在这种情况下，可能需要重新创建口令文件，指定新的口令。Oracle提供了一个命令，名称是orapwd，通过这个命令对口令文件进行管理。这个命令是由oracle用户在操作系统中执行的。例如：

$orapwd file=$ORACLE_HOME/dbs/orapworcl.ora password="1234"force=y

这个命令的各个参数的含义如下：

file：用来指定口令文件的路径和名称，如果不指定路径，将在当前目录下产生。

password：用来指定SYS用户的新口令。

force：如果口令文件已经存在，则覆盖。

4.9　数据字典视图与动态性能视图

在Oracle数据库中，与用户有关的表有三种，一是用户自己创建的表，二是数据字典视图，三是动态性能视图。在后两种视图中，用户可以查询自己关心的信息。

4.9.1　数据字典视图

数据字典是一种系统表，它在数据库被创建时自动产生，并且由数据库服务器本身进行维护和更新。在数据字典中包含了数据库的相关信息。在数据字典中存储了以下信息：

·数据库的物理结构和逻辑结构信息。

·用户和权限信息。

·数据库对象的信息，如表、视图、索引、存储程序、约束等。

·审计信息。

由于数据字典是一个非常大而且非常复杂的表，用户不方便对其进行直接的访问，而且这样做也不安全。为了用户访问方便，Oracle提供了许多数据字典视图，这些视图就建立在数据字典基表上，我们平常所说的数据字典就是指这些数据字典视图。数据字典视图的结构可以通过在SQL*Plus中执行DESC命令来查看。对一个用户而言，可以访问的数据字典视图很多，如果按照所包含的信息的范围大小来划分，有三类主要的数据字典视图，这些视图的名称分别以以下标识符开始：

user_存储当前用户所拥有的某类对象的信息。

all_存储当前用户有权访问的某类对象的信息。

dba_存储数据库中所有的某类数据对象的信息，仅管理员可以访问。

例如，从数据字典视图user_objects中可以查询当前用户所拥有的所有对象的信息，包括表、视图、索引、存储程序等。如果要查询当前用户所拥有的所有对象的名称、类型、创建时间、状态等信息，可以执行下列SELECT语句：

SQL＞SELECT object_name, object_type, created, status FROM user_objects；

在数据字典视图user_tables中存储了当前用户所拥有的表的信息。例如，要查询当前用户所拥有的表的名称和所在的表空间名称，可以执行下列SELECT语句：

SQL＞SELECT table_name, tablespace_name FROM user_tables；

查询的结果类似如下，根据不同的用户查询结果会有所不同：

TABLE_NAME TABLESPACE_NAME

BONUS SYSTEM

DEPT SYSTEM

EMP SYSTEM

SALGRADE SYSTEM

STU SYSTEM

从数据字典视图all_tables中可以查询当前用户可以访问的表的信息，包括用户自己创建的表，以及其他用户授权该用户可以访问的表。例如，通过执行下面的SELECT语句，可以了解当前用户可以访问的表的名称：

SQL＞SELECT table_name FROM all_tables；

从数据字典视图user_tab_columns中可以查询当前用户所拥有的表的各个列的定义。例如，通过执行下列的SELECT语句，可以了解表DEPT的各个列的定义：

SQL＞SELECT table_name, column_name, data_type, data_length

FROM user_tab_columns

WHERE table_name='DEPT'；

这条SELECT语句的执行结果类似如下：

TABLE_NAME COLUMN_NAME DATA_TYPE DATA_LENGTH

DEPT DEPTNO NUMBER 22

DEPT DNAME VARCHAR2 14

DEPT LOC VARCHAR2 13

从数据字典视图dba_tables中能够查询当前数据库中所有表的信息，这类以dba_开始的视图只能由SYS用户查看。例如，下面的SELECT语句用于查询数据库中所有表的名称、所在表空间的名称、所有者等信息：

SQL＞SELECT table_name, tablespace_name, owner FROM dba_tables；

一个用户可以访问的数据字典是很多的，这些数据字典的名字是与所存储的对象信息有关的。例如，数据字典视图user_indexes可以查询当前用户所创建的索引的信息，在dba_users中，可以查询当前数据库中所有用户的信息。在以后的章节中，我将陆续介绍很多有用的数据字典视图。

4.9.2　动态性能视图

如果说数据字典视图反映了数据库的信息，那么动态性能视图则主要反映了实例的信息。数据字典视图中的信息是静态的，来自数据字典基表，它反映的是数据库的信息，这些信息不会因为数据库服务器的关闭而消失。动态性能视图中的信息则是动态变化的，它反映了实例的实际运行情况，这些信息来自SGA或者控制文件，随着实例的关闭和重新启动，这些信息将重新产生。

动态性能视图的名称基本上是以v$开始的。从这些视图中可以获得一些有用的统计信息，这些信息主要用于对数据库的性能进行调优。例如，执行下面的SELECT语句可以了解数据库中一些等待事件的发生情况：

SQL＞SELECT event, total_waits, time_waited, average_wait from v$system_event；

下面的语句用于查看SGA中每种缓冲区的大小：

SQL＞SELECT name, bytes/1024/1024 as MB from v$sgastat；

4.10　初始化参数

Oracle提供了许多的初始化参数，通过这些参数对数据库及实例进行设置。管理员可以查看这些参数的设置，也可以修改这些参数的值。初始化参数具有以下功能：

·对实例进行设置，如设置SGA中每种缓冲区的大小。

·设置数据库的属性，如设置数据块的大小。

·对用户进程进行设置，如设置最大的并发用户进程数。

·指定数据库中一些重要文件的路径。

·对数据库资源的使用进行限制。

4.10.1　参数文件

初始化参数的值存储在参数文件中。Oracle提供了许多初始化参数，但是在参数文件中只对少量的初始化参数进行了设置，其他大部分参数都采用默认值。在第5章中将介绍如何创建参数文件。

在Oracle数据库中有两种参数文件，其中一种称为服务器参数文件，这是一个二进制文件，文件的命名规则是spfile＜sid＞.ora，其中＜sid＞代表实例的名称。另一个文件称为文本参数文件，这是一个文本文件，用户可以直接修改文件内容，文件的命名规则是init＜sid＞.ora。这两个文件默认都存放在Oracle安装目录的dbs子目录（UNIX/Linux平台）或database子目录（Windows平台）中。

在启动实例时，Oracle将在指定目录下先查找服务器参数文件，如果这个文件不存在，就继续查找文本参数文件。如果这两个文件都不存在，用户就需要在命令行中指定参数文件的路径。例如，在SQL*Plus中通过以下命令启动实例：

SQL＞startup pfile='/export/home/oracle/init.ora'

其中pfile用于指定一个文本参数文件。也可以通过以下命令在启动实例时指定一个服务器参数文件：

SQL＞startup spfile='/export/home/oracle/spfile.ora'

需要注意的是，如果用户通过SQL*Plus远程登录，并且希望通过文本参数文件启动实例，那么必须通过pfile指定参数文件的路径，而这个文件是存储在执行SQL*Plus命令的客户端，而不是存储在服务器端。

4.10.2　初始化参数的查看

管理员可以通过EM或SQL*Plus中查看初始化参数的设置。例如，在SQL*Plus中通过执行以下命令查看数据块的大小：

SQL＞show parameter db_block_size

如果我们不知道某个初始化参数的具体名称，但是知道这个参数的某一部分，也可以通过这种方式查看初始化参数的值。例如，以下命令用于查看包含字符串“log_archive_”的所有参数：

SQL＞show parameter log_archive_

我们也可以通过执行SELECT命令，从动态性能视图v$parameter查看初始化参数的信息。例如：

SQL＞select value from v$parameter where name='db_block_size'；

4.10.3　初始化参数的修改

根据作用范围的不同，我们把初始化参数分为三种类型包括：静态参数、实例级参数、会话级参数。其中静态参数是不能修改的，自从数据库创建之后这些参数就一直保持某个特定值，如表示数据块大小的DB_BLOCK_SIZE。实例级参数和会话级参数是可以修改的，其中实例级参数在整个实例范围内有效，这样的参数只能由SYS用户修改。会话级参数只在当前会话范围内有效，会话结束后参数就失去作用，每个用户都可以修改自己的会话级参数。

为了查看初始化参数的详细信息，可以在SQL*Plus中执行以下SELECT命令：

SQL＞SELECT value, isdefault, isses_modifiable, issys_modifiable, ismodified

FROM v$parameter

WHERE name='utl_file_dir'

各列含义如下：

ISDEFAULT　指定当前参数是否采用了默认值。

ISSES_MODIFIABLE　指定当前参数是否可以在一个会话中进行修改。

ISSYS_MODIFIABLE　指定当前参数是否可以在实例范围内进行修改。

IS_MODIFIED　指定自动实例最近启动以来，当前参数是否被修改过。

有两种方法可以用来修改初始化参数，一种方法是直接编辑文本参数文件，然后重新启动实例，这样新的参数值就会起作用；另一种方法是在EM或SQL*Plus中动态修改。Oracle推荐使用后一种方法，因为这种方法可以把对数据库系统的影响减到最小。

有些参数修改以后立即起作用，有些参数修改以后必须重新启动实例才能起作用。在SQL*Plus中修改初始化参数的命令是ALTER SYSTEM。例如：

SQL＞ALTER SYSTEM SET utl_file_dir='/export/oracle/dict'；

这条命令在执行时将产生错误信息，原因是这个参数的值不能立即起作用，必须把实例后参能生效。在修改参数时，可以通过SCOPE关键字指定参数什么时候生效。例如：

SQL＞ALTER SYSTEM SET utl_file_dir='/export/oracle/dict'SCOPE=SPFILE；

SCOPE有三个可选值，含义如下：

MEMORY：参数值立即其作用，但是实例重新启动后新的参数值将失效。

SPFILE：将参数值记录在服务器参数文件中，重新启动实例后参数值将生效。

BOTH：一方面参数值立即生效，另一方面把参数值记录在服务器参数文件中，这样就可以永久其作用。这个值是默认值。

第5章　数据库的创建

我们知道，一个数据库服务器包括实例和数据库两部分，其中数据库是存储在硬盘上的文件，而实例则是一组内存结构和后台进程的集合。数据库的创建意味着要创建实例和数据库两部分。

在安装Oracle 11g软件时，安装向导将引导用户创建一个数据库。此后，数据库管理员可以根据需要再创建一个数据库。在一台计算机上可以运行多个数据库服务器。

通常所说的“创建数据库”实际上就是指创建一个新的数据库服务器。创建数据库服务器涉及两方面的内容，即创建实例和创建数据库。创建实例涉及创建参数文件、创建口令文件、指定SGA、启动实例等。而创建数据库则涉及创建数据文件、创建控制文件、创建重做日志文件等操作。数据库管理员应该首先创建实例，在实例启动后再创建数据库。考虑到数据库服务器运行的安全、高效等要求，在创建数据库之前需要进行详细、周密的规划。

Oracle提供了两种创建数据库的方式，一种是利用DBCA（Database Configuration Assistant）工具的方式，另一种是利用命令行的方式。其中DBCA是Oracle提供的一个图形化工具，利用这个工具可以方便地创建实例和数据库，而不必记忆复杂的命令，这个工具对初学者非常合适。命令行的方式比较复杂，需要用户执行大量的操作系统命令和SQL命令，这种方式要求用户对命令的用法十分熟悉。初学者可能会感到第二种方式很困难，但是只要读者能克服困难，坚持用命令行的方式创建数据库，直到成功，那么你对实例和数据库的结构将会有更深刻的理解，你所获得的成就感是用DBCA方式时所体会不到的。

5.1　数据库的规划

在创建数据库之前，应该进行详尽的规划，因为数据库一旦投入运行，很多信息是根本无法改变的，或者需要耗费大量的人力、财力和时间才能修改。实践证明，许多系统集成公司的工程师在创建数据库时仅仅凭一纸安装文档，虽然能够成功，但是这样的数据库由于缺少规划，在运行一段时间后，就会暴露出很多问题。因此，创建数据库之前的规划可以使数据库管理的工作更加轻松。

数据库的规划涉及多方面的内容，如内存的布局、硬盘空间的使用等，数据库设计人员需要在日常工作中不断积累经验，制定最佳的计划。

5.1.1　SGA的规划

SGA规划的内容包括为各个缓存区指定大小。SGA设置是否合理对数据库服务器的运行性能有很大的影响。

SGA是内存中的一段存储区域，而且这段存储区域只能由数据库服务器使用，这就意味着操作系统可以使用的内存减少了。由于现在的计算机硬件配置都比较高，所有可以将SGA的大小直接设置为物理内存的1/2，随后对数据库的运行情况进行监视，在不带来额外的内外存交换的情况下，逐步增加SGA的大小。

如果SGA太大，操作系统可以使用的内存将大大减少，这时系统将使用虚拟内存来弥补内存的不足，将SGA的一部分放在外存上，这时在内外存之间将进行频繁的交换，系统的性能将降低。这种做法显然违背了建立SGA的初衷。

在Oracle 11g中，有三种方法对SGA进行设置，所有的设置都是通过一些初始化参数来完成的。相关设置方法请参考第4章的相关内容。

SGA由数据库高速缓存、重做日志缓冲区、共享池、Java池、大池等组成，这几部分大小的总和就是SGA的大小。与这些存储区域有关的初始化参数如表5.1所示。

5.1.2　数据文件的规划

数据文件用来存储数据库中的所有数据，包括表、视图、索引、存储程序等数据库对象。数据文件规划的内容包括表空间的数目、数据文件在磁盘上的分布、数据库对象在表空间中的分布、数据块大小等内容。

数据块的大小对数据库的访问性能有很大的影响。如果数据块太小，那么一次数据访问可能需要读多个数据块，从而增加了硬盘读操作的次数。反之，如果数据块太大，用户真正需要的数据可能只占整个数据块的一小部分，大量无用的数据被读到SGA中，从而浪费了SGA的存储空间。Oracle建议在决策支持型（DSS）的数据库系统中，使用较大的数据块，而在联机事务处理型（OLTP）的数据库系统中，使用较小的数据块。

在创建数据文件时，应该综合考虑数据库服务器读写数据文件的效率。在一个数据库中可以创建多个数据文件。如果计算机中有多块硬盘，应该使这些数据文件分布在不同的硬盘上，这样在读写数据文件时，可以在多个硬盘之间同时进行读写，从而提高了数据库的访问性能。如果不同硬盘的读写速度有差异，应当把访问较频繁的数据文件放在速度相对较快的硬盘上，这样可以从整体上提高访问速度。当然数据文件并不是越多越好，因为数据文件越多，打开它们时就要消耗越多的内存空间。

与数据文件相比，重做日志文件的写操作是相对比较频繁的，所以应当把重做日志文件和数据文件分开存放在不同的硬盘上，而且把重做日志文件放在读写速度较快的硬盘上，以减少硬盘写操作时的冲突次数。从安全的角度来讲，这种做法也是可取的。如果一个硬盘损坏，重做日志文件和数据文件不会同时损坏，数据库管理员可以根据没有损坏的文件进行数据库恢复。

5.1.3　控制文件的规划

控制文件是数据库中一个至关重要的文件，它的功能是维护数据库的状态，并且在实例和数据库之间建立对应关系。数据库服务器在启动时，根据控制文件的内容查找数据文件，然后打开数据文件。如果控制文件损坏，数据库服务器将无法正常启动。

在一个数据库中可以只有一个控制文件，但是考虑到这个文件的特殊重要性，应当为它建立多个镜像文件，并且将它们存放在不同的硬盘上。如果一个控制文件损坏，数据库服务器可以通过其他控制文件启动。数据库中的多个控制文件互为镜像，它们的内容是完全相同的。数据库服务器在启动时，只要读其中一个文件即可，而在修改控制文件时，则同时写入所有控制文件。

5.1.4　重做日志文件的规划

重做日志文件的内容是对用户的DDL和DML操作所做的记录，它的功能是在系统出现故障时对数据库进行恢复。

由于重做日志文件的重要性，对重做日志文件有一些特殊的要求。首先，在数据库中需要定义多个重做日志文件组。其次，在每个重做日志文件组中应该包含多个重做日志文件，而且同一个重做日志文件组中的日志文件应该存放在不同的硬盘上。

确定重做日志文件组的合适数量的一个好方法是观察数据库服务器的运行情况，检查是否出现影响检查点和日志切换的故障。如果重做日志文件组太少，可能会影响检查点的发出，也可能会影响日志归档的正常进行。数据库管理员应当经常查看警告文件和跟踪文件，检查是否有这类信息产生，最后确定一个不产生任何不利影响的最小数目。

5.1.5　参数文件的规划

参数文件的功能是为实例提供初始化参数。实例在启动时，将读取参数文件的内容，并且进行一些初始化的工作，如获得数据库名称，设置SGA，设置数据库操作模式等。

Oracle 11g提供了200多个初始化参数，其中只有少数初始化参数需要明确指定参数值，其余大部分参数都有默认值，如果在参数文件中没有为某个初始化参数指定参数值，数据库服务器将自动采用它的默认值。

Oracle 11g提供了两种类型的参数文件，一种是服务器端的参数文件，一种是文本参数文件。在默认情况下实例启动时，数据库服务器将读取服务器端参数文件的内容。

服务器端的参数文件是一个二进制文件，用户不能直接编辑这个文件的内容，否则可能使数据库服务器下次无法启动。有些初始化参数可以在数据库服务器运行过程中动态修改，修改的结果保存在这个参数文件中，对初始化参数修改通过命令ALTER SYSTEM完成。使用服务器端的参数文件好处是，通过ALTER SYSTEM可以很方便地修改初始化参数，这样减少了数据库服务器重新启动的次数。

文本参数文件有两个功能，一是在创建实例时为其提供初始化参数，二是在启动实例时为实例提供初始化参数。在默认情况下数据库服务器不会读这个文件，所以需要在启动实例的命令行中通过pfile选项指定一个文本参数文件。在这个文件中修改一个初始化参数的值，然后再用这个文件重新启动实例，新的参数值将起作用。对于那些不能动态修改的初始化参数，可以在文本参数文件中进行修改，然后再用这个文件重新启动实例，最后再重新创建服务器端的参数文件。

在创建实例时尚不存在服务器端的参数文件，这时需要指定一个文本参数文件，利用这个参数文件创建实例，并启动这个实例。文本参数文件默认位于Oracle安装目录的dbs子目录（UNIX/Linux系统）中，或者位于database子目录（Windows系统）中，文件的命名规则为init＜SID＞.ora，其中SID为实例的名称。如果希望使用另外一个文本参数文件，而不是默认的参数文件，需要在启动实例时通过pfile选项明确指定。

获得文本参数文件的一个简单方法是根据一个标准参数文件的内容，修改必要的参数，或者添加自己需要的参数，最后用指定的文件名保存在指定的目录即可。Oracle提供了一个模板文件，用户可以修改其中的相关内容，构造自己的参数文件。这个文件位于Oracle安装目录下，文件名为init.ora。

在服务器端参数文件和文本参数文件中，Oracle推荐使用服务器端参数文件，而且这是默认使用的参数文件。但这个文件并不自动产生，需要根据一个文本参数文件的内容来创建，创建方法是首先构造一个文本参数文件，然后用这个参数文件创建实例，并启动这个实例，最后在SQL*Plus中执行CREATE命令创建服务器端的参数文件。

在表5.2中列出了一些最重要的初始化参数。

5.2　如何利用DBCA创建数据库

Oracle提供了一个图形化的实用工具—DBCA（Database Configuration Assistant），利用这个实用工具，用户可以方便地创建数据库，而不必记忆复杂的命令，不必了解每个初始化参数的意义，也不必了解SGA的分布，用户只需要输入少量的信息，通过键盘、鼠标进行简单的操作，便可轻松地对创建数据库。

DBCA实用工具能够减少用户工作量的真正原因是它提供了许多数据库模板，这些模板为用户提供了一些创建数据库的方案，在每个方案中对数据库的各个设置都指定了合理的默认值，如初始化参数、数据文件、SGA等。用户只需要根据数据库的用途，选择其中一个模板，并根据需要输入少量信息，或者修改少量信息，DBCA便可根据默认方式或用户希望的方式创建数据库。用户也可以创建自己的模板，甚至可以从一个数据库创建模板。

在Windows系统中启动DBCA实用工具的方法是在开始菜单中选择“Oracle—＜ORACLE_HOME＞”-＞“Configuration and Migration Tools”-＞“Database Configuration Assistant”。在UNIX/Linux系统中启动DBCA的方法是：以oracle用户登录系统，在命令提示符下输入命令dbca。在DBCA运行时首先显示的是欢迎信息，在这个窗口中单击“下一步”按钮，将打开“操作”窗口，如图5.1所示。从这个窗口中提供的菜单可以看出，DBCA具有以下功能：

·创建数据库

·配置数据库选项

·删除数据库

·管理数据库模板

在接下来的各节中，我们将详细介绍DBCA提供的每个功能。

图　5.1　DBCA操作的选择

在图5.1中选择“创建数据库”，并单击“下一步”按钮，将开始创建数据库的过程。在这个过程中，DBCA将按照我们选择的模板创建实例，按照默认的初始化参数创建参数文件，然后创建数据库，创建SYS和SYSTEM用户，并使数据库具有该模板指定的功能。

在接下来的窗口中，用户可以根据自己的实际情况，选择其中一个模板，使自己的数据库具有该模板指定的功能，如图5.2所示。模板实际上是一组配置信息，它包含了创建数据库所必须指定的一些参数和选项。

在图5.2中显示的模板都是Oracle预先提供的。如果用户创建了自己的模板，这个模板也将显示在这个窗口中。各个模板的适用情况如表5.3所示。

上述模板按照是否包含数据文件可以分为两种类型：不包含数据文件的模板和包含数据文件的模板。在不包含数据文件的模板中，只有数据库结构的信息，用户可以任意指定和修改所有的数据库设置，也可以将数据库的设置信息保存为脚本文件。如果选择不包含数据文件的模板，则创建数据库需要花费较长的时间，因为必须运行脚本文件来创建所有的数据库对象。

图　5.2　选择数据库模板

在包含数据文件的模板中，既包含数据库的结构信息，也包含它的数据文件。如果选择该类模板，创建数据库的速度较快，因为在数据文件中已经存在数据库对象的信息。另外，控制文件和重做日志文件也将自动创建。用户可以添加或删除控制文件和日志组，也可以修改数据文件的位置和名称，但是不能添加或删除数据文件、表空间。

Oracle预定义的每个模板都提供了一些默认的设置，包括公共选项、初始化参数、字符集、数据文件、控制文件和重做日志文件。其中公共选项指定了数据库应该具有的标准功能；初始化参数提供了最重要的初始化参数值；字符集为数据库指定了默认的数据库字符集和国家字符集；而数据文件、控制文件和重做日志文件则是根据该模板创建数据库时将产生的一组文件。

如果希望了解每个模板的详细信息，可以在图5.2中选择一个模板，然后单击“显示详细资料”按钮，该模板提供的默认设置将在另一个窗口中列出来。图5.3显示了“Custom Database”模板的详细信息。为了查阅方便，用户还可以将每个模板的默认设置存储为一个HTML文件。

在图5.2中选择“Custom Database”模板后，单击“下一步”按钮，进入“数据库标识”窗口，如图5.4所示。在这个窗口中，用户需要输入数据库的全局名称和实例名称（SID）。

Oracle数据库通过“全局数据库名”来唯一地标识。全局数据库名是当前数据库区别于其他数据库的数据库全称。全局数据库名的形式为“数据库名.域名”，其中数据库名是为数据库取的一个简称，域部分指定数据库所处的数据库域，Oracle建议将数据库域映射为网络域。SID是当前实例的标识，对于任何数据库，至少需要一个实例，每个实例对应一个SID和一组文件。例如，当创建SID为TEST的数据库时，将同时创建实例以及数据库文件（包括参数文件、控制文件、重做日志文件和数据文件）。一般情况下SID与数据库名相同。

图　5.3　模板详细信息的查看

图　5.4　指定数据库标识

注意，如果希望通过命令的方式访问数据库，在操作系统中必须设置ORACLE_SID环境变量，使其等于要访问的SID，否则将不能登录数据库。

数据库在创建之后，可以通过浏览器，以Web方式对其进行远程管理，这时需要对EM（企业管理器）进行设置，如图5.5所示。

Oracle在创建数据库的同时，也创建了几个用户，其中包括SYS和SYSTEM用户。在为这些用户指定口令时，可以为所有用户指定相同的口令，也可以分别指定不同的口令。无论是哪种情况，都需要再次输入口令以确认，如图5.6所示。Oracle 11g对口令的要求是：口令长度至少为8个字符，至少包含一个大写字符，一个小写字母，一个数字字符。

图　5.5　对EM进行配置

图　5.6　为用户指定口令

接下来，需要为数据库指定存储方式及存储位置，如图5.7所示。数据库中的文件最终是存储在磁盘上的，数据库文件在磁盘上有两种存储方式：文件系统和自动存储管理（ASM）。其中文件系统方式是传统方式，在操作系统中先对磁盘进行分区，或者创建逻辑卷，然后创建文件系统，数据库文件就存储在文件系统对应的目录中。自动存储方式是利用Oracle的ASM功能，把若干个磁盘组织为一个磁盘组，数据库文件存储在磁盘组上，这些文件对操作系统是不可见的。从Oracle 11g开始，不再支持裸设备，但是如果在以前版本的数据库中使用了裸设备，那么将其升级到11g时，可以继续使用裸设备。

如果在图5.7中选择了“文件系统”方式，那么数据库文件的存储位置有两种设置方法，一种是按照模板的设置，将文件存储在默认的位置。第二种方式是将所有文件存储在同一个目录中。如果选择“ASM”方式，那么需要选择一个已经存在的磁盘组，数据库中的所有文件将存储在这个磁盘组中。

图　5.7　存储方式的选择

接下来需要指定快速恢复区，如图5.8所示。快速恢复区是一个目录或者一个磁盘组，用来为备份文件和恢复文件指定默认存储位置，也可以作为归档日志文件的默认存储位置。当数据库发生故障时需要利用这里的文件对数据库进行恢复，管理员也可以通过Flashback技术，利用这里的文件，使数据库回到过去某个时间点。在图5.8中，需要指定快速恢复区的位置和大小，还可以同时启动归档日志模式。在默认情况下，数据库处于非归档模式下，如果选择了“启动归档”，那么数据库在创建之后即处于归档模式下。

图　5.8　快速恢复区设置

如果单击“编辑归档模式参数”按钮，可以进一步设置归档模式的相关信息，如图5.9所示。在这里指定归档日志文件的存储位置和文件名，点击“OK”返回。

在图5.8所示的窗口中单击“下一步”，接下来需要选择在数据库中可以执行的一些脚本（如果利用另外两个模板创建数据库，则没有这一步），如图5.10所示。在这个窗口中用户可以选择数据库需要具备的附加功能。例如，Oracle Spatial提供了一种在数据库中存储和检索多维数据的方法，这个功能主要用于在地理信息系统（GIS）来实现地理参考并解决某些查询。

图　5.9　归档日志文件的位置和名称

图　5.10　数据库附加功能的选择

在图5.10所示的窗口中单击“下一步”按钮，接下来需要对数据库可使用的内存大小以及数据库的属性进行设置，如图5.11所示。

图　5.11　SGA及数据库属性的设置

在图5.11中，可以分别设置SGA大小、数据块大小、数据库字符集和连接模式。分别单击窗口上部的四个标签，可以分别对这四种信息进行设置。

在Oracle数据库中，有很多的初始化参数，这些参数就是用来对SGA、数据库属性等信息进行设置的，大部分参数都具有默认值，用户可以根据需要对它们进行修改，或者采用它们的默认值。在图5.11中单击“所有初始化参数”按钮，将打开另外一个窗口，在这个窗口中列出了所有的初始化参数，如图5.12所示。在这个窗口中用户可以查看和修改所有的初始化参数的设置。如果需要修改某个初始化参数，在这个窗口中选择对应的行，并在对应的值列中输入新的参数值。初始化参数修改完毕后，单击“关闭”按钮，所有的修改将被保存起来。

内存设置用于指定SGA和PGA的大小，以及SGA中各种缓冲区的大小。从图5.11可以看出，有两种方案可供选择，一种是典型设置，一种是自定义设置。

典型设置是按照物理内存的大小将一定百分比或者指定大小的内存分配给SGA和PGA，并按照自动管理的方法自动指定SGA的各个缓冲区的大小。这种设置的好处是用户不必关心SGA中每种缓冲区的大小，只需要指定可分配给Oracle的最大内存大小即可，实例将对SGA和PGA进行自动管理。这就是在第4章中介绍的“自动内存管理”方法。

自定义设置允许用户根据自己的需要，分别为各个缓冲区以及PGA指定大小。这种方法更为灵活，用户可以根据数据库的实际用途控制SGA中各个缓冲区的大小。自定义设置又包括两种方法，一种是“自动共享内存管理”，另一种是“手工共享内存管理”。如果选择“自动共享内存管理”，只需要指定SGA的最大大小和PGA的大小，实例将自动指定SGA中各种缓冲区的大小。如果选择“手工共享内存管理”，那么SGA中各种缓冲区的大小都需要手工指定。

在图5.13中，可以设置数据块的大小和用户进程的数量。数据块的大小在数据库创建之前必须确定，而且在数据库创建之后不能再修改。进程数目是指将来可能访问数据库的进程的最大数目，其中包括DBWR等后台进程，所以这个数目至少需要设置为（后台进程数量加一个用户进程）。

图　5.12　所有的初始化参数

图　5.13　数据块大小和进程数目设置

字符集的设置用于设置数据库字符集和国家字符集，如图5.14所示。数据库字符集有三种设置方法，一种是采用默认字符集，这是Oracle根据操作系统的字符集自动指定的，第二种方法是采用unicode编码方式，第三种方法允许用户在多个字符集中选择其中一个。

国家字符集目前只有两个选择，一个是AL16UTF16，一个是UTF8。

字符集是在计算机屏幕上显示字符时所使用的编码方案，一种语言可以有多个不同的字符集，选择字符集首先要考虑的是希望在数据库中存储哪种语言。Oracle在以下情况中使用数据库字符集：

·对象的标识符，如表名、列名、PL/SQL变量等。

·CHAR、VARCHAR2、CLOB和LONG类型的列中存储的数据。

·输入并存储SQL和PL/SQL程序源代码。

在Oracle中，原来采用国家字符集的数据类型（NCHAR、NVARCHAR2和NCLOB）已重新定义为专有的Unicode数据类型。可以将Unicode字符集存储到这些数据类型的列中。Unicode数据类型（NCHAR、NVARCHAR2、和NCLOB）的国家字符集仅限于AL16UTF16或UTF8。默认的国家字符集是AL16UTF16。

图　5.14　字符集的设置

Oracle提供了两种数据库连接模式，即专用服务器模式和共享服务器模式。在专用服务器模式下，每个用户进程都需要有一个专用服务器进程为其服务。Oracle建议在数据仓库中，或者用户进程比较少的情况下采用这种模式。在共享服务器模式下，多个用户进程可以共享一个服务器进程，并且在数据库服务器中可以启动多个服务器进程。连接模式的选择如图5.15所示。

图　5.15　连接模式的选择

如果用户选择了共享服务器模式，数据库服务器将按照默认设置启动调度程序和服务器进程。当然，用户也有机会指定调度器和服务器进程的数目。在图5.15所示的窗口中选择共享服务器模式，并单击“编辑共享服务器参数”，进入“共享服务器模式”的设置窗口，如图5.16所示。

在共享服务器模式中可以对共享服务器进程和调度器的数目进行序设置，图5.16表示共享服务器进程数目的配置。可以根据情况指定服务器进程及调度器的最大数目和最小数据。如果需要修改某个设置，用鼠标选中对应的复选框，并在对应的文本框中输入相应的数目，然后单击“确定”按钮即可。

在对内存、数据块大小、字符集和连接模式进行设置后，单击“下一步”按钮，将进入“数据库存储”窗口，如图5.17所示。在这个窗口中列出了将要被创建的数据库中所包含的控制文件、数据文件、重做日志文件和表空间。在这里也可以对表空间、数据文件、重做日志文件进行增加、删除和修改等操作。

图　5.16　共享服务器模式设置

图　5.17　数据库的文件和表空间

在窗口左侧以树形列表的形式列出了即将被创建的数据文件、控制文件、重做日志文件和表空间，每一类对象都以文件夹的形式组织在一起。随着所选模板的不同，树形列表的内容也不完全相同。

单击窗口左侧的某个文件夹，在窗口右侧将列出这个文件夹中包含的所有对象。此时如果单击树型列表下侧的“创建”按钮，可以向该文件夹中添加成员。如果双击某个文件夹，或单击该文件夹前面的“+”标志，该文件夹即被展开。如果选择其中一个成员，该成员的详细信息将被显示在窗口右侧。此时如果单击树型列表下侧的“删除”按钮，可以将该成员删除。

这里仅以控制文件的处理为例来说明对数据库存储的设置。在图5.17中的窗口左侧选择“控制文件”，在窗口右侧将列出所有的控制文件。用户可以修改这些文件的名称和位置，在文件列表的最后一行也可以添加新的控制文件。单击窗口右侧的“选项”标签，可以修改最大数据文件数、最大重做日志文件数和最大日志成员数这三个参数。

在图5.17中单击“下一步”按钮，将进入“创建选项”窗口，如图5.18所示。在这个窗口中用户可以控制数据库的创建操作。有三种创建操作可供用户选择：

·创建数据库。

·另存为数据库模板。

·生成数据库创建脚本。

这三个操作可以单独执行，也可以同时执行。如果要执行某个或某几个操作，只要选种对应的复选框，然后单击“完成”按钮即可开始执行所选操作。

“创建数据库”操作将按照模板提供的设置和用户的设置创建数据库，包括创建数据文件、控制文件、参数文件、用户等。这个操作将需要较长的一段时间才能完成。

“另存为数据库模板”操作提供了一种创建模板的方法，用户可以将前面所有的设置存储为一个模板，这样以后就可以利用这个新的模板创建数据库。创建的模板将在下一次启动DBCA时出现在图5.2所示窗口的模板列表中。用户需要为这个模板指定一个名称，也可以为模板指定一段描述信息。在默认情况下模板文件将存储在Oracle安装目录下的assistants/dbca/templates子目录中。

“生成数据库创建脚本”将把这次数据库创建过程转化为一些脚本文件，在这些脚本文件中包含了创建数据库的命令序列。通过执行这些脚本文件，也可以创建数据库。在这些文件中有一个Shell脚本文件，文件名为＜SID＞.sh，它是这些文件中第一个需要执行的文件，通过这个文件可以调用其他脚本文件，其中SID为实例名称。在Windows系统中，这个文件是一个批处理文件，文件名为＜SID＞.bat。以后用户创建数据库时，只需要在操作系统的命令行中执行这个脚本文件，其他脚本文件将被自动调用执行，数据库将被自动创建。读者如果对命令行创建数据库的方式不太熟悉，可以把这些文件作为参考。在这里也生成了一个文本参数文件。

在图5.18的窗口中选择“创建数据库”，然后单击“Finish”按钮，接下来将显示当前数据库的属性信息，如图5.19所示。

图　5.18　数据库的创建

在图5.19中单击“OK”按钮，便开始创建数据库，如图5.20所示。这个过程需要持续较长时间。在创建结束后，将出现一个窗口，显示数据库所使用的各个端口号。至此，整个创建过程结束。

图　5.19　数据库的属性总结

图　5.20　开始创建数据库

5.3　如何利用命令行创建数据库

利用命令行创建数据库是一个比较复杂的过程，主要涉及编辑文本参数文件、创建实例、创建数据库、执行必要的脚本等。

5.3.1　编辑文本参数文件

启动实例时，需要读参数文件。在创建数据库之前，需要手工编写一个文本参数文件，利用这个文件启动实例。实例启动后，可以把这个文件转换为服务器参数文件。以下是一个文本参数文件的例子：

db_name=test

db_domain=""

dispatchers="（PROTOCOL=TCP）（SERVICE=testXDB）"

audit_file_dest=/u01/app/base/admin/test/adump

compatible=11.2.0.0.0

remote_login_passwordfile=EXCLUSIVE

processes=150

undo_tablespace=UNDOTBS1

control_files=（"/u01/app/base/oradata/test/control01.ctl"，"/u01/app/base/oradata/test/control02.ctl"）

audit_trail=db

memory_target=1717567488

db_block_size=8192

open_cursors=300

在UNIX/Linux系统中，这个文件默认位于目录ORACLE_HOME/dbs中，文件名称为init＜SID＞.ora。在Windows系统中，这个文件默认位于目录ORACLE_HOME/database中。其中ORACLE_HOME为Oracle的安装目录，SID为实例的名称。

需要注意的是，在这个文本参数文件中出现的所有目录都需要手工创建，并确保oracle用户对目录有写权限。

5.3.2　实例的管理

在创建数据库之前，首先要创建一个实例，当删除一个数据库时，应该将实例一起删除。实例的管理涉及实例的创建、删除、启动与关闭，以及口令文件管理等方面的内容。需要注意的是，本小节介绍的内容只适用于Windows系统。

Oracle提供了一个实用工具，即oradim，该命令的功能是管理实例，通过这个命令可以创建、删除、启动与关闭实例。这个命令的格式为：

oradim主要选项附加选项

其中主要选项决定了所要进行的操作，而附加选项是依赖于主要选项的。oradim命令有以下主要选项：

-new：创建一个新实例。

-delete：删除一个实例。

-startup：启动一个实例。

-shutdown：关闭一个实例。

-edit：编辑一个实例。

如果需要创建实例，需要执行带-new选项的oradim命令。在创建实例之前，首先要创建一个文本参数文件，为实例提供初始化参数。例如，如果要创建名为ORCL的实例，oradim命令将在默认的目录中搜索名为initORCL.ora的参数文件，如果没有找到这个文件，实例创建将以失败告终。如果这个文件在其他目录下，那么在启动实例时需要通过pfile参数来指定文件的路径。

创建实例的命令格式为：

oradim-new-sid 实例名称-intpwd口令-startmode 启动方式-pfile 参数文件

除了-new为主要选项外，其余选项为附加选项，这些选项的意义如下：

-sid：指定实例的名称。

-intpwd：为特权用户SYS指定口令。

-startmode：指定实例的启动方式，可选方式有自动方式（a）和手工方式（m）。

-pfile：指定一个文本参数文件。在默认情况下使用默认的文本参数文件，否则需要通过这个选项指定另外一个参数文件。

例如：

c：\＞oradim-new-sid test-intpwd 1234-startmode m-pfile c：\scripts\init.ora

这条命令创建了实例test，它的启动方式为自动启动，特权用户SYS的口令为1234，在创建实例时使用了非默认的参数文件c：\scripts\init.ora。

SYS用户是数据库的特权用户，它拥有最大的权限，同时具有SYSDBA和SYSOPER身份。这样的用户及其口令存储在数据库的口令文件中。在创建实例时，口令文件将自动产生。

当指定实例的启动方式为自动启动时，实例在创建之后便自动启动到NOMOUNT状态，否则需要手工启动。实例启动后，就可以创建数据库了。

创建实例之后，在系统中产生了以下结果：

·创建了一个实例。

·创建了口令文件。

·创建了一个Windows的系统服务。

如果在创建实例时指定它的启动方式为手工启动，那么实例在创建之后我们需要手工启动它。启动实例的命令格式为：

oradim-startup-sid 实例名称-pfile 参数文件

在Windows系统中，创建实例的同时，也创建了一个系统服务，所以也可以通过这个系统服务来启动、关闭实例。例如，在Windows系统的命令行中执行下面的命令，可以启动系统服务OracleServicetest，同时启动了实例test：

c：\＞net start OracleServicetest

其中OracleServicetest为实例test所对应的系统服务名称。系统服务的命名原则是在字符串“OracleService”之后加上实例的名称。

既然在Windows系统中每个实例都有一个对应的系统服务，那么利用“服务”组件也可以启动、关闭实例。方法是打开控制面板，双击“管理工具”组件，然后打开“服务”组件。在列出的所有系统服务中选择要启动的系统服务，再单击启动按钮，该系统服务即可启动，对应的实例也自动启动。利用“服务”组件启动实例的方法如图5.21所示。

实例被关闭后，SGA存储区将被释放，所有后台进程也将终止，用户将暂时无法访问数据库。关闭实例的命令格式为：

oradim-shutdown-sid 实例名称

例如，执行下面的命令将关闭实例test：

c：\＞oradim-shutdown-sid test

同样，也可以通过关闭系统服务来关闭对应的实例。例如，执行下面的命令将关闭系统服务OracleServicetes，同时关闭实例test：

c：\＞net stop OracleServicetes

利用“服务”组件关闭实例的方法与启动实例的方法类似，在图5.21中选择需要关闭的系统服务，然后单击停止按钮，即可关闭该系统服务及对应的实例。

图　5.21　利用系统服务启动实例

删除实例的命令格式为：

oradim-delete-sid实例名称

在删除数据库时，应该将实例一起删除。实例被删除后，用户将永远无法访问数据库。在Windows系统中，删除实例时，对应的系统服务将被同时删除。

实例在创建之后，我们可以编辑它的一些属性，如实例名称、启动方式等。编辑实例的命令格式为：

oradim-edit-sid 实例名称-newsid 实例的新名称-intpwd 口令

-startmode 启动方式-pfile 参数文件

在编辑实例时，可以通过选项-newsid为实例指定一个新的名称。实例名称被修改后，口令文件的名称随即也被自动修改。在编辑实例时，有时需要提供该实例的口令，选项-intpwd的作用是指定实例原来的口令。需要注意的是，通过这条命令是无法修改口令的，只能修改口令文件名称，如果需要修改口令本身，只能执行命令orapwd。-startmode选项的作用是改变实例的启动方式，可选方式仍然为自动方式和手工方式。如果将实例的启动方式改为自动方式，实例将随即自动启动。选项-pfile的作用是指定一个非默认的参数文件。

需要注意的是，实例的名称被修改后，文本参数文件的名称应该与实例的新名称对应。编辑实例时参数文件的名称是不会自动改变的，需要手工修改。

例如，下面的命令将把实例test的名称改为test1，启动方式改为手工方式：

oradim-sid test-newsid test1-startmode m

5.3.3　口令文件的创建

口令文件用来记录具有SYSDBA或者SYSOPER限的用户的口令。在UNIX/Linux系统中，口令文件存放在Oracle安装目录的dbs子目录下，文件命名规则是：orapw＜sid＞.ora，其中＜sid＞指的是实例名称。在Windows中，口令文件存储在database子目录下，文件的命名规则是：PWD＜sid＞.ora。

在实例刚刚创建时，这个文件只包含SYS一个用户。在实例的运行过程中，如果一个普通用户获得了SYSDBA或者SYSOPER权限，这个用户将被自动添加到这个文件中。

Oracle提供了一个orapwd命令，它的功能是管理口令文件，包括创建口令文件、改变口令文件的位置和名称、修改SYS用户的口令等。命令的执行格式为：

orapwd file=口令文件password=口令entries=用户数目

其中参数file用于指定口令文件的位置和名称，password参数用于指定SYS用户的口令，entries用于指定口令文件可以记录的最大用户数目。例如，下面的命令为实例orcl创建口令文件：

orapwd file=/u01/app/oracle/dbs/orapworcl.ora password=1234 entries=30

在创建实例时，如果在命令oradim中使用-intpwd选项为SYS用户指定了口令，那么口令文件将自动创建。在实例运行过程中，可以废弃以前的口令文件，创建新的口令文件，同时可以修改SYS用户的口令。

为了管理方便，Oracle提供了动态性能视图v$pwfile_users，如果希望了解口令文件中的用户信息，可以查询这个视图，如：

SQL＞SELECT*FROM v$pwfile_users；

USERNAME SYSDBA SYSOPER

------------------------------------------------

SYS TRUE TRUE

其中USERNAME列是指口令文件中的用户，SYSDBA列指定用户是否具有SYSDBA权限，而SYSOPER列指定该用户是否具有SYSOPER权限。

如果一个用户具有SYSDBA或SYSOPER权限，这个用户将被记录在口令文件中。例如，以下命令将创建用户user1，并授予这个用户SYSDBA和SYSOPER权限：

SQL＞CREATE USER user1 IDENTIFIED BY"1234"；

SQL＞GRANT SYSDBA TO user1；

SQL＞GRANT SYSOPER TO user1；

这时如果再查询动态性能视图V$PWFILE_USERS，结果如下：

SQL＞SELECT*FROM v$pwfile_users；

USERNAME SYSDBA SYSOPER

-------------------------------------------------

SYS TRUE TRUE

USER1 TRUE TRUE

如果执行命令REVOKE从某用户回收了SYSDBA和SYSOPER权限，这个用户将被从口令文件中删除，但这个用户在数据库中依然存在。

对口令文件有影响的一个初始化参数是REMOTE_LOGIN_PASSWORDFILE，这个参数的作用是指定是否用口令文件来验证SYS用户的登录。这个参数有三个可选值：

·NONE：忽略口令文件，SYS用户只能通过操作系统进行验证。

·EXCLUSIVE：用户登录时，通过口令文件验证SYS用户的身份。

·SHARED：口令文件将在多个数据库之间共享。

当把初始化参数REMOTE_LOGIN_PASSWORDFILE的值指定为EXCLUSIVE时，系统通过口令文件验证SYS用户的身份。在这种情况下，同一个系统中不同的数据库都有一个单独的口令文件，并且在口令文件中可以容纳多个具有SYSDBA或SYSOPER权限的用户。

当把初始化参数REMOTE_LOGIN_PASSWORDFILE的值指定为SHARED时，系统也通过口令文件验证SYS用户的身份，但是同一个系统中多个数据库之间共享同一个口令文件，用同一个SYS用户可以登录所有的数据库服务器。在这种情况下，口令文件中只能包含SYS一个用户。

当某个用户以SYSDBA或SYSOPER身份登录数据库服务器时，需要使用AS SYSDBA或AS SYSOPER子句，例如：

SQL＞CONN scott/tiger AS SYSDBA

这时系统实际上将用户的身份切换到了SYS用户，这一点通过以下命令的执行结果可以得到验证：

SQL＞SHOW USER

USER为"SYS"

5.3.4　数据库的创建

实例创建以后，现在可以创建数据库了。创建数据库的命令是CREATE DATABASE，这条命令只能由SYS用户或者其他具有SYSDBA权限的用户执行。创建数据库的操作涉及创建数据文件、创建表空间、指定字符集及国家代码、创建重做日志文件等。

在创建数据库之前，应首先确保实例已经启动，然后以SYS用户或者其他具有SYSDBA权限的用户连接实例，将实例启动到NOMOUNT状态。为了确保用户能够连接到正确的实例，在此之前还需要设置系统变量ORACLE_SID的值。以下是在Windows系统中启动实例test的命令序列：

C：\＞set ORACLE_SID=test

C：\＞oradim-startup-sid test

C：\＞sqlplus sys/1234 AS SYSDBA

SQL＞STARTUP NOMOUNT pfile='c：\init.ora'

以下是在UNIX/Linux系统启动实例test的命令序列：

$ORACLE_SID=test

$export ORACLE_SID

$sqlplus sys/1234 AS SYSDBA

SQL＞STARTUP NOMOUNT pfile='/home/oracle/init.ora'

当把实例启动到NOMOUNT状态时，SGA在内存中已经存在，并且后台进程已经启动。只有在这种情况下才能执行CREATE DATABASE命令。以下是创建数据库test的CREATE DATABASE命令：

SQL＞CREATE DATABASE test

MAXINSTANCES 1

MAXLOGFILES 5

MAXLOGMEMBERS 5

MAXLOGHISTORY 1

MAXDATAFILES 100

LOGFILE GROUP 1（'/u01/app/oracle/test/redo01.log'）SIZE 100M，

GROUP 2（'/u01/app/oracle/test/redo02.log'）SIZE 100M，

GROUP 3（'/u01/app/oracle/test/redo03.log'）SIZE 100M

DATAFILE'/u01/app/oracle/test/system01.dbf'SIZE 325M REUSE EXTENT MANAGEMENT LOCAL

SYSAUX DATAFILE'/u01/app/oracle/test/sysaux01.dbf'SIZE 325M REUSE DEFAULT

TEMPORARY TABLESPACE tempts1 TEMPFILE'/u01/app/oracle/test/temp01.dbf'SIZE 20M REUSE

UNDO TABLESPACE undotbs DATAFILE'/u01/app/oracle/test/undotbs01.dbf'

SIZE 200M REUSE AUTOEXTEND ON MAXSIZE UNLIMITED

CHARACTER SET US7ASCII

NATIONAL CHARACTER SET AL16UTF16；

其中位于CTEATE DATABASE命令之后的字符串“test”为数据库名，它必须与初始化参数DB_NAME的值相同。在CTEATE DATABASE命令中可以使用很多的参数，利用这些参数可以在创建数据库的同时创建表空间、创建重做日志文件、指定字符集等。表5.4列出了上述命令中各个参数的含义。

在上述CREATE DATABASE命令中，第一个DATAFILE参数指定了SYSTEM表空间对应的数据文件system01.dbf，然后分别指定了SYSAUX表空间、UNDO表空间和临时表空间的数据文件，每个数据文件都通过SIZE子句指定初始大小。REUSE子句的作用是当该文件已经存在时即覆盖它。AUTOEXTEND子句的作用是指定该数据文件是否可以扩展，如果指定为ON，则该文件被写满时可以自动扩展，每次扩展的大小通过子句NEXT指定，但文件的最大大小不能超过MAXSIZE子句指定的数值。

表空间区管理模式有目前一般只使用一种，即本地管理模式。需要注意的是，如果在上述语句中没有将SYSTEM表空间的区管理方式指定为“EXTENT MANAGEMENT LOCAL”，那么SYSTEM表空间的区管理方式将是DICTIONARY，即字典管理方式。

字符集和国家字符集规定了在表中可以存储的字符编码。在指定这些信息时需要仔细选择，因为数据库在创建之后就很难再修改了。

LOGFILE参数的作用是指定数据库的重做日志文件，其中通过GROUP子句指定了若干个重做日志组，每个重做日志组中包含若干个重做日志文件，每个文件的大小通过SIZE子句指定。在本例中创建了三个重做日志组，每个重做日志组中有一个文件，所有重做日志文件的大小都是100MB。

当CREATE DATABASE命令成功执行后，在系统中将产生以下结果：

·创建了数据文件、重做日志文件和控制文件，其中数据文件和重做日志文件在命令文本中指定，控制文件在参数文件中指定。

·创建了SYSTEM用户，它的默认口令为manager。

·创建了SYSTEM表空、SYSAUX表空间、UNDO表空间、临时表空间，并在SYSTEM表空间上创建了一个回滚段。

·创建了数据字典基表。

在执行CREATE DATABASE命令时，系统将自动执行脚本文件sql.bsq，这个文件位于Oracle安装目录下的rdbms/admin子目录中，执行这个命令的结果是创建了SYSTEM用户、创建了SYSTEM表空间、创建了数据字典基表、在数据字典上建立了索引，并创建了一系列角色。

5.3.5　如何创建数据字典视图

数据字典基表是一个非常重要的系统表，在这个表中记录了重要的系统信息，如实例的结构、用户权限、数据文件等信息。对这个表的维护由数据库服务器自动完成，普通用户无法直接访问这个表，特权用户可以查看这个表的内容，但不能修改表的内容。

当数据库创建之后，数据字典基表被自动创建。为了使用户能够查看数据库中的重要信息，需要为这个基表创建一些有用的视图，用户可以通过这些视图方便地查看系统信息。

Oracle提供了两个脚本文件，在数据库中执行这两个脚本文件可以创建数据字典视图。这两个文件位于Oracle安装目录的rdbms/admin子目录下，文件名分别为catalog.sql和catproc.sql。其中文件catalog.sql用于创建大部分视图，包括动态性能视图。文件catproc.sql用于创建PL/SQL程序包和其他视图。PL/SQL程序包是Oracle提供的一组PL/SQL程序的集合，用户可以直接执行这些程序。

在创建数据字典视图时，用户必须以SYS身份登录数据库服务器，然后将上述两个文件调入内存并执行。以下是在SQL*Plus中创建数据字典视图的情况：

SQL＞@$ORACLE_HOME/rdbms/admin/catalog

SQL＞@$ORACLE_HOME/rdbms/admin/catproc

其中字符串$ORACLE_HOME的意思是引用环境变量ORACLE_HOME的值，这个环境变量代表Oracle软件的安装目录。在Windows系统中，引用这个环境变量的字符串是%ORACLE_HOME%。

5.3.6　如何创建默认的profile

profile是对用户使用口令以及使用系统资源所施加的限制。在默认情况下，对用户的限制是UNLIMITED，即没有限制。为了对用户的口令、会话及资源请求进行控制，需要创建profile。

创建profile需要以SYSTEM用户的身份执行脚本文件pupbld.sql，这个文件位于Oracle安装目录的sqlplus/admin子目录下。以下是创建profile的命令：

SQL＞CONN system/manager

SQL＞@$ORACLE_HOME/sqlplus/admin/pupbld

5.3.7　如何创建SCOTT模式

考虑到用户测试、学习的需要，Oracle提供了一个样例模式，在这个模式中包含一个用户SCOTT和若干个样例表，用户可以利用这个模式进行测试。用户可以以SYS或SYSTEM的身份执行脚本文件utlsampl.sql，这个文件位于Oracle安装目录的rdbms/admin子目录下。以下是以SYSTEM身份执行这个脚本的情况：

SQL＞CONN system/manager

SQL＞@$ORACLE_HOME/rdbms/admin/utlsampl

执行这个脚本的结果是首先创建了用户SCOTT，然后在SCOTT模式中创建了emp、dept和bonus等几个表。

5.3.8　如何创建服务器参数文件

创建服务器端参数文件的命令格式为：

CREATE SPFILE='服务器端参数文件'FROM PFILE='文本参数文件'

例如：

SQL＞CREATE SPFILE='/u01/app/oracle/dbs/spfiletest.ora'

FROM PFILE='/home/oracle/init.ora'

服务器端参数应该放在默认的存储位置，文件名也应该采用默认的命名规则，这样实例在启动时，就可以自动找到这个文件。

反过来，我们也可以根据服务器参数文件的内容创建文本参数文件。对应的命令格式为：

CREATE PFILE='文本参数文件'FROM SPFILE='服务器端参数文件'

例如：

SQL＞CREATE PFILE='/home/oracle/init.ora'

FROM SPFILE='/u01/app/oracle/dbs/spfiletest.ora'

在个别情况下，通过ALTER SYSTEM命令修改初始化参数可能会导致服务器参数文件格式错误，这样将导致数据库服务器无法重新启动。这时需要先根据服务器参数文件的内容创建文本参数文件，然后在文本参数文件中修改初始化参数的值，并利用这个文件启动数据库实例，最后再根据文本参数文件创建服务器参数文件。

5.4　数据库服务器的启动和关闭

Oracle服务器的启动分为三个步骤：启动实例，装载数据库，打开数据库。

在启动实例时，实例将读取参数文件的内容，根据参数的设置分配系统全局区（SGA），建立后台进程，并打开其他一些文件。在默认情况下将读取服务器端的参数文件。如果希望读取另一个参数文件，需要通过PFILE关键字指定参数文件的路径和名称。

装载数据库时，实例将打开控制文件，取得数据库的数据文件和重做日志文件的名称和状态，并在实例和数据库之间建立关联关系。

打开数据库的结果是实例打开所有的数据文件和重做日志文件，为用户访问数据做好准备。如果控制文件、数据文件和重做日志文件的状态不一致，还要进行实例恢复。至此，Oracle服务器启动完毕。

在对数据库进行管理时，常常需要根据某个目的，将数据库服务器启动到某个指定状态，这时可以分步启动。在SQL*Plus中执行STARTUP命令，可以启动数据库服务器，这条命令有若干个选项，用于控制Oracle服务器的启动过程。STARTUP命令的格式如下：

STARTUP 选项

可以使用的选项有：

·nomount：读参数文件，启动实例。

·mount：打开控制文件，并装载数据库。

·open：打开数据库中的数据文件和重做日志文件。

·force：重新启动数据库服务器。

·pfile=文件名：指定文本参数文件。

如果不带任何选项，执行STARTUP命令的结果是启动实例，并打开数据库。数据库管理员可以使用上述选项，控制数据库服务器的启动过程。例如：

SQL＞STARTUP NOMOUNT PFILE='/home/oracle/init.ora'；

然后可以将数据库服务器的状态转入更高级别，例如：

SQL＞ALTER DATABASE MOUNT；

SQL＞ALTER DATABASE OPEN；

数据库管理员可以从动态性能视图v$instance中查看实例目前的状态，例如：

SQL＞SELECT instance_name, status FROM v$instance；

关闭数据库服务器的命令是SHUTDOWN。关闭数据库服务器的过程跟启动过程正好相反，首先关闭数据文件和重做日志文件，然后关闭控制文件，最后关闭实例。可以通过几个参数控制关闭的方式。SHUTDOWN命令的格式如下：

SHUTDOWN选项

可以使用的选项有：

·NORMAL：默认方式，正常关闭数据库服务器。

·TRANSACTIONAL：正常关闭数据库服务器，如果有未提交的事务，则等待。

·IMMEDIATE：关闭数据库服务器，如果有未提交的事务，则回滚。

·ABORT：关闭数据库服务器，如果有未提交的事务，则回滚。

表5.5对四种关闭数据库服务器的方式进行了比较。

在Oracle 11g中，还可以把实例从较高级别的状态转到较低级别的状态。例如，下面的命令用于关闭数据库，把实例从open状态转到mount状态：

SQL＞ALTER DATABASE CLOSE；

在UNIX/Linux系统中，还可以使用Oracle的命令dbstart和dbshut来启动和关闭数据库服务器。如果数据库是利用DBCA工具创建的，那么在/etc目录下将产生一个文件oratab，在文件中包含以下内容：

orcl：/u01/app/oracle：N

其中每行代表一个数据库实例，在每行中，第一个字段代表实例名称，第二个字段为Oracle软件的安装目录，第三项为YES或者NO。如果把第三项改为Y（即YES），那么在操作系统中执行dbstart时将启动这个数据库实例，执行dbshut时将关闭它。

如果数据库是通过命令行创建的，那么文件/etc/oratab需要手工创建，文件内容也需要手工添加。

5.5　如何利用NET Manager配置客户端与服务器端的通信

数据库创建之后，就可以配置服务器与客户端的通信了。现在的数据库大多数都运行在网络环境中，所以配置数据库的远程访问是十分必要的。

Oracle具有自己的通信协议，这种协议以低层的网络协议如TCP/IP为基础，向用户提供远程的访问机制。为了处理远程的客户请求，在数据库服务器端需要创建监听器，并把实例注册到监听器中。

5.5.1　监听器的创建

Oracle提供了一个图形化的实用工具NET Manager，用来创建监听器。在Windows系统中，从开始菜单中可以找到这个工具，在UNIX/Linux中，以oracle用户登录系统，执行netmgr命令，即可启动这个程序，如图5.22所示。

在图5.22中，单击“Listeners”文件夹，在窗口右侧将列出已经创建的监听器。单击创建左上角的绿色“+”，创建一个新的监听器，如图5.23所示。

图　5.22　NET Manager实用工具

图　5.23　指定监听器的名称

在图5.23中指定监听器的名称。默认情况下第一个监听器的名称为LISTENER，第二个监听器的名称为LISTENER1，依此类推。单击“OK”按钮，监听器便创建成功。如图5.24所示。

在图5.24中，单击右右侧的“Add Address”按钮，指定监听器所监听的IP地址和端口号。默认情况下监听本机所有IP地址的1521端口。

在图5.24中右上侧的下拉列表框中选择“Database Services”，把实例注册到当前监听器中，这样监听器就可以监听所有的发送到这个实例上的用户请求，如图5.25所示。在这里需要输入数据库的名称，Oracle软件的安装目录以及实例的名称。

图　5.24　指定监听器的监听地址和端口号

图　5.25　为监听器指定要监听的数据库实例

监听器配置结束后，单击“File”菜单中的“Save”子菜单，把所有的配置信息保存起来。

5.5.2　监听器的管理

Oracle提供了一个命令lsnrctl，用来对监听器进行状态查看、启动、关闭等控制。通过这个命令可以对所有的或者单个监听器进行控制。例如，下面的命令分别用于查看所有的监听器和某个特定的监听器的状态：

$lsnrctl status

$lsnrctl status LISTENER

下面的命名分别用来启动所有的和某个特定的监听器：

$lsnrctl start

$lsnrctl start LISTENER

下面的命令分别用来关闭所有的和某个特定的监听器：

$lsnrctl stop

$lsnrctl stop LISTENER

5.5.3　Oracle客户端的配置

凡是访问Oracle数据库的应用程序和实用工具，都可以认为Oracle客户端。在数据库服务器端创建并启动监听器之后，客户端就可以远程访问数据库了。客户端的配置随应用环境的不同而有所变化，如在Windows系统中配置ODBC数据源，在中间件服务器中配置JDBC数据源。如果希望通过SQL*Plus、RMAN等Oracle自带的工具远程访问数据库，则需要在客户端创建服务命名。

我们知道，为了访问远程的数据库服务器，在客户端需要指定服务器的IP地址、端口号以及数据库名称。在Oracle客户端创建服务命名中就包含了这些信息。

服务命名是通过客户端的NET Manager工具创建的，如图5.26所示。点击窗口左边的“Service Naming”文件夹，将显示已经创建的服务命名。单击左上角的绿色“+”，将创建一个新的服务命名。输入你自己喜欢的名称，然后单击“Next”按钮。

图　5.26　服务命名的创建

接下来需要选择底层的网络通信协议，默认为TCP/IP协议，如图5.27所示。

接下来需要输入数据库服务器的IP地址和端口号，端口号默认为1521，如图5.28所示。

接下来需要输入数据库实例的服务名称和连接模式，如图5.29所示。服务名称用来代表要连接的实例，一般与实例的名称相同。可以通过查看初始化参数SERVICE_NAME的方式来确定实例的服务名称。连接模式有两种可选值：共享模式和专用模式。

图　5.27　选择底层通信协议

图　5.28　指定数据库服务器的IP地址和端口号

服务命名创建成功之后，在窗口的右侧将显示该服务命名的详细信息，如图5.30所示。

图　5.29　指定数据库实例的服务名称和连接模式

图　5.30　服务命名的详细信息

单击“File”菜单的“Save”菜单项，将服务命名的配置信息保存在文件中，这个服务命名在客户端就可以使用了。下面的命令是通过SQL*Plus工具远程连接数据库服务器的例子：

$sqlplus scott/tiger@orcl

$sqlplus sys/1234@orcl as sysdba

在SQL*Plus中，也可以通过CONNECT命令远程连接数据库服务器。例如：

SQL＞conn scott/tiger@orcl

第6章　表空间的管理

表空间是数据库的逻辑组织形式，在一个数据库中可以创建多个表空间。表空间的主要作用是将不同用途的数据分离开来，以提高数据的安全性，并提高系统的性能。

表空间在数据库中起着非常重要的作用。一方面，表空间在逻辑结构上由多个段组成，数据都存储在表空间的段中；另一方面，表空间在物理结构上对应着多个数据文件，数据库对象中的数据最终存储在这些数据文件中。

表空间的类型较多，根据不同的分类标准，将得到不同的分类结果。根据存储数据类型的不同，表空间可分为系统表空间、UNDO表空间、临时表空间和用户表空间。根据存储空间方式的不同，表空间可分为字典管理表空间和本地管理表空间。根据是否支持大文件来划分，表空间可分为大文件表空间和小文件表空间。

表空间的管理主要涉及各种类型表空间的创建、删除、扩展、修改状态等。

6.1　表空间的结构

一个表空间由多个段组成，每个段可能代表一个数据库对象。当用户创建表、索引、簇等数据库对象时，在表空间中将自动创建一个段，以存储该对象的数据。一个段占用一个或多个区，当区的空间被写满后，段就会自动扩展。在创建表空间时就需要指定区的分配方式。

6.1.1　区管理方式

区管理方式指的是为一个数据库对象分配存储空间的方式，在创建表空间时需要指定区管理方式。Oracle支持两种区管理方式，字典管理和本地管理。

在创建字典管理的表空间时，需要指定若干存储参数，以后在这个表空间中创建数据库对象时，就按照这些存储参数为数据库对象分配所需要的区，当这些区被写满后，数据库服务器将按照存储参数为数据库对象分配新的区。表空间的存储参数记录在数据字典中。

在创建字典管理的表空间时，可以指定以下几个存储参数：

·INITIAL：指定第一个区的大小，即首先要为数据库对象分配的区的大小。

·NEXT：指定下一个区的大小。

·MINEXTENTS：为数据库对象分配的最少区的个数，默认值为1。

·MAXEXTENTS：最多为数据库对象分配的区个数。

·PCTINCREASE：从第三个区开始，每一个区在前一个区的基础上增长的百分比。

在字典管理表空间中，区的分配和回收都是基于数据字典进行的。当为数据库对象分配区时，需要从数据字典中查询存储参数，分配结束后，需要把分配的结果写入数据字典。这样要在数据字典上执行很多的查询操作和DML操作，并且产生重做日志和回滚数据。另外，由于数据库对象中的区大小不同，随着数据库服务器的运行，在段中将产生越来越的存储碎片。

在本地管理表空间中，区的大小都是相同的。在创建表空间时，可以通过参数指定统一的区大小，或者由数据库服务器根据实际情况自动指定区的大小。

与字典管理方式相比，本地管理方式有以下优点：

·区的分配和回收不再基于数据字典，从而避免了对数据字典的递归访问，也不会产生重做日志和回滚数据。

·所有区的大小都相同，这就减少了存储空间中的存储碎片。

·不需要合并表空间中的存储碎片，数据库服务器自动监视存储空间的使用情况，并合并相邻的空闲存储空间。

Oracle建议大家使用本地管理表空间。在Oracle 11g的数据库中创建的表空间默认就是本地管理的，如SYSTEM表空间目前是字典管理的，可以通过下面的方式，调用PL/SQL程序包中的存储过程，把它转化为本地管理表空间：

SQL＞EXECUTE DBMS_SPACE_ADMIN.TABLESPACE_MIGRATE_TO_LOCAL（'SYSTEM'）；

6.1.2　段管理方式

当用户向表中写入数据时，这些数据被写入表段中的空闲数据块中，服务器进程先查找表中的空闲数据块，然后将数据写入数据块，最后还要修改数据块的状态。对于段空间管理，Oracle提供了两种方式，一种是手工管理，另一种是自动管理。

手工管理段空间的方式是这样实现的：在每个段的头部都有一段保留空间，在保留空间中维护一个空闲列表，在这个列表中记录当前段中所有的空闲块。当用户向表中写数据时，服务器进程在这个空闲列表中查找足够数量的空闲块，将数据写入这些块。如果一个块已经被写满了，这个块就从空闲列表中被删除。在查找空闲块的过程中，为防止其他服务器进程使用同样的空闲块，服务器进程将对空闲列表加锁，只有空闲列表上的锁被释放后，其他服务器进程才可以在空闲列表中查找空闲块。如果用户在某个表上的事务很频繁，那么空闲列表将成为延缓事务执行的瓶颈。

自动管理方式是Oracle 11g数据库默认采用的段管理方式。这种管理方式是这样实现的：在每个段的头部有一个位图，在位图中记录当前段中每个数据块的状态，用二进制数0和1表示数据块的状态，每个数据块在位图中只占一位。使用位图的好处首先是节省空间，其次，当用户向表中写入数据时，服务器进程只要扫描位图就可以查找空闲数据块，而且不需要对位图加锁。

在创建表空间时，需要为表空间指定段空间管理方式。

6.1.3　数据文件

表空间中的所有数据都是以数据文件的形式存储在磁盘上的。一个表空间包含一个或多个数据文件。在创建表空间时，至少要为表空间指定一个数据文件。

如果表空间只包含一个数据文件，那么表空间中的数据将全部存储在这个数据文件中。如果表空间中包含多个文件，表空间中的数据将以区为单位分布在各个数据文件中。假设某个表空间中有三个数据文件，那么在该表空间中创建一个表时，表中的第一、四、七……个区将位于第一个数据文件，第二、五、八、……个区将位于第二个数据文件中，依此类推。如果将数据文件存放在不同的磁盘上，那么用户在访问表空间中的数据时，可以同时读写多个数据文件，从而减少了磁盘读写的冲突次数。

当表空间中的存储空间被消耗完时，用户将无法再写入数据。数据库管理员必须扩展表空间，为其分配新的存储空间。可以向表空间中增加新的数据文件，也可以手工扩展现有的数据文件，还可以激活数据文件的自动扩展功能，使它能够自动扩展。

6.2　本地管理表空间的管理

本地管理表空间是Oracle 11g中的默认表空间类型。对这种表空间的管理主要涉及创建、删除、修改等操作。

6.2.1　本地管理表空间的创建

创建表空间的任务一般由SYS用户完成，普通用户如果希望执行这样的操作，需要具有CREATE TABLESPACE系统权限。

在创建表空间时，需要指定表空间的区管理方式、段管理方式以及表空间所包含的数据文件。在Oracle 11g中，表空间的默认区管理方式是本地管理（LOCAL）。例如，通过以下语句创建本地管理表空间ts1：

SQL＞CREATE TABLESPACE ts1

DATAFILE'/home/oracle/ts11.dbf'SIZE 10M

EXTENT MANAGEMENT LOCAL AUTOALLOCATE

SEGMENT SPACE MANAGEMENT AUTO；

在上面的语句中，EXTENT MANAGEMENT LOCAL用于指定表空间的区管理方式为本地管理，这是可以省略的，因为表空间默认的区管理方式就是本地管理。但是如果在CREATE语句中指定了AUTOALLOCATE或者UNIFORM SIZE，那么EXTENT MANAGEMENT LOCAL是不能省略的。

AUTOALLOCATE表示这个表空间中的区大小由数据库服务器根据实际情况自动指定，默认为64KB。如果在这个表空间中创建可变大小的数据库对象，而且这个对象需要大小不同的区，那么AUTOALLOCATE是一种很好的选择。如果希望控制表空间中区的使用，则通过UNIFORM SIZE指定统一的区大小。

SEGMENT SPACE MANAGEMENT用于指定表空间中段空间的管理方式，目前有两种可选方式，即AUTO和MANUAL。AUTO使用位图的方式跟踪段中每个数据块的状态，而MANUAL方式是通过空闲列表来记录每个段中的空闲块的。由于AUTO使段空间管理更加高效，所以Oracle建议为表空间指定这种段管理方式，而且这也是默认方式。

DATAFILE用于指定表空间所包含的数据文件及其大小，一个表空间包含一个或多个数据文件。在UNIX/Linux系统中，为了在某个目录下创建数据文件，需要保证操作系统中的oracle用户对该目录具有写权限。

下面的语句用于创建表空间TS2，表空间中区的统一大小为256KB。如果没有通过SIZE子句指定大小，则采用默认值1MB。

SQL＞CREATE TABLESPACE ts2

DATAFILE'/home/oracle/ts21.dbf'SIZE 10M

EXTENT MANAGEMENT LOCAL UNIFORM SIZE 256K

SEGMENT SPACE MANAGEMENT AUTO；

数据库中的数据块大小通过初始化参数DB_BLOCK_SIZE指定，通过这个参数指定的数据块称为标准快。表空间中的数据块默认也采用标准块大小。如果希望在数据库中定义其他大小的非标准块，那么在创建表空间时需要指定它所采用的数据块大小。例如，通过下面的语句创建的表空间具有16KB的数据块：

SQL＞CREATE TABLESPACE ts3

DATAFILE'/home/oracle/ts31.dbf'SIZE 10M，

'/home/oracle/ts32.dbf'SIZE 20M

BLOCKSIZE 16K；

非标准块中的数据是不能被读到一般的数据库高速缓存中的。为了能够访问这种表空间中的数据，在创建表空间之前，需要通过初始化参数DB_nK_CACHE_SIZE在内存中为非标准块定义相应的数据库高速缓存，缓存也是由许多缓冲区组成，缓冲区的大小与非标准块大小相同。例如：

SQL＞ALTER SYSTEM SET db_16k_cache_size=16K；

6.2.2　表空间信息的查询

与表空间有关的相关信息可以从数据字典中获得，与表空间有关的数据字典视图有两个，即dba_tablespaces和dba_data_files。

从数据字典dba_tablespaces中可以获得表空间的基本信息。例如，通过下面的SELECT语句可以获得表空间的名称、区管理方式、段管理方式、内容类型、状态等信息：

SQL＞SELECT tablespace_name, extent_management，

segment_space_management, contents, status

FROM dba_tablespaces

上述语句的执行结果类似如下：

TABLESPACE_NAME EXTENT_MAN SEGMEN CONTENTS STATUS

SYSTEM LOCAL MANUAL PERMANENT ONLINE

SYSAUX LOCAL AUTO PERMANENT ONLINE

UNDOTBS1 LOCAL MANUAL UNDO ONLINE

从数据字典dba_data_fies可以获得表空间中数据文件的信息，如文件名称、大小（以MB为单位）、状态等信息。例如：

SQL＞SELECT file_name, bytes/1024/1024 AS MB, status FROM dba_data_files

WHERE tablespace_name='TS1'；

6.2.3　表空间的删除

当一个表空间不再需要时，可以将其从数据库中删除。删除表空间的操作一般由SYS用户完成，普通用户如果希望执行这样的操作，需要具有DROP TABLESPACE系统权限。删除表空间的命令格式为：

DROP TABLESPACE 表空间

例如，下面的命令用于删除表空间ts3：

SQL＞DROP TABLESPACE ts3；

需要注意的是，如果用户已经在这个表空间中创建了表、索引的数据库对象，这个表空间是不能被直接删除的，需要先把表空间中的所有数据库对象删除。例如，下面的DROP语句用于删除表空间以及其中的数据库对象：

SQL＞DROP TABLESPACE ts3 INCLUDING CONTENTS；

表空间虽然被删除了，但是表空间所包含的数据文件并没有被删除，还占用磁盘上的存储空间，这些文件需要通过手工方式删除。在有些情况下，手工删除数据文件可能不太容易，如在裸设备中或ASM磁盘组中。下面的DROP语句用于删除表空间以及其中的数据库对象和对应的数据文件：

SQL＞DROP TABLESPACE ts3 INCLUDING CONTENTS AND DATAFILES；

6.3　大文件表空间的管理

大文件（BigFile）表空间是一种特殊类型的表空间，在每个大文件表空间中只包含一个数据文件，但这个数据文件的大小可以达到4GB个数据块。

使用大文件表空间的最大好处是可以大大扩展数据库的容量。假设数据块的大小为8KB，那么一个数据文件的大小就可以达到8KB（4GB=32TB。另外，使用大文件表空间可以大大减少数据文件的数目，因为一个表空间只有一个数据文件。Oracle推出大文件表空间的主要目的是支持自动存储管理（ASM）和RAID、条带状逻辑卷，这些技术结合起来，为数据库提供大容量的、对用户透明的存储空间。

需要注意的是，大文件表空间的区管理方式只能是本地管理，段管理方式只能是自动管理。UNDO表空间、临时表空间和SYSTEM表空间属于例外情况，如果把它们指定为大文件表空间，它们的区管理方式只能是本地管理，但是段管理方式可以是自动管理或手工管理。

6.3.1　大文件表空间的支持

在默认情况下，数据库中表空间的类型为小文件（SmallFile）表空间，如果要创建大文件表空间，则需要通过关键字BIGFILE指定。在创建数据库时，可以将默认的表空间类型指定为大文件表空间，这样以后在数据库中创建的表空间默认都属于大文件表空间。

在创建数据库的CREATE DATABASE命令中，如果使用了SET DEFAULT BIGFILE TABLESPACE子句，那么表空间的默认类型将为大文件表空间。如果使用了SET DEFAULTSMALLFILE TABLESPACE子句，或者这两条子句都没有使用，那么表空间的默认类型将为小文件表空间。例如，以下语句在创建数据库时指定表空间的默认类型为大文件表空间：

SQL＞CREATE DATABASE test

SET DEFAULT BIGFILE TABLESPACE

……

在数据库的运行过程中，数据库管理员也可以动态修改表空间的默认类型，修改以后的表空间类型对以后创建的所有表空间起作用。例如：

SQL＞ALTER DATABASE SET DEFAULT BIGFILE TABLESPACE；

SQL＞ALTER DATABASE SET DEFAULT SMALLFILE TABLESPACE；

从数据字典视图database_properties中可以获得数据库的属性，包括默认的表空间类型。例如：

SQL＞SELECT property_value FROM database_properties

WHERE property_name='DEFAULT_TBS_TYPE'；

大文件表空间的删除方法与普通表空间相同，这里不再介绍。

6.3.2　大文件表空间的创建

在数据库的运行过程中，数据库管理员可以创建一个大文件表空间。这个表空间的区管理方式为本地管理，段管理方式为自动管理，在创建表空间的语句中可以不指定这些信息。在创建大文件表空间的语句中，只能指定一个数据文件。例如：

SQL＞CREATE BIGFILE TABLESPACE bts1

DATAFILE'/home/home/bts1.dbf'SIZE 10G；

如果数据库中表空间的默认类型已经是大文件表空间，那么在创建大文件表空间的语句中可以省略BIGFILE关键字。但如果要创建一个小文件表空间，则必须在CREATE TABLESPACE语句中使用SMALLFILE关键字，反之亦然。

6.3.3　大文件表空间的修改

修改大文件表空间的操作涉及两个方面的内容：重新指定数据文件的大小以及修改数据文件的自动扩展属性。由于大文件表空间只有一个数据文件，所以在修改时不需要指定数据文件的信息。例如，下面的语句用来把数据文件BTS1.DBF的大小改为20GB：

SQL＞ALTER TABLESPACE bts1 RESIZE 20G；

下面的语句用于修改数据文件的自动扩展属性：

SQL＞ALTER TABLESPACE bts1 AUTOEXTEND ON NEXT 5G MAXSIZE 1T；

6.4　临时表空间的管理

临时表空间用于存储临时数据。当用户执行排序、创建索引这样的操作时，将产生大量的中间结果，这些临时数据将首先存储在PGA的排序区中。当排序区的大小不足以容纳这些数据时，将用到临时表空间。如果没有为用户指定临时表空间，那么用户在执行排序操作时将把SYSTEM表空间作为临时表空间，并在SYSTEM表空间中创建临时段，以存储临时数据。由于用户的排序操作会在SYSTEM表空间中频繁地产生临时段和释放临时段，这样在SYSTEM表空间中将产生大量的存储空间碎片，从而降低数据库的性能。Oracle建议在数据库中创建专门的临时表空间，并为用户指定一个临时表空间。

在临时表空间中只能创建临时段，而不能创建数据段、索引段等永久性的段。在数据库中第一次执行排序操作时，临时段自动创建，在数据库关闭时，临时段被删除。这些临时段为所有用户所共享。

6.4.1　临时表空间的创建

就像创建普通表空间一样，在创建临时表空间时需要指定区管理方式、段管理方式、区的大小等信息，也可以创建大文件临时表空间。在创建表空间时需要指定关键字TEMPORARY。例如，以下语句用来创建本地管理临时表空间tts1：

SQL＞CREATE TEMPORARY TABLESPACE tts1

TEMPFILE'/home/oracle/tts11.dbf'SIZE 10M

EXTENT MANAGEMENT LOCAL

UNIFORM SIZE 128K；

在创建本地管理临时表空间时，首先要用关键字TEMPORARY指定表空间的类型为临时性的，其次，要通过TEMPFIL子句指定表空间的数据文件，而不是使用DATAFFILEF子句。另外，只能通过UNIFORM子句为临时表空间指定的区大小，而不能使用AUTOALLOCATE子句。

在创建临时表空间时，Oracle建议将临时表空间的大小设置为排序区的整数倍，以减少存储空间的碎片，使数据库获得最佳性能。排序区的大小通过初始化参数SORT_AREA_SIZE来确定。

临时表空间的作用仅限于存储临时数据，因而与普通表空间相比它有一些特殊的地方。从数据字典视图dba_tablespaces中查询临时表空间的信息时，发现LOGGING列的值为NOLOGGING，这就意味着对临时空间中的数据所做的任何修改都不会产生重做日志。用户执行排序等操作时，临时文件中的数据是不断变化的，因而临时文件的访问权限不能为只读方式。另外，还需要注意的是，临时表空间中的数据文件的信息是从数据字典视图dba_temp_files中获得的。

在创建数据库时，默认就创建了一个临时表空间，它就作为数据库的默认临时表空间。如果更改数据库的临时表空间，可以执行下面的ALTER语句：

SQL＞ALTER DATABASE DEFAULT TEMPORARY TABLESPACE tts1；

临时表空间的删除方法与普通表空间相同，这里不再介绍。

6.4.2　临时表空间组

在数据库中只有一个默认临时表空间，所有用户都是用这个临时表空间。如果在数据库中有大量的排序、创建索引，或者以并发方式向表中写入大量数据，这时可以创建多个临时表空间，为每个用户指定不同的临时表空间，每个用户都使用自己的临时表空间，这样做的好处是可以将磁盘I/O分布在不同的磁盘上，减少了磁盘I/O的冲突，从总体上可以提高数据库的性能。例如，下面的ALTER为用户指定临时表空间：

SQL＞ALTER USER scott TEMPORARY TABLESPACE tts2；

由于不同用户对数据库的访问是不同的，因而对这些临时表空间的使用频率也是不同的。如果能使所有用户平均地使用数据库中的多个临时表空间，就能进一步提高数据库的性能。临时表空间组是解决这个问题的最佳途径。

临时表空间组是一组临时表空间的逻辑集合，当为用户指定临时表空间组时，用户可以平均地使用其中的所有临时表空间。临时表空间组不需要创建，只要在创建或修改临时表空间时为临时表空间组指定一个名称即可。例如：

SQL＞ALTER TABLESPACE tts1 TABLESPACE GROUP group1；

SQL＞ALTER TABLESPACE tts2 TABLESPACE GROUP group1；

然后将这个临时表空间组作为数据库的默认临时表空间，或者作为某个用户的默认临时表空间。例如：

SQL＞ALTER DATABASE DEFAULT TEMPORARY TABLESPACE group1；

从临时表空间组中删除临时表空间的操作也很简单，只要为临时表空间指定一个空的临时表空间组即可。临时表空间虽然在逻辑上不再属于任何临时表空间组，但它在数据库中依然存在，还可以作为数据库或者某个用户的默认临时表空间。下面的语句用于将临时表空间TTS1从临时表空间组中删除。

SQL＞ALTER TABLESPACE tts1 TABLESP ACE GROUP''；

临时表空间组的信息可以从数据字典视图dba_tablespace_groups中获得。例如，下面的语句用于查询一个临时表空间组中所包含的临时表空间：

SQL＞SELECT tablespace_name FROM dba_tablespace_groups

WHERE GROUP_NAME='GROUP1'；

6.5　UNDO表空间的管理

当用户在数据库中执行INSERT、DELETE、UPDATE等DML操作时，相关的命令集合组成一个事务。在事务提交之前，用户还有机会回滚事务。事务回滚之后对数据库所作的修改将被取消，就好像没有发生任何事情一样。

回滚事务的结果是将被修改的数据还原为原状，也就是将回滚数据重新写回原来的地方。回滚数据也称为UNDO数据，是事务执行以前的数据，这些数据被存储在回滚段中。当用户提交事务时，回滚数据就失去了存在的意义。而当用户回滚事务时，数据库服务器将回滚数据从回滚段重新写入数据段，于是数据被恢复为原状。

回滚数据对于维护数据的一致性有非常重要的意义。例如，某个用户要在两个银行账号之间转账，数据库先在第一个账户上减去转账的金额，然后在第二个账户上加上相同的金额。这两步操作就构成一个事务，作为一整体来执行。假设在第一步刚执行完时，系统突然断电，结果会怎样呢？两个账户上的金额就对不上了。Oracle能够很好地解决这个问题。当数据服务器重新启动时，这个未提交的事务将被回滚，数据库服务器将回滚数据写入原来的位置，于是这次转账以失败告终，这样就保证了两个银行账户之间不会出现收支不平衡的现象。

回滚数据被存储在数据库中一段特殊的存储区域中。在Oracle 11g中，有两种管理回滚数据的方法，一种是自动方式，这种方式利用专门的UNDO表空间管理UNDO数据。第二种方法利用回滚段管理UNDO数据，这种方式称为手工方式。在一个数据库中我们只能选择使用其中一种方式。

手工方式即回滚段方式，是以前的Oracle版本使用的传统方式，这种方式的管理相当复杂，需要数据管理员做大量的工作，如确定回滚段的数量和大小，指定可用的回滚段等。如果回滚段的数量太少，或者空间太小，用户的事务可能被延缓执行，这时管理员需要回滚段进行手工扩展。

自动方式利用专门的UNDO表空间来管理回滚数据。如果创建了UNDO表空间，数据库服务器将自动使用UNDO表空间来管理回滚数据，整个过程不需要人工干预，从而大大减轻了管理员的负担。如果将UNDO表空间设置为可自动扩展，那么当用户的事务太多时，数据库服务器可以根据需要对UNDO表空间进行扩展。Oracle建议采用自动管理方式。

实际上自动方式也是利用回滚段来管理会滚数据的，回滚段包含在专门的UNDO表空间中，只不过回滚段不需要数据库管理员管理，而是由数据库服务器根据需要自动建立和删除的。

在创建数据库时，将自动创建一个UNDO表空间，表空间的名称为UNDOTBS1。在自动管理方式下，如果没有建立UNDO表空间，数据库服务器将利用SYSTEM表空间中的回滚段管理回滚数据。虽然数据库服务器也能运行，但这属于一种异常情况，将产生一条警告信息，而且这种情况将导致在SYSTEM表空间中产生大量存储碎片。因此，Oracle强烈建议创建一个UNDO表空间。

6.5.1　UNDO表空间的创建

有两种创建UNDO表空间的方式，一种是在创建数据库的同时创建UNDO表空间，另一种方式是在数据库运行过程中，通过CREATE UNDO TABLESPACE命令来创建。

创建数据库时，在默认情况下，将自动创建UNDO表空间UNDOTBS1。通过CREATE命令创建UNDO表空间的命令语法与普通表空间类似，主要的区别在于使用了“UNDO TABLESPACE”子句。

例如，以下语句用来创建大文件UNDO表空间UNDOTBS2：

SQL＞CREATE BIGFILE UNDO TABLESPACE UNDOTBS2

DATAFILE'/home/oracle/tbs2.dbf'SIZE 50M REUSE

EXTENT MANAGEMENT LOCAL；

6.5.2　UNDO表空间的切换

数据库服务器可以采用自动方式，也可以采用手工方式管理回滚数据，这取决于初始化参数UNDO_MANAGEMENT的设置。如果参数值为AUTO，则采用自动方式，如果设置为MANUAL，则采用手工方式。如果希望从一种方式切换到另一种方式，需要修改初始化参数UNDO_MANAGEMENT的值。与回滚数据的管理方式有关的初始化参数还有以下几个：

·UNDO_TABLESPACE用来指定一个可用的UNDO表空间。这个表空间必须存在。如果采用手工方式，这个参数的设置无效。在数据库服务器运行的过程中，这个参数的值可以动态修改。

·UNDO_RETENTION指定已经无效的UNDO数据在UNDO表空间中可以保留的时间，默认为900秒。这个参数的值也可以动态修改。

UNDO表空间在创建之后，为了保证用户能够使用这个表空间，首先必须将数据库UNDO管理方式设置为自动管理方式，即确保初始化参数UNDO_MANAGENT的值为AUTO。另外，还需要将初始化参数UNDO_TABLESPACE的值设置为新创建的UNDO表空间。例如：

SQL＞ALTER SYSTEM SET UNDO_MANAGEMENT=auto scope=spfile；

SQL＞ALTER SYSTEM SET UNDO_TABLESPACE=UNDOTBS2；

初始化参数UNDO_MANAGEMENT的值需要在参数文件中修改，并且只有在数据服务器重新启动之后才能起作用。而初始化参数UNDO_TABLESPACE的值可以在参数文件中修改，也可以动态修改。

在切换UNDO表空间时，如果指定的UNDO表空间不存在，或者指定的表空间不是UNDO表空间，或者有另一个实例正在使用这个表空间，切换操作将无法正常进行。

在切换UNDO表空间之后，新的事务将使用切换后的UNDO表空间。但是原来的UNDO表空间不会马上失去作用，如果一个事务开始于切换表空间之前，并一直持续到切换之后，那么它还将继续使用原来的表空间，这样原来的UNDO表空间需要一直等待在其中运行的事务都结束后才能失去作用。

UNDO表空间的扩展和删除方法与普通表空间相同，这里不再介绍。

6.6　表空间的扩展

在创建表空间时，需要表空间中的数据文件及其大小，表空间中的数据就写在这些数据文件中。如果数据文件的空间被写满，新的数据将无法写入，用户的事务可能无法顺利执行。在创建表空间或扩展表空间时应该根据以下原则指定数据文件：

·尽量使用少量的大文件，而不要使用大量的小文件。

·同一个表空间的不同数据文件应该存放在不同的磁盘上。

·应该将数据文件和重做日志文件分别存放在不同的磁盘上。

在数据库中能够容纳的数据文件数目是有限的，文件数目越多，在打开文件时就需要越多的内存空间。数据文件的数目受两个因素的制约，一是在创建数据库时指定的MAXDATAFILES永久参数，二是初始化参数DB_FILES。

MAXDATAFILES永久参数指定了一个最大数据文件数目，这个数目将被记录在控制文件中。当使用CREATE DATABASE命令创建数据库，或者执行CREATE CONTROLFILES命令创建控制文件时，都可以通过这个参数指定数据文件的最大数量。然而如果实际的数据文件数目超过指定的数目时，数据库服务器将扩展控制文件，以记录更多的数据文件。

初始化参数DB_FILES指定在数据库中可以创建的数据文件的最大数目。当实际的数据文件数目达到这个数目时，将无法创建或添加新的数据文件。但是初始化参数DB_FILES的值是可以修改的。在参数文件中为这个参数指定新值，然后重新启动数据库服务器，新的参数值将起作用。DB_FILES的值并不是越大越好，大的参数值固然允许你创建更多的数据文件，也将消耗更多的内存空间。

将数据文件存放在不同的磁盘上，并将其与重作日志文件分开存放，这种做法的目的是将磁盘的读写操作分散在各个磁盘上，从而减少磁盘的I/O冲突。

扩展表空间的方法有两种，一种是向表空间中添加新的数据文件，另一种方法是扩展现有的数据文件。扩展数据文件又可以分为两种方法，一种是允许数据文件自动扩展，另一种方法是手工扩展数据文件。

6.6.1　如何添加新的数据文件

在向表空间中添加数据文件时，通过ADD子句指定数据文件的路径和大小。例如：

SQL＞ALTER TABLESPACE TS1

ADD DATAFILE'/home/oracle/ts12.dbf'SIZE 10M REUSE；

在添加数据文件时，通过SIZE关键字指定文件的大小。如果在指定的位置已经存在一个同名的数据文件，可通过使用REUSE关键字将其覆盖。如果要向表空间中添加多个数据文件，由DATAFILE关键字引导多个文件，每个文件分别指定路径和大小。例如：

SQL＞ALTER TABLESPACE TS6

ADD DATAFILE'/home/oracle/ts6_1.dbf'SIZE 10M REUSE，

'/home/oracle/ts6_2.dbf'SIZE 20M REUSE；

对于本地管理的临时表空间，在添加数据文件时需要通过ADD TEMPFILE子句指定新的临时文件。例如，以下语句用于向临时表空间TTS1中添加临时文件TTS1_2.DBF：

SQL＞ALTER TABLESPACE TTS1

ADD TEMPFILE'/home/oracle/tts1_2.dbf'SIZE 10M REUSE；

添加后的数据文件信息可以从数据字典dba_data_files中得到确认。

6.6.2　如何扩展数据文件

扩展数据文件意味着在磁盘上为数据文件分配新的磁盘空间。数据文件有两种扩展方式，即手工扩展和自动扩展。

在创建表空间或者向表空间中添加新的数据文件时，如果通过关键字AUTOEXTEND指定数据文件的自动扩展属性为ON，则该数据文件可以自动扩展。如果没有指定数据文件的自动扩展属性，那么这个文件是不能自动扩展的，只能手工进行扩展。

如果数据文件以前没有自动扩展功能，可以为其指定自动扩展的功能。例如，以下语句为表空间TS6中的数据文件TS6.DBF指定自动扩展功能：

SQL＞ALTER DATABASE

DATAFILE'/home/oracle/ts61.dbf'

AUTOEXTEND ON NEXT 128K MAXSIZE 100M；

在上面的语句中，NEXT用于指定每次对数据文件扩展的空间大小，MAXSIZE用于指定数据文件最大允许的大小。对于已经具有自动扩展功能的数据文件，也可以修改它的NEXT和MAXSIZE属性。

我们还可以取消数据文件的自动扩展功能。如：

SQL＞ALTER DATABASE

DATAFILE'/home/oracle/ts61.dbf'

AUTOEXTEND OFF；

在创建表空间时，或者向表空间中添加数据文件时，可以同时指定数据文件的自动扩展属性。例如：

SQL＞CREATE TABLESPACE TS6

DATAFILE'/home/oracle/ts6_1.dbf'SIZE 10M AUTOEXTEND ON NEXT 256K，

'/home/oracle/ts6_2.dbf'SIZE 20M AUTOEXTEND ON NEXT 1M；

数据文件的自动扩展信息可以从数据字典视图dba_data_files中获得。例如，下面的语句用于查看表空间TS1中数据文件的自动扩展信息，包括文件目前的大小、是否可以自动扩展、每次扩展的大小、文件的最大大小。

SQL＞SELECT file_name, bytes, autoextensible, increment_by, maxbytes

FROM dba_data_files

WHERE tablespace_name='TS1'；

在上述命令的执行结果中，文件的目前大小和最大大小均以字节为单位，而每次扩展的大小则以数据块数为单位。

在手工扩展数据文件时，需要通过RESIZE为数据文件指定新的大小。例如，以下语句用于将数据文件TS6_1.DBF的大小改为100MB：

SQL＞ALTER DATABASE

DATAFILE'/home/oracle/ts6_1.dbf'RESIZE 100M；

实际上，即使数据文件具有自动扩展属性，我们也可以手工改变文件的大小。为数据文件指定的大小可以比原来的大，也可以比原来的小。也就是说，数据文件的大小可以手工扩展，也可以手工缩减。在缩减数据文件的大小时，必须保证能够容纳已有的数据。

6.7　表空间的维护

表空间在创建之后，管理员可能会根据需要修改表空间的状态，或者修改表空间的读写状态，或者修改数据文件的存储位置和名称等。

表空间的状态默认为联机状态，读写权限为可读可写。修改表空间的操作一般由sys用户完成，普通用户如果希望执行这样的操作，需要具有ALTER TABLESPACE系统权限。

6.7.1　表空间的联机与脱机

表空间在创建后默认处于联机状态，数据库管理员可以将它的状态在联机和脱机之间切换。当表空间处于脱机状态时，表空间中的数据文件也都处于脱机状态，这时用户无法访问表空间中的数据。在数据库服务器的运行过程中，有时需要将某个表空间置于脱机状态，如在不影响整个数据库运行的情况下对某个表空间进行备份，或者对某些数据进行保护，不允许用户访问等。

除了SYSTEM表空间、默认的临时表空间、正在使用的UNDO表空间以外，其他表空间都可以置于脱机状态。由于数据库在运行时需要时时刻刻访问SYSTEM表空间，所以它不能被置于脱机状态。将表空间的状态置为脱机的命令格式为：

ALTER TABLESPACE表空间名OFFLINE脱机方式

其中关键字OFFLINE的作用是将表空间脱机。脱机方式有四种选择：NORMAL、TEMPORARY、IMMEDIATE和FOR RECOVER。

其中NORMAL方式是默认的脱机方式，是指将表空间置于正常的脱机状态。在脱机之前，首先要保证数据库高速缓存中的数据与数据文件相一致。数据库服务器促使CKPT进程发出一次检查点，并检查数据文件的状态是否为可用，然后将数据库高速缓存中所有与当前表空间有关的脏缓冲区都写入数据文件，然后关闭表空间中的所有数据文件，并使表空间脱机。由于数据文件与数据库高速缓存中的数据完全一致，在表空间脱机后数据文件也不会发生任何变化，所以当表空间再次联机时不需要恢复数据。

如果希望以NORMAL方式使表空间脱机，脱机方式可以省略。例如，以下语句将使表空间TS3以NORMAL方式脱机：

SQL＞ALTER TABLESPACE ts3 OFFLINE；

TEMPORARY方式是指以临时方式将表空间脱机。在脱机前，CKPT进程将发出一次检查点，将数据库高速缓存中的相关脏缓冲区写入数据文件。但CKPT进程不检查数据文件的状态，如果某个数据文件被损坏，部分脏缓冲区中的数据可能无法写入数据文件，所以在表空间下次重新联机时需要进行数据恢复。

例如，以下语句用于将表空间ts3以TEMPORORY方式脱机：

SQL＞ALTER TABLESPACE ts3 OFFLINE TEMPORARY；

IMMEDIATE方式是指将表空间立即置于脱机状态。在脱机之前，数据库服务器不发出检查点，而直接将表空间脱机。由于数据库高速缓存中的相关脏缓冲区没有被写入数据文件，所以在表空间下次联机之前需要进行数据恢复。

FOR RECOVER方式是用于恢复表空间的脱机状态。如果已经对表空间进行了备份，并希望用以前的备份对表空间进行恢复，首先需要将表空间以FOR RECOVER方式脱机。例如，以下语句用于将表空间ts3以FOR RECOVER方式脱机：

SQL＞ALTER TABLESPACE ts3 OFFLINE FOR RECOVER

数据库服务器在重新启动时，可以将脱机的表空间联机，数据库管理员也可以手工将表空间联机。如果表空间是以NORMAL方式脱机的，那么在再次联机时不需要进行数据恢复。表空间再次联机后，用户又可以访问其中的数据。

用于手工将表空间联机的命令格式为：

ALTER TABLESPACE 表空间 ONLINE

6.7.2　数据文件的联机与脱机

像表空间一样，数据文件也有联机和脱机两种状态。只有当数据文件处于联机状态时，用户才能访问其中的数据。数据文件在被创建后，默认处于联机状态。

由于数据文件是属于表空间的，所以当表空间处于联机状态时，表空间中的所有数据文件同时处于联机状态，除非这个数据文件损坏。同样，当表空间处于脱机状态时，表空间中的所有数据文件同时处于脱机状态。

改变数据文件状态的命令是ALTER DATABASE。例如，以下语句将使数据文件TS11.DBF处于脱机状态：

SQL＞ALTER DATABASE DATAFILE'/home/oracle/ts11.dbf'OFFLINE；

如果数据文件损坏，数据库服务器将自动使其脱机。如果数据库处于非归档模式下，要使损坏的数据文件脱机时，可以使用DROP关键字，这个数据文件就再也不能处于联机状态了。例如：

SQL＞ALTER DATABASE DATAFILE'/home/oracle/ts11.dbf'OFFLINE FOR DROP；

为了使数据文件重新联机，需要在ALTER DATABASE命令中使用ONLINE关键字。例如，以下语句用于使数据文件TS6_00.DBF重新联机：

SQL＞ALTER DATABASE DATAFILE'/home/oracle/ts11.dbf'ONLINE；

通过ALTER TABLESPACE命令可以使一个表空间中的所有数据文件都处于联机或脱机状态。例如，下面两条语句分别使表空间ts1和临时表空间tts1中的所有数据文件处于脱机状态：

SQL＞ALTER TABLESPACE ts1 DATAFILE OFFLINE；

SQL＞ALTER TABLESPACE tts1 TEMPFILE OFFLINE；

6.7.3　表空间的读写权限

用户对表空间的访问权限有读写和只读两种方式。如果表空间处于读写状态，那么用户可以在表空间中创建数据库对象，可以删除其中的数据库对象，可以查看和修改数据库对象的信息。如果表空间处于只读状态，用户只能查看表空间中的数据，而不能对其进行修改。

将表空间的访问权限改为只读的主要目的是保护一些历史数据，禁止用户修改这些数据。一个更好的做法是，将只读表空间中的数据文件存放在CD或者DVD这类只读存储介质上，这样不仅可以保护表空间中的数据，而且可以节省磁盘空间。

表空间在创建后，默认处于读写状态，数据库管理员可以将表空间的状态设置为只读状态。除SYSTEM表空间和UNDO表空间外，其他表空间都可置为只读状态。例如，以下命令可以使表空间ts3变为只读状态：

SQL＞ALTER TABLESPACE ts3 READ ONLY；

需要注意的是，当表空间处于只读状态时，用户在表空间上可以执行SELECT操作，不能执行DELETE、INSERT、CREATE等DML和DDL操作，但是有一条DDL命令比较特殊，这条命令是DROP。在只读表空间上可以通过DROP命令删除数据库对象。为了在只读表空间执行DML和DDL操作，需要将表空间的状态置为读写状态。例如，以下语句执行的结果是将表空间ts3置为读写状态：

SQL＞ALTER TABLESPACE ts3 READ WRITE；

6.7.4　数据文件的移动和重命名

数据文件的移动是指将数据文件从一个存储位置移动到另一个存储位置，在移动的同时还可以修改文件的名称。数据文件的重命名是指在原来的位置将文件的名称改为其他字符串。

移动数据文件的主要目的是为了更换磁盘，或者平衡磁盘的I/O操作。Oracle建议尽量将数据文件分布在不同的磁盘上，并与重做日志文件分别存放。

数据文件的移动和重命名需要分四步进行，第一步是将数据文件处于脱机状态，第二步是在操作系统中将磁盘上的数据文件移动到另一个位置，或者仅仅修改它的名称，第二步是在数据库中对文件进行重命名，第四步是将数据文件处于联机状态。

对于不同的表空间，需要使用不同的方法移动或重命名文件。对于普通表空间，先使表空间脱机，然后再对数据文件进行移动或重命名，在这种情况下需要执行ALTER TABLESPACE命令。对于SYSTEM表空间，或者其中包含活动回滚段的表空间，因为无法使它们处于脱机状态，所以应该先将数据库切换到MOUNT状态，然后对数据文件进行移动或重命名。在这种情况下需要执行ALTER DATABASE命令。

例如，对于普通表空间TS1，假设希望将数据文件ts11.dbf重命名为ts1_1.dbf，或者改变它的存储位置，首先使表空间TS1处于脱机状态。脱机的目的是防止用户访问表空间，以保证数据文件的一致性。

SQL＞ALTER TABLESPACE TS1 OFFLINE；

接下来在操作系统中对数据文件TS11.DBF进行重命名，将其名称改为ts1_1.dbf，或者将它移动到另外一个存储位置。

接下来在SQL*Plus中执行ALTER TABLESPACE修改数据文件的名称或路径。命令如下：

SQL＞ALTER TABLESPACE TS1

RENAME DATAFILE'/home/oracle/ts11.dbf'

TO'/home/oracle/ts1_1.dbf'；

数据文件的位置和名称信息记录在控制文件和数据字典中，ALTER TABLESPACE命令中的RENAME子句的作用就是修改控制文件和数据字典的相关信息。在执行ALTER TABLESPACE命令之前，必须保证数据文件在操作系统中已经被正确地重命名了。

实际上，表空间中的多个数据文件可以同时被移动或重命名。例如，以下语句将把表空间TOOLS中的数据文件tools01.dbf和tools02.dbf分别重命名为tools001.dbf和tools002.dbf：

SQL＞ALTER TABLESPACE TOOLS

RENAME DATAFILE

'/home/oracle/tools01.dbf'，

'/home/oracle/tools02.dbf'

TO

'/home/oracle/tools001.dbf'，

'/home/oracle/tools002.dbf'；

数据文件被移动或重命名后，可以再次通过查询数据字典dba_data_files来了解修改后的信息。

最后，为了使用户能够访问表空间，应该将表空间处于联机状态，例如：

SQL＞ALTER TABLESPACE TS1 ONLINE；

现在再以表空间SYSTEM为例来说明另一种特殊表空间中数据文件的移动和重命名方法。假设我们希望将SYSTEM表空间中的数据文件system01.dbf从原来的存储位置移动到目录“/home/oracle/orcl”下，并将它重命名为“system_01.dbf”。由于SYSTEM表空间和包含活动回滚段的表空间不能处于脱机状态，所以需要将数据库转入MOUNT状态。这一系列操作需要数据库管理员来执行。

SQL＞CONN sys/1234 AS SYSDBA

SQL＞SHUTDOWN

SQL＞STARTUP MOUNT

接下来在操作系统中对数据文件进行移动和重命名，然后在SQL*Plus中执行ALTER DATABASE命令，在控制文件和数据字典中修改该数据文件的相关信息。命令如下：

SQL＞ALTER DATABASE

RENAME FILE'/home/oracle/system01.dbf'

TO'/home/oracle/orcl/system01.dbf'；

需要注意的事，在ALTER DATABASE命令的RENAME子句中使用的是FILE关键字，而不是DATAFILE关键字。

最后，为了使用户能够访问数据库，需要打开数据库：

SQL＞ALTER DATABASE OPEN；

在有些情况下需要对表空间进行重命名，这样的操作是非常简单的，表空间中的数据文件没有任何变化。例如，下面的语句用于把表空间ts1重命名为ts11：

SQL＞ALTER TABLESPACE ts1 RENAME TO ts11；

第7章　存储空间管理

在数据库的逻辑结构中，表空间将不同类型的数据分别组织在一起，如系统数据、用户数据、临时数据、回滚数据等。在同一个表空间中，数据以数据库对象为单位组织在一起，一般情况下一个数据库对象对应一个段，一个表空间中包含多个段。

在段中存储数据库对象中的数据，数据所占用的存储空间以区为单位进行分配和回收。一个数据库对象占用若干个区，所以段是由若干个区组成的。当为数据库对象分配存储空间时，一次分配一个或多个区。区是分配和回收存储空间的基本单位，一个区是由若干个连续的数据块组成的。

数据块是逻辑结构中的最小存储单位，是Oracle读写数据的基本单位。当用户访问数据时，服务器进程首先将数据从数据块读到数据库高速缓存中，并存储在与数据块大小相同的缓冲区中，然后在缓冲区中对数据进行读写。

数据库中的数据在物理上是存储在磁盘上的，需要占用一定的操作系统块。一个数据块是由若干个操作系统块组成的，因此，数据块的大小是操作系统块的整数倍。数据库服务器对数据块的读写最终将转化为对多个操作系统块的读写。

7.1　段的管理

在表空间中，一个段一般代表一个数据库对象，对象的所有数据都存储在对应的段中。一般情况下段不需要用户自己创建，在创建数据库对象将自动产生段。为段所分配的空间就位于表空间的数据文件中，这些存储空间由若干个区组成。段、区和数据块之间的关系如图7.1所示。

图　7.1　段、区和数据块之间的关系

7.1.1　段的类型

Oracle 11g支持10余种类型的段。通过查询数据字典dba_segments，可以得到当前数据库中已经存在的段类型。例如：

SQL＞SELECT DISTINCT segment_type FROM dba_segments；

SEGMENT_TYPE

TABLE

INDEX

ROLLBACK

CLUSTER

LOBSEGMENT

LOBINDEX

TABLE PARTITION

INDEX PARTITION

……

1.表段（TABLE）

表段是用来存储表中的数据的，这是最常见的一种段。在创建表时将自动创建一个同名的段，表中的数据就存储在数据段中。

2.索引段（INDEX）

索引是依赖于表的数据库对象，它的功能是加快表的查询速度。当用户执行CREATE INDEX命令创建索引时，在数据库中将产生一个索引段。当用户在表上定义主键约束和唯一性约束时也将自动产生索引段。索引段的名称与索引相同，索引中的数据就存储在索引段中。

3.临时段

当用户执行排序等操作时，将产生大量的临时数据。临时数据将首先存储在PGA的排序区中，如果排序区的大小不足以存放这些临时数据时，将使用临时段。一般情况下，用户在执行以下操作时可能用到临时段：

SELECT……FROM……ORDER BY……

SELECT……FROM……GROUP BY……

SELECT DISTINCT……FROM……

SELECT……UNION

CREATE INDEX……

ANALYZE

临时段一般位于专门的临时表空间中。如果没有专门的临时表空间，数据库服务器将把SYSTEM表空间作为用户的临时表空间。临时段不是一种永久性的段，而是随着用户的排序操作的进行而动态产生和消失的。当排序操作开始时，在临时表空间中将产生一个临时段，在排序结束时临时段将被删除。如果在SYSTEM表空间中存储这些临时数据，将导致大量的存储碎片的产生，从而大大降低数据库服务器的性能。因此，Oracle建议创建专门的临时表空间。

4.回滚段（ROLLBACK）

回滚段是用来存储回滚数据的。如果使用自动方式管理回滚数据，需要在数据库中创建一个UNDO表空间，随着用户事务的执行，在UNDO表空间中将自动产生回滚段。

5.分区表段（TABLE PARTITION）

在数据库中可以将一个大表分成几个分区（PARTITION），每个分区对应一个段。这些分区表段可以存储在同一个表空间中，也可以存储在不同的表空间中。

分区表主要应用于数据仓库，它的主要作用是缩小数据库查询的范围，加快查询的速度。例如，某公司的客户表中存储了上万条客户信息，假设将这个表分成13个区，将客户信息按照其姓名的首字母不同而分别存储在不同的分区中。姓名以“A”和“B”开始的客户存储在第一个分区中，以“C”和“D”开始的存储在第二个区中，依此类推。这样一个表中的数据就被分开存储在13个分区中，如果要查询一个客户的信息，只需要在它所在的分区中查询即可，查询的范围将大约缩小为原来的1/13。

6.分区索引段（INDEX PARTITION）

与表的分区类似，索引也可以分区。将一个索引划分为几个分区，每个分区对应一个段，索引的数据将分别存储在这些段中。对索引进行分区的目的是为了加快查询索引的速度。需要注意的是，只有对表进行分区后，才能在表上创建分区索引。

7.LOB段

如果在表中定义了CLOB或BLOB类型的列，在表中就可以存储大文本或二进制数据，如文档、图像、声音等。LOB数据并不是直接存储在表段中，而是单独存放在一个LOB段中，在表中仅仅存放LOB段的指针。

8.LOB索引段

如果在表上定义了LOB类型的列，在数据库中将自动创建LOB段，同时自动创建LOB索引段。LOB索引段的作用是加快LOB列的查询速度。

9.簇段（CLUSTER）

簇是一种数据库对象，它的作用是将逻辑上相关的数据组织在一起，簇中的数据来自一个或多个表。每个簇对应一个簇段，在创建簇时将自动创建簇段，并且簇段的名称与簇完全相同。使用簇可以加快多表查询的速度。

10.索引组织表（INDEX ORGANIZED TABLE）段

如果经常要根据一个表的主键查询数据，可以将主键列上的索引与表放在一个段中，这种表称为索引组织表，它被存储在一个表段中，这种表段就是索引组织表段。索引组织表段的主要作用是加快基于主键的查询速度。

假设在数据库中创建一个表，在表上创建一个索引，在表中包含一个LOB列，那么创建表的结果是产生了四个段：表段、索引段、LOB段和LOB索引段。图7.2表示这四个段之间的关系。

图　7.2　数据库对象与段的关系

7.1.2　段的空间管理

一般来说，段不需要用户专门创建，当用户创建一个数据库对象时，在数据库中将自动产生对应的段。临时段和回滚段比较特殊，临时段位于临时表空间中，它是随着用户的排序等操作的进行而动态创建和消失的。回滚段位于UNDO表空间中，并且随着用户事务的执行而动态产生和消失。

在本地管理表空间中，区的分配策略有自动方式和手工方式两种。如果采用自动方式（AUTOALLOCATE），数据库服务器将决定区的大小，并自动进行区的分配和回收，区的大小是可变的，默认的区大小为64KB。如果采用手工方式，则需要通过UNIFORM指定区的大小，默认为1MB。

在创建本地管理表空间时，还可以使用SEGMENT SPACE MANAGEMENT子句指定段空间的管理方式。如果采用手工方式，则表空间将为每个段维护一个空闲列表（Free List），它位于数据文件的头部，在空闲列表中记录当前数据文件中所有的空闲块，段利用空闲列表管理块的分配和回收。如果选择自动方式，段将采用位图的方式管理块的分配和回收。

为了对段中数据块的使用进行控制，在段中设置了一个HWM（High Water Mark）。HWM可以认为是一个指针，在段刚被创建时，HWM位于段的开始位置。在HWM之上是没有被格式化，尚不能写入数据的数据块，而在HWM之下的数据块有以下几种情况：

·已经包含数据。

·以前曾经包含数据，但后来数据被删除。

·已经被格式化，且状态为空闲的。

·尚未被格式化。

为了给用户数据分配数据块，在段中还设置了一个低HWM，位于低HWM之下的数据块要么是已经包含数据的，要么是曾经包含数据，但后来数据被删除的。对一个用户事务而言，可用的数据块就是那些位于低HWM和HWM之间的数据块。当用户向段中写入数据时，服务器进程将在低HWM和HWM之间查找那些已经被格式化的数据块，将数据写入这些数据块。如果没有足够的数据块可用，服务器进程将在这个范围内查找那些尚未被格式化的数据块，对其进行格式化，然后将数据写入。如果这样的数据块还不能满足要求，服务器进程将把HWM向上移动，为这次事务分配其他尚未格式化的数据块，然后对其进行格式化，并将数据写入。在将数据写入数据块之后，服务器进程将把低HWM向上移动，使其指向第一个可用的数据块。数据块的分配情况如图7.3所示。

图　7.3　数据块的分配

7.2　区的管理

区是Oracle为数据库对象分配存储空间的基本单位。当用户创建表、索引、簇等数据库对象时，数据库服务器将为该对象对应的段分配若干个区，以存储该对象的数据。当段的空间被消耗完时，可以进行扩展，数据库服务器将为该段分配新的区。

当一个数据库对象被删除时，它所占用的区将被释放，数据库服务器负责回收这些区，并在适当的时机将这些空闲区分配给其他数据库对象。

7.2.1　区的分配

在本地管理表空间中，区的大小要么由数据库服务器自动指定，要么由数据库管理员指定。当在表空间中创建表、索引等数据库对象时，数据库服务器将按照指定的大小为段分配相应数量的区，在默认情况只分配一个初始区，尽管在这个区中还没有包含数据，但这个区不能分配给其他数据库对象。当初始区被写满后，数据库服务器将根据需要为段分配其他的区。

在表空间的每个数据文件的头部，都有一个位图，用来控制块的分配。当为段分配一个区时，数据库服务器将在位图中查找可用的数据块，将它们分配这个区。

首先我们执行下面的语句来创建本地管理表空间ts4：

SQL＞CREATE TABLESPACE ts4

DATAFILE'/home/oracle/ts41..dbf'SIZE 10M

EXTENT MANAGEMENT LOCAL AUTOALLOCATE；

由于采用了自动分配方式（AUTOALLOCATE），因此在表空间ts4中所有区的大小默认都是64KB。接下来我们在表空间ts4中创建表tab3：

SQL＞CREATE TABLE tab3（col int）

TABLESPACE ts4；

这样将为段TAB3分配一个区，大小为64KB，占用8个数据块（每个数据块大小为8KB）。执行下面的查询语句，将得到为段TAB3分配得区的信息：

SQL＞SELECT bytes, blocks, extents FROM dba_segments

WHERE segment_name='TAB3'；

BYTES BLOCKS EXTENTS

65536 8 1

一个段所占用的区的详细信息可以通过查询数据字典DBA_EXTENTS获得。例如，下面的SELECT语句用于查询段TAB3所占用的区的ID、大小、区中所包含数据块的数目以及第一个数据块的ID：

SQL＞SELECT extent_id, bytes/1024||'K'KBytes, blocks, block_id

FROM dba_extents

WHERE segment_name='TAB3'；

EXTENT_ID BLOCK_ID KBYTES BLOCKS

0 17 64K 8

7.2.2　区的回收

在一般情况下，从段中删除数据时，数据所占用的空间并没有被释放，除非这个数据库对象被删除。在以下几种情况中，会发生区的回收：

·一个对象被删除。

·对表进行整理。

·对索引进行重建或合并。

·在表上执行TRUNCATE命令。

·手工执行命令释放段中HWM以下未使用的空间。

在这里需要注意的是，TRUNCATE命令的执行结果是将表中的数据删除，从表面上来看类似于DELETE，但是这两条命令有很大的区别。在执行TRUNCATE命令时，表段中的HWM被移动到段的开始位置，这就意味着下次写数据是从段的开始位置进行的，以前的数据所占用的空间被释放。而在执行DELETE命令时，HWM并没有移动，这就意味着数据虽然被删除了，但是数据所占用的空间并没有被释放。

7.3　数据块的管理

数据块是Oracle中最小的存储单位，也是数据库服务器读写数据的基本单位。用户在访问数据库时，服务器进程首先将数据从数据块中读到SGA的数据库高速缓存中，并放在在与数据块大小相同的缓冲区中，然后在缓冲区中对数据进行读写。如果数据发生了变化，DBWR后台进程负责将脏缓冲区中的数据重新写入数据块。

在Oracle 11g的数据库中可以定义标准块，也可以定义非标准块。标准块的大小由初始化参数DB_BLOCK_SIZE指定。非标准块的大小可以是2KB、4KB、16KB、32KB等，只要不与标准块的大小相同即可。无论是哪种块，在SGA中都需要为其定义数据库高速缓存，缓存是由许多缓冲区组成，缓冲区大小与数据块相同。当用户访问数据块中的数据时，服务器进程将把数据从数据块读到相应的缓冲区中。

为了提高数据库的性能，Oracle规定数据块的大小必须是操作系统块的整数倍。例如，在Windows系统中，操作系统块的大小是4KB，所以数据块的大小可以是4KB、8KB、16KB等。在UNIX中，操作系统块的大小是512字节或1KB，所以数据块的大小可以是1KB、2KB、4KB等。

7.3.1　数据块的组成

在数据块中可以存储不同类型的数据，如用户数据、系统数据、表、索引等。无论是什么类型的数据，最终都是存储在数据块中的，这些数据块具有相同的结构。

一个数据块由以下几部分组成：

·块头：用来记录块本身的信息，如块地址、段类型等。

·表目录：如果在数据块中存储表中的数据，则需要把表的相关信息记录在表目录中。

·行目录：如果在数据块中存储表中的数据，还需要把这些行的信息记录在行目录中。

·空闲空间：是指数据块中尚未使用的存储空间。当向表中插入数据时，将使空闲空间减少。当删除表中的数据时，将使空闲空间增加。而当执行UPDATE操作时，可能使空闲空间增加，也可能使其减少。

·已使用空间：是指数据块中已写入数据的空间。数据库对象的数据就存储在数据块的已使用空间中。

在数据块中，块头、表目录和行目录中的信息只能由数据库服务器来维护，用户的操作只能直接影响空闲空间和已使用空间。

7.3.2　数据块的空间管理

数据块的空间管理主要涉及空闲空间的管理。当数据库服务器为数据库对象分配存储空间时，以区为单位分配几个连续的数据块，数据库对象的数据就存储在这些数据块中。当区中的一个数据块被写完时，数据将继续写入下一个数据块。当区中所有数据块都被写完时，数据将被写入下一个区。

这里以表为例来说明数据块空间的管理。当用户在表上执行INSERT操作时，新数据被写入数据块的空闲空间。当用户执行DELETE操作时，数据块中相应的数据将被删除。为了提高数据库的性能，在每个数据块中都需要保留一定的空间，这段空间是为执行UPDATE操作而保留的。

当用户执行UPDATE操作时，修改后的数据可能占用比原来更多的存储空间。例如，假设员工SMITH原来的奖金（COMM）为NULL，因为NULL是不占用存储空间的，所以当执行语句“UPDATE emp SET comm=5000 WHERE ename='SMITH'”时，该行数据将需要更多的存储空间。如果在当前块中没有预留空间，那么这行数据将被分开存储在两个块中，于是产生了块间的迁移。块间的迁移将降低数据库的性能，因为如果用户要访问这一行数据时，将不得不从两个数据块中读取数据，这样就增加了磁盘I/O。

如果在数据块中设置了保留空间，那么可能会出现这种情况：当用户执行UPDATE语句修改一行数据时，这行数据可能会占用比以前更多的空间，这时就可以使用保留空间，这样就减少了数据块之间的迁移。当然保留空间也可能无法满足UPDATE操作的需要，于是一行数据还是要被分开存储在两个数据块中，在这种情况下也将产生数据块间的迁移，只不过产生迁移现象的机会将大大减少。

如果表空间的区管理方式为本地管理，并且在创建表空间时使用SEGMENT SPACE MANAGEMENT子句指定段空间管理方式为AUTO，那么表空间将为每个段维护一个位图，并以位图的方式管理段中的数据块，这种管理是自动进行的，不需要人为干预。在其他情况下，表空间为每个段指定一个空闲列表，并以空闲列表的方式管理段中的数据块。在后一种情况下用户可以通过PCTFREE和PCTUSED两个参数控制数据块空间的使用。

PCTFREE和PCTUSED两个参数是用在数据库对象上的。在创建表、索引等数据库对象时，可以指定这两个参数，也可以在创建数据库对象之后修改这两个参数。

1.PCTFREE参数

PCTFREE参数用于设置数据块中必须保留的最少空间，参数值是一个百分比，保留的空间是为UPDATE操作使用的。当用户执行UPDATE操作时，修改后的数据可能需要比原来更多的存储空间（当然也可能比原来更少），如果没有保留空间，修改后的数据将不得不因为空间不足而另外使用一个数据块，这样一行数据将存储在两个数据块中，这种现象称为数据块间的迁移。当用户下次再访问这行数据时，将不得不从两个数据块中取出所需的数据，这样就增加了磁盘的I/O，从而降低了数据库的性能。

假设PCTFREE参数的值设置为20，那么在数据块中将有20%的保留空间，其余的80%的空间可用来存储数据。随着数据库服务器的运行，当80%的空间被消耗完时，新的数据将无法再写入这个块，于是需要使用一个新的数据块来存储数据。20%的保留空间是用于UPDATE操作的，如果用户一直没有执行UPDATE操作，或者经过UPDATE操作后数据需要的存储空间比原来反而更少，这部分空间将一直保持空闲。

PCTFREE参数的用法如图7.4所示。

PCTFREE参数的值可以设置在0～99之间。如果参数的值较大，那么发生数据块间迁移的机会将减少，数据库的性能比较高，但是数据块空间的浪费比较严重。如果将参数值设置得较小，数据块存储空间的利用率较高，但是发生数据块间迁移的机会将增加，数据库的性能将受到较大影响。

图　7.4　PCTFREE参数的用法

2.PCTUSED参数

PCTUSED参数用于设置可再次向数据块中写入数据时，已使用空间的百分比。如果将PCTFREE参数的值设置为20，那么最多可以使用数据块80%的存储空间。当可用空间被消耗完时，用户将无法继续向这个块中插入数据。当用户执行DELETE操作删除数据时，将使已使用空间减少，空闲空间增加。执行UPDATE操作也可能使已使用空间减少。随着已使用空间的不断减少，用户可再次写入数据，但是必须满足一定的条件，即已使用空间必须减少到PCTUSED参数指定的百分比。如果将PCTUSED参数值设置为60，那么当数据块中已使用空间减少到60%时，用户才可以再次向这个数据块中插入数据。

PCTUSED参数的用法如图7.5所示。

一般情况下PCTFREE和PCTUSED两个参数配合使用，共同控制数据块存储空间的使用。图7.6显示了PCTFREE和PCTUSED两个参数分别设置为20和60时的情况。

图　7.5　PCTUSED参数的用法

PCTFREE和PCTUSED参数的用法可以通过现实中的一个例子来说明。假设有一个水池，我们可以向水池中放水，也可以从水池排水。一方面，如果向水池中不断放水，当注入80%的水时即停止放水，这时水池中剩余20%的空间。另一方面，我们可以从水池中排水，当剩余60%或更少的水时，可以再次向水池中放水。如果水池中的水保持在60%～80%，不能向水池中放水。

图　7.6　数据块空间的控制

如果一个段位于本地管理表空间中，并且段空间管理方式为MANUAL，或者位于字典管理表空间中，表空间将为每个段维护一个空闲列表。在空闲列表中记录所有完全空闲的数据块，或者已经使用的，但是可以继续写入数据的数据块。如果一个数据块中的空闲空间一直减少，当达到PCTFREE参数指定的数值时，这个数据块将被从空闲列表中删除。在这两种类型的表空间中，我们可以通过PCTFREE和PCTUSED参数控制数据块存储空间的使用。

例如，以下语句用于创建表tab5，并为段tab5指定PCTFREE和PCTUSED参数：

SQL＞CREATE TABLE tab5（col int）

TABLESPACE ts10

PCTFREE 20

PCTUSED 40；

第8章　控制文件管理

控制文件是Oracle数据库中一种非常重要的文件。在Oracle数据库中主要包括：数据文件、控制文件和重做日志文件。在数据文件中存储数据库中的数据，包括各种数据库对象及其数据。在重做日志文件中存放用户执行DML及DDL命令的记录。在控制文件中存放数据库的结构信息。具体来说，在控制文件中包含以下重要信息：

·数据库的名称。

·数据文件和重做日志文件的位置和名称。

·当前日志序列号。

·检查点信息。

·表空间的名称。

·RMAN的备份与恢复信息。

控制文件是一个二进制文件，用户不能修改与查看这个文件的内容。数据库服务器在启动时需要读取这个文件的内容，以便打开数据库。为了使大家理解控制文件的重要性，首先介绍数据库服务器的启动过程。这个过程分为以下三个阶段。

第一个阶段，启动到NOMOUNT状态，数据库服务器根据参数文件的内容在内存中建立SGA，并启动DBWR、LGWR等后台进程。在这一阶段，实例将被启动；

第二阶段，启动到MOUNT状态。数据库服务器根据参数文件中记录的控制文件信息，打开控制文件；

第三阶段，启动到OPEN状态，数据库服务器根据控制文件中记录的信息，打开数据文件和重做日志文件，这时数据库服务器可正常运行并处理用户的请求。

由此可见，控制文件是实例和数据库之间的枢纽，如果控制文件丢失或损坏，数据库服务器将无法正常启动。正因为控制文件具有特殊的重要性，在创建数据库之后，或者在对数据库的结构进行修改之后，都应该及时备份控制文件。

8.1　控制文件的规划

因为控制文件具有特殊的重要性，所以对控制文件的数目和存放位置必须进行详细的规划。控制文件的数目和位置在创建数据库时就已经确定，在数据库服务器运行的过程中也可以根据需要进行修改。

8.1.1　控制文件的镜像

在一个数据库中至少需要一个控制文件。如果控制文件丢失或损坏，将导致数据库服务器无法启动。为了防止这种情况，在数据库中需要创建多个控制文件，这些控制文件互为镜像，内容完全相同。

控制文件的镜像是通过初始化参数control_files实现的。例如，通过这个参数指定三个控制文件：

control_files=（"/u01/app/base/oradata/orcl/control01.ctl"，

"/u01/app/base/oradata/orcl/control02.ctl"，

"/u01/app/base/oradata/orcl/control03.ctl"）

当数据库服务器启动时，只需要读取第一个控制文件的内容，而当数据库的结构发生变化时，被修改的信息必须同时写入所有的控制文件。如果其中一个控制文件不可用，那么数据库服务器将无法正常运行。在这种情况下，数据库管理员需要修改初始化参数control_files的值，删除其中不可用的控制文件，然后重新启动数据库服务器。假设控制文件control01.ctl损坏或丢失，初始化参数control_files可修改为：

control_files=（"/u01/app/base/oradata/orcl/control02.ctl"，

"/u01/app/base/oradata/orcl/control03.ctl"）

8.1.2　控制文件的存储位置

为了保证控制文件的安全，在创建控制文件时一个基本的要求是对它进行镜像。但如果把所有控制文件存放在同一个磁盘上，危险仍然潜在。如果磁盘损坏，那么所有的控制文件都将丢失。因此，控制文件应该分别存放在不同的磁盘上（注意，不是同一块硬盘的不同磁盘分区）。控制文件在多个磁盘上的镜像情况如图8.1所示。

图　8.1　控制文件的镜像

例如我们可将前面使用的三个控制文件分别存放在三个磁盘上，然后修改初始化参数CONTROL_FILES，例如：

control_files=（"/u01/app/base/oradata/orcl/control01.ctl"，

"/u02/control02.ctl"，

"/u03/control03.ctl"）

其中/u01、/u02和/u03是三个分别位于不同磁盘上的文件系统。如果要改变控制文件的名称或位置，首先应该关闭数据库服务器，然后在操作系统中将控制文件复制到指定的位置，如果必要，可同时修改文件名。然后修改初始化参数control_fies的值，使其指向修改后的控制文件，最后重新启动数据库服务器。

现在的数据库一般都存储在磁盘阵列上，这种存储设备本来就通过冗余机制保证文件的安全，这时候把所有的控制文件存放在同一个存储位置下即可。需要注意的是，在这种情况下，多个控制文件仍然是有必要的。

8.2　控制文件的重新创建

在创建数据库时，将根据参数文件中初始化参数CONTROL_FILES的值自动创建控制文件。在数据库服务器运行的过程中，可能会根据需要，创建新的控制文件。

8.2.1　如何增加新的控制文件

数据库服务器在运行时，在数据库中至少已经存在一个控制文件。如果要增加新的控制文件，或者更换损坏的控制文件，首先需要关闭数据库服务器。这是因为在控制文件中记录了数据库的结构信息，数据库服务器在运行时，控制文件的内容可能随时被修改，我们不能对一个已经打开的文件进行复制、删除等操作。然后根据一个已经存在的、没有损坏的控制文件复制一个新的控制文件，这需要利用操作系统的复制功能。接下来需要修改参数文件中初始化参数control_files的值，使其包含新的控制文件。最后，重新启动数据库服务器，新的控制文件即可投入使用。

无论是增加新的控制文件、删除某个控制文件，还是修改控制文件的名称和位置，都需要修改初始化参数control_files的值。尽管在SQL*Plus中可以通过ALTER SYSTEM命令修改这个参数，把修改结果写入服务器参数文件，但这种方法很容易使服务器参数文件的格式出现错误。一个可行的办法是，首先根据服务器参数文件创建一个文本参数文件，在文本参数文件中修改这个初始化参数，然后通过文本参数文件启动实例，最后根据文本参数文件重新创建服务器参数文件。

8.2.2　如何重新创建控制文件

控制文件是在创建数据库时自动创建的，在数据库服务器运行的过程中，除了镜像控制文件外，重新创建控制文件的机会不是很多。但是一旦有这样的要求，就意味着对数据库要进行一次重要的操作。一般说来，重新创建控制文件的时机有以下几种情况：

·控制文件全部损坏，并且没有可用的备份。

·修改数据库的名称。

·修改某些永久性参数。

当然，重新创建控制文件并不是一件轻松的事情。如果没有周密的计划，可能会造成更大的损失。在创建控制文件之前，必须掌握所有数据文件和重做日志文件的名称、位置和大小等信息，在必要时还需要掌握数据库所使用的字符集。为了防止对数据文件和重做日志文件造成不必要的损坏，尽可能对这些文件进行一次备份。

1.控制文件全部不可用的情况

在控制文件中记录了数据库的结构信息，如果数据库的结构发生了变化，如新创建了一个表空间，或新添加了一个重做日志文件等，应该及时对控制文件进行一次备份。如果数据库中的控制文件全部丢失或损坏，而手头没有可用的控制文件的备份，这不能不说是数据库管理员的失职。

如果发生了这种情况，数据库服务器将无法正常启动，因而就不可能通过数据字典视图查询数据文件和重做日志文件的信息。唯一的办法是在磁盘上查找这些文件，并了解它们的大小。如果漏掉了某个文件，数据库将不完整。更可怕的事情是，如果在同一个系统中创建了多个数据库，并且数据文件和重做日志文件的存放位置没有任何规律，那么要确定一个文件是否属于当前数据库是一件很困难的事情。

创建控制文件的命令是CREATE CONTROLFILE。一般情况下创建控制文件的任务由SYS用户完成，如果以普通用户的身份创建控制文件，则需要具有SYSDBA权限。

在创建控制文件时，需要首先启动实例，将数据库服务器启动到NOMOUNT状态。控制文件名是不需要指定的，因为在启动实例时，将读取参数文件的内容，根据初始化参数control_files的值确定了控制文件的位置和名称。以下是创建控制文件的一个例子：

SQL＞CREATE CONTROLFILE REUSE DATABASE ORCL NORESETLOGS

MAXLOGFILES 5

MAXLOGMEMBERS 3

MAXDATAFILES 100

MAXINSTANCES 1

MAXLOGHISTORY 292

LOGFILE

GROUP 1'/u01/app/base/oradata/orcl/redo01.log'SIZE 50M BLOCKSIZE 512，

GROUP 2'/u01/app/base/oradata/orcl/redo02.log'SIZE 50M BLOCKSIZE 512，

GROUP 3'/u01/app/base/oradata/orcl/redo03.log'SIZE 50M BLOCKSIZE 512

DATAFILE

'/u01/app/base/oradata/orcl/system01.dbf'，

'/u01/app/base/oradata/orcl/sysaux01.dbf'，

'/u01/app/base/oradata/orcl/undotbs01.dbf'，

'/u01/app/base/oradata/orcl/users01.dbf'

CHARACTER SET WE8MSWIN1252

在创建控制文件的CREATE CONTROL FILE语句中，使用了以下子句：

·REUSE：如果在指定的位置存在同名的控制文件，则用新的控制文件覆盖它。

·RESETLOGS：清空重做日志文件中的信息。如果不希望清空重做日志，可以使用NORESETLOGS。

·DATABASE：指定数据库的名称。这个名称必须与初始化参数DB_NAME的值相同。

·LOGFILE：指定所有的重做日志文件的名称和大小。如果每个日志组有一个以上的日志成员，还需要通过GROUP关键字指定日志文件的分组情况。

·DATAFILE：指定所有数据文件的名称和大小。

·CHARACTER SET：指定字符集的名称。

控制文件被创建之后，数据库服务器就打开控制文件，进入MOUNT状态了。为了使用户能够访问数据库，必须将数据库服务器的状态转入OPEN状态，可以执行以下命令：

SQL＞ALTER DATABASE OPEN；

如果在创建控制文件时使用了RESETLOGS子句，那么在打开数据库时也必须使用这个子句。比如：

SQL＞ALTER DATABASE OPEN RESETLOGS；

2.修改数据库名称的情况

在有些情况下，可能需要修改数据库的名称。比如要在两个数据库之间进行高级复制，一个基本的要求就是两个数据库的名称不能，如果相同，必须修改其中的一个数据库的名称。

数据库的名称由初始化参数DB_NAME指定，实例的名称通过初始化参数INSTANCE_NAME指定。通过查询这两个初始化参数，可以了解数据库和实例的名称。例如：

SQL＞SHOW PARAMETER DB_NAME

NAME TYPE VALUE

db_name string ORCL

SQL＞SHOW PARAMETER INSTANCE_NAME

NAME TYPE VALUE

instance_name string ORCL

在修改数据库的名称时，需要修改初始化参数DB_NAME的值。另外，数据库的名称还必须记录在控制文件中。因此，必须重新创建控制文件，才能使新的数据库名称起作用。

在这种情况下，数据库服务器是可以正常运行的，因此，我们可在创建控制文件之前，查询数据字典，了解数据文件和重做日志文件的信息。例如，以下查询将得到所有数据文件的位置和名称：

SQL＞SELECT NAME FROM V$DATAFILE；

以下查询将得到所有的重做日志文件：

SQL＞SELECT GROUP#，MEMBER FROM V$LOGFILE ORDER BY GROUP#；

在创建控制文件时，还需要指定字符集的名称，这些信息可以通过查询动态性能视图V$NLS_PARAMENTS获得。例如：

SQL＞SELECT PARAMETER, VALUE FROM V$NLS_PARAMETERS

WHERE PARAMETER='NLS_CHARACTERSET'；

PARAMETER VALUE

NLS_CHARACTERSET ZHS16GBK

接下来需要修改初始化参数DB_NAME的值。假设要将数据库名称改为ORCL1，需要按照以下方式修改参数文件中的初始化参数：

DB_NAME=ORCL1

现在就可以创建控制文件了。首先用新的参数文件将数据库服务器的状态转入NOMOUNT状态，然后执行CREATE CONTROLFILE命令。这时在命令行中也要指定新的数据库名称。例如：

SQL＞CREATE CONTROLFILE REUSE SET DATABASE orcl1 RESETLOGS

……

控制文件创建成功之后，将数据库服务器启动到OPEN状态，用户就可以访问数据库了。这时新的控制文件已经在起作用了。

如果要验证数据库的名称确实已经被修改，可以再次查询初始化参数DB_NAME的值。例如：

SQL＞SHOW PARAMETER DB_NAME

NAME TYPE VALUE

-----------------------

db_name string ORCL1

查询结果表明，数据库的名称和实例的名称确实已经被修改。

3.修改永久性参数的情况

在创建数据库时，可以指定一些永久性参数，如MAXINSTANCES、MAXDATAFILES等。这些永久性参数的意义描述如下：

·MAXINSTANCES：指定可以访问该数据库的最大实例个数。

·MAXLOGHISTORY：指定在控制文件中可以记录的最大历史日志条数。

·MAXLOGFILES：指定在数据库中包含的最大重做日志组数。

·MAXLOGMEMBERS：指定每个日志组中可以包含的最大重做日志文件数目。

·MAXDATAFILES：指定在数据库中可以创建的最大数据文件数目。

以上永久性参数对数据库的扩充极限进行了限制。例如，假设参数MAXDATAFILES的值为100，那么在数据库中最多只能创建100个数据文件，如果达到了这个极限，数据库将无法扩充。又比如，假设原来的数据库只允许一个实例访问，现在希望把数据库移植到RAC环境，就需要修改参数MAXINSTANCES的值。

与初始化参数不同的是，永久性参数不是记录在参数文件中，而是记录在控制文件中。如果要修改永久性参数的值，就需要重新创建控制文件。

在Oracle 11g以前，如果要在数据库中修改永久性参数，就需要重新创建控制文件。从11g开始，这个问题简化了许多，这些永久性参数的设置可以直接突破。假设数据库中只能创建100个数据文件，那么第101个数据文件还是可以直接创建的，控制文件也将自动扩展，以容纳新增加的数据文件。

在Oracle 11g以前的数据库中为了修改永久性参数，与前面两种情况相同，重新创建控制文件的操作需要在实例的NOMOUNT状态下进行，在CREATE CONTROLFILE语句中同样要指定所有数据文件和重做日志文件的位置、名称和大小，以及字符集的名称，同时要为需要修改的永久性参数指定新的参数值。

例如，为了修改永久性参数的值，可以执行以下语句，重新创建数据库ORCL的控制文件：

SQL＞CREATE CONTROLFILE REUSE DATABASE ORCL NORESETLOGS

MAXLOGFILES 16

MAXLOGMEMBERS 3

MAXDATAFILES 100

MAXINSTANCES 8

MAXLOGHISTORY 292

LOGFILE

GROUP 1'/u01/app/base/oradata/orcl/redo01.log'SIZE 50M BLOCKSIZE 512，

GROUP 2'/u01/app/base/oradata/orcl/redo02.log'SIZE 50M BLOCKSIZE 512，

GROUP 3'/u01/app/base/oradata/orcl/redo03.log'SIZE 50M BLOCKSIZE 512

DATAFILE

'/u01/app/base/oradata/orcl/system01.dbf'，

'/u01/app/base/oradata/orcl/sysaux01.dbf'，

'/u01/app/base/oradata/orcl/undotbs01.dbf'，

'/u01/app/base/oradata/orcl/users01.dbf'

CHARACTER SET WE8MSWIN1252

在创建控制文件之后，将数据库服务器启动到OPEN状态，新的永久性参数就能起作用了。

8.3　控制文件的备份与删除

因为控制文件具有特殊的重要性，所以要对其采取必要的保护措施。除了镜像控制文件外，还要经常进行对它进行备份。备份控制文件的操作可以在操作系统下进行，也可以在数据库中进行。在操作系统下备份控制文件时，首先要停止数据库服务器的运行，然后将控制文件复制到备份设备上，最后重新启动数据库服务器。用这种方法备份控制文件时，数据库服务器将暂时停止，在这段时间内用户将无法访问数据库。

在数据库中备份控制文件时，不需要关闭数据库服务器。可以将控制文件备份为一个二进制文件，也可以在跟踪文件中生成一条SQL语句。例如以下语句将控制文件备份为另一个二进制文件：

SQL＞ALTER DATABASE BACKUP CONTROLFILE TO'/home/oracle/control.bak'

如果控制文件丢失或损坏，需要关闭数据库服务器，将这个备份文件复制到控制文件所在的各个目录，并将它的名称改为与控制文件相同，然后重新启动数据库服务器。

以下语句用于将控制文件备份到跟踪文件之中：

SQL＞ALTER DATABASE BACKUP CONTROLFILE TO TRACE；

跟踪文件的默认位置为Oracle安装目录下的admin/＜SID＞/udump子目录，这个目录可以通过初始化参数USER_DUMP_DEST来确定。利用这种方法备份控制文件，在跟踪文件中将生成一条CREATE CONTROLFILE语句，这是一条用来重新创建控制文件的完整语句。当控制文件损坏或丢失时，将数据库服务器启动到NOMOUNT状态，然后执行跟踪文件中的这条语句，新的控制文件将被创建。

如果部分控制文件损坏，或者希望减少控制文件的数目，就需要将损坏的或者无用的控制文件删除。在删除控制文件时，首先需要关闭数据库服务器，然后修改初始化参数CONTROL_FILES，去掉将被删除的控制文件，接下来在操作系统中删除那些控制文件，最后重新启动数据库服务器。

8.4　控制文件信息的查询

控制文件的路径和名称可以从初始化参数或动态性能视图中获得，反过来，在控制文件中记录了数据库的结构信息。通过查询相关的动态性能视图，可以了解与控制文件有关的信息。

8.4.1　查询控制文件的位置和名称

控制文件的位置和名称信息可以从动态性能视图V$CONTROLFILE中获得。通过查询这个动态性能视图，可以获得数据库中的所有控制文件。例如：

SQL＞SELECT status, name FROM V$CONTROLFILE；

STATUS NAME

---------------------------------------

/u01/app/base/oradata/orcl/control01.ctl

/u01/app/base/oradata/orcl/control02.ctl

控制文件的位置和名称是通过初始化参数CONTROL_FILES指定的。通过查询这个初始化参数的值，也可以了解控制文件的信息。例如：

SQL＞SHOW PARAMETER CONTROL_FILES；

NAME TYPE VALUE

---------------------------------------------------------

control_file sstring/u01/app/base/oradata/orcl/control01.ctl，

/u01/app/base/oradata/orcl/control02.ctl

所有的初始化参数的信息都可以从动态性能视图V$PARAMENTER中获得，通过查询这个视图，也可以了解控制文件的信息。例如：

SQL＞SELECT VALUE FROM V$PARAMETER

WHERE NAME='control_files'；

8.4.2　查询控制文件中记录的信息

在控制文件中记录了数据库的结构信息，如数据文件和重做日志的名称及大小，永久性参数的信息等。控制文件在创建时，它的大小就已经确定，每条信息在控制文件中占用固定大小的存储空间。控制文件的信息是以记录（record）的形式组织的，每条记录所占的存储空间大小、控制文件能够存储的这种记录的数目，以及已经存储的这种记录数等信息，都可以从动态性能视图v$controlfile_record_section中查询。考虑以下查询：

SQL＞SELECT type, record_size, records_total, records_used

FROM v$controlfile_record_section；

type record_size records_total records_used

------------------------------------------------------

DATABASE 316 1 1

CKPT PROGRESS 8180 11 0

REDO THREAD 256 8 1

REDO LOG 72 16 3

DATAFILE 520 100 4

FILENAME 524 2298 8

TABLESPACE 68 100 5

TEMPORARY FILENAME 56 100 1

RMAN CONFIGURATION 1108 50 0

LOG HISTORY 56 292 1

OFFLINE RANGE 200 16 0

在查询结果中列出了每种类型记录的大小、允许的最大数目以及已存记录的数目。其中type表示记录类型，record_size表示该类型的每条记录所占用的字节数，records_total表示在控制文件中能存储的该类型记录的数目，records_used表示这种记录已经存在的数目。例如，记录类型TABLESPACE表示表空间，这种类型的记录每条占用68字节，在控制文件中最多可以存储100条这样的记录，目前已经存储了8条这样的记录，这说明在当前数据库中最多能创建100个表空间，目前已经创建了5个。又比如，记录类型DATAFILE表示每个数据文件在控制文件中占用520字节，在数据库中最多可以创建100个数据文件，目前已经存在4个数据文件。

第9章　重做日志管理

重做日志是对用户的DML和DDL操作所做的记录，它是保证数据库安全的一种重要手段。当用户执行DML或DDL命令时，数据库服务器将生成重做日志，并将其记录在重做日志文件中。为了保证重做日志文件的安全，需要对它们进行归档。如果数据库出现故障，可以利用重做日志文件和归档日志文件进行实例恢复或者介质恢复。无论是重做日志文件还是归档日志文件，都是以二进制的形式记录重做日志的。用户如果需要了解这些文件的内容，就需要对它们进行分析，将日志信息转化为文本形式。

本章的主要内容包括重做日志文件的管理、归档日志文件的管理以及日志分析等内容。与重做日志有关的内容较多，接下来我们首先对这些内容做一次综合介绍，以使大家对重做日志有一个全面、深刻的认识。

当用户执行SQL语句时，在用户进程和服务器进程之间需要建立一条连接。用户进程向服务器进程发送SQL语句，服务器进程对这些SQL语句进行解析，并生成执行计划，然后将解析代码和执行计划存储在SGA的库缓冲区中，然后按照执行计划执行SQL语句。在执行阶段，服务器进程首先检查用户访问的数据是否已经位于数据库高速缓存中。如果是，则直接在数据库高速缓存的缓冲区中访问数据，否则，需要从数据文件中将对应的数据块读到数据库高速缓存中。如果用户的SQL命令是DML或DDL，那么在访问数据之前首先生成重做日志，并将重做日志记录在重做日志缓冲区中，然后修改数据库高速缓存中的缓冲区。被修改的缓冲区叫做脏缓冲区。

重做日志缓冲区是SGA的一部分，它的作用是记录数据库中的重做日志。重做日志是对用户执行的DML或DDL操作的记录，其中包括被修改的数据块、修改的位置以及修改后的数据等信息。重做日志缓冲区的大小由初始化参数LOG_BUFFER指定。当实例启动时，在内存中按照这个参数的值为重做日志缓冲区分配内存空间。

从上面的描述可以看到，当用户在修改数据时，服务器进程首先生成重做日志，然后才修改缓冲区中的数据。无论是重做日志还是被修改后的数据，都是位于内存中的。由于内存中的数据是易消失的，Oracle提供了两个后台进程，用于将这些数据写入磁盘上的文件中。其中DBWR的作用是在一定的时机下将数据库高速缓存中的脏缓冲区写入数据文件，LGWR进程的作用是在一定的时机下将重做日志缓冲区中的内容写入重做日志文件。DBWR和LGWR进程的执行时机并不是同步的，但是在DBWR工作之前，LGWR一定将重做日志写入了重做日志文件中。

用户在进行一次数据访问时，尽管被修改的数据可能已经写入了数据文件，重做日志也可能写入了重做日志文件，但这次访问并不一定是一个有效的事务，因为对数据库所做的任何修改都必须同时记录在数据文件、控制文件和重做日志文件中。Oracle是通过SCN来维护这个一致状态的。数据库服务器每执行一个事务，都将产生一个新的、递增的SCN。如果用户没有提交事务，新的SCN是不被写入任何一个文件的。这时如果系统突然断电，那么数据库服务器在重新启动时，SMON后台进程将检查数据文件、控制文件和重做日志文件中的SCN，并回滚所有的未提交事务，然后打开数据库。

如果用户在访问数据之后提交了事务，那么重做日志缓冲区的重做日志连同最新的SCN一起被写入重做日志文件。既然SCN和重做日志一起被写入了重做日志文件，实例恢复和介质恢复就成为可能。如果发生了系统掉电或者数据文件损坏等情况，可以根据重做日志文件的内容对数据库中的数据进行恢复。

用户提交事务后，最新的SCN仅仅写入了重做日志文件，而数据文件和控制文件中SCN的写入则依赖于检查点。检查点是一种数据库事件，当数据库服务器发生检查点时，DBWR进程将数据库高速缓存中的所有脏缓冲区写入数据文件，并且使数据文件、控制文件和重做日志文件中的SCN达到一致状态。检查点是由后台进程CKPT发出的，CKPT进程在一定的时机下发生检查点，同步数据文件、控制文件和重做日志文件的状态。

假设在用户提交事务后CKPT进程恰好发出一个检查点，这时三种文件的状态完全一致，如果这时系统突然断电，那么数据库服务器在重新启动时，可以直接打开数据库，不需要进行实例恢复，所以这次启动会很快。

如果在用户提交事务后CKPT还没有来得及发出检查点时系统突然断电，那么数据库服务器在重新启动时就需要进行实例恢复。在这种情况下，数据文件和控制文件中的SCN应该是一致的，并且小于重做日志文件中的SCN。SMON后台进程将检测到三个文件中的SCN不一致，于是将两个SCN之间的所有已经提交的事务重新执行一遍，并回滚所有未提交的事务，然后再打开数据库。Oracle利用这种方法来保证用户已经提交的事务不受系统故障的影响。

当用户执行DML操作时，服务器进程将修改前的数据首先写入回滚段，然后在数据库高速缓存中对数据进行修改。如果用户提交了事务，回滚段中的数据成为无效数据。如果用户回滚了该事务，那么回滚段中的数据将被写回原来位置，用户对这些数据所做的修改便宣告无效。

因为重做日志缓冲区的大小是有限的，而且它不能永久存储数据，因此重做日志需要被写入重做日志文件。在数据库中一般有若干个重做日志组，每个日志组中有若干个日志成员，数据库服务器以循环的方式将重做日志写入这些重做日志组。当一组重做日志文件被写满后，数据库服务器自动切换到下一个日志组。当最后一组重做日志文件被写满后，数据库服务器又自动切换到第一组。

在数据库服务器切换日志时，新的重做日志被写入重做日志文件，文件中原来的内容将被覆盖。为了保留以前的重做日志，需要对重做日志文件进行归档。重做日志文件的归档是由后台进程ARCH完成的。

9.1　重做日志的规划

在创建数据库之前，对重做日志进行详细的规划是很有必要的。重做日志的规划包括重做日志缓冲区规划、重做日志组规划以及重做日志文件的规划。通过对重做日志进行规划，不仅可以提高数据库系统的性能，而且能够保证重做日志的安全。

9.1.1　重做日志缓冲区的规划

当用户执行一个事务时，服务器进程首先生成重做日志，并记录在重做日志缓冲区中，而不是将其直接写入重做日志文件中，这样做的好处当然是因为内存的读写速度比硬盘快得多。在重做日志缓冲区中可以存储多条重做日志，LGWR后台进程在一定的时机下一次将多条重做日志全部写入重做日志文件，这样可以减少写磁盘的次数，从而提高数据库的性能。

重做日志缓冲区是一段循环使用的存储区，重做日志将从缓冲区的头部开始，依次写入缓冲区的各个部分。当最后一部分缓冲区被写满时，重做日志又重新被写入缓冲区的开始部分。在一定的时机下，LGWR进程将缓冲区中的重做日志写入重做日志文件，并清空这一部分重做日志所占用的存储区。例如，当重做日志缓冲区1/3的存储空间被写满时，LGWR进程便开始工作。

重做日志缓冲区的大小由初始化参数LOG_BUFFER指定，这个参数的值越大，LGWR进程写入磁盘文件的次数就越少。因此，只要计算机的内存足够大，尽可能为重做日志缓冲区分配更大的存储区，这样可以减少磁盘写操作的次数，从而提高系统的性能。

9.1.2　重做日志文件组的规划

数据库中的重做日志文件是分组的，在一个数据库中至少需要两个日志组，日志组的最大数目由永久性参数MAXLOGFILES限定，这个数目在创建数据库时就已经确定。在数据库中创建多个重做日志组的主要目的是为了对重做日志文件进行归档，因为当数据库正在使用一个重做日志文件时，是不能对它进行归档的，只有当切换到下一个日志组时，才能对它进行归档。其次，使用多个日志组可以节约磁盘空间，如果只有一个日志组，那么所有的重做日志都将被写入这组重做日志文件，文件的大小将无限制地增加，从而消耗大量的磁盘空间。

重做日志是以循环的方式被写入各个重做日志组的。如图9.1所示，当数据库服务器刚开始运行时，重做日志被写入第一个日志组，当第一个日志组中的重做日志文件被写满时，数据库服务器将自动切换到第二个日志组，重做日志将接着被写入第二个日志组，依此类推。当最后一个日志组被写满后，数据库服务器将自动切换到第一个日志组，这样重做日志便被写入第一个日志组。当数据库服务器再次使用一个日志组时，这组重做日志文件中以前的内容将被新的重做日志覆盖。

图　9.1　重做日志组

在对重做日志文件进行规划时，应该选择合适的日志组数目。如果日志组太多，可能会消耗太多的磁盘空间。如果日志组太少，可能会影响数据库的性能。如果数据库处于归档日志模式下，当数据库服务器切换到下一个日志组时，ARCH进程要对刚刚使用过的日志组进行归档。如果归档的速度较慢，或者数据库服务器写日志的速度较快，那么可能在日志归档还没有结束时，数据库服务器又切换到了这个日志组。当数据库服务器试图使用一个尚未归档或者归档未结束的日志组时，LGWR进程将被阻塞，用户事务将无法执行，直到归档结束。

如果在数据库中有上述情况发生，在数据库的警告文件中将产生这样的警告信息：“checkpoint incomplete”。虽然这种情况持续的时间并不长，但是对数据库性能的影响是比较大的。解决这种问题的最直接的办法是在数据库中增加日志组，或者将目前的重做日志文件替换为更大的文件。

重做日志组数目的选择需要一定的经验，数据库管理员可以在数据库的运行过程中不断观察，在不影响LGWR进程工作的前提下，将日志组的数目设置为最小的可能值。

如果发生了LGWR进程被阻塞的情况，在警告文件和LGWR进程的跟踪文件中将产生相应的信息。数据库管理员在设置重做日志组时，可以不断观察这个文件的内容，以判断是否有这种情况发生。

现在以一个实际的例子来说明重做日志组太少，或者重做日志文件太小对数据库系统所造成的不利影响。在某个实际的生产系统中，数据库管理员经常接到用户的抱怨。在业务最繁忙的时段内，用户进程向数据库中写入一行数据需要等待50秒左右。通过对数据库进行诊断后发现，在该数据库中每天大约产生3GB的重做日志，而数据库中当时只有三个重做日志组，每个组中的成员只有50MB。通过进一步对重做日志文件的归档情况进行观察发现，在业务最繁忙的时段内，一个重做日志组被写满所用的时间大约为2分钟，而对一个重做日志组进行归档所用的时间大约为4分钟，而且在警告文件中有大量的警告信息：“checkpoint incomplete”。

在找到问题发生的原因后，解决这个问题就很简单了。在该数据库中另外增加了6个重做日志组，每个日志组的日志成员大小为200MB，然后删除了以前的三个重做日志组，这个问题基本上就解决了。

9.1.3　如何对重做日志文件进行规划

在每一个重做日志组中，至少有一个重做日志文件，日志组中的重做日志文件叫做日志成员。重做日志组中的日志成员在数据库创建时已经确定，在数据库的运行过程中也可以添加新的日志成员。

重做日志对于数据库的恢复起着重要的作用。如果重做日志文件丢失或者损坏，那么在数据库发生故障时，可能无法对数据进行完全恢复。对于重做日志文件，应该采取特殊的措施来保证它的可用性。一般要求一个重做日志组中至少有两个日志成员，这些重做日志文件的内容、大小完全相同，它们互为镜像。LGWR后台进程将把重做日志同时写入日志组的所有日志成员。当其中一个日志成员丢失或损坏时，LGWR进程将被阻塞。数据库管理员可以利用其他完好的日志成员复制一个新的日志成员，数据库服务器则可继续正常运行。

为了防止磁盘发生故障，一般要求同一个日志组中的各个日志成员分别存储在不同的磁盘上，这样即使其中一个磁盘发生故障，其他磁盘上的日志成员的内容仍然是完好的。

图9.2表示日志成员的镜像情况。在图中有三个日志组，每个日志组有两个日志成员，它们分别存储在两个磁盘上。

需要注意的是，同一个日志组的日志成员大小完全相同，但不同日志组的日志成员可以有不同的大小，而且不同日志组的成员数目也可以不同。

图　9.2　日志成员的镜像

在数据库服务器运行的过程中，LGWR进程可能会遇到以下异常情况：

·某个重做日志组中的一个日志成员不可用。LWGR进程将忽略不可用的日志成员，把重做日志写入其他可用的日志成员。

·某个重做日志组正在被归档或者尚未归档。LGWR进程将等待该重做日志组被归档完成。·某个重做日志组中的所有日志成员都不可用。在数据库服务器进行日志切换时，该日志组对LGWR进程不可用，数据库实例将自动关闭。

·当前重做日志组中的所有日志成员突然都不可用。因为LGWR进程无法写重做日志，数据库实例将自动关闭。

9.2　重做日志文件的管理

在创建数据库时，重做日志组与日志成员的数目已经确定。在数据库的运行过程中，可以根据需要增加重做日志组和日志成员的数目。重做日志组的最大允许数目由永久参数MAXLOGFILES限定，每个日志组最大允许的日志成员数目由永久参数MAXLOGMEMBERS限定。如果重做日志组或日志成员的数目已经达到规定的极限，就需要重新创建控制文件，增加永久参数的值。在Oracle 11g的数据库中，这两个永久性参数是可以突破的。重做日志组和日志成员的数目也可以根据需要适当地减少。如果日志成员的存储位置不合适，还可以改变它们的存储位置和名称。

9.2.1　增加重做日志组

在一个小型应用系统中，由于用户的事务较少，两个重做日志组就可能足够使用。对于大型的应用系统，用户的事务非常繁忙，需要更多的重做日志组来记录重做日志。如果日志组太少，可能会发生这样的情况：LGWR进程在向一个重做日志组中写入重做日志时，发现这个重做日志组尚未归档，或者归档尚未结束，于是LGWR进程不得不等待归档的结束，整个数据库服务器的运行也将暂时停止。

出现上述情况的原因是重做日志组太少，数据库管理员应该增加重做日志组的数目，以免因为重做日志组太少而影响数据库的性能。

增加重做日志组的命令是ALTER DATABASE，在这条命令中使用ADD LOGFILE子句指定重做日志组的组号以及该日志组中每个日志成员的名称及大小。例如：

SQL＞ALTER DATABASE ADD LOGFILE GROUP 4

（'/home/oracle/orcl/redo41.log'，

'/home/oracle/orcl/redo42.log'）SIZE 200M；

重做日志组的组号应该为现有的最大组号加1。当然，也可以不指定组号，这时数据库服务器将按照这个原则自动分配组号。例如：

SQL＞ALTER DATABASE ADD LOGFILE

（'/home/oracle/orcl/redo41.log'，

'/home/oracle/orcl/redo42.log'）SIZE 200M；

9.2.2　增加日志成员

如果一个重做日志组中只有一个日志成员，那么数据库管理员可能需要向这个日志组中添加新的日志成员。当日志组中的一个日志成员损坏时，数据库管理员需要删除这个日志成员，然后再添加一个。

添加日志成员的命令是ALTER DATABASE，在这条命令中通过ADD LOGFILE MEMBER子句指定新的日志成员，并通过TO子句指定目标重做日志组。例如：

SQL＞ALTER DATABASE ADD LOGFILE MEMBER

'/home/oracle/orcl/redo22.log'，'/home/oracle/orcl/redo23.log'

TO GROUP 2；

通过以上命令，向重做日志组2添加了两个日志成员。它们分别是redo22.log和redo23.log。新添加的日志成员的大小与该组中已有成员的大小完全相同。

9.2.3　修改重做日志文件的存储位置和名称

在数据库的运行过程中，有时可能会根据需要修改重做日志文件的存储位置或名称。假如一个日志组的所有成员存储在同一个磁盘上，为了防止磁盘故障，有必要将这些成员存储在不同的磁盘上。

重做日志文件的信息是记录在控制文件中的，修改重做日志文件的名称或存储位置就意味着修改控制文件中的信息。在数据库中无法真正实现重做日志文件的移动或重命名，这一点需要在操作系统下完成。因此，要实现重做日志文件的移动或重命名，需要将数据库服务器切换到MOUNT状态，这时控制文件已经打开，而重做日志文件尚未打开。

首先，关闭数据库。注意应该以NORMAL方式关闭数据库，确保所有的用户事务正常结束。接下来需要在操作系统下将重做日志文件移动到新的存储位置，或者修改它的名称。然后将数据库服务器启动到MOUNT状态。修改重做日志文件的位置或名称的命令是ALTER DATABASE，在这条命令中通过RENAME FILES子句修改重做日志文件的位置或名称。实际上，通过这种方法也可以修改数据文件的位置和名称。

例如，以下语句将重做日志文件redo23.log的位置改为“/u01/orcl”，并将名称修改为redo203.log。

SQL＞ALTER DATABASE RENAME

'/home/oracle/orcl/redo23.log'TO'/u01/orcl/redo203.log'；

多个重做日志文件的位置和名称可以通过同一条语句来修改。例如，下面的语句同时将重做日志文件redo41.log和redo42.log修改为新的名称。

SQL＞ALTER DATABASE RENAME FILE

'/home/oracle/orcl/redo41.log'，'/home/oracle/orcl/redo42.log'

TO

'/home/oracle/orcl/redo401.log'，'/home/oracle/orcl/redo402.log'；

9.2.4　删除重做日志文件

在一些情况下，需要将某个重做日志组或日志成员删除。例如，由于某个日志成员损坏而导致整个重做日志组不可用，这时需要将损坏的日志成员删除。

删除日志组时，日志组中的所有日志成员都将被删除。为了保证数据库服务器的运行，应该保证至少有两个日志组可用。另外，如果日志组的状态为“CURRENT”，那么该日志组是不能删除的，这时必须进行日志组的切换。“CURRENT”状态表明数据库服务器当前正在使用该日志组。在删除日志组之前，应该先查询日志组的状态。例如：

SQL＞SELECT GROUP#，STATUS FROM v$log；

GROUP#STATUS

--------------------------

1 CURRENT

2 INACTIVE

3 INACTIVE

4 INACTIVE

删除日志组的命令是ALTER DATABASE，通过DROP LOGFILE子句指定要删除的日志组。例如，以下语句用于删除日志组4：

SQL＞ALTER DATABASE

DROP LOGFILE GROUP 4；

为了保证重做日志的连续性，在归档模式下删除一个重做日志组时，应先对其进行归档。重做日志组的归档状态也可以通过查询动态性能视图v$log获得。

当重做日志组被删除后，所有的日志成员都将被删除。在有些情况下，仅需要删除日志组的某些日志成员，其余成员仍然保留。比如当某个重做日志文件损坏时，应该将该日志成员删除，以保证数据库服务器的正常运行。

删除日志成员时，不能删除重做日志组中唯一的成员。为了保证重做日志文件的安全，应该保证日志组中至少有两个成员。当某个日志组中仅剩一个日志成员时，应该及时添加其他的日志成员。如果某个日志组处于“CURRENT”状态，不能删除该重做日志组中的任何成员，这时必须手工切换日志，使重做日志组的状态切换为“INACTIVE”状态。

删除日志成员的命令仍然是ALTER DATABASE，在命令中通过DROP LOGFILE MEMBER子句指定要删除的日志成员。因为重做日志文件的位置和名称是唯一的，因此，不需要指定该日志成员所属的重做日志组。例如，下面的语句用于删除重做日志组4的日志成员redo402.log：

SQL＞ALTER DATABASE

DROP LOGFILE MEMBER'/home/oracle/orcl/redo402.log'；

当一个重做日志组或一个日志成员被删除后，对应的重做日志文件并没有被真正删除。为了彻底删除重做日志文件，还需要在操作系统中手工执行删除操作。

9.2.5　重做日志文件的清空

我们不仅可以删除重做日志组和日志成员，还可以清空重做日志文件的内容。在数据库的运行过程中，如果由于重做日志文件的损坏而导致归档操作无法进行，数据库服务器的运行最终将停止，在这种情况下，可以将重做日志文件的内容全部清空。

清空重做日志文件的操作可以在不关闭数据库的情况下进行，即使数据库中只有两个日志组，或者该重做日志所属日志组的状态为“CURRENT”。

清空重做日志文件的命令是ALTER DATABASE，在这条命令中通过CLEAR LOGFILE子句指定要清空的日志组，执行清空操作时一个重做日志组中的所有日志成员都将被清空。例如，下面的语句用于清空重做日志组3中的所有日志成员：

SQL＞ALTER DATABASE

CLEAR LOGFILE GROUP 3；

如果要清空的重做日志组尚未归档，则需要使用UNARCHIEVED关键字。例如：

SQL＞ALTER DATABASE

CLEAR UNARCHIVED LOGFILE GROUP 3；

以上语句在清空尚未归档的重做日志文件时，避免对其进行归档。尽管没有归档，但被清空的重做日志文件可再次使用了。需要注意的是，如果清空了尚未归档的重做日志文件，应该对数据库进行一次备份。

如果一个重做日志组被清空，而在进行数据库恢复时恰好需要这个组的重做日志文件，那么恢复操作将无法进行。数据库服务器将把这种无法进行的恢复情况写入警告文件。

9.2.6　重做日志的切换

数据库服务器在运行过程中，LGWR后台进程将重做日志写入重做日志文件。当第一个日志组写满后，LGWR将自动切换到第二个日志组，并将重做日志写入第二个日志组。依此类推，当最后一个日志组被写满后，LGWR将自动切换到第一个日志组，这个循环的过程是自动进行的。

在有些特殊的情况下，需要手工切换日志，在当前重做日志组尚未写满的时候，强制LGWR进程将重做日志写入下一个日志组。比如，要删除当前日志组时，需要先强制切换日志。手工切换重做日志的命令是ALTER SYSTEM，执行手工切换重做日志任务的用户需要有ALTER SYSTEM系统权限。命令的完整格式为：

ALTER SYSTEM SWITCH LOGFILE

无论是自动切换还是手工切换，当发生重做日志的切换时，日志序列号将自动加一，日志序列号和当前的SCN被写入控制文件。随后CKPT后台进程将发出检查点，将SCN写入控制文件和数据文件，然后激活DBWR后台进程，将数据库高速缓存中的脏缓冲区写入数据文件。

尽管重做日志组是循环使用的，但是日志序列号却是无限递增的。每次发生重做日志的切换时，数据库服务器为当前重做日志组指定一个日志序列号，这个序列号是在前一个序列号的基础上加一而得的。在对重做日志文件进行归档时，这个序列号被一起写入了归档日志文件。重做日志文件和归档日志文件就是通过重做日志序列号来唯一地标识的。在进行数据库的恢复时，将按照序列号递增的顺序依次查找所需的重做日志文件和归档日志文件。

9.2.7　重做日志信息的查询

与重做日志文件有关的动态性能视图有以下三个：

·v$log：记录重做日志文件组的信息。

·v$logfile：记录重做日志文件的信息。

·v$log_history：记录重做日志组的切换信息。

其中从动态性能视图v$log可以查询重做日志文件组的组号、序列号、大小、是否归档、状态等信息。例如，以下语句将获得每个重做日志组的成员数目、序列号和大小等信息：

SQL＞SELECT group#，sequence#，bytes/1024/1024 AS MB FROM v$log；

下面的查询语句将获得每个重做日志组是否归档、状态以及最小的SCN等信息：

SQL＞SELECT archived, status, first_change#FROM v$log；

表9.1列出了重做日志文件的各种状态以及它们的意义。

从动态性能视图v$logfile中可以获得每个重做日志组中的日志成员及其状态等信息。例如：

SQL＞SELECT group#，member, status FROM v$logfile；

其中日志成员的状态有以下几种：

·INVALID：文件不可访问。

·STALE：文件的内容不完整。

·DELETED：文件不在使用。

·空：正常。

9.3　归档日志的管理

对于实际的生产系统来说，重做日志文件的归档是非常重要的。归档日志管理主要包括修改数据库的日志模式、指定归档文件的位置和名称、手工对重做日志文件进行归档等内容。

9.3.1　数据库的日志模式

数据库服务器有两种日志模式：归档模式和非归档模式。在非归档模式下，当一个重做日志组被写满时，LGWR进程将自动切换到下一个重做日志组，这个重做日志组中以前的重做日志将被覆盖。而在归档模式下，每个重做日志组在被覆盖之前都要进行归档，生成一个归档日志文件。

重做日志文件的归档对于数据库的安全有着重要的意义：如果数据库被破坏，利用重做日志和归档日志可以进行完全恢复。如果没有对重做日志文件进行归档，那么只能进行不完全恢复。

现在举个例子来说明归档日志的作用。假设某数据库有三个重做日志组，在周一的时候数据库管理员对数据库做了一次完全备份，以后每天都做一次增量备份，到周四的时候数据库被破坏。数据库管理员首先可以利用最近的一次完全备份将数据库恢复到周一时的状态，然后依次利用后面的增量备份把数据库一直恢复到周三时的状态。

如果数据库处于非归档模式下，那么周三到周四的数据将无法恢复。如果数据库处于归档模式下，在这段时间只要发生过日志切换，就会产生归档日志文件，数据库管理员可以利用归档日志和联机重做日志将数据库恢复到发生故障之前的状态。由此可见，在进行数据的恢复时，最后一段时间没有被备份的数据完全是依靠重做日志来恢复的。

在默认情况下，数据库初始处于非归档模式下，数据库管理员可以将数据库在两种日志模式之间进行切换。如果要确定数据库当前所处的日志模式，可以执行命令ARCHIVE LOG LIST。以下是该命令执行的情况：

SQL＞ARCHIVE LOG LIST；

数据库日志模式　存档模式

自动存档　启用

存档终点/u01/base/archive

最早的概要日志序列　36

下一个存档日志序列　37

当前日志序列　37

以上结果表明数据库当前处于归档模式下，并且启动了自动归档功能。

数据库的日志模式还可以通过以下SELECT语句获得：

SQL＞SELECT log_mode FROM v$database；

9.3.2　切换日志模式

数据库服务器可以在归档模式和非归档模式之间进行切换。在切换日志模式时，需要将数据库服务器关闭，然后启动到MOUNT状态，在这种状态下切换日志模式，最后将数据库服务器启动到OPEN状态。例如，以下命令序列用于将数据库服务器从非归档模式切换到归档模式：

SQL＞SHUTDOWN；

SQL＞STARTUP MOUNT；

SQL＞ALTER DATABASE ARCHIVELOG；

SQL＞ALTER DATABASE OPEN；

以下命令序列用于将数据库服务器从归档模式切换到非归档模式：

SQL＞SHUTDOWN

SQL＞STARTUP MOUNT

SQL＞ALTER DATABASE NOARCHIVELOG；

SQL＞ALTER DATABASE OPEN；

在归档模式下，需要对每个重做日志组进行归档。Oracle有两种归档方式：手工归档和自动归档。在默认情况下，数据库服务器将启动若干个ARCH进程，对重做日志文件自动进行归档。通过下面的SELECT语句可以确定自动归档功能是否已经启动：

SQL＞SELECT archiver FROM v$instance；

如果数据库处于归档模式下，利用ARCH进程对重做日志文件进行归档，这是最佳的选择。然而也可以采用手工归档方式，即数据库管理员需要执行命令，对重做日志文件进行归档。如果希望采用手工归档方式，那么需要对归档方式进行设置。在数据库服务器处于MOUNT状态时，执行下面的命令：

SQL＞ALTER DATABASE ARCHIVELOG MANUAL；

然后把数据库服务器启动到OPEN状态。从这时候开始，对重做日志文件进行归档的操作全部要由数据库管理员通过命令完成。例如，以下语句对当前重做日志组进行归档：

SQL＞ALTER SYSTEM ARCHIVE LOG CURRENT；

在对当前重做日志组进行归档之前，数据库服务器首先进行一次日志切换，然后对切换前所使用的重做日志组进行归档。以下语句用于归档所有尚未归档的重做日志组：

SQL＞ALTER SYSTEM ARCHIVE LOG ALL；

手工归档的任务是由数据库管理员完成的，在这种情况下数据库管理员需要监视重做日志的使用情况和切换情况。如果某个重做日志组没有被及时归档，那么当数据库服务器再次切换到该重做日志组时，将因为等待它的归档而停止运行。在大多数情况下，对重做日志文件不应该进行手工归档，而应该进行自动归档。

在归档模式下，如果启动了自动归档功能，重做日志的归档是由后台进程ARCH自动完成的。在默认情况数据库服务器只启动2个ARCH进程。当重做日志组的切换非常频繁时，可能需要多个ARCH进程同时工作。初始化参数LOG_ARCHIVE_MAX_PROCESSES用于指定启动的ARCH进程数目。在一个数据库服务器中最多可以启动30个ARCH进程（ARC0—ARC29）。例如，以下设置将使数据库服务器启动5个ARCH进程：

SQL＞ALTER SYSTEM SET log_archive_max_processes=5；

对于一个重做日志组，最近所需要进行的归档是否已经进行，可以通过下面的语句查询：

SQL＞SELECT group#，archived FROM v$log；

如果一个重做日志组的归档已经完成，那么本次归档所产生的归档日志文件可以通过下面的语句查询：

SQL＞SELECT name FROM v$archived_log；

9.3.3　设置归档位置

归档日志文件存储在数据库服务器的某个特定位置。在Oracle中，归档日志文件的存储位置和文件名称是由初始化参数指定的。由于归档日志文件的特殊重要性，在数据库服务器中可以对每个重做日志组同时产生多个归档日志文件，这些归档日志文件的内容完全相同。需要注意的是，为了防止磁盘损坏，同时为了缩短归档的时间，应该将这些归档日志文件存放在不同的磁盘上。

在数据库中，可以通过LOG_ARCHIVE_DEST参数和LOG_ARCHIVE_DUPLEX_DEST参数指定两个归档位置。

假设对这两个参数进行了如下设置，那么数据库服务器在对每个重做日志组进行归档时，将同时产生两个归档日志文件。

SQL＞ALTER SYSTEM SET LOG_ARCHIVE_DEST="/home/oracle/archive1"；

SQL＞ALTER SYSTEM SET LOG_ARCHIVE_DUPLEX_DEST="/home/oracle/archive2"；

在Oracle 11g的数据库中，可以通过另外一组初始化参数设置LOG_ARCHIVE_DEST_n（n为1～31）指定最多31个存储位置，这样在对每组重做日志文件进行归档时，将同时产生多个内容完全相同的归档日志文件。需要注意的时，这组初始化参数和前一组不能同时使用。

通过初始化参数LOG_ARCHIVE_DEST_n既可以指定本地目录，也可以指定一个ASM磁盘组，还可以指定网络上另一台数据库服务器中。本地目录的指定格式为“LOCATION=本地目录”，ASM磁盘组的指定各式为“LOCATION=磁盘组名称”，网络上另一台数据库服务器的指定格式为“SERVICE=网络服务”。例如，通过以下初始化参数为归档日志文件指定了5个存储位置：

LOG_ARCHIVE_DEST_1="location=/home/oracle/archive1"

LOG_ARCHIVE_DEST_2="location=/home/oracle/archive2"

LOG_ARCHIVE_DEST_3="location=/home/oracle/archive3"

LOG_ARCHIVE_DEST_4="location=/home/oracle/archive4"

LOG_ARCHIVE_DEST_5="service=backup"

在初始化参数LOG_ARCHIVE_DEST_n中还可以通过关键字MANDATORY指定归档位置为强制归档，或者通过关键字OPTIONAL指定为可选归档。如果一个归档位置为强制归档，那么数据库服务器将在这个位置产生一个归档日志文件，如果这个归档位置不可用，那么数据库服务器将停止运行。如果是可选归档，那么当这个归档位置不可用时，数据库服务器将忽略它。

如果没有通过上述两组初始化参数指定任何一个归档位置，那么归档日志文件将被存储在目录ORACLE_HOME/rdbms目录中，其中ORACLE_HOME代表Oracle软件的安装目录。

初始化参数LOG_ARCHIVE_FORMAT的作用是指定归档日志文件的名称。归档日志文件的名称应该是可变的，并且永远不会重复。因为数据库服务器循环地为每个重做日志组进行归档，如果归档日志文件的名称固定不变，那么后产生的归档日志文件将覆盖先产生的。因此，应该有一种机制保证归档日志文件的名称是唯一的。

在设置初始化参数LOG_ARCHIVE_FORMAT时可以使用以下格式符：

·%T：代表归档线程号，并且可以在左边填充0，补足三位。

·%S：代表被归档的重做日志文件的日志序列号，可以在左边填充0，补足五位。

·%t：代表归档线程号。

·%s：代表被归档的重做日志文件的日志序列号。

重做线程指的是一个数据库实例的重做日志，在单机版数据库中，因为只有一个实例，所以只有一个重做线程。而在RAC环境中，则有多个重做线程。

我们注意到，在一个数据库服务器中，重做线程号是唯一的，而日志序列号是递增的，并且永远不会重复。因此可以把%T、%t与%S、%s的组合作为归档文件名称的一部分，这样可以保证文件名称是唯一的。例如，以下设置指定归档日志文件的名称为arc%t_%s.log：

SQL＞ALTER SYSTEM SET LOG_ARCHIVE_FORMAT=arc%t_%s.log；

根据这个设置，数据库服务器将依次产生以下归档日志文件：

arc1_1.log

arc1_2.log

arc1_3.log

……

如果没有设置初始化参数LOG_ARCHIVE_FORMAT，那么数据库服务器将把arc%t.%s作为归档日志的默认文件名称。

9.3.4　归档信息的查询

与归档日志有关的动态性能视图如下所示：

·v$database：记录数据库的日志模式。

·v$archived_log：记录已经产生的归档日志文件信息。

·v$archive_dest：记录归档日志文件的存储位置信息。

·v$archive_processes：记录归档进程（ARCH）的信息。

·v$backup_redolog：记录归档日志的备份信息。

例如，以下语句将得到各个归档文件存储位置的设置情况，包括参数名称、存储位置、状态、强制归档还是可选归档、本地归档还是远程归档等信息：

SQL＞SELECT dest_name, destination, status, binding, target

FROM v$archive_dest；

其中BINDING列有两个值，MANDATORY指强制归档，OPTIONAL指可选归档。

TARGET列有两个值，PRIMARY指本地归档，STANDBY指远程归档。

9.4　如何对重做日志进行分析

无论是重做日志文件，还是归档日志文件，都是以二进制的形式保存重做日志信息的。这种日志组织形式便于数据库服务器对其进行解读，但是不便于人工查看。

Oracle提供了一个实用工具LogMiner，数据库管理员可以利用这个实用工具对重做日志和归档日志进行分析。通过分析日志，把二进制的重做日志以文本的形式显示出来。通过这种方式，可以跟踪某个用户的DML或DDL操作，或者跟踪用户在某个特定的数据库对象上的操作，或者撤销用户在特定数据库对象上的操作。

在分析日志时，首先需要创建一个字典文件，用来保存数据库对象的信息，如对象名称和结构等，这些信息是从数据字典中得到的。在一个数据库中可能有许多的重做日志文件和归档日志文件，在分析日志时需要指定对哪些文件进行分析。在进行日志分析时，可以根据开始时间和终止时间来指定日志分析的范围，也可以根据开始SCN和终止SCN指定日志分析的范围。日志分析的结果保存在相关的动态性能视图中，数据库管理员可以通过查询这些动态性能视图来了解日志的信息。日志分析完后应该结束这次分析过程。

利用LogMiner实用工具分析日志的过程如下：

1）创建字典文件。

2）指定需要分析的重做日志文件和归档日志文件。

3）开始日志分析。

4）查看分析结果。

5）结束日志分析。

日志分析是利用一系列的程序包来完成的。LogMiner实用工具提供了以下两个包：

·DBMS_LOGMNR_D：用于解读数据字典信息，创建字典文件。

·DBMS_LOGMNR：用于对日志文件进行分析。

9.4.1　如何创建字典文件

字典文件用于保存数据库对象的结构、权限等信息，这些信息来自数据字典。字典文件可以是普通的文本文件，也可以是其他的重做日志文件，在默认情况采用文本文件。

字典文件的存储位置通过初始化参数utl_file_dir指定。在创建字典文件之前，需要对初始化参数utl_fille_dir进行设置。例如：

SQL＞ALTER SYSTEM SET utl_file_dir='/home/oracle/dict'SCOPE=spfile；

然后重新启动数据库服务器，使这个初始化参数起作用。

程序包DBMS_LOGMNR_D中的存储过程BUILD用于创建字典文件。在调用这个存储过程时通过参数指定字典文件的名称、存储位置和文件类型。例如：

SQL＞BEGIN

DBMS_LOGMNR_D.BUILD（dictionary_filename=＞'dict.ora'，

dictionary_location=＞'/home/oracle/dict'，

options=＞DBMS_LOGMNR_D.STORE_IN_FLAT_FILE）；

END；

其中第一个参数用于指定字典文件的名称，第二个参数用于指定字典文件的存储位置，它的值必须与初始化参数utl_file_dir的设置一致。第三个参数用于指定字典文件是文本文件还是重做日志文件，默认情况下是文本文件（STORE_IN_FLAT_FILE），如果要采用重做日志文件，则为这个参数的值为STORE_IN_REDO_LOGS，这两个参数值都是在DBMS_LOGMNR_D包中定义的常量。

9.4.2　如何创建分析列表

在进行日志分析之前，需要指定对哪些日志文件进行分析。LogMiner工具不仅可以分析重做日志文件，也可以分析归档日志文件。

利用程序包DBMS_LOGMNR中的存储过程ADD_LOGFILE可以指定需要分析的日志文件，这就是创建分析列表的过程。LogMiner工具可以一次对多个日志文件进行分析。例如，下面的语句用于创建一个新的分析列表，并添加第一个日志文件：

SQL＞BEGIN

dbms_logmnr.add_logfile（LogFileName=＞'/home/oracle/orcl/redo11.log'，

options=＞DBMS_LOGMNR.NEW）；

END；

在向分析列表中添加第一个日志文件时，options的参数值为NEW。如果要分析多个日志文件，需要多次调用add_logfile存储过程，并将options的参数值指定为ADDFILE，将其他日志文件追加到分析列表中。这两个参数值都是在DBMS_LOGMNR包中定义的常量。例如，下面的语句用于向分析列表中追加其他的日志文件：

SQL＞BEGIN

dbms_logmnr.add_logfile（LogFileName=＞'/home/oracle/orcl/redo21.log'，

options=＞DBMS_LOGMNR.ADDFILE）；

END；

在进行日志分析开始之前，还可以将一个日志文件从分析列表中删除。Oracle 11g提供了一个remove_logfile存储过程，用于从分析列表中删除一个日志文件。在此之前，删除一个日志文件的方法仍然是调用add_logfile过程，只不过options参数需要指定为removefile。下面的语句用于把一个日志文件从分析列表中删除。

SQL＞BEGIN

dbms_logmnr.remove_logfile（

LogFileName=＞'/home/oracle/orcl/redo21.log'）；

END；

9.4.3　如何开始日志分析

在创建了分析列表，并向其中添加了需要分析的日志文件后，就可以启动日志分析的过程了。启动日志分析的方法是调用程序包dbms_logmnr中的过程start_logmnr。这个存储过程有若干个参数，首先需要通过DictFileName指定字典文件的存储位置和名称。然后可以通过可选参数starttime和endtime指定日志分析的时间范围，也可以通过可选参数sndscn和endscn指定日志分析的SCN范围。如果没有指定后面的四个可选参数，LogMiner工具将对分析列表中所有的日志文件进行分析。

例如，下面的语句将用于分析从2010年5月1日到2010年5月10日之间产生的日志：

SQL＞BEGIN

dbms_logmnr.start_logmnr（DictFileName=＞'/home/oracle/dict/dict.ora'，

startTime=＞TO_DATE（'2010-05-01'，'YYYY-MM-DD'），

endTime=＞TO_DATE（'2010-05-10'，'YYYY-MM-DD'））；

END；

9.4.4　如何查看日志分析结果

当执行start_logmnr存储过程对日志文件进行分析后，就可以查看分析结果了。日志分析的结果存放在动态性能视图v$logmnr_contents中，通过查询这个视图，可以获得用户所执行的DML或DDL语句的相关信息。这个动态性能视图的结构如表9.2所示。

例如，下面的语句用于查询用户在emp表上执行的DDL或DML语句、用户名、执行时间以及用于取消这次操作所对应的SQL语句：

SQL＞SELECT sql_redo, username, timestamp, sql_undo

FROM v$logmnr_contents

WHERE table_name='EMP'；

通过查询动态性能视图v$logmnr_contents，可以对用户访问数据库对象的情况进行统计分析，例如统计某个用户频繁访问的表，或者频繁访问某个表的用户等。还可以了解用户执行的某个误操作执行的精确时间，利用基于时间点的恢复方法或者Flashback技术可以把数据库恢复到这个误操作之前的时刻。

在对日志进行分析时，所指定的相关信息都存储在其他动态性能视图中。例如，从动态性能视图v$logmnr_logs中可以获得分析列表中的日志文件；从动态性能视图v$logmnr_dictionary中可以获得字典文件的存储位置和名称；而从动态性能视图v$logmnr_parameters中可以获得分析日志时所使用的参数，如起始时间和终止时间、起始SCN和终止SCN等信息。例如，通过以下语句将得到被分析的日志文件：

SQL＞SELECT filename

FROM v$logmnr_logs；

需要注意的是，分析日志的结果以及所指定的信息都存储在SGA中，用户只能通过上述动态性能视图获得这些信息。这些信息都是临时的，只要当前会话结束，或者本次日志分析结束，这些信息将消失。如果再要获得这些信息，需要对日志文件重新进行分析。为了永久保存这些信息，可以在查询这些信息时通过SQL*Plus中的SPOOL命令将查询结果输出到一个操作系统文件中。

9.4.5　如何结束日志分析

当获得日志分析的结果后，就可以调用程序包DBM S_LOGMNR中的存储过程END_LOGMNR来结束本次日志分析了。这个过程的调用格式如下：

SQL＞EXEC DBMS_LOGMNR.END_LOGMNR；

第10章　基本数据库对象管理

无论是数据库管理员，还是普通用户，都需要经常对数据库对象进行管理，如数据库对象的创建、删除、修改等。Oracle中的数据库对象包括表、索引、视图、存储程序、序列等，这些数据库对象以一种逻辑关系组织在一起，这就是模式（schema）。

模式是一个用户所拥有的所有数据库对象的集合。每个数据库对象都属于某个用户，一个用户所拥有的数据库对象就组成了一个模式，模式的名称与用户名相同。当创建用户时，就同时产生了一个模式，在默认的情况下，用户在自己的模式中有所有的权限。

在第1章中，我们已经详细介绍了基本数据库对象的用法，只是当时是站在普通用户的角度，以一种通用的方式介绍它们的用法的。在本章中，我们将站在数据库管理员的角度，重新考虑这些数据库对象在Oracle中所涉及的特性，如存储结构、数据的组织方式等。

10.1　表的管理

表的管理涉及表的结构、表的创建、修改与删除等操作，以及临时表、分区表和索引组织表三种特殊类型的表。

10.1.1　表的结构

在数据库中，表是最基本的数据库对象，用来存储系统或用户的数据。表中的数据是按照行和列的格式存放的。表中的各行数据一般以写入的先后顺序存放，而一行中的各列一般按照定义表时指定的顺序存放的。

在逻辑结构上，一个表位于某个表空间。当创建一个表时，将同时创建一个表段，用于存放表中的数据。在物理结构上，表中的数据都存放在数据块中，因而在数据块中存放的是一行行的数据。图10.1为数据块中一行数据的结构。

图　10.1　行的结构

其中行的头部记录了该行中列的个数、行间的全连接、加锁信息等。列长度记录一个列实际占用的字节数，而列值则记录了该列实际存放的数据。

表中的每一行数据都有一个行号，用于标识该行数据的物理位置。根据这个行号，可以直接定位该行数据。行号可以通过伪列ROWID获得。例如，以下查询得到表DEPT中的数据及每行的行号。

SQL＞SELECT ROWID, DEPTNO, DNAME, LOC FROM DEPT；

ROWID DEPTNO DNAME LOC

-------------------------------------------------------

AAABhdAABAAAFuKAAA 10 ACCOUNTING NEW YORK

AAABhdAABAAAFuKAAB 20 RESEARCH DALLAS

AAABhdAABAAAFuKAAC 30 SALES CHICAGO

AAABhdAABAAAFuKAAD 40 OPERATIONS BOSTON

行号是由数据库服务器自动生成的字符串，包含18个字符。行号的组成如图10.2所示。

图　10.2　行号的组成

其中前六个字符表示数据库对象的编号，用来指定该行数据属于哪个数据库对象。在数据库中每个数据库对象都有一个唯一的编号。

从第七个到第九个共三个字符表示数据文件的相对编号，用来指定该行数据存储在哪个数据文件中。在数据库中每个数据文件中有两个编号，一个是绝对编号，它是数据文件在整个数据库范围内的编号，另一个是相对编号，它是数据文件在一个表空间范围内的编号。

从第十到第十五共六个字符表示数据块的编号，用来指定该行数据位于哪个数据块中。

最后三个字符表示行号，用来指定该行数据在数据块中位于第几行。为了使用户对行号的进行解析，Oracle提供了一个DBMS_ROWID程序包，利用这个程序包中的函数可以对行号进行分析。DBMS_ROWID中各个函数的用法如表10.1所示。

例如，以下查询将得到dept表中每行数据所在的文件编号、数据库对象的编号、数据块编号和在数据块中的行号：

SQL＞SELECT DBMS_ROWID.ROWID_RELATIVE_FNO（rowid）AS相对文件号，

DBMS_ROWID.ROWID_OBJECT（rowid）AS对象编号，

DBMS_ROWID.ROWID_BLOCK_NUMBER（rowid）AS数据块编号，

DBMS_ROWID.ROWID_ROW_NUMBER（rowid）AS行号

FROM dept；

10.1.2　表的创建

在创建表时，可以同时为表指定一些重要的属性，如存储参数、所属表空间等。这些属性都通过CREATE TABLE命令的子句指定。

1.PCTFREE和PCTUSED子句

这两个参数的作用是用来控制数据块的空间使用情况。为了减少数据块间的迁移，在创建表时可以通过PCTFREE和PCTUSED子句指定数据块空间的使用情况。考虑以下创建表的语句：

SQL＞CREATE TABLE T1（

name varchar2（10））

PCTFREE 20

PCTUSED 40；

在表T1中，每个数据块都有20%的保留空间。当可用空间使用完后，新的数据将被写入另外一个数据块。当从表中删除数据时，数据块中已用空间不断减少，当减少到40%时，可再次向该数据块中插入数据。

在使用PCTFREE和PCTUSED子句时，可以参考以下原则：

·PCTFREE和PCTFUSED的值必须小于或等于100%。

·如果在一个表上很少执行UPDATE操作，可以将PCTFREE设置得尽量小。

·PCTFREE与PCTUSED之和越接近100%，数据块的空间利用率越高。

2.TABLESPACE子句

TABLESPACE子句用来指定将表创建在哪个表空间上。如果不指定TABLESPACE子句，用户将在自己的默认表空间上创建表。

为了能够在指定的表空间上创建表，当前用户必须在该表空间上有足够的空间配额或在数据库中具有UNLIMITED TABLESPACE权限。

3.INITRANS和MAXTRANS子句

数据库中的数据存储在数据块中，用户的事务最终要修改数据块中的数据。Oracle允许多个并发的事务同时修改一个数据块中的数据。每当用户的事务开始作用于一个数据块时，数据库服务器将在该数据块的头部为该事务分配一个事务项，以记录事务的相关信息。事务结束时，对应的事务项将被删除。

INITRANS和MAXTRANS参数用于控制一个数据块上的并发事务数量，其中INITRANS用于指定初始的事务数量。MAXTRANS用于指定最大的并发事务数量。

当创建一个表时，数据库服务器按照INITRANS的值为每个数据块分配一定的事务项，这些事务项将一直保留到该表被删除。当一个事务访问数据块时，将占用其中的一个事务项，事务结束时，将释放事务项。当这些预先创建的事务项全部被占用后，如果又有新的并发事务发生，数据库服务器将在数据块的可用空间中为事务创建一个新的事务项。在任一时刻，数据块中的事务项不会超过MAXTRANS参数值。

例如，在利用以下语句创建表时，指定初始的事务项为10，最大的并发事务数量为200。

SQL＞CREATE TABLE T3（

name varchar2（10）

）

INITRANS 10

MAXTRANS 200；

INITRANS和MAXTRANS参数的值可以根据用户对表的访问情况进行设置。如果参数值过大，事务项将占用更多的数据块空间，那么数据可以利用的空间将减少。如果参数设置过小，有些事务将因为无法分配到事务项而等待，从而降低了数据库的性能。一般情况下，如果多个用户同时访问表的情况很少发生，可以为这两个参数设置较小的参数值，反之要为这两个参数指定较大的参数值。

4.CACHE子句

CACHE子句用于指定将表中的数量放在数据库高速缓存中，并保留一段时间。如果在创建表时指定了CACHE字句，那么在用户第一次访问表中的数据时，这个表将整个被读到数据库高速缓存中，并保留较长的一段时间，这样用户以后再访问该表时，可直接访问数据库高速缓存中的数据，从而提高访问的效率。

在默认情况下创建表时使用NOCACHE子句。对于一些较小的、用户访问频繁的表，在创建时可以考虑使用CACHE子句，以提高访问效率。

5.PARALLEL子句

在一般情况下，通过INSERT命令向表中写入数据时，一次写入一行数据，这样的写操作是串行进行的。如果在创建表时指定了PARALLEL子句，那么在向表中以批量方式写入大量数据时就是以并发方式进行的，这样可以大大提高处理的速度。例如，利用以下语句创建表时，将实现并发操作。

SQL＞CREATE TABLE T4

PARALLEL

AS SELECT*FROM emp；

如果不希望在表上以并发方式写入数据，在创建表时需要指定NOPARALLEL。

6.LOGGLING子句

在默认情况下，用户在表上执行DDL和DML命令时，服务器进程都会产生重做日志。

如果不希望产生重做日志，在创建表时需要指定NOLOGGING子句。使用NOLOGGING子句有以下好处：

·由于不写重做日志，因而节约了重做日志文件的存储空间。

·减少了处理时间。

·在以并行方式向表中写入大量数据时提高了效率。

当然在使用NOLOGGING子句时也有不好的一面。因为没有重做日志，当表被破坏时，将无法进行恢复，所以在表创建后应该及时对其进行备份。

7.COMPRESS子句

如果在创建表时使用了COMPRESS子句，那么一个数据块中两行完全相同的数据将被压缩为一行，并存储在数据块的开始，在数据块中本应存储这两行数据的地方只存储该行数据的引用。

使用表的压缩功能可以减少表所占用的存储空间和数据库高速缓存空间，并且可以提高查询速度。

表的压缩功能一般用在向表中批量插入数据的情况（例如基于查询创建表）。一个表中可以包含压缩的和未压缩的数据，所有DML操作均可应用于这些压缩的数据。

例如，下面的语句用于创建表T5，这个表具有压缩功能，支持并发的数据写入，对DDL和DML命令不产生重做日志。

SQL＞CREATE TABLE T5

PARALLEL

NOLOGGING

COMPRESS

AS SELECT*FROM EMP；

临时表是一种特殊类型的表，表中的数据并不永久保存，而是一些临时数据。这些临时数据只在当前事务或当前会话中有效，当事务或会话结束时，这些临时数据将被全部删除。

创建临时表的命令是CREATE GLOBAL TABLE。在创建临时表时还需要通过ON COMMIT子句指定临时数据的有效范围。如果指定了ON COMMIT DELETE ROWS子句，那么临时表是事务级的，当事务提交或回滚时，临时表中的数据即被删除。如果指定了ON COMMIT PRESERVE ROWS子句，那么临时表是会话级的，表中的数据将一直保留，直到当前会话结束时才被删除。

以下语句用于创建一个事务级的临时表：

SQL＞CREATE GLOBAL TEMPORARY TABLE T6（

name varchar2（10）

）

ON COMMIT DELETE ROWS；

10.1.3　表的修改

在数据库服务器的运行过程中，如果发现表的设计不合理，可以对其进行修改。一般来说，对表的修改涉及以下内容：

·表的结构的修改，如增加列、删除列、修改某个列的定义。

·约束的修改，如添加约束、删除约束、激活约束与禁止约束。

·修改表的物理属性，如PCTFREE参数、PCTUSED参数。

·表的移动，如移动到一个新的数据段或表空间。

·表的存储空间的手工分配和回收。

其中前两项内容在第1章中已经进行了详细的描述，本节仅对其他内容进行描述。

修改表的命令是ALTER TABLE，普通用户只能修改自己的表。如果要修改其他用户的表，必须具有ALTER ANY TABLE系统权限。

1.修改表的物理属性

在创建表时可以指定PCTFREE、PCTUSED、INITRANS、CACHE等参数，这些参数对于表中存储空间的利用有直接的影响。表在创建以后，用户也可以对这些参数进行修改。例如，下面的语句对表T5的参数进行了修改：

SQL＞ALTER TABLE T5

PCTFREE 30 PCTUSED 60

INITRANS 15 MAXTRANS 220

CACHE

NOLOGGING；

2.表的移动

表的移动意味着把表中的数据移动到一新的表段中，同时可以把表段移动到另外一个表空间中。表的移动在以下场合非常有用：

·消除表中的存储碎片。

·消除表中数据块间的链接。

·把表移动到另外一个表空间中。

·修改表所使用的数据块大小。

在对表进行移动时，表中的数据将被重新排列，这样就可以消除表中的存储碎片和数据块的链接。如果两个表空间所使用的数据块大小不同，那么表在两个表空间中移动时，也将使用不同大小的数据块。

移动表所使用的命令是ALTER TABLE。在移动表时，先为表创建一个新的表段，然后把表中的数据移动到这个新段中，最后删除原来的表段。例如：

SQL＞ALTER TABLE T2

MOVE；

又如，下面的ALTER语句用于把表T2移动到表空间TOOLS中：

SQL＞ALTER TABLE T2

MOVE

TABLESPACE TOOLS；

需要注意的是，表被移动后，表的行号将发生变化，所以表上原来的索引将不可用。在表移动后应该删除原来的索引并重新创建。

3.存储空间的手工分配和回收

表在创建后，随着数据的增加，Oracle将按照存储参数的设置不断为表分配新的区。这一过程是自动进行的，不需要用户的干预。

在有些情况下，用户希望为表分配一个指定大小的区，这时需要利用ALTER TABLE命令及其ALLOCATE EXTENTS子句为表手工分配一个区。

在手工为表分配区时，可以为其指定大小。如果没有指定大小，数据库服务器将按照该表所在表空间的区大小，为表分配一个区。例如，以下语句为表T2手工分配了一个512KB的区：

SQL＞ALTER TABLE T2

ALLOCATE EXTENT（SIZE 512K）；

实际上，手工指定的区大小与该表所在表空间的区大小可能不一致。假设表空间的区大小是64KB，那么上述命令的执行结果是为表T2分配了8个64KB的区。

在表段的HWM以下，可能有一些尚未使用的数据块。为了节省磁盘空间，可以通过命令把这些存储空间回收。例如：

SQL＞ALTER TABLE T2 DEALLOCATE UNUSED；

10.1.4　表的删除

当一个表不需要时，可以将其删除。删除表时，将产生以下结果：

·表的结构信息从数据字典中被删除，表中的数据不可访问。

·表上的所有索引和触发器被一起删除。

·所有建立在该表上的同义词、视图和存储程序变为无效。

·所有分配给表的区被标记为空闲，可被分配给其的对数据库对象。

一般情况下，普通用户只能删除自己的表。若希望删除其他用户的表，则必须具有DROP ANY TABLE系统权限。为了防止用户对表进行误删除，在数据库中提供了一个回收站。当表被删除时，表所占用的存储空间并不是立即被释放，而是被放进了回收站。

回收站实际上是一个数据字典表，用于记录被删除的表、索引等数据库对象的信息。当一个数据库对象被删除时，它所占用的存储空间并不立即释放，而是被重命名后放进了回收站。如果后来用户发现某个对象是被误删除的，可以从回收站中将其恢复。

当用户删除一个表空间时，表空间中的数据库对象并不被放入回收站，而且回收站中原来属于该表空间的数据库对象也将被清除。当一个用户被删除时，属于这个用户的数据库对象也不被放入回收站，而且回收站中原来属于该用户的数据库对象也将被清除。

用户可以在回收站中查看属于自己的、被删除的数据库对象，数据库管理员可以查看所有被删除的数据库对象。用户可以通过以下的语句查看回收站中的内容：

SQL＞SELECT*FROM RECYCLEBIN；

数据库对象被删除后，它将被重命名并放入回收站。重命名的目的是为了防止被删除对象的名称相互冲突。重命名的规则为：

BIN$id$版本

其中id是由oracle产生的包含26个字符的字符串，是被删除数据库对象的唯一标识。版本是由数据库服务器自动指定的版本号。

为了查看回收站的方便，Oracle数据库提供了两个数据字典视图：

USER_RECYCLEBIN：包含当前用户的被删除的数据库对象，RECYCLEBIN是它的同义词。

DBA_RECYCLEBIN：包含所有被删除的数据库对象，仅数据库管理员可以访问。

例如，以下语句用于查询SCOTT用户被删除的数据库对象：

SQL＞SELECT object_name, original_name FROM DBA_RECYCLEBIN

WHERE owner='SCOTT'；

OBJECT_NAME ORIGINAL_NAME

BIN$yrMKlZaLMhfgNAgAIMenRA==$0 T2

用户也可以通过执行SQL*Plus命令show recyclebin来查看回收站。例如：

SQL＞show recyclebin

ORIGINAL NAME RECYCLEBIN NAME OBJECT TYPE DROP TIME

-----------------------------------------------------------------------------

T2 BIN$yrMKlZaVMhfgNAgAIMenRA==$0 TABLE 2010-03-30：09：51：05

尽管表已经被放入回收站，用户还是可以访问表中的数据，只是表名必须使用它在回收站中的名称。例如：

SQL＞SELECT*FROM"BIN$yrMKlZaVMhfgNAgAIMenRA==$0"；

当用户确信一个数据库对象不再需要时，可以执行PURGE命令将其从回收站中清除，该对象及其相关对象所占用的存储空间将一起被释放。清除数据库对象时，可以使用回收站中的名称，也可以使用被删除前的名称。例如，以下两条语句的作用都是从回收站中清除表T2：

SQL＞PURGE TABLE BIN$yrMKlZaVMhfgNAgAIMenRA==$0；

SQL＞PURGE TABLE T2；

如果希望在删除表的同时释放存储空间，可以在DROP TABLE命令中使用PURGE子句，这个表就直接被删除了，而不是被放到回收站中。例如：

SQL＞DROP TABLE T1 PURGE；

用户还可以选择清除回收站中原来属于某个表空间的所有数据库对象，或者清除某个用户的原来属于某个表空间的所有数据库对象，还可以清除属于自己的数据库对象。这三种操作对应的命令格式分别为：

SQL＞PURGE TABLESPACE USERS；

SQL＞PURGE TABLESPACE USERS USER SCOTT；

SQL＞PURGE RECYCLEBIN；

对于第三种用法，数据库管理员可用来清除回收站中的所有内容，只是要将RECYCLEBIN替换为DBA_RECYCLEBIN。

如果一个数据库对象被删除了，那么在被从回收站中清除之前，可以通过执行FLASHBACK命令将其恢复，并可通过RENAME子句为其指定一个新的名称。例如：

SQL＞FLASHBACK TABLE T2 TO BEFORE DROP

RENAME TO TT2；

以上语句把表T2从回收站中恢复到被删除前的状态，并将其重命名为TT2。如果没有通过RENAME子句为它指定名称，它将使用原来的名称。

10.2　分区表的管理

随着数据库系统的运行，数据库中存储的数据越来越多。在现代企业的数据库中，许多表的存储空间可达几百个GB，甚至几个TB。对于这样的大型表如果执行全表查询或者DML操作时，效率是非常低的。

为了提高大型表的访问效率，Oracle提供了一种分区技术，利用这种技术可以把表、索引等数据库对象中的数据分割成小的单位，分别存放在一个个单独的段中，用户对表的访问便转化为对相对较小段的访问。

10.2.1　分区的概念

分区是指将表、索引等数据库对象划分为较小的可管理片段的技术，每个片断称为一个分区或子分区。每个分区存储在一个单独的段中，可分别进行管理。这些分区具有相同的逻辑结构，比如，一个分区表中的所有分区与表有相同的列定义和约束定义。

一个表被分区后，对表的查询操作可以局限于某个分区进行，而不是整个表，这样可以大大提高查询速度。例如，通信公司将用户通话信息记录在一个表中，在这个表中一年产生40GB的数据。假设要对用户的通话信息按照季度进行统计，那么这样的统计需要在全表范围内进行。如果对该表按季度进行分区，那么每个分区的大小平均为10GB左右，这样在进行统计时，只需要在10GB范围内进行。当在表上进行并行DML操作时，可以在所有分区上同时进行，同样可以大大减少处理时间。

尽管一个表的所有分区具有相同的结构，但是它们被单独存储在一个段中，这些段可以位于同一个表空间中，也可以位于不同的表空间中。将这些分区放在不同的表空间上具有以下的好处：

·减少了所有数据都损坏的可能性。

·可以针对每个分区单独进行备份和恢复。

·可以将同一个表中的数据分布在不同的磁盘上，从而均衡磁盘上的I/O操作。

·提高了表的可管理型、可利用性和访问效率。

在创建分区表时，以表中某个列或多个列的组合为依据，创建多个分区。表中的数据将按照分区列上数据的不同，分布在不同的分区中。目前Oracle支持的分区方法有以下几种：

·范围分区

·列表分区

·散列分区

·范围—散列分区

·范围—列表分区

10.2.2　范围分区

范围分区的方法是按照某个列或几个列的值的范围来创建分区，当用户向表中写入数据时，数据库服务器将按照这些列上的数据的大小，将数据写入相应的分区。

在创建范围分区时，首先要指定按照哪些列进行分区，然后要为每个分区指定数据范围。范围分区的原则是：数据应尽可能均匀地分布在各个分区中，如果做不到这一点，应该考虑使用其他类型的分区。

例如，以下语句创建一个分区表call，用来记录用户的电话通话信息，包括主叫、被叫、通话的年、月、日及时长，并且根据月进行分区。

SQL＞CREATE TABLE call（

caller char（15），

callee char（15），

year number（4），

month number（2），

day number（2），

duration number（4））

PARTITION BY RANGE（month）

（PARTITION P1 VALUES LESS THAN（4）TABLESPACE ts1，

PARTITION P2 VALUES LESS THAN（7）TABLESPACE ts2，

PARTITION P3 VALUES LESS THAN（10）TABLESPACE ts3，

PARTITION P4 VALUES LESS THAN（13）TABLESPACE ts4

）；

需要说明的是，在上述例子中创建的表可能不符合常规，这仅仅是为了说明分区表的创建方法，因为在一般的表中，年、月、日这样的列是合在一起，通过一个列实现的。

在创建分区表时，首先通过“PARTITION BY RANGE”子句指定分区的类型为范围分区，然后在这个子句之后的小括号中指定一个或多个列，作为分区的依据。

表中的每个分区都可以通过“PARTITION”子句指定一个名称，如果没有指定，数据库服务器将自动为其指定一个名称。每个分区都有一个范围，通过“VALUE LESS THAN”子句可以为分区指定上界，而它的下界是前一个分区的上界。对于最后一个分区，它的上界可以用“MAXVALUE”来代替。

当在分区表中执行DML操作时，实际上是在各个分区上透明地修改数据。当执行SELECT命令时，可以指定查询哪个分区上的数据，如果不指定，则查询整个表中的数据。例如：

SQL＞SELECT*FROM call PARTITION（P1）；

10.2.3　列表分区

范围分区是按照某个列上的数据范围进行分区的。如果某个列上的数据无法通过划分范围的方法进行分区，并且该列上的数据是相对固定的值，可以考虑使用列表分区。一般说来，对于数字型或者日期型的数据，适合采用范围分区的方法。而对于字符串型数据，则适合采用列表分区方法。

例如创建一个产品销售记录表sales，记录产品的销售情况。由于产品只在几个固定的城市销售，所以可以按照销售城市对该表进行分区。

SQL＞CREATE TABLE sales（

sales_id number（6），

year number（4），

month number（2），

day number（2），

salesman char（8），

city char（10）

）

PARTITION BY LIST（CITY）

（PARTITION P1 VALUES（'北京'，'上海'），

PARTITION P2 VALUES（'天津'，'广州'），

PARTITION P3 VALUES（'沈阳'，'武汉'），

PARTITION P4 VALUES（'西安'，'成都'）

）；

在创建列表分区时，通过PARTITION BY LIST子句指定对表进行列表分区，然后通过PARTITION子句定义多个分区，在每个分区中分区列的取值通过VALUES子句指定。当用户向表中插入数据时，只要分区列的数据与VALUES子句指定的数据之一相等，该行数据便被写入对应的分区中。

10.2.4　散列分区

在很多情况下，用户无法预测某个列上数据的变化范围，因而无法事先创建固定数量的范围分区或列表分区，使用户的数据按照分区列上的数据分布在相应的分区中。

在这种情况下，可以创建散列分区。当用户向表中写入数据时，数据库服务器将根据一个散列函数对数据进行计算，把数据均匀地分布在各个分区中。在散列分区中，用户无法预测数据将被写入哪个分区。

现在我们重新考虑产品销售表的例子。如果销售城市不是相对固定的，而是遍布全国各地，这时很难对该表进行列表分区。如果为该表进行散列分区，可以很好地解决这个问题。

SQL＞CREATE TABLE sales（

sales_id number（6），

year number（4），

month number（2），

day number（2），

salesman char（8），

city char（10））

PARTITION BY HASH（city）

（PARTITION P1，

PARTITION P2，

PARTITION P3，

PARTITION P4

）；

10.2.5　复合分区

复合分区是指先对表进行范围分区，然后对每个分区再进行散列分区或列表分区，产生若干子分区。根据子分区的划分方法不同，复合分区可分为范围分区-散列分区和范围分区-列表分区。

对表进行复合分区后，分区仅仅是逻辑上的概念，只有子分区才是物理上的对象。每个子分区对应一个段，它们可分别位于不同的表空间中，但是同一个分区的所有子分区具有相同的存储参数。

现在仍以产品销售表为例来说明复合分区的用法。如果销售数据很多，并且销售城市遍布全国各地，那么我们可以考虑先按照销售的月份对表进行范围分区，使销售数据按季度分布在四个范围分区中，然后对每个分区再进行散列分区，划分若干子分区，使一个季度的销售数据再按照销售城市的不同而均匀分布在各个子分区中。以下是创建这个分区表的语句：

SQL＞CREATE TABLE sales（

sales_id number（6），

year number（4），

month number（2），

day number（2），

salesman char（8），

city char（10））

PARTITION BY RANGE（month）

SUBPARTITION BY HASH（city）

（PARTITION P1 VALUES LESS THAN（4）

（SUBPARTITION P11，

SUBPARTITION P12，

SUBPARTITION P13

），

PARTITION P2 VALUES LESS THAN（7）

（SUBPARTITION P21，

SUBPARTITION P22，

SUBPARTITION P23

），

PARTITION P3 VALUES LESS THAN（10）

（SUBPARTITION P31，

SUBPARTITION P32，

SUBPARTITION P33

），

PARTITION P4 VALUES LESS THAN（13）

（SUBPARTITION P41，

SUBPARTITION P42，

SUBPARTITION P43

））；

在上述例子中，先按照月份对表进行范围分区，定义了四个分区，然后对每个分区再按照城市进行散列分区，产生3个子分区，这样一共产生了12个子分区。定义子分区的子句是SUBPARTITION。

对于每个分区，我们可以为其指定存储参数，该分区中的所有子分区都使用同样的存储参数。对于每个子分区，我们可以用TABLESPACE子句为其指定所属的表空间，使这些子分区分别位于不同的表空间中。

总之，创建分区表的目的是把数据的查询或统计限制在一定范围之内，以减少磁盘I/O。在这些表上执行DML命令时，和普通表没有什么区别，只不过数据将按照分区的条件被写入到不同的段中。

10.3　索引的管理

索引的管理主要包括索引的创建、删除、修改以及索引的重新组织等操作。本节主要介绍几种特殊的索引，如反向索引、位图索引等，这些索引虽然并不常用，但是在特殊的场合能起到非常重要的作用。

10.3.1　索引概述

索引是一种数据库对象，它建立在表的一个或多个列上，目的是为了提高该表上的查询速度。索引有两种创建方式，一种方式是在表上指定主键约束或唯一性约束时自动创建，另一种方式是通过命令手工创建。这里我们主要介绍后一种方式。

索引虽然可以提高表的查询速度，但是如果在表上执行DML操作，索引中的数据可能需要重新排序，从而降低数据库的性能。因此，如果在表上主要执行DML操作，而不是查询操作，那么应该考虑减少甚至不创建索引。如果要通过SQL*Loader或者import工具向表中插入大量的数据，那么可以考虑在这个操作完成之后再创建索引。

索引的形式很多，按照数据的组织形式不同，可以把索引分为B*树索引、反向索引、位图索引、基于函数的索引和分区索引等多种形式。其中B*树索引是最常用的索引形式，这部分内容已经在第1章中进行了详细的描述，在本章中主要介绍其他几种形式的索引。

索引是一种数据库对象，它虽然建立在某个表之上，但一般情况下它被单独存放在一个索引段中，因此我们在创建索引时可以为索引段指定物理属性和存储参数等信息。例如，下面的语句用于在表dept的dname列上创建一个索引。

SQL＞CREATE INDEX dept_idx1 ON dept（dname）

TABLESPACE users

PCTFREE 20

INITRANS 5 MAXTRANS 10

NOLOGGING

PARALLEL；

在CREATE INDEX命令中所使用的子句与创建表时使用的子句意义基本相同。需要注意的是，在创建索引时不能使用PCTUSED和CACHE子句。

10.3.2　反向索引

B*树索引是最经常用的索引形式。在一个表中，索引列上的数据越随机，就越能体现B*树索引的优越性。然而如果表中某个列上的数据已经有序，或者基本有序，那么在这个列上建立B*树索引就没有什么意义了。在这种情况下，如果按照该列上相反顺序的值建立索引，那么可以降低索引的层次，从而达到创建索引的目的。

实际上索引是一种特殊形式的B*树索引，只不过是把索引列上的值按照相反的顺序存储在索引中，从而把该列上数据的有规律分布转换为无规律的分布，然后按照转换后的数据创建一个B*树索引。图10.3表示反向索引的构成。

如果表中一个列上的值已经有序，或者基本有序，那么在该列上建立索引时，应该选择反向索引。创建反向索引的方法与创建B*树索引的方法类似，只是需要使用REVERSE关键字进行区别。例如，假设要在表emp的empno列上创建反向索引，可以执行下面的语句：

SQL＞CREATE INDEX emp_idx1 ON emp（empno）REVERSE；

需要注意的是，反向索引并不像B*树索引那样可以直接起作用。为了使用反向索引，首先要对表进行分析，收集统计它的数据。例如，为了用反向索引emp_idx，应该执行以下语句对表emp进行分析：

SQL＞ANALYZE TABLE emp COMPUTE STATISTICS；

图　10.3　反向索引

10.3.3　位图索引

如果在一个表中某个列上的重复值很多，那么在该列上创建B*树索引或者反向索引都是不合适的。例如，职工的性别只有“男”和“女”两个值，如果在该列上创建B*树索引，那么在根据性别对该表进行查询时，大约要对表中50%的数据进行扫描，这显然失去了索引的意义。

如果一个表中某个列上的重复值很多，那么适合在该列上创建位图索引。在位图索引中，为索引列上每个不同的值分配一个位图，这个不同的值称为键值。表中的每行数据在位图中对应一个二进制位。如果该行中索引列的值与键值相同，那么对应二进制位1，否则对应二进制位0。

例如，在表emp中，员工分布在少数的几个部门中，所有列deptno上的重复值很多。当在该列上创建位图索引时，每个不同的部门号将对应一个位图。位图结构如图10.4所示。

图　10.4　位图索引

从图10.4中可以看出，表emp的deptno列上共有三个不同的值，所以对应三个位图。其中第7、第8行中deptno列的值为10，所以在键值为10的位图中这些行对应二进制位1。第1、第4、第11行中deptno列的值为20，所以在键值为20的位图中这些行对应二进制位1。

如果要在表emp的deptno列上创建位图索引，可以执行下面的语句：

SQL＞CREATE BITMAP INDEX dept_idx2 ON emp（deptno）；

使用位图索引有两个好处，一是可以减少索引所占用的磁盘空间，表中的每行数据在位图索引的每个位图中只占用一个二进制位，而位图的数量取决于表中索引列上不重复值的多少。显然不重复的值越少，重复值越多，那么位图的数量就越少，这样可以大大节省索引所占用的磁盘空间。二是可以加快查询速度，如果要根据索引列对表进行查询，比如要查询部门10中的员工，那么首先在位图索引中查找键值为10的位图，然后在这个位图中查找所有与二进制位1对应的行，从而在表中获得满足条件的数据。

如果一个表比较大，那么创建位图索引可能需要较长的时间。为了加快创建索引的速度，可以通过初始化参数CREATE_BITMAP_AREA_SIZE在SGA中为其指定更大的内存空间。

在使用位图索引时需要注意，位图索引并不能直接起作用。在SELECT语句中通过关键字INDEX_COMBINE指定提示，可以使该语句使用指定的位图索引。例如：

SQL＞SELECT/*+index_combine（emp deptno）*/SUM（sal）FROM emp

WHERE DEPTNO=30；

10.3.4　基于函数的索引

基于函数的索引是将索引建立在某个函数或者某个表达式的基础上。在一些查询中，需要对某个列的值进行某种运算，在这种情况下，可以创建基于函数的索引。例如，考虑下面的查询：

SQL＞SELECT*FROM emp WHERE lower（ename）='smith'；

如果没有在表emp的ename列上创建索引，那么在执行这条语句时，需要把ename列的值转换成小写，然后与字符串“smith”进行比较。为了加快查询速度，可以在ename列上创建一个索引，先对该列上的值进行相应的转换，然后把转换后的值存储在索引中，这样在执行查询时就不需要再进行转化了。以下语句用来在表emp的ename列上创建基于函数lower的索引。

SQL＞CREATE INDEX emp_idx2 ON emp（lower（ename））；

索引所基于的函数可以是预定义函数，也可以是用户自定义的函数。无论是哪种情况，函数必须已经存在。例如，为了计算员工的个人所得税，在数据库中先创建一个用来计算员工个人所得税的函数，假设税率为3%。

CREATE OR REPLACE FUNCTION tax（sal IN emp.sal%type，

comm IN emp.comm%type）

RETURN NUMBER

DETERMINISTIC

IS

BEGIN

RETURN（（sal+NVL（comm，0））*0.03）；

END；

利用上面的函数，可以很方便地计算员工应该缴纳的个人所得税。假设经常要根据个人所得税查询员工的信息，可以在表emp上创建一个基于上述函数的索引：

SQL＞CREATE INDEX emp_tax_indx

ON emp（tax（sal, comm））；

那样以后在查询语句中就可以引用这个索引了，例如：

SQL＞SELECT ename, sal, comm FROM emp

WHERE tax（sal, comm）＞50

基于函数的索引还有另外一种形式，那就是用一个表达式代替函数。例如，个人所得税可以通过表达式（sal+nvl（comm，0））*0.03来计算，那么可以基于这个表达式在表emp上创建一个索引：

SQL＞CREATE INDEX emp_idx3 ON emp（（sal+nvl（comm，0））*0.03）；

这个索引在以下形式的查询中将起作用：

SQL＞SELECT ename, sal, comm FROM emp

WHERE（sal+nvl（comm，0））*0.03＞50

需要注意的是，基于函数的索引并不是可以直接起作用的。在创建基于函数的索引之后，应该对表进行分析。例如：

SQL＞ANALYZE TABLE emp COMPUTE STATISTICS；

10.3.5　分区索引

和表的分区一样，也可以对索引进行分区。分区后的索引对应若干个索引段。分区索引是建立在分区表之上的。在分区表上建立索引时，可以选择是建立全局索引还是分区索引。如果是全局索引，那么索引中的数据将存储在同一个索引段中。如果建立分区索引，那么索引中的数据将存储在若干个索引段中，表中的每个分区将对应一个单独的索引段。

例如，下面的语句将在分区表call上创建一个全局索引：

SQL＞CREATE INDEX call_idx1 ON call（month）GLOBAL；

下面的语句将在分区表call上创建一个分区索引：

SQL＞CREATE INDEX call_idx2 ON call（month）LOCAL；

10.3.6　索引的维护

索引的维护内容主要包含修改索引的物理属性、手工分配和回收存储空间、重建索引、合并索引等。用户必须是索引的属主，或者具有ALTER ANY INDEX系统权限。

例如，以下语句用于修改索引dept_idx1的一些属性：

SQL＞ALTER INDEX dept_idx1

INITRANS 6 MAXTRANS 12

LOGGING

NOPARALLEL；

对于那些因施加主键约束或唯一性约束而产生的索引，则需要通过执行ALTER TABLE命令来修改它的存储参数。例如：

SQL＞ALTER TABLE dept

ENABLE PRIMARY KEY USING INDEX

PCTFREE 20

INITRANS 5 MAXTRANS 10；

当索引段的空间被使用完以后，它可以自动扩展。用户也可以手工为索引段分配一些空间，也可以手工回收那些没有使用的存储空间。例如，下面的语句为索引dept_idx1分配一个256K的区：

SQL＞ALTER INDEX dept_idx1 ALLOCATE EXTENT（SIZE 256K）；

在为索引手工分配区时，如果没有指定区的大小，则采用索引所在表空间的区大小。实际上，手工指定的区大小与索引所在表空间的区大小可能不一致。假设表空间的区大小是64KB，那么上述命令的执行结果是为索引分配了4个64KB的区。

下面的语句用于回收索引dept_idx1中未使用的存储空间：

SQL＞ALTER INDEX dept_idx1 DEALLOCATE UNUSED；

重建索引是提高索引访问效率的一种有效方法。随着用户不断地在表上执行DML操作，索引段中碎片将越来越多，重建索引可以把索引段中的数据移动到另一块存储区，并把它们以紧凑的方式重新排列，从而提高索引的访问效率。例如：

SQL＞ALTER INDEX dept_idx1 REBUILD；

对于在线创建的索引，可以对其进行在线重建。在线重建的意思是在表上有用户正在执行DML操作的时候，对索引进行重建。例如：

SQL＞ALTER INDEX emp_dept_idx REBUILD ONLINE；

合并索引可以使索引的存储空间得到充分利用。如果相邻的数据块中有空闲空间，可以将这些数据块中的索引项合并在一个数据块中。例如：

SQL＞ALTER INDEX dept_idx1 COALESCE；

10.4　簇的管理

簇是一种数据库对象，它由一组共享相同数据块的表组成。簇中的表根据簇键组合在一起，簇键相同的行存储在相同的数据块或相邻的数据块中。簇键是一个列或多个列的组合，簇中的每个表都必须具有与簇键相同的列。

在常规的情况下，每个表对应一个单独的表段，表中的数据存储在表段中。当多个表以簇的形式组织在一起后，单独的表段将不存在，表中的数据都将存储在一个簇段中。

把表组织为簇的主要目的，是在进行多表联合查询时，减少磁盘操作次数，提高查询速度。例如，假设要经常执行下面的查询：

SQL＞SELECT empno, ename, sal, dname

FROM emp, dept

WHERE emp.deptno=dept.deptno；

由于是在两个表之间进行连接查询，所以至少需要两次磁盘读操作。如果把这两个表以簇的形式组织在一起，磁盘操作的次数可以减少到一次。由于表emp和dept都有deptno列，所以这个列在簇中就作为簇键。两个表中簇键相同的行将存储在同一个数据块或相邻的多个数据块中。图10.5表示簇与普通表的区别。

图　10.5　簇与普通表的区别

10.4.1　簇的创建

要把表以簇的形式组织在一起，首先需要创建一个簇，然后在簇中创建表。用户需要具有CREATE CLUSTER系统权限和表空间中的配额。如果希望在其他用户的模式中创建簇，需要具有CREATE ANY CLUSTER系统权限。在创建簇时应注意以下原则：

·簇中的表主要用来查询，而不应该频繁地执行DML操作。

·簇中的表应该是经常用来进行连接查询的表。

·簇中的表必须包含一个或多个共同的列，并把这样的列作为簇键。

·由于簇中的数据存储在簇段中，所以可以为簇指定存储参数。

·可以为每个簇键列的值和相关行指定平均大小。

创建簇的命令是CREATE CLUSTER。例如，下面的语句将创建簇emp_dpt：

SQL＞CREATE CLUSTER emp_dept（deptno number（3））

TABLESPACE USERS

SIZE 1024

PCTFREE 20；

在上面的语句中，deptno是簇中的簇键。当在簇中创建表时，每个表都必须包含与簇键相同的列，最好将两个表的主键和外键作为簇键。SIZE子句的作用是为每个簇键列的值和相关行指定平均大小，单位为字节。簇键值相同的行存储在同一个数据块中，或者相邻的多个数据块中。簇键值不同的行也可存储在同一个数据块中。如果SIZE指定的大小不合适，那么可能发生数据块的迁移，或者浪费数据块空间，所以在创建簇之前，应该充分估计簇键列的值和相关行的大小。

在创建簇以后，就可以在簇中创建表了。由于表中的数据存储在簇段中，所以不能为表指定存储参数。在创建表时必须通过CLUSTER指定表所在的簇及簇键。下面的语句用于在簇emp_dept中创建表dept和emp。

SQL＞CREATE TABLE dept（

deptno number（3）primary key，

dname varchar（10），

loc varchar（20）

）CLUSTER emp_dept（deptno）；

SQL＞CREATE TABLE emp（

empno number（4）primary key，

ename varchar（10），

deptno number（3）references dept（deptno）

）CLUSTER emp_dept（deptno）；

在往簇中的表里插入数据之前，必须在簇上创建索引。创建簇索引的用户必须是簇的属主，或者具有CREATE ANY CLUSTER系统权限。除了要指定所在的簇以外，簇索引的创建方法与普通索引类似，索引和簇可以位于同一个表空间，也可以位于不同的表空间中。下面的语句将用于在簇emp_dept上创建索引：

SQL＞CREATE INDEX emp_dept_idx

ON CLUSTER emp_dept

TABLESPACE ts1；

10.4.2　簇的修改

簇的修改涉及簇本身的修改、表的修改和索引的修改等内容。

由于簇是存在于簇段中的。因此可以像修改其他段一样修改簇的物理属性和存储参数，还可以修改每个簇键值及相关行的平均大小。修改簇的用户必须是簇的属主，或者具有ALTER ANY CLUSTER系统权限。例如下面的语句将用来修改簇emp_dept：

SQL＞ALTER CLUSTER emp_dept

SIZE 512

PCTFREE 30 PCTUSED 60

INITRANS 10

PARALLEL

CACHE

DEALLOCATE UNUSED；

表的修改主要涉及表结构的修改。由于簇中的表并不是单独存储在一个表段中，而是存储在簇段中，因此不能修改表的物理属性和存储属性。用户可以在表上增加、修改列，删除非主键列，创建约束，还可以创建触发器。这些内容在相关章节已经作了详细描述。这里不再赘述。

簇索引的修改主要涉及物理属性和存储参数的修改。与普通索引类似，簇索引也是单独存放在索引段中的，因此它的修改方法是相似的，详细信息请参阅索引的有关内容。

10.4.3　簇的删除

当簇中的表不再需要时，可以将簇删除。簇被删除后，簇中的表和索引都将不再存在。删除簇的用户必须是簇的属主，或者具有DROP ANY CLUSTER系统权限。如果簇中包含一个或多个表，应该先将这些表删除，然后删除这个空的簇，删除表的方法与删除其他普通表的方法相似。删除表的用户必须是簇的属主，或者具有DROP ANY CLUSTER系统权限，除此之外不需要有其他权限，即使这个表位于其他模式中。假设簇dep_dept中的所有表都已经被删除，下面的语句用于将这个空簇删除：

SQL＞DROP CLUSTER emp_dept；

如果簇中的表尚未删除，可以使用DROP语句和INCLUDEING TABLES将簇和表一起删除。例如：

SQL＞DROP CLUSTER emp_dept INCLUDING TABLES；

如果簇中的某个表包含一个主键，而在簇之外的另一个表通过外键引用这个表，那么用上面的语句是无法直接删除簇的。在这种情况下需要通过DROP语句和CASCADE CONSTRAINS子句将簇删除。例如：

SQL＞DROP CLUSTER emp_dept INCLUDING TABLES CASCADE CONSTRAINS；

簇索引可以单独被删除，当簇被删除时簇索引也将一起被删除。如果簇索引单独被删除，簇中的表也将无法使用，这时必须为簇重新创建簇索引。

10.4.4　簇信息的查询

簇的相关信息都存储在数据字典中，用户可以通过查询相关的数据字典来了解簇的信息。与簇有关的数据字典有以下几个：

·dba_clusters：记录数据库中的所有簇。

·all_clusters：记录当前用户所能访问的所有簇。

·user_clusters：记录当前用户所拥有的所有簇。

·dba_clu_columns：记录簇键与表中的列的对应关系。

·user_clu_columns：记录簇键与表中的列的对应关系（当前用户所拥有的簇）。

例如，要了解簇中的簇键，以及表中与簇键对应的列，可以执行以下查询：

SQL＞SELECT*FROM user_clu_columns WHERE cluster_name='EMP_DEPT'；

CLU_COLUMN_NAME TABLE_NAME TAB_COLUMN_NAME

DEPTNO DEPT1 DEPTNO

DEPTNO EMP1 DEPTNO

在查询结果中，三个列分别代表簇键列、簇中的表以及表中与簇键对应的列。

10.5　索引组织表的管理

像其他数据库对象一样，索引组织表的管理包括创建、删除、修改等操作。其中索引组织表的删除与普通表完全相同，这里不再介绍。

10.5.1　索引组织表的概念

索引组织表（Index-Organized Table，简称IOT）是一种特殊类型的表，它把表中的数据和表的索引存放在同一个段中，并以B*树的方式组织在一起，从而可以加快表的查询速度。在索引组织表中必须有一个主键，表中的数据按照主键进行排序，在B*树的叶块中同时存储主键列和非主键列。

在传统的表中，表中的数据和索引是分别存放在两个段中的，表中的数据是无序存放的。在索引段中只存放索引列的值和和每行数据的ROWID，并且按照索引列排序。当对表进行查询时，首先根据索引列的值在索引中找到相应的ROWID，再根据ROWID在表中获得对应的数据。如果要经常根据主键列进行查询，可以创建一个索引组织表，按照主键列对该表进行排序，并以B*树的方式组织数据，这样在进行查询时，根据主键列的值可直接获得所需的数据，从而加快查询速度。图10.6表示普通表和索引组织表的区别：

图　10.6　普通表和索引组织表的区别

由于索引组织表是将整行数据都组织在索引中的，因而索引可能十分庞大。为了加快查询速度，可以为每个索引组织表定义为一个“溢出区”。当表中非主键列的数据超过数据块大小的一定比例时，将一部分非主键列移动到溢出区，而主键列和其他一些需要经常访问的非主键列仍然存储在索引中。用户在创建索引组织表时，可以指定溢出区的位置，需要移动到溢出区中的非主键列以及非主键列在数据块中占用空间的比例。

除了数据的组织方式与普通表不同外，索引组织表具有普通表的所有功能，用户可以在表上创建约束、触发器、视图，还可以创建其他索引。

10.5.2　索引组织表的创建

创建索引组织表的命令是CREATE TABLE。例如，下面的语句用于创建索引组织表employee。

SQL＞CREATE TABLE employee（

empno number（4）PRIMARY KEY，

ename varchar（10），

sal number（7，2），

deptno number（2），

photo blob）

ORGANIZATION INDEX

TABLESPACE users

PCTTHRESHOLD 20

INCLUDING deptno

OVERFLOW TABLESPACE SYSTEM；

在创建索引组织表时，需要通过PRIMARY KEY关键字定义一个主键。如果主键只包含一个列，则可以在列级或表级定义，如果包含多个列，则必须在表级定义。为了说明该表为索引组织表，在CREATE TABLE命令中需要使用ORGANIZATION INDEX子句。

在CREATE TABLE命令中还可以使用以下子句：

·TABLESPACE：指定表所在的表空间，表与溢出区可以位于不同的表空间中。

·PCTTHRESHOLD：指定一个百分数，例如20。当表中非主键列的数据超过数据块大小的这个比例时，一部分非主键列将被移动到溢出区中。

·OVERFLOW TABLESPACE：指定溢出区所位于的表空间。

·INCLUDING：指定表中的一个列，当非主键列的数据超过数据块大小的指定比例时，该列之后的所有非主键列将被移动到溢出区中，而该列及之前的非主键列和主键列将仍然保留在原来的表中。

为了加快索引组织表的查询速度，应该把那些不经常被访问的列移动到溢出区中。在创建表时，应该合理安排各个列的顺序，并指定一个合适的百分比和一个分界点，使经常被访问的列确实能被存储在原来的表中，而不经常访问的列在某个时刻可以被移动到溢出区中。

10.5.3　索引组织表的维护

索引组织表是一种特殊类型的表，所有针对普通表的维护都是适用于索引组织表，用户可以像对待普通表一样，在索引组织表上执行ALTER TABLE命令。

需要注意的是，索引组织表中的溢出区与普通表一样，也存储在表段中。用户可以同时修改索引组织表和溢出区的相关信息。例如：

SQL＞ALTER TABLE employee INITRANS 5

OVERFLOW PCTFREE 30 INITRANS 10；

针对索引组织表的特点，相关的维护工作主要涉及溢出区的操作。用户可以重新指定非主键列在数据块中的比例，以及需要移动到溢出区中的非主键列。例如：

SQL＞ALTER TABLE employee

PCTTHRESHOLD 30

INCLUDING sal；

如果在创建表时没有指定溢出区，可以通过ALTER TABLE命令为其添加溢出区。例如：SQL＞ALTER TABLE another_iot ADD OVERFLOW TABLESPACE users；

对于一个索引组织表，我们还可以通过ALTER TABLE命令将表和溢出段一起移动到其他的表空间中。例如：

SQL＞ALTER TABLE employee MOVE TABLESPACE data1

OVERFLOW TABLESPACE data2；

当索引组织表被移动到另一个表空间时，表上的索引同时被重建。重建索引是减少索引中存储碎片的一种有效方法。由于索引组织表是将表和索引组织在一起的，随着DML操作的频繁运行，索引中的存储碎片将逐渐增加，这将大大降低数据库的性能。重建索引也可以在原来的表空间中进行，只要对表进行移动操作，索引重建便随之进行。例如：

SQL＞ALTER TABLE employee MOVE；

第11章　用户与权限管理

用户和权限是Oracle数据库系统最基本的安全管理机制。用户通过SQL*Plus或者应用程序登录Oracle时，必须提供一个账号，即用户名和口令。登录后还必须有一定的权限才能完成相应的操作。用户具有什么样的权限，才能完成什么样的操作。如果用户希望访问数据库，首先应该由数据库管理员为其分配一个账号，然后再为它指定一定的权限，这样用户才能完成相应的操作。数据库管理员还可以通过PROFILE对用户使用口令及系统资源的情况进行控制。

11.1　用户管理

用户是一个数据库对象，是一系列数据库对象和权限的统称。用户所有的操作默认在自己的模式下进行，模式是一个用户所拥有的数据库对象的集合，每个用户都有自己的模式，用户与模式之间是一一对应的，模式的名字与用户名相同。例如，SCOTT用户的模式为SCOTT，在这个模式中包含了用户SCOTT拥有的所有数据库对象，包括表、视图、索引、存储程序等。

用户的数据库对象和数据分布在表空间中，每个用户都有默认的表空间。默认的表空间在创建用户时指定，如果不指定，那么SYSTEM表空间将被指定为该用户的默认表空间。

11.1.1　数据库中有哪些用户

在Oracle数据库中有三类用户，一类是sysdba，一类是sysoper，第三类是普通用户。其中前两类用户称为特权用户，它们拥有对所有数据库对象的一切权限，包括数据库本身。sys用户同时具有sysdba和sysoper两种权限，它在创建数据库时自动产生，不需要手工创建。

特权用户的口令一方面存放在数据库中，另一方面存储在口令文件中。如果为一个普通用户指定了sysdba或sysoper权限，那么这个用户也将成为特权用户。特权用户的信息可以从动态性能视图v$pwfile_users中获得。一个用户只要具有这两种权限，就可以以“as sysdba”或者“as sysoper”的方式登录数据库服务器。例如：

$sqlplus scott/tiger as sysdba

如果用户以“as sysdba”方式登录数据库服务器，那么他将成为sys用户。如果以“as sysoper”的方式登录，那么他将成为PUBLIC用户。这两种权限的范围大小是不一样的，例如，如果一个用户以“as sysdba”方式登录，那么他可以无条件地访问任何用户的数据，例如：

$sqlplus scott/tiger as sysdba

SQL＞select*from user1.t1；

如果一个用户以“as sysoper”方式登录，那么他是不能访问其他用户的数据的。在后面的内容中，将对这两种权限进行详细的比较。

普通用户一般由sys用户创建，这类用户的权限比较小，一般只限于访问自己模式中的数据库对象。普通用户如果希望对数据库进行其他的访问，就需要具有相应的权限。

11.1.2　如何创建用户

数据库系统在运行的过程中，往往要根据实际需求创建用户，然后为用户指定适当的权限。创建用户的操作一般只能由sys用户完成，如果普通用户也要创建用户，必须具有一个系统权限，即CREATE USER。

创建用户的命令是CREATE USER，创建用户所涉及的内容包括为用户指定用户名、口令、默认表空间、存储空间配额等。其中用户名是代表用户账号的标识符，它的命名规则是：

·必须以字母开始。

·长度为1～30个字符。

·从第二个字符开始，可以包括大小写字母、数字、_、$和#等字符。

·大写和小写是相同的。

用来创建用户的CREATE USER命令的完整语法格式为：

CREATE USER 用户名 IDENTIFIED BY"口令"

DEFAULT TABLESPACE 表空间

TEMPORARY TABLESPACE 临时表空间

QUOTA 空间配额大小 ON 表空间

PASSWORD EXPIRE

ACCOUNT LOCK|UNLOCK

其中DEFAULT TABLESPACE用来为用户指定默认表空间，如果不指定，那么SYSTEM表空间将被指定为这个用户的默认表空间。如果不特别指定，用户创建的表、索引等数据库对象就位于默认表空间中。为了提高数据库的性能，同时为了方便管理数据，Oracle建议为用户指定一个默认表空间。

DEFAULT TABLESPACE用于为用户指定临时表空间。在一个数据库中可以创建多个临时表空间，为每个用户可以指定不同的临时表空间。还可以把多个临时表空间组织为一个表空间组，把这个表空间组作为整个数据库或者某个用户的默认临时表空间。

QUOTA子句用于为用户在表空间上指定空间配额。尽管为用户指定了默认表空间，但是用户在这个表空间上还不能创建数据库对象，因为它在这个表空间上没有可支配的存储空间。空间配额以字节、KB、MB等为单位，还可以指定为UNLIMITED，即无限制的空间配额。如果希望用户在所有表空间上都具有无限制的空间配额，只要为其指定UNLIMITED TABLESPACES系统权限即可。

PASSWORD EXPIRE用于指定用户的口令过期，用户在第一次登录数据库服务器时必须修改自己的口令。在创建用户账号时，用户的初始口令是由管理员指定的。如果以口令过期的方式强迫用户修改自己的口令，这样将使用户的账号更加安全。

ACCOUNT子句用于指定用户账号的状态，如果为UNLOCK，这个用户就能够登录数据库服务器，这是默认设置。如果为LOCK，则为锁定状态。

例如，下面的CREATE语句用于创建用户user1，并为其指定相关属性：

SQL＞CREATE USER user1 IDENTIFIED BY"1234"

DEFAULT TABLESPACE users

TEMPORARY TABLESPACE temp

QUOTA 20M ON users

PASSWORD EXPIRE

ACCOUNT UNLOCK；

用户被创建之后，还没有任何权限，甚至不能登录数据库，只有当sys用户为它指定了一定的权限后，它才能对数据库进行访问。

用户的相关信息可以从数据字典视图dba_users中获得。例如，下面的SELECT语句用于查询当前数据库中所有用户的名称、口令、默认表空间和账号状态等信息：

SQL＞SELECT username, password, default_tablespace, account_status FROM dba_users；

这条命令的执行结果为（这里只列出了部分用户）：

username password default_tablespace account_status

SYS SYSTEM OPEN

SYSTEM SYSTEM OPEN

SCOTT SYSTEM OPEN

USER1 USERS EXPIRED

在列出的用户中，包括特权用户、数据库预创建的用户和刚刚手工创建的用户。用户的口令都是经过加密的，在较早版本的数据库中，可以显示加密的口令，在现在的版本中不显示口令。如果不特别指定，用户的默认表空间是SYSTEM表空间。除最后一个用户外，其余用户的账号都是活动的（OPEN）。

11.1.3　如何修改用户的信息

为了防止不法之徒盗取用户的口令，用户应该经常改变自己的口令。用户的口令不应该是类似1234、abcd这样简单的字符串，更不要指定为自己生日和姓名，也不要指定为一个英文单词，因为这样的口令很容易被破译。一个好的口令应该包括大小写字母、数字、_、＆、%、$等各种符号在内的混合字符串。统计表明，一个口令中包含的成分越复杂，就越难破译。

修改用户口令的操作一般由用户自己完成，sys用户可以无条件修改任何一个用户的口令。普通用户只能修改自己的口令，如果要修改其他用户的口令，必须具有ALTER USER这个系统权限。

修改用户口令的命令是ALTER USER。修改用户口令的ALTER USER命令格式为：

ALTER USER用户IDENTIFIED BY"新口令"；

例如，将刚才创建的用户user1的口令改为“book”，对应的ALTER USER语句为：

ALTER USER user1 IDENTIFIED BY"book"；

除了SQL命令ALTER USER以外，SQL*Plus也提供了一个PASSWORD命令，可以用来修改用户的口令。利用SQL*Plus的这条命令，sys用户可以修改其他用户的口令，普通用户可以修改自己的口令，在授权的情况下也可以修改其他用户的口令。一个用户在修改自己的口令时，必须知道自己以前的口令。这条命令只有一个参数，就是用户名，如果默认了这个参数，就修改自己的口令。以下是用户scott修改自己的口令的情况：

SQL＞PASSWORD

更改SCOTT的口令

旧口令：*****

新口令：******

重新键入新口令：******

口令已更改

ALTER USER命令除了修改用户口令外，还可以修改用户的默认表空间、存储空间配额、账号状态等信息。例如，下面的语句用于锁定用户scott：

SQL＞ALTER USER scott ACCOUNT LOCK；

下面的语句用于修改用户user1的默认表空间、临时表空间，并将该用户在表空间ts1上的空间配额修改为无限制：

SQL＞ALTER USER user1

DEFAULT TABLESPACE ts1

TEMPORARY TABLESPACE ts1

QUOTA UNLIMITED ON ts1；

11.1.4　如何删除用户

一个用户不再访问数据库系统时，应该将这个用户及时地从数据库中删除，否则可能会有安全隐患。一个更好的做法是先将这个用户锁定，过一段时间如果确定这个用户不再需要，再将其删除。

删除用户的操作一般由sys用户完成，也可以由具有DROP USER权限的用户来完成。一个用户被删除后，这个用户本身的信息，以及它所拥有的数据库对象的信息都将从数据字典中被删除。删除用户的命令是DROP USER。这条命令的语法格式为：

DROP USER用户名；

例如，要删除用户USER1，可以执行下面的DROP USER语句：

SQL＞DROP USER user1；

需要注意的是，如果在一个用户的模式中已经包含一些数据库对象，那么这个用户是不能被直接删除的，在删除用户时系统将显示类似以下的信息：

ERROR位于第1行：

ORA-01922：必须指定CASCADE以删除'SCOTT'

出现错误信息的原因是在用户scott的模式中包含某些数据库对象，所以不能直接删除。如果要强制删除用户，可以在DROP USER命令中使用CASCADE选项，这样用户以及所拥有的数据库对象将一起被删除。例如，删除用户scott可以执行下面的语句：

SQL＞DROP USER scott CASCADE；

11.2　用户权限的管理

Oracle数据库对用户的权限有着严密的规定，如果没有权限，用户将无法完成任何操作，甚至不能登录数据库。例如，刚刚创建的用户user1在试图登录数据库时，将看到以下错误信息：

ORA-01045：user USER1 lacks CREATE SESSION privilege；logon denied

也就是说，用户若要登录数据库，必须具有CREATE SESSION权限。用户权限有两类，即系统权限和对象权限。系统权限主要是指对数据库对象的创建、删除、修改的权限，对数据库进行创建、备份等权限，而对象权限主要是指对数据库对象中的数据的访问权限。系统权限一般由sys用户指定，也可以由具有特权的其他用户授予。对象权限一般由数据库对象的所有者授予用户，也可以由sys用户指定，或者由具有该对象权限的其他用户授予。

11.2.1　系统权限的管理

系统权限是与数据库安全有关的最重要的权限，这类权限一般是针对数据库管理员的。系统权限的管理主要包括权限的分配、回收和查询等操作。Oracle 11g提供了200余种系统权限，表11.1列出了与用户有关的最重要的几种系统权限。

对于表、视图、会话、用户、触发器这些数据库对象，有关的系统权限包括创建、删除和修改它们的权限，相关的命令分别是CREATE、DROP和ALTER。表、视图、触发器、存储程序等对象是与用户有关的，在默认情况下对这些对象的操作都是在当前用户自己的模式下进行的。如果要在其他用户的模式下操作这些类型的对象，需要具有对应的ANY权限。例如，要能够在其他用户的模式下创建表，当前用户必须具有CREATE ANY TABLE系统权限，如果希望能够在其他用户的模式下删除表，必须具有DROP ANY TABLE系统权限等。

系统权限一般有三种授予者，一种是sys用户，即数据库管理员，这是最主要的一种授予者，大部分的系统权限管理工作都由sys用户完成。第二种是具有特权的普通用户，普通用户一旦具有了sysdba或者sysoper特权，也可以管理系统权限。第三种是被授予了某种系统权限的用户，系统允许它把所拥有的系统权限再授予其他用户。

为用户授予权限的GRANT命令的语法格式为：

GRANT 权限1，权限2，……

TO用户1，用户2，……

WITH ADMIN OPTION；

GRANT命令执行后，所有指定用户都将获得指定的权限。如果希望把一个权限授予所有用户，可以用PUBLIC代替所有的用户名。选项“WITH ADMIN OPTION”的功能是使得权限的获得者可以再将权限授予其他用户。

例如，刚刚创建用户user1时，这个用户没有任何权限。如果把CREATE SESSION权限授予这个用户，那么这个用户就可以登录数据库了。如果再把CREATE TABLE权限授予这个用户，那么他就可以在自己的模式中创建表了。授予这两个权限的语句为：

GRANT CREATE SESSION, CREATE TABLE TO user1；

用户在登录数据库系统后应该可以创建表了，但是当这个用户在自己的模式中创建表时，将遇到以下的错误：

ORA-01950：表空间'SYSTEM'中无权限

出错的原因是当前用户在他的默认表空间中没有空间配额，即没有可以使用的存储空间，所以无法创建表。这时需要为该用户指定空间配额，或者为这个用户指定UNLIMITED TABLESPACE系统权限。所以为了使一个用户可以操作数据库，至少应该把上述三个权限授予这个新用户。例如，下列的授权语句使得所有用户都可以使用无限制的磁盘空间：

SQL＞GRANT UNLIMITED TABLESPACE TO PUBLIC；

如果将ALTER USER的系统权限授予某用户，那么它就可以修改其他用户的信息。现在考察下列权限操作的语句序列：

SQL＞CONN sys/1234 AS sysdba；

SQL＞GRANT ALTER USER TO user1；

SQL＞CONN user1/1234；

SQL＞ALTER USER scott IDENTIFIED BY"hello"；

如果上述语句都执行成功，那么用户scott的口令将被修改为“hello”。首先以特权用户sys登录，并将系统权限ALTER USER授予用户user1。然后以用户user1登录，这个用户就可以修改其他用户的信息了。

回收系统权限的命令是REVOKE，这条命令一般由SYS用户执行。普通用户如果要回收其他用户的权限，则他必须具有“GRANT ANY PRIVILEGE”这个系统权限。如果一个用户在接受某个系统权限时是以“WITH ADMIN OPTION”方式接受的，他随后又将这个系统权限授予了其他用户，那么他也可以将这个系统权限从其他用户回收。REVOKE的语法格式为：

REVOKE 系统权限1，系统权限2……

FROM 用户1，用户2……

如果要从所有用户回收某个系统权限，可以用PUBLIC代替所有的用户名。例如，要将刚才授予用户user1的系统权限CREATE SESSION和CREATE TABLE回收，可以执行下面的REVOKE语句：

SQL＞REVOKE CREATE SESSION, CREATE TABLE FROM user1；

这样，用户USER1就不能登录数据库，更不能创建表了。

需要注意的是，系统权限可以转授，但是回收时不能间接回收。假设有三个用户，第一个用户将某个系统权限以“WITH ADMIN OPTION”的方式授予第二个用户，第二个用户又将这个权限授予第三个用户，那么当第一个用户从第二个用户回收这个权限时，并不能同时从第三个用户回收这个权限，第三个用户这时仍然具有这个权限。现在考虑下列权限操作的语句序列：

SQL＞CONN sys/1234 AS sysdba

SQL＞GRANT CREATE SESSION, CREATE TABLE TO user1 WITH ADMIN OPTION；

SQL＞CREATE USER user2 IDENTIFIED BY"1234"；

SQL＞CONN user1/1234

SQL＞GRANT CREATE SESSION, CREATE TABLE to user2；

SQL＞CONN sys/1234 AS sysdba

SQL＞REVOKE CREATE SESSIOIN, CREATE TABLE FROM user1；

SQL＞CONN user2/1234

SQL＞SELECT*FROM user_sys_privs；

假设上面的语句都执行成功，最后一条语句的执行结果为：

USERNAME PRIVILEGE ADM

USER2 CREATE TABLE NO

USER2 CREATE SESSION NO

现在我们解释一下这些语句的执行过程。首先以特权用户sys登录数据库，将两个系统权限授予用户user1，然后创建用户user2。接着以user1登录，将这两个权限转授予用户user2。这时用户user2就可以登录数据库了。接着再以用户sys登录，并从用户user1回收这两个系统权限。按照前面的说法，这两个权限并没有从用户user2回收，所以最后仍然能以user2用户登录数据库。经用户user2查询数据字典user_sys_privs，结果表明该用户仍然具有这两个权限。要回收这种转授的权限，可以由sys用户或者权限授予者直接从权限的接受者回收。

11.2.2　对象权限的管理

对象权限主要是对数据库对象中的数据的访问权限，这类权限主要是针对普通用户的。表11.2列出了最主要对象权限。

SELECT、INSERT、DELETE和UPDATE权限分别是指对数据库对象中的数据的查询、插入、删除和修改的权限。对于表和视图来说，查询和删除操作是整行进行的，而插入和修改却可以在一行的某个列上进行，所以在指定权限时，SELECT和DELETE权限只要指定所要访问的表就可以了，而INSERT和UPDATE权限还可以进一步指定是对哪个列的权限。

REFERENCES权限是指可以与一个表建立关联关系的权限，如果具有了这个权限，当前用户就可以通过自己的一个表中的外键，与对方的表建立关联。关联关系是通过主键和外键进行的，所以在授予这个权限时，可以指定表中的列，也可以不指定。

EXECUTE权限是指可以执行存储函数、存储过程和程序包的权限。有了这个权限，一个用户就可以执行另一个用户的存储程序。

当一个用户获得另一个用户的某个对象的访问权限后，以“用户名.对象名”的形式访问这个数据库对象。一个用户所拥有的对象和可以访问的对象是不同的，这一点在数据字典视图中也有所反映。在默认情况下用户可以直接访问自己模式中的数据库对象，但是要访问其他用户所拥有的对象，就必须具有相应的对象权限。例如，用户user1要查询用户scott的表emp中的数据，就必须具有对表emp的SELECT权限。从数据字典视图user_objects中可以获得当前用户所拥有的全部数据库对象的信息，而从数据字典视图all_objects中则可以获得当前用户所能访问的全部对象的信息。

对象权限的授予一般由对象的所有者完成，也可以由sys用户，或者由具有某对象权限的用户授予，但最好由对象的所有者完成。授予对象权限的命令是GRANT，回收权限的命令是REVOKE，与系统权限的操作相比，这两条命令有些不同的地方。授予对象权限的GRANT命令语法格式为：

GRANT对象权限1（列名），对象权限2（列名）……

ON对象TO用户1，用户2……

WITH GRANT OPTION；

在授予对象权限时，不仅要说明是什么权限，还要指定是对哪个对象的访问权限，这是与系统权限的授予不同的地方。例如，假设当前用户是scott，下面的语句将表emp的SELECT权限授予用户user1和user2：

SQL＞GRANT SELECT ON emp TO user1，user2；

如果要将某个对象权限授予所有用户，可以用PUBLIC代替所有的用户名。用户访问其他用户的对象时，需要用对方的用户名限定对象。例如，用户user1访问emp表的语句为：

SELECT empno, ename, sal FROM scott.emp；

如果没有SELECT权限，用户在查询其他用户的一个表时，将遇到“ORA-00942：表或视图不存在”这样的错误。

如果在为用户指定某个对象权限时使用了“WITH GRANT OPTION”选项，那么这个用户在接受了这个权限后，可以再将这个权限授予其他的用户。假设当前用户是scott，考察下面的权限操作语句：

SQL＞GRANT SELECT ON dept TO user1 WITH GRANT OPTION；

SQL＞CONN user1/1234

SQL＞GRANT SELECT ON scott.dept TO user2；

SQL＞CONN user2/1234

SQL＞SELECT*FROM scott.dept；

如果上面的语句都执行成功，最后一条语句将显示dept表中的数据。首先用户scott将表dept上的SELECT权限以“WITH GRANT OPTION”的方式授予用户user1。然后以用户user1登录，再将这个权限授予用户user2。最后以用户user2登录，这时就可以查询用户scott的dept表中的数据了。

对于INSERT和UPDATE两个对象权限，还可以进一步指定是在表中的哪个列上具有访问权限，也就是说，可以规定其他用户可以对表中的哪个列进行插入和修改操作。例如，假设当前用户是scott，下面的语句将表emp中对列empno的插入权限和sal列的修改权限授予用户user1和user2：

SQL＞GRANT INSERT（empno），UPDATE（sal）ON emp TO user1，user2；

这样用户user1和user2就可以对表emp的列empno进行插入访问，对sal列进行修改访问，而对其他列没有任何访问权限。

EXECUTE权限是指可以执行一个用户所拥有的存储函数、存储过程和程序包的权限，假设当前用户创建了一个存储函数function1，通过下面的语句可以把对这个函数的执行权限授予用户user1；

SQL＞GRANT EXECUTE ON function1 TO user1；

REFERENCES权限是指其他用户的表可以与当前用户的表建立关联关系的权限。假设当前用户有一个表，在这个表的某个列上建立了主键约束。如果把这个表的REFERENCES权限授予其他用户，那么其他用户就可以通过外键将自己的表与这个表建立关联关系，也就是可以引用这个表中的主键列的值。假设当前用户是scott，在表dept的deptno列上建立了主键约束。假设用户scott要将表dept上的deptno列的REFERENCES权限授予用户user1，可以执行下面的GRANT语句：

SQL＞GRANT REFERENCES（deptno）ON dept TO user1；

这样用户user1就可以创建一个表，在表上指定一个外键，并且与表dept上的deptno列建立关联关系，或者修改一个已经存在的表，在表上指定一个外键，同样可以建立这种关联关系。例如：

CREATE TABLE sample（

deptno number REFERENCES scott.dept（deptno）

……）；

回收对象权限的命令是REVOKE。一般回收对象权限的操作由权限的授予者完成。这条命令的语法格式为：

REVOKE 对象权限1，对象权限2……ON对象

FROM 用户1，用户2……

如果要回收与某个数据库对象有关的所有对象权限，可以用ALL代替权限列表。例如，用户scott要从用户user1回收对表emp的所有权限，可以执行下面的REVOKE语句：

SQL＞REVOKE ALL ON emp FROM user1；

如果要从所有用户回收某个对象权限，可以用PUBLIC代替所有用户的名称。

需要注意的是，权限INSERT、UPDATE和REFERENCES在分配时可以指定相关的列，但是在回收时不能指定列，而只能指定表名。例如，用户scott要从用户user2回收对表emp中sal列上的UPDATE权限，下面的语句是错误的：

SQL＞REVOKE UPDATE（sal）ON emp FROM user2；

正确的语句应该是：

SQL＞REVOKE UPDATE ON emp FROM user2；

如果某个对象权限是以“WITH GRANT OPTION”方式授予一个用户的，那么这个用户可以将这个权限再授予其他用户。在从这个用户回收对象权限时，也将同时从其他用户回收，这一点也是与系统权限不同的。例如，假设当前用户scott将表emp的SELECT权限以“WITH GRANT OPTION”方式授予了用户user1，user1又将这个权限授予了用户user2，那么当用户scott从用户user1回收这个权限时，同时将这个权限从用户user2回收。

11.2.3　权限信息的查询

用户的系统权限和对象权限信息，都可以从数据字典视图中获得。与系统权限有关的数据字典视图包括：system_privilege_map、dba_sys_privs、user_sys_privs和session_privs。

其中从视图system_priviege_map中可以获得当前数据库中已经定义的所有系统权限。例如，下面的语句可以列出数据库中所有的系统权限：

SQL＞SELECT name FROM system_privilege_map；

SYS用户可以从数据字典视图dba_sys_privs中查询任何用户所具有的系统权限，而普通用户可以从数据字典视图user_sys_privs中查询自己所拥有的系统权限。例如，下面的SELECT语句查询用户scott所拥有的系统权限：

SQL＞SELECT grantee, privilege, admin_option

FROM dba_sys_privs

WHERE grantee='SCOTT'；

其中列grantee指的是权限的被授予者，privilege列表示权限名称，adm列表示是否允许用户再将这个权限授予其他用户。

又如，假设当前用户是scott，下面的查询语句列出了当前用户所拥有的系统权限：

SQL＞SELECT username, privilege, admin_option

FROM user_sys_privs；

这条语句与上面一条语句的执行结果是相同的。

从数据字典视图session_privs中可以查询一个用户在当前会话中所具有的系统权限。如果分别从字典视图session_privs和user_sys_privs中查询一个用户所具有的系统权限，我们会发现查询结果并不相同。这是因为，从user_sys_privs中查询到的系统权限是用户直接获得的系统权限，即sys用户通过GRANT命令授予当前用户的系统权限。而从字典视图session_privs到的系统权限不仅包括该用户直接获得的权限，还包括该用户从角色中间接获得的系统权限。

与系统权限一样，对象权限的信息也可以从数据字典视图中查询。与对象权限有关的数据字典视图包括以下几个：dba_tab_privs、dba_col_privs、user_tab_privs和user_col_privs。

其中从dba_tab_privs查询任何用户所具有的对象权限。例如，下列的SELECT语句查询用户user1所具有的对象权限：

SQL＞SELECT grantee, privilege, grantor, table_name, grantable FROM dba_tab_privs WHERE grantee='USER1'；

这条语句的执行结果为：

GRANTEE PRIVILEGE GRANTOR TABLE_NAME GRANTABLE

USER1 REFERENCES SCOTT DEPT NO

USER1 REFERENCES SCOTT STUDENT NO

查询结果表明，用户user1对用户scott的两个表具有REFERENCES权限，权限的授予者都是scott，并且这两个权限都不能再被user1授予其他用户。

在另一个数据字典dba_col_privs中记录了用户在列上的权限。如果REFERENCES权限、INSERT权限和UPDATE权限在授予时没有指定具体的列，那么它们将被记录在数据字典dba_tab_privs中。如果涉及了具体的列，这些列的信息可以从数据字典视图dba_col_privs中查询。例如，下面的SELECT语句查询user1所具有的列权限：

SQL＞SELECT grantee, TABLE_name, column_name, privilege, grantor, grantable

FROM dba_col_privs

WHERE grantee='USER1'；

这条语句的执行结果为：

grantee TABLE_name column_name privilege grantor grantable

USER1 DEPT DEPTNO REFERENCES SCOTT NO

USER1 EMP EMPNO INSERT SCOTT NO

USER1 STUDENT SNO REFERENCES SCOTT NO

从查询结果可以看出，用户user1对表dept的deptno和表student的列sno具有REFERENCES权限，对表emp的列empno具有INSERT权限，这三个权限的授予者都是用户scott，它们都不能再被user1授予其他用户。

上面两个数据字典视图只能由sys用户查询，或者由具有特权的用户查询。如果一个普通用户想查看自己所具有的对象权限，可以从另外两个数据字典视图user_tab_privs和user_col_privs中查询。例如，假设当前用户是scott，下面的SELECT语句将查询这个用户所具有的全部对象权限：

SQL＞SELECT grantee, TABLE_name, column_name, privilege, grantor, grantable

FROM user_tab_privs

WHERE grantee='SCOTT'；

11.3　角色的管理

角色是一组权限的集合，使用角色的目的是使权限管理更加方便。假设有10个用户，这些用户为了访问数据库，最少应该具有CREATE SESSION、CREATE TABLE、UNLIMITED TABLESPACE等几个系统权限。如果将这些权限分别授予这些用户，那么需要进行的授权次数是比较多的。但是如果把这些权限事先放在一起，然后作为一个整体授予这些用户，那么每个用户只需一次授权，授权的次数将大大减少，而且用户数越多，需要指定的权限越多，这种授权方式的优越性就越明显。这些事先组合在一起的一组权限就是角色，角色中的权限既可以是系统权限，也可以是对象权限。

为了使用角色，首先在数据库中创建一个角色，这时角色中没有任何权限。然后向角色中添加权限。最后将这个角色授予用户，这个用户就具有了角色中的所有权限。在使用角色的过程中，可以随时向角色中添加权限，也可以随时从角色中删除权限，用户的权限也随之改变。如果要回收所有的权限，只要将角色从用户回收即可。图11.1表示用户与角色之间的关系。

图　11.1　角色与授权

11.3.1　角色的创建和删除

在数据库中有两类角色，一类是Oracle预定义的角色，一类是用户自定义的角色。Oracle预定义的角色在数据库被创建之后即存在，并且已经包含了一系列的权限，数据库管理员可以将这些角色直接授予用户。常用的角色有CONNECT、RESOURCE、DBA等角色，表11.3列出了常见的几个角色及其所包含的权限。

在新创建一个用户时，为了使其具有最基本的权限，最简单的方法是将CONNECT和RESOURCE两个角色授予该用户。

DBA是一个特殊的角色，在这个角色中包含了数据库中的绝大部分系统权限。表11.4列出了这个角色所包含的部分系统权限。

除了Oracle预定义的角色以外，用户还可以自己定义角色。一般情况下创建角色的操作只能由sys用户完成，如果普通用户要定义角色，必须具有“CREATE ROLE”系统权限。创建角色的命令是CREATE ROLE，它的语法格式为：

CREATE ROLE角色名；

例如，创建一个名为role1的角色，可以执行以下语句：

SQL＞CREATE ROLE role1；

删除角色的命令是“DROP ROLE”。角色被删除时，角色中的权限都被从用户回收。例如，删除刚才创建的角色的语句为：

SQL＞DROP ROLE role1；

11.3.2　角色中权限的添加和删除

角色刚被创建时，没有包含任何权限。用户可以将权限授予该角色，使这个角色成为一个权限的集合。向角色授权的方法与向用户授权的方法是相同的，只要将用户名用角色名代替就可以了。

需要注意的是，如果向角色授予系统权限，可以使用WITH ADMIN OPTION选项，但是向角色授予对象权限时，不能使用WITH GRANT OPTION。

例如，用户scott通过下列语句向角色role1授予若干权限：

SQL＞GRANT SELECT ON emp TO role1；

SQL＞GRANT UPDATE（sal）ON emp TO role1；

SYS用户也可以通过下列语句向该角色授予若干系统权限：

SQL＞GRANT CREATE SESSION, CREATE TABLE, CREATE VIEW TO role1；

如果要从角色中删除权限，可以执行REVOKE命令。权限回收的方法与从用户回收权限的方法相同，只是用角色名代替用户名就可以了。例如，用户将SELECT权限从角色role1回收，对应的语句为：

SQL＞REVOKE SELECT ON emp FROM role1；

11.3.3　角色的分配和回收

只有将角色授予用户，用户才会具有角色中的权限。可以一次将角色授予多个用户，这样这些用户就都具有了这个角色中包含的权限。将角色授予用户的命令是GRANT，授予角色的方法与授予权限的方法相同，只是将权限名用角色名代替就可以了。例如，下面的语句将角色role1授予用户user1和user2：

SQL＞GRANT role1 TO user1，user2；

在一般情况下，将角色授予用户的操作由sys用户完成，普通用户如果要完成这样的操作，必须具有GRANT ANY ROLE的系统权限。

要将角色从用户回收，需要执行REVOKE命令。角色被回收后，用户所具有的属于这个角色的权限都将被回收。从用户回收角色与回收权限的方法是相同的，这样的操作一般也由sys用户完成。例如，下面的语句将角色role1从用户user1回收：

SQL＞REVOKE role1 FROM user1；

11.3.4　角色信息的查询

角色的信息也存储在相关的数据字典中。与角色有关的数据字典有以下几个：

·dba_roles：记录数据库中的所有角色。

·dba_role_privs：记录所有被授予用户或另一角色的角色。

·user_role_privs：记录所有被授予当前用户的角色。

·role_role_privs：记录一个角色中包含的其他角色。

·role_sys_privs：记录一个角色中包含的系统权限。

·role_tab_privs：记录一个角色中包含的对象权限。

·session_roles：记录当前会话中所使用的角色。

例如，下面的语句得到系统中目前所有的角色：

SQL＞SELECT role FROM dba_roles；

下面的语句用于查询用户USER1所拥有的角色和默认角色：

SQL＞SELECT granted_role, default_role FROM dba_role_privs WHERE grantee='USER1'；

要查询角色CONNECT中所包含的系统权限，可以执行下面的查询语句：

SQL＞SELECT privilege, admin_option FROM role_sys_privs WHERE role='CONNECT'；

11.4　PROFILE的管理

PROFILE是一种数据库对象，它的功能是对用户使用口令的情况进行控制，或者对用户消耗系统资源的情况进行控制。在默认情况下，在数据库中对用户使用口令或者消耗资源是没有什么限制的，或者限制很小。

在一个数据库中，可能因为一个用户无意中执行了包含死循环的PL/SQL程序，或者一个带有恶意的用户对一个数百GB的大表进行全表检索，这些行为虽然不能破坏数据，但是会使数据库系统的性能急剧降低。数据库管理员如果及时发现了这种情况，可以强制终止用户会话，然后将带有恶意的用户锁定。例如，下面的三条语句分别用户查询用户的会话信息、强制终止用户会话、锁定用户账号（其中144和7分别为用户会话的sid和serial#）：

SQL＞SELECT username, sid, serial#FROM v$session WHERE username='SCOTT'；

SQL＞ALTER SYSTEM KILL SESSION'144，7'；

SQL＞ALTER USER scott ACCOUNT LOCK；

如果在数据库中创建了PROFILE，通过PROFILE可以采取主动防御的措施，对用户消耗系统资源的情况进行控制，这样就可以避免很多严重后果的产生。

11.4.1　PROFILE的创建与删除

为了能够使用PROFILE，首先需要以system用户的身份执行脚本pupbld.sql。这个脚本位于Oracle安装目录下的sqlplus/admin子目录中。例如：

SQL＞@$ORACLE_HOME/sqlplus/admin/pupbld.sql

其中$ORACLE_HOME是对环境变量ORACLE_HOME的引用，它代表Oracle的安装目录。在Windows系统中，应该以下面的方式执行这个脚本：

SQL＞@%ORACLE_HOME%\sqlplus\admin\pupbld.sql

在数据库中需要创建PROFILE这种数据库对象，在PROFILE中指定对用户所施加的各种限制，并将这个PROFILE指定给用户，这样用户的口令以及对资源的消耗就会受到限制。PROFILE被创建，可以指定给多个用户。

创建PROFILE的操作一般由SYS用户完成，如果普通用户希望创建PROFILE，需要具有CREATE PROFILE系统权限。创建PROFILE的命令格式为：

CREATE PROFILE profile名称LIMIT

口令限制

资源限制

在创建用户时，可以为其指定PROFILE。对于一个已经存在的用户，也可以为其指定PROFILE。例如：

SQL＞CREATE USER user2 identified by"1234"

PROFILE p1；

SQL＞ALTER USER scott

PROFILE p1；

为每个用户只能指定一个PROFILE。从数据字典视图dba_users中可以获得为每个用户指定的PROFILE。例如：

SQL＞SELECT profile FROM dba_users

WHERE username='SCOTT'；

在一个PROFILE中所包含的各种限制可以从数据字典视图dba_profiles中获得。例如，下面的命令用户查询P1对用户使用口令及使用资源进行的限制：

SQL＞SELECT resource_name, limit

FROM dba_profiles

WHERE PROFILE='P1'；

上述语句的执行结果类似如下：

CPU_PER_CALL 10

LOGICAL_READS_PER_CALL 50

SESSIONS_PER_USER DEFAULT

CPU_PER_SESSION DEFAULT

LOGICAL_READS_PER_SESSION DEFAULT

……

从上述命令的执行结果可以看出，除了明确设置的限制外，PROFILE中所包含的大部分限制都采用了默认值。

删除PROFILE的命令是DROP PROFILE。如果一个PROFILE已经被指定给了某个用户，那么在删除PROFILE需要指定CASCADE关键字。例如：

SQL＞DROP PROFILE p1 CASCADE；

11.4.2　如何利用PROFILE对用户口令进行控制

对用户口令的限制主要涉及口令的使用期限，在一个PROFILE中可以指定多种限制。下面列出了对用户口令可以施加的限制。

·FAILED_LOGIN_ATTEMPTS：允许的失败登录次数。如果用户的失败登录达到指定的次数，用户账号将被锁定。这个参数的默认值为10。

·PASSWORD_LOCK_TIME：账号锁定时间。当用户失败的登录达到指定的次数后，用户账号将被锁定指定的天数。这个参数的默认值为1天。

·PASSWORD_LIFE_TIME：口令的有效期。当用户口令到达有效期后，必须进行修改，如果不修改，用户账号将被锁定。这个参数的默认值为180天。

·PASSWORD_GRACE_TIME：口令有效期的延长期。对口令的有效期可以指定延长期。在延长期内用户仍然可以登录，但是每次登录都将接到警告信息。如果用户在延长期内没有修改口令，用户账号将被锁定。这个参数的默认值为7天。

·PASSWORD_REUSE_TIME：为了再次使用过去用过的口令，必须经过的天数。

·PASSWORD_REUSE_MAX：为了再次使用过去用过的口令，必须使用不同口令的次数。

其中最后两个参数是一起使用的。如果为两个参数都指定了具体的值，那么在指定的天数内，用户的口令必须被修改过指定的次数后，才能使用过去用过的口令。如果为其中一个参数指定了具体值，而将另外一个参数值指定为UNLIMITED，那么用户将永远不能使用过去用过的口令。如果把两个参数都设置为UNLIMITED，那么对用户的口令没有限制。

例如，下面创建的PROFILE对用户的失败登录次数进行控制，如果用户失败的登录次数达到3次，这个用户账号将被锁定5天。

SQL＞CREATE PROFILE p1 LIMIT

FAILED_LOGIN_ATTEMPTS 3

PASSWORD_LOCK_TIME 5；

又如，下面创建的PROFILE对用户修改口令进行限制，要求用户在30天内必须把口令修改过5次后，才能使用过去用过的口令。

SQL＞CREATE PROFILE p2 LIMIT

PASSWORD_REUSE_TIME 30

PASSWORD_REUSE_MAX 5；

为了通过PROFILE对用户使用口令的情况进行控制，必须把PROFILE指定给某个用户。例如：

SQL＞ALTER USER scott PROFILE p1；

11.4.3　如何利用PROFILE对用户使用资源进行控制

对用户使用资源的限制主要包括对连接时间、CPU时间、数据块数量所进行的限制。可以从两个层次对用户使用系统资源进行限制，一个是会话级，另一个是调用级。其中会话级限制指的是，对用户在整个会话范围内所使用资源总和进行的限制。而调用级限制指的是对用户执行的每条命令所使用资源进行的限制。

在默认情况下，即使创建了PROFILE，而且也为用户指定了PROFILE，用户使用CPU的时间、可以访问的数据块还是不受限制的。为了使PROFILE对用户使用系统资源所做的限制起作用，需要在数据库中将初始化参数RESOURCE_LIMIT的值设置为TRUE。

SQL＞ALTER SYSTEM set resource_limit=TRUE；

下面列出了可以对用户的一个会话施加的限制：

·SESSIONS_PER_USER：一个用户所允许的并发会话的数目。

·CPU_PER_SESSION：用户在一个会话内所使用的CPU时间的总和，时间单位为0.01秒。·LOGICAL_READS_PER_SESSION：用户在一个会话内所能访问的数据块的数量（这个数量包括物理读和逻辑读的数量）。

·CONNECT_TIME：一个用户会话所能持续的时间，以分钟为单位。超过这个时间，会话将自动断开。

·IDLE_TIME：一个用户会话所允许的连续的空闲时间，以分钟为单位。超过这个时间，会话将自动断开。

·PRIVATE_SGA：如果数据库服务器的连接模式为共享模式，这个参数用来限制为一个用户会话所分配的SGA空间。

下面列出了可以对用户执行的每条命令施加的限制：

·CPU_PER_CALL：用户执行的每条命令所使用的CPU时间，时间单位为0.01秒。

·LOGICAL_READS_PER_CALL：用户执行的每条命令所能访问的数据块的数量。

如果能够对用户消耗CPU时间、访问数据块的数量等这些资源进行限制，那么就可以有效地防止诸如死循环的执行、访问一个大表中的所有数据这类情况的发生。

例如，下面创建的PROFILE用来对一个在会话范围内所使用的CPU时间、能够访问的数据块以及会话的持续时间进行限制：

SQL＞CREATE PROFILE p3 LIMIT

SESSIONS_PER_USER 100

LOGICAL_READS_PER_SESSION 500

CONNECT_TIME 5；

SQL＞CREATE PROFILE p4 LIMIT

CPU_PER_CALL 10

LOGICAL_READS_PER_CALL 50；

11.4.4　默认的PROFILE

在数据库中有一个默认PROFILE，名称为DEFAULT，如果没有为用户指定PROFILE，那么用户使用口令、使用系统资源是受这个默认PROFILE限制的。如果要对所有用户进行同样的限制，那么只要修改这个默认PROFILE中所包含的限制，并且不要为用户指定其他的PROFILE。

如果在一个PROFILE中没有对某个限制进行设置，那么这样的限制将采用默认值。默认值实际上就来自这个默认的PROFILE。

SQL＞SELECT resource_name, limit

FROM dba_profiles

WHERE PROFILE='DEFAULT'；

在以前版本的数据库中，默认PROFILE中包含的所有限制都采用UNLIMITED，即没有限制。在Oracle 11g的数据库中，对一部分限制进行了设置，其余限制仍然采用UNLIMITED。

第三部分　自动文件管理和自动存储管理

第12章　自动文件管理

Oracle 11g的一个突出的特点是增强了文件和存储的自动管理功能，这使得数据库管理员的工作更加轻松。

在创建数据库时，需要指定若干个数据文件、控制文件和重做日志文件的名称、存储位置、大小、是否自动扩展等信息，编写这样的代码对初学者来说，是比较困难的事情。借助于文件的自动管理机制，数据库管理员在创建、维护数据库时，不再需要直接指定这些信息，这些文件由Oracle数据库服务器自动指定，这样可以大大简化代码的编写，减少用户的输入。我们把这样的文件称为Oracle管理的文件（Oracle Managed File，简称为OMF）。

需要注意的是，虽然Oracle 11g的自动管理功能大大简化了数据库管理员的工作，但是以前传统的文件管理方法在Oracle 11g中同样适用。

自动文件管理功能简化了数据库管理员的工作，数据库管理员通过初始化参数来指定文件的位置，Oracle将自动创建、管理这些文件，并在不需要这些文件时将它们删除。文件的存储位置可以是本地的文件系统，也可以是ASM磁盘组。

在管理以下数据库结构时，数据库管理员可以使用自动文件管理功能：

·表空间

·重做日志文件

·控制文件

·归档日志文件

·RMAN备份文件

12.1　如何激活自动文件管理功能

在使用Oracle的自动文件管理时，需要设置以下初始化参数：

·DB_CREATE_FILE_DEST

·DB_CREATE_ONLINE_LOG_DEST_n

·DB_RECOVERY_FILE_DEST

其中初始化参数DB_CREATE_FILE_DEST用来指定一个文件系统目录或者磁盘组，作为表空间中数据文件的默认存储位置。在数据库中创建表空间时，如果没有指定存储位置，这些数据文件将在该目录中创建。这个目录也可以作为重做日志文件和控制文件的默认存储位置，如果没有指定初始化参数DB_CREATE_ONLINE_LOG_DEST_n。

初始化参数DB_CREATE_ONLINE_LOG_DEST_n用来为重做日志文件和控制文件指定默认位置，其中n为整数，变化范围1～5。通过这个参数可以最多为重做日志文件和控制文件指定5个存储位置。

初始化参数DB_RECOVERY_FILE_DEST用来为RMAN备份文件指定默认存储位置。如果没有通过其他方式为归档日志文件指定存储位置，它们也将存储在这个目录中。同时，如果没有设置初始化参数DB_CREATE_ONLINE_LOG_DEST_n，这个目录也作为重做日志文件和控制文件的一个存储位置。

以上三个初始化参数都是动态的。数据库管理员可以在参数文件中指定参数值，也可以在数据库运行过程中，通过命令ALTER SYSTEM或ALTER SESSION来修改它们的值。需要注意的是，当通过以上初始化参数指定文件的存储位置时，Oracle并不自动创建这些目录或磁盘组，数据库管理员需要手工创建相应的目录和磁盘组，并保证oracle用户对它们具有写权限。

如果在参数文件中为以上初始化参数指定了参数值，或者通过命令为它们指定了参数值，那么在数据库中创建相关文件时，可以不指定文件的任何信息，这时创建的文件即为Oracle管理的文件。

12.2　文件的命名规则

Oracle遵守一定的命名规则，确保数据库中的文件命名不会重复。在为文件命名时，Oracle遵守以下原则：

·数据库文件的名称明显地区别于操作系统中其他文件。

·一种类型文件的命名明显地区别于其他类型的文件。

·文件名称应该包含重要的属性信息。例如，数据文件名称中应该包含它所属的表空间。

例如，在Solaris系统中，表空间中数据文件的命名规则为：

存储位置/数据库名称/datafile/o1_mf_%t_%u_.dbf

其中存储位置由初始化参数DB_CREATE_FILE_DEST指定，数据库名称由初始化参数DB_UNIQUE_NAME指定，%t代表所属表空间的名称，%u为一个长度为8字符的唯一的字符串。假设在参数文件中设置了以下两个初始化参数：

DB_CREATE_FILE_DEST=/export/home/oracle/oradata

DB_UNIQUE_NAME=TEST

那么在该数据库中，属于表空间SYSTEM的一个数据文件的可能名称为：

/export/home/oracle/oradata/test/datafile/01_mf_system_lakib3y_.dbf

12.3　如何创建OMF数据库

在创建数据库时，可以利用Oracle的自动文件管理功能创建数据文件、控制文件和重做日志文件。

在创建数据库的同时，创建了SYSTEM表空间、SYSAUX表空间、UNDO表空间、临时表空间，同时还可以创建其他表空间。

对于SYSTEM和SYSAUX表空间，在创建数据库的语句中可以不使用DATAFILE和SYSAUX DATAFILE子句来指定它们所包含的数据文件。这时如果设置了初始化参数DB_CREATE_FILE_DEST，那么数据文件将在这个初始化参数指定的位置被自动创建。否则，Oracle将为这两个表空间分别创建一个数据文件，它们的大小、名称将取决于具体的操作系统平台。在后一种情况下，创建的数据文件不是OMF文件。

Oracle管理的数据文件的默认大小为100MB，并且可以自动扩展，每次扩展100M。当然，用户也可以为数据文件指定初始大小和扩展属性等重要信息。通过DATAFILE子句为SYSTEM和SYSAUX表空间指定相关属性，可以覆盖它们的默认属性。

对于UNDO表空间，可以不指定表空间对应的数据文件名，Oracle将根据初始化参数DB_CREATE_FILE_DEST指定的存储位置创建数据文件。如果没有设置这个初始化参数，创建数据库的语句将执行失败。

在创建数据库的CREATE DATABASE语句中，UNDO TABLESPACE子句也是可选的。如果没有指定这个子句，Oracle将激活自动UNDO管理，并且创建名称为SYS_UNDOTS的UNDO表空间，在该表空间包含一个20M的数据文件，这个数据文件的大小可以自动扩展，它的存储位置取决于初始化参数DB_CREATE_FILE_DEST。

对于临时表空间，可以不指定表空间对应的数据文件名称，这时Oracle将根据初始化参数DB_CREATE_FILE_DEST的值创建数据文件，它的默认大小为100MB，并且可以自动扩展。如果没有设置这个初始化参数，创建数据库的CREATE DATABASE语句将执行失败。

在创建数据库时可以不创建临时表空间，所以在CREATE DATABASE语句中，DEFAULT TEMPORARY TABLESPACE子句也是可选的。如果没有指定这个子句，将不创建临时表空间。

在创建数据库时，Oracle将根据初始化参数CONTROL_FILES的设置创建控制文件。如果没有设置这个初始化参数，Oracle将按照以下原则创建控制文件：

·根据初始化参数DB_CREATE_ONLINE_LOG_DEST_n的设置，在指定的存储位置创建控制文件。其中在第一个存储位置中创建的文件称为主控制文件。

·如果没有设置初始化参数DB_CREATE_ONLINE_LOG_DEST_n，但是设置了初始化参数DB_CREATE_FILE_DEST和DB_RECOVRY_FILE_DEST，那么Oracle将根据这两个参数的设置，在指定的两个目录中分别创建一个控制文件。

·如果仅仅设置了初始化参数DB_CREATE_FILE_DEST和DB_RECOVERY_FILE_DEST其中一个，那么Oracle将在指定的目录中创建一个控制文件。

·如果以上三个初始化参数以及CONTROL_FILES都没有设置，Oracle将根据操作系统的不同，在默认的位置至少创建一个控制文件。为了能够正常访问控制文件，在创建数据库后，数据库管理员应该在参数文件中手工添加初始化参数CONTROL_FILES的设置。在创建数据库的CREATE DATABASE语句中，可以不使用LOGFILE子句，Oracle将按照与创建控制文件相同的原则创建重做日志文件，每个重做日志文件的默认大小为100MB。

当然，也可以在创建重做日志文件时为其指定属性，如文件大小等。与传统的方法一样，可以使用LOGFILE子句，并指定相关的属性，但是不需要指定文件名。

在对所有需要的参数进行设置之后，就可以创建数据库了。例如，要借助于自动文件管理功能创建一个数据库，首先在参数文件中设置以下初始化参数：

DB_CREATE_FILE_DEST='/export/home/Oracle/oradata1'

DB_CREATE_ONLINE_LOG_DEST_1='/export/home/Oracle/oradata2'

DB_CREATE_ONLINE_LOG_DEST_2='/export/home/Oracle/oradata3'

这样可以通过一条非常简单的命令创建数据库：

SQL＞CREATE DATABASE test1；

在这条命令中，没有使用任何附加的子句，所以Oracle将按照默认的方式创建数据文件、控制文件和重做日志文件，这些文件都属于Oracle管理的文件。

下面的命令用于创建数据库test1，同时指定SYSTEM表空间的区管理方式为本地管理。如果不指定，SYSTEM表空间将是字典管理方式的。Oracle建议创建本地管理的表空间。

SQL＞CREATE DATABASE test1 EXTENT MANAGEMENT LOCAL；

现在我们再来分析一个例子。要借助于自动文件管理功能创建一个数据库，数据库文件的存储位置是根据初始化参数的设置来确定的，名称是自动产生的，而文件的大小可以根据需要自己指定。

首先在参数文件中按照以下方式设置初始化参数：

DB_CREATE_FILE_DEST='/export/home/Oracle/oradata1'

DB_CREATE_ONLINE_LOG_DEST_1='/export/home/Oracle/oradata2'

DB_CREATE_ONLINE_LOG_DEST_2='/export/home/Oracle/oradata3'

然后执行下面的语句创建数据库：

SQL＞CREATE DATABASE test2

DATAFILE SIZE 250M autoextend on next 10240K

SYSAUX DATAFILE SIZE 150M autoextend on next 10240K

UNDO TABLESPACE uts1 DATAFILE SIZE 20M autoextend on next 10240K

DEFAULT TEMPORARY TABLESPACE dts1 TEMPFILE SIZE 20M；

这条语句执行后，将创建另外一个数据库test2，为表空间SYSTEM和SYSAUX各创建了一个数据文件，大小分别是250MB和150MB，可自动扩展，下一个扩展的区的大小为1024KB。同时创建了一个UNDO表空间和一个临时表空间，数据文件的大小均为20MB。这四个数据文件的位置通过初始化参数DB_CREATE_FILE_DEST指定。

虽然在CREATE DATABASE语句中没有指定重做日志文件，但是因为设置了初始化参数DB_CREATE_ONLINE_LOG_DEST_n，所以在指定的两个目录中将分别创建一个重做日志文件，文件将采用默认的大小100MB。

控制文件的情况与重做日志文件类似，虽然没有设置初始化参数CONTROL_FILES，但是根据初始化参数DB_CREATE_ONLINE_LOG_DEST_n的设置，在指定的每个目录中分别创建一个控制文件。

12.4　如何创建OMF表空间

数据库创建后，我们还可以根据需要，在数据库中创建新的表空间，或者向一个已经存在的表空间中添加新的数据文件。这些数据文件可以是常规文件，也可以是Oracle管理的文件。

如果设置了初始化参数DB_CREATE_FILE_DEST，那么当执行CREATE TABLESAPCE语句创建表空间时，可以省略DATAFILE子句。如果使用了DATAFILE子句，可以不指定文件名。在这两种情况下，将创建Oracle管理的数据文件，数据文件的存储位置由初始化参数DB_CREATE_FILE_DEST指定，文件的默认大小为100MB，并且可以自动扩展。如果没有设置这个初始化参数，表空间将创建失败。可以在DATAFILE子句中为数据文件指定相关的属性，如初始大小、自动扩展等，文件名称可以省略。如果通过SIZE关键字为数据文件指定了初始大小，则必须使用AUTOEXTEND关键字指定它的自动扩展属性，否则该数据文件将没有自动扩展属性。

当执行ALTER TABLESPACE语句向表空间中添加数据文件时，可以省略ADD DATAFILE子句。如果使用了ADD DATAFILE子句，可以不指定文件名。添加数据文件的情况与创建新的表空间的情况类似。

初始化参数的值可以在参数文件中设置，也可以在数据库运行过程中动态修改。例如，考虑以下语句序列：

SQL＞ALTER SYSTEM SET DB_CREATE_FILE_DEST='/export/home/Oracle/oradata4'；

SQL＞CREATE TABLESPACE tbs1；

SQL＞CREATE TABLESPACE tbs2 DATAFILE SIZE 100M SIZE 50M AUTOEXTEND ON；

在上述语句中，首先通过ALTER SYSTEM语句修改初始化参数DB_CREATE_FILE_DEST的值，然后按照默认方式创建表空间tbs1，最后创建了表空间tbs2。在表空间tbs2中有两个数据文件，其中第一个数据文件的初始化大小为100MB，不能自动扩展。第二个数据文件的初始化大小为50MB，可以自动扩展。在创建以上两个表空间时没有指定数据文件的存储位置和名称，因此这些数据文件的存储位置均由初始化参数DB_CREATE_FILE_DEST指定，文件的名称按照Oracle的命名规则自动指定。

如果要向一个已经存在的表空间中添加新的数据文件，情况是类似的。例如，假设已经设置了初始化参数DB_CREATE_FILE_DEST，以下语句将向表空间tbs1中添加一个数据文件，它的初始化大小为50MB，可自动扩展，并且每次扩展1024KB，文件的最大大小为200M。

SQL＞ALTER TABLESPACE tbs1

ADD DATAFILE SIZE 50M AUTOEXTEND ON NEXT 1024K MAXSIZE 200M；

对于临时表空间和UNDO表空间，无论是创建新的表空间，还是向表空间中添加新数据文件，采用的方法与普通表空间类似，这里不再详细描述。

12.5　如何创建OMF控制文件

在数据库运行过程中，可以根据需要重新创建控制文件。控制文件的位置由初始化参数CONTROL_FILES指定。如果没有设置这个初始化参数，则由DB_CREATE_FILE_DEST等三个初始化参数决定。详细信息请参阅12.3节。

在重新创建控制文件时，需要指定数据库中的所有数据文件和重做日志文件的信息。如果这些文件是Oracle管理的文件，那么它们的名称是由Oracle指定的，它们的大小可能是默认的，也可能是用户指定的。所以应该注意，在CREATE CONTROLFILE语句中，文件的相关信息要与实际情况保持一致。

创建控制文件的CREATE CONTROLFILE语句格式与传统方法所使用的格式基本相同，这里不再赘述。

12.6　如何创建OMF重做日志文件

在向数据库中添加一个新的日志组，或者向一个日志组中添加一个新的日志成员时，可以不指定文件的存储位置和名称。重做日志文件的位置由DB_CREATE_FILE_DEST、DB_RECOVERY_FILE_DEST和DB_CREATE_ONLINE_LOG_DEST_n三个初始化参数共同确定。详细信息请参阅12.3节。

在默认的情况下，新创建一个日志组时，日志组中每个日志成员的大小为100M，然而用户可以根据需要指定它的大小。当向一个已经存在的日志组中添加新的日志成员时，它的大小与该日志组中其他成员的大小一致。

例如，假设在参数文件中设置了以下初始化参数：

DB_CREATE_ONLINE_LOG_DEST_1='/export/home/Oracle/redo1'

DB_CREATE_ONLINE_LOG_DEST_2='/export/home/Oracle/redo2'

DB_CREATE_ONLINE_LOG_DEST_3='/export/home/Oracle/redo3'

然后执行下面的语句创建新的一个日志组：

SQL＞ALTER DATABASE ADD LOGFILE SIZE 50M；

那么这个日志组中将有3个日志成员，分别存放在由初始化参数DB_CREATE_ONLINE_LOG_DEST_n指定的三个目录中，它们的大小为50MB。

第13章　自动存储管理

自动存储管理（Automatic Storage Management, ASM）是Oracle内置的卷管理器，是对自动文件管理功能的扩充，它进一步简化了数据库管理的工作。如果仅仅使用自动文件管理功能，那么数据库文件都将存储在一些操作系统目录中。自动存储管理提供了卷管理功能，数据库管理员可以创建磁盘组，把多个磁盘以磁盘组的形式组织在一起，数据库中的文件将存储在磁盘组中。利用自动存储管理功能，还可以实现磁盘的镜像和条带状划分，从而提高数据库系统的性能和数据的安全。

在数据库运行的过程中，如果要创建表空间、重做日志文件、控制文件，可以把磁盘组指定为文件的存储位置，文件的名称和大小可以省略，Oracle将利用自动文件管理功能创建并且管理这些文件。

从Oracle 11g开始，还提供了一个集群文件系统（ASM Cluster File System, ACFS），在这种文件系统中，可以存储Oracle软件的可执行文件、数据库的跟踪文件、警告文件、视频、音频等类型的文件。需要注意的是，在ACFS中目前还不能存储数据库文件，所有能存储在ASM磁盘组中的文件都不能存储在ACFS中。

ASM、ACFS以及OMF是Oracle强力推荐使用的文件存储机制，尽管在单机版数据库中用户不一定会使用这些技术，但是RAC环境中，它们的应用非常多。通过这些技术，Oracle向用户提供了一个统一的存储数据库文件的方法，这样在不同的操作系统中配置RAC环境时，不再完全依赖操作系统所提供的存储方法。

为了使用Oracle 11g的ASM和ACFS，需要一个ASM实例。这个实例与数据库实例是相互独立的，在启动ASM实例时并不需要启动数据库实例，但是在启动数据库实例时，ASM实例必须已经启动起来。

13.1　ASM实例

为了在数据库中使用ASM，需要安装Grid Infrastructure软件，然后创建一个ASM实例，并在ASM实例下创建磁盘组。数据库中的数据文件、重做日志文件、控制文件、服务器参数文件等类型的文件就存储在ASM磁盘组中。ASM实例可以利用命令行、EM、ASMCA工具等来创建。

如果通过命令行创建ASM实例，就需要为它创建一个参数文件。参数文件的用法与数据库实例类似，在参数文件中只需要设置少数几个初始化参数。实例启动起来之后，就可以通过SQL*Plus登录，然后通过CREATE DISKGROUP命令创建磁盘组。表13.1列出了与ASM实例相关的初始化参数。

利用ASMCA使用工具可以很方便地创建ASM实例，同时创建ASM磁盘组。在创建ASM实例时，需要为SYS用户和ASMSNMP用户指定口令，如图13.1所示。

图　13.1　为ASM实例指定用户口令

在图13.1中单击“指定磁盘组”按钮，可以创建一个磁盘组，如图13.2所示。需要为磁盘组指定名称、冗余级别以及磁盘组中所包含的磁盘，单击“OK”按钮，磁盘组就可创建成功。以后还可以利用ASMCA工具创建别的磁盘组。

图　13.2　磁盘组的创建

在创建磁盘组时，要确保安装Grid Infrastructure软件的用户对磁盘组中的所有磁盘具有写权限。在UNIX/Linux系统中安装Oracle软件的方法请参阅附录B。假设安装这个软件的用户是oracle，那么下面的命令用于在AIX系统中设置磁盘的权限（root用户执行）：

#chown oracle：asmadmin/dev/rhdisk1

#chmod 660/dev/rhdisk1

ASM实例建立后，就可以启动它了。ASM实例的启动和关闭方法与数据库实例类似，利用SQL*Plus登录，通过执行STARTUP命令，即可启动ASM实例。同样，执行SHUTDOWN命令，即可关闭ASM实例。ASM实例也会读自己的参数文件。ASM实例的名称默认为+ASM，参数文件的默认名称为spfile+ASM.ora，存储在默认位置。

Oracle还提供了实用工具SRVCTL，利用这个工具可以对ASM实例进行控制。常用以下三条命令：

·srvctl status asm：ASM实例的状态。

·svrctl stop asm：关闭ASM实例。

·svrctl start asm：启动ASM实例。

13.2　磁盘组的管理

磁盘组是指一组磁盘，Oracle通过ASM技术将多个磁盘组织在一起，使它们作为一个整体向数据库提供存储空间，数据库中的大部分类型的文件都可以存储在磁盘组上，就像使用一个单独的磁盘一样。

在数据库运行时，数据将平均分布在磁盘组的各个磁盘上。数据库管理员可以在不关闭数据库的情况下向磁盘组中添加新磁盘，或者将磁盘从磁盘组中删除。在添加或删除磁盘时，自动存储管理将重新平衡磁盘，即将数据重新平均分布在各个磁盘中。

为了获得最佳性能，在创建磁盘组时，应该将大小、性能相同或者相近的磁盘放在一个磁盘组中。如果磁盘的大小或性能差别较大，应该将它们放在不同的磁盘组中。

磁盘组的管理涉及磁盘组的创建与删除、磁盘的添加和删除、磁盘组的加载和卸载等内容。在这里仅介绍如何通过SQL*Plus工具对磁盘组进行管理。

创建磁盘组时，需要为磁盘组起一个名字，还要指定磁盘组中的磁盘。为了防止磁盘组出现故障，可以为磁盘组指定“失败组”，即镜像组，同时指定磁盘的冗余级别。普通冗余至少需要两个磁盘组互为镜像，高冗余至少需要三个互为镜像的磁盘组。实际上，由于这些磁盘组互为镜像，所以我们可以把它们都成为“失败组”。如果使用磁盘阵列中的RAID盘，那么指定外部冗余就可以了。

创建磁盘组的命令是CREATE DISKGROUP。用户以sys身份，通过操作系统验证的方式登录ASM实例，然后将ASM实例启动到NOMOUNT状态。例如，以下语句用于创建磁盘组dg1：

$export ORACLE_SID=+ASM

$sqlplus/as sysasm

SQL＞CREATE DISKGROUP dg1 NORMAL REDUNDANCY

FAILGROUP fg1 DISK

'/dev/rhdisk2'，

'/dev/rhdisk3'，

FAILGROUP fg2 DISK

'/dev/rhdisk4'，

'/dev/rhdisk5'；

在创建磁盘组dg1时，为它指定了普通冗余级别，并指定了一个“失败组”，磁盘组和失败组互为镜像，它们分别包含两个磁盘。磁盘组创建成功后，磁盘即被格式化为ASM磁盘。

如果使用磁盘阵列中的RAID盘，就不需要为磁盘组指定镜像了。

删除磁盘组的操作很简单。删除磁盘组时，磁盘组上的所有文件将一起被删除。为了删除磁盘组，ASM实例必须启动，磁盘组必须被加载，并且磁盘组上的所有文件都必须关闭。

删除磁盘组的命令是DROP DISKGROUP。例如，下面的语句将用于删除磁盘组dg1：

SQL＞DROP DISKGROUP dg1；

如果磁盘组中包含文件，上述命令将执行失效。为了将磁盘组和文件一起删除，还必须使用DROP DISKGROUP命令的INCLUDING CONTENTS子句。例如：

SQL＞DROP DISKGROUP dg1 INCLUDING CONTENTS；

1.磁盘的添加和删除

磁盘组被创建以后，用户可以在不关闭数据库的情况下向磁盘组中添加磁盘，或者删除磁盘组中的磁盘。无论是哪种操作，ASM实例都将对磁盘组进行重新平衡。例如，下面的语句将向磁盘组中添加两个磁盘：

SQL＞ALTER DISKGROUP dg1

ADD DISK

'/dev/rhdisk6'NAME disk6，

'/dev/rhdisk7'NAME disk7；

由于没有指定磁盘所属的失败组，所以Oracle自动确定磁盘属于哪个失败组。如果要明确地将磁盘添加到某个失败组，则需要在ADD关键字之后指定FAILGROUP关键字及失败组的名称。

删除磁盘的方法是使用ALTER DISKGROUP命令及DROP DISK子句。可以一次删除一个磁盘，或者删除一个失败组中的所有磁盘。删除磁盘时，磁盘组要进行重新平衡，被删除磁盘上的内容被重新平均分布到其他磁盘上。

下面的语句将删除磁盘组DG1中的磁盘DISK1：

SQL＞ALTER DISKGROUP dg1 DROP DISK disk1

下面的语句用于删除失败组FG2中的所有磁盘：

SQL＞ALTER DISKGROUP dg1

DROP DISKS IN FAILGROUP fg2；

删除磁盘的操作需要较长的一段时间。在删除操作完成之前，可以取消这次操作。但如果删除操作已经完成，或者在删除命令中使用FORCE关键字进行强行删除，磁盘上的内容将无法恢复。例如，下面的语句用于取消删除磁盘的操作：

SQL＞ALTER DISKGROUP dg1 UNDROP DISKS；

2.磁盘组信息的查询

在ASM实例中，可以从动态性能视图中查看磁盘组磁盘的信息。从视图V$ASM_DISKGROUP中可以查询磁盘组的信息。例如，下面的SELECT语句用于查看所有磁盘组的名称、状态、冗余级别、总的空间大小、剩余空间大小：

SQL＞SELECT name, state, type, total_mb, free_mb

from v$asm_diskgroup；

从视图v$asm_disk中可以查询磁盘的信息，无论这个磁盘是属于某个磁盘组，还是一个空闲的磁盘。例如，下面的SELECT语句用于查询所有磁盘的所属磁盘组号、操作系统中的路径、状态、总的存储空间、剩余存储空间：

SQL＞select group_number, path, state, total_mb, free_mb

from v$asm_disk；

3.磁盘组的重新平衡

当磁盘组中的磁盘数目发生改变时，ASM实例将对其自动进行一次重新平衡，将磁盘组中的内容重新平均分布到现有的各个磁盘上。用户也可以手工对磁盘组进行平衡，目的是为了获得不同的平衡速度。

磁盘组的平衡能力从0到11共分为12级，其中0表示停止平衡操作，11表示速度最快的平衡操作。在手工进行平衡操作时，可以指定平衡的级别。当然，指定的级别越高，消耗的系统资源就越多。初始化参数ASM_POWER_LIMIT限制了可以使用的最高平衡级别。如果在进行平衡操作时指定的级别高于初始化参数ASM_POWER_LIMIT的值，是不起任何作用的。

例如，下面的语句用于对磁盘组dg1进行平衡操作。在语句中通过POWER关键字指定平衡的级别为3。

SQL＞ALTER DISKGROUP dg1 REBALANCE POWER 3；

4.磁盘组的加载和卸载

为了能够访问磁盘组中的文件，必须在ASM实例启动时加载该磁盘组。初始化参数ASM_DISKGROUPS用于指定自动加载的磁盘组。当关闭ASM实例时，磁盘组被自动卸载。在刚刚创建一个磁盘组时，用户需要手工加载或卸载磁盘组。下面的语句用于手工加载磁盘组dg1：

SQL＞ALTER DISKGROUP dg1 MOUNT；

如果希望加载所有的磁盘组，用ALL关键字代替上述命令中的磁盘组名称即可。用同样的方法可以卸载一个磁盘组或所有的磁盘组。下面的语句用于卸载磁盘组dg1：

SQL＞ALTER DISKGROUP dg1 DISMOUNT；

如果磁盘组上的某些文件处于打开状态，卸载操作将失败。这时可以在上述语句的最后加上FORCE关键字，强制卸载磁盘组。

5.目录管理

在磁盘组中包含一套完整的目录层次，这些目录在创建磁盘组时自动产生，数据库文件就存储在这些目录中。每个文件的名称是由Oracle自动产生的，称为“系统别名”。用户也可以为数据库文件指定用户别名，并将它们存储在用户创建的目录中。

如果用户希望为数据文件指定用户别名，首先需要创建一系列目录，每个目录以“+”符号和磁盘组的名称开始，并包括它的完整路径。在创建任何一个目录时，它的上层目录必须已经存在。例如，下面的语句分别在磁盘组dg1上创建目录dir1和它的子目录dir2：

SQL＞ALTER DISKGROUP dg1 ADD DIRECTORY'+dg1/dir1'；

SQL＞ALTER DISKGROUP dg1 ADD DIRECTORY'+dg1/dir1/dir2'；

对于一个已经存在的目录，可以对其进行重命名。对于磁盘组上一开始就存在的目录（即由Oracle自动产生的目录），不能修改它们的名称。例如，下面的语句用于把目录dir2改为dir22：

SQL＞ALTER DISKGROUP dg1 RENAME DIRECTORY'+dg1/dir1/dir2'

TO'+dg1/dir1/dir22'；

如果一个目录不再需要，可以将其删除。如果目录中包含文件，那么在删除目录时需要使用FORCE关键字，将该目录强制删除。对于磁盘组上一开始就存在的目录，不能对其进行删除操作。例如：

SQL＞ALTER DISKGROUP dg1 DROP DIRECTORY'+dg1/dir1'FORCE；

6.别名管理

在磁盘组上创建数据库文件时，Oracle自动为每个文件指定一个系统别名。由于系统别名的命名规则比较复杂，用户可能希望为每个文件指定一个用户别名。在添加用户别名时，需要指定该别名的完整路径。例如，下面的语句用于为数据文件emp.421.3指定别名emp.dbf：

SQL＞ALTER DISKGROUP dg1 ADD ALIAS'+dg1/dir1/dir22/emp.dbf'

FOR'+dg2/orcl/datafile/emp.421.3'；

对于一个已经存在的用户别名，我们可以修改它的名称和存储位置。例如：

SQL＞ALTER DISKGROUP dg1 RENAME ALIAS'+dg1/dir1/dir22/emp.dbf'

TO'+dg1/dir2/dir2/emp1.dbf'；

当一个用户别名不再需要时，可以将其删除。删除用户别名时不会影响原来的数据库文件。例如：

SQL＞ALTER DISKGROUP dg1

DELETE ALIAS'+dg1/dir2/dir2/emp1.dbf'

系统别名是数据库服务器为了引用数据库文件为其指定的名称，这种文件的管理是自动的，不需要用户的干预。用户别名是为了方便用户引用数据库文件而添加的名称，这种文件需要用户手工进行管理。

13.3　如何使用ASM磁盘组

Oracle引入ASM的目的是为了简化数据库的管理工作。用户在创建数据库文件时，可以将磁盘组作为文件的默认存储位置，这样便省去了指定文件名称、文件大小等信息的诸多麻烦，从而大大减轻了数据库管理员的负担。

当利用ASM管理数据库文件时，这些文件将存储在磁盘组的磁盘中。磁盘组中的文件对操作系统是不可见的，用户只能通过Oracle提供的实用工具查看这些文件。

数据库中的大部分文件类型都可以利用自动存储管理功能进行管理，只有少数类型的文件如警告文件、跟踪文件、审计文件、备份文件等，不能存储在磁盘组中。

1.创建数据文件

数据文件是属于某个表空间的，所以创建数据文件的情况一般有两种，一种情况是在创建表空间的同时创建数据文件，另一种情况是向一个已经存在的表空间中添加数据文件。如果要在某个磁盘组上创建数据文件，首先需要通过初始化参数将该磁盘组指定为数据文件的默认存储位置。例如：

DB_CREATE_FILE_DEST='+dg1'

然后通过命令向一个表空间中添加数据文件，或者创建一个全新的表空间。例如：

SQL＞CREATE TABLESPACE ts1；

这样就在数据库中创建了一个表空间ts1，表空间所对应的数据文件存储在磁盘组dg1上，数据文件的大小和扩展属性都采用了默认的信息。

如果在创建数据文件时不希望采用默认信息，那么可以指定文件大小和扩展属性等信息。例如：

SQL＞CREATE TABLESPACE ts2 DATAFILE'+dg1'SIZE 50M AUTOEXTEND ON；

2.添加重做日志文件

在一个日志组中可以有多个日志成员，这些日志成员可以位于不同的磁盘组上。在添加新的日志组时，可以通过初始化参数指定多个日志成员，也可以直接在命令中指定。假设有以下初始化参数设置：

DB_CREATE_ONLINE_LOG_DEST_1='+dg1'

DB_CREATE_ONLINE_LOG_DEST_2='+dg2'

DB_CREATE_ONLINE_LOG_DEST_2='+dg3'

那么通过下面的语句可以添加一个日志组，在这个日志组中有三个日志成员，它们分别位于磁盘组dg1、dg2和dg3上。

SQL＞ALTER DATABASE ADD LOGFILE；

3.创建数据库

利用自动存储管理功能，我们可以通过最简单的语句创建一个数据库，不再需要指定数据文件、重做日志文件和控制文件等信息。假设有以下初始化参数设置：

DB_CREATE_FILE_DEST='+dg1'

DB_RECOVERY_FILE_DEST='+dg2'

DB_RECOVERY_FILE_DEST_SIZE=500M

那么我们可以通过下面的语句创建数据库test：

SQL＞CREATE DATABASE test；

这条语句执行的结果是：创建了SYSTEM和SYSAUX表空间，对应的数据文件存储在磁盘组dg1上；创建了一个重做日志组，它包含两个日志成员，分别位于磁盘组dg1和dg2上；如果在数据库实例的参数文件中没有设置初始化参数CONTROL_FILES，那么将创建两个控制文件，它们分别位于磁盘组dg1和dg2上。上述文件均属于Oracle自动管理的文件。

第四部分　备份与恢复

第14章　数据库的导入与导出

导入与导出是Oracle提供的实用工具，利用这种工具，可以在不同用户、不同数据库之间移植数据。导出意味着把数据库中的数据写入一个操作系统文件，导入意味着把导出文件中所包含的数据写入数据库。导出文件是一个二进制文件，只能通过导入工具读取它的内容。

导入与导出一般是成对使用的。在以下情况中，导入与导出是很好的解决方案：

·在不同的数据库之间移植数据。

·对表进行重新组织，如消除表中的碎片和链接。

·在不同的用户之间传输数据。

·对数据库进行一次逻辑备份。

·对表进行类型转换：比如把普通表转换为分区表，或者反过来。

利用导入与导出工具可以在不同的数据库之间移植数据，如不同硬件平台、不同操作系统、不同版本的数据库。比如，生产系统最初使用的是Windows系统中的单机版Oracle9i的数据库，现在要对数据库进行升级，将其移植到Linux系统中RAC环境下的Oracle 11g数据库中，先将数据从原来的数据库中导出，然后将其导入到已经建好的RAC数据库中。

利用导入与导出工具可以对表进行重新组织，消除表中的碎片和链接，从而提高数据库的访问性能。很多生产系统在投入使用一段时间之后，系统的性能逐渐降低，最终导致用户无法忍受。其中可能的原因之一就是数据库中的数据量越来越大，在表中产生了很多的存储碎片和链接。在这种情况下，先把相关的表导出，然后把这个表删除，最后导入该表，在导入的过程中，表中的数据重新排列，这样就消除了碎片和链接。如果把表放置在本地管理表空间中，就可以有效防止存储碎片。

如果希望把普通表转换为分区表，可以先把原来的表导出，然后按照要求创建分区表，把数据导入到分区表中，最后把原来的表删除。把分区表转换为普通表的方法是类似的。

在生产系统出现性能问题时，很多工程师把解决问题的重点放在如何提高CPU、内存的访问性能上。实践证明，CPU、内存的调整空间是很有限的，更换速度更快的CPU、增加新的内存，这种可能性并不大，而且这种调整将是无休止的。而对系统磁盘I/O的调整空间则是很大的，调整的效果也是很明显的，例如，在表空间中创建多个数据文件，并把它们放置在不同的磁盘上，或者放置在磁盘阵列的RAID盘上；创建分区表；把表放置在本地管理的表空间上；在表上创建合适的索引；对表进行重新组织等。

14.1　导入导出工具的用法

利用导入与导出工具，可以以不同的模式对数据进行导入导出。在不同的场合，可以选择不同的导入导出模式。Oracle提供了以下几种导入导出模式：

·对单个表进行导入导出：包括表的结构和数据，以及表上的权限、约束、触发器等。

·对单个用户进行导入导出：包括这个用户拥有的所有数据库对象。

·对表空间进行导入导出。

·对整个数据库进行导入导出：导出数据库中除sys用户之外其他用户的数据库对象。

在导出数据时，可以使用常规路径导出和直接路径导出两种方式。常规路径导出方式通过SELECT命令将数据读到数据高速缓存中，经过格式转换后再写入导出文件。而直接路径导出方式在把数据读入数据高速缓存时，就采用与导出文件相同的格式，所以不需要经过格式转换，直接把数据从数据高速缓存写入导出文件。因此，在导出大量数据时，采用直接路径导出要比采用常规路径导出方式快很多。

导入导出工具实际上是两个可执行文件，即在操作系统中执行的命令行工具。利用exp命令对数据进行导出，利用imp命令对数据进行导入。这两个命令的格式是相同的。命令格式如下：

exp用户名/口令参数=参数值

其中用户名是能够登录数据库的有效用户，它在数据库中需要具有相关的权限。在这两条命令中还要使用若干个参数。在操作系统中以下面的方式执行这两条命，可以分别获得这两条命令的详细用法：

$exp help=y

……

USERID username/password FULL export entire file（N）

BUFFER size of data buffer OWNER list of owner usernames

FILE output files（EXPDAT.DMP）TABLES list of table names

……

从命令的执行结果可以看出，在exp和imp中可以使用若干个参数，其中有些参数有默认值，在参数后面的圆括号中指定。如果不特别指定，在导入或导出数据时就采用默认的参数值。下面列出了一些常用的参数：

·USERID　指定执行导入/导出操作的数据库用户，这个参数名称可以省略。

·FILE　指定导出所产生的文件，导入时要读这个文件。

·TABLES　指定要导入/导出的表。

·OWNER　指定要导入/导出的用户。

·BUFFER　以字节为单位，指定缓冲区的大小。缓冲区越大，导入/导出的效率越高。

·FULL　指定是否要对整个数据库进行导入/导出。

·GRANTS　指定是否导出表上的权限。

·ROWS　指定是否只导入/导出表的结构和表上的对象，而不导入/导出表中的数据。

·DIRECT　指定是否使用直接路径导出方式。

·RECORDLENGTH　在以直接路径方式导出数据时，指定缓冲区的大小。

在imp/exp命令行中，如果需要指定多个参数，为了简化命令的书写，可以考虑把这些参数放在一个参数文件中，在命令行中只要指定这个参数文件即可。参数文件是一个文本文件，每行包含一个参数。需要注意的是，如果把用户名和口令也放在参数文件中，那么必须放在第一行，而且需要通过USERID参数指定。例如，下面是一个参数文件的例子，这个文件用于导出scott用户的两个表。

USERID=scott/tiger

FILE=/export/home/scott.exp

TABLES=（dept, emp）

DIRECT=N

假设这个文件存储在/export/home/oracle目录下，文件名为e1.txt，这样在进行导出时，相应的命令行就可以简化为下面的形式：

$exp PARFILE=/export/home/oracle/e1.txt

在imp/exp命令行中，需要指定有效的数据库用户。在有些情况下，需要以sys用户登录数据库，这时用户名和口令需要采用以下特殊的格式：

\'sys/password AS SYSDBA\'

如果希望对用户的口令进行保密，在命令行及参数文件中不要指定用户名和口令，在执行imp/exp命令时，命令将提示输入用户名和口令，这样口令就不会在屏幕上显示了。

在对数据进行导出时，将产生一个二进制的导出文件，这个文件无法通过一般的文本编辑器进行查看，但是可以通过imp命令显示这个文件的内容。例如：

$imp scott/tiger file=/export/home/scott.exp SHOW=y FULL=y

在执行这条命令后，大家会发现这个文件中只包含一些SQL语句。实际上，表的结构及表上的索引等对象，都导出为CREATE语句，而表中的数据导出为INSERT语句。在导入数据时，就是要把这些SQL语句执行一次。

14.2　表的导入与导出

普通用户可以导出自己的表，如果希望导出其他用户的表，必须在数据库中具有系统权限EXP_FULL_DATABASE，这个权限默认已经指定给所有的dba用户。例如，下面的语句导出scott用户的dept表和emp表：

$exp scott/tiger FILE=/home/oracle/scott1.exp TABLES=（dept, emp）

About to export specified tables via Conventional Path……

..exporting table DEPT 4 rows exported

..exporting table EMP 14 rows exported

Export terminated successfully without warnings.

下面的语句导出scott用户的dept表，但不导出表中的数据和索引：

$exp scott/tiger FILE=/home/oracle/scott2.exp TABLES=dept ROWS=N INDEXES=N

下面的语句导出分区表emp上的两个分区m和n：

$exp scott/tiger FILE=/home/oracle/scott3.exp TABLES=（emp：m, emp：n）

如果user1用户获得了EXP_FULL_DATABASE系统权限，就可以导出其他用户的表。例如，下面的语句以user1用户导出scott用户的dept表：

$exp user1/1234 FILE=/home/oracle/scott4.exp TABLES=scott.dept

导入数据意味着先通过CREATE语句创建表以及表上的索引、约束等对象，然后通过INSERT语句将数据写入表中。普通用户可以从自己导出的文件中导入自己的表。例如：

$imp scott/tiger FILE=/home/oracle/scott1.exp TABLES=（dept, emp）

如果要导入的表已经存在，导入操作将失败。所以在导入前，要确保在执行导入操作的用户的模式中，不存在这样的表。如果要向一个已经存在的表中导入数据，需要在imp命令中指定IGNORE参数的值为Y。还需要注意的是，表上的约束和触发器都可能导致导入失败，所以在导入前可以先删除表上的约束或触发器，或者将它们置为DISABLE状态。

即使一个表不属于某个用户，这个用户可以将这个表从导出文件中导入到自己的模式。通过这种方法，可以在不同用户之间传递数据。例如，通过下面的语句，user1从导出文件中将scott用户的dept表导入：

$imp user1/1234 FILE=/home/oracle/scott1.exp TABLES=（dept）

前面提到，一个用户为了能够导出其他用户的表，需要具有EXP_FULL_DATABASE系统权限。为了从这样的导出文件中导入一个表，imp命令中的用户也必须具有特殊的系统权限IMP_FULL_DATABASE，否则，即使是表的所有者，也不能从这个文件中导入表。例如，假设user1用户获得了系统权限IMP_FULL_DATABASE，下面的语句用于将scott用户的dept表从scott4.exp文件中导入到自己的模式中：

$imp user1/1234 FILE=/home/oracle/scott4.exp FROMUSER=scott TABLES=dept

假设希望在两个用户之间传输数据，但是执行导出和导入操作的是另外两个不相同的用户，这样在导入数据时经常会出现错误。在对数据库进行升级，或者在不同数据库之间移植数据时，这是经常困扰工程师的一个问题。在这里对这种复杂的关系进行一个简单的总结：

·一个用户在导出其他用户的表时，需要具有EXP_FULL_DATABASE系统权限。

·如果一个用户希望从这样的导出文件中导入一个表，需要具有IMP_FULL_DATABASE系统权限，而且需要通过参数FROMUSER指定表的所有者。

·如果一个用户希望从这样的导出文件将一个表导入到另外一个用户的模式中，需要具有IMP_FULL_DATABASE系统权限，而且需要通过参数FROMUSER指定表的所有者，通过参数TOUSER指定目标用户的名称。如果不指定TOUSER参数，则导入到当前用户自己的模式中。

例如，我们先以sys用户导出scott用户的dept表，该操作所对应的命令是：

$exp\'sys/1234 AS SYSDBA\'FILE=/home/oracle/scott5.exp TABLES=scott.dept

为了从文件scott5.exp中将表dept导入数据库，imp命令中的用户必须具有特殊的系统权限IMP_FULL_DATABASE。假设user1用户获得了IMP_FULL_DATABASE系统权限，下面的命令将scott用户的dept表导入到user2的模式中：

$imp user1/1234 FILE=/home/oracle/scott5.exp FROMUSER=scott

TABLES=dept TOUSER=user2

下面的命令将scott用户的dept表导入到scott的模式中：

$imp user1/1234 FILE=/home/oracle/scott5.exp FROMUSER=scott

TABLES=dept TOUSER=scott

14.3　用户模式的导入与导出

用户的导出意味着把一个用户所拥有的对象全部导出，如表、索引、存储过程等。在导入时，可以选择导入所有的对象，或者只导入部分对象。例如，下面的命令用于导出SCOTT用户的所有对象：

$exp scott/tiger FILE=scott6.exp OWNER=scott GRANTS=y ROWS=y

下面的语句用于从导出文件scott6.exp中导入表dept：

$imp scott/tiger FILE=scott6.exp TABLES=（DEPT）

一个用户可以从另外一个用户导出的文件中导入自己所需要的数据库对象。如果要在两个用户模式之间进行导入与导出，而执行导入与导出操作的是其他用户，那么同样需要注意系统权限IMP_FULL_DATABASE和EXP_FULL_DATABASE的问题。

14.4　数据库的导入与导出

数据库的导出意味着把数据库中除sys用户所拥有的对象之外的其他数据库对象全部导出。数据库的导入意味着把导出文件中的数据库对象全部导入。执行导出操作的用户需要具有EXP_FULL_DATABASE系统权限，同样，执行导入操作的用户需要具有系统权限IMP_FULL_DATABASE。例如，下面两条命令分别用于导出和导入整个数据库：

$exp\'sys/1234 AS SYSDBA\'FULL=y FILE=dba.exp GRANTS=y ROWS=y

$imp\'sys/1234 AS SYSDBA\'FULL=y IGNORE=y FILE=dba.exp

如果要导出数据库中所有的对象，而且这些对象分别属于不同的用户，放在不同的表空间中，那么导出整个数据库是最好的选择。这种方法也常常用来在不同版本、不同操作系统下的数据库之间移植数据。

14.5　表空间的导入与导出

表空间的导出指的是把表空间的结构和数据文件导出，表空间的导入就是把表空间的结构和数据文件写入数据库，这意味着在数据库中产生了一个新的表空间。利用表空间的导入与导出可以在不同的数据库之间移植一个表空间。在导出表空间时，不需要导出表空间中的表，所以这种方法比导出表空间中所有的表要快得多。

如果要在不同的数据库中移植数据，而这些数据都存储在同一个表空间中，那么最有效的方法就是对这个表空间进行导入与导出。首先在原来的数据库中导出这个表空间，把表空间的结构信息导出到一个文件中，这个文件称为元数据文件。然后把表空间中的数据文件和这个元数据文件一起复制到目标数据库中，然后根据元数据文件的内容把表空间导入到数据库中，这样表空间中所有的数据库对象都被导入到目标数据库中。

利用导入/导出方法移植表空间时，要求源数据库与目标数据库必须具有相同的字符集，在目标数据库中不能有同名的表空间，而且表空间要满足“自包含”的条件。“自包含”是指与当前表空间中的数据库对象相关的其他数据库对象，也要位于当前表空间中。当以下情况之一存在时，表空间不满足“自包含”的条件：

·在当前表空间中有SYS用户创建的表。

·在其他表空间中有一个表，这个表上的索引位于当前表空间中。

·某个分区表的一部分分区位于当前表空间中。

·当前表空间中的某个表上的LOB列位于其他表空间中。

·两个具有主键-外键关联关系的表分别位于当前表空间和另外一个表空间中。

在导出表空间之前，应该对它进行检查，看它是否满足“自包含”的条件。如果有上述情况之一存在，应该把相关的数据库对象移出或移入当前表空间。Oracle提供了一个程序包，利用这个包中存储过程对表空间进行检查。这个存储过程的执行格式如下：

SQL＞BEGIN

DBMS_TTS.TRANSPORT_SET_CHECK（ts_list=＞'USERS'，

incl_constraints=＞TRUE）；

END；

在这个存储过程中，第一个参数指定要检查的表空间名称，第二个参数的值为TRUE。检查结果可以从数据字典视图transport_set_violations中获得。例如，下面的检查结果表明，在当前表空间中存在sys用户创建的表：

SQL＞SELECT*FROM transport_set_violations；

VIOLATIONS

--------------------------------------------------------------------------------

ORA-39917：SYS owned object T1 in tablespace USERS not allowed in pluggable set

如果表空间满足“自包含”的条件，就可以对它进行导出操作了。导出的步骤如下：

1）在源数据库中将表空间置为READ ONLY状态。例如：

SQL＞ALTER TABLESPACE USERS READ ONLY；

2）利用EXP命令将表空间的结构信息导出到元数据文件中。

3）将表空间中的数据文件和元数据文件复制到目标数据库所在的系统中。

4）利用IMP命令将表空间导入到目标数据库中。

5）在源数据库中将表空间置为READ WRITE状态。例如：

SQL＞ALTER TABLESPACE USERS READ WRITE；

在导出表空间时，要求用户具有EXP_FULL_DATABASE系统权限，在导入表空间时，用户需要具有IMP_FULL_DATABASE系统权限。例如，下面两条命令分别用来在两个数据库中对表空间users进行导出和导入操作：

$exp\'sys/1234 AS SYSDBA\'FILE=users.exp\

TRANSPORT_TABLESPACE=Y TABLESPACES=users

$imp\'sys/1234 AS SYSDBA\'FILE=users.exp\

TRANSPORT_TABLESPACE=Y DATAFILES=（/home/oracle/users01.dbf）

在这两条命令中用到了另外三个参数，这三个参数仅仅在对表空间进行导入/导出时才使用。这三个参数的含义如下：

·TABLESPACES：指定要导出的表空间，如TABLESPACES=（users, data）。

·TRANSPORT_TABLESPACE：指定是否导入或导出表空间的元数据。

·DATAFILES：在导入表空间时指定表空间的数据文件。

利用导入/导出工具虽然可以在不同数据库之间移植表空间，但是Oracle建议使用Data pump工具[1]对表空间进行导入/导出。我们还可以在不同的操作系统平台下移植表空间，这时需要对数据文件的格式进行转换，这种转换要借助于RMAN工具才能完成。详细信息请参考第16章的相关内容。

[1]Data pump是Oracle 10g的新特性，用于导入/导出数据。——编辑注

第15章　数据库的常规备份与恢复

数据库的备份与恢复是系统容灾的重要方法。在一个生产系统中，数据库往往处于核心的地位。为了保证数据的安全，人们想出了各种各样的方法，比如创建RAC环境，配置异地容灾，使用磁盘阵列，备份与恢复等。

备份意味着把重要的数据复制到安全的存储介质上，恢复意味着在必要的时候再把以前备份的数据复制到最初的位置，以保证用户可以访问这样的数据。

Oracle主要提供了两种备份与恢复的方法，包括常规方法和RMAN，在这里我们重点掌握RMAN方法。虽然很多第三方软件如Tivoli，也可以用来对数据库进行备份与恢复，但是它们也是通过接口调用RMAN的功能。常规的备份与恢复方法虽然并不常用，但是对于读者理解一些重要的概念很有帮助。本章主要通过一些例子来介绍重要的概念。

15.1　备份与恢复的相关概念

为了对数据库进行备份与恢复，首先需要掌握一些重要概念。

15.1.1　冷备份与热备份

冷备份指的是把数据库关闭之后所进行的备份，而热备份指的是在数据库服务器正常运行的过程中所进行的备份。由于在进行冷备份时需要关闭数据库，所以会对用户的访问产生影响，而且很多重要的数据库都需要进行7×24访问。冷备份的好处是，由于在备份过程中用户无法修改数据库中的数据，数据库中的数据是一致的，所以在有些资料中也把这种备份方法称为一致的备份。热备份虽然能把对用户的影响减到最小，但是会造成数据的不一致，所以这种备份方法也称为不一致的备份。幸运的是，Oracle可以自己解决这样的不一致。

数据库有两种日志模式，即归档模式和非归档模式。如果数据库处于非归档模式，只能对其进行冷备份。如果数据库处于归档模式下，则可以对其进行冷备份或热备份。

15.1.2　物理备份与逻辑备份

物理备份指的是对数据库中相关文件所进行的备份，也就是说，把这些文件复制到备份介质上。逻辑备份一般指的是对数据进行导出，这时候是针对数据库中的表进行备份，把表中的数据以SQL命令的方式导出到文件中。

15.1.3　完全备份与增量备份

完全备份与增量备份针对的对象包括整个数据库、一个表空间，或者一个数据文件。完全备份意味着把整个数据库、或者一个表空间，或者一个数据文件中的数据全部备份下来，而增量备份则意味着把整个数据库、或者一个表空间，或者一个数据文件中在一段时间内被修改的数据备份下来，在这段时间内没有被修改的数据就不需要备份。一般来说，在一段时间内被修改的数据总是比较少的，这就意味着增量备份的数据量是比较小的。

完全备份与增量备份是通过备份级别来体现的。在Oracle中，把备份级别分为5种，其中级别0为完全备份，级别1～4为增量备份。在对数据库进行备份时，既要用到完全备份，也可能用到增量备份。

15.1.4　备份策略

由于完全备份需要把整个数据库、或者整个表空间，或者整个数据文件中的数据全部备份下来，所以这样备份需要花费很长的时间，要使用很大的磁盘空间，所以这种备份并不频繁进行，一般一周或一个月进行一次。增量备份虽然只需要对较少的数据进行备份，但是这种备份方法不能单独使用，必须以完全备份为基础。这样，我们在对数据库进行备份时，要综合考虑各种因素，然后制定一个合理的备份计划，这样的备份计划称为备份策略。

一个好的备份策略不但可以减轻数据库管理员的负担，还能把数据库的风险减到最小，一旦数据库出现故障，还可以把数据的损失减到最小。备份策略制定好之后，就通过各种方法来实现了，如通过操作系统中的任务调度。

图　15.1　一个备份策略的例子

图15.1反映的是一种常用的备份策略的例子。周日对数据库进行一次完全备份，周一到周四每天进行一次较高级别的备份。每次备份的内容都包含这样的数据：自最近一次较低级别的备份以来，被修改的数据，而在这段时间内没有被修改的数据就不会被备份。

假设发生了这样的情况：在周五的备份还没有来得及进行的时候，数据库出现了故障，造成数据的丢失。现在正是利用备份进行恢复的时候了。首先利用周日的0级备份进行恢复，然后依次利用周一到周四的备份进行恢复，这样周四之前的数据就完全恢复了。周四到周五之间的数据没有备份，只能依靠重做日志来恢复了。假设数据库的日志模式为归档模式，那么数据库是可以进行完全恢复的。如果数据库的日志模式为非归档模式，那么最后这一段时间的数据是无法恢复的。

图　15.2　另外一个备份策略的例子

图15.2反映的是另外一种备份策略的例子。与第一种备份策略不同的是，周四又进行了一次1级备份。这样周四备份的内容就包括这样的数据：自最近一次0级备份以来，被修改的数据。假设在周五的备份还没有来得及进行的时候，数据库发生了故障，造成了数据的丢失。在这种情况下，首先利用周日的0级备份进行恢复，然后利用周四的1级备份就能把数据库直接恢复到周四的状态，周四到周五之间的数据依靠日志进行恢复。

15.1.5　完全恢复与不完全恢复

恢复意味着在必要的时候，利用备份所产生的文件，重新产生数据库中的数据。完全恢复意味着把数据库恢复到数据库发生故障的时候。不完全恢复意味着只能把数据库恢复到过去某个时间点。例如，数据库发生故障后，最后一段时间的数据是没有被备份的，这段时间的数据只能依靠重做日志和归档日志来恢复。假设这段时间的日志没有保留完整，那么以缺失日志的地方为界，之后的数据将丢失，而之前的数据可以根据保留下来的日志进行恢复。

15.1.6　日志模式对备份与恢复的影响

从上面的描述可以看出，数据库的日志模式对备份与恢复有很大的影响。如果数据库处于非归档模式，备份与恢复有以下特点：

·只能对数据库进行冷备份。

·只能对整个数据库进行备份，不能针对某个表空间或数据文件进行备份，每次备份都需要备份数据库中的所有数据。

·在进行恢复时，只能恢复到最后一次备份点的状态，之后的数据将全部丢失。如果数据库处于归档模式，备份与恢复有以下特点：

·可以对数据库进行热备份，也可以进行冷备份。

·可以针对整个数据库，或者某个表空间，或者某个数据文件进行备份。

·可以对整个数据库，某个表空间，或者某个数据文件进行完全备份和增量备份。

·在进行恢复时，可以把数据库恢复到故障点的状态。

通过上面的比较可以看出，归档模式有更多的优点，所以生产系统的数据库都应该处于归档模式。如果目前数据库还处于非归档模式，应该立即切换到归档模式。

15.1.7　哪些情况将导致数据丢失

在数据库运行的过程中，很多情况都会导致数据的丢失，在不同情况下，恢复的方法也是不一样的。下面列出了几种典型情况。

1.语句失败

语句失败指的是用户的SQL语句没有执行成功，导致用户修改的数据没有写入数据库。例如，用户试图向表中写入错误类型的数据；用户写入的数据违反了表上的约束；用户执行了没有权限的访问；数据文件没有足够的存储空间且不能自动扩展；数据文件所在的磁盘没有足够的存储空间。

在这些情况下，数据库服务器将对事务进行回滚，保证这样的数据不能写入数据库。另一方面，应用程序的开发人员在开发应用程序时应该处理这种情况，保证把事务的处理结果通知用户。

2.用户进程失败

用户进程失败指的是访问数据库的客户机意外关机，或者客户端应用程序意外终止。无论是什么原因，实例中的PMON进程都将进行清理工作，把用户的事务进行回滚，把用户进程所占用的资源及锁全部释放。

3.用户错误

用户错误指的是由于误操作或者恶意的操作而导致数据的丢失。例如，不小心删除了某个重要的表，或者恶意清空了一个表中的数据。这时候需要人工进行恢复。可以采用下面方法：

·利用以前导出的数据恢复数据。

·对数据库进行基于时间点的恢复，把数据库恢复到丢失数据的时间点之前。

·利用Flashback进行恢复。

·利用回收站恢复被删除的表。

4.实例失败

实例失败指的是数据库服务器意外关闭的情况。在重新启动实例时，SMON后台进程将根据重做日志进行实例恢复，把所有没有提交的事务进行回滚。

假设用户执行了一个事务，但是还没有提交，那么这样的事务将被回滚。如果用户提交了这个事务，那么根据第4章中的介绍，这次事务修改的数据可能已经写入数据库，也可能没有，但是不管怎样，只要提交了，事务就是有效的。在重新启动实例时，SMON后台进程进行实例恢复，这样的数据将被写入数据库。

5.介质失败

介质失败指的是由于磁盘损坏、病毒感染，或者人为原因导致数据库中的文件无法访问的情况，这是管理员真正需要关心的情况。针对这种情况进行的恢复叫做介质恢复。在后面的章节中，主要介绍介质恢复。

15.1.8　哪些文件需要备份

在对数据库进行备份之前，应该进行详细的规划，确定哪些文件需要备份，哪些不需要备份，还要制定详细的备份策略，这样既能使备份工作变得简单，而且能最大程度地保证数据库的安全。

在数据库中，不同文件的备份要求是不一样的。数据文件是要重点备份的。其次，控制文件需要备份，在数据库投入使用的时候进行一次备份，以后只要数据库的结构有变化，就应该及时备份控制文件。参数文件需要备份一次。重做日志文件一般不需要备份。归档日志文件可以考虑进行备份，也可以对每个日志组产生多个归档日志文件，并且放在不同的磁盘上，这样就可以不对它们进行备份了。

15.2　控制文件的备份与恢复

在第8章中，我们已经介绍过控制文件的备份方法。对控制文件的备份可以采用两种方法。一种方法是产生一个二进制的映像文件，这个文件与实际的控制文件内容完全一致。另一种方法是在跟踪文件中产生一条CREATE CONTROLFILE语句。

一旦控制文件损坏，数据库将无法正常运行，这时需要把实例启动到NOMOUNT状态，把刚才产生的CREATE CONTROLFILE语句复制到SQL*Plus中并执行它，这样就重新产生了所有的控制文件，然后通过下面的命令打开数据库：

SQL＞ALTER DATABASE OPEN；

或者把产生的映像文件复制到控制文件以前所在的每个位置，并把文件名称更改为以前的名称，然后通过下面的命令打开数据库：

SQL＞ALTER DATABASE OPEN RESETLOGS；

15.3　重做日志文件的备份与恢复

重做日志文件一般不需要进行备份。一方面，通过多元化的方法可以保证文件的安全，另一方面，对重做日志文件进行归档，把以前的重做日志保留下来。

在数据库打开的情况下，如果某个重做日志文件损坏，那么由于这个文件的归档无法进行，最终将使数据库停止运行。这时需要清空这个文件，即在不关闭数据库的情况下，对重做日志文件所在的整个日志组重新进行初始化。例如：

SQL＞ALTER DATABASE CLEAR LOGFILE GROUP 3；

如果重做日志文件还没有进行归档，可以通过下面的命令清空这个文件：

SQL＞ALTER DATABASE CLEAR UNARCHIVED LOGFILE GROUP 3；

对重做日志文件进行清空后，由于这个日志组中记录的重做日志全部丢失，所以应该尽快对数据库进行一次备份。

15.4　数据文件的备份

数据文件属于表空间，对数据文件的备份就是对表空间的备份。然而不同表空间的备份要求是不一样的。而且在不同的日志模式下，备份的方法也是不一样的。

在非归档模式下，每次备份都需要备份整个数据库，不能备份单个的表空间或单个文件。由于生产系统的数据库一般都不是处于非归档模式下，所以对这种备份方法不再详细介绍。

在归档模式下，我们可以备份单个的表空间和数据文件。为了减少备份的工作量，我们对不同的表空间制定不同的备份方案。例如：

·SYSTEM和SYSAUX表空间：只需要备份一次。

·UNDO表空间：不需要备份。

·临时表空间：不需要备份。

·只读表空间：只需要备份一次。

·数据表空间：需要频繁备份。

其中在数据表空间存放的是生产系统的业务数据，这部分数据需要重点进行备份。对表空间进行备份的方法是：首先应该在数据字典视图dba_data_files中查询表空间中所包含的数据文件。然后在SQL*Plus中执行下面的命令：

SQL＞ALTER TABLESPACE users BEGIN BACKUP；

其中USERS是需要备份的表空间。然后再操作系统中将表空间中的数据文件复制到存储介质上，这要借助操作系统的复制命令或备份命令。最后，在SQL*Plus中执行下面的命令以结束备份：

SQL＞ALTER TABLESPACE users END BACKUP；

如果还需要备份其他表空间，按照同样的方法进行操作即可。

从上面的例子可以看出，传统的备份方法实际上是将数据文件转入一个一致状态之后对其进行的复制，这种方法容易理解，但缺点是需要在操作系统和数据库中来回切换，不容易实现数据库的自动备份。

15.5　数据库的完全恢复

如果数据库处于归档模式下，可以在实例的OPEN或者MOUNT状态下对数据库进行恢复。可以针对一个表空间、一个数据文件或者整个数据库进行恢复。如果数据文件损坏，数据库将无法正常运行，也无法重新启动实例。这时候需要将表空间或者数据文件转到OFFLINE状态，然后进行恢复。为了确定损坏的数据文件，可以采取下列方法：

·查询动态性能视图v$recover_file。

·查看警告文件或者DBWR后台进程的跟踪文件。

·在重新启动实例时查看SQL*Plus中的错误提示。

例如，下面的语句用于查询需要恢复的数据文件的编号、状态以及出错原因：

SQL＞SELECT file#，online_status, error FROM v$recover_file；

在确定出错的数据文件之后，需要把表空间或者数据文件的状态转为OFFLINE，然后进行恢复。例如：

SQL＞ALTER TABLESPACE users OFFLINE；

SQL＞ALTER DATABASE DATAFILE 2 OFFLINE；

其中语句中的数字是数据文件的编号，这个编号可以从视图dba_data_files中获得，这上述语句中，可以指定数据文件的编号，也可以指定完整的名称。

通常我们所说的恢复实际包括两个步骤，即RESTORE和RECOVER。其中RESTORE指的是把备份的文件复制到数据文件原来所在的位置，覆盖原来的数据文件，这样就把数据库恢复到最后一个备份的时间点了，这个步骤需要管理员手工完成。RECOVER指的是在RESTORE的基础上，利用重做日志重新产生最后一段时间的数据，这样所有的数据就恢复到发生故障的时间点了。

对于普通表空间，可以在实例处于OPEN状态或者MOUNT时进行恢复。我们建议在OPEN状态下对普通表空间的数据文件进行恢复。可以通过下列语句之一进行恢复：

SQL＞RECOVER TABLESPACE users；

SQL＞RECOVER DATAFILE 2；

SQL＞ALTER DATABASE RECOVER DATAFILE 2；

如果要对SYSTEM表空间或者整个数据库进行恢复，需要把实例转入MOUNT状态，这是因为在数据库正常运行时，无法把表空间SYSTEM转入OFFLINE状态。下面的语句用于在实例处于MOUNT状态时恢复整个数据库，或者一个数据文件，或者一个表空间：

SQL＞RECOVER DATABASE；

SQL＞RECOVER DATAFILE 1；

SQL＞RECOVER TABLESPACE system；

SQL＞ALTER DATABASE RECOVER DATABASE；

最后将数据库实例转入OPEN状态，数据库就可以正常访问了。

15.6　两个实际的备份与恢复的例子

在本节中，通过两个实际的例子，介绍整个备份与恢复的过程。第一个例子模拟数据文件损坏的情况，第二个例子模拟磁盘损坏的情况。

15.6.1　模拟数据文件损坏的例子

以下是模拟USRS表空间中数据文件损坏的例子：

1）对表空间USERS中的数据文件进行备份。

2）在表空间中创建一个表t1，然后向表中写入几行数据，提交事务。

3）为了模拟文件损坏，把数据库实例关闭，在操作系统中删除文件，或者重命名。

4）重新启动实例，这时候在SQL*Plus中将看到如下出错信息：

ORA-01157：cannot identify/lock data file 4-see DBWR trace file

ORA-01110：data file 4：'/u01/app/base/oradata/orcl/users01.dbf'

5）将数据文件转入OFFLINE状态，然后打开数据库：

SQL＞ALTER DATABASE DATAFILE 4 OFFLINE；

SQL＞ALTER DATABASE OPEN；

6）把备份文件复制到数据文件原来的位置。

7）执行RECOVER：

SQL＞RECOVER DATAFILE 4；

8）将数据文件转入ONLINE状态：

SQL＞ALTER DATABASE DATAFILE 4 ONLINE；

从上面的过程可以看出，表空间USERS中最初的数据是依靠备份来恢复的，而表t1中的数据没有备份，只能依靠日志来恢复。

15.6.2　模拟磁盘损坏的例子

如果磁盘损坏，只能把数据文件恢复到另外一个磁盘上。以下是模拟磁盘损坏，从而导致USRS表空间无法访问的例子：

1）对表空间USERS中的数据文件进行备份。

2）在表空间中创建一个表t2，然后向表中写入几行数据，提交事务。

3）为了模拟磁盘损坏，把数据库实例关闭，在操作系统中删除文件，或者重命名。

4）重新启动实例，这时候再SQL*Plus中将看到相关的出错信息。

5）将数据文件转入OFFLINE状态，然后打开数据库。

6）把备份文件复制到另外一个位置，如/home/oracle。

7）在数据库中修改这个数据文件的路径：

SQL＞ALTER DATABASE RENAME FILE'/u01/app/base/oradata/orcl/users01.dbf'TO'/home/oracle/users01.dbf'；

8）执行RECOVER：

SQL＞RECOVER DATAFILE 4；

9）将数据文件转入ONLINE状态。

15.7　Flashback技术在数据库恢复中的应用

Flashback是从Oracle 10g开始出现的一种技术，利用这种技术，可以很方便地查看数据库中过去某个时刻的数据，或者把数据库恢复到过去某个时刻的状态，还可以恢复被误删除的表。

为了使用Flashback技术，数据库必须满足以下条件：

·数据库必须是处于归档日志模式。

·在参数文件中通过下面两个初始化参数指定一个快速恢复区的路径及大小：

DB_RECOVERY_FILE_DEST

DB_RECOVERY_FILE_DEST_SIZE

·以sys用户登录实例，执行下面的命令：

SQL＞ALTER DATABASE FLASHBACK ON；

快速恢复区是一个目录或者一个ASM磁盘组，在这个位置将保存一段时间内所有的归档日志文件。每次数据库服务器对重做日志文件进行归档时，都将在这个位置产生一个归档日志文件。Flashback的基本原理就是利用过去的重做日志还原数据。下面的两条命令分别用于指定快速恢复区的大小及路径（注意两条命令的顺序）：

SQL＞ALTER SYSTEM SET DB_RECOVERY_FILE_DEST_SIZE=2G；

SQL＞ALTER SYSTEM SET DB_RECOVERY_FILE_DEST='/u01/app/recovery_area'；

利用Flashback技术，我们对数据库可以进行如下操作：

·数据库的Flashback：把整个数据库恢复到过去某个时刻。

·表的Flashback：把某个表恢复到过去某个时刻。

·Flashback drop：还原对表的drop操作。

·Flashback查询：查询数据库在过去某个时刻的数据。

·Flashback事务查询：查询某个表在过去某个时间段的事务。

15.7.1　回收站的应用

当用户删除表、索引等数据库对象时，数据并没有被立即删除，而是放在了回收站中。回收站实际上是一个数据字典视图，从这个视图中可以查看被删除的表的信息。回收站中的每个对象都被指定了一个复杂的、唯一的名称。

为了在一个会话中使用回收站，登录用户必须在SQL*Plus中执行下面的命令：

SQL＞ALTER SESSION SET recyclebin=on；

为了在会话中取消回收站的使用，登录用户必须在SQL*Plus中执行下面的命令：

SQL＞ALTER SESSION SET recyclebin=off

为了在整个数据库中使用或者取消回收站的功能，sys用户必须在SQL*Plus中执行下面的命令：

SQL＞ALTER SYSTEM SET recyclebin=on

SQL＞ALTER SYSTEM SET recyclebin=off

回收站就是一些数据字典视图，普通用户可以从视图user_recyclebin或者RECYCLEBIN查看属于自己的被删除的对象。sys用户可以从视图dba_recyclebin查看整个数据库内所有被删除的对象。例如：

SQL＞SHOW recyclebin；

SQL＞SELECT object_name, original_name, type FROM recyclebin；

OBJECT_NAME ORIGINAL_NAME TYPE

---------------------------------------------------------------------------

BIN$gqB40awS/ubgQAB/AQAUkA==$0 T1 TABLE

在查询结果中我们可以看到被删除对象在回收站的名称、原来的名称以及对象类型等。例如，在上面的查询结构中，有一个表，被删除之前的名称是t1，在回收站中的名称是BIN$gqB40awS/ubgQAB/AQAUkA==$0。

假设用户创建了一个数据库对象t1，然后将其删除，后来又创建了一个同名的数据库对象，又将其删除，这两个对象都将被放在回收站中。尽管两个数据库对象最初的名称是相同的，但是在回收站中的名称是不同的。通过回收站中的名称可以查询这些对象中被删除之前的数据。例如：

SQL＞SELECT*FROM"BIN$gqB40awS/ubgQAB/AQAUkA==$0"；

如果一个数据库对象是被误删除的，可以利用FLASHBACK命令将其从回收站中恢复。例如，下面的命令用于恢复表：

SQL＞FLASHBACK TABLE"BIN$gqB40awS/ubgQAB/AQAUkA==$0"TO BEFORE DROP；

假设用户删除一个数据库对象后又重新创建了一个同名同类型的数据库对象，那么在恢复被删除的数据库对象时就会遇到名称冲突的问题。在恢复数据库对象的时候还可以更改它的名称。例如：

SQL＞FLASHBACK TABLE"BIN$gqB40awS/ubgQAB/AQAUkA==$0"TO BEFORE DROP RENAME TO T2；

如果觉得一个数据库对象确实没有存在的必要了，可以利用PURGE命令将其从回收站中删除，这样可以节省磁盘空间。例如：

SQL＞PURGE TABLE emp；

SQL＞PURGE TABLE"BIN$gqB40awS/ubgQAB/AQAUkA==$0"；

如果确定要删除一个数据库对象，可以在DROP命令中使用PURGE关键字，这样就不会将其放在回收站中了。例如：

SQL＞DROP TABLE dept PURGE；

15.7.2　Flashback技术在表上的应用

利用Flashback技术，可以查询表在过去某个时刻的数据，例如：

SQL＞SELECT*FROM dept AS OF timestamp to_timestamp（'2010-03-27 16：42：00'，'YYYY-MM-DD HH24：MI：SS'）；

利用Flashback技术，还可以很方便把表中的数据恢复到过去某个时刻，而不会影响数据库中其他数据。在恢复之前，需要在表上执行下面的命令：

SQL＞ALTER TABLE dept ENABLE ROW MOVEMENT；

同时还要求当前用户对这个表具有FLASHBACK、INSERT、ALTER、DELETE和SELECT等几种权限。下面的命令用来把表DEPT中的一行数据恢复到指定的时刻：

SQL＞INSERT INTO dept

（SELECT*FROM dept AS OF TIMESTAMP to_timestamp（'2009-10-27 16：42：00'，'YYYY-MM-DD HH24：MI：SS'）WHERE deptno=50）；

15.7.3　Flashback技术在数据库恢复中的应用

利用Flashback技术，可以把整个数据库恢复到过去某个时刻，或者恢复到过去某个SCN。例如，某个非常重要的表被误删除了，在回收站中也没有这个表的信息。为了恢复这个表，只能利用Flashback技术把整个数据库恢复到删除表的时刻之前，这个表就被恢复了。表被删除的准确时间可以通过分析日志来获得。当然，为了恢复这个被误删除的表，代价也是比较大的，因为数据库被恢复到过去某个时刻，所有的表都回到了过去这个时刻的状态，所有还要采用其他方法解决这个问题。

下面的命令用于把数据库恢复到指定的时刻：

SQL＞FLASHBACK DATABASE TO TIMESTAMP to_timestamp（'2010-03-27 16：42：00'，'YYYY-MM-DD HH24：MI：SS'）；

下面的语句用于把数据库恢复到指定时刻之前一秒：

SQL＞FLASHBACK DATABASE TO BEFORE TIMESTAMP to_timestamp（'2010-03-27 16：42：00'，'YYYY-MM-DD HH24：MI：SS'）；

第16章　如何利用RMAN对数据库进行备份与恢复

RMAN是Oracle提供的一个功能强大的备份/恢复工具，利用这个工具不仅可以对数据库进行备份与恢复，而且可以在不同平台的数据库之间移植数据。许多第三方的存储管理工具也提供了RMAN的接口，利用RMAN的功能对数据库进行备份与恢复。

与传统的备份与恢复方法相比，RMAN有很多的优点。首先，在RMAN中可以对整个数据库、单个表空间，或者单个数据文件进行完全备份和增量备份。其次，所有的备份与恢复都通过RMAN中的命令或者脚本完成，管理员不需要到操作系统中复制文件。再次，RMAN支持主流的磁带机设备，可以把备份产生的文件直接写入磁带。最后，利用RMAN可以对数据文件中的坏块进行恢复。

总之，RMAN是一个非常实用而且功能强大的工具。管理员需要做的工作包括：制定合理的备份策略，编写备份脚本，并与操作系统的任务调度结合起来，实现备份的自动化。

RMAN是Oracle软件的一部分，数据库管理员无需安装额外的软件。RMAN可以通过EM和命令行两种方式使用，在本章仅介绍命令行的用法。

16.1　RMAN的基本结构

RMAN虽然功能强大，但是它的结构是比较复杂的。为了更好地使用RMAN，需要了解它的结构。接下来将对RMAN的重要组成部分进行详细的介绍。

1.目标数据库

目标数据库就是我们要进行备份的数据库，也就是要重点保护的数据库。利用RMAN可以对数据库进行冷备份和热备份，如果数据库处于归档日志模式，可以针对整个数据库，针对某个表空间，针对某个数据文件进行完全备份和增量备份。

2.恢复目录

为了对目标数据库进行备份，RMAN需要了解目标数据库的结构信息，即在目标数据库中有哪些表空间，哪些数据文件等，同时还需要了解以前所做的备份的情况。这些信息称为恢复目录。恢复目录可以存储在目标数据库的控制文件中，也可以存储在另外一个数据库中。Oracle建议把恢复目录存储在另外一个数据库中，而且把两个数据库分开在两个不同的计算机中运行。这么做的目的是在最大程度上保证数据库的安全。

如果把恢复目录存储在目标数据库的控制文件中，一旦计算机硬件出现故障，数据库和控制文件可能同时损坏，这时候即使管理员手头有完整的备份文件，也无法对数据进行恢复。如果把恢复目录存储在另外一个数据库中，即使目标数据库所在的计算机发生故障，管理员利用备份文件和恢复目录的内容可以对数据库进行完全恢复。

3.RMAN客户端

RMAN客户端是Oracle软件的一部分，就是用来连接目标数据库，并对其进行备份与恢复的部分。管理员可以通过EM和命令行两种方式使用RMAN。命令行的好处是可以编写复杂的脚本，把RMAN和操作系统的任务调度结合起来，可以实现备份的自动化。

4.通道

实际上，RMAN客户端并没有备份与恢复的功能。当RMAN连接目标数据库时，在数据库实例中将启动一个服务器进程，RMAN客户端的有关备份与恢复的命令都发送给服务器进程并由它负责执行。通道代表到备份设备的一个数据流，并对应一个服务器进程。通道将数据读到服务器进程的PGA中，并对数据进行处理，然后把数据写入备份设备。图16.1表示RMAN客户端、通道和目标数据库的关系。

RMAN客户端支持两种类型的通道，即磁盘通道和SBT（system backup to tape）通道，SBT设备包括磁带机和带库两种设备，通常由设备厂家提供的介质管理器进行控制。Oracle软件集成了主流SBT设备的驱动程序，在备份与恢复时，可以直接对SBT设备进行读写。

图　16.1　RMAN客户端、通道和目标数据库的关系

每次在对数据库进行备份或恢复时，都需要分配一个或多个通道，如果希望把数据库备份到磁带上，就分配SBT通道，如果希望备份到磁盘上，则分配磁盘通道。通道的分配有两种方法，即自动分配和手工分配。

5.备份集

利用RMAN工具，将数据库中的一个或多个数据文件、控制文件、重做日志文件等备份到一个称为“备份集”的逻辑结构中，备份集的格式是特定的，只能由RMAN来创建和访问。一个备份集包含一个或多个特定格式的二进制文件，每个文件称为一个备份片。假如要备份10个数据文件，将产生一个备份集，这个备份集中可能只包含一个备份片，即一个文件。如果备份集的大小超过了文件系统对文件大小的限制，这时将产生多个备份片，每个备份片的大小不超过文件系统对单个文件大小的限制。

在对数据进行备份时，也可以指定备份片的大小，以适应存储设备的容量。例如，假设一个备份集的大小大约为80GB，而一盘磁带的容量为20GB，我们可以指定备份片的大小为20GB，那么这个备份集将包含4个备份片，正好使用4盘磁带。

利用RMAN，还可以对数据库中的文件进行“映像复制”式备份，这种备份相当于在操作系统中对文件进行的复制，备份产生的文件不是RMAN特定的格式。

16.2　RMAN的配置

为了使用RMAN对数据库进行备份与恢复，需要对RMAN进行一系列的配置，主要包括创建恢复目录，配置RMAN客户端的连接等。

假设有两个正常运行的数据库，ORCL作为目标数据库，TEST作为恢复目录数据库，这两个数据库可以位于同一个系统中，也可以位于不同的系统中。为了保证数据库的安全，我们建议把这两个数据库分开放在不同的系统中。RMAN客户端可以位于另外一个系统或者同一个系统中，通过网络同时连接两个数据库。如图16.2所示。

图　16.2　RMAN与目标数据库及恢复目录数据库的连接

16.2.1　如何配置RMAN客户端的连接

一般情况下，通过SQL*Plus等客户端工具连接数据库实例时，需要通过系统变量ORACLE_SID指定实例的名称。RMAN客户端需要同时连接两个数据库实例，因此单纯指定这个系统变量是不够的，这时可以考虑通过网络服务命名的方式连接数据库实例。具体的做法是，通过NetManager工具为每个数据库创建并启动一个监听器，在RMAN客户端通过NetManager工具为每个数据库实例创建一个服务命名，这样就可以利用RMAN工具以下面的方式分别登录两个数据库：

$rman CATALOG rman/1234@test

$rman TARGET sys/1234@orcl

其中CATALOG和TARGET是关键字，分别用于指定登录恢复目录数据库和目标数据库。@符号后面的两个字符串就是代表两个数据库的服务命名。

有关如何利用NetManager工具配置客户端与服务器端的通信的内容，请参考第5章。

16.2.2　恢复目录的创建

在TEST数据库中，单独创建一个表空间，恢复目录就存放在这个表空间中。还要单独创建一个RMAN用户，以后对目标数据库的所有备份与恢复工作都由这个用户来完成。RMAN用户登录TEST数据库中后，需要创建恢复目录，然后将目标数据库注册到恢复目录中。具体步骤如下：

1）以sys用户登录test数据库，创建表空间rman_ts，这个表空间不需要太大，20MB的空间就足够了。

$export ORACLE_SID=test

$sqlplus sys/1234 as sysdba

SQL＞CREATE TABLESPACE rman_ts

DATAFILE'/u01/app/base/oradata/test/rman_ts.dbf'SIZE 20M；

2）以sys用户的身份在test数据库中创建RMAN用户，并指定相关权限。这个用户的名称可以任意指定，但需要特殊的权限recovery_catalog_owner。

SQL＞CREATE USER rman IDENTIFIED BY"1234"

DEFAULT TABLESPACE rman_ts

QUOTA UNLIMITED ON rman_ts；

SQL＞GRANT connect, resource, recovery_catalog_owner

TO rman；

3）RMAN用户通过RMAN工具登录test数据库，创建恢复目录。

$rman catalog rman/1234@test

RMAN＞CREATE CATALOG TABLESPACE rman_ts；

4）通过RMAN工具同时连接两个数据库，并将目标数据库注册到恢复目录中。一般以sys身份连接目标数据库，而且不需要指定“AS SYSDBA”。

$rman target sys/1234@orcl catalog rman/1234@test

RMAN＞REGISTER DATABASE；

通过RMAN工具也可以以下面的方式分别连接两个数据据库：

$rman

RMAN＞CONNECT target sys/1234@orcl

RMAN＞CONNECT catalog rman/1234@test

退出RMAN的方法是在RMAN提示符下输入EXIT命令。

到此为止，RMAN的配置就完成了，现在就可以RMAN工具对数据库进行备份与恢复了。

16.3　如何利用RMAN对数据库进行备份

利用RMAN，可以对数据库中的数据文件、控制文件、服务器参数文件以及归档日志文件进行备份。

16.3.1　通道的设置

在对数据库进行备份或恢复时，需要分配一个或多个通道。通道有两种分配方式，即自动分配和手工分配。如果在RMAN中先对通道进行设置，比如设置通道的数目、备份文件的存储路径和文件名、默认的通道类型等，那么在进行备份或恢复时，就按照这样的设置分配通道，这种方式称为自动分配。如果在每次备份或恢复时通过命令指定通道的信息，这种分配方式就称为手工分配。手工指定的通道信息将覆盖自动分配的通道信息。

在RMAN中可以对磁盘通道和磁带通道分别进行设置，并指定默认的通道类型。一般来说，如果要同时备份多个文件，或者将文件备份到多个设备上，可以分配多个通道，这样可以对数据进行并行读写。下面是通道设置的一些例子：

RMAN＞CONFIGURE DEVICE TYPE sbt PARALLELISM 2；

RMAN＞CONFIGURE DEVICE TYPE DISK BACKUP TYPE TO BACKUPSET；

RMAN＞CONFIGURE DEVICE TYPE sbt BACKUP TYPE TO COPY；

RMAN＞CONFIGURE DEFAULT DEVICE TYPE TO DISK；

RMAN＞CONFIGURE CHANNEL DEVICE TYPE DISK FORMAT'/tmp/%U'；

RMAN＞CONFIGURE CHANNEL DEVICE TYPE DISK FORMAT'+dgroup1'；

在上面的例子中，第一条命令指定磁带通道的数目为2，即只要使用磁带通道，每次都分配2个。第二条命令指定磁盘通道的备份形式为备份集，也就是说，如果要用磁盘通道进行备份，将产生一个备份集。第三条命令指定磁带通道的备份形式为映像复制。第四条命令指定默认的通道类型为磁盘通道，即每次备份或恢复时，默认都分配磁盘通道。第五条命令指定备份文件的存储路径和文件名称，也就是说，如果不另外指定，利用磁盘通道进行备份时，将在指定的目录下以指定的文件名称生成备份文件夹。第六条命令指定通道类型为磁盘通道，备份文件的存储位置为一个ASM磁盘组，文件名称自动产生。

手工分配通道的方式指的是在备份或恢复时，通过ALLOCATE命令分配通道，或者在BACKUP命令中指定通道。例如，下面的例子是在备份数据库的命令中指定磁盘通道：

RMAN＞BACKUP DEVICE TYPE DISK DATABASE；

下面的例子是在备份或恢复之前通过ALLOCATE命令分配一个磁盘通道，同时指定备份文件的存储路径和文件名称：

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK FORMAT'/home/oracle/%U'；

……

}

16.3.2　存储脚本的用法

在对数据库进行备份或恢复时，往往需要执行多条命令，而且每次备份可能都需要执行同样的命令。为了减少输入命令的工作量，同时为了减少输入的错误，我们可以把这些命令组合为一个存储脚本。存储脚本的功能类似于数据库中的存储过程，它存储在CATALOG数据库中，可以被反复调用。为了使用存储脚本，首先需要在RMAN中创建它。例如，下面的代码是创建存储脚本的例子，这个脚本的功能是利用磁盘通道对数据库进行0级备份。

RMAN＞CREATE SCRIPT db_0_backup

{

allocate channel ch1 device type disk；

backup incremental level 0 database format'/home/oracle/%t_%s'；

sql'alter system archive log current'；

release channel ch1；

}

以后只要对数据库进行0级备份，就可以调用这个存储脚本。调用的格式如下：

RMAN＞RUN{

execute script db_0_backup；

}

在创建存储脚本时，也可以把CREATE命令中一对花括号之间的命令放在一个操作系统的文本文件中，然后在CREATE命令中指定这个文本文件。例如：

RMAN＞CREATE SCRIPT db_0_backup

FROM FILE'/tmp/my_script.txt'；

存储脚本的名称和代码都存储在CATALOG数据库中。通过LIST命令，可以列出所有存储脚本的名称。例如：

RMAN＞LIST SCRIPT NAMES；

RMAN＞LIST ALL SCRIPT NAMES；

通过PRINT命令可以查看存储脚本的代码，还可以把存储脚本的代码重定向到操作系统的文本文件中。例如：

RMAN＞PRINT SCRIPT db_0_backup；

RMAN＞PRINT SCRIPT db_0_backup TO FILE'/home/oracle/my_script.txt'；

通过DELETE命令，可以删除不再使用的存储脚本。例如：

RMAN＞DELETE SCRIPT db_0_backup；

16.3.3　控制文件的备份

对控制文件可以进行自动备份和手工备份。如果在RMAN中进行了以下设置，那么在每次对数据文件做备份之后，或者数据库的结构做了修改之后，控制文件和服务器参数文件将被自动备份。在控制文件的自动备份中包含了数据库结构的信息，这对数据库的恢复是非常关键的。

RMAN＞CONFIGURE CONTROLFILE AUTOBACKUP ON；

如果没有设置控制文件的自动备份，就需要对它进行手工备份了。例如，下面的命令利用默认的通道将数据文件备份到默认的位置：

RMAN＞BACKUP CURRENT CONTROLFILE；

下面的命令对控制文件进行“映像复制”式备份，将备份结果放在/tmp目录下：

RMAN＞BACKUP AS COPY CURRENT CONTROLFILE FORMAT'/tmp/control01.ctl'；

在备份其他文件的同时，可以对控制文件进行备份。例如，下面的命令利用SBT通道对users表空间进行备份，同时备份控制文件：

RMAN＞BACKUP DEVICE TYPE sbt TABLESPACE users

INCLUDE CURRENT CONTROLFILE；

另外，在数据库中有一个ID为1的数据文件，这个文件属于system表空间。每次在对ID为1的数据文件进行备份时，控制文件将被自动备份。

控制文件的备份信息可以通过下面的命令查询：

RMAN＞LIST BACKUP OF CONTROLFILE；

16.3.4　参数文件的备份

为了对参数文件进行备份，数据库实例必须处于MOUNT或者OPEN状态，而且实例在启动时读的是服务器参数文件，而不是文本参数文件。

对参数文件也可以进行自动备份和手工备份。参数文件的自动备份是伴随控制文件的自动备份一起完成的，只要设置了控制文件的自动备份，参数文件就可以被自动备份。比如在对ID为1的数据文件进行备份时，或者在对数据库的结构做了修改之后，控制文件和服务器参数文件将被自动备份。

通过BACKUP命令可以对参数文件进行手工备份。例如，下面的命令利用SBT通道对参数文件进行备份：

RMAN＞BACKUP DEVICE TYPE sbt SPFILE；

参数文件的备份信息可以通过下面的命令查询：

RMAN＞LIST BACKUP OF SPFILE；

16.3.5　归档日志文件的备份

归档日志文件对于数据库的恢复是非常重要的，所以可以考虑对它们进行备份。对于这种文件可以单独进行备份，也可以和其他文件一起备份。在备份时可以选择是备份所有归档日志文件还是备份一定范围内的归档日志文件。例如，下面的语句用于备份所有的归档日志文件：

RMAN＞BACKUP ARCHIVELOG ALL；

如果要备份一定范围内的归档日志文件，可以通过时间、SCN或者日志序列号来指定这个范围。

RMAN＞BACKUP ARCHIVELOG FROM TIME'sysdate-30'UNTIL TIME'sysdate-7'；

RMAN＞BACKUP ARCHIVELOG FROM SEQUENCE 121 UNTIL SEQUENCE 125；

RMAN＞BACKUP ARCHIVELOG FROM SCN 1000 UNTIL SCN 2000；

在通过BACKUP命令对数据文件或控制文件进行备份时，可以同时备份归档日志文件。例如，下面的语句利用默认的通道备份数据库，同时备份归档日志文件：

RMAN＞BACKUP DATABASE PLUS ARCHIVELOG；

在数据库中可以对归档日志文件指定多个归档位置，这样在对重做日志文件进行归档时，将产生多个归档日志文件，这些文件的内容是完全相同的。也就是说，同一个归档日志文件有多个复本。在备份归档日志文件时，只需要读其中一个复本。备份结束后，可以将原来的归档日志文件删除。可以选择删除归档日志文件的所有复本，还是删除其中用于备份的那个复本。例如，下面的命令利用SBT通道备份归档日志文件，在备份结束后从所有的归档位置删除归档日志文件的全部复本。

RMAN＞BACKUP DEVICE TYPE sbt ARCHIVELOG ALL DELETE ALL INPUT；

下面的语句用于备份归档日志文件，在备份结束后只删除归档日志文件的用于备份的那个复本。

RMAN＞BACKUP DEVICE TYPE sbt ARCHIVELOG ALL DELETE INPUT；

16.3.6　非归档模式下数据文件的备份

如果数据库处于非归档模式，我们只能对它进行冷备份，也就是说，需要把数据库关闭之后进行备份，而且只能对整个数据库进行备份，不能针对表空间或数据文件进行备份。例如，下面的命令序列用于对处于非归档模式的数据库进行备份：

RMAN＞CONFIGURE CHANNEL DEVICE TYPE DISK FORMAT'/tmp/%U'；

RMAN＞CONFIGURE DEFAULT DEVICE TYPE TO DISK；

RMAN＞SHUTDOWN IMMEDIATE；

RMAN＞STARTUP MOUNT；

RMAN＞BACKUP DATABASE；

RMAN＞SQL'ALTER SYSTEM ARCHIVE LOG CURRENT'；

在上面的命令序列中，首先对通道进行配置，然后关闭数据库，把数据库实例启动到MOUNT状态，然后利用通道对数据库进行备份，最后对当前的重做日志文件进行归档。

如果要在RMAN中执行ALTER这样的SQL语句，需要通过SQL关键字和一对单引号指定完整的SQL语句。例如：

RMAN＞SQL'ALTER DATABASE DATAFILE 6 OFFLINE'；

16.3.7　归档模式下数据文件的备份

生产系统的数据库一般都处于归档模式下，而数据库中的数据文件是需要重点保护的，所以，在归档模式下对数据文件的备份方法需要读者重点掌握。

在归档模式下对数据文件的备份是很灵活的，可以针对整个数据库，或者针对一个表空间，或者针对表空间中的某个数据文件进行备份。尽管我们可以对数据库进行冷备份，但是对于生产系统而言，进行热备份显然是最合适的。

利用RMAN，可以对数据库进行完全备份和增量备份。完全备份和增量备份通过不同的备份级别来实现。在Oracle数据库中，可以实现以下几个级别的备份：

·FULL：完全备份。

·0：完全备份。

·1～4：增量备份。

需要注意的是，完全备份并不是指对整个数据库的备份。完全备份和增量备份可以针对整个数据库，或者一个表空间，或者一个数据文件。如果把整个数据库，或者某个表空间，或者某个数据文件中的数据全部备份，这次备份就是完全备份。反之，如果只把一段时间内数据库，或者表空间，或者数据文件中被修改的数据进行备份，这次备份就是增量备份。

大家还需要注意的是，尽管FULL和0备份都是完全备份，但是两者是有区别的。0级备份是增量备份的基础，而FULL备份不是。也就是说，在做增量备份之前，必须先做一次0级备份，如果没有做，那么RMAN将自动做一次0级备份。FULL备份可以认为是一种独立的备份，即使对数据库做了FULL备份，在做增量备份之前，还需要做一次0级备份。

例如，下面的代码用于对表空间users做一次0级备份：

RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch2 DEVICE TYPE DISK；

BACKUP INCREMENTAL LEVEL 0 tablespace users

FORMAT'/home/oracle/users_0_%T_%p'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

下面的代码用于对表空间users做一次1级备份：

RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch2 DEVICE TYPE DISK；

BACKUP INCREMENTAL LEVEL 1 tablespace users

FORMAT'/home/oracle/users_1_%T_%p'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

如果把上述代码中的BACKUP命令换成下面的形式，则分别用来对整个数据库和某个数据文件进行0级和1级备份。

BACKUP INCREMENTAL LEVEL 0 database FORMAT'/home/oracle/db_0_%T_%p'；

BACKUP INCREMENTAL LEVEL 1 database FORMAT'/home/oracle/db_1_%T_%p'；

BACKUP INCREMENTAL LEVEL 0 datafile 6 FORMAT'/home/oracle/file6_0_%T_%p'；

BACKUP INCREMENTAL LEVEL 1 datafile 6 FORMAT'/home/oracle/file6_1_%T_%p'；

在对数据文件进行备份时，可以指定数据文件的ID号，也可以指定它的完整路径。数据文件的ID号可以从数据字典视图dba_data_files中获得。

上述命令序列通过一对花括号限定，在命令序列的前面是一条RMAN中的命令RUN。通过这种形式把逻辑上相关的一组命令组合在一起，使它们组成一个相对独立的代码空间，在这里可以对通道进行配置，所做配置只在这个空间中有效。当输入花括号“}”时，这些命令即开始按顺序执行。

在BACKUP命令中，通过FORMAT关键字指定备份文件的存储路径和文件名称。文件的命名原则首先是要保证文件名的唯一性，否则以前产生的备份文件可能会被覆盖。其次，根据文件名能够判断这个文件是对谁做的什么级别的备份。为此，在文件名称中可以使用一些变量，如%T。这些变量具有特殊的含义，在产生备份文件时，这些变量被替换为实际的变量值。在表16.1中列出了在RMAN中可以使用的一些变量。

一般来说，在一个数据库中，我们把备份的重点放在业务数据所在的表空间上，不需要对整个数据库进行备份，临时表空间、UNDO表空间、SYSTEM表空间和SYSAUX表空间是不需要备份的。为了备份方便，应该把业务数据所相关的表单独存放在一个表空间中，而且把表上的索引单独存放在另外一个表空间中，因为索引表空间也不需要备份。

16.3.8　备份集的备份

利用RMAN不仅可以对数据库中的文件进行备份，而且可以对以前产生的备份集进行备份，还可以对快速恢复区中的所有文件进行备份。在默认情况下，快速恢复区作为归档日志文件的一个存储位置，而且对数据文件、控制文件的备份也存放在这里，因此，在快速恢复区中可能包含了所有的归档日志文件和所有的备份文件，利用快速恢复区中的文件可以对数据库进行完全恢复。

对备份集进行备份的目的有两个，其一是保证每个备份集有两个复本，这样可以保证备份集的安全。其二是把磁盘上的备份集备份到磁带上，磁盘上的存储空间即可释放。

在对备份集进行备份之前，先通过LIST命令查看备份集的详细信息，例如，查看对数据库或者表空间或者数据文件所产生的备份集。

例如，下面的命令用于查看所有备份集的概要信息：

RMAN＞LIST BACKUPSET SUMMARY；

下面的命令用于查看某个数据文件的备份：

RMAN＞LIST BACKUP OF DATAFILE 5 SUMMARY；

上述两条命令的执行结果类似下面这样：

Key TY LV S Device Type Completion Time#Pieces Tag

110 B0A DISK 05-APR-10 1 TAG20100405T152315

211 B0A DISK 13-APR-101 TAG20100413T143513

在上述两条命令的执行结果中，每行代表一次备份，其中第一个字段代表备份集的编号。通过下面的命令可以查看某个备份集的详细信息，其中数字211为备份集的编号。

RMAN＞LIST BACKUPSET 211；

在上述命令的执行结果中，将显示备份集中所包含的文件名称和编号、备份的时间、备份类型等信息。下面的两条命令用于对备份集进行备份，其中第一条命令备份所有的备份集，并删除原来的备份集，第二条命令对备份集1和备份集2进行备份，并保留原来的备份集。

RMAN＞BACKUP BACKUPSET ALL DELETE INPUT；

RMAN＞BACKUP BACKUPSET 1，2；

16.4　如何对数据库进行完全恢复

对数据库进行备份的最终目的是进行恢复，一旦数据库中的数据发生了丢失或者被破坏的情况，就可以利用备份对数据进行恢复了。

16.4.1　如何对备份文件进行校验

RMAN提供了一个对备份文件进行校验的功能，利用这个功能对以前产生的备份文件进行校验，以保证这样的备份文件可以用来对数据进行恢复。

备份文件不可用的情况有两种，一是文件损坏，二是找不到备份文件。所以在校验之前确保备份文件已经从其他位置复制到最初的存放位置。如果不知道备份集最初的存放位置，可以在RMAN中通过LIST命令查询。下面两条命令分别用于对数据库的备份文件和归档日志文件的备份进行校验：

RMAN＞RESTORE DATABASE VALIDATE；

RMAN＞RESTORE ARCHIVELOG ALL VALIDATE；

如果发现备份文件不存在，则校验结果中可能包含类似下面的信息：

ORA-19505：failed to identify file"/home/oracle/users_0_20100420_1"

ORA-27037：unable to obtain file status

SVR4 Error：2：No such file or directory

Additional information：3

16.4.2　如何对数据文件进行恢复

当数据文件丢失或损坏时，数据库服务器将根据目前所处状态的不同，将会有不同的表现形式。如果数据库实例处于OPEN状态，那么最直观的表现为业务系统运行不正常，对数据文件的读写将发生错误。如果数据库服务器正处于启动的过程中，在SQL*Plus命令行或其他工具中将显示类似以下信息：

ORA-01157：cannot identify/lock data file 4-see DBWR trace file

ORA-01110：data file 4：'/home/oracle/users01.dbf'

无论是哪种情况，可以通过两种途径获得数据文件的状态信息，一是查询动态性能视图v$datafile_header，二是查看后台进程DBWR的跟踪文件。例如，下面的SELECT命令用于查询目标数据库中数据文件的状态：

SQL＞SELECT FILE#，STATUS, ERROR, RECOVER, NAME FROM v$datafile_header；

1 ONLINE NO/u01/app/base/oradata/orcl/system01.dbf

2 ONLINE NO/u01/app/base/oradata/orcl/sysaux01.dbf

3 ONLINE NO/u01/app/base/oradata/orcl/undotbs01.dbf

4 OFFLINE OFFLINE NORMAL

数据文件损坏或丢失时，DBWR进程将不能正常工作，在该进程的跟踪文件中将记录相关的错误信息。通过查看初始化参数BACKGROUND_DUMP_DEST的值，可以了解后台进程跟踪文件的存储路径。例如，下面的信息是从DBWR进程的跟踪文件中获得的：

ORA-01157：cannot identify/lock data file 4-see DBWR trace file

ORA-01110：data file 4：'/home/oracle/users01.dbf'

ORA-27037：unable to obtain file status

Error：2：No such file or directory

为了对数据文件进行恢复，需要把数据文件或所属表空间的状态转为OFFLINE。如果数据库处于正在启动的过程中，那么将损坏的数据文件置为OFFLINE状态后，执行命令ALTER DATABASE OPEN，将数据库打开后再对数据文件进行恢复。需要注意的是，如果要对整个数据库或者SYSTEM表空间进行恢复，只能在实例处于MOUNT状态时进行恢复。

数据的恢复过程包括两个步骤，第一步是RESTORE，第二步是RECOVER。其中RESTORE的功能是将数据文件从备份集中还原到它最初的存储位置（即丢失前或损坏前的存储位置），代替以前的数据文件。RECOVER的功能是在前面所还原的数据文件的基础上，利用重做日志对最后一段时间内没有备份的数据进行恢复。备份集的存储位置可以利用RMAN中的LIST命令获得。

假设某个数据文件需要恢复，在SQL*Plus中通过下列语句之一将数据文件置为OFFLINE状态：

SQL＞ALTER TABLESPACE users OFFLINE；

SQL＞ALTER DATABASE DATAFILE 4 OFFLINE；

然后在RMAN中对该数据文件进行恢复。例如：

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

RESTORE DATAFILE 4；

RECOVER DATAFILE 4；

SQL'ALTER DATABASE DATAFILE 4 ONLINE'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

在对数据文件进行恢复时，在上述命令序列中可以指定它的完整名称，也可以指定它的ID。如果要对整个数据库进行恢复，先将数据库实例的状态切换到MOUNT，然后进行恢复。相应的命令序列如下：

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

RESTORE DATABASE；

RECOVER DATABASE；

SQL'ALTER DATABASE OPEN；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

如果要对一个普通表空间进行恢复，可以在数据库实例处于MOUNT或者OPEN状态时，将表空间的状态置为OFFLINE，然后对其进行恢复。相应的命令序列如下：

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

RESTORE TABLESPACE users；

RECOVER TABLESPACE users；

SQL'ALTER TABLESPACE users ONLINE'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

如果数据文件所在的磁盘损坏，需要把文件恢复到另外一个位置。在RESTORE命令之前，需要为数据文件指定一个新的存储位置。命令格式如下：

SET NEWNAME FOR DATAFILE 4 TO'new_location'；

在对数据文件进行恢复时，可以在SQL*Plus中，也可以在RMAN中执行相应的SQL命令，将数据文件或表空间的状态置为OFFLINE。在RMAN的RUN命令序列中，在分配通道的命令之后，加入下面的命令即可：

SQL'ALTER DATABASE DATAFILE 4 OFFLINE'；

16.5　两个实际的例子

在本节中通过两个实际的例子分别模拟数据文件损坏和磁盘损坏的情况，完整地介绍如何对表空间进行完全备份和增量备份，以及如何利用这些备份和重做日志对数据文件进行恢复。

16.5.1　模拟数据文件损坏的例子

首先对表空间进行备份。一方面，以sys用户对表空间users进行完全备份和增量备份，另一方面，以普通用户scott登录，在表空间users中创建一个表，并写入一些数据。每次增量备份的内容就是新修改的数据。以下是对表空间进行备份和恢复的步骤：

1）sys用户利用RMAN工具连接目标数据库，对表空间users进行0级备份。

$rman target sys/1234@orcl catalog rman/1234@test

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch2 DEVICE TYPE DISK；

BACKUP INCREMENTAL LEVEL 0 tablespace users

FORMAT'/home/oracle/users_0_%T_%p'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

2）以scott用户登录目标数据库，在表空间users中创建表t1。

3）sys用户对表空间users进行1级备份。

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch2 DEVICE TYPE DISK；

BACKUP INCREMENTAL LEVEL 1 tablespace users

FORMAT'/home/oracle/users_1_%T_%p'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

4）scott用户向表t1中写入两行数据，并提交事务。

5）sys用户对表空间users进行2级备份（相应的命令省略）。

6）scott用户向表t1中写入另外两行数据，并提交事务。

在上面的命令中，对表空间既做了完全备份，又做了增量备份，以后在对数据文件进行恢复时，先利用0级备份，再依次利用后面所做的增量备份进行恢复。而在最后一步中scott用户写入的数据是没有备份的，只能依靠重做日志进行恢复。

为了模拟数据文件损坏，需要关闭数据库，然后在操作系统中删除表空间USERS中的数据文件，或者对其改名，然后利用SQL*Plus或RMAN启动数据库实例。在SQL*Plus或RMAN中将看到类似以下错误信息：

ORA-01157：cannot identify/lock data file 4-see DBWR trace file

ORA-01110：data file 4：'/home/oracle/users01.dbf

接下来对损坏的数据文件进行恢复。首先要保证备份集中的文件都位于FORMAT命令所指定的位置。为了不影响用户对整个数据库的访问，在将损坏的数据文件置为OFFLINE状态后，或者将表空间置为OFFLINE之后，将数据库打开。然后对表空间或数据文件进行恢复，最后将表空间的状态置为ONLINE。恢复结束后，以scott用户登录目标数据库，检查t1表中的数据是否完全被恢复。下面是对表空间users进行恢复的命令序列：

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

SQL'ALTER TABLESPACE users OFFLINE'；

SQL'ALTER DATABASE OPEN'；

RESTORE TABLESPACE users；

RECOVER TABLESPACE users；

SQL'ALTER TABLESPACE users ONLINE'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

16.5.2　模拟磁盘损坏的例子

如果由于磁盘损坏而导致数据文件无法访问，这种情况对用户来说同样表现为数据文件无法访问。在对数据文件进行恢复时，需要指定一个新的存储位置，把数据文件恢复到这个位置。以下是对表空间users进行备份和恢复的步骤：

1）按照16.5.1节中的第1～6步对表空间进行备份。

2）关闭数据库实例，删除表空间users中的数据文件，模拟磁盘故障。

3）启动数据库实例，观察出现的错误信息。

4）将users表空间或者数据文件置为OFFLINE状态，然后打开数据库，并对表空间users进行恢复，将数据文件恢复到新的位置。相应的命令序列如下：

RMAN＞RUN{

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

ALLOCATE CHANNEL ch1 DEVICE TYPE DISK；

SQL'ALTER TABLESPACE users OFFLINE'；

SQL'ALTER DATABASE OPEN'；

SET NEWNAME FOR DATAFILE TO'/u01/app/orcl//users01.dbf'；

RESTORE TABLESPACE users；

RECOVER TABLESPACE users；

SQL'ALTER TABLESPACE users ONLINE'；

RELEASE CHANNEL ch1；

RELEASE CHANNEL ch2；

}

16.6　如何对坏块进行恢复

在实际的生产系统中，一种常见的故障是磁盘中有坏块，当用户进程访问这些位于坏块中的数据时，就会出错。利用RMAN工具，不仅可以对整个数据文件进行恢复，还可以恢复数据文件中的坏块。

16.6.1　什么叫块介质恢复

块介质恢复是指对数据文件中单个数据块所进行的恢复。与恢复数据文件、恢复表空间、恢复整个数据库相比，块介质恢复有两个好处，一是只需要恢复少数几个数据块，恢复的时间大大减少。二是数据文件仍然可以处于ONLINE状态，用户仍然可以访问数据文件中的其他数据块。

数据库服务器在第一次发现一个数据块损坏时，便把这个数据块标记为损坏，并记录下来。对这个数据块随后进行的访问都将失败，直到这个块被恢复。数据块损坏的情况有两种，一种叫做物理损坏，另一种叫做逻辑损坏。其中物理损坏也叫介质损坏，指的是数据库服务器无法识别数据块的情况，比如数据块的校验和无效，数据块中包含全0，或者数据块的头部数据无效等。逻辑损坏指的是数据块的校验和、头部信息都是有效的，但是数据块的内容在逻辑上不一致。

在下列情况下，数据库服务器将发现一个数据块是损坏的：

·在利用RMAN对数据库进行备份时。

·通过ANALYZE命令对表进行分析时。

·通过dbv工具对数据文件进行校验时。

·通过SQL命令访问数据文件时。

如果发现一个数据块损坏，就需要对其进行恢复。块介质恢复需要满足以下条件：

·数据库的日志模式必须是归档模式，而且数据库服务器的状态是OPEN或MOUNT。

·对数据库必须进行过0级或FULL备份，且备份文件是可用的。

·数据库的Flashback功能必须是ENABLED。

·数据库的归档日志必须是保留完整的。

16.6.2　如何进行块介质恢复

数据块损坏的信息可以从多种渠道获得，例如，数据库的警告文件、用户进程的跟踪文件、DBVERIFY工具的执行结果、动态性能视图v$database_block_corruption等。例如，在用户进程的跟踪文件将发现类似如下的信息：

ORA-01578：ORACLE data block corrupted（file#7，block#3）

ORA-01110：data file 7：'/u01/app/base/orcl/t11.dbf'

利用RMAN工具，可以对单个的数据块进行恢复。例如：

$rman target sys/1234@orcl catalog rman/1234@test

RMAN＞RECOVER

DATAFILE 7 BLOCK 3

DATAFILE 2 BLOCK 19；

在恢复之前，确保通道是可用的。在恢复时也可以指定从一个特定的备份集中进行恢复，例如：

RMAN＞RECOVER

DATAFILE 7 BLOCK 3

DATAFILE 2 BLOCK 19

FROM BACKUPSET 10；

其中备份集的编号是通过下面的命令获得的：

RMAN＞LIST BACKUPSET SUMMARY；

通过下面的命令，可以对动态性能视图v$database_block_corruption中记录的坏块一起进行恢复：

RMAN＞RECOVER CORRUPTION LIST；

16.7　如何对数据进行跨平台移植

在两个Oracle数据库之间移植数据是很方便的。如果两个数据库的操作系统平台相同，可以利用第14章中介绍的方法，把一个表、一个用户或者一个表空间在两个数据库之间进行移植。

如果两个数据库所处的操作系统平台不同，也可以利用导入与导出的方法把一个表或者一个用户在数据库之间进行移植。如果要移植的数据量比较大，最好的方法是对一个表空间进行移植。尽管在不同平台之间文件的格式是不相同的，但是Oracle数据库在跨平台移植方面有一些很受欢迎的特点，使得这样的移植非常容易。在有些平台之间可以直接移植数据文件，不需要进行格式转换。下面分两种情况对不同平台下数据文件的移植方法进行介绍。

16.7.1　字节存储次序相同时的移植

在不同的操作系统平台下，数据文件的格式差别就在于字节存储次序的不同。在动态性能视图v$transportable_platform中能够查询Oracle所支持的操作系统平台的ID和名称，以及在这些平台下数据文件的字节存储次序。例如：

SQL＞SELECT platform_id, platform_name, endian_format

FROM v$transportable_platform

ORDER BY platform_id；

PLATFORM_ID PLATFORM_NAME ENDIAN_FORMAT

1 Solaris[tm]OE（32-bit）Big

2 Solaris[tm]OE（64-bit）Big

3 HP-UX（64-bit）Big

4 HP-UX IA（64-bit）Big

5 HP Tru64 UNIX Little

6 AIX-Based Systems（64-bit）Big

7 Microsoft Windows IA（32-bit）Little

8 Microsoft Windows IA（64-bit）Little

9 IBM zSeries Based Linux Big

10 Linux IA（32-bit）Little

11 Linux IA（64-bit）Little

12 Microsoft Windows x86 64-bit Little

13 Linux x86 64-bit Little

……

如果数据文件在两个操作系统平台中的字节存储次序（ENDIAN_FORMAT）是相同的，比如都是Little，那么数据文件可以在两个数据库之间直接移植，不需要进行格式转换。

在对数据文件进行跨平台移植之前，首先确定数据文件在这个两个操作系统之中的字节存储次序是否相同。从动态性能视图v$database中可以获得当前数据库所处操作系统平台的ID和名称。例如，下面两条SQL语句分别获得两个数据库的操作系统平台信息：

SQL＞SELECT platform_id, platform_name

FROM v$database；

PLATFORM_ID PLATFORM_NAME

12 Microsoft Windows x86 64-bit

SQL＞SELECT platform_id, platform_name

FROM v$database；

PLATFORM_ID PLATFORM_NAME

13 Linux x86 64-bit

然后从动态性能视图v$transportable_platform中可以得知，数据文件在这两个操作系统下的字节存储次序是相同的，都是Little。这样，在这两个数据库之间移植一个表空间就很方便了。假设要从第一个数据库中将表空间users移植到第二个数据库中，实施的步骤如下：

1）在第一个数据库中将表空间置为READ ONLY状态。例如：

SQL＞ALTER TABLESPACE USERS READ ONLY；

2）利用EXP命令将表空间的结构信息导出到元数据文件中。

3）将表空间中的数据文件和元数据文件复制到第二个数据库所在的系统中。

4）利用IMP命令将表空间导入到第二个数据库中。

5）在第一个数据库中将表空间置为READ WRITE状态。例如：

SQL＞ALTER TABLESPACE USERS READ WRITE；

在导出表空间时，要求用户在第一个数据库中具有EXP_FULL_DATABASE系统权限，在导入表空间时，用户需要在第二个数据库中具有IMP_FULL_DATABASE系统权限。例如，下面两条命令分别用来在两个数据库中对表空间users进行导出和导入操作：

$exp\'sys/1234 AS SYSDBA\'FILE=users.exp\TRANSPORT_TABLESPACE=Y TABLESPACES=users

$imp\'sys/1234 AS SYSDBA\'FILE=users.exp\TRANSPORT_TABLESPACE=Y DATAFILES=（/home/oracle/users01.dbf）

从上面的过程可以看出，在不同操作系统平台下移植表空间时，如果数据文件在两个操作系统中的字节存储次序相同，那么这个移植过程与两个相同平台之间表空间的移植方法是一样的，这无疑是很方便的。

16.7.2　字节存储次序不同时的移植

如果在两个操作系统平台下数据文件的字节存储次序不同，那么在移植表空间时需要对数据格式进行转换。例如，要将Windows x86 64-bit平台下的一个表空间移植到AIX 64-bit平台下，就需要对数据文件进行转换。

转换数据文件格式的方法有两种，一种是在原来的数据库中进行转换，另一种方法是在对方数据库中进行转换。在原来的数据库中只能通过命令CONVERT TABLESPACE对整个表空间进行转换。例如，下面的命令是在Windows平台下对表空间tools进行转换的例子：

rman target sys/1234@source_db

RMAN＞CONVERT TABLESPACE tools

TO PLATFORM'AIX-Based Systems（64-bit）'

FORMAT='d：\test\%U'；

转换的结果是在指定的目录下产生新的数据文件，文件名称通过%U或其他变量指定。这种转换方法的好处是对整个表空间进行转换，不需要指定每个数据文件的名称，缺点是转换后的数据文件只能通过变量指定名称。需要注意的是，在对数据文件进行转换时，在RMAN命令中需要指定原来的数据库或者对方数据库的操作系统平台的完整名称。

将数据文件从原来的数据库复制到对方系统之后，在对方数据库中可以对单个的数据文件进行转换，转换的命令是CONVERT DATAFILE。例如，下面的命令是在对方数据库中对数据文件进行转换的例子：

rman target sys/1234@destination_db

RMAN＞CONVERT DATAFILE

'/tmp/tools01.dbf'，'/tmp/tools02.dbf'

DB_FILE_NAME_CONVERT

'/tmp'，'/u01/app/base/orcl'

FROM PLATFORM'Windows x86 64-bit'；

在对单个数据文件进行转换时，需要通过DB_FILE_NAME_CONVERT关键字指定转换之前和转换之后数据文件所在的目录。在上述例子中，数据文件在转换之前位于目录/tmp中，转换之后位于目录/u01/app/base/orcl中，文件名称不变。

在不同的操作系统之间，当数据文件的字节存储次序不同时，移植表空间的步骤如下：

1）在原来的数据库中将表空间的状态置为READ ONLY。

2）利用EXP命令将表空间的结构信息导出到元数据文件中。

3）在原来的数据库中对表空间进行格式转换，将转换以后的数据文件复制到对方系统中。或者将数据库文件复制到对方系统中之后，再进行格式转换。

4）在对方数据库中利用IMP命令将表空间导入。在导入时指定元数据文件和转换后的数据文件。

5）在原来的数据库中将表空间置为READ WRITE状态。

实际上，利用RMAN还可以在ASM磁盘组和文件系统之间复制文件。如果把数据库中的文件存储在ASM磁盘组中，在操作系统中是看不到这些文件的，当然也不能对它们进行复制等操作。利用RMAN可以把磁盘组中的文件复制到文件系统中，或者把操作系统中的文件复制到磁盘组中。例如，下面的命令用于把磁盘组DG1中的两个数据文件复制到/tmp目录中：

RMAN＞CONVERT DATAFILE

'+dg1/orcl/datafile/tools01.dbf'，

'+dg1/orcl/datafile/tools02.dbf'

DB_FILE_NAME_CONVERT

'+dg1/orcl/datafile'，'/tmp'；

附录A　Oracle 11g在AIX下的安装

Oracle软件在Windows系统中的安装是非常简单的，只要软件、硬件满足要求，任何工程师都能进行安装。然而Oracle在UNIX/Linux系统中的安装是比较困难的，这个过程涉及对系统进行很多的设置，还要涉及多个用户及用户组。本书在最后分三个附录分别介绍Oracle 11g在AIX、Solaris和Linux三种操作系统中的安装过程。

对不同系统进行配置的方法是不一样的，然而一旦开始安装，就没有多大区别了，安装的图形界面也基本上是一样的。在这里，我们对AIX的系统配置和安装过程进行了详细的描述，而在后面的两个附录中，仅仅对Solaris和Linux系统所要进行配置进行了描述，省略了安装过程。

在本附录中出现的所有图片，都是在实际的安装过程中截屏幕而来的。为了保证图片尽量清晰，在这里只保留了图片的上半部分，对下半部分进行了裁减。图片的下半部分主要包含一些按钮，如“Next”、“Back”、“Finish”等。

需要注意的是，如果要使用ASM，在安装Oracle软件之前，需要先安装Grid Infrastructure。经验证明，在单机数据库中，用户更喜欢使用文件系统来存储数据库中的文件，而不喜欢使用ASM技术。尽管Oracle推荐用户使用ASM，但是这项技术主要用在RAC环境中。读者可以根据自己的实际情况来决定是否安装Grid Infrastructure软件。

A.1　系统需要满足什么条件

为了安装Oracle软件，创建Oracle数据库，计算机必须满足一定的硬件、软件条件。在表A.1中列出了所需的硬件环境。

在表A.2中列出了安装Oracle所需要的软件环境。

另外，还需要对一些内核参数进行调整。表A.3中列出了需要调整的内核参数以及推荐值。

为了在AIX中安装Oracle软件，还需要在系统中创建一些用户组和用户。表A.4和表A.5分别列出了所需的用户组和用户。

针对oracle用户和grid用户，我们还需要分别设置它们在SHELL中对资源的使用情况，如对CPU、内存、数据段等。为了保证数据库能够运行，需要这些资源的限制设置为-1，即没有限制。表A.6中列出了需要设置的SHELL资源限制。

一般来说，要安装Oracle软件，首先以root用户登录系统，创建上述用户组和用户。如果使用ASM，先以grid用户登录系统，安装Grid Infrastructure软件，并且创建ASM实例和ASM磁盘组。然后以oracle用户登录系统，安装Oracle数据库软件，并且创建数据库，以后对数据库的所有管理工作，都是以oracle用户的身份来完成的。为了简化安装过程，我们可以把oracle用户和grid用户合二为一，只创建oracle用户，这个用户同时属于上述所有用户组，以后所有Oracle产品的安装和配置工作，都以这个用户身份来完成。

A.2　root用户需要完成的工作

root用户的工作主要涉及在操作系统中所做的配置，如创建用户和用户组，配置内核参数，配置存储设备，安装必要的软件，创建必要的目录，指定目录权限等。以下命令序列用于检查系统的软件、硬件环境：

#oslevel-s　检查操作系统版本以及补丁级别

#getconf HARDWARE_BITMODE　检查硬件体系结构

#getconf KERNEL_BITMODE　检查内核位数

#lsattr-EH-l sys0-a realmem　检查物理内存大小

#lsps-a　检查SWAP空间的大小

#df-m/home　以MB为单位察看/home文件系统的空间使用情况

#lslpp-l bos.adt.base bos.adt.lib bos.adt.libm bos.perf.perfstat bos.perf.libperfstat bos.perf.proctools　检查指定的软件是否已经安装

#lsattr-EH-l sys0-a ncargs　检查内核参数ncargs的值

#lsattr-EH-l sys0-a maxuproc　检查内核参数maxuproc的值

#lsattr-EH-l aio0-a maxreqs　在AIX 5.3中检查内核参数maxreqs的值

#ioo-o aio_maxreqs　在AIX 6.1中检查内核参数maxreqs的值

如果硬件体系结构、内核位数或者内存大小不满足要求，Oracle将无法安装。如果文件系统大小或SWAP空间大小不满足要求，则可以动态扩展，条件是有足够的硬盘空间。例如：

#chps-s 10 hd6　将SWAP空间hd6扩展10个逻辑分区

#chfs-a size=+5G/home　将文件系统/home扩展5G的存储空间

如果发现某个系统软件没有安装，可以利用installp命令或者smit工具进行安装。

以下命令用于调整内核参数：

#chdev-l sys0-a ncargs=128

#chdev-l sys0-a maxuproc=16384

#chdev-P-l aio0-a maxreqs=65536　在AIX 5.3中设置内核参数maxreqs的值

#ioo-o aio_maxreqs=65536　在AIX 6.1中检查内核参数maxreqs的值

以下命令序列用于创建必要的用户和用户组，并为用户指定口令：

#mkgroup oinstall

#mkgroup dba

#mkgroup asmadmin

#mkgroup asmdba

#mkuser pgrp=oinstall groups=dba, asmdba oracle

#passwd oracle

#mkuser pgrp=oinstall groups=asmadmin, asmdba grid

#passwd grid

为了对grid用户和oracle用户在SHELL中使用的资源进行限制，可以执行以下命令，按照表A.6中的推荐值对两个用户分别进行设置：

smit chuser

或者可以直接修改文件/etc/security/limits，在该文件中以下面的格式分别对两个用户进行设置：

oracle：

fsize=-1

fsize_hard=-1

core=2097151

cpu=-1

data=262144

rss=65536

stack=65536

nofiles=2000

nofiles_hard=1655536

以下命令用于创建必要的目录，并且指定相应的权限，Oracle软件将被安装在这样的目录下。需要注意的是，oracle用户对这些目录必须具有写权限。

#mkdir-p/u01/app

#mkdir/u01/app/base

#mkdir/u01/app/oracle

#chown-R oracle：oinstall/u01/app

#chmod-R ug+w/u01/app

A.3　oracle用户需要完成的工作

Oracle用户的工作主要涉及配置自己的环境变量，安装软件，管理数据库等。以oracle用户登录系统，编辑主目录下的文件.dtprofile，在该文件中设置以下环境变量：

ORACLE_BASE=/u01/app/base

ORACLE_HOME=/u01/app/oracle

DISPLAY=127.0.0.1：0.0

PATH=$ORACLE_HOME/bin：$PATH

export ORACLE_BASE ORACLE_HOME DISPLAY PATH

为了使环变量生效，需要重新登录系统。通过下面的方式，可以检查各个变量的设置是否正确：

echo$ORACLE_HOME

如果oracle用户在本机登录，变量DISPLAY指定本机的IP地址即可，如果用户从PC机中以图形方式远程登录，那么变量DISPLAY应该指定PC机的IP地址。例如：

DISPLAY=192.168.1.100：0.0

A.4　开始安装Oracle软件

把操作系统中的所有工作完成之后，就可以开始安装Oracle软件了。实际上，在UNIX/Linux系统中安装Oracle的主要难点就是操作系统中的这些工作。以oracle用户登录系统，在Oracle安装文件所在的目录下，执行以下命令，开始安装：

./runInstaller

在执行这个命令时，安装程序提问是否已经执行脚本rootpre.sh。如果没有执行，应该先以root身份执行这个脚本，然后重新开始安装。这个脚本检查是否有以前版本的Oracle进程在执行，在AIX 5.3中，这个脚本还将激活异步输入输出进程（aio）。

在出现的欢迎界面中，单击“Next”按钮，出现如图A.1的对话框，在这里输入E-mail地址和Oracle技术支持的密码，这样就可以经常接收到Oracle公司发送的安全性补丁。如果没有这样的密码，可以忽略。单击“Next”按钮。

图　A.1　获得Oracle安全更新的途径

在图A.2中，有三种选择：安装软件并创建一个数据库；只安装软件；对已经存在的数据库进行升级。选择第二项，单击“Next”按钮。软件安装完成后可以利用DBCA或者命令行来创建数据库。

在图A.3中，有两个选择：在单机上安装软件；在RAC环境中安装软件。选择第一项，单击“Next”按钮。

在图A.4中，选择Oracle软件运行的语言环境，默认为英语，在这里可以添加其他的语言环境，如中文。语言环境的选择将对数据库中的字符集产生影响。单击“Next”按钮。

图　A.2　选择安装方式

图　A.3　选择单机安装或者RAC安装

图　A.4　选择安装的语言环境

在图A.5中选择安装Oracle企业版还是标准版。选择企业版，单击“Next”按钮。

在图A.6中，指定Oracle软件的安装目录。如果环境变量ORACLE_BASE和ORACLE_HOME设置正确，在这里将自动出现环境变量所指定的目录。单击“Next”按钮。

在图A.7中，选择oracle用户所属的dba和oper用户组，如果没有创建oper用户组，可以用dba组代替。单击“Next”按钮继续。

图　A.5　选择安装企业版还是标准版

图　A.6　指定安装目录

图　A.7　选择相关的用户组

在图A.8中，安装程序对系统的软件、硬件环境进行检查，以确定当前系统是否满足Oracle安装所需要的条件。如果有不满足的情况，需要到系统中解决这样的问题，然后重新安装。如果所有条件都满足，单击“Next”按钮，将出现图A.9所示的确认信息。在图A.9中单击“Next”按钮，安装程序便开始安装软件，如图A.10所示。安装过程需要较长的一段时间。

图　A.8　安装程序对安装的硬件、软件环境进行检查

图　A.9　安装之前的确认信息

图　A.10　开始安装

在安装即将结束时，安装程序将弹出一个对话框，在这里要求root用户执行指定的脚本，如图A.11所示。

图　A.11　安装结束后需要执行的脚本

在当前的系统桌面环境CDE中，打开另外一个终端窗口，通过su命令切换到root用户，在命令行中执行指定的脚本，按照提示输入相关的信息，一般只要直接按回车键即可。第一个脚本的执行情况是：

#/u01/base/oraInventory/orainstRoot.sh

Changing permissions of/u01/base/oraInventory.

Adding read, write permissions for group.

Removing read, write, execute permissions for world.

Changing groupname of/u01/base/oraInventory to oinstall.

The execution of the script is complete.

第二个脚本的执行情况是：

#/u01/app/oracle/root.sh

Running Oracle 11g root.sh script……

The following environment variables are set as：

ORACLE_OWNER=oracle

ORACLE_HOME=/u01/app/oracle

Enter the full pathname of the local bin directory：[/usr/local/bin]：

Copying dbhome to/usr/local/bin……

Copying oraenv to/usr/local/bin……

Copying coraenv to/usr/local/bin……

Creating/etc/oratab file……

Entries will be added to the/etc/oratab file as needed by

Database Configuration Assistant when a database is created

Finished running generic part of root.sh script.

Now product-specific root actions will be performed.

Finished product-specific root actions.

上述脚本执行之后，在图A.11中点击“OK”按钮，整个安装过程结束。随后oracle用户便可利用DBCA或者命令行创建数据库了。

附录B　Oracle 11g在Linux下的安装

这里以RedHat AS5为例，介绍在安装Linux中Oracle 11g软件之前，需要对操作系统做的配置工作。Oracle 11g软件的安装过程在这里不再介绍，详细安装情况请参阅附录A。

B.1　系统需要满足什么条件

安装Oracle软件所需要满足的硬件条件请参阅附录A。

在表B.1中列出了安装Oracle所需要的软件环境。

如果Linux的内核是64位的，那么和C/C++有关的32位、64位开发工具软件都需要安装。如gcc-c++、glibc等。

表B.2列出了需要调整的内核参数。

为了在Linux中安装Oracle软件，还需要在系统中创建一些用户组和用户。表B.3和表B.4分别列出了所需的用户组和用户。

针对oracle用户和grid用户，我们还需要分别设置它们在SHELL中对资源的使用情况。表B.5中列出了需要设置的SHELL资源限制。

一般来说，要安装Oracle软件，首先以root用户登录系统，创建上述用户组和用户。如果使用ASM，先以grid用户登录系统，安装Grid Infrastructure软件，并且创建ASM实例和ASM磁盘组。然后以oracle用户登录系统，安装Oracle数据库软件，并且创建数据库，以后对数据库的所有管理工作，都是以oracle用户的身份来完成的。为了简化安装过程，我们可以把oracle用户和grid用户合二为一，只创建oracle用户，这个用户同时属于上述所有用户组，以后所有Oracle产品的安装和配置工作，都以这个用户身份来完成。

B.2　root用户需要完成的工作

root用户的工作主要涉及创建用户和用户组，配置内核参数，配置存储设备，安装必要的软件，创建必要的目录，指定目录权限等。

以下命令序列用于检查系统的软件、硬件是否满足安装要求：

#grep MemTotal/proc/meminfo　查看内存大小

#grep SwapTotal/proc/meminfo　查看SWAP空间大小

#df-m　以MB为单位查看文件系统的使用情况

#uname-a　查看内核版本以及位数

#rpm-q setarch glibc　查看所需软件是否已经安装，只要分别指定每个软件即可

以下命令用于安装必要的系统软件：

#rpm-ivh setarch-2.0-1.1.i386.rpm

#rpm-ivh compat-libstdc++-33-3.2.3-61.i386.rpm

……

以下命令用于创建必要的用户和用户组，并为用户指定口令：

#groupadd oinstall

#groupadd dba

#groupadd asmdba

#groupadd asmadmin

#useradd-g oinstall-G dba, asmdba oracle

#passwd oracle

#useradd-g oinstall-G admadmin, asmdba grid

#passwd grid

以下命令用于创建必要的目录，并且指定相应的权限，Oracle软件将被安装在这样的目录下。需要注意的是，oracle用户对这些目录必须具有写权限。

#mkdir-p/u01/app

#mkdir/u01/app/base

#mkdir/u01/app/oracle

#chown-R oracle：oinstall/u01/app

#chmod-R ug+w/u01/app

以下脚本用于设置内核参数：

#cat＞＞/etc/sysctl.conf＜＜EOF

kernel.shmall=2097152

kernel.shmmax=536870912

kernel.shmmni=4096

kernel.sem=250 32000 100 128

fs.file-max=658576

net.ipv4.ip_local_port_range=1024 65000

net.core.rmem_default=262144

net.core.wmem_default=262144

net.core.rmem_max=1048536

net.core.wmem_max=1048536

aio-max-nr=1048576

EOF

为了使上述内核参数起作用，还需要系统中执行下面的命令：

#/sbin/sysctl-p

以下脚本用于对oracle用户和grid用户设置SHELL资源限制：

#cat＞＞/etc/security/limits.conf＜＜EOF

oracle soft nproc 2047

oracle hard nproc 16384

oracle soft nofile 1024

oracle hard nofile 65536

grid soft nproc 2047

grid hard nproc 16384

grid soft nofile 1024

grid hard nofile 65536

EOF

以下脚本用于指定一个可加载模块：

#cat＞＞/etc/pam.d/login＜＜EOF

session required/lib/security/pam_limits.so

EOF

B.3　oracle用户需要完成的工作

oracle用户的工作主要涉及配置自己的环境变量，安装软件，管理数据库等。以oracle用户登录系统，编辑主目录下的文件.bash_profile，在该文件中设置以下环境变量：

ORACLE_BASE=/u01/app/base

ORACLE_HOME=/u01/app/oracle

DISPLAY=127.0.0.1：0.0

PATH=$ORACLE_HOME/bin：$PATH

export ORACLE_BASE ORACLE_HOME DISPLAY PATH

为了使环变量生效，需要重新登录系统。通过下面的方式，可以检查各个变量的设置是否正确：

echo$ORACLE_HOME

如果oracle用户在本机登录，变量DISPLAY指定本机的IP地址即可，如果用户从PC机中以图形方式远程登录，那么变量DISPLAY应该指定PC机的IP地址。例如：

DISPLAY=192.168.1.100：0.0

附录C　Oracle 11g在Solaris下的安装

这里以Solaris 10 For Sparc为例，介绍在Solaris系统中安装Oracle 11g软件之前，需要对操作系统做的配置工作。Oracle 11g软件的安装过程在这里不再介绍，详细安装情况请参阅附录A。

C.1　系统需要满足什么条件

安装Oracle软件所需要满足的硬件条件请参阅附录A。

在表C.1中列出了安装Oracle所需要的软件环境。

表C.2列出了需要调整的内核参数。

为了在Solaris中安装Oracle软件，还需要在系统中创建一些用户组和用户。表C.3和表C.4分别列出了所需的用户组和用户。

一般来说，要安装Oracle软件，首先以root用户登录系统，创建上述用户组和用户。如果使用ASM，先以grid用户登录系统，安装Grid Infrastructure软件，并且创建ASM实例和ASM磁盘组。然后以oracle用户登录系统，安装Oracle数据库软件，并且创建数据库，以后对数据库的所有管理工作，都是以oracle用户的身份来完成的。为了简化安装过程，我们可以把oracle用户和grid用户合二为一，只创建oracle用户，这个用户同时属于上述所有用户组，以后所有Oracle产品的安装和配置工作，都以这个用户身份来完成。

C.2　root用户需要完成的工作

root用户的工作主要涉及创建用户和用户组，配置内核参数，配置存储设备，安装必要的软件，创建必要的目录，指定目录权限等。

以下命令序列用于检查系统的软件、硬件是否满足安装要求：

#/usr/sbin/prtconf|grep"Memory size"查看内存大小

#/usr/sbin/swap-l　查看SWAP空间大小，单位为512字节的块

#/bin/isainfo-kv　查看硬件位数

#df-k　以KB为单位查看文件系统的使用情况

#uname-r　查看操作系统版本

#cat/etc/release　查看操作系统的release号

#pkginfo-i SUNWarc SUNWbtool SUNWhea　查看所需软件是否已经安装

以下命令用于安装必要的系统软件：

#pkgadd-d/cdrom/cdrom0/Solaris_10/Product SUNWarc

#pkgadd-d/cdrom/cdrom0/Solaris_10/Product SUNWbtool

在文件/etc/system中添加以下内容，以配置内核参数：

noexec_user_stack=1

semsys：seminfo_semmni=100

semsys：seminfo_semmns=1024

semsys：seminfo_semmsl=256

semsys：seminfo_semvmx=32767

shmsys：shminfo_shmmax=4294967295

shmsys：shminfo_shmmni=100

为了使内核参数起作用，需要重新启动系统。

以下命令用于创建必要的用户和用户组，并为用户指定口令：

#groupadd oinstall

#groupadd dba

#groupadd asmdba

#groupadd asmadmin

#useradd-g oinstall-G dba, asmdba-s/usr/bin/bash oracle

#passwd oracle

#useradd-g oinstall-G admadmin, asmdba-s/usr/bin/bash grid

#passwd grid

以下命令用于创建必要的目录，并且指定相应的权限，Oracle软件将被安装在这样的目录下。需要注意的是，oracle用户对这些目录必须具有写权限。

#mkdir-p/u01/app

#mkdir/u01/app/base

#mkdir/u01/app/oracle

#chown-R oracle：oinstall/u01/app

#chmod-R ug+w/u01/app

C.3　oracle用户需要完成的工作

oracle用户的工作主要涉及配置自己的环境变量，安装软件，管理数据库等。以oracle用户登录系统，编辑主目录下的文件.bash_profile，在该文件中设置以下环境变量：

ORACLE_BASE=/u01/app/base

ORACLE_HOME=/u01/app/oracle

DISPLAY=127.0.0.1：0.0

PATH=$ORACLE_HOME/bin：$PATH

export ORACLE_BASE ORACLE_HOME DISPLAY PATH

为了使环变量生效，需要重新登录系统。通过下面的方式，可以检查各个变量的设置是否正确：

echo$ORACLE_HOME

如果oracle用户在本机登录，变量DISPLAY指定本机的IP地址即可，如果用户从PC机中以图形方式远程登录，那么变量DISPLAY应该指定PC机的IP地址。例如：

DISPLAY=192.168.1.100：0.0

目　录

前言

第一部分　Oracle使用基础

第1章　SQL语言基础

1.1　查询语句

1.1.1　查询语句的基本用法

1.1.2　查询语句中的条件

1.1.3　查询语句中的单行函数

1.1.4　分组函数与分组统计

1.1.5　数据的排序

1.1.6　多表查询

1.1.7　子查询

1.2　DML语句

1.2.1　INSERT语句

1.2.2　DELETE语句

1.2.3　UPDATE语句

1.3　事务控制语句

1.4　DDL语句

1.4.1　表的创建

1.4.2　如何修改表的结构

1.4.3　其他DDL命令

1.5　约束

1.5.1　约束的类型

1.5.2　如何在创建表时指定约束

1.5.3　如何在创建表之后指定约束

1.5.4　约束的维护

1.6　视图

1.6.1　视图的创建、修改和删除

1.6.2　如何对视图进行访问

1.6.3　复杂视图

1.7　索引

1.7.1　索引的基本概念

1.7.2　索引的创建、修改和删除

1.7.3　索引信息的查询

1.8　序列

1.8.1　序列的创建、修改和删除

1.8.2　序列的使用

1.8.3　序列信息的查询

1.9　同义词

1.9.1　同义词的概念和类型

1.9.2　同义词的创建与删除

1.9.3　同义词信息的查询

第2章　SQL*Plus用法指南

2.1　SQL*Plus的基本用法

2.1.1　登录与退出

2.1.2　如何获取帮助信息

2.1.3　如何修改SQL*Plus的设置信息

2.1.4　实例的启动与关闭

2.2　SQL*Plus中的缓冲区

2.2.1　执行缓冲区中的内容

2.2.2　编辑缓冲区的内容

2.2.3　如何对操作系统文件进行读写

2.3　如何在SQL*Plus中使用变量

2.3.1　用户自定义的变量

2.3.2　参数变量

2.3.3　与变量有关的交互式命令

2.4　SQL*Plus的报表功能

2.4.1　报表的标题设计

2.4.2　报表显示格式的设计

2.4.3　如何对特定列进行统计

第3章　PL/SQL编程

3.1　PL/SQL概述

3.2　PL/SQL中的变量

3.2.1　变量的定义与使用

3.2.2　如何在PL/SQL中定义类型

3.3　PL/SQL中的流控制

3.3.1　IF语句

3.3.2　LOOP语句

3.3.3　WHILE语句

3.3.4　FOR语句

3.4　PL/SQL如何访问数据库

3.4.1　如何对数据进行查询

3.4.2　如何使用DML语句

3.5　子程序设计

3.5.1　如何使用过程

3.5.2　如何使用函数

3.5.3　函数与过程的重载

3.5.4　函数与过程的递归调用

3.6　存储过程与存储程序

3.6.1　存储过程

3.6.2　存储函数

3.6.3　程序包

3.6.4　系统预定义程序包

3.6.5　与存储程序有关的数据字典

3.7　异常处理

3.7.1　异常处理程序

3.7.2　预定义的异常

3.7.3　非预定义异常

3.7.4　用户自定义的异常

3.7.5　异常的传递

3.8　游标的应用

3.8.1　隐式游标

3.8.2　显式游标

3.8.3　带参数的游标

3.8.4　如何通过游标修改表中的数据

3.9　触发器

3.9.1　触发器的使用

3.9.2　语句级触发器

3.9.3　行触发器

3.9.4　视图上的触发器

3.9.5　与触发器有关的数据字典

第二部分　Oracle DBA

第4章　Oracle体系结构

4.1　实例的体系结构

4.1.1　实例的概念

4.1.2　实例的组成

4.2　实例的内存结构

4.2.1　数据库高速缓存

4.2.2　重做日志缓冲区

4.2.3　共享池

4.2.4　Java池

4.2.5　PGA

4.3　实例中的后台进程

4.3.1　DBWR进程

4.3.2　LGWR进程

4.3.3　CKPT进程

4.3.4　SMON进程

4.3.5　PMON进程

4.3.6　ARCH进程

4.4　实例的内存结构管理

4.4.1　自动内存管理

4.4.2　自动共享内存管理

4.4.3　手工共享内存管理

4.5　数据库的连接模式

4.5.1　专用数据库连接模式

4.5.2　共享数据库连接模式

4.5.3　如何设置共享连接模式

4.6　数据库的逻辑结构

4.6.1　表空间

4.6.2　段

4.6.3　区

4.6.4　数据块

4.7　数据库的物理结构

4.7.1　数据文件

4.7.2　控制文件

4.7.3　重做日志文件

4.7.4　跟踪文件和警告文件

4.8　特权用户与口令文件

4.9　数据字典视图与动态性能视图

4.9.1　数据字典视图

4.9.2　动态性能视图

4.10　初始化参数

4.10.1　参数文件

4.10.2　初始化参数的查看

4.10.3　初始化参数的修改

第5章　数据库的创建

5.1　数据库的规划

5.1.1　SGA的规划

5.1.2　数据文件的规划

5.1.3　控制文件的规划

5.1.4　重做日志文件的规划

5.1.5　参数文件的规划

5.2　如何利用DBCA创建数据库

5.3　如何利用命令行创建数据库

5.3.1　编辑文本参数文件

5.3.2　实例的管理

5.3.3　口令文件的创建

5.3.4　数据库的创建

5.3.5　如何创建数据字典视图

5.3.6　如何创建默认的profile

5.3.7　如何创建SCOTT模式

5.3.8　如何创建服务器参数文件

5.4　数据库服务器的启动和关闭

5.5　如何利用NET Manager配置客户端与服务器端的通信

5.5.1　监听器的创建

5.5.2　监听器的管理

5.5.3　Oracle客户端的配置

第6章　表空间的管理

6.1　表空间的结构

6.1.1　区管理方式

6.1.2　段管理方式

6.1.3　数据文件

6.2　本地管理表空间的管理

6.2.1　本地管理表空间的创建

6.2.2　表空间信息的查询

6.2.3　表空间的删除

6.3　大文件表空间的管理

6.3.1　大文件表空间的支持

6.3.2　大文件表空间的创建

6.3.3　大文件表空间的修改

6.4　临时表空间的管理

6.4.1　临时表空间的创建

6.4.2　临时表空间组

6.5　UNDO表空间的管理

6.5.1　UNDO表空间的创建

6.5.2　UNDO表空间的切换

6.6　表空间的扩展

6.6.1　如何添加新的数据文件

6.6.2　如何扩展数据文件

6.7　表空间的维护

6.7.1　表空间的联机与脱机

6.7.2　数据文件的联机与脱机

6.7.3　表空间的读写权限

6.7.4　数据文件的移动和重命名

第7章　存储空间管理

7.1　段的管理

7.1.1　段的类型

7.1.2　段的空间管理

7.2　区的管理

7.2.1　区的分配

7.2.2　区的回收

7.3　数据块的管理

7.3.1　数据块的组成

7.3.2　数据块的空间管理

第8章　控制文件管理

8.1　控制文件的规划

8.1.1　控制文件的镜像

8.1.2　控制文件的存储位置

8.2　控制文件的重新创建

8.2.1　如何增加新的控制文件

8.2.2　如何重新创建控制文件

8.3　控制文件的备份与删除

8.4　控制文件信息的查询

8.4.1　查询控制文件的位置和名称

8.4.2　查询控制文件中记录的信息

第9章　重做日志管理

9.1　重做日志的规划

9.1.1　重做日志缓冲区的规划

9.1.2　重做日志文件组的规划

9.1.3　如何对重做日志文件进行规划

9.2　重做日志文件的管理

9.2.1　增加重做日志组

9.2.2　增加日志成员

9.2.3　修改重做日志文件的存储位置和名称

9.2.4　删除重做日志文件

9.2.5　重做日志文件的清空

9.2.6　重做日志的切换

9.2.7　重做日志信息的查询

9.3　归档日志的管理

9.3.1　数据库的日志模式

9.3.2　切换日志模式

9.3.3　设置归档位置

9.3.4　归档信息的查询

9.4　如何对重做日志进行分析

9.4.1　如何创建字典文件

9.4.2　如何创建分析列表

9.4.3　如何开始日志分析

9.4.4　如何查看日志分析结果

9.4.5　如何结束日志分析

第10章　基本数据库对象管理

10.1　表的管理

10.1.1　表的结构

10.1.2　表的创建

10.1.3　表的修改

10.1.4　表的删除

10.2　分区表的管理

10.2.1　分区的概念

10.2.2　范围分区

10.2.3　列表分区

10.2.4　散列分区

10.2.5　复合分区

10.3　索引的管理

10.3.1　索引概述

10.3.2　反向索引

10.3.3　位图索引

10.3.4　基于函数的索引

10.3.5　分区索引

10.3.6　索引的维护

10.4　簇的管理

10.4.1　簇的创建

10.4.2　簇的修改

10.4.3　簇的删除

10.4.4　簇信息的查询

10.5　索引组织表的管理

10.5.1　索引组织表的概念

10.5.2　索引组织表的创建

10.5.3　索引组织表的维护

第11章　用户与权限管理

11.1　用户管理

11.1.1　数据库中有哪些用户

11.1.2　如何创建用户

11.1.3　如何修改用户的信息

11.1.4　如何删除用户

11.2　用户权限的管理

11.2.1　系统权限的管理

11.2.2　对象权限的管理

11.2.3　权限信息的查询

11.3　角色的管理

11.3.1　角色的创建和删除

11.3.2　角色中权限的添加和删除

11.3.3　角色的分配和回收

11.3.4　角色信息的查询

11.4　PROFILE的管理

11.4.1　PROFILE的创建与删除

11.4.2　如何利用PROFILE对用户口令进行控制

11.4.3　如何利用PROFILE对用户使用资源进行控制

11.4.4　默认的PROFILE

第三部分　自动文件管理和自动存储管理

第12章　自动文件管理

12.1　如何激活自动文件管理功能

12.2　文件的命名规则

12.3　如何创建OMF数据库

12.4　如何创建OMF表空间

12.5　如何创建OMF控制文件

12.6　如何创建OMF重做日志文件

第13章　自动存储管理

13.1　ASM实例

13.2　磁盘组的管理

13.3　如何使用ASM磁盘组

第四部分　备份与恢复

第14章　数据库的导入与导出

14.1　导入导出工具的用法

14.2　表的导入与导出

14.3　用户模式的导入与导出

14.4　数据库的导入与导出

14.5　表空间的导入与导出

第15章　数据库的常规备份与恢复

15.1　备份与恢复的相关概念

15.1.1　冷备份与热备份

15.1.2　物理备份与逻辑备份

15.1.3　完全备份与增量备份

15.1.4　备份策略

15.1.5　完全恢复与不完全恢复

15.1.6　日志模式对备份与恢复的影响

15.1.7　哪些情况将导致数据丢失

15.1.8　哪些文件需要备份

15.2　控制文件的备份与恢复

15.3　重做日志文件的备份与恢复

15.4　数据文件的备份

15.5　数据库的完全恢复

15.6　两个实际的备份与恢复的例子

15.6.1　模拟数据文件损坏的例子

15.6.2　模拟磁盘损坏的例子

15.7　Flashback技术在数据库恢复中的应用

15.7.1　回收站的应用

15.7.2　Flashback技术在表上的应用

15.7.3　Flashback技术在数据库恢复中的应用

第16章　如何利用RMAN对数据库进行备份与恢复

16.1　RMAN的基本结构

16.2　RMAN的配置

16.2.1　如何配置RMAN客户端的连接

16.2.2　恢复目录的创建

16.3　如何利用RMAN对数据库进行备份

16.3.1　通道的设置

16.3.2　存储脚本的用法

16.3.3　控制文件的备份

16.3.4　参数文件的备份

16.3.5　归档日志文件的备份

16.3.6　非归档模式下数据文件的备份

16.3.7　归档模式下数据文件的备份

16.3.8　备份集的备份

16.4　如何对数据库进行完全恢复

16.4.1　如何对备份文件进行校验

16.4.2　如何对数据文件进行恢复

16.5　两个实际的例子

16.5.1　模拟数据文件损坏的例子

16.5.2　模拟磁盘损坏的例子

16.6　如何对坏块进行恢复

16.6.1　什么叫块介质恢复

16.6.2　如何进行块介质恢复

16.7　如何对数据进行跨平台移植

16.7.1　字节存储次序相同时的移植

16.7.2　字节存储次序不同时的移植

附录A　Oracle 11g在AIX下的安装

A.1　系统需要满足什么条件

A.2　root用户需要完成的工作

A.3　oracle用户需要完成的工作

A.4　开始安装Oracle软件

附录B　Oracle 11g在Linux下的安装

B.1　系统需要满足什么条件

B.2　root用户需要完成的工作

B.3　oracle用户需要完成的工作

附录C　Oracle 11g在Solaris下的安装

C.1　系统需要满足什么条件

C.2　root用户需要完成的工作

C.3　oracle用户需要完成的工作