【问题标题】:Query taking too long to execute with scalar function in where clause使用 where 子句中的标量函数执行查询花费的时间太长
【发布时间】:2016-01-20 22:26:05
【问题描述】:

下面是我的标量函数:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER FUNCTION [CheckClients]
(
    @UserId Varchar(3),
    @DbrNo varchar(10),
    @V_DBR_CLIENT varchar(6)
)
RETURNS int
AS
BEGIN
     Declare @Flag int
     set @Flag=1

     if(@V_DBR_CLIENT='XXXXXX')
     BEGIN
         if((select COUNT(USR_CLI) 
             from USRAGYCLI 
             inner join DBR on DBR_CLIENT = USR_CLI 
             where USR_CODE = @UserId and DBR_SERIES like @DbrNo +'T') <> 
            (select COUNT(DBR_CLIENT) 
             from DBR 
             where DBR_SERIES like @DbrNo + 'T') OR 
            (select COUNT(DBR_CLIENT) 
             from DBR 
             where DBR_SERIES like @DbrNo +'T') <= 0)
         BEGIN
             set @Flag=0
         END
      END

      RETURN @Flag
END

这是我的存储过程:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [SEL_CLI]
    @V_USER_ID VARCHAR(3),
    @V_NUMBER_OF_ROWS INT,
    @V_STARTS_WITH INT  
AS
BEGIN
    CREATE TABLE #tmpDbrNo 
    (
         Code VARCHAR(10),
         Name VARCHAR(100),
         NumberOfDebtors int,
         rownum int 
    )   

    ;WITH Temp AS
    (
        SELECT 
            CLT_NO AS Code,
            CLT_NAME AS Name,
            COUNT(DBR_NO) AS NumberOfDebtors
        FROM
            DBR 
        JOIN 
            USRAGYCLI ON DBR_CLIENT = USR_AGY_CLI
        JOIN 
            CLT ON DBR_CLIENT = CLT_NO
        WHERE       
            AND USR_CODE = @V_USER_ID           
            AND 1 = CheckClients(@V_USER_ID, DBR_NO, DBR_CLIENT)
        GROUP BY        
            CLT_NO, CLT_NAME
    )               
    INSERT INTO #tmpDbrNo   
        SELECT 
            Code, Name, NumberOfDebtors, 
            ROW_NUMBER() OVER (ORDER by Code) rownum 
        FROM
            Temp

    SELECT 
        Code, Name, NumberOfDebtors  
    FROM
        #tmpDbrNo 
    WHERE
        rownum BETWEEN @V_STARTS_WITH AND @V_STARTS_WITH + @V_NUMBER_OF_ROWS
END

执行上述查询大约需要 25 秒,等待时间过长。如果我在where 子句中注释掉调用标量函数的行,则执行查询需要 0 秒。

任何人都可以提出更好的方法来执行查询吗?我试图在如下情况下调用函数,但没有成功。

AND 1 = CASE WHEN DBR_CLIENT='XXXXXX' THEN CheckClients(@V_USER_ID,DBR_NO,DBR_CLIENT) ELSE 1 END

【问题讨论】:

  • 那是因为标量函数非常低效。当你把它放在 where 子句中时,就会呈现查询 nonSARGable。换句话说,它必须为表中的每一行计算函数的值。可能可以将您的标量函数转换为内联表值函数,并且性能几乎与排除您现在看到的函数一样快。
  • 您可能可以用 SQL EXISTS 替换您的函数用法并显着提高性能。
  • 必须使用函数,而不是使用内部子查询

标签: sql-server sql-server-2008 sql-server-2008-r2


【解决方案1】:

这只是在黑暗中的一个镜头,因为我们没有提供任何 ddl 或太多可以使用的东西。我想我正确地解释了标量函数中的现有逻辑。作为一般规则,您应该避免使用标志。这是一种非常老派的思维方式,根本不适合关系数据。我怀疑这可以通过了解实际需求来大大改善,但这是我能用有限的细节做的最好的事情。

CREATE FUNCTION [CheckClients]
(
    @UserId Varchar(3),
    @DbrNo varchar(10),
    @V_DBR_CLIENT varchar(6)
)
RETURNS table as return

with RowCounts as
(
    select
    (
        select COUNT(DBR_CLIENT) 
        from DBR 
        where DBR_SERIES like @DbrNo + 'T'
    ) as ClientCount
    , 
    (
        select COUNT(USR_CLI) 
        from USRAGYCLI u
        inner join DBR d on d.DBR_CLIENT = u.USR_CLI 
        where u.USR_CODE = @UserId 
            and d.DBR_SERIES like @DbrNo +'T'
    ) as UserCount
)

select case 
    when @V_DBR_CLIENT = 'XXXXXX' then
        Case when rc.UserCount <> rc.ClientCount then 0
            when rc.ClientCount < 0 then 0
            else 1
        end
    else 1
    end as Flag
from RowCounts rc

【讨论】:

  • 感谢您的回答。已尝试您的功能,但性能似乎没有变化。
  • 您应该衡量性能,而不仅仅是运行它,看看它需要多长时间。查看执行计划并找出瓶颈所在。我在此处发布的函数将执行标量函数并且更加灵活。但这可能是函数内部的查询是问题所在。我们没有表定义或执行计划来查看实际发生的情况。我们只是在猜测最有可能出现的问题。
