(《SQL Server 2005 编程入门经典》 第11章)
11.1 脚本基础
脚本示例:
USE Northwind DECLARE @Ident INT INSERT INTO Orders (CustomerID, OrderDate) VALUES (‘ALFKI’, DATEADD(day, -1, GETDATE())) SELECT @Ident = @@IDENTITY INSERT INTO OrderDetails (OrderID, ProductID, UnitPrice, Quantity) VALUES (@Ident, 1, 50, 25) SELECT ‘The OrderID of the INSERTed row is ’ + CONVERT(VARCHAR(8), @Ident)
11.1.1 USE语句
USE语句用于设置当前数据库。USE语句会影响在完全限定对象名的数据库部分使用默认值的任何地方。
11.1.2 声明变量
DECLARE语句具有相当简单的语法:
DECLARE @<variable name> <variable type> [, …]
可以一次仅仅声明一个变量,也可以一次声明几个变量。变量开始的值将总是为NULL,直到显示地将变量设置为一些其他的值。
在这个案例中,已经将一个名为@Ident的局部变量声明为整形。@@IDENTITY是一个系统函数,返回在当前连接中分配的最近标识值。对于大多数系统函数来说,应当形成一个习惯,即显示地将@@IDENTITY中的值移动到一个局部变量中。
1. 为你的变量设置值
目前在变量中设置值的方法有两种。可以使用SELECT语句或者SET语句。从功能上看,它们的作用几乎是相同的,不同的是SELECT语句具有使得源值来自SELECT语句中的某一列的能力。
使用SET设置变量
SET通常用于以更加程序化的语言中所使用的方式来设置变量。经典的使用示例是:
SET @TotalCost = 10 SET @TotalCost = @UnitCost * 1.1
使用SET,不能将查询得到的值赋给变量——必须将查询和SET分开。例如:
USE Northwind DECLARE @Test MONEY SET @Test = (SELECT MAX(UnitPrice) FROM OrderDetails) SELECT @Test
注意:
尽管这个语法可以起作用,但习惯上,从来不采用这种方法实现代码。
使用SELECT设置变量
当变量中存储的信息来源于查询的时候,经常用SELECT给变量赋值。例如,上面最后的示例中使用SELECT是更加常用的做法:
USE Northwind DECLARE @Test MONEY SELECT @Test = MAX(UnitPrice) FROM OrderDetails SELECT @Test
关于如何使用哪一种方法的约定如下所示:
- 当执行一个简单的变量赋值时,使用SET——这时值已经以一个显示的值或者其他变量的形式表示。
- 当基于查询进行变量赋值时,使用SELECT。
2. 系统函数概述
可用的不带参数的系统函数有30多个。其中最值得关心的如下所示:
变量 | 用途 | 注释 |
@@CURSOR_ROWS | 返回在当前连接上打开的最近的指针集中当前行的数目 | SQL 7能异步地填充指针。要知道如果指针仍然在填充过程中,那么变量中的值可能改变。 |
@@DATEFIRST | 返回当前设置的每个星期的第一天(比如星期日或者星期一) | 是一个系统范围的设置——如果有人改变了这个设置,就不能得到所期望的结果。 |
@@ERROR | 返回在当前连接上的最近的T-SQL语句错误的数目。如果没有错误,返回0 | 在每个新的语句下重新设置。如果需要保存这个值,应该立刻把这个值移动到一个局部变量中。 |
@@FETCH_STATUS | 和FETCH语句联合使用 | 对于有效的取回返回0,对于超过指针集和的末端返回%,对于一个丢失的(被删除的)行返回-2。经典的gotcha假定任何非零值意味着在指针的末端——一个-2可能意味着一个丢失的记录。 |
@@IDENTITY | 返回插入的最近的标识值,作为最近的INSERT或者SELECT INTO语句的结果 | 如果没有标识值产生,那么设置为NULL。即使缺少标识值是由于一个运行的语句的失败,也是如此。如果通过一个语句执行多个插入,那么只返回最后的标识值。 |
@@OPTIONS | 返回通过使用SET命令设置的选项的信息 | 因为只得到一个值,但是可以有许多选项集合,SQL Server使用二进制标记以指示设置了什么值。为了检验是否设置了感兴趣的选项,必须将这个选项值和一个位运算符一起使用。 |
@@REMSERVER | 仅仅在存储过程中使用。返回称为存储过程的服务器的数值 | 在希望sproc根据远程服务器不同表现出不同的行为时,这个选项是很方便的。 |
@@ROWCOUNT | 一个最经常使用的系统函数。返回最近的语句所影响的行的数目。 | 一般在非运行时错误检查时使用。例如,如果尝试通过使用一个WHERE字句删除一行,并且没有行被影响,那么那将意味着一些不期望的事情发生了。 |
@@SERVERNAME | 返回脚本正在运行的局部服务器的名称 | 可以通过使用sp_addserver然后重新启动SQL Server来改变,但是很少需要改变。 |
@@TRANCOUNT | 返回活动事务的数目——本质上嵌套水平的事务——用于当前的连接 | 除非正在使用存储点,否则ROLLBACK TRAN语句将@@TRANCOUNT减少到0。BEGIN TRAN使得@@TRANCOUNT加1,COMMIT TRAN使得@@TRANCOUNT减1。 |
@@VERSION | 返回当前的SQL Server版本以及日期、处理器和操作系统体系结构 | 遗憾的是,不能将信息返回为任何结构化的字段排列,所以如果需要对特定的信息进行测试的话,必须对它进行语法分析。 |
11.1.3 使用@@IDENTITY
@@IDENTITY是所有的系统函数中最重要的一个。标识列是这样的列,在那里没有提供一个值,而是SQL Server自动地插入一个值。任何的INSERT或者INSERT INTO语句都会更新这个函数的返回值。如果没有新的标识列被插入,将返回NULL。如果插入了多个行,生成了多个标识值,则@@IDENTITY将返回最后生成的标识值。如果语句触发了一个或多个触发器,该触发器又执行了生成标识值的插入操作,那么,在语句执行后立即调用@@IDENTITY将返回触发器生成的最后一个标识值。如果对包含标识列的表执行插入操作后触发了触发器,并且触发器对另一个没有标识列的表执行了插入操作,则@@IDENTITY将返回第一次插入的标识值。出现INSERT或SELECT INTO语句失败或大容量复制失败,或者事务被回滚的情况时,@@IDENTITY值不会恢复为以前的设置。
11.1.4 使用@@ROWCOUNT
@@ROWCOUNT说明上一个SQL语句(SELECT、UPDATE、INSERT和DELETE等)影响了多少行。示例:
USE Northwind GO DECLARE @RowCount INT SELECT * FROM Categories SELECT @RowCount = @@ROWCOUNT PRINT ‘The value of @@ROWCOUNT was ’ + CAST(@RowCount AS VARCHAR(5))
则最后一行显示:
The value of @@ROWCOUNT was 8
11.2 批处理
批处理是进入一个逻辑单元的T-SQL语句组。一个批处理中的所有语句被组合为一个执行计划,因此对所有语句一起进行语法分析,并且必须通过语法验证,否则将没有一个语句会执行。但是,这并不能防止运行时错误的发生。如果发生运行时错误,那么任何在发生运行时错误之前执行的语句将仍然是有效的。简言之,如果一个语句不能通过语法分析,那么不会运行任何语句。如果一个语句在运行时失败,那么产生错误语句之前的所有语句都已经运行了。
可以将一个脚本分开为多个批处理,方法是使用GO语句。GO语句:
- 必须自成一行(只有注释可以在相同的行上)。
- 使得从脚本或者上一个GO语句开始的所有语句编译成一个执行计划并发送到服务器,与任何其他批处理无关。
- 不是T-SQL命令,而是由各种SQL Server命令实用程序识别的命令。
代码示例:
USE AdventureWorks DECLARE @MyVarchar VARCHAR(50) SELECT @MyVarchar = ‘Honey, I’’m home…’ PRINT ‘Done with first batch…’ GO PRINT @MyVarchar PRINT ‘Done with second batch’ GO PRINT ‘Done with third batch’ – Notice that this still gets executed even after the error GO
结果如下所示:
Done with first batch…
Msg 137, Level 15, State 2, Line 2
Must declare the scalar variable “@MyVarchar”
Done with third batch
11.2.1 批处理中的错误
批处理中的错误分成两类:
- 语法错误
- 运行时错误
如果查询分析器发现一个语法错误,那么批处理的处理过程会立即取消。因为语法检查发生在批处理编译或者执行之前,所以在语法检查期间的失败意味着还没有批处理被执行。运行时错误的工作方式则不同。因为任何在遇到运行时错误之前执行的语句已经完成了,所以除非是未提交的事务的一部分,否则这些语句所做的任何事情的影响将保留下来。一般而言,运行时错误将终止从错误发生地方到批处理末端的批处理的执行。
11.2.2 什么时候使用批处理
批处理有几个目的,但是所有的批处理具有一个共同点——在脚本中当一些事情必须发生在另外一件事之前或者分开发生时,使用批处理。
1. 要求有自己的批处理的语句
有一些命令必须完全是它们自己的批处理的一部分。这些命令包括:
- CREATE DEFAULT
- CREATE PROCEDUER
- CREATE RULE
- CREATE TRIGGER
- CREATE VIEW
如果你想在一个脚本中将这些语句中的任意一些和其他的语句进行组合,那么需要通过使用GO语句将它们分开为各自的批处理。
注意:
注意,如果DROP一个对象,那么应该将DROP语句放在它自己的批处理中或者至少和其他DROP语句在一个批处理中。
2. 使用批处理建立优先权
使用批处理语句的最可能如果在下一个任务开始之前,需要全部完成上一个任务。例如,在尝试使用新数据库时,需要先完成CREATE DATABASE语句:
CREATE DATABASE Test GO USE Test CREATE TABLE TestTable ( col1 INT, col2 INT )
另外,当使用ALTER TABLE语句显著地修改一个列的类型或者添加列时,直到执行修改任务的批处理已经完成时,才能利用这些变化。
USE Test ALTER TABLE TestTable ADD col3 INT GO INSERT INTO TestTable(col1, col2, col3) VALUES (1, 1, 1)
11.3 SQLCMD
SQLCMD是一个实用程序,允许你在Windows命令框中运行来自命令提示符的脚本。
11.4 动态SQL:以EXEC命令即时产生代码
SQL Server允许使用一些gotcha,以通过使用字符串操作即时构建SQL语句。需要这么做的原因是:往往是直到运行时,才能知道一些内容的细节。语法如下所示:
EXEC ({string variable}|’<literal command string>’)
或者:
EXECUTE ({string variable}|’<literal command string>’)
示例:
USE Northwind GO -- Create The Table. We’ll pull info form here for our dynamic SQL CREATE TABLE DynamicSQLExample ( TableID INT IDENTITY NOT NULL CONSTRAINT PKDynamicSQLExample PRIMARY KEY, TableName VARCHAR(128) NOT NULL ) GO -- Populate the table. In this case, we’re grabbing every user table -- object in this database INSERT INTO DynamicSQLExample SELECT TABLE_NAME FROM Information_Schema.Tables WHERE TABLE_TYPE = ’Base TABLE’ GO DECLARE @TableName VARCHAR(128) SElECT @TableName = TableName FROM DynamicSQLExample WHERE TableID = 14 -- Finally, pass that value into the EXEC statement EXEC (‘SELECT * FROM ’ + @TableName)
EXEC的Gotcha
使用EXEC不是没有试验和考验的。EXEC的gotcha(陷阱,错误)有:
- EXEC和它调用的代码在分开的作用域下运行——也就是说,调用代码不能引用EXEC语句中的变量,并且在调用代码中的变量被分解为用于EXEC语句的字符串之后,EXEC不能引用这些变量。
- EXEC与当前的用户运行在相同的安全语境下——而不是调用对象的安全语境下。
- EXEC与调用对象运行在相同的连接和事务环境下。
- 要求一个函数调用的串联在EXEC字符串上必须先于实际调用EXEC语句执行——在EXEC调用时,不能在相同的语句中做函数的串联。
- EXEC不能在用户自定义函数内使用。
1. EXEC的作用域
调用EXEC语句的实际语句行与批处理的其他部分或者EXEC语句所运行的过程具有相同的作用域,但是作为EXEC语句的结果来执行的代码被认为是处在其自身的批处理中。代码示例:
USE Northwind DECLARE @InVar VARCHAR(50) DECLARE @OutVar VARCHAR(50) SET @InVar = ‘SELECT @OutVar = FirstName FROM Employees WHERE EmployeeID = 1’ EXEC (@InVar) EXEC (‘SELECT @OutVar = FirstName FROM Employees WHERE EmployeeID = 1’) SELECT @OutVar
输出:
Msg 137, Level 15, State 1, Line 7
Must declare the scalar variable ‘@OutVar’.
Msg 137, Level 15, State 1, Line 8
Must declare the scalar variable ‘@OutVar’.
---------------------------------------------------------
NULL
(1 row(s) affected)
另一端示例:
USE Northwind -- This time, we only need one variable. It does need to be longer though. DECLARE @InVar VARCHAR(200) /* ** Set up our string to feed into the EXEC command. This time we’re going ** to feed it several statements at a time. They will all execute as one ** batch */ SET @InVar = ‘DECLARE @OutVar VARCHAR(50) SELECT @OutVar = FirtsName FROM Employees WHERE EmployeeID = 1 SELECT ‘The value is ’ + @OutVar -- Now run it EXEC (@InVar)
这次返回更接近于所期望的结果:
The value is Nancy
注意:
临时表的存活期只能和EXEC过程一样长,这种行为特性将在讲述触发器和存储过程的时候继续讨论。
2. 该规则的例外情况
在EXEC的作用域内发生了一件事情,这可以在执行EXEC之后看出来——系统函数——所以,像@@ROWCOUNT之类的事物仍然可以使用。
3. 安全上下文和EXEC
当你给某个人运行存储过程的权限时,你暗示他们获得了执行在sproc内调用的操作的权限。例如,假设有一个存储过程,用于列出在去年雇佣的所有的雇员。有权限执行sproc的人就可以这么做,即使他们没有直接访问Employees表的权限。
但是,何在EXEC语句中所做的引用都将运行在当前用户的安全上下文下。所以,假设我具有运行名为spNewEmployees过程的权限,但是我没有访问Employees表的权限,那么一切工作正常。但是,如果spNewEmployees使用EXEC语句执行SELECT语句,那么因为我没有在Employees表上运行SELECT语句的权限,所以EXEC语句将失败。
4. 在串联和EXEC中函数的使用
你不能将一个函数作为EXEC的字符串参数来运行。例如:
USE Northwind -- This’s won’t work DECLARE @NumberOfLetters INT SET @NumberOfLetters = 15 EXEC (‘SELECT LEFT(CompanyName, ’ + CAST(@NumberOfLetters AS VARCHAR) + ‘)AS ShortName FROM Customers’)
因为CAST函数需要在EXEC行之前被完全地解析,所以第一个示例给了一个错误信息:
Msg 102, Level 15, State 1, Line 6
Incorrect syntax near ‘CAST’
但是可以这么做:
USE Northwind -- This’s won’t work DECLARE @NumberOfLetters INT SET @NumberOfLetters = 15 DECLARE @Str AS VARCHAR(255) SET @Str = ‘SELECT LEFT(CompanyName, ’ + CAST(@NumberOfLetters AS VARCHAR)+ ‘)AS ShortName FROM Customers’ EXEC (@str)
因为@Str已经是一个完整字符串,所以工作得很好。
5. EXEC和UDF
不允许使用EXEC以在UDF中运行动态的SQL。然而,在一些情况下使用EXEC以运行一个存储过程是合法的。