【问题标题】:SQL Server 2005 Table-valued Function weird performanceSQL Server 2005 表值函数奇怪的性能
【发布时间】:2011-05-10 13:59:48
【问题描述】:

我在 1 分钟的查询和表值函数中的相同查询之间存在巨大的执行时间差异。

但最奇怪的是使用另一个(有效的)company_id 参数运行 UDF 在约 40 秒内给我一个结果,一旦我将此 company_id 更改为 12(再次有效),它就永远不会停止。 这两个查询的执行计划绝对不一样,当然长的那个是最复杂的。但是批处理版本和UDF版本之间的执行计划是一样的,而且批处理版本很快……!

如果我“手动”执行以下查询,则执行时间为 1min36s,包含 306 行:

 SELECT
  dbo.date_only(Call.date) AS date,
  count(DISTINCT customer_id) AS new_customers
 FROM
  Call
 LEFT OUTER JOIN
  dbo.company_new_customers(12, 2009, 2009) new_customers
   ON dbo.date_only(new_customers.date) = dbo.date_only(Call.date)
 WHERE
  company_id = 12
  AND year(Call.date) >= 2009
  AND year(Call.date) <= 2009
 GROUP BY
  dbo.date_only(Call.date)

我将这个完全相同的查询存储在一个函数中并像这样运行它:

SELECT * FROM company_new_customers_count(12, 2009, 2009)

现在它正在运行 13 分钟......而且我确信它永远不会给我任何结果。

昨天,我在超过 4 小时内出现了完全相同的类似无限循环的行为(所以我停止了它)。

这里是函数的定义:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION company_new_customers_count 
( 
 @company_id int, 
 @start_year int,
 @end_year int
)
RETURNS TABLE 
AS
RETURN 
(
 SELECT
  dbo.date_only(Call.date) AS date,
  count(DISTINCT customer_id) AS new_customers
 FROM
  Call
 LEFT OUTER JOIN
  dbo.company_new_customers(@company_id, @start_year, @end_year) new_customers
   ON dbo.date_only(new_customers.date) = dbo.date_only(Call.date)
 WHERE
  company_id = @company_id
  AND year(Call.date) >= @start_year
  AND year(Call.date) <= @end_year
 GROUP BY
  dbo.date_only(Call.date)
)
GO

我很乐意了解发生了什么。

谢谢

补充:

company_new_customers 的定义:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Description: Create the list of new customers of @company_id
--          in the given period.
-- =============================================
CREATE FUNCTION company_new_customers 
(   
    @company_id int, 
    @start_year int,
    @end_year   int
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT
        customer_id,
        date
    FROM
    (   -- select apparition dates of cutomers before @end_year
        SELECT
            min(date)       AS date,
            customer_id
        FROM
            Call
        JOIN
            Call_Customer ON Call_Customer.call_id = Call.call_id
        WHERE
            company_id = @company_id
            AND year(date) <= @end_year
        GROUP BY
            customer_id
    ) new_customers
    WHERE
        year(date) >= @start_year -- select apparition dates of cutomers after @start_year
)
GO

date_only 的定义:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      Julio Guerra
-- Create date: 14/10/2010
-- Description: Return only the date part of a datetime value
-- Example:         date_only('2010-10-25 13:00:12') returns 2010-10-25
-- =============================================
CREATE FUNCTION date_only
(
    @datetime datetime
)
RETURNS datetime
AS
BEGIN
    RETURN dateadd(dd, 0, datediff(dd, 0, @datetime))
END
GO

SELECT * FROM company_new_customers_count(8, 2009, 2009)的执行计划

SELECT * FROM company_new_customers_count(12, 2009, 2009)的执行计划

【问题讨论】:

  • 所有其他函数的定义是什么:dbo.company_new_customers、dbo.date_only?这些表的大小/索引是多少?
  • @mellamok : 在帖子末尾添加。
  • 2h26 和它仍在运行的最后一个执行计划......
  • “2h26 和它仍在运行的最后一个执行计划” - 听起来它处于锁定等待状态。

标签: sql sql-server sql-server-2005 tsql user-defined-functions


【解决方案1】:

从这些查询计划看来,您可以从这样的索引中受益(如果我正确推断出您的数据库架构):

CREATE INDEX IX_call_company_date ON call (company_id, date)

一般来说,这似乎是一个标准的查询优化问题,而表值函数实际上在这里并没有什么不同。

【讨论】:

  • @Pent Ploompuu :执行计划完全相同。我用另一个 company_id (8) 参数尝试了 UDF,它在 47 秒内给了我结果!另一台 (12) 仍在运行(> 53 分钟)。
  • @Pent Ploompuu:SELECT * FROM company_new_customers_count(12, 2009, 2009)(永无止境的查询)和 SELECT * FROM company_new_customers_count(8, 2009, 2009) 的执行计划有很多不同之处)(快速查询)。
  • 如果查询计划相同,这种行为有点奇怪。您可以尝试打开 IO 和 TIME 统计信息,并使用公司 ID 执行 UDF 版本和批处理版本的查询,在 40 秒内使用 UDF 版本给出结果。也许不同的统计数据可以为那里发生的事情提供线索。
  • 刚刚注意到您的其他评论。如果没有看到您的查询计划,我猜它没有使用索引来进行日期比较,如果优化器决定使用带有 company_id 的内部联接,这可能会导致一些非常糟糕的表扫描。我建议对日期范围条件使用 datetime 参数而不是 int 参数,因为这样优化器就可以正确使用索引。或者至少不要在条件中使用year(field) - 从查询中的@start_year/@end_year 计算日期时间值。
  • @Pent Ploompuu :我将 int 更改为 datetime 并且它没有改变任何东西。执行计划是一样的。我刚刚编辑了我的帖子并添加了两张执行计划的图片(在帖子中调整了大小)。快速的使用合并连接,而长的使用嵌套循环。
