【问题标题】:Table Valued Function vs Scalar Values Function for single return Value单返回值的表值函数与标量值函数
【发布时间】:2016-03-03 19:22:31
【问题描述】:

我从缩放器函数返回单个值,如下所示:

CREATE FUNCTION [dbo].[GetNoOfAssignedCases]
(
    @UserID INT,
    @FromD DATETIME,
    @ToD DATETIME
)
RETURNS NVARCHAR(MAX)
AS
BEGIN
    DECLARE @CaseCount INT = 0
    SELECT @CaseCount = COUNT(1) FROM Cases
    WHERE 
        CaseAssignedToAssessor = @UserID AND 
        CAST(ActionDateTime AS DATE) >= @FromD AND
        CAST(ActionDateTime AS DATE) <= @ToD
    RETURN @CaseCount
END

并如下使用它:

SELECT [Name], [DBO].[GetNoOfAssignedCases](UserID, GETDATE()-30, GETDATE()) FROM Users

可以用表值函数代替吗? 它会对性能产生任何影响吗?哪个会更快?

【问题讨论】:

  • 您刚刚编辑了您的问题并询问了性能:这取决于...如果您必须一次性为所有用户获取计数器,最快的可能是老式的GROUP BY...带有APPLY 的内联UDF 应该与带有子选择的JOIN 完全相同。这肯定比多行的标量计算或多语句 UDF 更快。有多少行(在UsersCases 中)?
  • 更多提示:询问性能,您应该阅读“可搜索表达式”。你的CAST(ActionDateTime...) 不会是 sargable...
  • @Shnugo 将有超过 100 个用户和超过 20,000 个案例。我正在使用其他 3 个类似的函数来根据一个选择的不同标准获取计数。所以我不认为我可以使用 group by。
  • 如果在Cases(和其他表)中的外键上有一个索引,一个简单的计数就可以了——当然! - 永远不会导致性能问题。内联 UDF - 如我的回答中所述 - 与子选择上的 JOIN 大致相同,并且 - 如果一次为多个用户完成 - 比逐行方法要好得多。
  • 嗨,我很好奇:你能解决你的问题吗?您选择了哪种方法?

标签: sql-server sql-server-2012 sql-server-2014


【解决方案1】:

我与 Jonatha Dickinson 的讨论(看看他的回答)让我做了一些快速测试:

结果表明,纯嵌入式标量子选择并没有那么糟糕。 查询一个值甚至是最快的。正如预期的那样,标量函数很糟糕。 TVF 反馈的场越多,相对性能增益就越好。

唯一确定的答案是:标量函数是最差的,多行 TVF - 大多数时候 - 比内联慢。 任何临时方法往往都更快

但我可以为所有情况(标量函数除外)设置特殊情况,其中一种方法是最快的。

结论:(一如既往:-))这取决于...

提示:最好是针对具有许多表和列的大型数据库。

CREATE FUNCTION dbo.CountColumnScalar(@TableSchema AS VARCHAR(100),@TableName AS VARCHAR(100))
RETURNS INT
AS
BEGIN
    RETURN(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE c.TABLE_SCHEMA=@TableSchema AND c.TABLE_NAME=@TableName);
END
GO
CREATE FUNCTION dbo.CountConstraintScalar(@TableSchema AS VARCHAR(100),@TableName AS VARCHAR(100))
RETURNS INT
AS
BEGIN
    RETURN(SELECT COUNT(*) FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS c WHERE c.TABLE_SCHEMA=@TableSchema AND c.TABLE_NAME=@TableName);
END
GO

CREATE FUNCTION dbo.CountAllTVF(@TableSchema AS VARCHAR(100),@TableName AS VARCHAR(100))
RETURNS TABLE
RETURN SELECT (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE c.TABLE_SCHEMA=@TableSchema AND c.TABLE_NAME=@TableName GROUP BY c.TABLE_SCHEMA,c.TABLE_NAME) AS ColCounter
             ,(SELECT COUNT(*) FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS c WHERE c.TABLE_SCHEMA=@TableSchema AND c.TABLE_NAME=@TableName GROUP BY c.TABLE_SCHEMA,c.TABLE_NAME) AS ConstraintCounter    ;
GO
CREATE FUNCTION dbo.CountAllTVF_multiline(@TableSchema AS VARCHAR(100),@TableName AS VARCHAR(100))
RETURNS @tbl TABLE (ColCounter INT,ConstraintCounter INT)
AS
BEGIN
INSERT INTO @tbl
SELECT (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE c.TABLE_SCHEMA=@TableSchema AND c.TABLE_NAME=@TableName GROUP BY c.TABLE_SCHEMA,c.TABLE_NAME) AS ColCounter
      ,(SELECT COUNT(*) FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS c WHERE c.TABLE_SCHEMA=@TableSchema AND c.TABLE_NAME=@TableName GROUP BY c.TABLE_SCHEMA,c.TABLE_NAME) AS ConstraintCounter;
