Sqlserver
 sql >> Database >  >> RDS >> Sqlserver

Il piano basato su set viene eseguito più lentamente della funzione con valore scalare con molte condizioni

Il termine chiave qui è INLINE FUNZIONI CON VALORI TABELLA . Sono disponibili due tipi di funzioni con valori con tabelle T-SQL:multi-statement e inline. Se la tua funzione T-SQL inizia con un'istruzione BEGIN, sarà una schifezza:scalare o altro. Non puoi inserire una tabella temporanea in un inline funzione con valore di tabella, quindi suppongo che tu sia passato da scalare a funzione con valore di tabella multi-istruzione che probabilmente sarà peggiore.

La tua funzione con valori di tabella in linea (iTVF) dovrebbe assomigliare a questa:

CREATE FUNCTION [dbo].[Compute_value]
(
  @alpha FLOAT,
  @bravo FLOAT,
  @charle FLOAT,
  @delta FLOAT
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT newValue = 
  CASE WHEN @alpha IS NULL OR @alpha = 0 OR @delta IS NULL OR @delta = 0 THEN 0
       WHEN @bravo IS NULL OR @bravo <= 0 THEN 100
       ELSE @alpha * POWER((100 / @delta), 
             (-2 * POWER(@charle * @bravo, DATEDIFF(<unit of measurement>,GETDATE(),'1/1/2000')/365)))
  END
GO;

Nota che, nel codice che hai pubblicato, il tuo DATEDIFF nell'istruzione manca il datepart parametro. Se dovrebbe assomigliare a:

@x int = DATEDIFF(DAY, GETDATE(),'1/1/2000')   

Andando un po' oltre:è importante capire perché gli iTVF sono migliori delle funzioni definite dall'utente con valore scalare T-SQL. Non è perché le funzioni con valori di tabella sono più veloci delle funzioni con valori scalari, è perché l'implementazione di Microsoft delle funzioni inline T-SQL è più veloce della loro implementazione di funzioni T-SQL che non sono inline. Nota le seguenti tre funzioni che fanno la stessa cosa:

-- Scalar version
CREATE FUNCTION dbo.Compute_value_scalar
(
  @alpha FLOAT,
  @bravo FLOAT,
  @charle FLOAT,
  @delta FLOAT
)
RETURNS FLOAT
AS
BEGIN
    IF @alpha IS NULL OR @alpha = 0 OR @delta IS NULL OR @delta = 0 
    RETURN 0

    IF @bravo IS NULL OR @bravo <= 0
        RETURN 100

    IF (@charle + @delta) / @bravo <= 0
        RETURN 100
    DECLARE @x int = DATEDIFF(dd, GETDATE(),'1/1/2000')     
    RETURN @alpha * POWER((100 / @delta), (-2 * POWER(@charle * @bravo, @x/365)))
END
GO

-- multi-statement table valued function 
CREATE FUNCTION dbo.Compute_value_mtvf
(
  @alpha FLOAT,
  @bravo FLOAT,
  @charle FLOAT,
  @delta FLOAT
)
RETURNS  @sometable TABLE (newValue float) AS 
    BEGIN
    INSERT @sometable VALUES
(
  CASE WHEN @alpha IS NULL OR @alpha = 0 OR @delta IS NULL OR @delta = 0 THEN 0
       WHEN @bravo IS NULL OR @bravo <= 0 THEN 100
       ELSE @alpha * POWER((100 / @delta), 
             (-2 * POWER(@charle * @bravo, DATEDIFF(DAY,GETDATE(),'1/1/2000')/365)))
  END
)
RETURN;
END
GO

-- INLINE table valued function
CREATE FUNCTION dbo.Compute_value_itvf
(
  @alpha FLOAT,
  @bravo FLOAT,
  @charle FLOAT,
  @delta FLOAT
)
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT newValue = 
  CASE WHEN @alpha IS NULL OR @alpha = 0 OR @delta IS NULL OR @delta = 0 THEN 0
       WHEN @bravo IS NULL OR @bravo <= 0 THEN 100
       ELSE @alpha * POWER((100 / @delta), 
             (-2 * POWER(@charle * @bravo, DATEDIFF(DAY,GETDATE(),'1/1/2000')/365)))
  END
GO

Ora per alcuni dati di esempio e test delle prestazioni:

SET NOCOUNT ON;
CREATE TABLE #someTable (alpha FLOAT, bravo FLOAT, charle FLOAT, delta FLOAT);
INSERT #someTable
SELECT TOP (100000)
  abs(checksum(newid())%10)+1, abs(checksum(newid())%10)+1, 
  abs(checksum(newid())%10)+1, abs(checksum(newid())%10)+1
FROM sys.all_columns a, sys.all_columns b;

PRINT char(10)+char(13)+'scalar'+char(10)+char(13)+replicate('-',60);
GO
DECLARE @st datetime = getdate(), @z float;

SELECT @z = dbo.Compute_value_scalar(t.alpha, t.bravo, t.charle, t.delta)
FROM #someTable t;

PRINT DATEDIFF(ms, @st, getdate());
GO

PRINT char(10)+char(13)+'mtvf'+char(10)+char(13)+replicate('-',60);
GO
DECLARE @st datetime = getdate(), @z float;

SELECT @z = f.newValue
FROM #someTable t
CROSS APPLY dbo.Compute_value_mtvf(t.alpha, t.bravo, t.charle, t.delta) f;

PRINT DATEDIFF(ms, @st, getdate());
GO

PRINT char(10)+char(13)+'itvf'+char(10)+char(13)+replicate('-',60);
GO
DECLARE @st datetime = getdate(), @z float;

SELECT @z = f.newValue
FROM #someTable t
CROSS APPLY dbo.Compute_value_itvf(t.alpha, t.bravo, t.charle, t.delta) f;

PRINT DATEDIFF(ms, @st, getdate());
GO

Risultati:

scalar
------------------------------------------------------------
2786

mTVF
------------------------------------------------------------
41536

iTVF
------------------------------------------------------------
153

L'udf scalare ha funzionato per 2,7 secondi, 41 secondi per mtvf e 0,153 secondi per iTVF. Per capire perché, diamo un'occhiata ai piani di esecuzione stimati:

Non lo vedi quando guardi il piano di esecuzione effettivo ma, con lo scalare udf e mtvf, l'ottimizzatore chiama alcune subroutine mal eseguite per ogni riga; l'iTVF no. Citando il cambiamento di carriera di Paul White articolo su APPLICA Paolo scrive:

In altre parole, iTVF consente all'ottimizzatore di ottimizzare la query in modi che semplicemente non sono possibili quando è necessario eseguire tutto l'altro codice. Uno dei tanti altri esempi del motivo per cui gli iTVF sono superiori è che sono l'unico dei tre tipi di funzione sopra menzionati che consentono il parallelismo. Eseguiamo ogni funzione ancora una volta, questa volta con il piano di esecuzione effettivo attivato e con traceflag 8649 (che forza un piano di esecuzione parallela):

-- don't need so many rows for this test
TRUNCATE TABLE #sometable;
INSERT #someTable 
SELECT TOP (10)
  abs(checksum(newid())%10)+1, abs(checksum(newid())%10)+1, 
  abs(checksum(newid())%10)+1, abs(checksum(newid())%10)+1
FROM sys.all_columns a;

DECLARE @x float;

SELECT TOP (10) @x = dbo.Compute_value_scalar(t.alpha, t.bravo, t.charle, t.delta)
FROM #someTable t
ORDER BY dbo.Compute_value_scalar(t.alpha, t.bravo, t.charle, t.delta)
OPTION (QUERYTRACEON 8649);

SELECT TOP (10)  @x = f.newValue
FROM #someTable t
CROSS APPLY dbo.Compute_value_mtvf(t.alpha, t.bravo, t.charle, t.delta) f
ORDER BY f.newValue
OPTION (QUERYTRACEON 8649);

SELECT @x = f.newValue
FROM #someTable t
CROSS APPLY dbo.Compute_value_itvf(t.alpha, t.bravo, t.charle, t.delta) f
ORDER BY f.newValue
OPTION (QUERYTRACEON 8649);

Piani di esecuzione:

Quelle frecce che vedi per il piano di esecuzione di iTVF sono il parallelismo:tutte le tue CPU (o tante quante sono MAXDOP della tua istanza SQL impostazioni consentono) lavorando insieme. Le UDF scalari T-SQL e mtvf non possono farlo. Quando Microsoft introduce le UDF scalari inline, le suggerirei per quello che stai facendo ma, fino ad allora:se le prestazioni sono ciò che stai cercando, allora inline è l'unica strada da percorrere e, per questo, gli iTVF sono l'unico gioco in città.

Nota che ho continuamente enfatizzato T-SQL quando si parla di funzioni... CLR Scalar e le funzioni con valori di tabella possono andare bene, ma questo è un argomento diverso.