发布时间:2014-08-01 00:00 来源:未知
一、简介
Microsoft 通过宿主 Microsoft .NET Framework 2.0 公共语言运行库 (CLR),SQL Server 2005显著地增强了数据库编程模型的功能。它支持用任何 CLR 语言(特别是 Microsoft Visual C# .NET、Microsoft Visual Basic .NET 和 Microsoft Visual C++)编写过程、触发器和函数。同时,它还提供了扩展数据库系统中的类型系统和聚合函数的功能,方法是允许应用程序创建它们自己的数据类型和聚合函数。
本白皮书从数据库应用程序开发人员的角度描述了如何最有效地利用这项技术。它还将 CLR 与 SQL Server 中支持的现有编程语言(即 TransacT-SQL (T-SQL) 和扩展存储过程 (XP))进行了比较。本白皮书不打算提供这些功能的基本参考资料,SQL Server 2005 Beta 2 Books Online 对其进行了详细介绍。
本白皮书的目标读者包括数据库应用程序开发人员、架构师和数据库管理员。本白皮书假设读者熟悉基于 .NET Framework 的编程和数据库编程。
二、CLR 集成概述下面简要概述了 CLR 集成执行的 SQL Server 功能,以及 Visual Studio 2005 最终发布版本如何支持这些功能。
注册和执行数据库中的托管代码包括以下步骤:
• |
开发人员将托管程序编写为一组类定义。将 SQL Server 内旨在用作存储过程、函数或触发器(下面统称为例程)的代码编写为类的 static(或 Microsoft Visual Basic .NET 中的 shared)方法。将旨在用作用户定义的类型和聚合的代码编写为一个整类。开发人员编译该程序并创建一个程序集。 |
• |
然后,将此程序集上载到 SQL Server 数据库,在其中使用 CREATE ASSEMBLY 数据定义语言 (DDL) 将它存储到系统目录。 |
• |
接着,创建 TransacT-SQL 对象,例如,例程(函数、过程和触发器)、类型和聚合,并将其绑定到已经上载的程序集中的入口点(对例程来说是方法,对类型和聚合来说是类)。使用 CREATE PROCEDURE/FUNCTION/ TRIGGER/TYPE/AGGREGATE 语句来完成这一步。 |
• |
在创建了这些例程之后,应用程序就可以像使用 T-SQL 例程一样使用它们。例如,可以从 T-SQL 查询中调用 CLR 函数,从客户端应用程序或从 T-SQL 批处理中调用 CLR 过程,就好像它们是 T-SQL 过程一样。 |
Visual Studio 2005 Beta 1 支持在 SQL Server 2005 中开发、部署和调试托管代码。有一种新的项目类型(称为 SQL Server 项目),它允许开发人员在 SQL Server 中开发、部署和调试例程(函数、过程和触发器)、类型和聚合。
构建和部署
SQL Server 项目提供了代码模板,这使得开发人员能够轻松地开始为基于 CLR 的数据库例程、类型和聚合编写代码。该项目还允许添加对数据库中其他的程序集的引用。在构建项目时,可以将其编译成一个程序集。部署此程序集可以将程序集的二进制文件上载到与该项目相关联的 SQL Server 数据库中。部署操作还自动创建在数据库的程序集中定义的例程、类型和聚合,方法是使用在代码中定义的自定义属性(SqlProcedure、SqlFunction 和 SqlTrigger 等等)。它还上载与该程序集相关联的源代码和 .pdb 文件(调试符号)。
调试
对于任何平台来说,调试都是开发人员体验的基本部分。SQL Server 2005 和 Visual Studio 2005 为数据库编程人员提供了这些功能。调试 SQL Server 2005 对象的关键部分在于其易于安装和使用。调试到运行 SQL Server 的计算机的连接在很大程度上同调试传统操作系统中运行的进程的方式一样。调试器的功能不会受到客户端到服务器的连接类型的影响。这样就可以调试表格数据流 (TDS) 和 HTTP 连接。而且,还可以跨语言进行无缝调试。因此,假如有一个调用 CLR 存储过程的 T-SQL 存储过程,调试会允许您从 T-SQL 过程进入到 CLR 过程。
三、CLR 与 TransacT-SQL现在我们进入本文的关键部分:对 CLR 集成和 SQL Server 中支持的现有编程语言进行比较。
TransacT-SQL (T-SQL) 是 SQL Server 支持的本机编程语言。和大多数 SQL Server 版本一样,它包含数据处理功能和数据定义功能。数据处理功能一般可以分为两类:查询语言(由 SELECT/INSERT/UPDATE/ DELETE 语句组成)和过程语言(WHILE、赋值、触发器、光标等)。一般来说,SQL Server 中的 CLR 支持为过程语言提供了 T-SQL 的替代方法。
即使在 SQL Server 中引入 CLR 支持以前,数据库应用程序应该尽可能多地使用查询语言,这始终被认为是很重要的。数据库应用程序应该利用面向集的查询处理器,并且只在查询语言无法表示逻辑时才转向过程编程。对于 SQL Server 中的 CLR 支持,这仍然是正确的。不应该使用 CLR 来编写可以用简单的 SELECT 语句表示的过程代码。在 SQL Server 2005 中增加了许多重要的功能来提高T-SQL 查询语言的表达能力。
• |
递归查询:遍历表中的递归层次的能力 |
• |
分析函数:RANK 和 ROW_NUMBER 允许排列结果集中的行 |
• |
新的关联操作:APPLY、PIVOT 和 UNPIVOT |
试图使用 CLR 功能的开发人员应该确保他们充分地利用查询语言,包括 SQL Server 2005 中的扩展在内。对于在查询语言中无法以声明方式表示的逻辑,他们应该考虑将 CLR 作为有效的替代办法。
现在让我们看一些方案,其中基于 CLR 的编程能够补充 T-SQL 查询语言的表达能力。通常,需要在查询(可称为函数)内嵌入过程逻辑。这允许许多方案,例如:
• |
根据数据库表中存储的值,对每行进行复杂的计算(必须用过程逻辑来表示)。这可以包括将这些计算的结果发送给客户端,或者使用计算来过滤发送给客户端的行集,如以下示例中所示: SELECT <complex-calculation>(<column-name>,...) FROM <table> WHERE <complex-calculation>(<column-name>,...) = ... |
• |
• 使用过程逻辑来评估表格结果,然后在 SELECT 或 DML语句的 FROM 子句中进行查询。 |
SQL Server 2000 引入了支持这些方案的 T-SQL 函数(标量和表值)。有了 SQL Server 2005,就可以用 CLR 语言更容易地编写这样的函数,并且会极大地提高它们的性能。之所以编写这些函数非常容易,是因为事实上编写 CLR 代码的开发人员可以利用 .NET Framework API中存在的大量有用函数和类。这个类/函数库比 TransacT-SQL 中支持的内置函数要丰富得多。此外,CLR 编程语言提供了 T-SQL 中所没有的丰富构造(例如数组和列表等)。与 T-SQL(它是一种解释语言)相比,CLR 编程语言之所以具有更好的性能,是因为托管代码是已编译的。对于涉及算术计算、字符串处理、条件逻辑等的操作,托管代码的性能可能要优于 T-SQL 一个数量级。
注:对于函数,几乎没有必要从函数中访问数据库。外部查询已经从数据库中检索到数据值,并且将其作为参数传递给函数。这是 CLR 的优势,在计算性任务上比 T-SQL 更胜一筹。
从 CLR 中访问数据
现在让我们从编程模型和性能这两个方面,看看如何用 CLR 来编写访问数据库的业务逻辑。
编程模型
使用 T-SQL,只是在过程代码内部嵌入查询语言语句 (SELECT/INSERT/UPDATE/ DELETE)。通过托管代码,可以使用 SQL Server 托管提供程序来实现 Microsoft ADO.NET 数据访问 API(也称为 in-proc ADO.NET)。使用这种方法,可以将查询语言语句(SELECT 和 DML 语句)作为动态字符串嵌入,并且将其作为参数传递给 ADO.NET API。与 T-SQL 的静态方法相比,过程代码内嵌入的 SQL 语句的基于动态 API 的特性是它们在编程模型上的主要区别。不利的是,in-proc ADO.NET 模型会产生比 T-SQL 更冗长的代码。此外,因为 SQL 语句是动态字符串,所以在执行之前不在语法或语义上进行编译和验证。有利的是,带有 ADO.NET 的数据库编程模型与客户端或中间层中使用的模型相似,因而更容易在各层之间移动代码和利用现有的技术。
此外,在基于 T-SQL 和基于 CLR 的编程模型中使用的都是同一 SQL 查询语言,不同之处在于过程部分,注意到这一点是非常重要的。
性能
正如已经提到的,在谈及过程代码、计算等方面时,与 T-SQL 相比,托管代码在性能方面具有决定性的优势。然而,对于数据访问方面,T-SQL 在性能方面通常会更好。因此,通用规则是用 CLR 编写计算和逻辑密集的代码要比数据访问密集的代码好。不过这值得更详细地考虑。
让我们看看数据访问编程中的一些典型的基元和模式,以及在这些情况下如何使用 ADO.NET 进行 T-SQL 和托管编程。
将结果发送到客户端
这包括将一组行发送到客户端,而没有在服务器中“消费”它们(即没有在例程内导航行)。使用 T-SQL,只需在 T-SQL proc 中嵌入一个 SELECT 语句就可以将 SELECT 产生的行发送到客户端。通过托管代码,可以使用 SqlPipe 对象将结果发送到客户端。T-SQL 和 in-proc ADO.NET 平台在这种情况下的作用是一样的。
提交 SQL 语句
这包括来自过程代码的 SQL 语句的执行往返。在这种情况下,T-SQL 具有很大的优势(比 in-proc ADO.NET 快两倍多)。
此处需要重点注意的是,之所以在 CLR 中出现性能降低,是因为增加了额外的代码层,包括将来自托管代码的 T-SQL 语句提交给原生 SQL 代码。在将该语句提交给查询处理器之后,基于语句源(T-SQL 或 in-proc ADO.NET)的性能方面就没有什么不同了。
注:典型的数据访问密集的存储过程可能涉及提交一系列的 SQL 语句。假如 SQL 语句简单,并且不需要花费大量的时间执行,则来自托管代码的调用开销可能占用大部分执行时间,这样的过程用 T-SQL 编写将执行得更好。
只进、只读行导航
这包括以只进、只读方式一次导航一个由 SQL 语句产生的行。在 T-SQL 中,这是通过只进、只读光标实现的。在 CLR 中,这是通过 SqlDataReader 实现的。通常,每一条语句都涉及一些处理。假如忽略了与每行相关联的处理,则导航行在 CLR 中就比在 T-SQL 光标中稍慢。然而,假如您关心为每行执行的处理,则 CLR 会更有优势,因为 CLR 在这种处理上比 T-SQL 做得好。
带有更新的行导航
假如需要根据光标中的当前位置更新行,则没有相关的性能比较,因为 in-proc ADO.NET 不支持此功能,而应该通过 T-SQL 可更新光标来进行此操作。
注 在任何可能的情况下,最好使用 UPDATE 语句来批量更新行,只有在这样的修改无法用单一的 UPDATE 语句进行表示时,才应使用基于光标导航的 UPDATE。
以下示例说明在特定情况下如何确定 T-SQL 和 CLR 将执行:
1. |
考虑这样一个过程,它执行一系列(或在最简单的情况下仅一个)INSERT/UPDATE/DELETE/SELECT 语句,带有几个或者不带返回到客户端的行,并且不导航 SELECT 产生的行。假如将这样的过程编写成 T-SQL 过程可能执行得更好。 |
2. |
考虑这样一个过程,它执行单一的 SELECT 语句,并且使用存储过程内的行,方法是,一次导航一行并进行一些不涉及对每行进行更多的数据访问的处理。这个过程可能在带有 in-proc ADO.NET 的 CLR 中执行得更好,特别是假如每行都有一些大量处理的开销时(因为这样的操作在 CLR 中比在 T-SQL 中更加高效)。 |
下面是一些简单的指导原则,可以用来在 CLR 和 T-SQL 之间进行选择:
• |
尽可能使用带有 T-SQL SELECT、INSERT、UPDATE 和 DELETE 语句的基于集的处理。只有在无法使用基于集的 DML 语句之一表示逻辑时,才应该使用过程和基于行的处理。 |
• |
假如过程仅仅是一个通过封装基本 INSERT/UPDATE/DELETE/SELECT 操作访问基表的包装,则应该用 T-SQL 进行编写。 |
• |
假如过程主要包括结果集中的只进、只读行导航,以及一些涉及每行的处理,则用 CLR 编写可能更有效。 |
• |
假如过程包括大量的数据访问以及计算和逻辑,则可以考虑将过程代码分隔为 CLR 来调用 T-SQL 过程,以进行大部分的数据访问(反之亦然)。另一个替代方法是,使用单一的 T-SQL 批处理,它包括从托管代码执行一次的一组查询,以减少从托管代码提交 T-SQL 语句的往返次数。 |
后面的部分将更深入地讨论在处理结果集时何时及如何适当地使用 T-SQL 和 CLR。
CLR 与 XP在 SQL Server 以前的版本中,扩展存储过程 (XP) 为数据库程序开发人员提供了唯一可用的机制来编写服务器端逻辑,这要么难于表示,要么不可能用 T-SQL 编写。CLR 集成提供了一种更健壮的替代方法来编写这种存储过程。此外,使用 CLR 集成,过去以存储过程形式编写的逻辑通常可以更好地表示为表值函数,因为它们允许它们允许将该函数构造的结果放在 SELECT 语句中进行查询(通过将这些结果嵌入到 FROM 子句中)。
以下是使用 CLR 过程或函数与 XP 相比的优势:
• |
粒度控制:很少可以控制 XP 能做什么或者不能做什么。使用代码访问安全模型,SQL Server 管理员可以分配三种权限之一:SAFE、EXTERNAL_ACCESS 或 UNSAFE,从而对托管代码允许进行的操作集进行不同程序的控制。 |
• |
可靠性:托管代码(特别是在 SAFE 和 EXTERNAL_ACCESS 权限集中)提供了比 XP 更安全、更可靠的编程模型。可验证的托管代码确保了所有对对象的访问都是通过强类型化的接口实现的,从而降低了程序访问或破坏属于 SQL Server 的内存缓冲的可能性。 |
• |
数据访问:使用 XP£¬编程人员必须向后显式连接到数据库(称为回环),以访问本地 SQL Server 数据库。而且,必须将此回环连接显式绑定到原来的会话事务上下文,以确保 XP 可以参与到调用它的同一个事务中。通过托管代码,可以使用更自然和更有效的编程模型来访问本地数据,这些模型利用当前的连接和事务上下文。 |
• |
性能:System.Data.SqlServer API 允许托管过程将结果集发送回客户端,其性能比 XP 使用的开放式数据服务 (ODS) API 更好。此外,System.Data.SqlServer API 支持新的数据类型(如 SQL Server 2005 中引入的 XML、(n)varchar(max)、varbinary(max)),而没有扩展 ODS API 来支持这些新的数据类型。 |
• |
可伸缩性:通过托管代码,SQL Server 可以管理资源(如内存、线程和同步)的使用,因为公开这些资源的托管 API 是在 SQL Server 资源管理器上实现的。相反,SQL Server 不能查看或控制 XP 的资源使用情况。举例来说,假如 XP 消耗了太多的 CPU 或内存资源,就没有办法使用 SQL Server 来检测或控制。然而,通过托管代码,SQL Server 可以检测到特定线程在一段很长的时间内一直没有退出,然后就强制该任务退出,这样其他工作可以按计划进行。因此,使用托管代码提供了更好的可伸缩性和健壮性。 |
正如上面所提到的,在数据访问和发送结果集给客户端方面,CLR 过程比 XP 做得更好。对于不包括数据访问和发送结果的代码,比较 XP 和托管代码的性能就是比较托管代码和原生代码的性能。一般来说,在这些情况下托管代码比不上原生代码的性能。而且,当在 SQL Server 内运行时,从托管代码到原生代码的事务处理过程有额外的开销,因为在移动到原生代码和从原生代码移回时,SQL Server 需要对特定于线程的设置进行额外的登记-保留。因此,对于在托管代码和原生代码之间有频繁的事务处理的情况,XP 大大地胜过在 SQL Server 内部运行的托管代码。
对于大多数扩展过程,假如考虑数据访问和结果发送的可伸缩性、可靠性和性能优势,CLR 过程提供了更好的替代方法。对于性能主要是由处理(数据访问和结果发送之外的)和频繁的托管-原生转换决定的情况,应该权衡 CLR 的可伸缩性和可靠性的优势与 XP 的原始性能优势。
四、代码位置:数据库与中间层
通过在数据库中提供丰富的编程模型,CLR 集成提供了将逻辑从其他层移动到数据库层的选择。然而,这显然并不意味着所有或大部分逻辑应该移到数据库中。
将逻辑移到数据库层可以减少网络中的数据流量,但是增加了服务器上宝贵的 CPU 资源的负荷。因此,在应用程序中做出代码放置的决定之前,要慎重权衡。以下注意事项适用于将数据库层作为首选的代码位置:
• |
数据验证:在数据层进行数据验证的逻辑可以更好地将数据和逻辑封装在一起。这样避免了在不同数据接触点(如:后端处理、批量上载和来自中间层的数据更新等)中重复验证逻辑。 |
• |
减少网络流量:对于需要处理大量的数据而产生很少的数据的数据处理任务(如数据分析应用程序中的需求预测、基于需求预测的生产安排等)来说,将逻辑放在数据库层中是合适的。 |
注即使在引入 CLR 支持之前,上面的注意事项也是有效的。数据库层中的 CLR 支持意味着编程语言的选择没有妨碍代码位置的正确选择。
示例:生产安排
生产安排是制造企业的常见任务。在高层次上,它包括制订何时生产多少单位数量的产品的计划,以便能够满足需求、最大程度的降低库存成本,同时将生产成本降到最低。有几个算法将需求预测、库存成本和生产线安装成本作为输入,而将制造策略作为输出。
假定将来的需求预测存储在 SQL Server 表中,则此类算法的实现有以下特征:
1. |
使用大量的数据作为输入(如需求预测)。 |
2. |
产生小结果(如在特定的日期内生产的单位数量)。 |
3. |
需要相当多的计算以便从输入中派生输出。 |
在中间层实现这样的算法是可行的,但是在数据库外移动输入集方面有性能损失。在 T-SQL 中将其实现为存储过程也是可行的,但是因为需要复杂的计算,性能损失就显现出来了。性能特征将随着实际的数据量和算法的复杂性的不同而不同。
为了验证 CLR 集成是否适合于这样的情况,我们举一个特定的生产安排算法的示例 - Wagner-Whitin 算法的动态编程实现。正如所预料的,CLR 集成优于 T-SQL。对于这种情况,使用托管代码还有其他好处。这种算法的实现需要使用大量的一维和多维数组、数据结构,而这些在 T-SQL 中是不可用的。总之,CLR 集成的性能要优于 T-SQL 实现几个数量级。
假定以下简单的数据库架构跟踪可以生产的产品列表。
表 1: t_products | |||
列名 | 类型 | 是否为空 | 说明 |
PID |
int |
非空 |
产品主键 ID |
Pname |
nvarchar(256) |
空 |
产品名称 |
InventoryCost |
int |
非空 |
存储该产品的每时段成本 |
StartupCost |
int |
非空 |
建立生产线来生产该产品的成本 |
下表存储了每周每个产品的需求预测信息。
表 2: t_SalesForecast | |||
列名 | 类型 | 是否为空 | 说明 |
PID |
Int |
非空 |
产品 ID |
WeekDate |
smalldatetime |
非空 |
需求预测周 |
DemandQty |
int |
非空 |
特定产品和特定周的需求预测 |
给定一组产品,它们的库存和启动成本以及未来需求预测,我们创建了接受如下输入参数的存储过程:1)制订生产进度表的日期,2)按进度表生产所需要的周数。
存储过程返回带有下表中的架构的行集。
表 3:存储过程架构 | ||
列名 | 类型 | 说明 |
Product |
nvarchar(256) |
产品名称 |
Period |
datetime |
进度周 |
Quantity |
int |
在指定周内制造的产品的数量 |
将 C# 版本的代码复制到下面的代码中,以说明这种可以从 CLR 集成中大大获益的情况:
using System; using System.Data; using System.Data.Sql; using System.Data.SqlServer; using System.Data.SqlTypes; public class ProductionSchedule { //4-year limit on scheduling public const int MAXPRODUCTS = 101; public const int MAXWEEKS = 210; public const int MAXNAME = 256; public ProductionSchedule() { } public static int Schedule(SqlDateTime startDate, int numWeeks) { SqlDateTime[] week = new SqlDateTime[MAXWEEKS]; int[] quantity; int[][] Cij; int[] Fk; int[] minK = new int[MAXWEEKS]; int product_id, current_product, product_count = 0; int startPeriod; // We'll use arrays to keep state about products and forecasts in memory. This is only viable given that we know we have a small number of products and weeks. // For larger data sets, we would have to consider cursors or temporary tables. // stored as CLR types since we know they can't be null int[] h = new int[MAXPRODUCTS]; int[] K = new int[MAXPRODUCTS]; // stored as nullable SqlChars since the table schema allows for null names SqlChars[] productNames = new SqlChars[MAXPRODUCTS]; bool moreProducts = true; int optimal_j; int period; int sum; SqlPipe pipe = SqlContext.GetPipe(); SqlDataRecord record; object[] values = new object[3]; SqlMetaData[] metadata = new SqlMetaData[3]; //Initialize algorithm arrays Cij = new int[MAXWEEKS][]; for( int l=0;l<MAXWEEKS;l++) Cij[l] = new int[MAXWEEKS]; Fk = new int[MAXWEEKS]; //Look up K and h for all products SqlCommand cmd = SqlContext.GetCommand(); cmd.CommandText = @"SELECT pname, InventoryCost, StartupCost from dbo.t_Products ORDER BY PID"; SqlDataReader reader = cmd.ExecuteReader(); while(reader.Read()) { productNames[product_count] = reader.GetSqlChars(0); //product name h[product_count] = reader.GetInt32(1); //holding cost K[product_count] = reader.GetInt32(2); //startup cost product_count++; // if we exceeded number of expected products then bail out with an exception if (product_count >= MAXPRODUCTS) { throw new Exception("Too many products"); } } reader.Close(); product_count = 0; //Get the list of product ids; cmd = SqlContext.GetCommand(); cmd.CommandText = @"select PID, weekdate, DemandQty from dbo.t_SalesForecast ORDER BY PID, WeekDate"; reader = cmd.ExecuteReader(); moreProducts=reader.Read(); //Set up the record for returning results metadata[0] = new SqlMetaData( "Product", SqlDbType.NVarChar,MAXNAME ); metadata[1] = new SqlMetaData( "Period", SqlDbType.DateTime ); metadata[2] = new SqlMetaData( "Quantity", SqlDbType.Int ); record = new SqlDataRecord( metadata ); while( moreProducts ) { product_id = current_product = reader.GetInt32(0); int index = 1; quantity = new int[MAXWEEKS]; while( current_product == product_id ) { week[index] = reader.GetSqlDateTime(1); quantity[index] = reader.GetInt32(2); index++; moreProducts = reader.Read(); if( !moreProducts ) break; current_product = reader.GetInt32(0); } //Determine the ordinal start week startPeriod = 1; //For each product ID calculate Cij for( int i = startPeriod; i < (startPeriod + numWeeks); i++ ) { for( int j = i+1; j <= (startPeriod + numWeeks+1); j++ ) { Cij[i][j] = GetCij(quantity,i,j,K [product_count],h[product_count]); } } //Calculate Fk for( int k = startPeriod + numWeeks + 1; k >= startPeriod; k--) { minK[k] = GetFk_SO(k,startPeriod + numWeeks,Cij,Fk); } //Send the results record.SetSqlChars(0,productNames[product_count]); pipe.SendResultsStart(record,false); for( int k = startPeriod; k < startPeriod + numWeeks; ) { period = k; optimal_j = minK[k]; sum = 0; while( k < optimal_j ) { sum = sum + quantity[k++]; } values[1] = week[period]; record.SetValue(1,values[1]); values[2] = sum; record.SetValue(2,values[2]); pipe.SendResultsRow(record); } pipe.SendResultsEnd(); product_count++; } reader.Close(); return 0; } private static int GetCij(int[] quantities, int i, int j, int K, int h) { if( j == i+1 ) return K; else return (j-1-i) * h * quantities[j-1] + GetCij(quantities, i, j-1,K,h); } private static int GetFk_SO(int k,int n,int[][] Cij, int[] Fk) { int j,min; j = k+1; min = j; if ( k == n+1 ) { Fk[k] = 0; return j; } Fk[k] = Cij[k][j] + Fk[j]; for(; k <= n ;k++) { j = k + 1; while( j <= n+1 ) { if( Cij[k][j] + Fk[j] < Fk[k] ) { min = j; Fk[k] = Cij[k][j] + Fk[j]; } j++; } } return min; } }五、处理常见数据库编程任务和问题
前一节在高层次上对基于 CLR 的编程与 T-SQL、中间层和扩展存储过程 (XP) 进行了比较。在这一节中,我们将考虑数据库应用程序开发人员经常遇到的一些编程任务和模型,并且讨论如何使用 CLR(以及在一些情况下如何不使用)进行处理。
使用 Framework 库进行数据验证
SQL Server 2005 中的 CLR 集成允许用户利用 .NET Framework 类库提供的丰富功能来解决其数据库编程问题。
常规表达式的使用可以很好地说明 CLR 集成如何增强了验证和过滤功能。在处理数据库中存储的文本数据方面,常规表达式提供的模式匹配功能比通过 T-SQL 查询语言中的 LIKE 运算符可用的模式匹配功能多。考虑以下 C# 代码,它只是 System.Text.RegularExpressions 命名空间中的 RegEx 类的一个简单包装:
using System; using System.Data.Sql; using System.Data.SqlTypes; using System.Text.RegularExpressions; public partial class StringFunctions { [SqlFunction(IsDeterministic = true, IsPrecise = true)] public static bool RegExMatch(string pattern, string matchString) { Regex r1 = new Regex(pattern.TrimEnd(null)); return r1.Match(matchString.TrimEnd(null)).Success; } [SqlFunction(IsDeterministic = true, IsPrecise = true)] public static SqlString ExtractAreaCode(string matchString) { Regex r1 = new Regex("\\(( <ac>[1-9][0-9][0-9])\\)"); Match m = r1.Match(matchString); if (m.Success) return m.Value.Substring(1, 3); else return SqlString.Null; } };
假设 StringFunctions.RegExMatch 和 StringFunctions.ExtractAreaCode 方法已经被注册为带有 RETURNS NULL ON NULL INPUT 选项的数据库中的用户定义函数(这允许该函数在任何输入都为 NULL 时返回 NULL,这样在该函数内就没有特殊的 NULL 处理代码):
现在,可以在使用上述代码的表的列中定义约束,以验证电子邮件地址和电话号码,如下所示:
create table Contacts ( FirstName nvarchar(30), LastName nvarchar(30), EmailAddress nvarchar(30) CHECK (dbo.RegExMatch('[a-zA-Z0-9_\-]+@([a-zA-Z0-9_\-]+\.)+(com|org|edu)', EmailAddress) = 1), USPhoneNo nvarchar(30) CHECK (dbo.RegExMatch('\([1-9][0-9][0-9]\) [0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9]', UsPhoneNo)=1), AreaCode AS dbo.ExtractAreaCode(UsPhoneNo) PERSISTED )
另外,请注意 AreaCode 列是使用 dbo.ExtractAreaCode 函数从 USPhoneNo 列中取出地区代码而得到的列。然后,可以对 AreaCode 列建立索引,这样便于在表格中根据特定地区代码查找联系人的查询。
更一般地讲,此示例演示了如何利用 .NET Framework 库来增强带有有用函数的 T-SQL 内置函数库,这些有用函数很难用 T-SQL 表达。
产生结果集
需要从运行在服务器内的数据库对象(如存储过程或视图)中产生结果集可能是最常见的数据库编程任务之一。假如可以使用单个查询(SELECT 语句)来构建结果集,则这只需使用视图或在线表值函数即可实现。然而,假如需要多个语句(过程逻辑)来构建结果集,则有两个选择:存储过程和表值函数。虽然 SQL Server 2005 有表值函数,但是它们只能用 T-SQL 进行编写。在 SQL Server 2005 中,通过 CLR 集成,还可以使用托管语言来编写这样的函数。在这一节中,我们将讨论如何决定使用存储过程还是使用表值函数,以及使用 T-SQL 还是使用 CLR。
从 T -SQL 过程可以将相关的结果作为表值函数的返回值返回,或者通过存储过程内曾经隐式存在的“调用者管道”返回。从存储过程的任何位置(不管执行的嵌套程度如何)执行 SELECT 语句都会把结果返回给调用者。更严格地讲,实际上 SELECT 语句并没有进行变量赋值。而且,FETCH、READTEXT、PRINT 和 RAISERROR 语句也隐式地将结果返回给调用者。
请注意,“调用者”一直没有正确地定义,它实际上取决于存储过程的调用上下文。
假如从任何客户端数据访问 API(如 ODBC、OLEDB 和 SQLClient)中调用存储过程,则调用者是实际的 API,并且它提供的任何一种抽象都可以表示结果(如 hstmt、IRowset 或 SqlDataReaderand)。这意味着,通常,从存储过程中产生的结果将始终返回到调用 API 中,而跳过堆栈中所有的 T-SQL 框架,如以下示例中所示:
create proc proc1 as select col1 from dbo.table1; create proc proc2 as exec proc1;
在执行过程 proc2 时,proc1 产生的结果将转到 proc2 的调用者。proc2 中只有一种方法可以捕获产生的结果,即通过使用 INSERT/EXEC 将其存储到永久表、临时表或表变量中,从而将结果流式处理到磁盘。
create proc proc2 as declare @t table(col1 int); insert @t (col1) exec proc1; -- do something with results
在使用 INSERT/EXEC的情况下,“调用者”是 INSERT 语句的目标表/视图。
SQL Server 2005 CLR 存储过程引入了新的“调用者”类型。当通过托管框架中的 in-proc 提供程序执行查询时,就可以通过 SqlDataReader 对象使结果可用,并且可以在存储过程中使用结果。
... SqlCommand cmd=SqlContext.GetCommand(); cmd.CommandText= "select col1 from dbo.table1"; SqlDataReader sdr=cmd.ExecuteReader(); while (sdr.Read()) { // do something with current row } ...
下面的问题是托管存储过程如何将结果返回给它的调用者而不是通过 SqlDataReader 来使用它。这可以通过称为 SqlPipe 的新类来实现。通过 SqlContext 类的静态方法可以使此类的实例对托管存储过程可用。SqlPipe 有几种方法可以将结果返回给存储过程的调用者。这两个类都是在 Sqlaccess.dll 中定义的。
SqlPipe
在 SqlPipe 类中可以使用的方法中,最容易理解的就是 Execute 方法,它将命令对象作为参数接受。这个方法主要执行命令,并且没有使执行的结果可用于托管框架,而是将结果发送给存储过程的调用者。发送结果的这种形式在语义上与将语句嵌入 T-SQL 存储过程内是一样的。在本文前面描述的性能方面,SqlPipe.Execute 与 T-SQL 是等价的。
create proc proc1 as select col1 from dbo.table1; The equivalent in C# would be: public static void proc1() { System.Data.SqlServer.SqlCommand cmd=SqlContext.GetCommand(); cmd.CommandText= "select col1 from dbo.table1"; SqlContext.GetPipe().Execute(cmd); }
对于返回的数据是由执行的查询直接产生的情况,SqlPipe.Execute 可以很好地工作。然而,在某些情况下可能希望1)从数据库中获得结果,进行操作或者转换,然后发送它们,或者 2)将结果发送回原地而不是本地 SQL Server 实例。
SqlPipe 提供了一组可以协同工作以使应用程序可以将任何结果返回给调用者的方法:SendResultsStart、SendResultsRow 和 SendResultsEnd。在很大程度上,这些 API 类似于对扩展存储过程的开发人员可用的 srv_describe 和 srv_sendrow API。
SendResultsStart 将 SqlDataRecord 作为参数接受,并且指示返回的新结果集的开头。该 API 从记录对象读取元数据信息,并且将其发送给调用者。该方法有重载,以允许发送元数据以及记录中的实际值。
随后可以返回行,方法是对要发送的每行调用一次 SendResultsRowows。在发送完全部所需的行之后,需要调用 SendResultsEnd 来指示结果集的结尾。
例如,下面的 C# 代码片段表示一个存储过程,它读取 XML 文档(来自 MSDN 的 Really Simple Syndication [RSS] 供给),使用 System.Xml 类进行解析,并且以相关的形式返回信息。请注意,这些代码应该创建为 EXTERNAL_ACCESS(或 UNSAFE)程序集,因为访问 Internet 所需的代码访问安全 (CAS) 权限只有在这些权限集中才是可用的。
// Retrieve the RSS feed XPathDocument doc = new XPathDocument("http://msdn.microsoft.com/sql/rss.xml"); XPathNavigator nav = doc.CreateNavigator(); XPathNodeIterator i = nav.Select("//item"); // create metadata for four columns // three of them are string types and one of the is a datetime SqlMetaData[] rss_results = new SqlMetaData[4]; rss_results[0] = new SqlMetaData("Title", SqlDbType.NVarChar, 250); rss_results[1] = new SqlMetaData("Publication Date", SqlDbType.DateTime); rss_results[2] = new SqlMetaData("Description", SqlDbType.NVarChar, 2000); rss_results[3] = new SqlMetaData("Link", SqlDbType.NVarChar, 1000); // construct the record which holds metadata and data buffers SqlDataRecord record = new SqlDataRecord(rss_results); // cache a SqlPipe instance to avoid repeated calls to SqlContext.GetPipe() SqlPipe sqlpipe = SqlContext.GetPipe(); // send the metadata, do not send the values in the data record sqlpipe.SendResultsStart(record, false); // for each xml node returned, extract four pieces // of information and send back each item as a row while (i.MoveNext()) { record.SetString(0, (string) i.Current.Evaluate("string(title[1]/text())")); record.SetDateTime(1, DateTime.Parse((string) i.Current.Evaluate("string(pubDate[1]/text())"))); record.SetString(2, (string) i.Current.Evaluate("string(description[1]/text())")); record.SetString(3, (string) i.Current.Evaluate("string(link[1]/text())")); sqlpipe.SendResultsRow(record); } // signal end of results sqlpipe.SendResultsEnd();
注:在 SendResultsStart 和 SendResultsEnd 调用之间,SqlPipe 被设置为繁忙状态,调用除 SendResultsRow 之外的任何 Send 方法都会导致错误发生。SqlPipe 处于繁忙状态中时,SendingResults 属性被设置为 TRUE。
表值函数
CLR 集成也启用了对用托管语言编写的表值函数 (TVF) 的支持。与 T-SQL 相似,TVF 主要用于返回表结果。最显著的不同在于,T-SQL 表值函数临时将结果存储在工作表中,而 CLR TVF 则能够对产生的结果数据进行流式处理。这意味着结果在从函数返回之前不需要物化。
注 T-SQL 还具有内联 TVF 的概念,即不临时存储结果。内联 TVF 在大部分语义上便于指定子查询(可能带有参数)。
托管 TVF 返回 ISqlReader 接口,这是由 SqlClient 和 SqlServer (in-proc) 托管提供程序中的 SqlDataReader 实现的一种只进光标抽象。查询处理器调用此接口上的 Read() 方法,以在每行返回 FALSE 之前获取它。
将上面的示例改为返回来自 RSS 供给的信息,返回结果的代码如下所示(不包括未实现的方法):
[SqlFunction] public static ISqlReader tvf1() { return (ISqlReader)new RssReader(); } public class RssReader : ISqlReader { SqlMetaData[] rss_results = null; XPathDocument doc; XPathNavigator nav; XPathNodeIterator i; // Construct helper class, initializing metadata for the results // reading from the RSS feed, creating the iterator public RssReader() { rss_results = new SqlMetaData[4]; rss_results[0] = new SqlMetaData("Title", SqlDbType.NVarChar, 250); rss_results[1] = new SqlMetaData("Publication Date", SqlDbType.DateTime); rss_results[2] = new SqlMetaData("Description", SqlDbType.NVarChar, 2000); rss_results[3] = new SqlMetaData("Link", SqlDbType.NVarChar, 1000); // Retrieve the RSS feed doc = new XPathDocument("http://msdn.microsoft.com/sql/rss.xml"); nav = doc.CreateNavigator(); i = nav.Select("//item"); } // # of columns returned by the function public int FieldCount { get { return rss_results.Length; } } // metadata for each of the columns public SqlMetaData GetSqlMetaData(int FieldNo) { return rss_results[FieldNo]; } // Read method positions the navigator iterator on next element public bool Read() { return i.MoveNext(); } // methods to return each column public Object GetValue(int FieldNo) { switch (FieldNo) { case 0: return new SqlString((string) i.Current.Evaluate("string(title[1]/text())")); case 1: return new SqlDateTime(DateTime.Parse( (string)i.Current.Evaluate("string(pubDate[1]/text())"))); case 2: return new SqlString((string) i.Current.Evaluate("string(description[1]/text())")); case 3: return new SqlString((string) i.Current.Evaluate("string(link[1]/text())")); } return null; } public string GetString(int i) { return (string)GetValue(i); } public DateTime GetDateTime(int i) { return (DateTime)GetValue(i); } public SqlChars GetSqlCharsRef(int i) { return new SqlChars((SqlString)GetValue(i)); } public SqlChars GetSqlChars(int i) { return new SqlChars((SqlString)GetValue(i)); } public SqlDateTime GetSqlDateTime(int i) { return (SqlDateTime)GetValue(i); } ... }
使用来自此表值函数的结果的一种简单查询如下所示:
select title, pubdate, description, link from dbo.GetRssFeed()
显然可以用此数据的 TVF 形式来表示更丰富的查询。假定函数 CanonicalURL() 会返回规范的 URL 版本。现在,可以使用规范的 URL 很容易地返回来自 RSS 供给的数据:
select title, pubdate, description, dbo.CanonicalURL(link) from dbo.tvf1() order by pubdate
请注意,在这个示例中,我们没有利用 TVF 的流化功能,因为我们正在使用整个 RSS 供给,在此之上建立导航器,然后在调用 Read() 时循环访问各个项目。然而,可以想象使用流式 API 来使用 Web 资源的结果,并且使用 XmlReader 来循环访问产生的 XML。需要重点注意的是,给出了 CLR 表值函数和 T-SQL 函数之间的执行模型的不同时,最主要的性能差异可能有利于 CLR TVF,特别是在可能将结果数据流化的情况下。
使用哪一个?
决定将存储过程与 SqlPipe (不管是 T-SQL 中使用隐式管道还是在托管中使用显式托管类)结合使用,还是使用表值函数,取决于以下几个必须考虑的因素:
• |
可组合性要求 |
• |
返回的数据的源 |
• |
对副作用操作的需要 |
• |
强类型化和结果集的数量 |
可组合性
有时可能需要重用或进一步处理 TVF 或存储过程中产生的结果。从可组合性的角度来说,表值函数更灵活。TVF 的返回类型是相关的行集,可以用在允许此类构建的任何地方。特别是,它可以用在 SELECT 语句的 FROM 子句中,因为这些产生的结果可以受益于子查询中的 SELECT、INSERT/SELECT、派生的表和通用表表达式等的可组合性。
另一方面,从 T-SQL 语言中,存储过程可以组合成 INSERT / EXEC 组合的唯一部分,这使得可以将产生的结果存储在永久或临时表中。INSERT 操作表示数据的实际副本,它可能会影响性能。
假如需要组合和重用服务器中的结果,TVF 是更好的替代方法。假如产生的结果只需要回流到客户端/中间层,任何一种方法都可以完成这项工作。
数据源
返回的数据源是在基于 T-SQL 和基于 CLR 的实现之间做出决定的另一个重要因素。可以通过使用 in-proc 提供程序读取本地实例中的一些数据源产生结果,也可以从 SQL Server 之外的数据源产生结果。本文前面描述的基于 Web 请求的结果构造的代码片段便是后者的示例。另一个远程数据源的示例是,使用 SqlClient 托管提供程序从远程SQL Server 实例中检索结果。对于这样的外部源,基于 CLR 的实现是更好的选择,因为使用它可以很容易地实现访问外部数据的逻辑。
现在让我们考虑这种情况,使用 in-proc 提供程序基于在本地实例中执行的查询生成结果。在使用 TVF 的情况下,默认的处理可能是返回由 in-proc 提供程序产生的 SqlDataReader,或者用 ISqlReader 的自定义实现包装这样的阅读器,以便在读取结果时对其进行转换。在使用存储过程的情况下,根据本地实例产生的结果必须执行查询,循环访问读取行,对结果执行一些操作,然后通过管道将其发送回去。
然而,SQL Server 2005 不允许表值函数返回时请求还处于未决状态。在函数体可以返回之前,必须全部执行任何通过 in-proc 提供程序执行的查询并且完全使用结果。假如执行了返回语句,而 in-proc 提供程序中的 SqlDataReader 操作还处于未决状态,就会引发错误。这意味着对于从本地数据库实例返回数据的大多数情况,无法通过 CLR TVF 流化结果。假如因为其他因素(例如可组合性)需要将此编写为 TVF,则使用 T-SQL 编写是唯一的选择。另外,通过 SqlPipe 使用托管存储过程是一个可能的选择。
请注意,对于基于来自本地实例的数据从存储过程中产生结果的情况,SendResultsXXX API 的使用只有在需要对结果进行修改或处理时才有意义。假如在未作修改的情况下将结果发送给调用者,则 SqlPipe.Execute 是更好的执行解决方案。
副作用操作
一般来说,不允许用户定义的函数(特别是表值函数)执行副作用操作。其中包括改变数据库状态的操作(如 DML 语句或事务处理操作)。在产生结果前后可能需要对系统状态作一些修改。例如,业务组件可能需要设置 SAVEPOINT 事务,执行一些 UPDATE,并且通过管道返回结果;但是假如出现错误,则回滚到 SAVEPOINT。
假如不允许从 TVF 执行副作用操作,则只有存储过程才能实现这样的方案,并且必须通过 SqlPipe 返回结果。
请注意,当 SqlPipe 忙于发送结果时,尤其不允许通过 in-proc 提供程序执行副作用操作。只有在完成结果集之前或之后允许进行这些操作。
强类型化和返回的结果集的数量
从上面的代码示例和与 T-SQL 一致的角度来看,由存储过程通过 SqlPipe 产生的结果的描述不同于 TVF。TVF 是强类型化的,并且作为注册 (CREATE FUNCTION) 语句的一部分,它必须静态地定义 TVF 产生的结果的列数和类型。
另一方面,存储过程声明并没有声明产生的结果 - 甚至是否返回结果。这看起来可能很方便,虽然它确实提供了更大的灵活性,但是在编写执行存储过程的应用程序时要更加细心,因为存储过程可以动态地重定义产生的结果的形式。
因此,自然而然建议根据元数据来描述结果:假如结果的架构需要根据调用的不同而变化,则只有 SqlPipe 才能提供这种灵活性。
同样地,通过存储过程内的 SqlPipe 产生的结果的弱类型化不能将单个结果的架构扩展为可能返回可变数量的结果集。存储过程可以自由地根据条件确定是否发送给定的行集和定义其形式。这样的灵活性增加了使用这种可变的结果流的应用程序的复杂性开销。
下表总结了如何在两者之间做出决定的指导原则:
• |
存储过程(使用隐式 SqlPipe 或基于显式 CLR 的方法)和 TVF |
• |
T-SQL 和 CLR |
表 4:关于生成结果集的指导原则 | ||
是 | 否 | |
需要可组合性? |
TVF |
TVF 过程或 TVF |
外部数据源(与只访问本地数据)? |
CLR TVF 或 CLR 过程 |
(只访问本地数据)T-SQL TVF 或过程 |
需要副作用? |
过程 |
过程或 TVF |
固定的结果架构? |
过程或 TVF |
过程 |
多个结果集? |
过程 |
过程或 TVF |
流化结果的能力? |
CLR TVF |
T-SQL TVF |
对于本节的大部分内容,通过 SqlPipe 发送结果是与过程紧密相关的。即使在 CLR 触发器主体中 SqlPipe 是可用的并且返回结果是可能的,也很不提倡这种做法,因为使用在目标对象中定义的触发器发出数据处理语言 (DML) 或数据定义语言 (DDL) 语句可能会导致意外的结果。
将标量分解为行
经常需要在应用程序中传送多值参数。例如,在定单处理系统中,可能需要编写存储过程来将定单插入到 Orders 表中。存储过程中的参数之一可能是定单中的行项目。在这种情况下,您会遇到 T-SQL 限制,它不支持表值参数或缺乏集合数据类型(如数组)。解决这个问题的一种方法是,将集合编码为一个标量值(如 nvarchar 或 xml),然后将其作为参数传递给存储过程。在存储过程内,可以使用表值函数来接受标量输入,并将其转换成一组行,然后将这些行插入到 LineItems 表中。
虽然可以用 T-SQL 编写表值函数,但是用 CLR 实现它有两个好处:
• |
System.Text 命名空间中的字符串处理函数使得编写表值函数更加容易。 |
• |
CLR TVF 提供了更有效的流实现,这避免了将结果加载到工作表中。 |
下面的代码片段显示了如何实现一个表值函数,它接受以‘;’分隔的一组值作为输入字符串,并且以一组行(字符串中的每个值一行)的形式返回该字符串。请注意,MySqlReader 类的构造函数实现了大部分工作,它使用 System.String.Split 方法将输入字符串分解为数组。
// TVF that cracks a ';' separated list of strings into a result // set of 1 nvarchar(60)column called Value public static ISqlReader GetStrings(SqlString str) { return (ISqlReader)new MySqlReader(str); } public class MySqlReader : ISqlReader { private string[] m_strlist; private int m_iRow = -1; // # rows read //The core methods //Initialize list public MySqlReader(SqlString str) { //Split input string if not database NULL; //else m_strlist remains NULL if (!str.IsNull) { m_strlist = str.Value.Split(';'); } } // SECTION: Metadata related: Provide #, names, types of // result columns public int FieldCount { get { return 1; } } public SqlMetaData GetSqlMetaData(int FieldNo) { if (FieldNo==0) return new SqlMetaData("Value", SqlDbType.NVarChar, 60); else throw new NotImplementedException(); } // SECTION: Row navigation. Read is called until it returns // false. After each Read call, Get<TypeName> for each // column is called. public bool Read() { //Return empty result set if input is DB NULL //and hence m_strlist is uninitialized if (m_strlist==null) return false; m_iRow++; if (m_iRow == m_strlist.Length) return false; return true; } //Column getters //Implement Get<SqlTypeName> for each column produced by //the TVF; in this case just one. public SqlChars GetSqlChars(int i) { if (i == 0) return new SqlChars(m_strlist[m_iRow]); else throw new NotImplementedException(); } //Methods not used by SqlServer omitted; //Actual implementation should provide an empty //implementation. ... } // public class MySqlReader } // class StringFunctions;
假定 GetStrings 方法注册为具有相同名称的 TVF。下面是存储过程的代码片段,它使用此 TVF 从定单中提取表形式的行项目。
CREATE PROCEDURE Insert_Order @cust_id int, @lineitems nvarchar(8000) AS BEGIN ... INSERT LineItems SELECT * FROM dbo.GetStrings(@lineitems) ... END
对数据进行自定义聚合
在许多情况下,您可能需要对数据进行聚合。这包括执行统计计算(如 avg、stddev 等等)。假如所需的聚合函数不是作为内置聚合函数直接支持的,SQL Server 2005 中有三种方法可以进行这样的自定义聚合:
• |
将聚合编写为用户定义的聚合 (UDA)。 |
• |
使用 CLR 存储过程编写聚合。 |
• |
使用服务器端光标。 |
让我们在一个称为 PRODUCT(int) 的简单聚合函数的上下文中检查这三种替代方法,该聚合函数计算一组给定值的乘积。
作为用户定义的聚合函数实现的 PRODUCT
下面是此函数的主干 C# 代码示例。所有的积累逻辑都在 Accumulate 函数中(为了简单起见,其他函数显示为 {...})。
[SqlUserDefinedAggregate(Format.Native)] public struct Product { public void Accumulate(SqlInt32 Value) { m_value *= Value; } public void Init() {...} public void Merge(Product Group) {...} public SqlInt32 Terminate() {...} }
在定义类型、创建程序集和注册到 SQL Server 之后,就可以通过以下方式使用 T-SQL中的聚合函数:
SELECT dbo.Product(intcol) FROM tbl GROUP BY col
作为使用 SqlDataReader 的托管存储过程实现的 PRODUCT
可以创建存储过程来执行查询和循环访问结果,以执行计算。这种循环访问是通过使用 SqlDataReader 类完成的。
[SqlProcedure] public static void Product(out SqlInt32 value) { SqlCommand cmd = SqlContext.GetCommand(); cmd.CommandText = "select intcolumn from tbl"; SqlDataReader r = cmd.ExecuteReader(); bool first = true; using (r) { while (r.Read()) //skip to the next row { if (first) { value = r.GetSqlInt32(0); first = false; } else { value *= r.GetSqlInt32(0); } } } }
可以使用 EXEC 语句来调用这一过程
EXEC Product @p OUTPUT
作为使用光标的 T-SQL 存储过程实现的 PRODUCT
可以创建 T-SQL 存储过程来执行查询和通过使用 T-SQL 光标循环访问结果,以执行计算。
create procedure TSQL_ProductProc (@product int output) as begin declare @sales int declare c insensitive cursor for select intcolumn from tbl open c fetch next from c into @sales if @@FETCH_STATUS = 0 set @product = @sales while @@FETCH_STATUS = 0 begin fetch next from c into @sales set @product = @product * @sales end close c deallocate c end
决定是使用 UDA 还是使用其他某种解决方案来产生结果取决于几个因素:
• |
可组合性要求。UDA 实际上是独立的对象,可以用于任何 T-SQL 查询,通常用在可以使用系统聚合函数的任何相同的地方。不需要假定它所操作的查询。例如,可以将其包括在视图定义(不过,索引视图中不支持 UDA)和标量子查询中。 |
• |
聚合算法细节。在 Order By 子句(假如查询中有)之前可能对 UDA 进行求值,因此不能保证传递给聚合函数的值的顺序。假如聚合算法需要按照特定的顺序使用值,则不能使用 UDA。同样地,UDA 从整组中使用值并且返回单一值。假如需要必须为组中的每个值返回值的聚合函数,则应该考虑使用存储过程或流表值函数来编写您的函数。详细信息请参见本文中的“产生结果”一节。 |
• |
对副作用和数据访问的需要。不允许 UDA 进行数据访问或有副作用。假如您的函数需要保留大量的数据作为聚合的中间状态,或因为其他某种原因需要进行数据访问,则必须使用过程。 |
使用 UDA 的第一种方法在这三个选择中可能提供最好的性能。通常,假如没有碰到上面所列的限制,就应该尝试将聚合函数编写为 UDA。假如无法使用 UDA 方法,则使用 SqlReader 的托管代码方法可能比 T-SQL 光标方法执行得更好。
可以用 UDA 方法编写的有用的聚合的示例还包括:找到每组中第 N 大(或第 N 小)值,找到每组中前 N 个最大值的平均值或总和,等等。
六 用户定义的类型 (UDT)现在,我们来讲 SQL Server 2005 中功能更强大但是经常被错误理解的一个功能。使用用户定义的类型 (UDT),可以扩展数据库的标量类型系统(不仅仅为系统类型定义您自己的别名,这在 SQL Server 以前的版本中一直可用)。定义 UDT 就像用托管代码编写类,创建程序集,然后使用“create type”语句在 SQL Server 中注册该类型一样简单。下面是实现 UDT 的主干代码:
[SqlUserDefinedTypeAttribute(Format.Native)] public struct SimpleUdt: INullable { public override string ToString() {...} public bool IsNull { get; } public static SimpleUdt Null { get; } public static SimpleUdt Parse(SqlString s) {...} ... } create type simpleudt from [myassembly].[SimpleUdt] create table t (mycolumn simpleudt)
何时创建 UDT
SQL Server 2005 中的 UDT 不是对象相关的扩展性机制。它们是扩展数据库的标量类型系统的一种方法。标量类型系统包括 SQLServer 附带的列类型(如 int、nvarchar 和 uniqueidentifier 等类型)。例如,使用 UDT,可以定义您自己的、用于列定义的类型。假如您的类型确实是一个适合建模为列的原子值,请创建 UDT。
假如需要定义您自己的标量类型,请使用 UDT。这种类型的示例情况包括各种日历中的自定义日期/时间数据类型和货币数据类型。使用 UDT,可以创建单个对象来公开类型上可用的所有行为,并且封装或隐藏该类型所存储的基础数据。需要访问数据的每个人都必须使用 UDT 的编程接口。假如能够利用 .NET Framework 中现有的功能(如国际化或日历功能),这实际上又是考虑将类型实现为 UDT 的一个很好的理由。
何时不创建 UDT
不要使用 UDT 来对复杂的业务对象(如雇员、联系人或客户)进行建模。您将会陷入 UDT 的所有列限制(如,8KB 大小限制、索引限制)和在更新 UDT 值时更新整个值的不利方面。对于复杂类型,UDT 不是合适的数据建模抽象;因此对于这种情况,最好使用中间层对象相关映射技术。
设计 UDT 时需要考虑的因素
因为它们是列形式的,所以可以定义整个 UDT 值的索引,并且创建参考完整性约束(如 UDT 列的唯一性)。还可以在比较和排序方案中使用 UDT。
比较 UDT 值是通过比较类型的基础二进制表示完成的。假如使用 Format.Native 作为持久性机制,则会按照同在该类型中定义的一样的字段顺序创建永久形式;因此,请确保这些字段是按照类型的正确顺序排列的。
UDT 上的每个操作(除了比较)都要求 UDT 的值反序列化,接着进行方法调用。这种模式有与之相关的固定开销。假如要将类型建模为 UDT(相对于表中的列),则在访问类型属性(相对于表中的列)时应该考虑这种差别。假如类型上的行为非常复杂,则应该考虑使用 UDT。假如类型没有任何与之相关的行为,则应该考虑将数据存储为表中的列。
假如发现需要实现相关函数的库,则 UDT 中的静态方法是一种可以方便地创建这种库的封装机制。可以通过使用 :: 语法来调用T-SQL 中的静态方法,如下所示:
select imagetype::MyFunction(@arg1)
实际方案
客户希望在 UmAlQuraCalendar 中存储日期时间值,这与 SQL Server 日期时间数据类型使用的 Gregorian 日历不同。他们想让这个日期类型具有相同的基本行为集,即字符串转换、日期部分、日期算法和 GetDate()。
下面是这一数据类型的代码片段。它使用 UmAlQuraCalendar 2.0 版,这是 .NET Framework 中的新类型。
[SqlUserDefinedType(Format.Native, IsByteOrdered = true)] public struct UmAlQuraDateTime : INullable { //Private state private long dtTicks; //Calendar object used for all calendar-specific operations private static readonly UmAlQuraCalendar s_calendar = new UmAlQuraCalendar(); public static UmAlQuraDateTime Null { get { UmAlQuraDateTime dt = new UmAlQuraDateTime(); dt.isNull = true; return dt; } } public bool IsNull { get { return this.isNull; } } //Convert into CLR DateTime type public DateTime DateTime { get { return new DateTime(this.dtTicks); } }
字符串转换和创建新值:
public static UmAlQuraDateTime Parse(SqlString s) ... public override string ToString() ... public static UmAlQuraDateTime ParseUsingFormat(string data, string fmt) ... public string ToStringUsingFormat(string format) ... public static UmAlQuraDateTime Now { get { return new UmAlQuraDateTime(DateTime.Now); } } public static UmAlQuraDateTime FromSqlDateTime(SqlDateTime d) { if (d.IsNull) return Null; return new UmAlQuraDateTime(d.Value); }
日期部分类似功能:
public int Year { get { return s_calendar.GetYear(this.DateTime); } } public int Month ... public int Hour ... public int Minute ... public int Second ... public double Milliseconds ... public long Ticks ... public SqlDateTime ToSqlDateTime() { return new SqlDateTime(this.DateTime); }
DateAdd/DateDiff:
public UmAlQuraDateTime AddYears(int years) { return new UmAlQuraDateTime(s_calendar.AddYears(this.DateTime, years)); } public UmAlQuraDateTime AddDays(int days) ... public UmAlQuraDateTime AddMonths(int months) ... public UmAlQuraDateTime AddHours(int hours) ... public UmAlQuraDateTime AddMinutes(int minutes) ... public double DiffDays(UmAlQuraDateTime other) ...
下面是一些处理 T-SQL 中的类型的示例操作:
-- convert it to arabic (for testing purposes) declare @h hijridatetime set @h = hijridatetime::ParseArabicString('01/02/1400') select @h.ToArabicString(), @h.ToArabicStringUsingFormat('F') -- convert sql datetime to hijri and back declare @h hijridatetime set @h = hijridatetime::FromSqlDateTime(GetDate()) select @h.ToArabicString(), @h.ToSqlDateTime() -- get the current hijri datetime, in two ways select hijridatetime::Now.ToString(), hijridatetime::FromSqlDateTime(GetDate()).ToString() -- do some arithmetic: declare @h hijridatetime, @d datetime set @h = hijridatetime::Now -- get the current hijri datetime set @h = @h.AddDays(10) -- add ten days to it set @d = GetDate() -- current sql date set @d = DateAdd(day, 10, @d) -- add ten days to the sql datetime -- print 'em both, should be the same select @h.ToSqlDateTime(), @d -- datepart declare @h hijridatetime set @h = hijridatetime::Now -- get the current hijri datetime select @h.Year as year, @h.Month as month, @h.Day as month
小结
该篇讲述了使用 SQL Server 2005 中的 CLR 集成功能的指导原则、具体使用情况和示例。数据库应用程序开发人员和架构师应该结合集中讨论 SQL Server 2005 中的其他功能区(如 TransacT-SQL、XML 和 Service Broker)的指导原则的其他文章来阅读本文。
将来,我们计划在这一领域提供更多白皮书,讲解 CLR 集成的可管理性、监控和故障排除方面的指导原则。
版权这是一个预备文档,在此处描述的软件的最终商业版本发布之前可能对该文档进行补充性更改。本文档中包含的信息代表 Microsoft Corporation 在发布时对所讨论问题的最新观点。由于 Microsoft 必须适应不断变化的市场情况,因此,这些信息并非 方面所作的承诺,Microsoft 也不能保证出版日之后提供的任何信息都完全正确。
本白皮书仅供参考。MICROSOFT 对本文档中的有关信息不作任何明示或暗示的担保。
用户有责任遵从所有适用的版权法。除了版权法所赋予的权利以外,未经 Microsoft Corporation 明确书面许可,不得擅自将本文档的任何部分进行复制、存储在或输入可检索系统,或以任何形式或方式(电子、机械、影印、录制或其他方式)进行传播,或用作其他目的。
Microsoft 对本文档中的主题持有专利权、专利申请权、商标权、版权或其他相关的知识产权。Microsoft 只提供在任何书面许可协议中明确规定的权利,而不授予您本文档的上述专利权、商标权、版权或其他知识产权。
除非另有声明,否则本文中提到的示例公司、组织、产品、域名、电子邮件地址、徽标、人物、地点和事件都是虚构的,与任何真实的公司、组织、产品、域名、电子邮件地址、徽标、人物、地点或事件无任何关联,也不应该被推断为有任何关联。
© 2004 Microsoft Corporation. All rights reserved.
Microsoft、Visual C++ 和 Visual C# 是 Microsoft Corporation 在美国和/或其他国家/地区的注册商标或商标。
此处提到的真实的公司和产品名称是其各自所有者的商标。
一、简介
Microsoft 通过宿主 Microsoft .NET Framework 2.0 公共语言运行库 (CLR),SQL Server 2005显著地增强了数据库编程模型的功能。它支持用任何 CLR 语言(特别是 Microsoft Visual C# .NET、Microsoft Visual Basic .NET 和 Microsoft Visual C++)编写过程、触发器和函数。同时,它还提供了扩展数据库系统中的类型系统和聚合函数的功能,方法是允许应用程序创建它们自己的数据类型和聚合函数。
本白皮书从数据库应用程序开发人员的角度描述了如何最有效地利用这项技术。它还将 CLR 与 SQL Server 中支持的现有编程语言(即 TransacT-SQL (T-SQL) 和扩展存储过程 (XP))进行了比较。本白皮书不打算提供这些功能的基本参考资料,SQL Server 2005 Beta 2 Books Online 对其进行了详细介绍。
本白皮书的目标读者包括数据库应用程序开发人员、架构师和数据库管理员。本白皮书假设读者熟悉基于 .NET Framework 的编程和数据库编程。
二、CLR 集成概述下面简要概述了 CLR 集成执行的 SQL Server 功能,以及 Visual Studio 2005 最终发布版本如何支持这些功能。
注册和执行数据库中的托管代码包括以下步骤:
• |
开发人员将托管程序编写为一组类定义。将 SQL Server 内旨在用作存储过程、函数或触发器(下面统称为例程)的代码编写为类的 static(或 Microsoft Visual Basic .NET 中的 shared)方法。将旨在用作用户定义的类型和聚合的代码编写为一个整类。开发人员编译该程序并创建一个程序集。 |
• |
然后,将此程序集上载到 SQL Server 数据库,在其中使用 CREATE ASSEMBLY 数据定义语言 (DDL) 将它存储到系统目录。 |
• |
接着,创建 TransacT-SQL 对象,例如,例程(函数、过程和触发器)、类型和聚合,并将其绑定到已经上载的程序集中的入口点(对例程来说是方法,对类型和聚合来说是类)。使用 CREATE PROCEDURE/FUNCTION/ TRIGGER/TYPE/AGGREGATE 语句来完成这一步。 |
• |
在创建了这些例程之后,应用程序就可以像使用 T-SQL 例程一样使用它们。例如,可以从 T-SQL 查询中调用 CLR 函数,从客户端应用程序或从 T-SQL 批处理中调用 CLR 过程,就好像它们是 T-SQL 过程一样。 |
Visual Studio 2005 Beta 1 支持在 SQL Server 2005 中开发、部署和调试托管代码。有一种新的项目类型(称为 SQL Server 项目),它允许开发人员在 SQL Server 中开发、部署和调试例程(函数、过程和触发器)、类型和聚合。
构建和部署
SQL Server 项目提供了代码模板,这使得开发人员能够轻松地开始为基于 CLR 的数据库例程、类型和聚合编写代码。该项目还允许添加对数据库中其他的程序集的引用。在构建项目时,可以将其编译成一个程序集。部署此程序集可以将程序集的二进制文件上载到与该项目相关联的 SQL Server 数据库中。部署操作还自动创建在数据库的程序集中定义的例程、类型和聚合,方法是使用在代码中定义的自定义属性(SqlProcedure、SqlFunction 和 SqlTrigger 等等)。它还上载与该程序集相关联的源代码和 .pdb 文件(调试符号)。
调试
对于任何平台来说,调试都是开发人员体验的基本部分。SQL Server 2005 和 Visual Studio 2005 为数据库编程人员提供了这些功能。调试 SQL Server 2005 对象的关键部分在于其易于安装和使用。调试到运行 SQL Server 的计算机的连接在很大程度上同调试传统操作系统中运行的进程的方式一样。调试器的功能不会受到客户端到服务器的连接类型的影响。这样就可以调试表格数据流 (TDS) 和 HTTP 连接。而且,还可以跨语言进行无缝调试。因此,假如有一个调用 CLR 存储过程的 T-SQL 存储过程,调试会允许您从 T-SQL 过程进入到 CLR 过程。
三、CLR 与 TransacT-SQL现在我们进入本文的关键部分:对 CLR 集成和 SQL Server 中支持的现有编程语言进行比较。
TransacT-SQL (T-SQL) 是 SQL Server 支持的本机编程语言。和大多数 SQL Server 版本一样,它包含数据处理功能和数据定义功能。数据处理功能一般可以分为两类:查询语言(由 SELECT/INSERT/UPDATE/ DELETE 语句组成)和过程语言(WHILE、赋值、触发器、光标等)。一般来说,SQL Server 中的 CLR 支持为过程语言提供了 T-SQL 的替代方法。
即使在 SQL Server 中引入 CLR 支持以前,数据库应用程序应该尽可能多地使用查询语言,这始终被认为是很重要的。数据库应用程序应该利用面向集的查询处理器,并且只在查询语言无法表示逻辑时才转向过程编程。对于 SQL Server 中的 CLR 支持,这仍然是正确的。不应该使用 CLR 来编写可以用简单的 SELECT 语句表示的过程代码。在 SQL Server 2005 中增加了许多重要的功能来提高T-SQL 查询语言的表达能力。
• |
递归查询:遍历表中的递归层次的能力 |
• |
分析函数:RANK 和 ROW_NUMBER 允许排列结果集中的行 |
• |
新的关联操作:APPLY、PIVOT 和 UNPIVOT |
试图使用 CLR 功能的开发人员应该确保他们充分地利用查询语言,包括 SQL Server 2005 中的扩展在内。对于在查询语言中无法以声明方式表示的逻辑,他们应该考虑将 CLR 作为有效的替代办法。
现在让我们看一些方案,其中基于 CLR 的编程能够补充 T-SQL 查询语言的表达能力。通常,需要在查询(可称为函数)内嵌入过程逻辑。这允许许多方案,例如:
• |
根据数据库表中存储的值,对每行进行复杂的计算(必须用过程逻辑来表示)。这可以包括将这些计算的结果发送给客户端,或者使用计算来过滤发送给客户端的行集,如以下示例中所示: SELECT <complex-calculation>(<column-name>,...) FROM <table> WHERE <complex-calculation>(<column-name>,...) = ... |
• |
• 使用过程逻辑来评估表格结果,然后在 SELECT 或 DML语句的 FROM 子句中进行查询。 |
SQL Server 2000 引入了支持这些方案的 T-SQL 函数(标量和表值)。有了 SQL Server 2005,就可以用 CLR 语言更容易地编写这样的函数,并且会极大地提高它们的性能。之所以编写这些函数非常容易,是因为事实上编写 CLR 代码的开发人员可以利用 .NET Framework API中存在的大量有用函数和类。这个类/函数库比 TransacT-SQL 中支持的内置函数要丰富得多。此外,CLR 编程语言提供了 T-SQL 中所没有的丰富构造(例如数组和列表等)。与 T-SQL(它是一种解释语言)相比,CLR 编程语言之所以具有更好的性能,是因为托管代码是已编译的。对于涉及算术计算、字符串处理、条件逻辑等的操作,托管代码的性能可能要优于 T-SQL 一个数量级。
注:对于函数,几乎没有必要从函数中访问数据库。外部查询已经从数据库中检索到数据值,并且将其作为参数传递给函数。这是 CLR 的优势,在计算性任务上比 T-SQL 更胜一筹。
从 CLR 中访问数据
现在让我们从编程模型和性能这两个方面,看看如何用 CLR 来编写访问数据库的业务逻辑。
编程模型
使用 T-SQL,只是在过程代码内部嵌入查询语言语句 (SELECT/INSERT/UPDATE/ DELETE)。通过托管代码,可以使用 SQL Server 托管提供程序来实现 Microsoft ADO.NET 数据访问 API(也称为 in-proc ADO.NET)。使用这种方法,可以将查询语言语句(SELECT 和 DML 语句)作为动态字符串嵌入,并且将其作为参数传递给 ADO.NET API。与 T-SQL 的静态方法相比,过程代码内嵌入的 SQL 语句的基于动态 API 的特性是它们在编程模型上的主要区别。不利的是,in-proc ADO.NET 模型会产生比 T-SQL 更冗长的代码。此外,因为 SQL 语句是动态字符串,所以在执行之前不在语法或语义上进行编译和验证。有利的是,带有 ADO.NET 的数据库编程模型与客户端或中间层中使用的模型相似,因而更容易在各层之间移动代码和利用现有的技术。
此外,在基于 T-SQL 和基于 CLR 的编程模型中使用的都是同一 SQL 查询语言,不同之处在于过程部分,注意到这一点是非常重要的。
性能
正如已经提到的,在谈及过程代码、计算等方面时,与 T-SQL 相比,托管代码在性能方面具有决定性的优势。然而,对于数据访问方面,T-SQL 在性能方面通常会更好。因此,通用规则是用 CLR 编写计算和逻辑密集的代码要比数据访问密集的代码好。不过这值得更详细地考虑。
让我们看看数据访问编程中的一些典型的基元和模式,以及在这些情况下如何使用 ADO.NET 进行 T-SQL 和托管编程。
将结果发送到客户端
这包括将一组行发送到客户端,而没有在服务器中“消费”它们(即没有在例程内导航行)。使用 T-SQL,只需在 T-SQL proc 中嵌入一个 SELECT 语句就可以将 SELECT 产生的行发送到客户端。通过托管代码,可以使用 SqlPipe 对象将结果发送到客户端。T-SQL 和 in-proc ADO.NET 平台在这种情况下的作用是一样的。
提交 SQL 语句
这包括来自过程代码的 SQL 语句的执行往返。在这种情况下,T-SQL 具有很大的优势(比 in-proc ADO.NET 快两倍多)。
此处需要重点注意的是,之所以在 CLR 中出现性能降低,是因为增加了额外的代码层,包括将来自托管代码的 T-SQL 语句提交给原生 SQL 代码。在将该语句提交给查询处理器之后,基于语句源(T-SQL 或 in-proc ADO.NET)的性能方面就没有什么不同了。
注:典型的数据访问密集的存储过程可能涉及提交一系列的 SQL 语句。假如 SQL 语句简单,并且不需要花费大量的时间执行,则来自托管代码的调用开销可能占用大部分执行时间,这样的过程用 T-SQL 编写将执行得更好。
只进、只读行导航
这包括以只进、只读方式一次导航一个由 SQL 语句产生的行。在 T-SQL 中,这是通过只进、只读光标实现的。在 CLR 中,这是通过 SqlDataReader 实现的。通常,每一条语句都涉及一些处理。假如忽略了与每行相关联的处理,则导航行在 CLR 中就比在 T-SQL 光标中稍慢。然而,假如您关心为每行执行的处理,则 CLR 会更有优势,因为 CLR 在这种处理上比 T-SQL 做得好。
带有更新的行导航
假如需要根据光标中的当前位置更新行,则没有相关的性能比较,因为 in-proc ADO.NET 不支持此功能,而应该通过 T-SQL 可更新光标来进行此操作。
注 在任何可能的情况下,最好使用 UPDATE 语句来批量更新行,只有在这样的修改无法用单一的 UPDATE 语句进行表示时,才应使用基于光标导航的 UPDATE。
以下示例说明在特定情况下如何确定 T-SQL 和 CLR 将执行:
1. |
考虑这样一个过程,它执行一系列(或在最简单的情况下仅一个)INSERT/UPDATE/DELETE/SELECT 语句,带有几个或者不带返回到客户端的行,并且不导航 SELECT 产生的行。假如将这样的过程编写成 T-SQL 过程可能执行得更好。 |
2. |
考虑这样一个过程,它执行单一的 SELECT 语句,并且使用存储过程内的行,方法是,一次导航一行并进行一些不涉及对每行进行更多的数据访问的处理。这个过程可能在带有 in-proc ADO.NET 的 CLR 中执行得更好,特别是假如每行都有一些大量处理的开销时(因为这样的操作在 CLR 中比在 T-SQL 中更加高效)。 |
下面是一些简单的指导原则,可以用来在 CLR 和 T-SQL 之间进行选择:
• |
尽可能使用带有 T-SQL SELECT、INSERT、UPDATE 和 DELETE 语句的基于集的处理。只有在无法使用基于集的 DML 语句之一表示逻辑时,才应该使用过程和基于行的处理。 |
• |
假如过程仅仅是一个通过封装基本 INSERT/UPDATE/DELETE/SELECT 操作访问基表的包装,则应该用 T-SQL 进行编写。 |
• |
假如过程主要包括结果集中的只进、只读行导航,以及一些涉及每行的处理,则用 CLR 编写可能更有效。 |
• |
假如过程包括大量的数据访问以及计算和逻辑,则可以考虑将过程代码分隔为 CLR 来调用 T-SQL 过程,以进行大部分的数据访问(反之亦然)。另一个替代方法是,使用单一的 T-SQL 批处理,它包括从托管代码执行一次的一组查询,以减少从托管代码提交 T-SQL 语句的往返次数。 |
后面的部分将更深入地讨论在处理结果集时何时及如何适当地使用 T-SQL 和 CLR。
CLR 与 XP在 SQL Server 以前的版本中,扩展存储过程 (XP) 为数据库程序开发人员提供了唯一可用的机制来编写服务器端逻辑,这要么难于表示,要么不可能用 T-SQL 编写。CLR 集成提供了一种更健壮的替代方法来编写这种存储过程。此外,使用 CLR 集成,过去以存储过程形式编写的逻辑通常可以更好地表示为表值函数,因为它们允许它们允许将该函数构造的结果放在 SELECT 语句中进行查询(通过将这些结果嵌入到 FROM 子句中)。
以下是使用 CLR 过程或函数与 XP 相比的优势:
• |
粒度控制:很少可以控制 XP 能做什么或者不能做什么。使用代码访问安全模型,SQL Server 管理员可以分配三种权限之一:SAFE、EXTERNAL_ACCESS 或 UNSAFE,从而对托管代码允许进行的操作集进行不同程序的控制。 |
• |
可靠性:托管代码(特别是在 SAFE 和 EXTERNAL_ACCESS 权限集中)提供了比 XP 更安全、更可靠的编程模型。可验证的托管代码确保了所有对对象的访问都是通过强类型化的接口实现的,从而降低了程序访问或破坏属于 SQL Server 的内存缓冲的可能性。 |
• |
数据访问:使用 XP£¬编程人员必须向后显式连接到数据库(称为回环),以访问本地 SQL Server 数据库。而且,必须将此回环连接显式绑定到原来的会话事务上下文,以确保 XP 可以参与到调用它的同一个事务中。通过托管代码,可以使用更自然和更有效的编程模型来访问本地数据,这些模型利用当前的连接和事务上下文。 |
• |
性能:System.Data.SqlServer API 允许托管过程将结果集发送回客户端,其性能比 XP 使用的开放式数据服务 (ODS) API 更好。此外,System.Data.SqlServer API 支持新的数据类型(如 SQL Server 2005 中引入的 XML、(n)varchar(max)、varbinary(max)),而没有扩展 ODS API 来支持这些新的数据类型。 |
• |
可伸缩性:通过托管代码,SQL Server 可以管理资源(如内存、线程和同步)的使用,因为公开这些资源的托管 API 是在 SQL Server 资源管理器上实现的。相反,SQL Server 不能查看或控制 XP 的资源使用情况。举例来说,假如 XP 消耗了太多的 CPU 或内存资源,就没有办法使用 SQL Server 来检测或控制。然而,通过托管代码,SQL Server 可以检测到特定线程在一段很长的时间内一直没有退出,然后就强制该任务退出,这样其他工作可以按计划进行。因此,使用托管代码提供了更好的可伸缩性和健壮性。 |
正如上面所提到的,在数据访问和发送结果集给客户端方面,CLR 过程比 XP 做得更好。对于不包括数据访问和发送结果的代码,比较 XP 和托管代码的性能就是比较托管代码和原生代码的性能。一般来说,在这些情况下托管代码比不上原生代码的性能。而且,当在 SQL Server 内运行时,从托管代码到原生代码的事务处理过程有额外的开销,因为在移动到原生代码和从原生代码移回时,SQL Server 需要对特定于线程的设置进行额外的登记-保留。因此,对于在托管代码和原生代码之间有频繁的事务处理的情况,XP 大大地胜过在 SQL Server 内部运行的托管代码。
对于大多数扩展过程,假如考虑数据访问和结果发送的可伸缩性、可靠性和性能优势,CLR 过程提供了更好的替代方法。对于性能主要是由处理(数据访问和结果发送之外的)和频繁的托管-原生转换决定的情况,应该权衡 CLR 的可伸缩性和可靠性的优势与 XP 的原始性能优势。
四、代码位置:数据库与中间层
通过在数据库中提供丰富的编程模型,CLR 集成提供了将逻辑从其他层移动到数据库层的选择。然而,这显然并不意味着所有或大部分逻辑应该移到数据库中。
将逻辑移到数据库层可以减少网络中的数据流量,但是增加了服务器上宝贵的 CPU 资源的负荷。因此,在应用程序中做出代码放置的决定之前,要慎重权衡。以下注意事项适用于将数据库层作为首选的代码位置:
• |
数据验证:在数据层进行数据验证的逻辑可以更好地将数据和逻辑封装在一起。这样避免了在不同数据接触点(如:后端处理、批量上载和来自中间层的数据更新等)中重复验证逻辑。 |
• |
减少网络流量:对于需要处理大量的数据而产生很少的数据的数据处理任务(如数据分析应用程序中的需求预测、基于需求预测的生产安排等)来说,将逻辑放在数据库层中是合适的。 |
注即使在引入 CLR 支持之前,上面的注意事项也是有效的。数据库层中的 CLR 支持意味着编程语言的选择没有妨碍代码位置的正确选择。
示例:生产安排
生产安排是制造企业的常见任务。在高层次上,它包括制订何时生产多少单位数量的产品的计划,以便能够满足需求、最大程度的降低库存成本,同时将生产成本降到最低。有几个算法将需求预测、库存成本和生产线安装成本作为输入,而将制造策略作为输出。
假定将来的需求预测存储在 SQL Server 表中,则此类算法的实现有以下特征:
1. |
使用大量的数据作为输入(如需求预测)。 |
2. |
产生小结果(如在特定的日期内生产的单位数量)。 |
3. |
需要相当多的计算以便从输入中派生输出。 |
在中间层实现这样的算法是可行的,但是在数据库外移动输入集方面有性能损失。在 T-SQL 中将其实现为存储过程也是可行的,但是因为需要复杂的计算,性能损失就显现出来了。性能特征将随着实际的数据量和算法的复杂性的不同而不同。
为了验证 CLR 集成是否适合于这样的情况,我们举一个特定的生产安排算法的示例 - Wagner-Whitin 算法的动态编程实现。正如所预料的,CLR 集成优于 T-SQL。对于这种情况,使用托管代码还有其他好处。这种算法的实现需要使用大量的一维和多维数组、数据结构,而这些在 T-SQL 中是不可用的。总之,CLR 集成的性能要优于 T-SQL 实现几个数量级。
假定以下简单的数据库架构跟踪可以生产的产品列表。
表 1: t_products | |||
列名 | 类型 | 是否为空 | 说明 |
PID |
int |
非空 |
产品主键 ID |
Pname |
nvarchar(256) |
空 |
产品名称 |
InventoryCost |
int |
非空 |
存储该产品的每时段成本 |
StartupCost |
int |
非空 |
建立生产线来生产该产品的成本 |
下表存储了每周每个产品的需求预测信息。
表 2: t_SalesForecast | |||
列名 | 类型 | 是否为空 | 说明 |
PID |
Int |
非空 |
产品 ID |
WeekDate |
smalldatetime |
非空 |
需求预测周 |
DemandQty |
int |
非空 |
特定产品和特定周的需求预测 |
给定一组产品,它们的库存和启动成本以及未来需求预测,我们创建了接受如下输入参数的存储过程:1)制订生产进度表的日期,2)按进度表生产所需要的周数。
存储过程返回带有下表中的架构的行集。
表 3:存储过程架构 | ||
列名 | 类型 | 说明 |
Product |
nvarchar(256) |
产品名称 |
Period |
datetime |
进度周 |
Quantity |
int |
在指定周内制造的产品的数量 |
将 C# 版本的代码复制到下面的代码中,以说明这种可以从 CLR 集成中大大获益的情况:
using System; using System.Data; using System.Data.Sql; using System.Data.SqlServer; using System.Data.SqlTypes; public class ProductionSchedule { //4-year limit on scheduling public const int MAXPRODUCTS = 101; public const int MAXWEEKS = 210; public const int MAXNAME = 256; public ProductionSchedule() { } public static int Schedule(SqlDateTime startDate, int numWeeks) { SqlDateTime[] week = new SqlDateTime[MAXWEEKS]; int[] quantity; int[][] Cij; int[] Fk; int[] minK = new int[MAXWEEKS]; int product_id, current_product, product_count = 0; int startPeriod; // We'll use arrays to keep state about products and forecasts in memory. This is only viable given that we know we have a small number of products and weeks. // For larger data sets, we would have to consider cursors or temporary tables. // stored as CLR types since we know they can't be null int[] h = new int[MAXPRODUCTS]; int[] K = new int[MAXPRODUCTS]; // stored as nullable SqlChars since the table schema allows for null names SqlChars[] productNames = new SqlChars[MAXPRODUCTS]; bool moreProducts = true; int optimal_j; int period; int sum; SqlPipe pipe = SqlContext.GetPipe(); SqlDataRecord record; object[] values = new object[3]; SqlMetaData[] metadata = new SqlMetaData[3]; //Initialize algorithm arrays Cij = new int[MAXWEEKS][]; for( int l=0;l<MAXWEEKS;l++) Cij[l] = new int[MAXWEEKS]; Fk = new int[MAXWEEKS]; //Look up K and h for all products SqlCommand cmd = SqlContext.GetCommand(); cmd.CommandText = @"SELECT pname, InventoryCost, StartupCost from dbo.t_Products ORDER BY PID"; SqlDataReader reader = cmd.ExecuteReader(); while(reader.Read()) { productNames[product_count] = reader.GetSqlChars(0); //product name h[product_count] = reader.GetInt32(1); //holding cost K[product_count] = reader.GetInt32(2); //startup cost product_count++; // if we exceeded number of expected products then bail out with an exception if (product_count >= MAXPRODUCTS) { throw new Exception("Too many products"); } } reader.Close(); product_count = 0; //Get the list of product ids; cmd = SqlContext.GetCommand(); cmd.CommandText = @"select PID, weekdate, DemandQty from dbo.t_SalesForecast ORDER BY PID, WeekDate"; reader = cmd.ExecuteReader(); moreProducts=reader.Read(); //Set up the record for returning results metadata[0] = new SqlMetaData( "Product", SqlDbType.NVarChar,MAXNAME ); metadata[1] = new SqlMetaData( "Period", SqlDbType.DateTime ); metadata[2] = new SqlMetaData( "Quantity", SqlDbType.Int ); record = new SqlDataRecord( metadata ); while( moreProducts ) { product_id = current_product = reader.GetInt32(0); int index = 1; quantity = new int[MAXWEEKS]; while( current_product == product_id ) { week[index] = reader.GetSqlDateTime(1); quantity[index] = reader.GetInt32(2); index++; moreProducts = reader.Read(); if( !moreProducts ) break; current_product = reader.GetInt32(0); } //Determine the ordinal start week startPeriod = 1; //For each product ID calculate Cij for( int i = startPeriod; i < (startPeriod + numWeeks); i++ ) { for( int j = i+1; j <= (startPeriod + numWeeks+1); j++ ) { Cij[i][j] = GetCij(quantity,i,j,K [product_count],h[product_count]); } } //Calculate Fk for( int k = startPeriod + numWeeks + 1; k >= startPeriod; k--) { minK[k] = GetFk_SO(k,startPeriod + numWeeks,Cij,Fk); } //Send the results record.SetSqlChars(0,productNames[product_count]); pipe.SendResultsStart(record,false); for( int k = startPeriod; k < startPeriod + numWeeks; ) { period = k; optimal_j = minK[k]; sum = 0; while( k < optimal_j ) { sum = sum + quantity[k++]; } values[1] = week[period]; record.SetValue(1,values[1]); values[2] = sum; record.SetValue(2,values[2]); pipe.SendResultsRow(record); } pipe.SendResultsEnd(); product_count++; } reader.Close(); return 0; } private static int GetCij(int[] quantities, int i, int j, int K, int h) { if( j == i+1 ) return K; else return (j-1-i) * h * quantities[j-1] + GetCij(quantities, i, j-1,K,h); } private static int GetFk_SO(int k,int n,int[][] Cij, int[] Fk) { int j,min; j = k+1; min = j; if ( k == n+1 ) { Fk[k] = 0; return j; } Fk[k] = Cij[k][j] + Fk[j]; for(; k <= n ;k++) { j = k + 1; while( j <= n+1 ) { if( Cij[k][j] + Fk[j] < Fk[k] ) { min = j; Fk[k] = Cij[k][j] + Fk[j]; } j++; } } return min; } }五、处理常见数据库编程任务和问题
前一节在高层次上对基于 CLR 的编程与 T-SQL、中间层和扩展存储过程 (XP) 进行了比较。在这一节中,我们将考虑数据库应用程序开发人员经常遇到的一些编程任务和模型,并且讨论如何使用 CLR(以及在一些情况下如何不使用)进行处理。
使用 Framework 库进行数据验证
SQL Server 2005 中的 CLR 集成允许用户利用 .NET Framework 类库提供的丰富功能来解决其数据库编程问题。
常规表达式的使用可以很好地说明 CLR 集成如何增强了验证和过滤功能。在处理数据库中存储的文本数据方面,常规表达式提供的模式匹配功能比通过 T-SQL 查询语言中的 LIKE 运算符可用的模式匹配功能多。考虑以下 C# 代码,它只是 System.Text.RegularExpressions 命名空间中的 RegEx 类的一个简单包装:
using System; using System.Data.Sql; using System.Data.SqlTypes; using System.Text.RegularExpressions; public partial class StringFunctions { [SqlFunction(IsDeterministic = true, IsPrecise = true)] public static bool RegExMatch(string pattern, string matchString) { Regex r1 = new Regex(pattern.TrimEnd(null)); return r1.Match(matchString.TrimEnd(null)).Success; } [SqlFunction(IsDeterministic = true, IsPrecise = true)] public static SqlString ExtractAreaCode(string matchString) { Regex r1 = new Regex("\\(( <ac>[1-9][0-9][0-9])\\)"); Match m = r1.Match(matchString); if (m.Success) return m.Value.Substring(1, 3); else return SqlString.Null; } };
假设 StringFunctions.RegExMatch 和 StringFunctions.ExtractAreaCode 方法已经被注册为带有 RETURNS NULL ON NULL INPUT 选项的数据库中的用户定义函数(这允许该函数在任何输入都为 NULL 时返回 NULL,这样在该函数内就没有特殊的 NULL 处理代码):
现在,可以在使用上述代码的表的列中定义约束,以验证电子邮件地址和电话号码,如下所示:
create table Contacts ( FirstName nvarchar(30), LastName nvarchar(30), EmailAddress nvarchar(30) CHECK (dbo.RegExMatch('[a-zA-Z0-9_\-]+@([a-zA-Z0-9_\-]+\.)+(com|org|edu)', EmailAddress) = 1), USPhoneNo nvarchar(30) CHECK (dbo.RegExMatch('\([1-9][0-9][0-9]\) [0-9][0-9][0-9]\-[0-9][0-9][0-9][0-9]', UsPhoneNo)=1), AreaCode AS dbo.ExtractAreaCode(UsPhoneNo) PERSISTED )
另外,请注意 AreaCode 列是使用 dbo.ExtractAreaCode 函数从 USPhoneNo 列中取出地区代码而得到的列。然后,可以对 AreaCode 列建立索引,这样便于在表格中根据特定地区代码查找联系人的查询。
更一般地讲,此示例演示了如何利用 .NET Framework 库来增强带有有用函数的 T-SQL 内置函数库,这些有用函数很难用 T-SQL 表达。
产生结果集
需要从运行在服务器内的数据库对象(如存储过程或视图)中产生结果集可能是最常见的数据库编程任务之一。假如可以使用单个查询(SELECT 语句)来构建结果集,则这只需使用视图或在线表值函数即可实现。然而,假如需要多个语句(过程逻辑)来构建结果集,则有两个选择:存储过程和表值函数。虽然 SQL Server 2005 有表值函数,但是它们只能用 T-SQL 进行编写。在 SQL Server 2005 中,通过 CLR 集成,还可以使用托管语言来编写这样的函数。在这一节中,我们将讨论如何决定使用存储过程还是使用表值函数,以及使用 T-SQL 还是使用 CLR。
从 T -SQL 过程可以将相关的结果作为表值函数的返回值返回,或者通过存储过程内曾经隐式存在的“调用者管道”返回。从存储过程的任何位置(不管执行的嵌套程度如何)执行 SELECT 语句都会把结果返回给调用者。更严格地讲,实际上 SELECT 语句并没有进行变量赋值。而且,FETCH、READTEXT、PRINT 和 RAISERROR 语句也隐式地将结果返回给调用者。
请注意,“调用者”一直没有正确地定义,它实际上取决于存储过程的调用上下文。
假如从任何客户端数据访问 API(如 ODBC、OLEDB 和 SQLClient)中调用存储过程,则调用者是实际的 API,并且它提供的任何一种抽象都可以表示结果(如 hstmt、IRowset 或 SqlDataReaderand)。这意味着,通常,从存储过程中产生的结果将始终返回到调用 API 中,而跳过堆栈中所有的 T-SQL 框架,如以下示例中所示:
create proc proc1 as select col1 from dbo.table1; create proc proc2 as exec proc1;
在执行过程 proc2 时,proc1 产生的结果将转到 proc2 的调用者。proc2 中只有一种方法可以捕获产生的结果,即通过使用 INSERT/EXEC 将其存储到永久表、临时表或表变量中,从而将结果流式处理到磁盘。
create proc proc2 as declare @t table(col1 int); insert @t (col1) exec proc1; -- do something with results
在使用 INSERT/EXEC的情况下,“调用者”是 INSERT 语句的目标表/视图。
SQL Server 2005 CLR 存储过程引入了新的“调用者”类型。当通过托管框架中的 in-proc 提供程序执行查询时,就可以通过 SqlDataReader 对象使结果可用,并且可以在存储过程中使用结果。
... SqlCommand cmd=SqlContext.GetCommand(); cmd.CommandText= "select col1 from dbo.table1"; SqlDataReader sdr=cmd.ExecuteReader(); while (sdr.Read()) { // do something with current row } ...
下面的问题是托管存储过程如何将结果返回给它的调用者而不是通过 SqlDataReader 来使用它。这可以通过称为 SqlPipe 的新类来实现。通过 SqlContext 类的静态方法可以使此类的实例对托管存储过程可用。SqlPipe 有几种方法可以将结果返回给存储过程的调用者。这两个类都是在 Sqlaccess.dll 中定义的。
SqlPipe
在 SqlPipe 类中可以使用的方法中,最容易理解的就是 Execute 方法,它将命令对象作为参数接受。这个方法主要执行命令,并且没有使执行的结果可用于托管框架,而是将结果发送给存储过程的调用者。发送结果的这种形式在语义上与将语句嵌入 T-SQL 存储过程内是一样的。在本文前面描述的性能方面,SqlPipe.Execute 与 T-SQL 是等价的。
create proc proc1 as select col1 from dbo.table1; The equivalent in C# would be: public static void proc1() { System.Data.SqlServer.SqlCommand cmd=SqlContext.GetCommand(); cmd.CommandText= "select col1 from dbo.table1"; SqlContext.GetPipe().Execute(cmd); }
对于返回的数据是由执行的查询直接产生的情况,SqlPipe.Execute 可以很好地工作。然而,在某些情况下可能希望1)从数据库中获得结果,进行操作或者转换,然后发送它们,或者 2)将结果发送回原地而不是本地 SQL Server 实例。
SqlPipe 提供了一组可以协同工作以使应用程序可以将任何结果返回给调用者的方法:SendResultsStart、SendResultsRow 和 SendResultsEnd。在很大程度上,这些 API 类似于对扩展存储过程的开发人员可用的 srv_describe 和 srv_sendrow API。
SendResultsStart 将 SqlDataRecord 作为参数接受,并且指示返回的新结果集的开头。该 API 从记录对象读取元数据信息,并且将其发送给调用者。该方法有重载,以允许发送元数据以及记录中的实际值。
随后可以返回行,方法是对要发送的每行调用一次 SendResultsRowows。在发送完全部所需的行之后,需要调用 SendResultsEnd 来指示结果集的结尾。
例如,下面的 C# 代码片段表示一个存储过程,它读取 XML 文档(来自 MSDN 的 Really Simple Syndication [RSS] 供给),使用 System.Xml 类进行解析,并且以相关的形式返回信息。请注意,这些代码应该创建为 EXTERNAL_ACCESS(或 UNSAFE)程序集,因为访问 Internet 所需的代码访问安全 (CAS) 权限只有在这些权限集中才是可用的。
// Retrieve the RSS feed XPathDocument doc = new XPathDocument("http://msdn.microsoft.com/sql/rss.xml"); XPathNavigator nav = doc.CreateNavigator(); XPathNodeIterator i = nav.Select("//item"); // create metadata for four columns // three of them are string types and one of the is a datetime SqlMetaData[] rss_results = new SqlMetaData[4]; rss_results[0] = new SqlMetaData("Title", SqlDbType.NVarChar, 250); rss_results[1] = new SqlMetaData("Publication Date", SqlDbType.DateTime); rss_results[2] = new SqlMetaData("Description", SqlDbType.NVarChar, 2000); rss_results[3] = new SqlMetaData("Link", SqlDbType.NVarChar, 1000); // construct the record which holds metadata and data buffers SqlDataRecord record = new SqlDataRecord(rss_results); // cache a SqlPipe instance to avoid repeated calls to SqlContext.GetPipe() SqlPipe sqlpipe = SqlContext.GetPipe(); // send the metadata, do not send the values in the data record sqlpipe.SendResultsStart(record, false); // for each xml node returned, extract four pieces // of information and send back each item as a row while (i.MoveNext()) { record.SetString(0, (string) i.Current.Evaluate("string(title[1]/text())")); record.SetDateTime(1, DateTime.Parse((string) i.Current.Evaluate("string(pubDate[1]/text())"))); record.SetString(2, (string) i.Current.Evaluate("string(description[1]/text())")); record.SetString(3, (string) i.Current.Evaluate("string(link[1]/text())")); sqlpipe.SendResultsRow(record); } // signal end of results sqlpipe.SendResultsEnd();
注:在 SendResultsStart 和 SendResultsEnd 调用之间,SqlPipe 被设置为繁忙状态,调用除 SendResultsRow 之外的任何 Send 方法都会导致错误发生。SqlPipe 处于繁忙状态中时,SendingResults 属性被设置为 TRUE。
表值函数
CLR 集成也启用了对用托管语言编写的表值函数 (TVF) 的支持。与 T-SQL 相似,TVF 主要用于返回表结果。最显著的不同在于,T-SQL 表值函数临时将结果存储在工作表中,而 CLR TVF 则能够对产生的结果数据进行流式处理。这意味着结果在从函数返回之前不需要物化。
注 T-SQL 还具有内联 TVF 的概念,即不临时存储结果。内联 TVF 在大部分语义上便于指定子查询(可能带有参数)。
托管 TVF 返回 ISqlReader 接口,这是由 SqlClient 和 SqlServer (in-proc) 托管提供程序中的 SqlDataReader 实现的一种只进光标抽象。查询处理器调用此接口上的 Read() 方法,以在每行返回 FALSE 之前获取它。
将上面的示例改为返回来自 RSS 供给的信息,返回结果的代码如下所示(不包括未实现的方法):
[SqlFunction] public static ISqlReader tvf1() { return (ISqlReader)new RssReader(); } public class RssReader : ISqlReader { SqlMetaData[] rss_results = null; XPathDocument doc; XPathNavigator nav; XPathNodeIterator i; // Construct helper class, initializing metadata for the results // reading from the RSS feed, creating the iterator public RssReader() { rss_results = new SqlMetaData[4]; rss_results[0] = new SqlMetaData("Title", SqlDbType.NVarChar, 250); rss_results[1] = new SqlMetaData("Publication Date", SqlDbType.DateTime); rss_results[2] = new SqlMetaData("Description", SqlDbType.NVarChar, 2000); rss_results[3] = new SqlMetaData("Link", SqlDbType.NVarChar, 1000); // Retrieve the RSS feed doc = new XPathDocument("http://msdn.microsoft.com/sql/rss.xml"); nav = doc.CreateNavigator(); i = nav.Select("//item"); } // # of columns returned by the function public int FieldCount { get { return rss_results.Length; } } // metadata for each of the columns public SqlMetaData GetSqlMetaData(int FieldNo) { return rss_results[FieldNo]; } // Read method positions the navigator iterator on next element public bool Read() { return i.MoveNext(); } // methods to return each column public Object GetValue(int FieldNo) { switch (FieldNo) { case 0: return new SqlString((string) i.Current.Evaluate("string(title[1]/text())")); case 1: return new SqlDateTime(DateTime.Parse( (string)i.Current.Evaluate("string(pubDate[1]/text())"))); case 2: return new SqlString((string) i.Current.Evaluate("string(description[1]/text())")); case 3: return new SqlString((string) i.Current.Evaluate("string(link[1]/text())")); } return null; } public string GetString(int i) { return (string)GetValue(i); } public DateTime GetDateTime(int i) { return (DateTime)GetValue(i); } public SqlChars GetSqlCharsRef(int i) { return new SqlChars((SqlString)GetValue(i)); } public SqlChars GetSqlChars(int i) { return new SqlChars((SqlString)GetValue(i)); } public SqlDateTime GetSqlDateTime(int i) { return (SqlDateTime)GetValue(i); } ... }
使用来自此表值函数的结果的一种简单查询如下所示:
select title, pubdate, description, link from dbo.GetRssFeed()
显然可以用此数据的 TVF 形式来表示更丰富的查询。假定函数 CanonicalURL() 会返回规范的 URL 版本。现在,可以使用规范的 URL 很容易地返回来自 RSS 供给的数据:
select title, pubdate, description, dbo.CanonicalURL(link) from dbo.tvf1() order by pubdate
请注意,在这个示例中,我们没有利用 TVF 的流化功能,因为我们正在使用整个 RSS 供给,在此之上建立导航器,然后在调用 Read() 时循环访问各个项目。然而,可以想象使用流式 API 来使用 Web 资源的结果,并且使用 XmlReader 来循环访问产生的 XML。需要重点注意的是,给出了 CLR 表值函数和 T-SQL 函数之间的执行模型的不同时,最主要的性能差异可能有利于 CLR TVF,特别是在可能将结果数据流化的情况下。
使用哪一个?
决定将存储过程与 SqlPipe (不管是 T-SQL 中使用隐式管道还是在托管中使用显式托管类)结合使用,还是使用表值函数,取决于以下几个必须考虑的因素:
• |
可组合性要求 |
• |
返回的数据的源 |
• |
对副作用操作的需要 |
• |
强类型化和结果集的数量 |
可组合性
有时可能需要重用或进一步处理 TVF 或存储过程中产生的结果。从可组合性的角度来说,表值函数更灵活。TVF 的返回类型是相关的行集,可以用在允许此类构建的任何地方。特别是,它可以用在 SELECT 语句的 FROM 子句中,因为这些产生的结果可以受益于子查询中的 SELECT、INSERT/SELECT、派生的表和通用表表达式等的可组合性。
另一方面,从 T-SQL 语言中,存储过程可以组合成 INSERT / EXEC 组合的唯一部分,这使得可以将产生的结果存储在永久或临时表中。INSERT 操作表示数据的实际副本,它可能会影响性能。
假如需要组合和重用服务器中的结果,TVF 是更好的替代方法。假如产生的结果只需要回流到客户端/中间层,任何一种方法都可以完成这项工作。
数据源
返回的数据源是在基于 T-SQL 和基于 CLR 的实现之间做出决定的另一个重要因素。可以通过使用 in-proc 提供程序读取本地实例中的一些数据源产生结果,也可以从 SQL Server 之外的数据源产生结果。本文前面描述的基于 Web 请求的结果构造的代码片段便是后者的示例。另一个远程数据源的示例是,使用 SqlClient 托管提供程序从远程SQL Server 实例中检索结果。对于这样的外部源,基于 CLR 的实现是更好的选择,因为使用它可以很容易地实现访问外部数据的逻辑。
现在让我们考虑这种情况,使用 in-proc 提供程序基于在本地实例中执行的查询生成结果。在使用 TVF 的情况下,默认的处理可能是返回由 in-proc 提供程序产生的 SqlDataReader,或者用 ISqlReader 的自定义实现包装这样的阅读器,以便在读取结果时对其进行转换。在使用存储过程的情况下,根据本地实例产生的结果必须执行查询,循环访问读取行,对结果执行一些操作,然后通过管道将其发送回去。
然而,SQL Server 2005 不允许表值函数返回时请求还处于未决状态。在函数体可以返回之前,必须全部执行任何通过 in-proc 提供程序执行的查询并且完全使用结果。假如执行了返回语句,而 in-proc 提供程序中的 SqlDataReader 操作还处于未决状态,就会引发错误。这意味着对于从本地数据库实例返回数据的大多数情况,无法通过 CLR TVF 流化结果。假如因为其他因素(例如可组合性)需要将此编写为 TVF,则使用 T-SQL 编写是唯一的选择。另外,通过 SqlPipe 使用托管存储过程是一个可能的选择。
请注意,对于基于来自本地实例的数据从存储过程中产生结果的情况,SendResultsXXX API 的使用只有在需要对结果进行修改或处理时才有意义。假如在未作修改的情况下将结果发送给调用者,则 SqlPipe.Execute 是更好的执行解决方案。
副作用操作
一般来说,不允许用户定义的函数(特别是表值函数)执行副作用操作。其中包括改变数据库状态的操作(如 DML 语句或事务处理操作)。在产生结果前后可能需要对系统状态作一些修改。例如,业务组件可能需要设置 SAVEPOINT 事务,执行一些 UPDATE,并且通过管道返回结果;但是假如出现错误,则回滚到 SAVEPOINT。
假如不允许从 TVF 执行副作用操作,则只有存储过程才能实现这样的方案,并且必须通过 SqlPipe 返回结果。
请注意,当 SqlPipe 忙于发送结果时,尤其不允许通过 in-proc 提供程序执行副作用操作。只有在完成结果集之前或之后允许进行这些操作。
强类型化和返回的结果集的数量
从上面的代码示例和与 T-SQL 一致的角度来看,由存储过程通过 SqlPipe 产生的结果的描述不同于 TVF。TVF 是强类型化的,并且作为注册 (CREATE FUNCTION) 语句的一部分,它必须静态地定义 TVF 产生的结果的列数和类型。
另一方面,存储过程声明并没有声明产生的结果 - 甚至是否返回结果。这看起来可能很方便,虽然它确实提供了更大的灵活性,但是在编写执行存储过程的应用程序时要更加细心,因为存储过程可以动态地重定义产生的结果的形式。
因此,自然而然建议根据元数据来描述结果:假如结果的架构需要根据调用的不同而变化,则只有 SqlPipe 才能提供这种灵活性。
同样地,通过存储过程内的 SqlPipe 产生的结果的弱类型化不能将单个结果的架构扩展为可能返回可变数量的结果集。存储过程可以自由地根据条件确定是否发送给定的行集和定义其形式。这样的灵活性增加了使用这种可变的结果流的应用程序的复杂性开销。
下表总结了如何在两者之间做出决定的指导原则:
• |
存储过程(使用隐式 SqlPipe 或基于显式 CLR 的方法)和 TVF |
• |
T-SQL 和 CLR |
表 4:关于生成结果集的指导原则 | ||
是 | 否 | |
需要可组合性? |
TVF |
TVF 过程或 TVF |
外部数据源(与只访问本地数据)? |
CLR TVF 或 CLR 过程 |
(只访问本地数据)T-SQL TVF 或过程 |
需要副作用? |
过程 |
过程或 TVF |
固定的结果架构? |
过程或 TVF |
过程 |
多个结果集? |
过程 |
过程或 TVF |
流化结果的能力? |
CLR TVF |
T-SQL TVF |
对于本节的大部分内容,通过 SqlPipe 发送结果是与过程紧密相关的。即使在 CLR 触发器主体中 SqlPipe 是可用的并且返回结果是可能的,也很不提倡这种做法,因为使用在目标对象中定义的触发器发出数据处理语言 (DML) 或数据定义语言 (DDL) 语句可能会导致意外的结果。
将标量分解为行
经常需要在应用程序中传送多值参数。例如,在定单处理系统中,可能需要编写存储过程来将定单插入到 Orders 表中。存储过程中的参数之一可能是定单中的行项目。在这种情况下,您会遇到 T-SQL 限制,它不支持表值参数或缺乏集合数据类型(如数组)。解决这个问题的一种方法是,将集合编码为一个标量值(如 nvarchar 或 xml),然后将其作为参数传递给存储过程。在存储过程内,可以使用表值函数来接受标量输入,并将其转换成一组行,然后将这些行插入到 LineItems 表中。
虽然可以用 T-SQL 编写表值函数,但是用 CLR 实现它有两个好处:
• |
System.Text 命名空间中的字符串处理函数使得编写表值函数更加容易。 |
• |
CLR TVF 提供了更有效的流实现,这避免了将结果加载到工作表中。 |
下面的代码片段显示了如何实现一个表值函数,它接受以‘;’分隔的一组值作为输入字符串,并且以一组行(字符串中的每个值一行)的形式返回该字符串。请注意,MySqlReader 类的构造函数实现了大部分工作,它使用 System.String.Split 方法将输入字符串分解为数组。
// TVF that cracks a ';' separated list of strings into a result // set of 1 nvarchar(60)column called Value public static ISqlReader GetStrings(SqlString str) { return (ISqlReader)new MySqlReader(str); } public class MySqlReader : ISqlReader { private string[] m_strlist; private int m_iRow = -1; // # rows read //The core methods //Initialize list public MySqlReader(SqlString str) { //Split input string if not database NULL; //else m_strlist remains NULL if (!str.IsNull) { m_strlist = str.Value.Split(';'); } } // SECTION: Metadata related: Provide #, names, types of // result columns public int FieldCount { get { return 1; } } public SqlMetaData GetSqlMetaData(int FieldNo) { if (FieldNo==0) return new SqlMetaData("Value", SqlDbType.NVarChar, 60); else throw new NotImplementedException(); } // SECTION: Row navigation. Read is called until it returns // false. After each Read call, Get<TypeName> for each // column is called. public bool Read() { //Return empty result set if input is DB NULL //and hence m_strlist is uninitialized if (m_strlist==null) return false; m_iRow++; if (m_iRow == m_strlist.Length) return false; return true; } //Column getters //Implement Get<SqlTypeName> for each column produced by //the TVF; in this case just one. public SqlChars GetSqlChars(int i) { if (i == 0) return new SqlChars(m_strlist[m_iRow]); else throw new NotImplementedException(); } //Methods not used by SqlServer omitted; //Actual implementation should provide an empty //implementation. ... } // public class MySqlReader } // class StringFunctions;
假定 GetStrings 方法注册为具有相同名称的 TVF。下面是存储过程的代码片段,它使用此 TVF 从定单中提取表形式的行项目。
CREATE PROCEDURE Insert_Order @cust_id int, @lineitems nvarchar(8000) AS BEGIN ... INSERT LineItems SELECT * FROM dbo.GetStrings(@lineitems) ... END
对数据进行自定义聚合
在许多情况下,您可能需要对数据进行聚合。这包括执行统计计算(如 avg、stddev 等等)。假如所需的聚合函数不是作为内置聚合函数直接支持的,SQL Server 2005 中有三种方法可以进行这样的自定义聚合:
• |
将聚合编写为用户定义的聚合 (UDA)。 |
• |
使用 CLR 存储过程编写聚合。 |
• |
使用服务器端光标。 |
让我们在一个称为 PRODUCT(int) 的简单聚合函数的上下文中检查这三种替代方法,该聚合函数计算一组给定值的乘积。
作为用户定义的聚合函数实现的 PRODUCT
下面是此函数的主干 C# 代码示例。所有的积累逻辑都在 Accumulate 函数中(为了简单起见,其他函数显示为 {...})。
[SqlUserDefinedAggregate(Format.Native)] public struct Product { public void Accumulate(SqlInt32 Value) { m_value *= Value; } public void Init() {...} public void Merge(Product Group) {...} public SqlInt32 Terminate() {...} }
在定义类型、创建程序集和注册到 SQL Server 之后,就可以通过以下方式使用 T-SQL中的聚合函数:
SELECT dbo.Product(intcol) FROM tbl GROUP BY col
作为使用 SqlDataReader 的托管存储过程实现的 PRODUCT
可以创建存储过程来执行查询和循环访问结果,以执行计算。这种循环访问是通过使用 SqlDataReader 类完成的。
[SqlProcedure] public static void Product(out SqlInt32 value) { SqlCommand cmd = SqlContext.GetCommand(); cmd.CommandText = "select intcolumn from tbl"; SqlDataReader r = cmd.ExecuteReader(); bool first = true; using (r) { while (r.Read()) //skip to the next row { if (first) { value = r.GetSqlInt32(0); first = false; } else { value *= r.GetSqlInt32(0); } } } }
可以使用 EXEC 语句来调用这一过程
EXEC Product @p OUTPUT
作为使用光标的 T-SQL 存储过程实现的 PRODUCT
可以创建 T-SQL 存储过程来执行查询和通过使用 T-SQL 光标循环访问结果,以执行计算。
create procedure TSQL_ProductProc (@product int output) as begin declare @sales int declare c insensitive cursor for select intcolumn from tbl open c fetch next from c into @sales if @@FETCH_STATUS = 0 set @product = @sales while @@FETCH_STATUS = 0 begin fetch next from c into @sales set @product = @product * @sales end close c deallocate c end
决定是使用 UDA 还是使用其他某种解决方案来产生结果取决于几个因素:
• |
可组合性要求。UDA 实际上是独立的对象,可以用于任何 T-SQL 查询,通常用在可以使用系统聚合函数的任何相同的地方。不需要假定它所操作的查询。例如,可以将其包括在视图定义(不过,索引视图中不支持 UDA)和标量子查询中。 |
• |
聚合算法细节。在 Order By 子句(假如查询中有)之前可能对 UDA 进行求值,因此不能保证传递给聚合函数的值的顺序。假如聚合算法需要按照特定的顺序使用值,则不能使用 UDA。同样地,UDA 从整组中使用值并且返回单一值。假如需要必须为组中的每个值返回值的聚合函数,则应该考虑使用存储过程或流表值函数来编写您的函数。详细信息请参见本文中的“产生结果”一节。 |
• |
对副作用和数据访问的需要。不允许 UDA 进行数据访问或有副作用。假如您的函数需要保留大量的数据作为聚合的中间状态,或因为其他某种原因需要进行数据访问,则必须使用过程。 |
使用 UDA 的第一种方法在这三个选择中可能提供最好的性能。通常,假如没有碰到上面所列的限制,就应该尝试将聚合函数编写为 UDA。假如无法使用 UDA 方法,则使用 SqlReader 的托管代码方法可能比 T-SQL 光标方法执行得更好。
可以用 UDA 方法编写的有用的聚合的示例还包括:找到每组中第 N 大(或第 N 小)值,找到每组中前 N 个最大值的平均值或总和,等等。
六 用户定义的类型 (UDT)现在,我们来讲 SQL Server 2005 中功能更强大但是经常被错误理解的一个功能。使用用户定义的类型 (UDT),可以扩展数据库的标量类型系统(不仅仅为系统类型定义您自己的别名,这在 SQL Server 以前的版本中一直可用)。定义 UDT 就像用托管代码编写类,创建程序集,然后使用“create type”语句在 SQL Server 中注册该类型一样简单。下面是实现 UDT 的主干代码:
[SqlUserDefinedTypeAttribute(Format.Native)] public struct SimpleUdt: INullable { public override string ToString() {...} public bool IsNull { get; } public static SimpleUdt Null { get; } public static SimpleUdt Parse(SqlString s) {...} ... } create type simpleudt from [myassembly].[SimpleUdt] create table t (mycolumn simpleudt)
何时创建 UDT
SQL Server 2005 中的 UDT 不是对象相关的扩展性机制。它们是扩展数据库的标量类型系统的一种方法。标量类型系统包括 SQLServer 附带的列类型(如 int、nvarchar 和 uniqueidentifier 等类型)。例如,使用 UDT,可以定义您自己的、用于列定义的类型。假如您的类型确实是一个适合建模为列的原子值,请创建 UDT。
假如需要定义您自己的标量类型,请使用 UDT。这种类型的示例情况包括各种日历中的自定义日期/时间数据类型和货币数据类型。使用 UDT,可以创建单个对象来公开类型上可用的所有行为,并且封装或隐藏该类型所存储的基础数据。需要访问数据的每个人都必须使用 UDT 的编程接口。假如能够利用 .NET Framework 中现有的功能(如国际化或日历功能),这实际上又是考虑将类型实现为 UDT 的一个很好的理由。
何时不创建 UDT
不要使用 UDT 来对复杂的业务对象(如雇员、联系人或客户)进行建模。您将会陷入 UDT 的所有列限制(如,8KB 大小限制、索引限制)和在更新 UDT 值时更新整个值的不利方面。对于复杂类型,UDT 不是合适的数据建模抽象;因此对于这种情况,最好使用中间层对象相关映射技术。
设计 UDT 时需要考虑的因素
因为它们是列形式的,所以可以定义整个 UDT 值的索引,并且创建参考完整性约束(如 UDT 列的唯一性)。还可以在比较和排序方案中使用 UDT。
比较 UDT 值是通过比较类型的基础二进制表示完成的。假如使用 Format.Native 作为持久性机制,则会按照同在该类型中定义的一样的字段顺序创建永久形式;因此,请确保这些字段是按照类型的正确顺序排列的。
UDT 上的每个操作(除了比较)都要求 UDT 的值反序列化,接着进行方法调用。这种模式有与之相关的固定开销。假如要将类型建模为 UDT(相对于表中的列),则在访问类型属性(相对于表中的列)时应该考虑这种差别。假如类型上的行为非常复杂,则应该考虑使用 UDT。假如类型没有任何与之相关的行为,则应该考虑将数据存储为表中的列。
假如发现需要实现相关函数的库,则 UDT 中的静态方法是一种可以方便地创建这种库的封装机制。可以通过使用 :: 语法来调用T-SQL 中的静态方法,如下所示:
select imagetype::MyFunction(@arg1)
实际方案
客户希望在 UmAlQuraCalendar 中存储日期时间值,这与 SQL Server 日期时间数据类型使用的 Gregorian 日历不同。他们想让这个日期类型具有相同的基本行为集,即字符串转换、日期部分、日期算法和 GetDate()。
下面是这一数据类型的代码片段。它使用 UmAlQuraCalendar 2.0 版,这是 .NET Framework 中的新类型。
[SqlUserDefinedType(Format.Native, IsByteOrdered = true)] public struct UmAlQuraDateTime : INullable { //Private state private long dtTicks; //Calendar object used for all calendar-specific operations private static readonly UmAlQuraCalendar s_calendar = new UmAlQuraCalendar(); public static UmAlQuraDateTime Null { get { UmAlQuraDateTime dt = new UmAlQuraDateTime(); dt.isNull = true; return dt; } } public bool IsNull { get { return this.isNull; } } //Convert into CLR DateTime type public DateTime DateTime { get { return new DateTime(this.dtTicks); } }
字符串转换和创建新值:
public static UmAlQuraDateTime Parse(SqlString s) ... public override string ToString() ... public static UmAlQuraDateTime ParseUsingFormat(string data, string fmt) ... public string ToStringUsingFormat(string format) ... public static UmAlQuraDateTime Now { get { return new UmAlQuraDateTime(DateTime.Now); } } public static UmAlQuraDateTime FromSqlDateTime(SqlDateTime d) { if (d.IsNull) return Null; return new UmAlQuraDateTime(d.Value); }
日期部分类似功能:
public int Year { get { return s_calendar.GetYear(this.DateTime); } } public int Month ... public int Hour ... public int Minute ... public int Second ... public double Milliseconds ... public long Ticks ... public SqlDateTime ToSqlDateTime() { return new SqlDateTime(this.DateTime); }
DateAdd/DateDiff:
public UmAlQuraDateTime AddYears(int years) { return new UmAlQuraDateTime(s_calendar.AddYears(this.DateTime, years)); } public UmAlQuraDateTime AddDays(int days) ... public UmAlQuraDateTime AddMonths(int months) ... public UmAlQuraDateTime AddHours(int hours) ... public UmAlQuraDateTime AddMinutes(int minutes) ... public double DiffDays(UmAlQuraDateTime other) ...
下面是一些处理 T-SQL 中的类型的示例操作:
-- convert it to arabic (for testing purposes) declare @h hijridatetime set @h = hijridatetime::ParseArabicString('01/02/1400') select @h.ToArabicString(), @h.ToArabicStringUsingFormat('F') -- convert sql datetime to hijri and back declare @h hijridatetime set @h = hijridatetime::FromSqlDateTime(GetDate()) select @h.ToArabicString(), @h.ToSqlDateTime() -- get the current hijri datetime, in two ways select hijridatetime::Now.ToString(), hijridatetime::FromSqlDateTime(GetDate()).ToString() -- do some arithmetic: declare @h hijridatetime, @d datetime set @h = hijridatetime::Now -- get the current hijri datetime set @h = @h.AddDays(10) -- add ten days to it set @d = GetDate() -- current sql date set @d = DateAdd(day, 10, @d) -- add ten days to the sql datetime -- print 'em both, should be the same select @h.ToSqlDateTime(), @d -- datepart declare @h hijridatetime set @h = hijridatetime::Now -- get the current hijri datetime select @h.Year as year, @h.Month as month, @h.Day as month
小结
该篇讲述了使用 SQL Server 2005 中的 CLR 集成功能的指导原则、具体使用情况和示例。数据库应用程序开发人员和架构师应该结合集中讨论 SQL Server 2005 中的其他功能区(如 TransacT-SQL、XML 和 Service Broker)的指导原则的其他文章来阅读本文。
将来,我们计划在这一领域提供更多白皮书,讲解 CLR 集成的可管理性、监控和故障排除方面的指导原则。
版权这是一个预备文档,在此处描述的软件的最终商业版本发布之前可能对该文档进行补充性更改。本文档中包含的信息代表 Microsoft Corporation 在发布时对所讨论问题的最新观点。由于 Microsoft 必须适应不断变化的市场情况,因此,这些信息并非 方面所作的承诺,Microsoft 也不能保证出版日之后提供的任何信息都完全正确。
本白皮书仅供参考。MICROSOFT 对本文档中的有关信息不作任何明示或暗示的担保。
用户有责任遵从所有适用的版权法。除了版权法所赋予的权利以外,未经 Microsoft Corporation 明确书面许可,不得擅自将本文档的任何部分进行复制、存储在或输入可检索系统,或以任何形式或方式(电子、机械、影印、录制或其他方式)进行传播,或用作其他目的。
Microsoft 对本文档中的主题持有专利权、专利申请权、商标权、版权或其他相关的知识产权。Microsoft 只提供在任何书面许可协议中明确规定的权利,而不授予您本文档的上述专利权、商标权、版权或其他知识产权。
除非另有声明,否则本文中提到的示例公司、组织、产品、域名、电子邮件地址、徽标、人物、地点和事件都是虚构的,与任何真实的公司、组织、产品、域名、电子邮件地址、徽标、人物、地点或事件无任何关联,也不应该被推断为有任何关联。
© 2004 Microsoft Corporation. All rights reserved.
Microsoft、Visual C++ 和 Visual C# 是 Microsoft Corporation 在美国和/或其他国家/地区的注册商标或商标。
此处提到的真实的公司和产品名称是其各自所有者的商标。