目录
数据库完整性
实体完整性
· 定义实体完整性
· 实体完整性检查和违约处理
参照完整性
· 定义参照完整性
· 参照完整性检查和违约处理
用户定义的完整性
· 属性上的约束条件
· 元祖上的约束条件
完整性约束命名子句
· 完整性约束命名子句
· 修改表中的完整性限制
域中的完整性限制
断言
· 创建断言的语句格式
· 删除断言的语句格式
触发器
· 定义触发器
· 激活触发器
· 删除触发器
数据库的完整性是指数据的正确性和相容性。数据的正确性是指数据是符合现实世界语义、反映当前实际状况的;数据的相容性是指数据库同一对象在不同关系表中的数据是符合逻辑的。
数据的完整性和安全性是两个既有联系又不尽相同的概念。数据的完整性是为了防止数据库中存在不符合语义的数据,也就是防止数据库中存在不正确的数据。数据的安全性是保护数据库防止恶意破坏和非法存取。因此,完整性检查和控制的防范对象是不合语义的、不正确的数据,防止它们进入数据库。安全性控制的防范对象是非法用户和非法操作,防止它们对数据库数据的非法存取。
为了维护数据库的完整性,数据库管理系统需要具备如下功能:
1、提供定义完整性约束条件的机制
完整性约束条件也称为完整性规则,是数据库中的数据必须满足的语义约束条件。它表达了给定的数据模型中数据及其联系所具有的制约和依存规则,用以限定符合数据模型的数据库状态以及状态的变化,以保证数据的正确、有效和相容、SQL标准使用了一系列概念来描述完整性,包括关系模型的实体完整性、参照完整性和用户定义完整性。这些完整性一般由 SQL 的数据定义语言语句来实现,它们作为数据库模式的一部分存入数据字典中。
2、提供完整性检查的方法
数据库管理系统中检查数据是否满足完整性约束条件的机制称为完整性检查。一般在 insert 、update 、 delete 语句执行后开始检查,也可以在事务提交时检查。检查这些操作执行后数据库中的数据是否违背了完整性约束条件。
3、进行违约处理
数据库管理系统若发现用户的操作违背了完整性约束条件将采取一定的动作,如拒绝 (no action) 执行该操作或级联 (cascade) 执行其他操作,进行违约处理以保证数据的完整性。
早期的数据库管理系统不支持完整性检查,因为完整性检查时费资源。现在商用的关系数据库管理系统产品都支持完整性规则,即完整性定义和检查控制由关系数据库管理系统实现,不必由应用程序来完成,从而减轻了应用程序员的负担。更重要的是,关系数据库管理系统使得完整性控制成为其核心支持的功能,从而能够为所有用户和应用提供一致的数据库完整性。因为由应用程序来实现完整性控制是有漏洞的,有的应用程序定义的完整性约束条件可能被其他应用程序来破坏,数据库数据的正确性仍然无法保障。
前面在数据定义时提到了关系数据库三类完整性约束的基本概念,这篇就是来解释 SQL语言中实现这些完整性控制功能的方法。
关系模型的实体完整性在 create table 中用 primary key 定义。对单属性构成的码有两种表示方法,一种是定义为列级约束条件,另一种是定义为表级约束条件。而对于有多个属性构成的码只有一种说明方法,即定义为表级约束条件。这在前面数据定义时已提到过,这里再说一下,具体例子如:
create table Student(
Sno char(9) primary key , /*列级定义主码*/
...
Sname char(20) not null ,
)
create table Student(
Sno char(9) ,
...
Sdept char(20) ,
primary key (Sno) /*表级定义主码*/
)
create table Sc(
Sno char(9) ,
Cno char(4) ,
Grade smallint ,
primary key (Sno,Cno) /*只能表级定义主码*/
)
在用 primary key 定义了关系的主码后,每当用户对基本表插入一条记录或对主码列进行更新操作时,RDBMS (关系数据库管理系统) 将会按照实体完整性规则(前面数据定义中提到过)自动进行检查。包括:1)、检查主码值是否唯一,如果不唯一则拒绝插入或修改;2)、检查主码的各个属性是否为空,只要有一个为空则拒绝插入或修改。这样就保证了实体完整性。
检查记录中主码值是否唯一的一种方法是全盘扫描,依次判断表中每一条记录的主码值与将插入记录的主码值(或者修改的新主码值)是否相同,但这种扫描是十分耗时的。为了避免对基本表进行全表扫描,关系数据库管理系统一般都在主码上自动建立一个索引,如 B+ 树索引 (如果对 B+ 树不了解可以见博客 B+ 树索引),通过索引查找基本表中是否已经存在新的主码值时不需要查看全部值,而是看特定的几个结点即可,大大提高查找效率。
关系模型的参照完整性在 create table 时 用 foreign key 短语定义哪些列为外码,用 references 短语指明这些外码参照哪些表的主码。仍用前边已提到过的例子,学生数据库中的选课表,例:
create table SC(
Sno char(9) ,
Cno char(4) ,
Grade smallint ,
primary key (Sno,Cno) /*只能表级定义主码*/
foreign key(Sno) references Student(Sno), /*在表级定义参照完整性*/
foreign key(Cno) references Course(Cno)
)
参照完整性将两个表中相应的元组联系起来了,因此对被参照表和参照表进行增、删、改等操作时均有可能破坏参照完整性,必须进行检查以保证这两个表的相容性。下面举几种可能破坏参照完整性的情况(以 SC 表和 Student 表为例):
(1)、SC 表中增加一个元组,该元组的 Sno 属性值在表 Student 中找不到一个元组,其 Sno 属性值与之相等;(2)、修改 SC 表中的一个元组,修改后该元组的 Sno 属性值在表 Student 中找不到一个元组,其 Sno 属性值与之相等;(3)、从 Student 表中删除一个元组,造成 SC表中某些元祖的 Sno 属性值在表 Student 中找不到一个元组,其 Sno 属性值与之相等;(4)、修改 Student 表中一个元组的 Sno 属性,造成 SC 表中某些元祖的 Sno 属性值在表 Student 中找不到一个元组,其 Sno 属性值与之相等。
上述的几种情况发生时均可能破坏参照性完整性,而此时系统可以采用以下策略进行处理:
1)、拒绝执行:不允许该操作执行,该策略一般作为默认策略执行。2)、级联操作:当删除或修改被参照表 ( Student ) 的一个元组导致与参照表 ( SC ) 的不一致时,删除或修改参照表中的所有导致不一致的元组。3)、设置为空值:当删除或修改被参照表的一个元组时造成了不一致,则将参照表中的所有造成不一致的元组的对应属性设置为空值。
这里再说一下处理情况中第三种设置空值的情况,外码能否接受空值的问题。例如有下面关系 :学生(学号,姓名,性别,专业号,年龄),专业(专业号,专业名) ,这其中学生表中的 “专业号” 是外码,“专业号” 是专业表中的主码,假设专业表中的某个元祖被删除,专业号为 12,按照设置为空值的策略,就要把学生表中专业号=12 的所有元组的专业号设置为空值,这对应了这样的语义:某个专业被删除了,该专业的所有学生专业未定,这种情况是可以为空值的。但在学生数据库中,关系 Student 为被参照关系,其主码为 Sno ,SC 为 参照关系,Sno 为外码,它显然是不能取空值的,因为 Sno 为 SC 表的主属性,按照实体完整性 Sno 不能为空值,若 SC 的 Sno 为空值,则表明尚不存在的某个学生,或某个不知学号的学生选修了某门课程,其成绩记录在 Grade 列中,这与学校的应用环境是不相符的,因此 SC 表的 Sno 列不能取空值,同样 SC 的 Cno 是外码 ,也是 SC 的主属性,也不能取空值。
因此对于参照完整性,除了应该定义外码,还应定义外码列是否允许为空值。
一般情况下,当对参照表和被参照表的操作违反了参照完整性时,系统选用默认策略,即拒绝执行。如果想让系统采用其他策略则必须在创建参照表时显式地加以说明。例:
create table SC(
Sno char(9) ,
Cno char(4) ,
Grade smallint ,
primary key (Sno,Cno) /*只能表级定义主码*/
foreign key(Sno) references Student(Sno)
on delete cascade /*当删除 Student 表中的元组时,级联删除 SC 表中对应的元组*/
on update cascade /*当更新 Student 表中的元组时,级联更新 SC 表中对应的元组*/
,
foreign key(Cno) references Course(Cno)
on delete no action /*当删除 Course 表中的元组造成与 SC 表不一致时,拒绝删除*/
on update cascade /*当更新 Course 表中的 Cno 时,级联更新 SC 表中相应的元组*/
)
由上述例题看出,我们可以对 delete 和 update 采用不同的策略。当删除被参照表 Course 表中的元组,造成与参照表 (SC) 不一致时,拒绝删除被参照表的元组;对更新操作则采取级联更新的策略。
关系数据库管理系统在实现参照完整性时,除了要提供定义主码、外码的机制外,还需要提供不同的策略供用户选择,而具体选择哪种策略,要根据应用环境要求确定。
用户定义的完整性就是针对某一具体应用的数据必须满足的语义要求。目前的关系数据库管理系统都提供了定义和检验这类完整性的机制,使用了和实体完整性、参照完整性相同的技术和方法来处理它们,而不必由应用程序承担这一功能。
属性上约束条件的定义:
在 create table 中定义属性的同时,可以根据应用要求定义属性上的约束条件,即属性值限制,包括:列值非空 ( not null )、列值唯一 ( unique )、检查列值是否满足条件表达式 ( check 短语) 。例如:
create table Student (
Sno int primary key ,
Sname char(6) unique not null , /*属性值唯一且不能为空*/
Ssex char(2) check (Ssex in ('男','女')) , /*性别属性只允许取 '男' 或 '女'*/
...
)
create table SC(
Sno char(6) not null ,
Cno char(4) not null , /*Sno,Cno 属性不允许取空值*/
Grade smallint check (Grade >= 0 and Grade <=100), /*Grade 取值范围是 0~100*/
primary key (Sno,Cno)
)
属性上约束条件的检查和违约处理:
当往表中插入元组或修改属性的值时,关系数据库管理系统将检查属性上的约束条件是否被满足,如果不满足则操作被拒绝执行。
元祖上约束条件的定义:
与属性上约束条件的定义类似,在 create table 语句中可以用 check 短语定义元祖上的约束条件,即元组级的限制。同属性值限制相比,元组级的限制可以设置不同属性之间的取值的相互约束条件。例:
create table Student(
Sno char(9),
Sname char(6) not null ,
Ssex char(2) ,
Sage smallint ,
Sdept char(20) ,
primary key (Sno),
check (Ssex='女' or Sname not like 'Mr.%')
);
--定义了元组中 Sname 和 Ssex 两个属性值之间的约束条件。
在上述例题中,性别是女性的元组都能通过该项 check 检查,因为 Ssex='女' 成立;但当性别为男性时,要通过检查名字一定不能以 Mr. 开头,因为当 Ssex='男' 时,条件要想成为真值,Sname not like 'Mr.%' 必须为真值。
元祖上约束条件的检查和违约处理:
当往表中插入元组或修改属性的值时,关系数据库管理系统将检查元祖上的约束条件是否被满足,如果不满足则操作被拒绝执行。
前面所提到的完整性约束条件都是在 create table 语句中定义,此外,SQL 还在 create table 语句中提供了完整性约束命名子句 constraint ,用来对完整性约束条件命名,从而能更加灵活地增加、删除一个完整性约束条件。
完整性约束条件包括:not null 、unique 、primary key 、foreign key 、check 、default 短语等,其命名格式为:
constraint <完整性约束条件名><完整性约束条件>
下面给出示例:
create table Student(
Sno numeric(6)
constraint c1 check (Sno between 100 and 999),
Sname char(20)
constraint n1 not null ,
Sage numeric(3)
constraint c2 check (Sage<30),
Ssex char(2)
constraint c3 check(Ssex in ('男','女')),
constraint d1 default '女' , /*默认为 女 */
constraint StudentKey primary key(Sno) /*主码约束 StudentKey*/
);
--同样外键也是如此设置
--constraint F_Sno foreign key(Sno) references Student(Sno)
同前面 数据定义 篇中修改一样,仍可以使用 alter table 语句修改表中的完整性限制。例:
alter table Student
drop constraint c1 ; /*删除 约束名为 c1 的约束。*/
--修改表中约束条件时,可以先对原约束删除,再增加新的约束。
alter table Student
drop constraint c2 ;
alter table Student
add constraint C2 check (Sage<28 and Sage>18);
/*将年龄的约束条件改为 18~28 之间。*/
域是数据库中一个重要概念,一般地,域是一组具有相同数据类型的值的集合。SQL 支持域的概念,并可以用 create domain 语句建立一个域以及该域应该满足的完整性约束条件,然后就可以用域来定义属性。这样定义的优点是数据库中不同的属性可以来自同一个域,当域上的完整性约束条件改变时,只要修改域的定义即可,而不必一一修改域上的各个属性。例:
create domain GenderDomain char(2)
check (value in ('男','女'));
--建立一个性别域,并对其限制命名
create domain GenderDomain char(2)
constraint GD check (value in ('男','女'));
--删除域 GenderDoma 的限制条件 GD
alter domain GenderDomain
drop constraint GD ;
--在域 GenderDomain 上增加性别的限制条件 GDD。
alter domain GenderDomain
add constraint GDD check(value in('1','0'));
在 SQL 中可以使用数据定义语言中的 create assertion 语句,通过声明性断言来指定更具一般性的约束,可以定义涉及多个表或聚集操作的比较复杂的完整性约束。断言创建以后,任何对断言中所涉及关系的操作都会触发关系数据库管理系统对断言的检查,任何使断言不为真值的操作都会被拒绝执行。
create assertion <断言名>
create assertion Ass_SC_DB_NUM
check (60>=(select count(*)
from Course,SC
where SC.Cno=Course.Cno and Course.Cname='数据库')
);
上述定义的断言是用来限制数据库课程最多有 60 名学生选修。每当学生选修课程时,将在 SC 表中插入一条元祖,该断言就被触发检查。如果选修数据库课程的人数已经超过 60 人,check 子句返回值为 “假” ,则对 SC 表的插入操作被拒绝。
--限制每一门课程最多 60 名学生选修
create assertion ASS_SC_NUM
check (60>= all(select count(*)
from SC
group by Cno)
);
--限制每个学期每一门课程最多 60 名学生选修
--首先修改 SC 表的模式,增加一个 “学期(term)的属性”,类型是 date
alter table SC
add term date ;
--然后定义断言
create assertion ASS_SC_CNUM
check (60>= all(select count(*)
from SC
group by Cno,term)
);
drop assertion <断言名> :如果断言很复杂,则系统在检测和维护断言上的开销较高,这是在使用断言时应该注意的,此时可以选择性地将其断言删除。
触发器是用户定义在关系表上的一类由事件驱动的特殊过程。一旦定义,触发器将被保存在数据库服务器中,任何用户对表的增、删、改操作均由服务器自动激活相应的触发器,在关系数据库管理系统核心层进行集中的完整性控制。触发器类似于约束,但是比约束更加灵活,可以实施更为复杂的检查和操作,具有更精细和更强大的数据控制能力。
需要注意的是,触发器在 SQL 99 之后才写入 SQL 标准,但是很多关系数据库管理系统很早就支持触发器,因此不同的关系数据库管理系统实现的触发器语法各不相同、互不兼容,希望大家在上机实验时注意下该情况。
触发器又叫做 事件-条件-动作 规则。当特定的系统事件(如对一个表的增、删、改操作,事务的结束等)发生时,对规则的条件进行检查,如果条件成立则执行规则中的动作,否则不执行该动作。规则中的动作体可以很复杂,可以涉及其他表和其他数据库对象,通常是一段 SQL 存储过程。
SQL 使用 create trigger 命令建立触发器,其一般格式为:
create trigger <触发器名> /*每当触发事件发生时,该触发器被激活*/
{before|after}<触发事件> on <表名> /*指明触发器激活的时间是在执行触发事件前或后*/
referencing new|old row as <变量> /* referencing 指出引用的变量 */
for each{row|statement} /*定义触发器的类型,指明动作体执行的频率*/
[when <触发条件>] <触发动作> /*仅当触发条件为真时才执行触发动作体*/
下面对定义触发器的各部分语法进行详细说明:
1)、只有表的拥有者,即创建表的用户才可以在表上创建触发器,并且一个表上只能创建一定数量的触发器。触发器的具体数量由具体的关系数据库管理系统在设计时确定。
2)、触发器名:触发器名可以包括模式名,也可以不包含模式名。同一模式下,触发器名必须是唯一的,并且触发器名和表名必须在同一模式下。
3)、表名:触发器只能定义在基本表上,不能定义在视图上。当基本表的数据发生变化时,将激活定义在该表上相应触发事件的触发器,因此该表也被称为触发器的目标表。
4)、触发事件:触发事件可以是 insert 、delete 或 update ,也可以是这几个事件的组合,如 insert or delete 等,还可以是 update of <触发列,···> ,即进一步指明修改哪些列时激活触发器。after/before 是触发的时机。after 表示在触发事件的操作执行后激活触发器;before 表示在触发事件的操作执行之前激活触发器。
5)、触发器类型:触发器按照所触发动作的间隔尺寸可以分为行级触发器和语句级触发器。假设在 teacher 表上创建了一个 after update 触发器,触发事件是 update 语句:update teacher set Deptno=5 ;假设表 teacher 有 100 行,如果定义的触发器为语句级触发器,那么执行完 update 语句后触发动作体执行一次;如果是行级触发器,触发动作体将执行 100 次。
6)、触发条件:触发器激活时,只有当触发器条件为真时触发动作体才执行,否则触发动作体不执行。如果省略 when 触发条件,则触发动作体在触发器激活后立即执行。
7)、触发动作体:触发动作体既可以是一个匿名 PL/SQL 过程块,也可以是对已创建存储过程的调用。如果是行级触发器,用户可以在过程中使用 new 和 OLD 引用 update/insert 事件之后的新值和 uipdate/delete 事件之前的旧值;如果是语句级触发器,则不能在触发动作体中使用 new 或 OLD 进行引用。
如果触发动作体执行失败,激活触发器的事件(即对数据库的增、删、改操作)就会终止执行,触发器的目标或触发器可能影响的其他对象不发生任何变化。下面用例子通俗地讲一下:
create trigger SC_T /* SC_T 是触发器的名字*/
after update of Grade on SC
/*触发的时机,表示当对 SC 的 Grade 属性修改完后再触发下面的规则*/
referencing
oldrow as OldTuple,
newrow as NewTuple
for each row /*行级触发器,每执行一次 Grade 的更新,下面的规则就执行一次*/
when(NewTuple.Grade >= 1.1* OldTuple.Grade)
insert into SC_U(Sno,Cno,OldGrade,NewGrade)
values(OldTuple.Sno,OldTuple.Cno,OldTuple.Grade,NewTuple.Grade)
上述例子是当对表 SC 的 Grade 属性进行修改时,若分数增加了 10% ,则将此次操作记录到另一个表 SC_U (Sno,Cno,Oldgrade,Newgrade) 中,其中 Oldgrade 是修改前的分数,Newgrade 是修改后的分数。本例中的 referencing 指出引用的变量,如果触发事件是 update 操作并且有 for each row 子句,则可以引用的变量有 oldrow 和 newrow ,分别表示修改之前的元组和修改之后的元组。若没有 for each row 子句,则可以引用的变量有 oldtable 和 newtable ,oldtable 表示原来表中的内容,newtable 表示表中变化后的部分。
有例题:将每次对表 Student 的插入操作所增加的学生个数记录到表 StudentInsertLog 中。
create trigger Student_Count
after insert on Student
referencing
new table as delta /* delta 是一个关系名,其模式与 Student 相同*/
for each statement /*语句级触发器,即执行完 insert 语句后下面的触发动作体才执行一次*/
insert into StudentInsertLog(Numbers)
select count(*) from delta;
有例题:定义一个 before 行级触发器,为教师表 Teacher 定义完整性规则 “教授的工资不得低于 4000 元,如果低于则自动改为 4000 元”。
create trigger Insert_Or_Update_Sal
before insert or update on Teacher
referencing
new row as newTuple
for each row
begin /*定义动作触发体,这是一个 PL/SQL 过程块*/
if(newtuple.Job='教授') and (newtuple.Sal < 4000) /*因为是行级触发器,可在过程体中*/
then newtuple.Sal = 4000 ;/*使用插入或更新操作之后的新值*/
end if ;
end ; /*触发动作体结束*/
因为定义的是 before 触发器,在插入和更新教室记录前就可以按照触发器的规则调整教授的工资,不必等插入之后再检查再调整。
触发器的执行是由触发事件激活,并由数据库服务器自动执行的。一个数据表上可能定义了多个触发器,如多个 before 触发器、多个 after 触发器等,同一个表上的多个触发器激活时遵循如下的执行顺序:
1)、执行该表上的 before 触发器;2)、激活触发器的 SQL 语句;3)、执行该表上的 after 触发器。
对于同一个表上的多个 before (after) 触发器,遵循“ 谁先创建谁先执行 ”的原则,即按照触发器创建的时间先后顺序执行。有些关系数据库管理系统是按照触发器名称的字母排序顺序执行触发器。
其语法为:drop trigger <触发器名> on <表名> ; 触发器必须是一个已经创建的触发器,并且只能由具有相应权限的用户删除。触发器是一种功能强大的工具,但在使用时要慎重,因为在每次访问一个表时都可能触发一个触发器,这样会影响系统的性能。