【问题标题】:Processing multiple results from CLR stored procedure in T-SQL在 T-SQL 中处理来自 CLR 存储过程的多个结果
【发布时间】:2011-09-03 07:46:22
【问题描述】:

我有一些用 C# 编写的复杂算法作为 CLR 存储过程。程序不是确定性的(取决于当前时间)。过程的结果是两个表。 我没有找到任何解决方案如何处理 T-SQL 中存储过程的多结果。这个过程的性能是关键(过程每约 2 秒调用一次)。

我发现更新表格最快的方法是:

UPDATE [db-table] SET ... SELECT * FROM [clr-func]

这比通过 ADO.NET 从 CLR 过程更新 db-table 快得多。

我使用静态字段存储结果,并在clr存储过程执行后查询。

调用栈是:

T-SQL proc
    -> CLR proc (MyStoredProcedure)
        -> T-SQL proc (UpdateDataFromMyStoredProcedure)
            -> CLR func (GetFirstResultOfMyStoredProcedure)
            -> CLR func (GetSecondResultOfMyStoredProcedure)

问题是,有时 CLR 函数在静态字段 result 中有 null,但在 CLR 过程中 result 不是 null。我发现,有时 CLR 函数在另一个 AppDomain 中调用,而不是 CLR 过程。但是 CLR 程序仍在运行,可以进行下一步操作,并且没有抛出异常。

有什么办法,如何强制在与“父”CLR 过程相同的 AppDomain 中调用 CLR 函数?

或者还有其他方法,如何实现我的意图?

