我们有一个几年前编写的SQL函数,它一直在导致零星的问题(我想已经有很多年了).

我终于弄到了一个客户数据库的备份,这样我就可以知道发生了什么.

该函数从数据库中提取最新的"会话"ID并返回它,除非@@ROWCOUNT等于0,在这种情况下,它返回一个空的string.

ALTER FUNCTION [dbo].[GetCurrentSessionId]()
RETURNS nvarchar(255)
AS
BEGIN
    declare @v nvarchar(255)

    select top 1 @v = SessionId
    from RestoreSession
    where 
        SessionState <> 3 and -- Complete
        SessionState <> 7 -- CompletedWithWarnings
    order by SessionId desc

    if @@ROWCOUNT = 0
        set @v = ''

    return @v
END

即使在数据库中有一个会话,这个函数每隔一次就会突然开始,总是返回空的string.

当我在调试问题时第一次看到上面的函数时,我注意到了@@ROWCOUNT,并认为它很奇怪.

我go 掉了@@ROWCOUNT的条件,并重新测试了,这次是session ID was correctly returned.然后,我重新添加@@ROWCOUNT条件并重新测试,它再次返回空string.

可能重要也可能无关紧要的进一步细节.此函数是使用System.Data.SqlClient命名空间中的实用程序从C#应用程序调用的.

调用上述SQL函数的方法如下所示.

public string GetCurrentSessionID()
{
    string res = string.Empty;
    using (var connection = _persistence.CreateSqlConnection())
    {
        var sessionsQuery = "select [dbo].GetCurrentSessionId() as SID";
        using (var sqlCommand = connection.CreateCommand(sessionsQuery))
        {
            using (var sessionReader = sqlCommand.ExecuteReader())
            {
                if (sessionReader.Read())
                {
                    if (!sessionReader.IsDBNull("SID"))
                    {
                        res = sessionReader.GetString("SID");
                    }
                }
            }
        }
    }
    return res;
}

在上面的C#个方法中,IsDBNullGetString都是定制扩展方法,如下所示:

public static bool IsDBNull(this IDataRecord reader, string name)
{
    return reader.IsDBNull(reader.GetOrdinal(name));
}

public static string GetString(this IDataRecord reader, string name)
{
    return reader.GetString(reader.GetOrdinal(name));
}

我还想提一下,当我在SSMS中执行SQL函数而不删除@@ROWCOUNT条件语句时,会正确返回会话ID.

这似乎只有在从C#应用程序调用函数时才会发生.

我认为修复方法是删除@@ROWCOUNT条件语句,因为当我这样做时,C#应用程序能够正确地提取会话ID.

我很好奇,在这种情况下,问题可能是什么?为什么当从C#应用程序调用函数时,@@ROWCOUNT似乎偶尔"不工作".

推荐答案

这里的问题是,您正在使用completely个未打补丁的SQL Server 2019版本和用户标量函数的内联,并且还期待来自其他SQL Server版本(如2019年之前的版本,其中不存在标量内联)或更新的2019版本(其中您遇到的问题已经得到解决)的same行为.

首先,是问题.如果我们看一下可内联标量UDF要求,你会注意到它说:

可内联标量UDF要求

如果满足以下所有条件,标量T-SQL UDF可以内联:

  • ...
  • UDF不包含对内联时可能改变>;结果的内部函数的引用(如@@ROWCOUNT)4.
  • ...

...
4 Restriction added in SQL Server 2019 (15.x) CU2

请注意,这一限制是在CU2中添加的;这是因为早些时候就注意到内联函数(如@@ROWVERSION)会导致意外/不需要的结果,因此Microsoft解决了这个问题,使使用这些函数的函数不会内联.因此,该函数返回为多行标量函数.

由于您使用的是2019 RTM,您没有此更新,因此该函数是内联的,并导致了已知的不良行为;这只是您应该保持SQL Server最新的许多原因之一.