【解决方案2】:

您可以优化标量函数查询以减少多次读取。喜欢:

ALTER FUNCTION [CheckClients] (
    @UserId VARCHAR(3),
    @DbrNo VARCHAR(10),
    @V_DBR_CLIENT VARCHAR(6)
    )
RETURNS INT
AS
BEGIN
    DECLARE @Flag INT

    SET @Flag = 1

    IF (@V_DBR_CLIENT = 'XXXXXX')
    BEGIN

        DECLARE @Count INT = ISNULL((
                SELECT COUNT(DBR_CLIENT)
                FROM DBR
                WHERE DBR_SERIES LIKE @DbrNo + 'T'
            ), 0);

        IF (
                (ISNULL((
                    SELECT COUNT(USR_CLI)
                    FROM USRAGYCLI
                    INNER JOIN DBR ON DBR_CLIENT = USR_CLI
                    WHERE USR_CODE = @UserId
                        AND DBR_SERIES LIKE @DbrNo + 'T'
                ), 0) <> @Count)
                OR (@Count <= 0)
            )
        BEGIN
            SET @Flag = 0
        END

    END

    RETURN @Flag
END

此外,您需要研究查询的执行计划,以找出查询执行时间成本高的地方。并在必要时创建非聚集索引。

--稍后编辑--

非 Sargable 问题(调用标量函数):

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [SEL_CLI]
    @V_USER_ID VARCHAR(3),
    @V_NUMBER_OF_ROWS INT,
    @V_STARTS_WITH INT  
AS
BEGIN
    CREATE TABLE #tmpDbrNo 
    (
         Code VARCHAR(10),
         Name VARCHAR(100),
         NumberOfDebtors int,
         rownum int 
    )   

    ;WITH Temp AS
    (
        SELECT 
            CLT_NO AS Code,
            CLT_NAME AS Name,
            COUNT(DBR_NO) AS NumberOfDebtors
        FROM
            DBR 
        JOIN 
            USRAGYCLI ON DBR_CLIENT = USR_AGY_CLI
        JOIN 
            CLT ON DBR_CLIENT = CLT_NO
        WHERE       
            USR_CODE = @V_USER_ID           
            AND 1 = 
            (CASE 
                WHEN (@V_DBR_CLIENT = 'XXXXXX') THEN 
                    (CASE 
                        WHEN (
                                ISNULL((
                                    SELECT COUNT(USR_CLI)
                                    FROM USRAGYCLI
                                    INNER JOIN DBR ON DBR_CLIENT = USR_CLI
                                    WHERE USR_CODE = @UserId
                                        AND DBR_SERIES LIKE @DbrNo + 'T'
                                ), 0) <> ISNULL((
                                    SELECT COUNT(DBR_CLIENT)
                                    FROM DBR
                                    WHERE DBR_SERIES LIKE @DbrNo + 'T'
                                ), 0)
                            )
                            OR (ISNULL((
                                    SELECT COUNT(DBR_CLIENT)
                                    FROM DBR
                                    WHERE DBR_SERIES LIKE @DbrNo + 'T'
                                ), 0) <= 0)            
                        THEN 0 
                        ELSE 1 
                    END)
                ELSE 1 
            END)--CheckClients(@V_USER_ID, DBR_NO, DBR_CLIENT)
        GROUP BY        
            CLT_NO, CLT_NAME
    )               
    INSERT INTO #tmpDbrNo   
        SELECT 
            Code, Name, NumberOfDebtors, 
            ROW_NUMBER() OVER (ORDER by Code) rownum 
        FROM
            Temp

    SELECT 
        Code, Name, NumberOfDebtors  
    FROM
        #tmpDbrNo 
    WHERE
        rownum BETWEEN @V_STARTS_WITH AND @V_STARTS_WITH + @V_NUMBER_OF_ROWS
END

如您所见,标量函数可以包含在同一个查询中,但如果您仔细研究该函数,很明显标量函数中的查询并不完全依赖于存储过程中的查询。它正在计数,并且每次都会重新读取和操作表中的数据。

因此,将这种类型的查询从非 Sargable 变为 Sargable 不会提高性能。该问题的可能解决方案是

  1. 以前在表中添加所需的数据并从那里检查。
  2. 研究您的查询计划(设计和执行)并进行相应优化。

【讨论】:

  • 这仍然是一个标量函数,OP 从 where 子句中调用它。它会更快但几乎没有。它仍然是一个非 SARGable 标量函数。
  • 没错,但我认为标量函数不是这里的大问题。问题在于这里使用的“LIKE”运算符,查询只是计算记录,所以,如果它有 DBR_SERIES 的索引,它可以执行得更好。
  • 我们不知道该列是否有索引。问题可能出在 LIKE 运算符上,但我们甚至不知道作为值传递的内容。如果它是一个领先的通配符,那么再多的索引也无济于事。当然,只要你坚持标量函数索引是没有意义的,因为它是 nonSARGable。
  • 我已经添加了一些东西,我认为这将成为一个解决方案。
猜你喜欢
  • 1970-01-01
  • 2019-12-28
  • 1970-01-01
  • 1970-01-01
  • 2018-01-10
  • 2017-08-04
  • 1970-01-01
  • 1970-01-01
  • 2018-09-12
相关资源
最近更新 更多