P.S.:最初复杂的算法是用 T-SQL 编写的,但性能很差(比 C# 中的算法慢约 100 倍)。

谢谢!

简化代码:

// T-SQL
CREATE PROC [dbo].[UpdateDataFromMyStoredProcedure] AS BEGIN
    UPDATE [dbo].[tblObject]
        SET ...
        SELECT * FROM [dbo].[GetFirstResultOfMyStoredProcedure]()
    UPDATE [dbo].[tblObjectAction]
        SET ...
        SELECT * FROM [dbo].[GetSecondResultOfMyStoredProcedure]()
END

// ... somewhere else
EXEC [dbo].[MyStoredProcedure]

-

// C#
public class StoredProcedures {
    // store for result of "MyStoredProcedure ()"
    private static MyStoredProcedureResult result;
    [SqlProcedure]
    public static int MyStoredProcedure() {
        result = null;
        result = ComputeComplexAlgorithm();
        UpdateDataFromMyStoredProcedure();
        result = null;
    }
    [SqlFunction(...)]
    public static IEnumerable GetFirstResultOfMyStoredProcedure() {
        return result.First;
    }
    [SqlFunction(...)]
    public static IEnumerable GetSecondResultOfMyStoredProcedure() {
        return result.Second;
    }
    private static void UpdateDataFromMyStoredProcedure() {
        using(var cnn = new SqlConnection("context connection=true")) {
            using(var cmd = cnn.CreateCommand()) {
                cmd.CommandType = System.Data.CommandType.StoredProcedure;
                cmd.CommandText = "[dbo].[UpdateDataFromMyStoredProcedure]";
                cmd.ExecuteNonQuery();
            }
        }
    }
}

【问题讨论】:

  • 也许在 CLR 中实现 [dbo].[UpdateDataFromMyStoredProcedure] 也有意义?
  • @Piotr Rodak:对于从 CLR proc 更新 db-table 我只知道一种方法:使用 SqlCommand 并为每条记录调用更新。与经典的 T-SQL UPDATE 相比,它非常慢 - 我需要非常快速地处理它:(。

标签: c# sql-server performance stored-procedures sqlclr


【解决方案1】:

根据Bob Beauchemin“SQLCLR 为每个程序集所有者创建一个 appdomain,而不是每个数据库一个 appdomain” 您的两个 SQLCLR 程序集是否具有相同的所有者?

【讨论】:

  • CLR 过程和 CLR 函数在同一个类中。所以是的,它们在同一个程序集中。但主要是 - 它工作正常。仅在某些情况下(每 ~(1x - 20x) 次运行一次)会出现问题。
【解决方案2】:

有两种可能:

  • 更有可能的情况与应用域因内存压力而被卸载有关。一般来说,一个特定的程序集(因此对于其中的代码)只有一个应用程序域,因为应用程序域是每个数据库、每个所有者的。因此,您的代码不会在两个应用程序域中被调用,至少在概念上没有。

    但是,对于 SQL Server 如何处理卸载您正在经历的应用程序域,存在特定的事件序列细微差别。正在发生的事情是您的系统正在经历内存压力并且正在标记要卸载的应用程序域。这可以在 SQL Server 日志中看到,因为它会告诉您正在卸载的应用程序域的确切名称。

    由于内存压力,AppDomain 61 ({database_name}.{owner_name}[runtime].60) 被标记为卸载。

    当一个应用域被标记为卸载时,它被允许继续运行,直到所有当前正在运行的进程完成。此时它的“状态”为E_APPDOMAIN_DOOMED,而不是正常的E_APPDOMAIN_SHARED。如果启动另一个进程,即使是在注定失败的应用域中,也会创建一个新的应用域。这就是导致您正在经历的行为的原因。事件顺序如下(是的,我已经重现了这种行为):

    1. 执行MyStoredProcedure:如果应用程序域 1 不存在,则创建。应用程序域 1“状态”是 E_APPDOMAIN_SHARED。 result 设置为 null。
      1. result 按预期填充
      2. MyStoredProcedure 执行 GetFirstResultOfMyStoredProcedure:应用域 1“状态”仍然是 E_APPDOMAIN_SHARED。 result 按预期检索。
      3. 应用域 1 标记为卸载:应用域 1“状态”更改为 E_APPDOMAIN_DOOMED
      4. MyStoredProcedure 执行 GetSecondResultOfMyStoredProcedure:应用域 1“状态”仍为 E_APPDOMAIN_DOOMED,因此无法使用。应用域 2 已创建。应用程序域 2“状态”是 E_APPDOMAIN_SHARED。 result 设置为 null。这就是为什么您有时一无所获的原因:此进程位于 App Domain 2 中(即使它是从 App Domain 1 启动的),无法访问 App Domain 1。
    2. MyStoredProcedure 完成:App Domain 1 已卸载。


    还有另一种可能是如何发生这一系列事件:在执行GetFirstResultOfMyStoredProcedure 之前,可以将应用程序域 1 标记为卸载。在这种情况下,应用域 2 在GetFirstResultOfMyStoredProcedure 执行时创建,它和GetSecondResultOfMyStoredProcedure 在应用域 2 中运行并且不返回任何内容。

    因此,如果您希望/需要在这些条件下抛出错误,那么您的 Get*ResultOfMyStoredProcedure 方法需要在尝试检索之前检查 result == null 是否为空,如果为 null,则抛出错误。或者,如果可以重新计算存储在静态变量中的值,那么如果是 null,只需重新填充它(例如再次调用 ComputeComplexAlgorithm)。

  • 不太可能的可能性是,由于应用程序域由该代码的所有会话/调用者共享,它是可能的,如果您没有以其他方式确保只有 1 次执行此一次进程,其他人或 SQL 代理作业或其他东西执行 MyStoredProcedure,这将在静态变量启动时清空。

    既然您已经接受使用UNSAFE 程序集来获取可更新的静态变量,那么您不妨添加一个锁定机制来确保MyStoredProcedure 是单线程的。


除了要查看的那些区域之外,这个过程很可能会以一种更简单的方式更快地完成。您可以使用表值参数 (TVP) 将数据流式传输回 SQL Server,就像从应用程序代码中一样。只需创建一个或两个用户定义的表类型 (UDTT),它们与 TVF 返回的两个结果集(GetFirstResultOfMyStoredProcedure 和 GetSecondResultOfMyStoredProcedure)的结构相匹配。请参阅我的回答here,了解如何正确输入结果。通过使用此模型,您可以:

  • 在MyStoredProcedure CLR proc 中进行内联更新
  • 摆脱静态变量
  • 可能不再需要UNSAFE(如果它仅用于静态变量)。如果您无法通过上下文连接传回结果,您可能仍需要EXTERNAL_ACCESS,在这种情况下您将使用常规连接(即连接字符串使用“Server=(local)”或未指定“Server ")。
  • 摆脱UpdateDataFromMyStoredProcedure方法
  • 摆脱UpdateDataFromMyStoredProcedure T-SQL proc
  • 去掉GetFirstResultOfMyStoredProcedureCLR函数
  • 去掉GetSecondResultOfMyStoredProcedureCLR函数
  • 释放该静态变量当前使用的所有内存来保存两个结果集!!

这种方法不仅更易于维护并且很可能更快,而且它还不允许您在此处遇到具有未初始化静态变量问题的新 App Domain :-)。

【讨论】:

  • 感谢更新。我们通过将数据存储在外部进程中解决了这个问题。
  • 没问题:)。如果您不介意,请您至少详细说明一下您如何将数据存储在另一个进程中?谢谢。
  • 非常简单。这只是静态字典。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多