然而,由于您在部署中使用了一系列版本,我真诚地建议您出于以下几个原因避开用户定义的标量函数(除非它们非常简单,而且即便如此……).多行标量函数的性能可能很差,而内联表值函数(ITVF)的性能通常要好得多.此外,它们的行为不会因版本而改变(或者更具体地说,从未打补丁的2019版到打补丁的2019版),因此您知道,在使用不同版本的多客户端环境中,情况不会改变(一些客户端在更新服务器方面确实毫无用处).

上述查询实际上可以很容易地重写为iTVF,如下所示:

CREATE FUNCTION dbo.GetCurrentSessionID()
RETURNS table
AS RETURN
    SELECT ISNULL((SELECT TOP (1) SessionId
                   FROM dbo.RestoreSession
                   WHERE SessionState <> 3 -- Complete
                     AND SessionState <> 7 -- CompletedWithWarnings
                   ORDER BY SessionID DESC),'') AS SessionID;

如果您必须使用内联标量函数,那么我建议,由于有一个具有不同版本的多客户端环境,您应该确保关闭标量内联.这在数据库级别可能是最好的,因此您应该确保在创建时,对于2019年以上的实例,通过在特定数据库上运行它,您具有以下配置集:

ALTER DATABASE SCOPED CONFIGURATION SET TSQL_SCALAR_UDF_INLINING = OFF;

虽然您可以在实例级别执行此操作,但如果您的客户在其实例上的其他位置使用内联,则可能不会感谢您.

或者,您也可以在运行2019+的实例上,在函数级别设置它,使用WITH INLINE = OFF:

ALTER FUNCTION [dbo].[GetCurrentSessionId]()
RETURNS nvarchar(255)
WITH INLINE = OFF
AS
BEGIN
    declare @v nvarchar(255);

    select top 1 @v = SessionId
    from RestoreSession
    where 
        SessionState <> 3 and -- Complete
        SessionState <> 7 -- CompletedWithWarnings
    order by SessionId desc;

    if @@ROWCOUNT = 0
        set @v = '';

    return @v;
END;

不过,如果我说实话,您也可以这样做,这样查询就不会使用@@ROWCOUNT并且只有一个RETURN值:

CREATE FUNCTION [dbo].[GetCurrentSessionId]()
RETURNS nvarchar(255)
AS
BEGIN
    RETURN (SELECT ISNULL((SELECT TOP (1) SessionId
                   FROM dbo.RestoreSession
                   WHERE SessionState <> 3 -- Complete
                     AND SessionState <> 7 -- CompletedWithWarnings
                   ORDER BY SessionID DESC),''));
END;

但是,我个人再次建议切换到iTVF,以便在所有环境(不仅仅是使用SQL Server 2019+的环境)上获得更好的性能.

Csharp相关问答推荐

System.Data.SQLite:判断SQLite数据库是否为空(任何表中至少有一行)

try 在Blazor项目中生成html

只有第一个LINQ.Count()语句有效

使用预定义对象减少Task.Run/Factory.StartNew中的关闭开销

方法从数据表中只 Select 一个条件?

当前代码Cosmos DB 3.37.1:PartitionKey key key mismatch exception

N层解决方案上的依赖注入-删除样板

HttpRequestMessage.SetPolicyExecutionContext不会将上下文传递给策略

如何在用户在线时限制令牌生成?

如何在不复制或使用输出的情况下定义项目依赖

未显示详细信息的弹出对话框

EF Core 7-忽略模型绑定中的虚拟属性

删除MudRadio时,MudRadioGroup未 Select 正确的MudRadio

如何在C#.NET桌面应用程序中动态更改焦点工具上的后退 colored颜色

在C#/ASP.NET Core 7中,什么可能导致POST请求作为GET请求发送

如何使用LINQ在C#中填充列表列表?

如何更改Datagridview行标题

ASP.NET核心MVC|如何在控制器方法之间传递值

有没有更好的方法来使用LINQ获取整行的计算组

我如何为我的Blazor应用程序构建一个动态教程标注?