我改变了标题。它与使用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是非常不符合要求的。
以下是图片中的两个执行计划:
这是XML中的执行计划。它还包含Sean的ITVF版本。它太大了,不适合这里,所以我把它托管在文本上传网站上。
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
答案 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
此处的执行计划比您之前发布的任何一个都简单得多,并且没有嵌套循环。