Sqlserver
 sql >> Base de Dados >  >> RDS >> Sqlserver

O plano baseado em conjunto é executado mais lentamente do que a função de valor escalar com muitas condições


O termo da palavra-chave aqui é INLINE FUNÇÕES COM VALOR DA TABELA . Você tem dois tipos de funções com valor tabelado T-SQL:multi-instrução e em linha. Se sua função T-SQL começar com uma instrução BEGIN, ela será uma porcaria - escalar ou não. Você não pode colocar uma tabela temporária em um inline função com valor de tabela, então estou assumindo que você passou de escalar para função com valor de tabela de várias instruções, o que provavelmente será pior.

Sua função com valor de tabela embutida (iTVF) deve se parecer com isto:
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;

Observe que, no código que você postou, seu DATEDIFF está faltando a instrução datepart parâmetro. Se deve ser algo como:
@x int = DATEDIFF(DAY, GETDATE(),'1/1/2000')   

Indo um pouco mais longe - é importante entender por que os iTVFs são melhores do que as funções definidas pelo usuário com valor escalar T-SQL. Não é porque as funções com valor de tabela são mais rápidas do que as funções com valor escalar, é porque a implementação da Microsoft de funções T-SQL inline são mais rápidas do que a implementação de funções T-SQL que não são inline. Observe as três funções a seguir que fazem a mesma coisa:
-- 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

Agora, para alguns dados de amostra e teste de desempenho:
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

Resultados:
scalar
------------------------------------------------------------
2786

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

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

O udf escalar funcionou por 2,7 segundos, 41 segundos para o mtvf e 0,153 segundos para o iTVF. Para entender o porquê, vejamos os planos de execução estimados:



Você não vê isso quando olha para o plano de execução real, mas, com o udf escalar e o mtvf, o otimizador chama algumas sub-rotinas mal executadas para cada linha; o iTVF não. Citando a mudança de carreira de Paul White artigo sobre APPLY Paulo escreve:

Em outras palavras, o iTVF permite otimizar para otimizar a consulta de maneiras que simplesmente não são possíveis quando todo esse outro código precisa ser executado. Um dos muitos outros exemplos de por que os iTVFs são superiores é que eles são o único dos três tipos de função mencionados acima que permitem o paralelismo. Vamos executar cada função mais uma vez, desta vez com o plano Real Execution ativado e com traceflag 8649 (que força um plano de execução paralela):
-- 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);

Planos de execução:



Essas setas que você vê para o plano de execução do iTVF são paralelismo - todas as suas CPUs (ou tantas quantas MAXDOP da sua instância SQL) configurações permitem) trabalhando em conjunto. UDFs escalares e mtvf T-SQL não podem fazer isso. Quando a Microsoft introduzir UDFs escalares inline, eu sugeriria aqueles para o que você está fazendo, mas, até então:se o desempenho é o que você procura, o inline é o único caminho a seguir e, para isso, os iTVFs são o único jogo na cidade.

Observe que enfatizei continuamente o T-SQL ao falar sobre funções... Funções CLR Scalar e Table value podem ser boas, mas isso é um tópico diferente.