第11章 编写脚本和批处理
本章内容简介:
• 如何在脚本和批处理中结合T-SQL语句
• 变量和IDENTITY值的作用域
• 在脚本中进行错误处理
• 如何从命令行运行批处理
• 如何构建和运行动态SQL语句
• T-SQL流程语句控制
不管您是否已经意识到这一点,现在可以编写SQL脚本了。您所编写的每一条CREATE 语句、每一条ALTER语句以及每一条SELECT语句可能是一个脚本的全部(如果运行单条语句)或者部分(多条语句)。然而,只有一行的脚本意义并不大,脚本必须在上下文中才有意义。
SQL脚本在很大程度上也是如此。当将一些命令串成一个更长的脚本时,事情会变得更有意思。现在可以设想添加更多的.NET语言元素。
脚本通常都有一个统一的目标。也就是说,脚本中所有的命令通常是为了达到一个总体的目的。本章的示例包括构建数据库的脚本(这些脚本可能用于系统安装),以及用于系统维护的脚本(备份、数据库一致性检查器(DBCC)实用程序、索引碎片整理等)——几个命令通常一起运行且用于任何情况的脚本。
本章除了介绍脚本以外,还会介绍批处理的概念——批处理控制SQL Server组合命令的方式。此外,还将简要介绍sqlcmd命令行实用程序,以及它与脚本的关系。
注意:sqlcmd是在SQL Server 2005中首次引入的。为了实现向后兼容,SQL Server继续支持osql.exe(以前执行命令行工作的工具)。可能还会看到isql.exe(不要将这个工具和isqlw.exe混淆),其在早期版本中提供了相同的功能。不过,SQL Server 2005 不再支持isql.exe。
11.1 脚本的基础知识
从技术上讲,只有将脚本存储到一个文件中并且可提取和重用时,脚本才成为一个真正的脚本。SQL脚本以文本文件的形式存储。SQL Server Management Studio提供了许多辅助编写脚本的工具。基本查询窗口是彩色编码的,这样不仅有助于识别关键字,而且有助于理解它们的性质。除此之外,还有IntelliSense、单步调试器、代码模板、对象浏览器等。
脚本通常被看做一个单元。也就是说,正常情况下,或者执行整个脚本,或者什么也不执行。脚本可以使用系统函数和局部变量。例如,看一下在第5章创建的Accounting数据库中插入订单记录的脚本:
USE Accounting;
DECLARE @Ident int;
INSERT INTO Orders
(CustomerNo,OrderDate, EmployeeID)
VALUES
(1, GETDATE(), 1);
SELECT @Ident = SCOPE_IDENTITY();
INSERT INTO OrderDetails
(OrderID, PartNo, Description, UnitPrice, Qty)
VALUES
(@Ident, '2R2416', 'Cylinder Head', 1300, 2);
SELECT 'The OrderID of the INSERTed row is ' +CONVERT(varchar(8),@Ident);
注意:如果没有填充版本的Accounting数据库,可从Wrox支持网站(wrox.com)下载一个以可用于本章的状态重新创建Accounting数据库的脚本。
这里用到了6个截然不同的命令,涵盖了在脚本中可能做的许多不同的事情。该脚本使用了系统函数、局部变量、USE语句、INSERT语句,以及SELECT语句的赋值版本和常规版本。所有这些命令协调一致地工作以完成一个任务——将全部的订单插入到数据库中。
11.1.1 使用USE语句选择数据库环境
USE语句用于设置当前数据库。这会影响到对完全限定对象名的数据库部分使用默认值的任何地方。在这个特定示例中,还没有指出INSERT或SELECT语句中的表来自什么样的数据库,但是,因为在使用INSERT和SELECT语句之前已经包含了USE语句,所以它们将使用指定的数据库(在这里使用Accounting数据库)。如果没有USE语句,那么就由执行脚本的任何用户来确定执行脚本时的当前数据库是正确的。
注意:通常,如果在脚本中命名特定于数据库的表(也就是非系统表),那么需要使用USE命令。如果脚本用于修改一个特定的数据库,那么可以发现这是非常有帮助的——正如本书之前几章中已经讲过的那样,本书多次无意地在master数据库中创建了大量的表,其实这些表本来应该用于用户数据库。
不要认为这里的意思是指应当总是在脚本中包含USE语句——这取决于脚本的目的。如果只是一个通用脚本,那么省去USE语句实际上可能更有益。
接下来使用DECLARE语句声明变量。本书前面的一些脚本中已使用了DECLARE语句,现在将作更详细的介绍。
11.1.2 声明变量
DECLARE语句最常用形式的语法相当简单:
DECLARE @
@
@
可以一次仅仅声明一个变量,也可以一次声明几个变量。人们常常以一次声明一个变量的方式重用DECLARE语句,而不是使用逗号分隔的方法一次声明多个变量。选择哪种方法取决于您自己,但是不论选择何种方法,都必须初始化变量(使用“=”语法),否则变量值为NULL, 直到显式地将其设置为一些其他值。
在这个示例中,将一个名为@ident的局部变量声明为整型。这里没有选择初始化该局部变量,因为这个变量用于接受另一个数据源的值。从技术上讲,可以不声明这个变量——而是可以选择直接使用SCOPE_IDENTITY()。SCOPE_IDENTITY()是一个系统函数。它总是可用的,并提供在当前作用域中分配的最近标识值。和大多数系统函数一样,应当形成一个习惯,即显式地将SCOPE_IDENTITY()中的值移动到一个局部变量中。这样,可以确保该值不会被无意修改。在这里没有这种危险,但是,为了保持一致性,还是要这么做。
注意:本书喜欢将从系统函数中取出的值移动到用户自己的变量中。这样,就能安全地使用这个值,并且知道只有当用户修改这个值时,它才会改变。而对于系统函数本身,因为大多数系统函数不是由您设置的,而是由系统设置的,所以有时不能确定函数值何时变化。这会造成一种情况,即不期望系统值改变时,偏偏系统改变了某个值,这样就会产生不可预知的可怕结果。
现在您已经知道如何声明标量变量。标量变量保存单个的原子值,例如整数或字符串。SQL Server也允许声明表变量。表变量的声明方式类似于标量变量,并且具有相同的作用域,但是它可以保存任意数量的行。本章后面将详细介绍表变量,目前只给出基本概念。
DECLARE @
)
该语法以DECLARE开头,但是接下来则类似于声明表,例如:
DECLARE @InterestingRows TABLE (
RowID intNOT NULL IDENTITY PRIMARY KEY,
DescriptorVARCHAR(255) NOT NULL
)
值得注意的是,表变量可以有键约束、标识列以及完备表的许多其他功能。在第7章中已经介绍过临时表(如果您按顺序阅读本书),表变量在与临时表类似的情况中是非常有用的。本章后面将介绍一些条件,帮助您在这两者之间进行选择。
11.1.3 设置变量中的值
知道了如何声明变量后,接下来的问题是,“如何修改它们的值? ”有3种设置变量值的方法。可以在DECLARE语句中初始化变量,或者可以使用SELECT语句或SET语句。从功能上看,SET和SELECT的作用几乎是相同的,不同的是SELECT语句可以做更多的工作:
• 允许源数据值来自SELECT语句中的某一列。
•SELECT可以在一条语句中将值分配给多个变量。
那么为什么有两种方法来做这件事情呢?在SQL Server的历史中,SELECT在SET之前出现,为什么还要麻烦地实现SET呢?有充分的理由认为,SET现在是ANSI/ISO标准的一部分, 这是保留SET的理由。然而,我找不到SELECT中的相同功能方面有任何错误,甚至ANSI/ISO似乎也认为它是正确的。我确信这种冗余有其意义,但是我说不清具体原因是什么。虽说如此,但两种方法在实际使用中仍有一些区别。
1. 使用SET设置变量
SET通常以在更加过程化的语言中常见的方式来设置变量。典型的使用示例是:
SET @TotalCost = 10
SET @TotalCost = @UnitCost * 1.1
可注意到,这些都是用显式的值或者其他变量来直接赋值的。使用SET,不能将查询得到的值赋给变量——必须将查询和SET分开。例如下列语句:
USE AdventureWorks;
DECLARE @Test money;
SET @Test = MAX(UnitPrice) FROM [Order Details];
SELECT @Test;
就会产生错误,但是下列语句:
USE AdventureWorks;
DECLARE @Test money;
SET @Test = (SELECT MAX(UnitPrice) FROM Sales.SalesOrderDetail);
SELECT @Test;
就没有问题。
注意:尽管后一种语法可以起作用,但是习惯上从不采用这种方法实现代码。我也不能确定“不采用这种方法实现”的原因,但是我猜想和可读性有关——您可能希望SELECT语句和检索表的数据相关,而SET用于简单的变量赋值。
2. 初始化变量
第一次声明变量时,它的值默认是NULL。如果您喜欢立刻赋值,那么可以使用=
DECLARE @Counter INT = 0;
DECLARE @@MaxPrice MONEY = (SELECT MAX (UnitPrice) FROMSales.SalesOrderDetail);
初始化变量的语法和规则遵循与使用SET时相同的模式。
3. 使用SELECT设置变量
当变量中存储的信息来源于查询时,经常用SELECT给变量赋值。例如,对于上一个示例,通常使用SELECT完成:
USE AdventureWorks;
DECLARE @Test money;
SELECT @Test = MAX(UnitPrice) FROMSales.SalesOrderDetail;
SELECT @Test;
可注意到,这样会更清楚一点(花费了更少的步骤来做同样的事情)。
再次强调,关于何时使用哪种方法的约定如下:
• 当执行简单的变量赋值时,使用SET——这时已知值是一个显式值或者其他变量。
• 当基于查询进行变量赋值时,使用SELECT。
注意:这里可能会有人表示怀疑,因为事实上可以看见,在本书中的许多地方都违反了最后一个约定。第一次将SET用于变量赋值是在7.0的版本中,我必须承认,甚至在其发行后将近10年的时间里,我仍然没有完全适应。虽然如此,这看起来实际上是Microsoft和SQL Server社团推动的约定,所以我强烈建议作出正确的选择,遵守这个约定。
11.1.4 系统函数回顾
可用的不带参数的系统函数有30多个。较老的系统函数都以@@符号打头——以前称为“全局变量”。不过所幸的是,现在它们都更准确地改称为系统函数,并且大多数系统函数都没有@@前缀。其中最值得关心的是在表11-1列出的一些系统函数。
表11-1常见的系统函数
变 量 |
用 途 |
注 释 |
@@DATEFIRST |
返回当前设置的一周的第一天(例如,星期日或星期一) |
这是一个系统范围的设置——如果有人改变了这个设置,就得不到所期望的结果 |
@@ERROR |
返回当前连接中最后一条执行的T-SQL语句的错误号。 如果没有错误,则返回0 |
在每个新的语句中重新设置。如果需要保存这个值,可以在执行完需要保存其错误代码的语句之后,立刻把这个值移到一个局部变量中 |
@@IDENTITY |
返回当前连接中作为最后一条 INSERT 或者 SELECT INTO语句的结果插入的标识值 |
如果没有生成标识值,那么设置其为NULL。即使缺少标识值是由于语句未成功运行,也是如此设置。如果是通过一条语句执行多个插入,那么只返回最后的标识值 |
IDENT_CURRENT('table_name') |
返回对特定表插入的最后标识值,而不管是哪个会话或作用域 |
如果插入多个表,它就不会被覆盖;但如果有其他连接插入到特定表,则它所提供的值就不是所预期的 |
@@OPTIONS |
返回通过使用SET命令设 置的选项的信息 |
尽管只得到一个值,但可以设置许多选项,SQL Server使用二进制标记说明设置什么值。为了测试是否设置了自己感兴趣的选项,必须将这个选项值和一个按位运算符一起使用 |
@@REMSERVER |
仅在存储过程中使用。返回调用存储过程的服务器的值 |
如果希望存储过程根据调用它的远程服务器(通常指地理位置)表现出不同的行为,那么这个选项是很方便的。在.NET时代,我仍要提出这样一个问题:任何需要这个变量的操作是否可以通过使用.NET中的其他更好的功能来编写 |
@@ROWCOUNT |
一个最常用的系统函数。返回最后一条语句所影响的行数 |
一般在非运行时错误检查时使用。例如,如果尝试通过使用一个WHERE子句删除一行,并且没有行被影响,那么这意味着一些不期望的事情发生了。然后可以手动引发错误 |
SCOPE_IDENTITY() |
类似于@@IDENTITY,但返回在当前会话和作用域中插入的最后一个标识 |
对于避免触发器或嵌套的存储过程执行重写预期的标识值的额外插入很有用。如果您尝试检索已经执行的插入的标识值,这就是一种可以采用的方法 |
@@SERVERNAME |
返回正在运行脚本的本地服务器的名称 |
可以通过sp_addserver然后重启SQL Server来改变,但是很少需要改变 |
@@TRANCOUNT |
返回当前连接的活动事务 数——实质是事务嵌套级别 |
除非使用了保存点,否则ROLLBACK TRAN语句会将@@ TRANCOUNT减少到0。BEGIN TRAN增量可使@@ TRANCOUNT递增1,COMMIT TRAN 减量使@@TRANCOUNT递减1
|
@@VERSION |
返回当前的SQL Server安 装版本以及日期、处理器和O/S体系结构 |
这不能以任何结构化的字段排列形式返回信息,所以如果需要使用它对特定信息进行测试,必须对它进行分析。还要确保使用了xp_msver扩展存储过程 |
如果不理解其中的一些术语,也不要担心。随着学习的不断深入,您会逐步了解它们,并且在以后需要的时候可以回过头来查看这个表或附录A。只是要记得可从这些地方找到关于系统和活动的当前状态的完整信息。
11.1.5 检索标识值
SCOPE_IDENTITY是所有系统函数中最重要的一个。标识列是这样的一种列,即不对其提供一个值,而是由SQL Server自动地插入一个已编号的值。
在示例中,在对Orders表执行一个插入操作之后,就得到了SCOPE_IDENTITY()的值。问题是没有提供这个表的键值——当执行插入操作时,它是自动创建键值的。现在想要将一个记录插入到OrderDetails表中,但是需要知道Orders表中关联记录的主键的值(记住,在引用Orders 表的OrderDetails表上有一个外键约束)。因为SQL Server生成了该值,而不是由用户提供值,所以需要有一种方法检索到该值,以便后面在脚本上进行相关的插入。SCOPE_IDENTITY()给出了自动生成的值,因为它是最后运行的语句。
在这个示例中,也可以不将SCOPE_IDENTITY()移动到一个局部变量中——可以在下一个INSERT查询中显式地引用它。但是,本书总是习惯于将它移动到一个局部变量中,以避免在确实需要其副本时可能出现的错误。例如,假定有另一个INSERT操作依赖于前面INSERTOrders表操作的标识值。如果没有将这个值移动到一个局部变量中,那么在执行下一个INSERT操作时,这个值就会丢失,因为它被来自于OrderDetails表的值覆盖,由于OrderDetails表没有标识列,因此这意味着SCOPE_IDENTITY()将被设置为NULL。将SCOPE_IDENTITY0的值移动到一个局部变量中也使得用户保存了这个值,可以保存在输出语句附近以备后用。
下面创建一些表进行试验:
CREATE TABLE TestIdent
(
IDCol int IDENTITY
PRIMARY KEY
);
CREATE TABLE TestChild1
(
IDcol int
PRIMARY KEY
FOREIGN KEY
REFERENCESTestIdent(IDCol)
);
CREATE TABLE TestChild2
(
IDcol int
PRIMARY KEY
FOREIGN KEY
REFERENCESTestIdent(IDCol)
);
这里得到一个父表——它有一个用作主键的标识列(这是该表具有的唯一一列)。还有两个子表。它们都是用于标识关系的对象——也就是说,两个子表都通过在另一个表(父表)上设置外键以获得主键的一部分或者全部(这里是主键的全部)。所以出现了这样一种情况,即两个子表需要从父表得到它们的键。因此,首先需要将一个记录插入到父表中,然后检索生成的标识值以在其他表中使用。
【试一试】 使用SCOPE_IDENTITY()
既然已经有一些可以操作的表,那么准备尝试一些测试脚本:
/*****************************************
** This script illustrates how the identity
** value gets lost as soon as another INSERT
** happens
****************************************** */
DECLARE @Ident INT; -- This will be a holding variable
/* We'll You'll use it to show how we you can
** move values from system functions
** into a safe place.
*/
INSERT INTO TestIdent
DEFAULT VALUES;
SET @Ident = SCOPE_IDENTITY();
PRINT 'The value we you got originally fromSCOPE_IDENTITY() was ' +
CONVERT(varchar(2),@Ident);
PRINT 'The value currently in SCOPE_IDENTITY() is '
+CONVERT(varchar(2),SCOPE_IDENTITY());
/* On this first INSERT using SCOPE_IDENTITY(), you'regoing to get lucky.
** We'll You'll get a proper value because there isnothing between the
** original INSERT and this one. You'll see that on theINSERT that
** will follow after this one, you won't be so luckyanymore. */
INSERT INTO TestChild1
VALUES
(SCOPE_IDENTITY());
PRINT 'The value we you got originally fromSCOPE_IDENTITY() was ' +
CONVERT(varchar(2),@Ident);
IF (SELECT SCOPE_IDENTITY()) IS NULL
PRINT 'The valuecurrently in SCOPE_IDENTITY() is NULL';
ELSE
PRINT 'The valuecurrently in SCOPE_IDENTITY() is '
+CONVERT(varchar(2),SCOPE_IDENTITY());
-- The next line is just a spacer for our your print out
PRINT '';
/* The next line is going to blow up because the onecolumn in
** the table is the primary key, and primary keys can'tbe set
** to NULL. SCOPE_IDENTITY() will be NULL because we youjust issued an
** INSERT statement a few lines ago, and the table we youdid the
** INSERT into doesn't have an identity field. Perhapsthe biggest
** thing to note here is when SCOPE_IDENTITY() changed -right after
** the next INSERT statement. */
INSERT INTO TestChild2
VALUES
(SCOPE_IDENTITY());
示例说明
在这个脚本中所做的是:如果直接依赖SCOPE_IDENTITY(),而不是将该值移动到一个安全的地方,那么将会发生什么呢?当执行前面的脚本时,一切将工作得很好,只有最后的INSERT有问题。最后的语句是尝试直接使用SCOPE_IDENTITY(),但是前面的INSERT语句已经修改了SCOPE_IDENTITY()中的值。因为该语句是针对没有标识列的表,所以SCOPE_IDENTITY()中的值被设置为NULL。因为在主键中不能有NULL值,所以最后的INSERT失败了:
(1 row(s) affected)
The value we got originally from @@IDENTITY was 1
The value currently in @@IDENTITY is 1
(1 row(s) affected)
The value we got originally from @@IDENTITY was 1
The value currently in @@IDENTITY is NULL
Msg 515, Level 16, State 2, Line 41
Cannot insert the value NULL into column 'IDcol',table
'Accounting.dbo.TestChild2'; column does not allow nulls.INSERT fails.
The statement has been ter minated.
如果只是稍作改变(即保存原始的SCOPE_IDENTITY()值):
/*****************************************
** This script illustrates how the identity
** value gets lost as soon as another INSERT
** happens
****************************************** */
DECLARE @Ident INT; -- This will be a holding variable
/* We'll You'll use it to show how we you can
** move values from system functions
** into a safe place.
*/
INSERT INTO TestIdent
DEFAULT VALUES;
SET @Ident = SCOPE_IDENTITY();
PRINT 'The value we you got originally fromSCOPE_IDENTITY() was ' +
CONVERT(varchar(2),@Ident);
PRINT 'The value currently in SCOPE_IDENTITY() is '
+CONVERT(varchar(2),SCOPE_IDENTITY());
/* On this first INSERT using SCOPE_IDENTITY(), you'regoing to get lucky.
** We'll You'll get a proper value because there isnothing between the
** original INSERT and this one. You'll see that on theINSERT that
** will follow after this one, you won't be so luckyanymore. */
INSERT INTO TestChild1
VALUES
(SCOPE_IDENTITY());
PRINT 'The value we you got originally fromSCOPE_IDENTITY() was ' +
CONVERT(varchar(2),@Ident);
IF (SELECT SCOPE_IDENTITY()) IS NULL
PRINT 'The valuecurrently in SCOPE_IDENTITY() is NULL';
ELSE
PRINT 'The valuecurrently in SCOPE_IDENTITY() is '
+CONVERT(varchar(2),SCOPE_IDENTITY());
-- The next line is just a spacer for our your print out
PRINT '';
/* This time all will go fine because we you are usingthe value that
** we you have placed in safekeeping instead ofSCOPE_IDENTITY() directly.*/
INSERT INTO TestChild2
VALUES
(@Ident);
这次一切都运行得非常好:
(1 row(s) affected)
The value we got originally from @@IDENTITY was 1
The value currently in @@IDENTITY is 1
(1 row(s) affected)
The value we got originally from @@IDENTITY was 1
The value currently in @@IDENTITY is NULL
(1 row(s) affected)
注意:在这个示例中,很明显存在问题,因为尝试将一个NULL值插入到主键中。现在,假设一种糟糕的情况——第二个表有一个标识列。您可能很容易就会将虚假的数据插入到表中,甚至都没有发觉这一点——至少直到出现一个非常严重的数据完整性问题时才发觉!
注意:到目前为止,完全可以采用@@IDENTITY而非SCOPE_IDENTITY()编写这个示例,实际上在本书的前一个版本中就是这么做的。我一般倾向于使用SCOPE_IDENTITY()的原因是,两个函数在一个重要的方面具有不同的行为。如果一个插入操作造成触发器被激活,并且该触发器会使数据插入到包含标识列的不同表,则@@IDENTITY将选取该值——在当前连接期间创建该值。SCOPE_IDENTITY()只会选取在当前批处理作用域内完成的插入值。对于两种函数具有不同行为的大多数情况,SCOPE_IDENTITY()就是我们所需的函数;而在其他情况下,两者是相同的。
11.1.6 生成序列
在检索值序列时,标识列有时会带来过多的限制。标识只存在于单个表中。当任何人访问该表时,标识不会循环或重复数值及其增量——无法在标识列中保存一定范围的数值。此外,修改标识(或标识列中的值)也是非常困难的。对于提供数值序列的较为一般的情况,SQL Server2012现在包含了SEQUENCE对象。
序列返回请求的下一个值,按照在创建时定义的值进行递增。由于序列是作为独立的对象存在的,因此可以在执行插入之前检索序列中的下一个值(获得标识),将其应用于多个表,按照任意列列表进行排序,甚至可以根据需要在到达最大值时循环遍历到最小值。
1. 创建序列
在讨论序列的具体作用之前,首先介绍如何创建序列。建立序列对象的语法如下:
CREATE SEQUENCE [schema_name.]sequence_name
[
[ ; ]
{
[AS { built_in_integer_type | user-defined_integer_type } ]
|START WITH
IINCREMENT BY
I{ MINVALUE
I{ MAXVALUE
I{ CYCLE | NO CYCLE }
I{ CACHE [
}
其中的每个元素都非常简单明了,表11-2介绍了这些元素。
表11-2 SEQUENCE语法元素
元 素 |
说 明 |
sequence_name |
sequence_name是所创建序列的唯一名称 |
AS {type} |
序列始终是某种类型的整型值。AS子句是可选的;默认情况下,您创建的任何序列的类型都是int。其他允许的类型是tinyint、smallint、 bigint(具有一定的模式)、精度为0的小数或数值类型(即整数),以及基于某种整数类型的任何用户自定义类型 |
START WITH |
该常量是第一次从序列检索时获得的数值。因为序列只可以返回其MINVALUE和MAXVALUE之间的值,所以该常量需要在这一范围内。默认情况下,升序序列以MINVALUE开头,而降序序列以MAXVALUE开头。因此,如果您需要其他的起始值,则可以在该元素中指定 |
INCREMENT BY |
每次使用NEXT VALUE FOR函数从序列中检索数值时,按照该常量递增序列。如果提供的常量是正数,则该序列是升序的;否则,该序列就是降序的。INCREMENT BY不能为0(这样就使序列区别于常量) |
MINVALUE | NO MINVALUE |
MINVALUE是序列可以保存的最小值。如果没有指定MINVALUE,则它是使用的数据类型可以保存的最小值——即使这是一个负数。如果您想要从1开始计数,则可以在此处进行设置(前提是您已经接受 START WITH的默认值) |
MAXVALUE | NO MAXV ALUE |
类似于MINVALUE,MAXVALUE采用数据类型作为其默认值。如果您的序列没有循环,则尝试读取超出MAXVALUE的值将产生错误 |
CYCLE | NO CYCLE |
NO CYCLE在行为上类似于标识(这是序列的默认行为)——最大递增到MAXVALUE,然后抛出错误——而设置为CYCLE的序列将在到达其限制值时从最小值(适用于升序序列)或最大值(适用于降序序列)重新开始。因为值可以重复,所以这种序列不适用于标识替换主键,但是非常适合于将数据划分到一系列存储桶中 |
CACHE [ |
如果您关注性能,就会知道在不需要的时候访问磁盘会影响性能。为 CACHE元素指定一个值意味着只有在从该序列检索一定数量的条目后,该序列才会返回访问磁盘。它甚至不会占用过多内存;它不是在缓存中存储所有的下一个值,而是仅在内存中存储当前值以及在访问磁盘前还可以提供多少个值。只要服务器是稳定的,这种方法就是安全的;预料之外的停机会导致丢失缓存中剩余的值
|
2. 使用序列
序列是一种主动的解决方案,而SCOPE_IDENTITY()则是反应性的解决方案。标识值检索函数的唯一作用就是指出分配了哪些值。使用SEQUENCE对象可以从特定的值开始,将其保存到所需的任意多个位置。接下来查看如何使用序列。
首先创建一组表,这些表类似于前面所使用的表,但是这一次添加SEQUENCE对象。
CREATE TABLE TestSequence
(
SeqCol int NOT NULL
PRIMARY KEY
);
CREATE TABLE TestSeqChild1
(
SeqCol int
PRIMARY KEY
FOREIGN KEY
REFERENCESTestSequence(SeqCol)
);
CREATE TABLE TestSeqChild2
(
SeqCol int
PRIMARY KEY
FOREIGN KEY
REFERENCESTestSequence(SeqCol)
);
CREATE SEQUENCE SeqColSequence AS int
START WITH 1
INCREMENT BY 1
MINVALUE 0;
还记得使用标识将数据插入到父表中而必须执行的操作,然后指出如何在子表中获得正确的值吗?接下来对Sequence表执行这些操作。
DECLARE @Seq int; -- This variable will holdour your Sequence value
-- Retrieve the next value in the sequence
SELECT @Seq = NEXT VALUE FOR SeqColSequence;
PRINT 'The value we you got from SeqColSequence was ' +
CONVERT(varchar(2),@Seq);
/* Now we you have the value of the next ID we you'regoing to work with.
** Inserting that value into the parent and child tablesbecomes
** a simple matter of performing the inserts using thevariable. */
INSERT INTO TestSequence (SeqCol) VALUES (@Seq);
INSERT INTO TestSeqChild1 VALUES (@Seq);
INSERT INTO TestSeqChild2 VALUES (@Seq);
当使用标识列时,代码必须:
(1)声明变量。
(2)插入父值。
(3)使用SCOPE_IDENTITY()填充变量。
(4)插入子值。
使用序列时,代码改为:
(1)声明变量。
(2)获取序列中的下一个值。
(3)插入父值和子值。
对于这种情况,使用序列比使用标识更不容易犯错,并且根据我的观点,它也更加直观。然而,序列是SQL Server2012中的新对象,因此无论其多么有用,您仍然应该准备好面对还有许多标识列存在的情况。在SQL Server2012发布之前编写的代码无疑并不会使用序列,而早期的SQL开发人员需要花费一定的时间来适应新的约定。
11.1.7 使用@@ROWCOUNT
在到目前为止所执行的许多查询中,可以很容易地知道一条语句影响了多少行——“查询” 窗口会告诉我们。例如,如果运行:
USE AdventureWorks
SELECT * FROMPerson.Person;
这样可以看见Person中的所有行,但是也可以看见查询所影响的行数(本例中是表中的所有行):
(19 972 row(s) affected)
但是如果需要通过编程来知道影响了多少行,那么应该怎么做呢?与SCOPE_IDENTITY() 类似,@@ROWCOUNT这个工具知道在脚本运行时正在发生什么——不过,这次得到的值是影响的行数而不是标识值。
用一个示例进一步检验这一点:
USE AdventureWorks;
GO
DECLARE @PersonCount int; --Notice the single @ sign
SELECT * FROM Person.Person;
SELECT @PersonCount = @@ROWCOUNT;
PRINT 'The value of @@ROWCOUNT was ' +
CONVERT(varchar(6),@PersonCount);
这一次同样显示了所有的行,但是注意得到的新结果:
The value of @@ROWCOUNT was 19972
在本书后面介绍存储过程时,可以看到这在哪些情形下可能是有用的方法。目前,只要认识到@@ROWCOUNT系统函数提供了一种了解语句所做事情的方法,并且这也并不局限于SELECT语句——UPDATE、INSERT和DELETE也可设置这个值。
注意:如果浏览这个示例,那么您可能注意到,和使用SCOPE_IDENTITY()时所做的工作一样,这里选择将这个值移动到一个存放的变量中。@@ROWCOUNT将会被接下来的语句重置为一个新的值,所以,如果使用@@ROWCOUNT值执行多个活动,那么应当将它移动到一个安全的存放位置中。
11.2 将语句分组到批处理中
批处理是作为一个逻辑单元的一组T-SQL语句。一个批处理中的所有语句被组合成一个执行计划,因此对所有语句一起进行语法分析,并且必须通过语法验证,否则将不执行任何一条语句。注意,尽管如此,这并不能防止运行时错误的发生。如果发生运行时错误,那么任何在发生运行时错误之前执行的语句将仍然是有效的。简而言之,如果一条语句不能通过语法分析,那么就不会运行任何语句。如果一条语句在运行时失败,那么在产生错误的语句之前的所有语句都已经运行。
到目前为止已经运行的所有脚本都是由一个批处理组成的。甚至迄今为止在本章中已经分析过的脚本都只是一个批处理。为了将一个脚本划分为多个批处理,可使用GO语句。GO语句具有以下特点:
• 必须自成一行(并且只有注释可以在同一行上);也有一个例外,稍后将讨论,但是这里先将GO语句看作需要自成一行。
• 使得从脚本的开始部分或者最近一个GO语句(任何一个较近的GO语句)以后的所有语句编译成一个执行计划并发送到服务器,与任何其他批处理无关。
• 不是T-SQL命令,而是由各种SQL Server命令实用程序(sqlcmd和ManagementStudio中的“查询”窗口)识别的命令。
11.2.1 自成一行
GO命令应当自成一行。在技术上,可以在GO命令之后的同一行上开始一个新的批处理,但是这会严重影响可读性。T-SQL语句不能放在GO语句之前,否则GO语句经常会被错误地解释,从而造成语法分析错误或产生一些其他不可预料的后果。例如,如果在WHERE子句之后使用一个GO语句:
SELECT * FROM Person.Person WHERE BusinessEntityID = 1;GO
分析器就不知道如何处理:
Msg 102, Level 15, State 1, Line 1
Incorrect syntax near 'GO'.
11.2.2 每个批处理单独发送到服务器
因为每个批处理被独立地处理,所以一个批处理中的错误不会阻止另一个批处理运行。要说明这一点,看一下下面的代码:
USE AdventureWorks;
DECLARE @MyVarchar varchar(50); --This DECLARE only lasts for this batch!
SELECT @MyVarchar = 'Honey, I''m home...';
PRINT 'Done with first Batch...';
GO
PRINT @MyVarchar; --This generates an error since @MyVarchar
--isn't declared in this batch
PRINT 'Done with second Batch';
GO
PRINT 'Done with third batch'; -- Notice that this still gets executed
-- even after the error
GO
如果这些批处理之间有任何相互依赖性,那么或者每个批处理都失败,或者至少在错误发生之后的每个批处理都失败——但是它们之间没有依赖性。如果执行以上的脚本,那么结果如下所示:
Donewith first Batch...
Msg137, Level 15, State 2, Line 2
Mustdeclare the scalar variabie "@MyVarchar".
Donewith third batch
重申一下,每个批处理在运行时是完全自治的。尽管如此,要记住,可以构建下面这种意义上的相互依赖关系:即后一个批处理试图执行的工作依赖于前一个批处理已完成的工作——在11.2.5节讨论什么能跨越批处理而什么不能时会介绍这方面的内容。
11.2.3 GO不是T-SQL命令
一个常见的错误是认为GO是T-SQL命令。其实GO是一个只能被编辑工具(ManagementStudio、sqlcmd)识别的命令。如果您使用了第三方工具,那么它可能支持也可能不支持GO命令,但是大多数声称支持SQL Server的工具是支持GO命令的。
当编辑工具遇到GO语句时,它会将GO语句看作一个终止批处理的标记,将其打包,并且作为一个独立的单元发送到服务器——不包括GO。这是正确的,因为服务器本身根本不知道GO应该是什么意思。
如果在一个贯通查询中用ODBC、OLE DB、ADO、ADO.NET、SqlNativeClient或者任何其他访问方法,那么会得到来自服务器的一个错误消息。GO只是该工具的一个指示器,指明什么时候结束当前的批处理,以及什么时候适合开始一个新的批处理。
11.2.4 批处理中的错误
批处理中的错误分为以下两类。
• 语法错误
• 运行时错误
如果查询分析器发现一个语法错误,那么批处理的处理过程会立即被取消。因为语法检查发生在批处理编译或者执行之前,所以在语法检查期间的一个失败意味着还没有批处理被执行——不管语法错误发生在批处理中的什么位置。
运行时错误的工作方式有很大不同。因为任何在遇到运行时错误之前执行的语句己经完成了,所以除非是未提交的事务的一部分,否则这些语句所做的任何事情将不受影响(第14章将介绍事务,但这里相关的内容是事务表示一个全部完成的或者什么都没有发生的情况)。运行时错误之外所发生的事情取决于错误的性质。一般而言,运行时错误将终止从错误发生的地方到批处理末端的批处理的执行。一些运行时错误(例如违反参照完整性)只会阻止违反相应约束的语句运行——仍然会执行批处理中的所有其他语句。后一种情况说明了为什么错误检查如此重要——第12章讨论存储过程时将详细介绍错误检查。
11.2.5 何时使用批处理
使用批处理有若干目的,但是所有的批处理具有一个共同点——当脚本中的一些事情必须发生在另外一件事情之前或者分开发生时,就需要使用批处理。
1. 要求有自己的批处理的语句
有一些命令必须有它们自己的批处理。这些命令包括:
l CREATEDEFAULT
l CREATEPROCEDURE
l CREATE RULE
l CREATETRIGGER
l CREATE VIEW
如果想在单个脚本中将这些语句中的任意一个和其他的语句进行组合,那么需要通过使用 GO语句将它们分散到各自的批处理中。
注意:如果DROP一个对象,那么可能想要将DROP语句放在它自己的批处理中,或者至少和其他DROP语句在一个批处理中。为什么呢?因为如果将在以后创建一个具有相同名称的对象,那么除非DROP已经发生了,否则 CREATE不能通过批处理语法分析。这意味着需要在前面一个单独的批处理中运行DROP,以使得当具有CREATE语句的批处理执行时,DROP操作已经完成。
2. 使用批处理建立优先权
当需要建立优先权时,就很可能用到批处理——也就是说,在下一个任务开始之前,需要彻底完成上一个任务。在大多数时候,SQL Server可以很好地处理这种情况——脚本中的第一条语句是首先执行的,并且脚本中的第二条语句可以依赖第一条语句运行时服务器所处的适当状态。尽管如此,SQL Server有时还是不能解决这种问题。
下面先看一个创建带有表的数据库的示例:
USE master;
CREATE DATABASE Test;
CREATE TABLE TestTable
(
col1 int,
col2 int
);
执行上面的脚本,最初看起来,每条语句都运行得很好:
Command(s) completed successfully.
不过,事情并不是它们所看起来的那样——在Test数据库中检验INFORMATION_SCHEMA:
USE Test;
SELECT TABLE_CATALOG
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'TestTable';
可以注意到缺少了一些内容:
(0 row(s) affected)。
为什么表被创建在错误的数据库中呢?答案取决于运行CREATE TABLE语句时当前的数据库是什么。在这个示例中,它碰巧是master数据库,所以表就创建在该数据库中。
注意:因为在运行这个脚本时当前数据库可能不是master数据库,所以可能得到一个不同的结果。不过,当前数据库几乎可能是任何数据库。这就是要注意使用USE语句的原因。
这看似是一个很容易修正的问题——只需要使用USE语句,但是在测试新的理论之前,必须删除旧的(较旧的)数据库:
USE MASTER;
DROP DATABASE Test;
DROP TABLE TestTable;
然后可以运行新修改的脚本:
CREATE DATABASE Test;
USE Test;
CREATE TABLE TestTable
(
col1 int,
col2 int
);
但是,这样也有它自身的问题:
Msg 911, Level 16, State 1, Line 3
Database ‘Test’ does not exist. Make sure that the nameis entered correctly.
分析器尝试验证该代码并发现其正在通过USE命令引用一个不存在的数据库。这时批处理语句不可或缺,因此在尝试使用新的数据库之前,需要先完成CREATEDATABASE语句:
CREATE DATABASE Test;
GO
USE Test;
CREATE TABLE TestTable
(
col1 int,
col2 int
);
现在事情变得好多了。结果看起来是相同的:
Command(s) completed successfully.
但是当运行INFORMATION_SCHEMA查询时,事情得到证实:
TABLE_CATALOG
----------------------------------
Test
(1 row(s) affected)
下面继续来看看另一个示例,在这个示例中更需要显式建立优先权。
当使用ALTERTABLE语句修改一个列的类型或者添加列时,只有执行修改任务的批处理完成时,才能利用这些更改。
如果添加一列到Test数据库里的TestTable表中,然后尝试在第一个批处理没有结束时引用添加的列:
USE Test;
ALTER TABLE TestTable
ADD col3 int;
INSERT INTO TestTable
(col1, col2,col3)
VALUES
(1,1,1);
那么会得到一条错误消息——SQL Server不能解析新的列名称,于是产生错误:
Msg 207, Level 16, State 1, Line 6
Invalid column name 'col3'.
不过只需要在ADDcol3 int之后添加一个简单的GO语句,一切就会正常运行:
(1 row (s) affected)
11.3 从命令提示符运行:sqlcmd
sqlcmd是一个允许通过Windows命令提示符来运行脚本的实用程序。这对于执行转换或者维护脚本是非常好的,也是一种获取文本文件的简易方式。
sqlcmd代替了旧的osql。osql仍包括在SQL Server中,不过只是为了向后兼容。更老的命令行实用程序isql现在已不再被支持,并且您应该准备好不再使用osql。
通过命令行运行sqlcmd的语法包含大量不同的开关,语法如下所示:
sqlcmd
[{ { -U
[ -N encrypt connection ][ -C trust the servercertificate ]
[ -z
[ -S
[ -d
[ -l
[ -i ] [ -o
[ -f
[ -u unicode output] [ -r [ 0 | 1 ] msgs to stderr ]
[ -R use client regional settings]
[ -q "
[ -e echo input ] [ -t
[ -I enable Quoted Identifiers ]
[ -v var = "
[ -h
[ -W remove trailing spaces ]
[ -k [ 1 I 2 ] remove [replace] control characters ]
[ -y
[ -b on error batch abort] [ -V
[ -a
[ -L [ c ] list servers [clean output] ]
[ -p [ 1 ] print statistics [colon format] ]
[ -x [ 1 ] ] disable commands, startup script,environment variables [and exit]
[ -? show syntax summary]
对于这些标记,需要记住的最重要的事情是,这些标记中的大部分(奇怪的是,不是全部)是区分大小写的。例如,“-Q”和“-q”都将执行查询,但是当查询完成时,前者将退出sqlcmd,而后者不会退出。
所以,下面尝试直接从命令行执行快速查询。要记住,这意味着在Windows命令提示符下运行(不使用Management Console):
SQLCMD -Usa -Pmypass -Q "SELECT * FROM AdventureWorks.Production.Location"
注意:-P是指示密码的标记。如果服务器配置成需要其他密码而不是空密码(应该这么设置!),那么需要紧跟在-P之后提供密码,中间没有空格。如果使用Windows身份验证而不是SQLServer身份验证,那么在-U和-P参数的位置用-E参数替换(删除这两个参数,而只替换成一个-E参数)。
如果在命令提示符下运行该命令,那么应当得到这样的结果:
C:\>SQLCMD -E -Q "SELECT * FROM AdventureWorks.Production.Location"
现在,快速创建一个文本文件以了解sqlcmd语法包括一个文件时的工作方式。在命令提示符下输入下列代码:
C:\>copy con testsql.sql
这样将转入一个空行(没有任何种类的提示符),在该空行中可以输入下列命令:
SELECT * FROM AdventureWorks.Production.Location
然后按F6键和回车键(结束文本文件的创建)。这时应得到这样的一条消息:
1 file(s) copied.
这次使用一个脚本文件重新尝试先前的查询。提示符处的命令行仅仅有一个细微的变化:
C:\>sqlcmd -Usa —Pmypass -i testsql.sql
所得到的结果与使用-Q运行查询时所得到的一样。当然,主要的区别在于这次是从一个文件获取命令。这个文件中可以有几百个(甚至几千个)不同的命令。
试一试 使用sqlcmd生成一个文本文件
作为说明sqlcmd的最后一个示例,下面将利用它生成一个文本文件,可能将这个文本文件导入到另外一个应用程序(例如Excel)中进行分析。
在第10章中创建了一个显示前一天订单的视图。因此,首先将视图的核心查询复制到一个文本文件中:
C:\copy con YesterdaysOrders.sql
这将再次转入一个空行(没有任何种类的提示符),在那里可以输入以下代码:
USE AdventureWorks
SELECT sc.AccountNumber,
soh.SalesOrderlD,
soh.OrderDate,
sod.ProductID,
pp.Name,
sod.OrderQty,
sod.UnitPrice,
sod.UnitPriceDiscount* sod.UnitPrice * sod.OrderQty AS TotalDiscount, sod.LineTotal
FROM Sales.Customer AS sc
INNER JOIN Sales.SalesOrderHeader ASsoh
ON sc.CustomerlD = soh.CustomerID
INNER JOIN Sales.SalesOrderDetail ASsod
ON soh.SalesOrderlD = sod.SalesOrderlD
INNER JOIN Production.Product AS pp
ON sod.ProductID = pp.ProductID
WHERE CAST(soh.OrderDate AS Date) =
CAST(DATEADD(day,-1,GETDATE()) AS Date)
再次按下F6键和Enter键,让Windows保存文件。
现在有了用于查询的文本文件源代码,并且基本上准备好了让sqlcmd帮助产生输出。尽管如此,首先需要有一些前一天的数据(除非运行了第10章使用的UPDATE语句改变了前一天订单的日期,否则没有样本数据会有前一天的数据)。这条语句将订单块改为前一天的日期。明白了这一点之后,再次运行该语句(如果喜欢的话,可以通过“查询”窗口来运行):
USE AdventureWorks
UPDATE Sales.SalesOrderHeader
SET OrderDate = CAST(DATEADD(day,-1,GETDATE()) AS Date),
DueDate= CAST(DATEADD(day,11,GETDATE()) AS Date),
ShipDate= CAST(DATEADD(day,6,GETDATE()) AS Date)
WHERESalesOrderlD < 43663;
所以现在至少有了一行数据,那是前一天的订单。这样就基本准备完成工作了;然而,因为前面已经提到想要将结果放到一个文本文件中,所以这次需要添加一些额外的参数到sqlcmd命令行,告诉SQL Server输出到何处:
C:\>sqlcmd -UMyLogin -PMyPass -iYesterdaysOrders.sql-oYesterdaysOrders.txt
当通过sqlcmd运行上述代码时,没有任何特别之处——只是再次得到Windows提示符(最可能是C:\),但是现在来检查一下YesterdaysOrders.txt文件中有什么内容:
C:\>TYPE YesterdaysOrders.txt
这样只得到一行:
C:\>TYPE YesterdaysOrders.txt
示例说明
这里开始将必需的SQL命令打包成一个脚本——首先是USE命令,然后是实际的SELECT语句。
然后使用sqlcmd执行语句。正如本章前文所述,-U和-P命令提供了登录用户名和密码信息。-i参数告诉sqlcmd有一个输入文件,并且紧跟在-i参数后面包含那个文件的名称。最后,包含了-o参数以告诉sqlcmd希望将输出写到一个文件中(然后提供了一个文件名——YesterdaysOrders.txt)。不要被这两个名称都是YesterdaysOrders的文件所迷惑——它们分别是带有.sql和.txt后缀的单独文件,后缀用来区分它们的特定用途。
有很多种不同的参数用于sqlcmd,但最重要的参数是登录名、密码和说明您想做什么的参数(直接查询或者输入文件)。可以将这些参数混合和匹配,以便通过这个看似很简单的命令行工具实现相当复杂的操作。
11.4动态SQL:用EXEC命令动态生成代码
将所有需要的代码保存在脚本中是非常好的,但是如果直到运行时才知道需要执行什么代码,那该怎么办呢?
注意:到目前为止都用sqlcmd来完成示例——下面的示例应用Management Console来运行。
11.4.1动态生成代码
SQL Server允许通过使用字符串操作动态构建SQL语句,但是这有一些缺陷。需要这么做的原因是:往往是直到运行时才能知道一些内容的细节。语法如下所示:
EXEC ({
或者:
EXECUTE ({
和执行一个存储过程一样,使用EXEC或者EXECUTE没有任何区别。
为了举例说明,下面在AdventureWorks数据库中创建一个哑表,以从中获得动态信息:
USE AdventureWorks;
GO
--Create The Table. You'll pull info from here for yourdynamic SQL
CREATE TABLE DynamicSQLExample
(
TableID int IDENTITY NOT NULL
CONSTRAINTPKDynamicSQLExample
PRIMARY KEY,
SchemaName varchar(128) NOT NULL,
TableName varchar(128) NOT NULL
);
GO
/* Populate the table. In this case, you're grabbingevery user
** table object in this database */
INSERT INTO DynamicSQLExample
SELECT s.name AS SchemaName, t.name AS TableName
FROM sys.schemass
JOIN sys.tablest
ON s.schema_id= t.schema_id;
结果类似如下所示:
(73 row(s) affected)
注意:实际的结果取决于您在本书中已经采用的示例、没有采用的示例,以及那些主动采用并在完成之后执行DROP操作的示例。在任何情况下,不要太担心。
所以,现在拥有的是当前数据库中所有表的一个列表。现在假设希望选择某一个表的一些数据,但是希望只在运行时通过使用表的ID来识别这个表。例如,取出表中ID为25的所有数据:
DECLARE @SchemaName varchar(128);
DECLARE @TableName varchar(128);
-- Grab the table name that goes with our ID
SELECT @SchemaName = SchemaName, @TableName = TableName
FROMDynamicSQLExample
WHERE TableID =25;
-- Finally, pass that value into the EXEC statement
EXEC ('SELECT * FROM ' + @SchemaName + '.' + @TableName);
如果就像这里所做的那样,将表的名称加入到DynamicSQLExample表,那么值为25的TablelD应该对应Production.UnitMeasure表。如果那样的话,得到的结果如下所示(为了简洁起见,最右边的列已经省略)。
11.4.2理解动态SQL的危险性
和大多数有用的功能一样,使用EXEC时也会产生一些问题。EXEC的陷阱包括以下几点:
l EXEC和调用它的代码都在单独的作用域下运行——也就是说,调用代码不能引用EXEC语句中的变量,并且在调用代码中的变量被解析为用于EXEC语句的字符串之后,EXEC不能引用这些变量。如果需要在动态SQL和调用它的例程间传递值,考虑使用 sp_executesql。
l 默认情况下,EXEC在当前用户的安全上下文下运行——而不是在调用对象(对象经常在对象所有者而非当前用户的安全上下文下运行)的安全上下文下运行。
l EXEC与调用对象运行在相同的连接和事务环境下(本书第14章将进一步讨论)。
l 对EXEC字符串执行的要求函数调用的串联必须先于实际调用的EXEC语句——不能在执行EXEC调用的相同语句中执行函数的串联。
l EXEC不能在用户自定义函数内使用。
l 如果您没有谨慎对待,那么EXEC会给黑客提供可攻击的漏洞。
因为上述的每一点都有些难以理解,所以分别来介绍。
1. EXEC的作用域
用EXEC语句确定变量作用域不是很直观。调用EXEC语句的实际语句行与运行EXEC语句的批处理或过程的其他部分具有相同的作用域,但是作为EXEC语句的结果来执行的代码被认为处在其自身的批处理中。通常就是这样的情形,下面这个示例最能说明问题:
USE AdventureWorks;
/* First, we'll declare two variables: one for stuffwe're putting into
** the EXEC, and one that we think will get somethingback out (it won't)
*/
DECLARE @InVar varchar(50);
DECLARE @OutVar varchar(50);
-- Set up our string to feed into the EXEC command
SET @InVar = 'SELECT @OutVar = FirstName FROMPerson.Person WHERE BusinessEntityID = 1';
-- Now run it
EXEC (@Invar);
-- Now, just to show there's no difference, run theselect without using a in variable
EXEC ('SELECT @OutVar = FirstName FROM Person.PersonWHERE BusinessEntityID = 1');
-- @OutVar will still be NULL because we haven't beenable to put anything in it
SELECT @OutVar;
现在,看看以上代码的输出:
Msg 137, Level 15, State 1, Line 1
Must declare the scalar variable "@OutVar".
Msg 137, Level 15, State 1, Line 1
Must declare the scalar variabie "@OutVar".
---------------------------------------------------------------------------------------------
NULL
(1 row(s) affected)
SQL Server立即指出用户显然不知道正在发生的事情。为什么在已经声明@OutVar的情况下还会得到一条“Must Declare”的错误消息呢?因为在外部作用域内声明了该变量——而不是在EXEC内部声明的。
如果运行下面略有区别的代码,那么将会发生什么事情呢?
USE AdventureWorks;
-- This time, we only need one variable. It does need tobe longer though.
DECLARE @InVar varchar(200);
/* Set up our string to feed into the EXEC command. Thistime we're going
** to feed it several statements at a time. They will allexecute as one
** batch.
*/
SET @InVar = 'DECLARE @OutVar varchar(50);
SELECT @OutVar = FirstName FROM Person.Person WHEREBusinessEntityID = 1;
SELECT ''The Value Is '' + @OutVar';
-- Now run it
EXEC (@Invar);
这次返回的结果更接近于所期望的结果:
--------------------------------------------------------------------------------------------------
The Value Is Ken
注意:这里使用连在一起的两个引号标志以表明实际上想要一个引号标志,而不是终止字符串。
所以,这里所看见的是有两个不同的作用域操作,并且这两个作用域也不能通信。但是,如果不使用外部机制,如临时表或名为sp_executesql的特殊存储过程,就没有办法在内部和外部作用域之间传递信息。如果决定使用一个临时表在作用域之间通信,那么要记住任何在EXEC语句作用域内创建的临时表的存活期只能与EXEC语句一样长。
注意:临时表的存活期只能和EXEC过程一样长,后面将在讲述触发器和存储过程时继续讨论这种行为特性。
2.该规则的例外情况
在EXEC的作用域内有一个例外情况,这可在执行EXEC之后看到——即系统函数,所以像@@ROWCOUNT这样的函数仍然可以使用。再次查看一个快速示例:
USE AdventureWorks;
EXEC('SELECT * FROM Production.UnitMeasure');
SELECT 'The Rowcount is ' + CAST(@@ROWCOUNT as varchar);
得到的结果为(在结果集之后):
The Rowcount is 38
3.安全上下文和EXEC
因为还没有讲述存储过程和安全性的问题,所以这时候讲述这一内容是比较困难的。不过EXEC命令还是属于本章的讨论范围,而不属于第12章的讨论范围,所以在这里讨论这个问题。
如果授予某人运行存储过程的权限,这就暗示着他们还获得了在存储过程内执行要求的操作的权限。例如,假设有一个存储过程,用于列出在去年雇佣的所有雇员。有权限执行该存储过程的人就可以这么做(并且得到返回的结果),即使他们没有直接访问Employees表的权限。这的确是很方便的,而这样做的原因将在第12章中探讨。
开发人员通常认为这样的隐含权限对EXEC语句也是有效的——但实际上不是这样的。任何在EXEC语句内部所作的引用默认将运行在当前用户的安全上下文下。所以,假设用户有运行名为spNewEmployees的过程的权限,但是没有访问Employees表的权限。如果 spNewEmployees通过运行一个简单的SELECT语句得到这些值,那么一切工作正常。但是,如果spNewEmployees使用EXEC语句执行这个SELECT语句,那么因为用户没有在Employees 表上运行SELECT语句的权限,所以EXEC语句将失败。
由于目前还不了解关于存储过程的更多信息,因此现在不会进一步讨论,但是在后面讨论存储过程时,将回过头来讨论这个问题。
4.函数串联和EXEC
这实际上是不太方便的做法,因为有一种更简单的解决方案。简单地说,不能对EXEC命令中的EXEC字符串运行函数。例如:
USE AdventureWorks;
-- This won't work
DECLARE @NumberOfLetters int = 15;
EXEC('SELECT LEFT(Name,' + CAST(@NumberOfLetters ASvarchar) + ') AS ShortName
FROM Production.Product');
GO
-- But this does
DECLARE @NumberOfLetters AS int = 15;
DECLARE @str AS varchar(255);
SET @str = 'SELECT LEFT(Name,' + CAST(@NumberOfLetters ASvarchar) + ') AS ShortName
FROM Production.Product';
EXEC(@str);
第一个例子出错是因为CAST函数需要在EXEC语句之前被完全地解析:
Msg 102, Level 15, State 1, Line 9
Incorrect syntax near 'CAST'.
而第二个例子正常工作,因为EXEC的输入已经是一个完整的字符串。
ShortName
--------------------------------
Adjustable Race
All-Purpose Bik
AWC Logo Cap
Women's Tights,
…
…
Women's Tights,
Women's Tights,
Women's Tights,
5. EXEC和UDF
因为至今还没有讨论过用户定义函数,所以这是一个很难讲述的问题,但可以肯定的是,不允许在UDF中使用EXEC运行动态SQL(然而,在有些情况下使用EXEC运行存储过程是合法的)。
6. SQL注入和EXEC
作为初级程序员,很多与安全相关的问题并不会给您带来真正的困扰。其他人(例如DBA)将执行大量的工作来确保应用程序体系结构是安全的,因此这并不是本书要介绍的主题。然而,在这里存在明显的危险,因此有必要稍作介绍。
当您使用动态SQL时,需要确保知道串联和运行的代码是您已经批准的代码。为此,确保用户提供的值被取消,或者(首选方式)是完全不在创建的SQL字符串中使用该值。如果您执行包含用户输入的字符串的代码,就存在被其他用户攻击的危险。这是SQL注入攻击的一种形式,通过确保仔细控制进入SQL字符串的内容来避免这种攻击。
11.5使用控制流语句
控制流语句对于如今的所有编程语言来说都是不可或缺的。如果说编写的代码不能根据条件改变运行的命令,这是无法想象的。T-SQL提供了许多用于控制流的语句,包括:
•IF... ELSE
•GOTO
•WHILE
•WAITFOR
•TYY/CATCH
还有CASE语句(也就是其他语言中的SELECT CASE、DO CASE和SWITCH/BREAK),但其控制流的功能无法与其他语言中的对应语句相比。
11.5.1 IF...ELSE语句
IF...ELSE语句的工作方式和其他语言中的该语句类似,不过本书认为其与c语言中的实现方式最接近。基本语法为:
IF
END
[ELSE
END]
其中的表达式几乎可以是任何布尔表达式。
注意:这让我们想到了一个大多数SQL编程人员都会掉入的陷阱——未正确使用NULL值。我之前很多次调试存储过程,都只是为了寻找如下所示的语句:
IF @myvar = NULL
当然,这对于大多数系统来说都是不正确的(参见下面的解释),最终忽略了系统所有的NULL值。事实上,应写成这样:
IF @myvar IS NULL
例外情况取决于是否将ANSI_NULLS选项设置为ON或OFF。默认值是ON,这时可看到上述行为。可以通过将ANSI_NULLS选项设置为OFF来改变这一行为,但是本书强烈建议不要这样做,因为这违反ANSI标准(这也是常见的错误)。
注意,只有紧跟在IF关键字之后的语句才被视为有条件性的(按照IF语句的要求)。使用BEGIN...END,可以将多条语句作为控制流块的一部分,稍后将作讨论。
为了显示其简单的用法,运行一个常见的构建脚本的示例。假定要创建一个表(如果没有的话),如果该表已经存在,就不进行任何操作。可以使用EXISTS操作符(您可能记得我曾抱怨过,联机丛书中将EXISTS称为关键字,而我认为它是操作符)。
-- We'll run a SELECT looking for our table to start withto prove it's not there
SELECT 'Found Table ' + s.name + '.' + t.name
FROM sys.schemass
JOIN sys.tablest
ONs.schema_id = t.schema_id
WHERE s.name ='dbo'
AND t.name ='MyIFTest';
-- Now we're run our conditional CREATE statement
IF NOT EXISTS (
SELECT s.nameAS SchemaName, t.name AS TableName
FROMsys.schemas s
JOINsys.tables t
ONs.schema_id = t.schema_id
WHERE s.name= 'dbo'
AND t.name= 'MyIFTest'
)
CREATE TABLEMyIFTest(
Col1 int PRIMARY KEY
);
-- And now look again to prove that it's been created.
SELECT 'Found Table ' + s.name + '.' + t.name
FROM sys.schemass
JOIN sys.tablest
ONs.schema_id = t.schema_id
WHERE s.name ='dbo'
AND t.name ='MyIFTest';
注意中间部分,只有在无匹配表的情况下,才运行CREATE TABLE语句。第一次检查(在脚本开始处)发现表不存在,因此可以知道IF条件成立,将执行CREATE TABLE语句。
---------------------------------------------------------------------------
(0 row(s) affected)
------------------------------------------------------------------Found
Table dbo.OurlFTest
(1 row(s) affected)
1. ELSE子句
有条件地运行语句这一概念是相当不错的,但它并不总是能够应对我们希望处理的所有情况。通常,在处理IF条件时,我们希望一些特定的语句不只是在条件为真的情况下执行,还希望一些语句在条件为假或ELSE条件下运行。
注意:您可能会碰到不能求得布尔值的情况——结果是未知的(例如,如果与NULL值比较)。任何返回未知值结果的表达式将被视为FALSE。
ELSE语句的工作方式和其他语言中的一样。确切语法可能稍有不同,但具体细节还是一样的;如果IF子句中的语句不成立,那么执行ELSE子句中的语句。
现在对前面的示例进行扩展,即如果未创建表,就输出警告消息:
-- Now we're run our conditional CREATE statement
IF NOT EXISTS (
SELECT s.nameAS SchemaName, t.name AS TableName
FROMsys.schemas s
JOINsys.tables t
ONs.schema_id = t.schema_id
WHERE s.name= 'dbo'
AND t.name= 'MyIFTest'
)
CREATE TABLEMyIFTest(
Col1 int PRIMARY KEY
);
ELSE
PRINT 'WARNING:Skipping CREATE as table already exists';
如果已经运行了前面的示例,那么表已存在,运行这第二个示例将得到警告消息:
WARNING: Skipping CREATE as table already exists
2. 将代码组合成块
有时,需要像对一条语句那样对待一组语句(如果执行这块语句,就全部执行它们——否则,一条语句都不执行)。例如,IF语句默认只将紧跟在IF关键字之后的语句视为条件代码的一部分。如果条件是要运行多条语句,那么该怎么办呢?如果要为条件中希望运行的每行代码都创建一个单独的IF语句,那会是一件很痛苦的事。
幸运的是,和大多数其他语言中的IF语句一样,SQLServer提供了一种将代码组合成块的方法,可以将它们视为一个整体。这种代码块从BEGIN语句开始,到END语句结束。其工作方式如下所示:
IF
BEGIN --Firstblock of code starts here -- executes only if
--expression is TRUE
Statement that executes If expression is TRUE
Additional statements
…
Still going with statements from TRUE expression
IF
BEGIN
Statement that executes if both outside and inside
expressions are TRUE
Additional statements
…
…
Still statements from both TRUE expressions
END
Out of the condition from inner condition, but still
part of first block
END --First blockof code ends here
ELSE
BEGIN
Statement that executes if expression is FALSE
Additional statements
…
…
Still going with statements from FALSE expression
END
注意代码块的嵌套。在每种情况下,内部代码块都被看成是外部代码块的一部分。嵌套BEGIN...END块的层数一般是无限制的,不过本书还是建议尽量少用一些。如果进行嵌套,则其可读性实际还是有限的——即使特别注意代码的格式问题。
为了运用上述概念,对创建表的示例进行一些修改。这次将提供一条通知消息,而不管是否创建表。
-- This time we're adding a check to see if the tableDOES already exist
-- We'll remove it if it does so that the rest of ourexample can test the
-- IF condition. Just remove this first IF EXISTS blockif you want to test
-- the ELSE condition below again.
IF EXISTS (
SELECT s.nameAS SchemaName, t.name AS TableName
FROMsys.schemas s
JOINsys.tables t
ONs.schema_id = t.schema_id
WHERE s.name= 'dbo'
AND t.name= 'MyIFTest'
)
DROP TABLEMyIFTest;
-- Now we're run our conditional CREATE statement
IF NOT EXISTS (
SELECT s.nameAS SchemaName, t.name AS TableName
FROMsys.schemas s
JOINsys.tables t
ON s.schema_id = t.schema_id
WHEREs.name = 'dbo'
ANDt.name = 'MyIFTest'
)
BEGIN
PRINT'Table dbo.MyIFTest not found.';
PRINT'CREATING: Table dbo.MyIFTest';
CREATETABLE MyIFTest(
Col1 int PRIMARY KEY
);
END
ELSE
PRINT'WARNING: Skipping CREATE as table already exists';
这里混合了IF语句的各种使用形式。有最基本的IF语句——没有BEGIN...END或ELSE语句。而在其他IF语句中,IF部分使用BEGIN...END块,但ELSE没有。
注意:这样做是为了说明如何混合使用它们。不过,这里还是建议您要注意一致性。如果混合分组IF语句的话,就很难分清由哪个IF...ELSE条件控制哪条语句。在实践中的做法是,如果在给定的IF块中的任一语句上使用BEGIN...END,就对IF语句中的每个代码块都使用它,即使对于特定条件只有一条语句也如此。
11.5.2 CASE语句
在某些方面,CASE语句可以与其他一些语言的某个语句相对应。过程式编程语言中与 CASE语句类似的语句有:
• Switch: C、C#、C++、Java、php、Perl 和 Delphi
• Select Case: Visual Basic
• Do Case: Xbase
• Evaluate: COBOL
我确信还有其他一些编程语言——上述这些只是来自我多年来使用过的语言。在T-SQL中使用CASE语句的唯一缺点是,它基本上只是个替换操作符而不是控制流语句。
有多种方法可以编写CASE语句——用输入表达式或布尔表达式。第一选择是使用输入表达式,它将与每个WHEN子句中使用的值比较。SQL Server文档将这种语句称为简单CASE语句:
CASE
WHEN
[…n]
[ELSE
END
另一种选择是给每个WHEN子句提供一个布尔表达式,该表达式将求值为TRUE或FALSE。SQL Server文档将这种语句称为搜索CASE语句:
CASE
WHEN
[…n]
[ELSE
END
CASE语句的最大优点是它可与SELECT语句“内联”使用(也就是作为其不可分割的一部分)。这种使用方式的作用实际上是非常强大的。
1.简单CASE语句
简单CASE语句采用一个比较结果为布尔值的表达式。看一下下面的示例:
USE AdventureWorks;
GO
SELECT TOP 10 SalesOrderID,
SalesOrderID % 10 AS 'Last Digit',
Position = CASE SalesOrderID % 10
WHEN 1 THEN 'First'
WHEN 2 THEN 'Second'
WHEN 3 THEN 'Third'
WHEN 4 THEN 'Fourth'
ELSE 'Something Else'
END
FROM Sales.SalesOrderHeader;
有些读者可能不清楚,%运算符用于取模。取模运算与除运算类似,不过它只返回余数。因此,16%4=0(16可被4除尽),但16%5=1(16除以5还余1)。在这个示例中,由于除以10,因此取模运算返回了所计算数字的最后一位。 结果如下所示:
注意,无论何时列表中有匹配值,都会调用THEN子句。由于有一个ELSE子句,与前面的值都不匹配的值将被赋予ELSE子句中的值。如果省略ELSE子句,那么相对应的值将为NULL。
再看一个详述表达式的用法的示例。这一次将使用查询中的另一列:
USE AdventureWorks;
GO
SELECT TOP 10 SalesOrderID % 10 AS 'OrderLastDigit',
ProductID % 10AS 'ProductLastDigit',
"HowClose?" = CASE SalesOrderID % 10
WHENProductID % 1 THEN 'Exact Match!'
WHENProductID % 1 - 1 THEN 'Within 1'
WHENProductID % 1 + 1 THEN 'Within 1'
ELSE 'MoreThan One Apart'
END
FROM Sales.SalesOrderDetail
ORDER BY SalesOrderID DESC;
注意,这里的每一步都使用了等式,但它仍然正常工作:
只要表达式的计算结果为与输入表达式的类型兼容的特定值,就可以分析表达式并应用适当的THEN子句。
2.搜索CASE语句
搜索CASE语句的工作方式与简单CASE语句很接近,不过有如下两处不同:
• 没有输入表达式(即CASE关键字和第一个WHEN关键字之间的部分)。
• WHEN表达式必须求值为一个布尔值(而在刚才看到的简单CASE示例中,使用了像1、3和ProductlD +1这样的值)。
搜索CASE语句最棒的地方是可以完全更改构成表达式基础的内容——根据可能的不同情形,混合搭配列表达式。
同样,理解其工作方式的最好途径是运用示例:
SELECT TOP 10 SalesOrderID % 10 AS 'OrderLastDigit',
ProductID % 10AS 'ProductLastDigit',
"HowClose?" = CASE
WHEN(SalesOrderID % 10) < 3 THEN 'Ends With Less Than Three'
WHENProductID = 6 THEN 'ProductID is 6'
WHENABS(SalesOrderID % 10 - ProductID) <= 1 THEN 'Within 1'
ELSE 'MoreThan One Apart'
END
FROM Sales.SalesOrderDetail
ORDER BY SalesOrderID DESC;
这与简单CASE语句有很大不同,但它仍然可以运行:
有关SQLServer如何求值有以下一些注意点:
• 即使两个条件求值为TRUE,也只使用第一个条件。例如,倒数第二行满足第1个条件 (最后一位小于3)和第3个条件(最后一位与ProductID之差小于等于1)。对于许多语言来说,包括Visual Basic,这种语句总是这样工作的。然而,如果使用C语言(或其他类似语言),那么在编写代码时要记住这一点;不需要“中断”的语句——它总是在一个条件满足后终止。
• 可以在条件表达式中混合搭配使用的字段。在这个示例中,使用了SalesOrderlD和 ProductID,并对两者作了混合使用。
• 可以执行任何表达式,只要最后它被求值为布尔结果。
现在看一个稍复杂的示例。在这个示例中,并不准备进行混合搭配——而是只采用一列(可以改变测试的列,但大部分时候并不需要)。下面将处理一个更现实的场景,用于更大型的电子商务站点。
这个场景是这样的:营销人员喜欢净价。他们讨厌在成本上加10%的价格,使得价格为 $10.13或$23.19。他们喜欢以49、75、95或99这样的数字结尾的灵活价格。在这里,假定创建一个新的价格列表用于分析,并希望它满足特定条件。
对于零头小于50美分的新价格(例如前面的$10.13),营销人员希望保持价格的美元数不变,但使零头增加为49美分(即$10.49)。将零头为50〜75美分的价格改为零头为75美分,零头大于75美分的价格改为零头为95美分。具体所希望的情况如表11-3所示。
表11-3营销部门喜欢的价格处理
如果新价格为 |
那么应改为 |
$10.13 |
$10.49 |
$17.57 |
$17.75 |
$27.75 |
$27.75 |
$79.99 |
$79.95 |
从技术上讲,这可以使用嵌套的IF...ELSE语句来实现,但它有下列缺点:
• 可读性差——特别是如果规则较复杂的话。
• 必须使用游标实现代码,并且一次检查一行。
总之是比较麻烦。
而CASE语句使这一过程变得非常简单。而且,可以使条件与查询内联,将它作为SET操作的一部分——这总是意味着将获得比使用游标更好的性能。
营销部门希望看到将价格提升10%后的结果,因此将10%的加价结合到CASE语句中,同时稍作一些额外的分析,就可以得到所要的数字:
USE AdventureWorks;
GO
/* I'm setting up some holding variables here. This way,if you get asked
** to run the query again with a slightly differentvalue, you'll only have
** to change it in one place.
*/
DECLARE @Markup money;
DECLARE @Multiplier money;
SELECT @Markup = .10; -- Change the markup here
SELECT @Multiplier = @Markup + 1; -- We want the end price, not the amount
-- of the increase, soadd 1
/* Now execute things for our results. Note that you'relimiting things
** to the top 10 items for brevity -- in reality, youeither wouldn't do this
** at all, or you would have a more complex WHERE clauseto limit the
** increase to a particular set of products
*/
SELECT TOP 10 ProductID, Name, ListPrice,
ListPrice *@Multiplier AS "Marked Up Price", "New Price" =
CASE WHENFLOOR(ListPrice * @Multiplier + .24)
>FLOOR(ListPrice * @Multiplier)
THEN FLOOR(ListPrice * @Multiplier) + .95
WHENFLOOR(ListPrice * @Multiplier + .5) >
FLOOR(ListPrice * @Multiplier)
THEN FLOOR(ListPrice * @Multiplier) + .75
ELSEFLOOR(ListPrice * @Multiplier) + .49
END
FROM Production.Product
WHERE ProductID % 10 = 0 -- this is just to help the example
ORDER BY ProductID DESC;
FLOOR函数是比较简单的——它获取提供的值并向下舍入为最接近的整数。
当听到某人说出“分析”时,我感到非常怀疑,特别是这是从营销或销售人员口中说出的。不要误会,其实这些人和我一样,也是在做本职工作。问题是,他们如果提出了某个要求,下次可能还会提出一个类似的要求。因此,我准备预先构建一个脚本——这样当他们要求将价格提升15%时,只需要改变@Markup的初始值。看一下提价10%的结果,如表11-4所示:
表11-4提价10%的结果
ProductID |
Name |
ListPrice |
Marked Up Price |
New Price |
990 |
Mountain-500 Black, 42 |
539.99 |
593.989 |
593.95 |
980 |
Mountain-400-W Silver, 38 |
769.49 |
846.439 |
846.49 |
970 |
Touring-2000 Blue, 46 |
1214.85 |
1336.335 |
1336.49 |
960 |
Touring-3000 Blue, 62 |
742.35 |
816.585 |
816.75 |
950 |
ML Crankset |
256.49 |
282.139 |
282.49 |
940 |
HL Road Pedal |
80.99 |
89.089 |
89.49 |
930 |
HL Mountain Tire |
35 |
38.5 |
38.75 |
920 |
LL Mountain Frame - Silver, 52 |
264.05 |
290,455 |
290.49 |
910 |
HL Mountain Seat/Saddle |
52.64 |
57.904 |
57.94 |
900 |
LL Touring Frame - Yellow, 50 |
333.42 |
366.762 |
366.95 |
仔细查看一下,就可发现结果正是所希望的。而且,这里不必构建游标。
11.5.3用WHILE语句进行循环
WHILE语句的工作方式与其他语言中的类似,基本上就是每次到达循环顶部时测试一个条件。如果条件仍为TRUE,那么再次执行循环,否则就退出。
其语法如下所示:
The syntax looks like this:
WHILE
[BEGIN
[BREAK]
[CONTINUE]
END]
尽管可以只执行一条语句(就像IF语句一样),但是对于完整的语句块,几乎所有的WHILE关键字之后都会跟有一个BEGIN...END语句块。
BREAK语句用于退出循环,而不需要等待到达循环底部和表达式被重新求值。
是否使用BREAK语句
使用BREAK语句通常是较差的做法。本书对这一点保持中立态度。不过尽可能避免使用它。在大部分情况下,可以通过移动一两条语句来避免它,而结果是一样的。其优点通常是获得更具可读性的代码。如果只有一个进入/退出点,那么这更易于处理循环结构(或就此而言的任何结构)。而使用BREAK违反了这一理念。
虽说如此,有时可以重新格式化代码来避免BREAK语句,但这只会使事情变得更糟。另外,我曾看到有人为了避免使用BREAK语句,导致所编写出来的代码运行速度更慢。
CONTINUE语句的作用与BREAK语句完全相反。简言之,它告诉WHILE循环回到起点。不管是在循环中的哪个位置,都会立即回到顶部,重新计算表达式(如果表达式不再为TRUE,就退出)。
下面将用一个简短的示例来说明。如前所述,在无游标的情况下,WHILE循环是很少见的。不过这里的示例只是为了演示。
这里准备使用WHILE循环和WAITFOR命令创建一个监控进程(11.5.4节将介绍WAITFOR命令)。该进程用于每天一次自动更新统计信息:
WHILE 1 = 1
BEGIN
WAITFOR TIME'01:00';
EXECsp_updatestats;
RAISERROR('Statistics Updated for Database', 1, 1) WITH LOG;
END
这将在每天凌晨1点更新数据库中的每个数据表的统计信息,并将日志项写入SQLServer日志和Windows应用程序日志。如果想查看该进程是否工作,则可以使其整夜运行并在早上检查日志。
注意:这样的无限循环的工作方式和调度任务不同。如果希望每天运行某项操作,可以使用Management Studio建立SQL Agent作业。除了不需要一直打开连接(前面的示例会做这个工作)外,还可以根据脚本成功与否执行后续动作。另外,还可以使用电子邮件和net-send命令发送有关完成状态的消息。
11.5.4 WAITFOR语句
很多时候,您并不希望或无法使某操作正好在某个时间点发生,同时也不希望操作被挂起,直到适当的时间再执行该操作。
这一点很容易实现——可以使用WAITFOR语句并使SQL Server等待。语法非常简单:
WAITFOR
DELAY<'time'> | TIME <'time'>
WAITFOR语句所做的就是等待参数指定的操作发生。可以为某操作的发生指定明确的时间,或是指定等待一段时间后执行该操作。
1. DELAY 参数
DELAY参数指定了等待的时间段。不能指定天数——只能指定小时数、分钟数和秒数。允许延迟的最长时间为24小时。因此,例如下列语句:
WAITFORDELAY '01:00';
将运行WAITFOR语句前的任何代码,然后到达WAITFOR语句,停止一个小时,之后继续执行下一条语句中的代码。
2. TIME参数
TIME参数指定到达指定时间的等待时间。这也不能指定日期——只能是24小时制的某个时间。同样,可延迟的最长时间也是一天。例如下列代码:
WAITFORTIME '01:00';
将运行WAITFOR语句前的任何代码,然后到达WAITFOR语句,直到凌晨1点停止执行,之后执行WAITFOR语句后的下一条语句。
11.5.5使用TRY/CATCH块处理错误
在以前(指SQL Server2005之前),错误处理的选项是相当有限的。用户可以检查错误条件,但必须主动执行该操作。有时,可能会有一些导致退出存储过程或脚本的错误,而根本无法捕获错误(现在这种情况仍会发生,但有一些选项用于降低错误的几率)。这里将把有关错误处理的更详细的讨论放到第12章有关存储过程的讨论中,而介绍新的TRY/CATCH块的基本原理。
在SQLServer中,TRY/CATCH块的工作方式与那些派生自C的语言(C、C++、C#、Delphi 及其他语言)中的TRY/CATCH块相似。其语法如下所示:
BEGINTRY
{
ENDTRY
BEGINCATCH
{
ENDCATCH [;]
简而言之,SQLServer将“尝试”运行BEGINTRY...END TRY块中的任何语句。当且仅当错误级别为11〜19的错误条件发生时,SQL Server才会立即退出TRY块并转到CATCH块中的第一行。因为错误级别不只是11〜19,还有更多可能的错误级别,如表11-5所示。
表11-5SQL错误级别
错误级别 |
性 质 |
描 述 |
1〜10 |
只是信息错误 |
这包括了上下文更改,如调整了设置或在进行聚合运算时发现NULL值。这些不会触发CATCH块,因此如果需要测试这个级别的错误,需要选用@@ERROR来手动实现 |
11 〜19 |
相对严重的错误 |
这些是可由代码处理的错误(例如违反外键)。有些错误比较严重,你可能不希望继续处理(例如超出内存错误),但至少可以捕获它们并适当退出 |
20 〜25 |
非常严重 |
这些通常是系统级错误。服务器端代码从来不知道此类错误发生,因为脚本和连接将立即终止 |
记住,如果需要处理超出11~19级范围的错误,那么需要作其他规划。
现在,为了进行测试,对之前介绍IF...ELSE语句时构建的CREATE脚本进行修改。您可能记得,最初测试表是否已存在的部分原因是避免创建一个导致脚本失败的错误条件。这种测试是以前的做法(其他选项也没有更多的内容)。通过采用TRY/CATCH块,只要尝试CREATE操作,如果有错误就进行处理。
BEGIN TRY
-- Try andcreate our table
CREATE TABLEMyIFTest(
Col1 int PRIMARY KEY
);
END TRY
BEGIN CATCH
-- Uh oh,something went wrong, see if it's something
-- we know whatto do with
DECLARE@ErrorNo int,
@Severity tinyint,
@State smallint,
@LineNo int,
@Message nvarchar(4000);
SELECT
@ErrorNo =ERROR_NUMBER(),
@Severity =ERROR_SEVERITY(),
@State =ERROR_STATE(),
@LineNo =ERROR_LINE (),
@Message =ERROR_MESSAGE();
IF @ErrorNo =2714 -- Object exists error, we knew this might happen
PRINT'WARNING: Skipping CREATE as table already exists';
ELSE -- hmm, wedon't recognize it, so report it and bail
BEGIN
PRINT@Message+' ErrorNo: '+CONVERT(NVARCHAR(5),@ErrorNo)
+ 'Severity: '+CONVERT(NVARCHAR(5),@Severity);
RAISERROR(@Message, 16, 1 );
END
END CATCH
注意,这里使用了一些特殊的函数来检索错误条件,如表11-6所示。
注意:本书将它们移到局部变量中,这样就不会丢失。
表11-6检索错误条件的系统函数
函 数 |
返 回 值 |
ERROR_NUMBER() |
实际错误号。如果这是一个系统错误,那么在sysmessages表(使用sys.messages查看)中会有一项与该错误匹配,并且包含将从其他错误相关函数中获取的信息 |
ERROR SEVERITY。 |
等同于本书和联机丛书中提到的“错误级别” |
ERROR_STATE() |
这个函数用作位置标记。对于系统错误来说,其值总为1。在第12 章深入讨论错误处理时,您将看到如何引发自定义错误。在那时,可用状态表明错误发生在存储过程、函数或触发器的哪个位置(这有助于在多个位置的其中任意一个位置中处理指定的错误) |
ERROR_PROCEDURE() |
在前面的示例中未使用该函数,因为它只与存储过程、函数和触发器相关。这提供了导致错误的存储过程的名称——如果存储过程是嵌套的,使用它就很方便,因为导致错误的存储过程可能不是实际处理该错误的存储过程 |
ERROR LINE() |
错误所在的行号 |
ERROR_MESSAGE() |
消息文本。对于系统消息,这与从sys.messages函数选择消息时所看到的一样。对于用户自定义错误,这是提供给RAISERROR函数的文本 |
这里的示例利用了一个已知的错误ID,如果试图创建已存在的对象,SQL Server就会引发该错误。通过从sys.messages表函数中选择,可以看到所有系统错误消息。
注意:从SQLServer 2005开始,sys.messages输出变得很长,以至于很难通过扫描发现正在查找的消息。此处所采用的解决方案并不完美,但是很有效——人为创建要查找的错误,然后查看系统提供的错误号(简单问题简单解决)。
本节只是执行想执行的代码(这里是CREATE语句)并处理出现的错误——就是这样。
第12章将更详细地介绍错误处理。同时,可使用TRY/CATCH为脚本提供基本的错误 处理。
11.6本章小结
理解脚本和批处理是理解SQL Server编程的基础。而脚本和批处理的概念是实现多种功能的基础,包括编写构建全部数据库的脚本、编写存储过程和触发器等。
局部变量的作用域只限于一个批处理。即使已在同一脚本内声明了变量,如果在一个新的批处理中引用它之前不重新声明该变量(并且在开始时赋值),那么将仍然会得到错误消息。
有许多系统函数可用。这里只是提供了一个最有用的系统函数的列表,但实际还有很多这样的函数。对于比较晦涩难懂的函数,可以查看联机丛书或本书后面的附录A。系统函数不需要声明,并且总是可用的。有些函数的作用域是整个服务器,而有些只返回当前连接的特定值。
可以使用批处理在脚本的不同部分创建优先权。第一个批处理从脚本的起点处开始,结束于脚本的末端或者第一条GO语句——无论哪一种情况先出现。下一个批处理(如果有的话)从第一个批处理结束后的那一行开始,执行到脚本的末端或者下一条GO语句——同样无论哪一种情况先出现。这个过程继续到脚本的末端。来自脚本顶部的第一个批处理最先执行,第二个批处理则随后执行,以此类推。每个批处理内的所有命令必须通过查询分析器的验证,否则系统不会执行该批处理中的任何命令;然而,任何其他的批处理将单独地分析,并且将仍然被执行(如果它们通过分析器的验证)。
另外,本章还介绍了如何动态地创建和执行SQL。这可用于处理不能完全预测的场景,或是构造语句所需的内容实际上是一块数据这样的情况。
最后介绍了SQL Server提供的控制流结构。通过混合使用这些结构,可以有条件地执行代码、创建循环或是提供某种字符串替换。
在下面几章中,将深化脚本和批处理的概念,把它们应用到存储过程、用户自定义函数和触发器中——即SQLServer中最接近于实际程序的内容。
练习题
1.编写一个简单脚本,创建两个整型变量(一个名为Var1,另一个名为Var2),将它们的值分别设置为2和4,然后输出这两个变量的和。
2.创建一个名为MinOrder的变量,并且用AdventureWorks中CustomerlD 1的打折后的最小行项总额填充该变量(注意:这里在处理货币,所以不要想当然地使用int类型)。输出MinOrder的最终值。
3.使用sqlcmd将查询SELECT COUNT(*) FROM Customers的结果输出到控制台窗口。
本章内容总结
主 题 |
概 念 |
脚本和批处理 |
脚本是完全由T-SQL语句组成的存储文件。批处理是一系列T-SQL命令,这些命令作为一个单元运行。单个脚本可以包含一个或多个批处理 |
USE语句 |
在批处理的开始位置使用USE语句为下面的语句选择数据库上下文 |
变量 |
使用@标记指定变量,而使用DECLARE @variable_name后跟数据类型来声明变量。变量的作用域是T-SQL代码块。可以使用SET或SELECT设置变量的值
|
检索标识值 |
当插入获得标识值的一行时,可以使用 SCOPE_IDENTITY()检索该值 |
序列 |
序列返回递增或循环的一系列数值,可用于替换标识列以及实现其他功能 |
GO语句 |
GO语句不是T-SQL的一部分,SSMS将其识别为批处理分隔符 |
EXEC命令 |
使用EXEC命令,可以将T-SQL语句汇编为纯文本,并且使系统执行它们。这是非常危险的行为,应该谨慎对待 |
IF...ELSE 和 WHILE 语句 |
T-SQL具有与其他编程语言类似的循环结构 |
CASE语句 |
从许多选项中选择第一个匹配的子句。可以在单个T-SQL语句中内联使用 |
TRY...CATCH 块 |
TRY块将检测特定严重性范围内的错误,它不会将错误抛出给调用程序,而是将控制权移交给CATCH块,从而使您可以执行错误处理 |