INSERT INTO T1 VALUE(3)返回连接1,再次运行脚本1。如果检查表ErrorLog内容,会看到一个主键违规错误已被记录。转到连接2,运行下面命令以删除刚插入的行: DELETE FROM T1 WHERE col1 = 3要测试锁超时错误,在连接2中运行下面代码: BEGIN TRAN UPDATE T1 SET col1 = 1返回连接1,再次运行脚本1。大约30秒后,你会得到一个错误。检查ErrorLog内容,会发现一个锁超时错误已被记录。转到连接2,执行ROLLBACK命令回滚事务。 要测试死锁,转到连接2,粘贴下面代码,但还不要运行它: DECLARE @i AS INT BEGIN TRAN SET @i = 1 WHILE @i <= 2 BEGIN UPDATE T2 SET col1 = 2 WAITFOR DELAY ''''00:00:10'''' SELECT * FROM T1 WAITFOR DELAY ''''00:00:05'''' SET @i = @i + 1 END ROLLBACK转到连接1,运行脚本1代码,然后立即在连接2中运行代码。约一分钟后,你会看到连接1发生错误。检查ErrorLog内容,注意到表中有死锁错误后两个重试尝试和第三个未导致错误的成功尝试。查询ErrorLog表,检查它的内容。 最后,如果你想在TRY块内抛出自己的事务异常错误,你可调用带TRAN_ABOAT选项的RAISERROR命令。 SNAPSHOT隔离 Yukon引入一名称为SNAPSHOT的新隔离级,它允许你以一种模式工作,在这种模式下,倘若为读取者提供需要的数据一个已提交版本,则写入者不会阻塞读取者。SQL Server Yukon在表tempdb中维护一个链接队列,它们跟踪行改变并为读取者构造一个稍老的已提交版本数据。当UPDATE冲突不是常见时,这种隔离有益于乐观锁定。如果进程1获取数据,后来试图修改它;如果进程2在进程1取数据和修改数据之间已修改同样的数据,那么SQL Server会产生一个关于进程1试图修改的错误,错误是因为冲突产生的。然后,进程1可设法重新执行事务。这种模式在update冲突不常见情景下是有效率的。 为了允许工作在SNAPSHOT隔离级,你必须开启数据库选项ALLOW_SNAPSHOT_ISOLATION, 这你将很快看到。要模拟一个写入者不阻塞读取者的场景,首先创建testdb数据库,开启适当的数据库选项,并用下面代码创建T1表同时设置datacol为“Version1”: CREATE DATABASE testdb GO USE testdb ALTER DATABASE testdb SET ALLOW_SNAPSHOT_ISOLATION ON CREATE TABLE T1 ( keycol INT NOT NULL PRIMARY KEY, datacol VARCHAR(10) NOT NULL ) INSERT INTO T1 VALUES(1, ''''Version1'''')在连接1中执行下列代码,它开启一个事务并改变datacol值为“Version2”: USE testdb BEGIN TRAN UPDATE T1 SET datacol = ''''Version2'''' WHERE keycol = 1 SELECT * FROM T1转到连接2,运行下列代码,它设置会话隔离级为SNAPSHOT并取回T1的内容: USE testdb SET TRANSACTION ISOLATION LEVEL SNAPSHOT SELECT * FROM T1注意你取回的是“Version1”,即使连接2已将它改变为“Version2”(但是还没有提交改变)。 现在转到连接1,提交事务并用COMMIT命令关闭所有连接。要尝试乐观锁定,打开两个新连接,在连接1中运行下列代码,它设置会话隔离级为SNAPSHOT,开启一个事务,然后从T1获取数据: SET TRANSACTION ISOLATION LEVEL SNAPSHOT BEGIN TRAN SELECT * FROM T1转到连接2,执行UPDATE: UPDATE T1 SET datacol = ''''Version3'''' WHERE keycol = 1返回连接1,设法update你以前获取的同样数据,这些数据已在连接2中被修改: UPDATE T1 SET datacol = ''''Version4'''' WHERE keycol = 1你应该得到一个错误,它通知你SQL Server不能使用SNAPSHOT隔离级去存取数据库testdb的表T1和应重试你的事务。 WAITFOR命令 Yukon中WAITFOR命令在数个方面被加强。除了等待指定间隔或等到某个时间点,现在还可请求等待至少影响一行的T-SQL语句。可指定此命令等待下列语句中的一种:SELECT、INSERT、UPDATE、DELETE或RECEIVE。前四种是不言自明的;RECEIVE指从队列收到一个消息。如果要在指定的毫秒数之后停止等待,可随意指定超时时间。WAITFOR命令语法是: WAITFOR(Yukon中另一增强允许返回从数据操纵语言(INSERT,UPDATE,DELETE)而不只是SELECT语句的输出。新的OUTPUT子句允许请求列的旧或新数据通过引用INSERTED和DELETED表被返回,类似在触发器中引用它们。你甚至能指定INTO子句并定向其输出到一个表变量。另一增强是允许在修改语句里指定READPAST提示,这样就可跳过锁定行。 使用上述增强的一个例子是,数个进程等待DELETE语句从一个表中至少删除一行,然后定向其输出到表变量中,同时每个进程并行处理数据的不同部分。要证明这点,创建下表MsgQueue: USE tempdb CREATE TABLE MsgQueue ( msgid INT NOT NULL IDENTITY PRIMARY KEY, msgdata VARCHAR(15) NOT NULL )打开一个或多个连接,在每个连接中运行下列代码以定时插入新消息到表中: SET NOCOUNT ON USE tempdb WHILE 1 = 1 BEGIN INSERT INTO MsgQueue VALUES(''''Msg'''' + CAST(CAST(RAND()*1000000000 AS INT) AS VARCHAR(10))) WAITFOR DELAY ''''00:00:01'''' END接着需打开数个其他新连接,并在每个连接中运行Figure 4中代码以模拟对新到达消息的处理。 BULK行集提供者(BULK Rowset Provider) Yukon引入了新BULK行集提供者,你能在OPENROWSET函数中使用它以有效地通过关系方式存取文件。可类似于使用BULK INSERT语句方式使用此行集提供者,但不必发送其输出到表中。然而,你确实必须指定一个与使用bcp.exe或BULK INSERT语句相同的格式文件。下列代码说明如何使用格式文件c:\temp\textfile1.fmt来存取文件c:\temp\textfile1.txt,倘若将结果表命令为C,结果列命令为col1、col2和col3: SELECT col1, col2, col3 FROM OPENROWSET(BULK ''''c:\temp\textfile1.txt'''', FORMATFILE = ''''c:\temp\textfile1.fmt'''') AS C(col1, col2, col3)除了FORMATFILE选项,还能在OPENROWSET函数里指定下列选项:CODEPAGE、DATAFILETYPE、FIELDTERMINATOR、FIRSTROW、LASTROW和ROWTERMINATOR。也可以使用INSERT SELECT来有效地载入数据到表中和为装载选项指定表提示: INSERT INTO MyTable WITH (BULK_CHECK_CONSTRAINTS) SELECT col1, col2, col3 FROM OPENROWSET(BULK ''''c:\temp\textfile1.txt'''', FORMATFILE = ''''c:\temp\textfile1.fmt'''') AS C(col1, col2, col3)能指定表提示的其他选项包括:BULK_BATCHSIZE, BULK_FIRE_TRIGGERS, BULK_KEEPIDENTITY, BULK_KEEPNULLS, BULK_KILOBYTES_PER_BATCH, BULK_MAXERRORS,和ROWS_PER_BATCH。 通过使用BULK行集提供者,可以比以前正规的DML语句更容易地载入文件到表中。现在,对于大对象可以不被限制在TEXT、NTEXT和IMAGE数据类型,你还可使用VARCHAR(MAX),NVARCHAR(MAX)和VARBINARY(MAX)数据类型。新MAX选项允许和正规数据类型一样的方式操纵大对象。例如,下列UPDATE语句存储文本文件到表列中,此列被定义为VARCHAR(MAX): UPDATE LOBs SET clob_col = (SELECT clob_data FROM OPENROWSET(BULK ''''c:\temp\textfile1.txt'''', SINGLE_CLOB) AS C(clob_data)) WHERE keycol = 1SINGLE_NCLOB选项通知SQL Server大对象是字符格式的。类似地,SINGLE_CLOB指定大对象为规则字符格式,SINGEL_BLOB为二进制格式。返回列的名称为BulkColumn,然而如上一代码片段所示,可以为它指定自己的列名。 TOP选项 Yukon中T-SQL的TOP选项有两个重大的增强。其一,可以为TOP指定一个表达式参数,此表达式能包含变量或甚至一个自包含查询。其二,可以在修改性DML(INSERT, UPDATE, DELETE)中使用TOP选项。 要指定表达式,必须将它包含在圆括号中。表达式当不使用PERCENT选项时为BIGINT数据类型,当使用PERCENT选项时为范围在0~100的浮点类型。下列代码表明如何使用一个带变量的表达式来返回AdventureWorks数据库SalesOrderHeader表的最先满足请求数目的订单: USE AdventureWorks DECLARE @n AS BIGINT SET @n = 5 SELECT TOP (@n) * FROM SalesOrderHeader AS SOH ORDER BY OrderDate, SalesOrderID排序时同种情况下SalesOrderID优先。类似地,下面例子示范如何使用PERCENT选项返回最先满足请求百分比的订单: DECLARE @p AS FLOAT SET @p = 0.01 SELECT TOP (@p) PERCENT * FROM SalesOrderHeader AS SOH ORDER BY OrderDate, SalesOrderID如果SalesOrderHeader表包含31519行,31519×0.0001结果向上取整为4, 你会得到4行结果。 允许TOP与修改性DML一起使用的主要动机是代替SET ROWCOUNT选项,这个选项SQL Server没有很好优化。SET ROWCOUNT选项通常在批处理中处理大量数据时与修改性语句协同使用,以避免事务日志爆炸性增加并防止个体锁逐步增加到满表锁。要领会如何使用新TOP功能在批处理中删除行,首先拷贝表SalesOrderHeader内容到表MySalesOrderHeader,然后创建OrderData和SalesOrderID列的索引。这些通过运行下列代码实现: SELECT * INTO MySalesOrderHeader FROM SalesOrderHeader CREATE UNIQUE CLUSTERED INDEX idx_uc_OrderDate_SalesOrderID ON MySalesOrderHeader(OrderDate, SalesOrderID)要以批处理量为1000的方式删除所有订单年份比2003年早的行,用下列代码: WHILE 1 = 1 BEGIN DELETE TOP (1000) FROM MySalesOrderHeader WHERE OrderDate < ''''20030101'''' IF @@rowcount < 1000 BREAK ENDSQL Server比使用SET ROWCOUNT更有效优化此代码。现在可以删除MySalesOrderHeader表: DROP TABLE MySalesOrderHeaderAPPLY算子 APPLY是一新的可在查询语句FROM子句中使用的关系算子。它允许对外部表中的每行调用表值(table-valued)函数,并可选择性地将外部表的列作为函数参数。APPLY算子有两种形式:CROSS APPLY和OUTER APPLY。前者不会返回外部表的行,如果表值函数返回空集给它,然而后者返回NULLS行而不是函数的列。要使用APPLY算子,首先创建下列Arrays表,它存储多组以逗号为间隔的值: CREATE TABLE Arrays ( arrid INT NOT NULL IDENTITY PRIMARY KEY, array VARCHAR(7999) NOT NULL ) INSERT INTO Arrays VALUES('''''''') INSERT INTO Arrays VALUES(''''10'''') INSERT INTO Arrays VALUES(''''20,40,30'''') INSERT INTO Arrays VALUES(''''-1,-3,-5'''')接着,创建fn_splitarr表值函数,它接受一队列为参数并返回一个包含个体元素和他们的位置的表(见Figure 5)。要测试函数,运行此代码: SELECT * FROM fn_splitarr(''''20,40,30'''')输出应与下面相似: pos value --- ----- 1 20 2 40 3 30现在,用CROSS ARRAY算子对表Arrays中每行调用函数: SELECT A.arrid, F.* FROM Arrays AS A CROSS APPLY fn_splitarr(array) AS F然后对照下面检查输出: arrid pos value ----- --- ----- 2 1 10 3 1 20 3 2 40 3 3 30 4 1 -1 4 2 -3 4 3 -5注意,表Arrays中arrid为1的行没有返回,因为函数对其返回空集。如果要返回表Arrays中所有行,且不管函数是否为它们返回行,使用OUTER APPLY。 支持APPLY算子的额外好处是能引用表值函数和在子查询中指定外部表的列为参数。例如,下列代码返回表Arrays中元素个数和小于和等于10的所有记录。 SELECT * FROM Arrays WHERE (SELECT SUM(value) FROM fn_splitarr(array)) <= 10通用表表达式(Common Table Expressions )和递归查询 通用表表达式(CTEs)允许写命名表表达式,它仅仅在查询中存活。以简单的形式,他们能提供混合功能的视和导出表。譬如视,CTE在外部查询里可被多次引用;譬如导出表,它仅在查询期存活。以较复杂的形式,可以写递归CTEs,它让操纵树和图更容易和更有效。 你可以用WITH子句定义CTE,WITH后跟随CTE名称;并可选择性地提供一列置于圆括号内的结果列别名。后面跟随AS子句和圆括号,圆括号中包含CTE查询表达式。最后,提供一个引用CTE结果的外部查询。在CTE查询表达式里,只要你喜欢你就能引用变量。 Figure 6中代码显示一个简单例子,写的是非递归CTE,它返回每天顾客的销售定单额。显然,不使用CTE也可以达到同样结果,但是假使对于每行都要返回上年总额和与本年度的差额。如果选用导出表,则必须在一导出表中指定本年度的查询,在另一导出表中指定上一年度的查询,然后用外部查询连接两者。而用CTE,只需写单个查询,就可返回每年总额,然后用外部查询两次引用它。(见Figure 7) 然而,CTE真正的威力在于其递归形式。你可在CTE圆括号里定义独立或反引用到此CTE的查询。独立查询(没有引用此CTE名称)被称为锚成员(anchor members),它们只能被调用一次。反引用到此CTE名称的查询被称为递归成员,它们可被重复调用直到查询不返回行。通过使用UNION或UNION ALL算子,锚成员可互相追加,选择哪个算子取决于你是否要去除重复记录。必须使用UNION ALL算子追加递归成员。 要示例递归CTE是有用的,设想数据库AdventureWorks中表BillOfMaterials。此表代表了典型的产品配件的材料清单场景,它们组成无环的有向图。每个产品由其他产品装配起来,它也可能被装配到其他产品,但没有循环关系。这种产品装配关系可由AssemblyID列和ComponentID列表示。PerAssemblyQty列包含ComponentID代表的产品的数量,对于每个个体产品则由AssemblyID代表。已废弃的关系在ObsoleteDate列中指定。如果你只对非废弃数据感兴趣,你应检查此列是否为NULL。表中还有其它有用信息,包括测量单位,但为了讲述此例,其他列将被忽略。 Figure 8中的代码向ProductID 210的分解视输出数据。Figure 9显示了此视的摘录和描绘了产品间的包含关系。CTE体内,第一个查询没有引用CTE名称,所以它是锚成员,如上所述它只被调用一次。注意此查询寻找CompentID为210和assembly ID为NULL的行,这意味着它是顶层产品。此查询证实这个关系不是废弃的,它返回ComponentID和数量。递归成员返回包含在装配件中的产品,这些装配件由上一步执行CTE名称和BillOfMaterials表的JOIN而返回。第一次调用递归成员时,上一步是锚成员返回的结果。第二次调用递归成员时,上一步是第一次递归成员调用返回的结果,如此下去,直到递归成员返回空集。 递归成员通过此部件的数量增加上一步的数量来计算部件的累计数量。外部查询对CTE名称的引用获取了所有锚成员和递归成员调用的统一结果。外部查询连接CTE与Products表以获取产品名称和产生如Figure 10所示的90行(已缩截的)。每个部件在输出中超过一次出现,例如产品835,因为它参与不同的装配。可以修改外部查询来以productID和name排序结果,这样可获取每个产品的总数。代码类似于Figure 8,但外部查询类似下面: SELECT B.ProductID, P.Name, SUM(B.Qty) AS TotalQty FROM BOMCTE AS B JOIN Product AS P ON P.ProductID = B.ProductID GROUP BY B.ProductID, P.Name ORDER BY B.ProductID;如果怀疑循环和想限制递归调用的次数,可紧跟在外部查询指后定MAXRECURSION选项: WITH... outer_query OPTION(MAXRECURSION 30)当CTE超过指定限制,此选项使SQL Server抛出错误。当没有指定此选项时,SQL Server默认设置为100。如果不想由限制,必须指定其为0。注意可编写检查循环关系的自定义代码,但这超出本文的范围。 PIVOT算子 SQL Server Yukon中新的PIVOT算子允许编写交叉查询,从而使行转换成列。UNPIVOT算子起相反的作用——使已PIVOT数据从列转换成行。当你想返回每个销售人员每年总销售订单额,Figure 11显示在数据库中使用PIVOT算子的结果,每年的数据显示于不同列。 当使用PIVOT算子时需要注意的重要事情是:要为它提供一个查询表达式,此表达式使用视、导出表或CTE来返回只感兴趣的列。原因是PIVOT在幕后对所有没有被算子显示引用的列执行了隐式的GROUP BY操作。此例中,你需要salesperson ID,订单年份和订单数据: USE AdventureWorks SELECT SOH.SalesPersonID, YEAR(SOH.OrderDate) AS OrderYear, SOD.OrderQty * SOD.UnitPrice AS OrderValue FROM SalesOrderHeader AS SOH JOIN SalesOrderDetail AS SOD ON SOD.SalesOrderID = SOH.SalesOrderIDSQL Server计算出,“GROUP BY”列清单应是输入表中列清单,它们没有被合计函数或IN子句里PIVOT算子显示引用。因此,你不会在隐式GROUP BY列清单中得到不需要的列,你需要为PIVOT算子提供一个输入表,它仅包含对合计函数、IN子句和隐式GROUP BY感兴趣的列。这可以通过使用CTE或导出表获取,其包含返回只感兴趣列的上一查询。 Figure 12代码示范如何在CTE中使用查询,和让外部查询对CTE结果执行PIVOT操作。SUM(OrderValue)告知PIVOT去计算哪个聚合以组合要PIVOT列的单元。FOR子句告知PIVOT哪个源列包含要转换到结果列的数据。IN子句包含将表现为列名的数据列表。 SQL Servr需要你在IN子句显示指定要转换成结果列的数据列表。不能使用静态查询并让SQL Server计算出OrderYear所有的明显数据。要达到此目的,必须使用动态执行去动态构建字符串,如Figure 13代码所示。 要了解UNPIVOT算子的运作,首先创建SalesPivoted表,它可通过运行Figure 12中的查询和在FROM子句前加上“SELECT INTO SalesPivoted”得到(见Figure 14)。UNPIVOT算子的参数很类似于PIVOT参数。但此次需指定一结果列的名称,此结果列将包含所有一列中已PIVOT的单元的数据。FOR子句后,需指定另一结果列的名称,此列将存储已PIVOT列的名称为列数据。IN子句后跟随圆括号,指定需要UNPIVOT的列清单: SELECT * FROM SalesPivoted UNPIVOT(OrderValue FOR OrderYear IN([2001], [2002], [2003], [2004])) AS UUNPIVOT不会返回含有NULL值单元的行。要清空刚才在数据库中已建立的表和索引,运行下列代码: DROP INDEX SalesOrderHeader.idx_nc_OrderDate DROP TABLE SalesPivoted触发器和通知 SQL Server Yukon Beta1版本引入数据定义语言(DDL)触发器的支持,从而允许跟踪DDL操作和作相应反应,并可选择性地回滚活动。DDL触发器以同步方式工作,其紧跟触发事件后,类似于SQL Server上一版本触发器的工作方式。SQL Servr通过使用通知也支持异步事件消费机制,它可让你当某种事件发生后被通知。 下列触发器被创建于数据库级,它捕获所有DROP TABLE企图: CREATE TRIGGER prevent_drop_table ON DATABASE FOR DROP_TABLE AS RAISERROR(''''Not allowed to drop tables.'''', 10, 1) ROLLBACK -- For debug PRINT ''''DROP TABLE attempt in database '''' + DB_NAME() + ''''.'''' PRINT EventData() GO可以定义触发器对指定DDL事件触发,这些事件有CREATE_TABLE、DROP_TABLE、ALTER_TABLE、CREATE_VIEW等等,或者,如果想触发器对数据库中所有DDL事件进行触发,可指定DDL_DATABASE_LEVEL_EVENTSXUAN。触发器内,可调用EventData函数以返回启动触发器的有关进程和活动的信息。可检查此函数返回的XML并作相应处理。 要测试触发器,首先通过下列代码创建表TestDrop和插入一行数据: CREATE TABLE TestDROP(col1 INT) INSERT INTO TestDROP VALUES(1)接着,尝试删除表: DROP TABLE TestDROPDROP企图被捕获,一条信息被打印以指示不允许删除此表。同样,为调试所需,EventData函数的返回值以XML格式被打印出。(实际上,触发器内可检查XML数据,它包含很多有用信息,以决定哪个过程行为最适合你需要。例如,可防止一天中某时刻删除某表。)触发器回滚活动,因而表没有从数据库中删除。要删除触发器,执行下列语句: DROP TRIGGER prevent_drop_table ON DATABASE也可以创建捕获服务级别事件的触发器。例如,下面触发器捕获登录操作事件,如创建、改变或删除一次登录。 CREATE TRIGGER audit_ddl_logins ON ALL SERVER FOR CREATE_LOGIN, ALTER_LOGIN, DROP_LOGIN AS PRINT ''''DDL LOGIN took place.'''' PRINT EventData() GO此例中,当事件发生后触发器仅仅打印通告信息和事件的详细信息。但是,当然,可检查事件详细内容并作相应的处理。要测试触发器,运行下列代码,然后看看结果: CREATE LOGIN login1 WITH PASSWORD = ''''123'''' ALTER LOGIN login1 WITH PASSWORD = ''''xyz'''' DROP LOGIN login1一DDL登录事件被识别,然后事件数据以XML格式产生。如果你愿意,可检查事件数据和审查对你重要的信息。 如果要删除触发器,运行下列代码。 DROP TRIGGER audit_ddl_logins ON ALL SERVER总结 Yukon提供的T-SQL增强和新特性允许你更有效操纵数据,更容易开发应用程序,和增加了错误处理能力。当处理数据操作时,T-SQL仍然是SQL Server中最佳开发选择,并且现在有了更丰富的开发环境。为更容易地试验这些新特性集,本文描述的所有例子的源码可从文章开首的链接处下载。 要了解背景信息,请查阅: SQL Server http://www.sqlmag.com 作者简介 Itzik Ben-Gan 是 Solid Quality Learning 公司的主要顾问。他在国际上教学、演讲、写作和咨询相关 SQL Server 信息和知识。Itzik合著了Advanced Transact-SQL for SQL Server 2000 (APress, 2000)这本书。他管理以色列SQL Server用户组,并是一位SQL Server MVP。他的联系邮件 :itzik@solidqualitylearning.com。 |