我们有一个多年前编写的 SQL
函数,该函数一直导致零星问题(我认为已经有很多年了)。
我终于能够获得客户数据库的备份,以便我能够弄清楚发生了什么。
相关函数从数据库中提取最新的“ session ”ID 并返回它,除非@@ROWCOUNT
等于0
,在这种情况下它返回一个空字符串
。
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
每隔一次,这个函数就会突然开始总是返回一个空的字符串
,即使数据库中有一个 session 也是如此。
当我在调试问题时第一次查看上述函数时,我注意到 @@ROWCOUNT
并认为它很奇怪。
我删除了 @@ROWCOUNT
条件并重新测试,这次正确返回了 session ID。然后,我重新添加了 @@ROWCOUNT
条件并重新测试,它再次返回一个空的字符串
。
更多可能重要也可能不重要的细节。使用 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#
方法中,IsDBNull
和 GetString
都是自定义扩展方法,如下所示:
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
条件语句时,会正确返回 session ID。
这似乎仅在从 C#
应用程序调用该函数时才会发生。
我认为修复方法是删除 @@ROWCOUNT
条件语句,就像我这样做时 C#
应用程序能够正确提取 session ID 一样。
我很好奇这种情况下可能出现什么问题?为什么从 C#
应用程序调用该函数时,@@ROWCOUNT
似乎偶尔“不起作用”。
最佳答案
这里的问题是,您正在使用完全未修补的 SQL Server 2019 版本和内联用户标量函数,并且还期望其他版本的 SQL 具有相同的行为服务器(例如 2019 年之前的服务器,其中不存在标量内联)或 2019 年的较新版本,其中您遇到的问题已得到解决。
首先是问题。如果我们看 Inlineable scalar UDF requirements您会注意到它指出:
Inlineable scalar UDF requirements
A scalar T-SQL UDF can be inlined if all of the following conditions are true:
- ...
- The UDF doesn't contain references to intrinsic functions that may alter the > results when inlined (such as @@ROWCOUNT) 4.
- ...
...
4 Restriction added in SQL Server 2019 (15.x) CU2
请注意,此限制是在 CU2 中添加的;这是因为人们很早就注意到内联函数(例如 @@ROWVERSION
)会导致意外/不期望的结果,因此 Microsoft 通过使使用所述函数的函数不内联来解决此问题。因此,该函数返回为多行标量函数。
由于您使用的是 2019 RTM,因此没有此更新,因此该函数被内联并导致已知的不良行为;这只是您应该保持 SQL Server 最新的众多原因之一。
但是,由于您在部署中使用了一系列版本,因此我真诚地建议您出于以下几个原因避免使用用户定义的标量函数(除非它们非常简单,甚至……) 。多行标量函数的性能可能很差,而内联表值函数 (iTVF) 的性能通常要好得多。此外,它们的行为不会因版本而改变(或者更具体地说,从未修补的 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+ 的环境)中获得更好的性能。
关于c# - 使用 @@ROWCOUNT 的旧 SQL 函数会导致问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/73241816/