SQL应用中级指南 Part2:(临时表、游标、存贮过程、触发机制)
(一) 临时表
这是一种简单的临时存在于数据库系统当中的表格,当结束数据库的联接或退出登录以后它们会被自动地删除。Transact-SQL 在 TempDB 中创建临时表,这个数据库是在你安装SQL-SERVER 时创建的。
-- 该表由 Create Table 命令以及创建表时的日期和时间组合而成一个唯一的表名
-- 临时表只可由它的创建者使用
-- 五十个用户可以在同时运行下边的命令
-- 表名开头的 # 标志是 SQL 用以标识临时表的标志
-- 五十个用户中每一个都可以获得一个他可以使用的表
-- 每一个用户都可以放心地插入更新删除表中的数据而不必担心其它的用户使该表中的数据失效
-- 创建临时表方法一
create table #albums (
artist char(30),
album_name char(50),
media_type int)
go
-- 该表也可以使用下边的命令来手动删除
drop table #albums
go
-- 创建临时表方法二
-- 这种方式创建出的临时表,不同之处在于当用户退出 SQL 或切断联接时该表不会被自动地删除
-- 用户必须使用 DROP 命令在 TEMPDB 中将其手动删除
-- 也可以将 SQL-SERVER 关闭并重新启动( 这将会把所有在 TEMPDB 中的表都删除掉)
create table tempdb..tablename (field1 datatype,......,fieldn datatype)
使用这两种方式创建的临时表的确是货真价实的表,通常用于暂时存贮从查询中返回的数据,这些数据可以在其它的查询中再次使用。
(二)游标
数据库指针类似于文字处理程序中的指针,当你按下方向键时游标依次从各行文本中滚动,按一下向上键游标向上跳一行,而按PageUp 和PageDown 则会向一次翻阅几行。
数据库游标的操作也类似,数据库游标允许你选择一组数据通过翻阅这组数据记录通常被称为数据集检查,每一个游标所在的特定的行你可以将游标和局部变量组合在一起对每一个记录进行检查,当游标移动到下一个记录时来执行一些外部操作。
游标的另一个常见的用法是保存查询结果以备以后使用。一个游标结果集是通过执行 SELECT 查询来建立的,如果你的应用程序或过程需要重复使用一组记录,那么第一次建立游标以后再重复使用将会比多次执行查询快得多,而且你还有在查询的结果集中翻阅的好处。
-- 1.创建游标
-- 如果使用 Transcat-SQL 来创建游标其语法如下
declare cursor_name cursor
for select_statement
[for {read only | update [of column_name_list]}]
-- 如果使用 ORACLE7 来创建游标其语法格式如下
DECLARE cursor_name CURSOR
FOR {SELECT command | statement_name | block_name}
-- 在执行 DECLARE cursor_name CURSOR 语句时你必须同时定义将要在你的所有的游标操作中使用的结果集
-- 一个游标有两个重要的部分:游标结果集 和 游标的位置
-- 2.打开游标
-- 最简单的打开游标命令如下
open cursor_name
-- 3.使用游标进行翻阅
-- Transcat-SQL 提供了 FETCH 命令
fetch cursor_name [into fetch_target_list]
-- ORACLE SQL 提供了下边的语法
FETCH cursor_name {INTO : host_variable
[[INDICATOR] : indicator_variable]
[, : host_variable
[[INDICATOR] : indicator_variable] ]...
| USING DESCRIPTOR descriptor }
-- 每次当 FETCH 命令运行时游标指针的好处是每次可以在结果集中移动一行
-- 如果有必要,移动到行的数据可以被填充到 fetch_target_list 变量中
-- Transcat-SQL 允许程序员通过下边的命令来实现一次移动多行
set cursor rows number for cursor_name
-- 注: 该命令不能使用 INTO 子句,但是当向前跳动的行数已知时用它来代替重复执行的 FETCH 命令则很有用
-- 你可以使用 WHILE 循环来循环查看整个结果集,但是如果知道已经到达了最后一个记录呢?
-- Transcat-SQL 可以让你在任何时候通过维护 @@sqlstatus 和 @@rowcount 这两个全局变量来检查当前游标的状态
-- 变量 @@sqlstatus 返回最后一次运行 FETCH 语句的状态信息
-- Transcat-SQL 规定除了 FETCH 命令,其他的命令不得修改 @@sqlstatus 变量!
-- 该变量可以取下表三个值中的一个
-- 0 Successful completion of the FETCH statement.
-- 1 The FETCH statement resulted in an error.
-- 2 There is no more data in the result set.
-- 而变量 @@rowcount 则返回上次 FETCH 命令设置的行号
-- 你可以用它来确定当前游标结果集的行数
-- 4.关闭游标
close cursor_name
-- 这时游标依然存在,但是它必须被再次打开方可使用
-- 关闭一个游标从本质上来说是关闭了它的结果集而并不是它的全部内容
-- 如果你已经完全结束了对一个游标的使用, DEALLOCATE 命令将释放让游标所占用的内存并且可以让游标的名字可以被再次使用
deallocate cursor cursor_name
-- 下例给出了用 Transcat-SQL 写的创建,使用,关闭释放一个游标的完整过程
--------------------------- 示例数据 -------------------------------
Soul Asylum Minneapolis Rock 1
Maurice Ravel France Classical 2
Dave Matthews Band Charlottesville Rock 3
Vince Gill Nashville Country 4
Oingo Boingo Los Angeles Pop 5
Crowded House New Zealand Pop 6
Mary Chapin-Carpenter Nashville Country 7
Edward MacDowell U.S.A. Classical 8
--------------------------------------------------------------------
declare @name char(30)
declare @homebase char(40)
declare @style char(20)
declare @artist_id int
create Artists_Cursor cursor for select * from ARTISTS // 1. Create cursor
open Artists_Cursor // 2. Open cursor
fetch Artists_Cursor into @name, @homebase, @style, @artist_id // 3. Read cursor
while (@@sqlstatus = 0)
begin
print @name
print @homebase
print @style
print char(@artist_id)
fetch Artists_Cursor into @name, @homebase, @style, @artist_id
end
close Artists_Cursor // 4. Close cursor but not release
deallocate cursor Artists_Cursor // 4. Release cursor
go
游标的适用范围:
与表索引以及其它的对象如触发机制和存贮过程不同,游标在创建以后并不作为一个数据库对象来看待,所以游标的使用会受到一些限制。
切记:无论何时要注意游标分配过的内存,尽管它的名字可能已经不存在了,当不在使用游标的时候或在进行游标能力之外的工作时一定要记得关闭游标,并将它释放掉。
可以在下列三种情况下创建游标:
1. 在会话中:会话在用户登录以后开始,如果用户在登录进行 SQL SERVER 以后创建了一个游标,那么游标的名字将一直存在到用户退出登录。用户不能再一次使用在本次登录中创建的游标名。
2. 在存贮过程中:游标在存贮过程的内部创建的好处在于只有当过程运行时它才真正起作用,一旦过程退出了则游标的名字将不再有效。
3. 在触发机制中:在触发机制中创建游标与在存贮过程中创建游标所受到的限制是相同的。
(三)创建和使用存贮过程
存贮过程是一个专业数据库编程人员必须掌握的概念。存贮过程可以在最大程度上发挥出 SQL 的潜能。该功能可以被编程语言像执行自己的函数一样地调用。
存贮过程是一组经过压缩处理的、经常使用的一组命令,如交叉表的查询,更新和插入操作。
存贮过程还有些附加的优点:
1. Transcat-SQL 允许在过程调用中提供输入或输出的参数,这种机制可以让你写出通用的存贮过程并将变量传递给它。
2. 它可以在设计的阶段执行。当在一个网站中执行大批量的 SQL 语句时,你的应用程序会不停地与 SQL SERVER 进行通讯,这会使得网站的负荷迅速增大,在多用户环境下通讯将异常的繁忙,你的服务器将变得越来越慢甚至崩溃。而使用存贮过程可以在最大程度上减轻通讯负荷。
3. 当存贮过程执行时,SQL 语句将在服务器中继续运行,一些数据信息将会返回给用户的电脑直至过程执行完毕,极大地提高性能并带来了附加的好处。
4. 存贮过程在第一次执行时在数据库经过了编译操作,编译的映象将存贮在服务器的过程中,因此你不必在每一次执行它的时候都对它进行优化,这也使性能得到了提高。
-- Transcat-SQL 创建存贮过程的语法如下
create procedure procedure_name
[[(]@parameter_name
datatype [(length) | (precision [, scale])
[= default][output]
[, @parameter_name
datatype [(length) | (precision [, scale])
[= default][output]]...[)]]
[with recompile]
as SQL_statements
-- 运行存贮过程的 EXECUTE 命令的语法如下
execute [@return_status = ]
procedure_name
[[@parameter_name =] value |
[@parameter_name =] @variable [output]...]]
[with recompile]
-- 创建一个简单的过程
create procedure Print_Artists_Name
as
declare @name char(30)
declare @homebase char(40)
declare @style char(20)
declare @artist_id int
create Artists_Cursor cursor for select * from ARTISTS
open Artists_Cursor
fetch Artists_Cursor into @name, @homebase, @style, @artist_id
while (@@sqlstatus = 0)
begin
print @name
fetch Artists_Cursor into @name, @homebase, @style, @artist_id
end
close Artists_Cursor
deallocate cursor Artists_Cursor
go
-- 使用 EXECUTE 命令来执行 Print_Artists_Name 过程
execute Print_Artists_Name
go
-- 输出如下:
Soul Asylum
Maurice Ravel
Dave Matthews Band
Vince Gill
Oingo Boingo
Crowded House
Mary Chapin-Carpenter
Edward MacDowell
-- 上例是一个很小的存贮过程,但是一个存贮过程中可以包含许多条语句,也就是说你不必逐条地执行这些语句
-- 在存贮过程中使用参数
-- 存贮过程可以接受参数并把它们输入到其中的 SQL 语句中,此外数据可以通过输出参数从存贮过程中返回
-- 输入参数必须以 @ 提示符开始,而且这些参数必须是 Transcat-SQL 的合法数据类型
-- 输出参数必须以 @ 提示符开始,此外 OUTPUT 关键字必须紧跟着输出参数的名字
create procedure Match_Names_To_Media @description char(30)
as
select ARTISTS.name from ARTISTS, MEDIA, RECORDINGS
where MEDIA.description = @description and MEDIA.media_type = RECORDINGS.media_type
and RECORDINGS.artist_id = ARTISTS.artist_id
go
execute Match_Names_To_Media "CD"
go
-- 输出如下:
Soul Asylum
Maurice Ravel
Vince Gill
Crowded House
Mary Chapin-Carpenter
-- 本例中给出的输出参数的用法,在该例中将使用艺术家的 HOMEBASE 作为输入,过程会将艺术家的名字作为输出
create procedure Match_Homebase_To_Name @homebase char(40), @name char(30) output
as
select @name = name from ARTISTS where homebase = @homebase
go
declare @return_name char(30)
execute Match_Homebase_To_Name "Los Angeles", @return_name = @name output
print @return_name
go
-- 输出如下:
Oingo Boingo
-- 删除存贮过程
drop procedure procedure_name
-- DROP 语句是经常使用的,当一个存贮过程被重新创建之前旧的存贮过程以及它的名字必须被删除掉
-- 根据我个人的经验只有极少的存贮过程在创建之后是不需要修改的
-- 有许多次在语句中产生的错误会上传至过程,我们推荐你在创建存贮过程时使用 SQL 脚本文件来存贮
-- 一定要记得将原有的存贮过程及相关表先删除,如果你忘记了执行 DROP 命令你将会收到一个错误信息
-- 在用SQL 的脚本来创建数据库对象时经常会用到下边的语法
if exists (select * from sysobjects where name = "procedure_name")
begin
drop procedure procedure_name
end
go
create procedure procedure_name
as
-- 该命令会检查 SYSOBJECTS 表,这里边存贮着 SQL SERVER 的数据库对象信息
-- 查看该对象是否存在,如果存在就在创建新对象之前先将它删除
-- 存贮过程的嵌套
-- 存贮过程也可以被嵌套调用以增强程序的模块化
-- 当嵌套存贮过程时,所有在存储过程内部创建的变量和数据库对象对于将要调用它的过程来说都是可见的
-- 全体局部变量或临时对象如临时表将由最后创建这些元素的存贮过程来删除
create procedure Example13_8b
as
select * from #temp_table
go
create procedure Example13_8a
as
create #temp_table (
data char(20),
numbers int)
execute Example13_8b
drop table #temp_table
go
-- 正如你所看到的过程 Example13_8b 执行了,但是 #temp_table 并没有被创建
-- 它是在过程 Example13_8a 中创建的,因此将会产生错误
-- 事实上 Example13_8b 并没有被创建,因为并没有临时表 #temp_table
-- 而过程 Example13_8a 也不会被创建,因为没有过程 Example13_8b
-- 下面的代码通过在创建第一个过程之前先创建 #temp_table 来修正这一错误
-- 其实 #temp_table 在第二个过程创建之前被删除了
create #temp_table (
data char(20),
numbers int)
go
create procedure Example13_8b
as
select * from #temp_table
go
drop table #temp_table
go
create procedure Example13_8a
as
create #temp_table (
data char(20),
numbers int)
execute Example13_8b
drop table #temp_table
go
(四)设计和使用触发机制
触发机制从本质上来说是一种特殊类型的存贮过程,它可以在下列的三种情况之一发生时自动运行:更新、插入、删除。
-- Transcat-SQL 创建触发机制的语法格式如下
create trigger trigger_name
on table_name
for {insert, update, delete}
as SQL_Statements
-- ORACLE 则使用下边的语法来创建触发机制
CREATE [OR REPLACE] TRIGGER [schema.]trigger_name
{BEFORE | AFTER}
{DELETE | INSERT | UPDATE [OF column[, column]...]}
[OR {DELETE | INSERT | UPDATE [OF column [, column] ...]}]...
ON [schema.]table
[[REFERENCING { OLD [AS] old [NEW [AS] new]
| NEW [AS] new [OLD [AS] old]}]
FOR EACH ROW
[WHEN (condition)] ]
pl/sql statements...
触发机制对于强制执行引用完整性非常有用,强制执行引用完整性可以保证在多表交叉访问时数据的有效性。
如果用户输入了下边的命令:insert RECORDINGS values (12, "The Cross of Changes", 3, 1994);
这是一个有效的在表 RECORDINGS 表中插入新记录的命令,可是对 ARTISTS 表进行一下快速的检查后你会发现并没有 ARTIST_ID=12 的记录,用户所拥有的在
RECORDINGS 表中插入记录的权利可以彻底地破坏你的数据引用完整性。尽管有许多数据库系统都可以通过在创建表的时候设置约束来强制执行数据的引用完整性,但是触发机制却提供了更为灵活的解决方法。约束将会把系统的错误信息返回给用户,并且你现在可能已经知道了这些错误信息有时对你没有多大的帮助,而作为另外一种方法,触发机制可以打印出错误信息,调用其它的存贮过程,如果有必要它还可以修正错误信息。
触发机制与事务处理
触发机制所进行的活动是被默认为事务处理的。一部分进行的主要的事件次序如下:
1. 默认地自动运行 BEGIN TRANSACTION 语句(对于表和触发机制而言)
2. 当插入、更新、删除操作发生时
3. 触发机制被调用,其中的语句被自动执行
4. 由触发机制自动的完成事务处理的取消或确认操作
-- 本例解决了早些时候更新 RECORDINGS 表时所带来的问题
create trigger check_artists
on RECORDINGS
for insert, update as
if not exists (select * from ARTISTS, RECORDINGS where ARTISTS.artist_id = RECORDINGS.artist_id)
begin
print "Illegal Artist_ID!"
rollback transaction
end
go
类似的问题也可能在删除 RECORDINGS 表中的记录时出现。如果你在 RECORDINGS 表中将某个艺术家删除了,你可能也想同时删除 ARTIST 表中的艺术家记
录。如果在触发机制激活之前记录已经被删除了,那么你如何才能知道哪一个 ARTIST_ID 的记录才是需要删除的记录呢?
对于这个问题有两种解决的办法:
1. 将 ARTIST 表中的所有不在 RECORDINGS 表中存在记录的艺术家删除
create trigger delete_artists
on RECORDINGS
for delete as
begin
delete from ARTISTS where artist_id not in (select artist_id from RECORDINGS)
end
go
2. 检查被删除过的逻辑表。Transcat-SQL 可以维护两个表 DELETED 和 INSERTED ,这两个表中保存着对真实表的最近所做的改动。它与触发机制所创建的表有着相同的结构,因此你可以从 DELETE 表中获得 ARTIST_ID 的内容,并将它从 ARTIST 中删除。
create trigger delete_artists
on RECORDINGS
for delete as
begin
delete ARTISTS from ARTISTS, deleted where ARTIST.artist_id = deleted.artist_id
end
go
使用触发机制时的限制
1. 不能在临时表中创建触发机制
2. 触发机制必须在当前的表所在的数据库中创建
3. 不能在视图中创建触发机制
4. 当表被删除以后 所有与之相关的触发机制会被自动地删除
触发机制的嵌套
触发机制也可以被嵌套。比如说你可以创建一个触发机制来执行删除动作,例如如果触发机制自己删除了一个记录,数据库服务器可以据此激活另一个触发机制,结果将会不停地循环直到表中的所有记录都被删除掉或一些其他的触发条件被激活。嵌套机制不是默认的,可是环境中必须提供这个功能。