2017-06-01 208 views
0

我有一個簡單的Table-Valued函數,需要大約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 
     [email protected] 
     ) 
INSERT INTO @result 
SELECT 
    ID, 
    Name, 
    BasePrice, 
    BasePrice*10.25 
FROM 
    CTE_Purchase 
RETURN; 

END

+2

你能發表功能代碼嗎? –

+0

@MikhailLobanov我剛剛添加了代碼。 –

+1

多語句表值函數的性能通常比標量函數更差。它們簡直是可怕的,應該像瘟疫一樣避免。此外,由於這似乎是某種財務應用程序,我強烈建議你停止在任何地方散佈NOLOCK提示的壞習慣。這個暗示有很多人沒有意識到的包袱。 http://blogs.sqlsentry.com/aaronbertrand/bad-habits-nolock-everywhere/ –

回答

2

爲什麼不單語句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 [email protected] 
) 
+0

我已經從函數中刪除了實際的業務邏輯並創建了一個示例函數。我需要在函數中使用IF,因此MSTVF。 –

+0

@AmaanKhan對不起,但沒有看到實際的功能,我沒有看到任何人可以做出明智的建議。 –

+0

小點。爲什麼在@name中使用nvarchar(max)? LOB數據存儲在完全獨立的數據頁面中,會減慢查詢速度。 nvarchar(4000)不會很多嗎?如果是這樣,那會加快速度。這不是問題的根源,但肯定會提高性能。 –

1

如果參數嗅探是發生它是你最擔心的 - 說多語句表值函數(mTVFs)應該避免像瘟疫時,肖恩擊中頭部釘。通過設計,它們將比內聯表值函數(iTVF)慢得多,因爲您可以定義一個表,填充它,然後返回它。另一方面,iTVF可以被認爲是接受參數並直接從底層表返回數據的視圖。

mTVFs的另一個巨大問題是它們會殺死並行性;這意味着如果您有2個CPUS或2,000個CPU,則只有一個將解決您的查詢問題。沒有例外。看起來看看傑夫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 

繼續我想解決@約翰卡佩萊蒂年代以前聲明:

我見過的權利要求像在此之前[關於MAX數據類型],但我還沒有看到任何令人信服的統計

對於一些引人注目的統計讓我們做一個小tweek到delimitedSplit8K的iTVF版本和改變輸入字符串爲varchar(最大):

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. 

兩者mTVF和iTVF版本需要VARCHAR(最大)是4-5倍慢。再次:避免mTVFs像瘟疫,並儘可能避免最大數據類型。

相關問題