RETURN;
END
GO

DECLARE @time DATETIME=GETDATE();
SELECT TABLE_SCHEMA,TABLE_NAME
     ,(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS AS c WHERE c.TABLE_SCHEMA=t.TABLE_SCHEMA AND c.TABLE_NAME=t.TABLE_NAME ) AS ColCounter
     ,(SELECT COUNT(*) FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS c WHERE c.TABLE_SCHEMA=t.TABLE_SCHEMA AND c.TABLE_NAME=t.TABLE_NAME ) AS ConstraintCounter
FROM INFORMATION_SCHEMA.TABLES AS t;
PRINT 'pure embedded scalar sub-select: ' + CAST(CAST(GETDATE()-@time AS TIME) AS VARCHAR(MAX)); SET @time=GETDATE();


SELECT TABLE_SCHEMA,TABLE_NAME
     ,dbo.CountColumnScalar(t.TABLE_SCHEMA,t.TABLE_NAME ) AS ColCounter 
     ,dbo.CountConstraintScalar(t.TABLE_SCHEMA,t.TABLE_NAME ) AS ConstraintCount 
FROM INFORMATION_SCHEMA.TABLES AS t
PRINT 'scalar function: ' + CAST(CAST(GETDATE()-@time AS TIME) AS VARCHAR(MAX)); SET @time=GETDATE();


SELECT t.TABLE_SCHEMA,t.TABLE_NAME
      ,colJoin.ColCount
      ,conJoin.ConstraintCount 
FROM INFORMATION_SCHEMA.TABLES AS t
INNER JOIN (SELECT COUNT(*) As ColCount,c.TABLE_SCHEMA,c.TABLE_NAME 
            FROM INFORMATION_SCHEMA.COLUMNS AS c 
            GROUP BY c.TABLE_SCHEMA,c.TABLE_NAME) AS colJoin ON  colJoin.TABLE_SCHEMA=t.TABLE_SCHEMA AND colJoin.TABLE_NAME=t.TABLE_NAME
INNER JOIN (SELECT COUNT(*) As ConstraintCount,c.TABLE_SCHEMA,c.TABLE_NAME 
            FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS c 
            GROUP BY c.TABLE_SCHEMA,c.TABLE_NAME) AS conJoin ON  conJoin.TABLE_SCHEMA=t.TABLE_SCHEMA AND conJoin.TABLE_NAME=t.TABLE_NAME
PRINT 'JOINs on sub-selects: ' + CAST(CAST(GETDATE()-@time AS TIME) AS VARCHAR(MAX)); SET @time=GETDATE();

SELECT t.TABLE_SCHEMA,t.TABLE_NAME
      ,ColCounter.*
FROM INFORMATION_SCHEMA.TABLES AS t
CROSS APPLY dbo.CountAllTVF(t.TABLE_SCHEMA,t.TABLE_NAME) AS ColCounter
PRINT 'TVF inline: ' + CAST(CAST(GETDATE()-@time AS TIME) AS VARCHAR(MAX)); SET @time=GETDATE();


SELECT t.TABLE_SCHEMA,t.TABLE_NAME
      ,ColCounter.*
FROM INFORMATION_SCHEMA.TABLES AS t
CROSS APPLY dbo.CountAllTVF_multiline(t.TABLE_SCHEMA,t.TABLE_NAME) AS ColCounter
PRINT 'TVF multiline: ' + CAST(CAST(GETDATE()-@time AS TIME) AS VARCHAR(MAX)); SET @time=GETDATE();
GO

DROP FUNCTION dbo.CountColumnScalar;
DROP FUNCTION dbo.CountAllTVF;
DROP FUNCTION dbo.CountAllTVF_multiline;
DROP FUNCTION dbo.CountConstraintScalar;

【讨论】:

  • 哪个是胖子?内嵌 TVF 还是多行 TVF?
  • 这是为数不多的肯定答案之一:内联更快。
  • @dotNetAddict,您的 SqlFiddle 是一个全新的问题......请不要在 cmets 中提出后续问题。如果您认为某个用户很可能会给您一个很好的答案,那么您可以提出一个新问题,并且如果您愿意的话,可以在评论中放置一个链接。如果你这样做,我会在那里详细回答,简而言之:你放在小提琴中的根本不是 TVF!,这是一个连接单词的函数,返回一个字符串。这最好与 CTE 内联完成。提出一个新问题并给我一个链接。我会尽快回复。
【解决方案2】:

是的,你可以这样做(未经测试)

CREATE FUNCTION [dbo].[GetNoOfAssignedCases]
(
    @UserID INT,
    @FromD DATETIME,
    @ToD DATETIME
)
RETURNS TABLE
AS
RETURN
    SELECT COUNT(1) AS CaseCount 
    FROM Cases
    WHERE 
        CaseAssignedToAssessor = @UserID AND 
        CAST(ActionDateTime AS DATE) >= @FromD AND
        CAST(ActionDateTime AS DATE) <= @ToD;