【解决方案2】:

短计划使用HashJoinPK_CALL 上的聚集索引扫描。长期计划使用 NestedLoops 并在 UK_Pair_... 中重复搜索。 '12, 2009, 2009' 的基数估计很可能由于系统内存不足而排除了 HashJoin,因此您最终会得到更糟糕的计划(尽管搜索而不是扫描)。可能 company_id 12 的客户比 company_id 8 多。

如果没有有关所有相关因素的确切信息(所使用的确切架构,包括每个索引,以及所涉及的每个表的确切统计信息和基数),就不可能给出解决方案。一个简单的途径是使用计划指南,请参阅Designing and Implementing Plan Guides

【讨论】:

    【解决方案3】:

    这里有几个部分的答案。对于第一部分,我将尝试回答关于为什么没有一个查询特别快的问题(你没有问)。这与您实际提出的问题有关,请耐心等待。

    您的日期标准通常不是您指定的SARGable - 例如在您的company_new_customers 函数中。这意味着服务器无法使用其统计数据来确定您的标准的选择性。 这意味着您的查询计划将对您的 customer_id 标准的选择性非常非常敏感,无论每个客户有多少日期行。

    在调用表中使用索引日期和 call_id 的范围查询应该可以大大提高所有情况下的性能,并降低查询对 customer_id 选择性的敏感性。假设日期在您的调用表上,我会像这样重写您的内部 UDF,并调整输入参数以改用日期。这样做也会使您的 UDF 更加通用:

    CREATE FUNCTION company_new_customers 
    (   
        @company_id INT, 
        @start_date DATETIME,
        @end_date   DATETIME
    )
    RETURNS TABLE 
    AS
    RETURN 
    (    
            SELECT
                MIN(c.[date]) AS [date],
                c.customer_id
            FROM dbo.[Call] c
            JOIN dbo.[Call_Customer] cc
                ON cc.call_id = c.call_id
            WHERE c.company_id = @company_id
            AND   c.[date]    <= @end_date
            AND   NOT EXISTS (
                    SELECT *
                    FROM  dbo.[Call] c1
                    WHERE c1.customer_id = c.customer_id
                    AND   c1.[date] <= @start_date              
            )    
            GROUP BY
                c.customer_id          
    )
    GO
    

    你的其他观点也是如此。通过使用 year() 和 date_only() 函数,您可以使您在日期上拥有的任何统计信息或索引几乎毫无用处(尽管优化器可以使用它们来限制扫描的数据量,但这是一个更大的讨论)。

    那么,为什么您的 UDF 需要永远存在?因为它调用另一个 UDF 并且您使用 date_only() 作为连接参数,所以它几乎无法“知道”任何关于 UDF 子查询中预期的内容,因此它选择了循环连接。它可能会选择该计划,因为它适合某些 customer_id 值。您可能在创建 UDF 后不久就针对这些选择性 customer_id 之一运行了查询,并且该查询的计划已被缓存——即使它不适用于 customer_id 的其他值。

    为什么存储过程不会永远占用?因为第一次运行它时,存储过程会根据您给它的第一个标准生成一个计划。也许您第一次运行 SP 时,使用了非选择性客户 ID,并且存储过程选择了散列连接。即席查询也是如此。优化器“注意到”您已向其传递了一个非选择性的 customer_id,并且正在选择为您创建一个哈希联接。

    无论哪种方式,除非您控制了 date-SARGability 问题,否则您会发现您的所有此类查询都将对您的 customer_id 输入非常敏感,并且根据您的使用模式,它们可能在性能方面炸毁你的脸 - UDF 与否。

    希望这会有所帮助!

    【讨论】:

    • 将 int 参数更改为 datetime 和 year(...)
    • 请注意,由于我提到的原因,您看到的相同执行计划并非巧合。在一般情况下,索引很可能使扫描/哈希操作“最佳”,但要小心。
    【解决方案4】:

    我已经在 SQL Server 2005 中看到了这一点。当我们对特定查询使用表值函数时,我们确实获得了糟糕的性能。获取完全相同的查询、参数和所有文本,将它们放入存储过程中,并可靠地获得了一个了不起的查询计划。使用与存储过程相同的参数调用函数会产生不同的行为(我们都从冷缓存开始)。非常令人失望!

    遗憾的是,我们没有时间更深入地诊断这种奇怪的行为,并在 2005 年将项目转移到避免使用表值函数。

    这可能表明 SQL Server 2005 中存在错误。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多