代码之家  ›  专栏  ›  技术社区  ›  Julio Guerra

SQL Server 2005表值函数怪异性能

  •  2
  • Julio Guerra  · 技术社区  · 14 年前

    我有一个 巨大的 表值函数中1分钟查询与同一查询的执行时间差。

    但最奇怪的是,用另一个(有效的)公司id参数运行UDF 在大约40秒内给我一个结果,只要我将公司id更改12(再次生效),它就不会停止。 这两个查询的执行计划完全不同 当然,长的是最复杂的。但是批量版本和自定义项版本的执行计划是一样的,批量版本很快。。。!

    如果我“手动”执行以下查询,则执行时间为1分钟36秒,共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
    

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

    谢谢

    附加:

    新客户的定义:

    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
    

    仅日期定义:

    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
    

    从公司新客户数中选择*的执行计划(8,2009,2009) alt text

    从公司新客户数中选择*的执行计划(12,2009,2009) alt text

    4 回复  |  直到 14 年前
        1
  •  2
  •   Pent Ploompuu    14 年前

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

    CREATE INDEX IX_call_company_date ON call (company_id, date)
    

    一般来说,这似乎是一个标准的查询优化问题,而表值函数在这里并没有产生实际的影响。

        2
  •  2
  •   Remus Rusanu    14 年前

    短期计划使用 HashJoin 和聚集索引扫描 PK_CALL . 长期计划使用嵌套循环和重复搜索 UK_Pair_... . 最有可能的情况是,2009年12月的基数估计值排除了HashJoin,因为系统内存不足,所以你的计划更糟(尽管是搜索而不是扫描)。也许12号公司的顾客比8号公司多。

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

        3
  •  1
  •   Dave Markle    14 年前

    这里有几个部分的答案。在第一部分中,我将尝试回答一个问题(你没有问)为什么没有一个查询特别快。这和你实际上问的问题有关,所以请容忍我。

    你的约会标准通常不是 SARGable 正如您所指定的--例如 company_new_customers 功能。这意味着服务器无法使用它的统计数据来确定您的条件有多高的选择性。 这意味着您的查询计划将对客户id标准的选择性非常敏感 ,无论每个客户有多少日期行。

    在调用表中使用带有日期索引和调用I d的范围查询,在任何情况下都会大大提高性能,并降低查询对客户id选择性的敏感度。假设日期在调用表中,我将重写内部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,而您使用date_only()作为连接参数,所以它几乎无法“知道”UDF子查询中的期望值,所以它选择了循环连接。它之所以选择该计划,很可能是因为它适合于customer_id的某些值。很可能是在创建UDF后不久,您就对这些选择性customer_id中的一个运行了查询,并且该查询的计划已被缓存,即使它不适合customer_id的其他值。

    为什么一个存储过程不需要永远?因为第一次运行它时,存储过程根据您给出的第一个条件生成计划。也许第一次运行SP时,您使用了非选择性的客户ID,而存储的proc选择了散列连接。对于即席查询也是如此。优化器“注意到”您已经向它传递了一个非选择性的客户id,并选择为您创建一个散列连接。

    不管怎样,除非你控制了日期的可搜索性问题,否则你会发现你的所有查询都会对你的客户id输入非常敏感,而且根据你的使用模式,它们可能会在你的脸上炸掉,不管你是不是udf。

    希望这有帮助!

        4
  •  0
  •   Sir Wobin    14 年前

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

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

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