GO


--And a call like this
SELECT [Name],CaseCounter.CaseCount
FROM Users
OUTER APPLY [DBO].[GetNoOfAssignedCases](UserID, GETDATE()-30, GETDATE()) AS CaseCounter

但是 - 如果你真的只需要一个标量值! - 我不知道为什么...

TVF 是个好主意的原因:

  • 您想一次性获取多行的“标量”值
  • 您希望 - 也许稍后 - 从此函数中获得更多价值

【讨论】:

    【解决方案3】:

    会对性能产生影响吗?

    是的。我们非常广泛地使用函数。在优化特定查询时,我发现 SQL 将标量值函数优化为一个单独的实体,其中 TVF 被“内联”到主查询中,然后作为一个整体进行优化。可能存在例外情况,但我们发现 TVF 普遍更快(仅比您自己内联函数稍慢)。

    可以用表值函数代替吗?

    是的。如果您担心它的性能,那么您应该使用以下格式:

    CREATE FUNCTION [dbo].[GetNoOfAssignedCases]
    (
        @UserID INT,
        @FromD DATETIME,
        @ToD DATETIME
    )
    RETURNS TABLE
    AS RETURN
        SELECT COUNT(1) AS Count FROM Cases
        WHERE 
            CaseAssignedToAssessor = @UserID AND 
            CAST(ActionDateTime AS DATE) >= @FromD AND
            CAST(ActionDateTime AS DATE) <= @ToD;
    

    然后可以使用交叉应用来执行函数:

    SELECT [Name], [AC].[Count] FROM Users
        CROSS APPLY [DBO].[GetNoOfAssignedCases](UserID, GETDATE()-30, GETDATE()) AS [AC]
    

    您需要RETURN SELECT 才能进行“内联”。此外,您的工作是确保这些函数不会返回超过一条记录(除非您真的想要 CROSS APPLY 行为 - 您几乎从不这样做)。

    【讨论】:

    • 你为什么重复我的回答?您回答的唯一新信息是WITH SCHEMABINDING可能会稍微提高性能。这将取决于我们不知道的许多情况。将其声明为对现有答案的评论还不够吗?顺便说一句:使用CROSS APPLY 将在根本没有案例的情况下超越用户......为什么函数从不返回多个记录?这个COUNT无论如何都不会,但为什么从不
    • @Shnugo 它回答了整个问题。您根本没有触及性能,这是明确询问的。 “我们不知道的情况”,不,那是记录在案的行为。标量 TVF 不应返回多于一条记录,因为 CROSS APPLY 是笛卡尔积:导致多于一行。有趣的是,提问者甚至将性能查询加粗。
    • 好吧,你不知道,我的答案在 之前 OP 通过编辑添加了性能问题。我直接在cmets中给出了一些答案。你说的是“标量 TVF”——不管它是什么……是的,人们可能会滥用 TVF 来提供一个标量值——但为什么要这样呢? CROSS APPLY 根本不会返回没有案例的用户。最好考虑OUTER APPLY
    • @Shnugo “但是为什么要一个呢?”我问题的第一部分回答说:性能。这正是为什么 RLS 使用 TVF 的裸(标量)形式。
    • 真的吗?如果您想获得几个 1000 个用户的 CaseCount,您应该 - 这正是我们俩回答的问题,请使用内联(!)TVF 而不是标量函数。但这不是“标量 TVF”,只是一个只有一列的表……嗯,说实话,我们俩的想法是一样的……让我们停止这种愚蠢的争吵,快乐编码!
    【解决方案4】:

    表值函数如下所示:

    CREATE FUNCTION [dbo].[GETNOOFASSIGNEDCASES] (@UserID INT,
                                                  @FromD  DATETIME,
                                                  @ToD    DATETIME)
    RETURNS @CaseCount TABLE (
      cnt INT NULL )
    AS
      BEGIN
          INSERT INTO @CaseCount
                      (cnt)
          SELECT Count(1)
          FROM   Cases
          WHERE  CaseAssignedToAssessor = @UserID
                 AND Cast(ActionDateTime AS DATE) >= @FromD
                 AND Cast(ActionDateTime AS DATE) <= @ToD
      END 
    

    您应该使用交叉应用或外部应用来调用上面定义的表值函数:

    SELECT [Name],tmp.cnt
    FROM   Users
           CROSS apply [DBO].[GETNOOFASSIGNEDCASES](UserID, Getdate() - 30, Getdate()) as tmp
    

    【讨论】:

    • 在这种情况下,应该总是更喜欢临时(或内联)UDF!并注意没有案例的用户。更好地使用OUTER APPLY...
    猜你喜欢
    • 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
    相关资源
    最近更新 更多