为什么在WHERE子句中调用CHARINDEX时,此查询在SQL Server 2014中表现如此糟糕?

时间:2015-10-28 16:17:57

标签: sql-server sql-server-2014

我改变了标题。它与使用UDF无关。这完全归功于CHARINDEX和LEFT JOIN。

原始问题

我在一个命令中有多个查询,该命令在大约5秒内在SQL Server 2005中运行得很好。我们最近迁移到SQL Server 2014,整个命令现在需要4分钟才能运行!

我把它一块一块地拆开,以确切地知道它在哪里放慢速度。在一个查询中,有一个表,其中大约有80,000行需要由UDF过滤。 UDF在单个值上使用LIKE进行一些简单的模式匹配。该表正在参与LEFT JOIN,并且在WHERE子句中调用UDF。这显然是SQL Server 2014的一个问题。当我在派生表中移动UDF调用并稍后对我生成的列进行过滤时,它运行正常。

UDF实际上被调用了两次,但只使用一个表中的列。在查看执行计划时,运行UDF的Filter操作在Join操作之前,因此在应用连接条件之前,它不像在交叉连接的每一行上运行一样愚蠢。我真的无法解释为什么表现如此糟糕。

我制作了一个完整的示例脚本,以简单的方式公开问题。这不是我真正的查询,除了UDF的主体是相同的,但它包含相同的问题。

该脚本运行两次相同的基本查询。第一个在我的服务器上运行80秒,第二个在1/10秒内运行!

SET NOCOUNT ON

--Create a table to hold numbers as strings.
IF OBJECT_ID( 'tempdb..#Numbers' ) IS NOT NULL
    DROP TABLE #Numbers

CREATE TABLE #Numbers
(
    n varchar(50),
    n2 varchar(50)
)

INSERT #Numbers (n) values (1)
GO

