簡體   English   中英

功能慢,但查詢運行快

[英]Function is slow but query runs fast

我有一個簡單的表值函數,大約需要5秒才能執行。 該函數保留一個查詢,該查詢將在1秒內返回數據。 我已經讀過一些博客,據說這可能是由於參數嗅探引起的,但尚未找到解決方法。 如果由於參數嗅探而導致該如何解決?

CREATE FUNCTION [dbo].[fn_PurchaseRecord]
(
@ID INT = NULL,
@Name nvarchar(MAX),
@PurchaseDate DATE
)
RETURNS  @result TABLE 
(
[ID] [int]  NULL,
[Name] [varchar](20) NULL,
[BasePrice] [FLOAT] NULL,
[Amount] [FLOAT]
)

AS BEGIN
WITH CTE_Purchase AS 
    (
    SELECT
        ht.ID,
        ProductName                             AS Name,
        BasePrice                               AS BasePrice
    FROM
        data.PurchaseRecord i (NOLOCK)
    WHERE  
        i.ID = @ID
        AND
        Date = @PurchaseDate
        AND
        BuyerName=@Name
        )
INSERT INTO @result
SELECT
    ID,
    Name,
    BasePrice,
    BasePrice*10.25
FROM
    CTE_Purchase
RETURN;

結束

為什么不使用單語句TVF?

CREATE FUNCTION [dbo].[fn_PurchaseRecordTESTFIRST]
(
@ID INT = NULL,
@Name nvarchar(MAX),
@PurchaseDate DATE
)
RETURNS TABLE 

Return (

    SELECT ID
          ,Name = ProductName
          ,BasePrice
          ,Amount = BasePrice*10.25
    FROM  data.PurchaseRecord i 
    WHERE i.ID = @ID
      AND Date = @PurchaseDate
      AND BuyerName=@Name
)

如果發生參數嗅探,這是您最少的擔心-當肖恩(Sean)說要避免像瘟疫那樣避免使用多語句表值函數(mTVF)時,他的想法就很少了。 通過設計,它們將比內聯表值函數(iTVF)慢得多,因為您定義了一個表,然后填充該表,然后將其返回。 另一方面,iTVF可以看作是接受參數並直接從基礎表返回數據的視圖。

mTVF的另一個巨大問題是它們殺死了並行性。 這意味着如果您有2個CPU或2,000個CPU,則只有一個能解決您的查詢。 沒有例外。 看看Jeff Moden的delimitedsplit8K:

CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
        (@pString VARCHAR(8000), @pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE!  IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
     -- enough to cover VARCHAR(8000)
  WITH E1(N) AS (
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
                ),                          --10E+1 or 10 rows
       E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
       E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
 cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
                     -- for both a performance gain and prevention of accidental "overruns"
                 SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
                ),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
                 SELECT 1 UNION ALL
                 SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter
                ),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
                 SELECT s.N1,
                        ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1,8000)
                   FROM cteStart s
                )
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
 SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
        Item       = SUBSTRING(@pString, l.N1, l.L1)
   FROM cteLen l;
GO

現在讓我們構建一個像這樣的mTVF版本並進行性能測試...

CREATE FUNCTION [dbo].[DelimitedSplit8K_MTVF]
        (@pString VARCHAR(8000), @pDelimiter CHAR(1))
RETURNS @table TABLE (ItemNumber int, Item varchar(100)) 
AS
BEGIN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
     -- enough to cover VARCHAR(8000)
  WITH E1(N) AS (
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
                ),                          --10E+1 or 10 rows
       E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
       E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
 cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
                     -- for both a performance gain and prevention of accidental "overruns"
                 SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
                ),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
                 SELECT 1 UNION ALL
                 SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter
                ),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
                 SELECT s.N1,
                        ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1,8000)
                   FROM cteStart s
                )
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
 INSERT @table
 SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
        Item       = SUBSTRING(@pString, l.N1, l.L1)
   FROM cteLen l;

 RETURN;
END
GO

在繼續之前,我想談談@John Cappelletti的聲明:

在[關於MAX數據類型]之前,我已經看到過這樣的聲明,但是我還沒有看到任何令人信服的統計數據

對於一些引人注目的統計數據,讓我們對iTVF版本的delimitedSplit8K進行一個小調,將輸入字符串更改為varchar(max):

CREATE FUNCTION [dbo].[DelimitedSplit8K_VCMAXINPUT]
        (@pString VARCHAR(max), @pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
 RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
     -- enough to cover VARCHAR(8000)
  WITH E1(N) AS (
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
                 SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
                ),                          --10E+1 or 10 rows
       E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
       E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
 cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
                     -- for both a performance gain and prevention of accidental "overruns"
                 SELECT TOP (ISNULL(DATALENGTH(@pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
                ),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
                 SELECT 1 UNION ALL
                 SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(@pString,t.N,1) = @pDelimiter
                ),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
                 SELECT s.N1,
                        ISNULL(NULLIF(CHARINDEX(@pDelimiter,@pString,s.N1),0)-s.N1,8000)
                   FROM cteStart s
                )
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
 SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
        Item       = SUBSTRING(@pString, l.N1, l.L1)
   FROM cteLen l;
GO

現在,我們有了該函數的三個版本:原始iTVF,一個接受varchar(max)的版本和一個mTVF版本。 現在進行性能測試。

-- sample data
IF OBJECT_ID('tempdb..#string') IS NOT NULL DROP TABLE #string;
SELECT TOP (10000) 
  id  = IDENTITY(int, 1,1), 
  txt = REPLICATE(newid(), ABS(checksum(newid())%5)+1)
INTO #string
FROM sys.all_columns a, sys.all_columns b;

SET NOCOUNT ON;

-- Performance tests:
PRINT 'ITVF 8K'+char(13)+char(10)+replicate('-',90);
GO
DECLARE @st datetime2 = getdate(), @x varchar(20);
SELECT  @x = ds.Item
FROM #string s
CROSS APPLY dbo.DelimitedSplit8K(s.txt, '-') ds;
PRINT datediff(ms, @st, getdate());
GO 5

PRINT 'MTVF 8K'+char(13)+char(10)+replicate('-',90);
GO
DECLARE @st datetime2 = getdate(), @x varchar(20);
SELECT  @x = ds.Item
FROM #string s
CROSS APPLY dbo.DelimitedSplit8K_MTVF(s.txt, '-') ds;
PRINT datediff(ms, @st, getdate());
GO 5

PRINT 'ITVF VCMAX'+char(13)+char(10)+replicate('-',90);
GO
DECLARE @st datetime2 = getdate(), @x varchar(20);
SELECT  @x = ds.Item
FROM #string s
CROSS APPLY dbo.DelimitedSplit8K_VCMAXINPUT(s.txt, '-') ds;
PRINT datediff(ms, @st, getdate());
GO 5

結果:

ITVF 8K
------------------------------------------------------------------------------------------
Beginning execution loop
280
267
284
300
280
Batch execution completed 5 times.

MTVF 8K
------------------------------------------------------------------------------------------
Beginning execution loop
1190
1190
1157
1173
1187
Batch execution completed 5 times.

ITVF VCMAX
------------------------------------------------------------------------------------------
Beginning execution loop
1204
1220
1190
1190
1203
Batch execution completed 5 times.

使用varchar(max)的mTVF和iTVF版本都慢4-5倍。 再次: 避免使用鼠疫之類的mTVF,並盡可能避免使用最大數據類型。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM