请注意,在这个示例中,我们没有利用 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 提供了更有效的流实现,这避免了将结果加载到工作表中。 |