--Double the number of rows in the table. This batch runs 14 times.
INSERT #Numbers 
( 
    n 
)
SELECT 
    CONVERT( int, n ) + (SELECT COUNT(*) FROM #Numbers)     
FROM 
    #Numbers
GO 14

--Populate the n2 column
UPDATE #Numbers SET n2 = 'A-' + n
GO

--Drop and create a UDF that uses pattern matching.
IF OBJECT_ID( '[dbo].[udf_Temp_SomePatternMatch]' ) IS NOT NULL
    DROP FUNCTION [dbo].[udf_Temp_SomePatternMatch]
GO

CREATE FUNCTION [dbo].[udf_Temp_SomePatternMatch]
(
    @SerialNumber varchar(50)
)
RETURNS bit
AS
BEGIN
    DECLARE @IsMatch bit

    SET @IsMatch = 
        CASE 
            WHEN 
                CHARINDEX( '-', @SerialNumber ) = 0 
                AND @SerialNumber NOT LIKE '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
                AND @SerialNumber NOT LIKE '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
                    THEN CAST( 1 AS bit )
            ELSE
                CAST( 0 AS bit )
        END

    RETURN @IsMatch
END
GO

--print an initial timestamp.
PRINT CONVERT( varchar, getdate(), 114 )

--The normal way with UDF calls in the WHERE clause. Runs in 80 seconds on my server.
SELECT
    COUNT(*)
FROM
    #Numbers Numbers
        LEFT JOIN
    #Numbers AS Numbers2
        ON Numbers2.n2 = Numbers.n
WHERE
    [dbo].[udf_Temp_SomePatternMatch]( Numbers.n ) = 1
    AND [dbo].[udf_Temp_SomePatternMatch]( Numbers.n2 ) = 0


PRINT CONVERT( varchar, getdate(), 114 )

--The "better" way. Runs in 1/10 of a second!
SELECT
    COUNT(*)
FROM
    (
        SELECT
            n,
            n2,
            [dbo].[udf_Temp_SomePatternMatch]( n ) AS nIsMatch,
            [dbo].[udf_Temp_SomePatternMatch]( n2 ) AS n2IsMatch
        FROM
            #Numbers
    ) Numbers
        LEFT JOIN
    #Numbers AS Numbers2
        ON Numbers2.n2 = Numbers.n
WHERE
    nIsMatch = 1
    AND n2IsMatch = 0

PRINT CONVERT( varchar, getdate(), 114 )

DROP FUNCTION [dbo].[udf_Temp_SomePatternMatch]

显然,在这种情况下,WHERE子句可能只是在派生表中,但在我的实际查询中我需要将结果与另一个表中的另一列进行OR,所以我只是计算派生表中的值并使用它作为过滤器。一旦UDF在派生表中,WHERE子句的位置无关紧要,但在WHERE子句中运行UDF是非常不符合要求的。

以下是图片中的两个执行计划:

Execution plans of the two queries in the script.

更新:

这是XML中的执行计划。它还包含Sean的ITVF版本。它太大了,不适合这里,所以我把它托管在文本上传网站上。

http://textuploader.com/5dhn6

UPDATE2:

UDF本身不是问题,也不是LIKE。这是CHARINDEX的问题。此查询还需要80秒才能运行,并且只有一次调用CHARINDEX:

SELECT
    COUNT(*)
FROM
    #Numbers Numbers
        LEFT JOIN
    #Numbers AS Numbers2
        ON Numbers2.n2 = Numbers.n
WHERE
    CHARINDEX( '-', Numbers.n ) = 0

如果我将其切换为INNER JOIN,CROSS JOIN或在左连接的右表上运行CHARINDEX,查询持续时间将降至1秒。嵌套循环仍然出现在执行计划中。

令人惊讶的是,使用相同的派生表模式(速度很快),使用CHARINDEX时速度并不快。此查询还需要80秒才能运行:

SELECT
    COUNT(*)
FROM
    (
        SELECT
            n,
            n2,
            CHARINDEX( '-', n ) AS i
        FROM
            #Numbers
    ) Numbers
        LEFT JOIN
    #Numbers AS Numbers2
        ON Numbers2.n2 = Numbers.n
WHERE
    i = 0

这也不是更快:

SELECT
    COUNT(*)
FROM
    (
        SELECT
            n,
            n2          
        FROM
            #Numbers
        WHERE
            CHARINDEX( '-', n ) = 0
    ) Numbers
        LEFT JOIN
    #Numbers AS Numbers2
        ON Numbers2.n2 = Numbers.n

然而,这个在不到一秒的时间内运行,并使用Hash Match而不是Nested Loops:

SELECT
    COUNT(*)
FROM
    #Numbers Numbers
        LEFT JOIN
    #Numbers AS Numbers2
        ON CHARINDEX( '-', Numbers.n ) = 0 AND Numbers2.n2 = Numbers.n

1 个答案:

答案 0 :(得分:1)

我仍然认为罪魁祸首是标量函数。以下是如何将该标量函数更改为内联表值函数。

CREATE FUNCTION [dbo].[SomePatternMatch]
(
    @SerialNumber varchar(50)
) RETURNS TABLE WITH SCHEMABINDING AS RETURN

SELECT
    CASE WHEN CHARINDEX( '-', @SerialNumber ) = 0 
            AND @SerialNumber NOT LIKE '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
            AND @SerialNumber NOT LIKE '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
        THEN CAST( 1 AS bit )
    ELSE
        CAST( 0 AS bit )
    END as IsMatch

现在使用它非常简单。

SELECT
    COUNT(*)
FROM #Numbers Numbers
LEFT JOIN #Numbers AS Numbers2 ON Numbers2.n2 = Numbers.n
CROSS APPLY dbo.SomePatternMatch(Numbers.n) m1
CROSS APPLY dbo.SomePatternMatch(Numbers.n2) m2
WHERE m1.IsMatch = 1
    AND m2.IsMatch = 0

此处的执行计划比您之前发布的任何一个都简单得​​多,并且没有嵌套循环。