A maneira mais eficiente de chamar a mesma função com valor de tabela em várias colunas em uma consulta

8

Estou tentando ajustar uma consulta em que a mesma função com valor de tabela (TVF) é chamada em 20 colunas.

A primeira coisa que fiz foi converter a função escalar em uma função com valor de tabela embutido.

Está usando CROSS APPLYa maneira com melhor desempenho para executar a mesma função em várias colunas em uma consulta?

Um exemplo simplista:

SELECT   Col1 = A.val
        ,Col2 = B.val
        ,Col3 = C.val
        --do the same for other 17 columns
        ,Col21
        ,Col22
        ,Col23
FROM t
CROSS APPLY
    dbo.function1(Col1) A
CROSS APPLY
    dbo.function1(Col2) B
CROSS APPLY
    dbo.function1(Col3) C
--do the same for other 17 columns

Existem alternativas melhores?

A mesma função pode ser chamada em várias consultas no número X de colunas.

Aqui está a função:

CREATE FUNCTION dbo.ConvertAmountVerified_TVF
(
    @amt VARCHAR(60)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
    WITH cteLastChar
    AS(
        SELECT LastChar = RIGHT(RTRIM(@amt), 1)
    )
    SELECT
        AmountVerified  = CAST(RET.Y AS NUMERIC(18,2))
    FROM (SELECT 1 t) t
    OUTER APPLY (
        SELECT N =
                CAST(
                    CASE 
                        WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                            THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0)-1
                        WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0) >0
                            THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0)-1
                        WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0) >0
                            THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0)-1
                        ELSE 
                            NULL
                    END
                AS VARCHAR(1))
        FROM
            cteLastChar L
    ) NUM
    OUTER APPLY (
        SELECT N =
            CASE 
                WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                    THEN 0
                WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQRpqrstuvwxy', 0) >0
                    THEN 1
                ELSE 0
            END
        FROM cteLastChar L
    ) NEG
    OUTER APPLY(
        SELECT Amt= CASE
                        WHEN NUM.N IS NULL
                            THEN @amt 
                        ELSE
                            SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + Num.N
                    END
    ) TP
    OUTER APPLY(
        SELECT Y =  CASE
                        WHEN NEG.N = 0
                            THEN (CAST(TP.Amt AS NUMERIC) / 100)
                        WHEN NEG.N = 1
                            THEN (CAST (TP.Amt AS NUMERIC) /100) * -1
                    END
    ) RET
) ;

GO

Aqui está a versão da função escalar que eu herdei, se alguém estiver interessado:

CREATE   FUNCTION dbo.ConvertAmountVerified 
(
    @amt VARCHAR(50)
)
RETURNS NUMERIC (18,3)  
AS
BEGIN   
    -- Declare the return variable here
    DECLARE @Amount NUMERIC(18, 3);
    DECLARE @TempAmount VARCHAR (50);
    DECLARE @Num VARCHAR(1);
    DECLARE @LastChar VARCHAR(1);
    DECLARE @Negative BIT ;
    -- Get Last Character
    SELECT @LastChar = RIGHT(RTRIM(@amt), 1) ;
    SELECT @Num = CASE @LastChar  collate latin1_general_cs_as
                        WHEN '{'  THEN '0'                                  
                        WHEN 'A' THEN '1'                       
                        WHEN 'B' THEN '2'                       
                        WHEN 'C' THEN '3'                       
                        WHEN 'D' THEN '4'                       
                        WHEN 'E' THEN '5'                       
                        WHEN 'F' THEN '6'                       
                        WHEN 'G' THEN '7'                       
                        WHEN 'H' THEN '8'                       
                        WHEN 'I' THEN '9'                       
                        WHEN '}' THEN '0'   
                        WHEN 'J' THEN '1'
                        WHEN 'K' THEN '2'                       
                        WHEN 'L' THEN '3'                       
                        WHEN 'M' THEN '4'                       
                        WHEN 'N' THEN '5'                       
                        WHEN 'O' THEN '6'                       
                        WHEN 'P' THEN '7'                       
                        WHEN 'Q' THEN '8'                       
                        WHEN 'R' THEN '9'

                        ---ASCII
                        WHEN 'p' Then '0'
                        WHEN 'q' Then '1'
                        WHEN 'r' Then '2'
                        WHEN 's' Then '3'
                        WHEN 't' Then '4'
                        WHEN 'u' Then '5'
                        WHEN 'v' Then '6'
                        WHEN 'w' Then '7'
                        WHEN 'x' Then '8'
                        WHEN 'y' Then '9'

                        ELSE ''

                END 
    SELECT @Negative = CASE @LastChar collate latin1_general_cs_as
                        WHEN '{' THEN 0         

                        WHEN 'A' THEN 0                 
                        WHEN 'B' THEN 0                     
                        WHEN 'C' THEN 0                     
                        WHEN 'D' THEN 0                     
                        WHEN 'E' THEN 0                     
                        WHEN 'F' THEN 0                     
                        WHEN 'G' THEN 0                     
                        WHEN 'H' THEN 0                     
                        WHEN 'I' THEN 0                     
                        WHEN '}' THEN 1 

                        WHEN 'J' THEN 1                     
                        WHEN 'K' THEN 1                     
                        WHEN 'L' THEN 1                     
                        WHEN 'M' THEN 1                 
                        WHEN 'N' THEN 1                     
                        WHEN 'O' THEN 1                     
                        WHEN 'P' THEN 1                     
                        WHEN 'Q' THEN 1                     
                        WHEN 'R' THEN 1

                        ---ASCII
                        WHEN 'p' Then '1'
                        WHEN 'q' Then '1'
                        WHEN 'r' Then '1'
                        WHEN 's' Then '1'
                        WHEN 't' Then '1'
                        WHEN 'u' Then '1'
                        WHEN 'v' Then '1'
                        WHEN 'w' Then '1'
                        WHEN 'x' Then '1'
                        WHEN 'y' Then '1'
                        ELSE 0
                END 
    -- Add the T-SQL statements to compute the return value here
    if (@Num ='')
    begin
    SELECT @TempAmount=@amt;
    end 
    else
    begin
    SELECT @TempAmount = SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + @Num;

    end
    SELECT @Amount = CASE @Negative
                     WHEN 0 THEN (CAST(@TempAmount AS NUMERIC) / 100)
                     WHEN 1 THEN (CAST (@TempAmount AS NUMERIC) /100) * -1
                     END ;
    -- Return the result of the function
    RETURN @Amount

END

Dados de teste de amostra:

SELECT dbo.ConvertAmountVerified('00064170')    --  641.700
SELECT * FROM dbo.ConvertAmountVerified_TVF('00064170') --  641.700

SELECT dbo.ConvertAmountVerified('00057600A')   --  5760.010
SELECT * FROM dbo.ConvertAmountVerified_TVF('00057600A')    --  5760.010

SELECT dbo.ConvertAmountVerified('00059224y')   --  -5922.490
SELECT * FROM dbo.ConvertAmountVerified_TVF('00059224y')    --  -5922.490
Mazhar
fonte

Respostas:

8

PRIMEIRO: deve-se mencionar que o método absolutamente mais rápido de obter os resultados desejados é o seguinte:

  1. Migrar dados para novas colunas ou mesmo para uma nova tabela:
    1. Nova abordagem de coluna:
      1. Adicione novas colunas {name}_newà tabela com o DECIMAL(18, 3)tipo de dados
      2. Faça uma migração única dos dados das VARCHARcolunas antigas para as DECIMALcolunas
      3. renomeie as colunas antigas para {name}_old
      4. renomear novas colunas para ser apenas {name}
    2. Nova abordagem de tabela:
      1. Crie uma nova tabela como {table_name}_newusando o DECIMAL(18, 3)tipo de dados
      2. Faça uma migração única dos dados da tabela atual para a nova DECIMALtabela.
      3. renomeie a tabela antiga para _old
      4. remover _newda nova tabela
  2. Atualizar aplicativo etc. para nunca inserir dados codificados dessa maneira
  3. após um ciclo de lançamento, se não houver problemas, descarte colunas ou tabelas antigas
  4. descartar TVFs e UDF
  5. Nunca mais fale disso!

QUE ESTÁ DIZIDO: Você pode se livrar de muito desse código, pois é uma duplicação em grande parte desnecessária. Além disso, há pelo menos dois bugs que, por vezes, fazem com que a saída esteja incorreta ou, às vezes, gere um erro. E esses bugs foram copiados no código de Joe, pois produzem os mesmos resultados (incluindo o erro) que o código do OP. Por exemplo:

  • Esses valores produzem um resultado correto:

    00062929x
    00021577E
    00000509H
  • Esses valores produzem um resultado incorreto:

    00002020Q
    00016723L
    00009431O
    00017221R
  • Este valor produz um erro:

    00062145}
    anything ending with "}"

Comparando todas as três versões com 448.740 linhas usando SET STATISTICS TIME ON;, todas elas foram executadas em pouco mais de 5000 ms de tempo decorrido. Mas para o tempo de CPU, os resultados foram:

  • TVF do OP: 7031 ms
  • TVF de Joe: 3734 ms
  • TVF de Salomão: 1407 ms

CONFIGURAÇÃO: DADOS

A seguir, cria uma tabela e a preenche. Isso deve criar o mesmo conjunto de dados em todos os sistemas executando o SQL Server 2017, pois eles terão as mesmas linhas spt_values. Isso ajuda a fornecer uma base de comparação entre outras pessoas testando em seu sistema, pois os dados gerados aleatoriamente levariam em consideração diferenças de tempo entre os sistemas, ou mesmo entre testes no mesmo sistema, se os dados da amostra forem regenerados. Comecei com a mesma tabela de 3 colunas que Joe, mas usei os valores de amostra da pergunta como um modelo para criar uma variedade de valores numéricos anexados a cada uma das opções possíveis de caracteres finais (incluindo nenhum caractere final). Foi também por isso que forcei o agrupamento nas colunas: não queria que estivesse usando uma instância de agrupamento binário para negar injustamente o efeito de usar oCOLLATE para forçar um agrupamento diferente no TVF).

A única diferença está na ordem das linhas na tabela.

USE [tempdb];
SET NOCOUNT ON;

CREATE TABLE dbo.TestVals
(
  [TestValsID] INT IDENTITY(1, 1) NOT NULL PRIMARY KEY,
  [Col1] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL,
  [Col2] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL,
  [Col3] VARCHAR(50) COLLATE Latin1_General_100_CI_AS NOT NULL
);

;WITH cte AS
(
  SELECT (val.[number] + tmp.[blah]) AS [num]
  FROM [master].[dbo].[spt_values] val
  CROSS JOIN (VALUES (1), (7845), (0), (237), (61063), (999)) tmp(blah)
  WHERE val.[number] BETWEEN 0 AND 1000000
)
INSERT INTO dbo.TestVals ([Col1], [Col2], [Col3])
  SELECT FORMATMESSAGE('%08d%s', cte.[num], tab.[col]) AS [Col1],
       FORMATMESSAGE('%08d%s', ((cte.[num] + 2) * 2), tab.[col]) AS [Col2],
       FORMATMESSAGE('%08d%s', ((cte.[num] + 1) * 3), tab.[col]) AS [Col3]
  FROM    cte
  CROSS JOIN (VALUES (''), ('{'), ('A'), ('B'), ('C'), ('D'), ('E'), ('F'),
              ('G'), ('H'), ('I'), ('}'), ('J'), ('K'), ('L'), ('M'), ('N'),
              ('O'), ('P'), ('Q'), ('R'), ('p'), ('q'), ('r'), ('s'), ('t'),
              ('u'), ('v'), ('w'), ('x'), ('y')) tab(col)
  ORDER BY NEWID();
-- 463698 rows

CONFIGURAÇÃO: TVF

GO
CREATE OR ALTER FUNCTION dbo.ConvertAmountVerified_Solomon
(
    @amt VARCHAR(50)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN

    WITH ctePosition AS
    (
        SELECT CHARINDEX(RIGHT(RTRIM(@amt), 1) COLLATE Latin1_General_100_BIN2,
                             '{ABCDEFGHI}JKLMNOPQRpqrstuvwxy') AS [Value]
    ),
    cteAppend AS
    (
        SELECT pos.[Value] AS [Position],
               IIF(pos.[Value] > 0,
                      CHAR(48 + ((pos.[Value] - 1) % 10)),
                      '') AS [Value]
        FROM   ctePosition pos
    )
    SELECT (CONVERT(DECIMAL(18, 3),
                    IIF(app.[Position] > 0,
                           SUBSTRING(RTRIM(@amt), 1, LEN(@amt) - 1) + app.[Value],
                           @amt))
                        / 100. )
                    * IIF(app.[Position] > 10, -1., 1.) AS [AmountVerified]
    FROM   cteAppend app;
GO

Observe:

  1. Eu usei um agrupamento binário (ie _BIN2), que é mais rápido que um agrupamento com distinção entre maiúsculas e minúsculas, pois não precisa levar em conta nenhuma regra lingüística.
  2. A única coisa que realmente importa é a localização (ou seja, o "índice") do caractere mais à direita na lista de caracteres alfa mais os dois colchetes. Tudo o que é feito operacionalmente é derivado dessa posição mais do que o valor do próprio personagem.
  3. Usei o parâmetro de entrada e os tipos de dados de valor de retorno, conforme indicado na UDF original, que foi reescrita pelo OP. A menos que houvesse um bom motivo para ir de VARCHAR(50)para VARCHAR(60)e de NUMERIC (18,3)para NUMERIC (18,2)(o bom motivo seria "eles estavam errados") com a assinatura / tipos originais.
  4. I adicionado um ponto período / decimal para o final das 3 numéricos constantes literais /: 100., -1., e 1.. Isso não estava na minha versão original desta TVF (no histórico desta resposta), mas notei algumas CONVERT_IMPLICITchamadas no plano de execução XML (já que 100é uma INToperação que precisa ser NUMERIC/ DECIMAL), então resolvi isso com antecedência .
  5. Crio um caractere de seqüência de caracteres usando a CHAR()função, em vez de passar uma versão de seqüência de caracteres de um número (por exemplo '2') para uma CONVERTfunção (que era o que eu estava originalmente fazendo, novamente na história). Parece ser um pouco mais rápido. Apenas alguns milissegundos, mas ainda assim.

TESTE

Observe que eu tive que filtrar as linhas que terminam com, }pois isso causou erro nos TVFs do OP e do Joe. Enquanto meu código lida }corretamente, eu queria ser consistente com as linhas que estavam sendo testadas nas três versões. É por isso que o número de linhas geradas pela consulta de instalação é um pouco maior que o número que observei acima dos resultados do teste para quantas linhas estavam sendo testadas.

SET STATISTICS TIME ON;

DECLARE @Dummy DECIMAL(18, 3);
SELECT --@Dummy =  -- commented out = results to client; uncomment to not return results
cnvrtS.[AmountVerified]
FROM  dbo.TestVals vals
CROSS APPLY dbo.ConvertAmountVerified_Solomon(vals.[Col1]) cnvrtS
WHERE RIGHT(vals.[Col1], 1) <> '}'; -- filter out rows that cause error in O.P.'s code

SET STATISTICS TIME OFF;
GO

O tempo de CPU é apenas um pouco menor ao descomentar o --@Dummy =, e a classificação entre os 3 TVFs é a mesma. Mas, curiosamente, ao descomentar a variável, as classificações mudam um pouco:

  • TVF de Joe: 3295 ms
  • TVF do OP: 2240 ms
  • TVF de Salomão: 1203 ms

Não sei por que o código do OP teria um desempenho muito melhor nesse cenário (enquanto o meu e o de Joe apenas melhoraram marginalmente), mas parece consistente em muitos testes. E não, não olhei para as diferenças do plano de execução, pois não tenho tempo para investigar isso.

MESMO MAIS RAPIDAMENTE

Concluí os testes da abordagem alternativa e ela fornece uma melhoria leve, mas definitiva, do que é mostrado acima. A nova abordagem usa o SQLCLR e parece ter melhor escala. Descobri que, ao adicionar na segunda coluna à consulta, a abordagem T-SQL duplicou no tempo. Mas, ao adicionar colunas adicionais usando um UDF escalar SQLCLR, o tempo aumentou, mas não na mesma quantidade que o tempo de coluna única. Talvez haja alguma sobrecarga inicial ao chamar o método SQLCLR (não associado à sobrecarga do carregamento inicial do Domínio do Aplicativo e do Assembly no Domínio do Aplicativo) porque os tempos eram (tempo decorrido, não tempo da CPU):

  • 1 coluna: 1018 ms
  • 2 colunas: 1750 - 1800 ms
  • 3 colunas: 2500 - 2600 ms

Portanto, é possível que o tempo (de dumping para uma variável, que não retorne o conjunto de resultados) tenha uma sobrecarga de 200 ms - 250 ms e, em seguida, 750 ms - 800 ms por tempo de instância. Os tempos de CPU foram: 950 ms, 1750 ms e 2400 ms para 1, 2 e 3 instâncias da UDF, respectivamente.

CÓDIGO C #

using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

public class Transformations
{
    private const string _CHARLIST_ = "{ABCDEFGHI}JKLMNOPQRpqrstuvwxy";

    [SqlFunction(IsDeterministic = true, IsPrecise = true,
        DataAccess = DataAccessKind.None, SystemDataAccess = SystemDataAccessKind.None)]
    public static SqlDouble ConvertAmountVerified_SQLCLR(
        [SqlFacet(MaxSize = 50)] SqlString Amt)
    {
        string _Amount = Amt.Value.TrimEnd();

        int _LastCharIndex = (_Amount.Length - 1);
        int _Position = _CHARLIST_.IndexOf(_Amount[_LastCharIndex]);

        if (_Position >= 0)
        {
            char[] _TempAmount = _Amount.ToCharArray();
            _TempAmount[_LastCharIndex] = char.ConvertFromUtf32(48 + (_Position % 10))[0];
            _Amount = new string(_TempAmount);
        }

        decimal _Return = decimal.Parse(_Amount) / 100M;

        if (_Position > 9)
        {
            _Return *= -1M;
        }

        return new SqlDouble((double)_Return);
    }
}

Eu originalmente usei SqlDecimalcomo o tipo de retorno, mas há uma penalidade de desempenho por usá-lo em oposição a SqlDouble/ FLOAT. Às vezes, o FLOAT tem problemas (por ser um tipo impreciso), mas eu verifiquei no T-SQL TVF por meio da seguinte consulta e nenhuma diferença foi detectada:

SELECT cnvrtS.[AmountVerified],
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col1])
FROM   dbo.TestVals vals
CROSS APPLY dbo.ConvertAmountVerified_Solomon(vals.[Col1]) cnvrtS
WHERE  cnvrtS.[AmountVerified] <> dbo.ConvertAmountVerified_SQLCLR(vals.[Col1]);

TESTE

SET STATISTICS TIME ON;

DECLARE @Dummy DECIMAL(18, 3), @Dummy2 DECIMAL(18, 3), @Dummy3 DECIMAL(18, 3);
SELECT @Dummy = 
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col1])
              , @Dummy2 =
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col2])
              , @Dummy3 =
       dbo.ConvertAmountVerified_SQLCLR(vals.[Col3])
FROM  dbo.TestVals vals
WHERE RIGHT(vals.[Col1], 1) <> '}';

SET STATISTICS TIME OFF;
Solomon Rutzky
fonte
Obrigado por isso. Vou testar sua função com meus dados. Ansioso para ver suas alterações para torná-lo ainda mais rápido e testar dados.
Mazhar
11
@ Mazhar Obrigado por aceitar :-). No entanto, concluí meus testes na abordagem alternativa e descobri que é um pouco mais rápido do que o que eu já tinha aqui. Ele usa SQLCLR, mas é melhor dimensionado. Também voltou a ser uma UDF escalar, de modo um pouco mais fácil de trabalhar (ou seja, sem precisar dos CROSS APPLY).
Solomon Rutzky
" Talvez haja alguma sobrecarga inicial ao chamar o método SQLCLR (não associado à sobrecarga do carregamento inicial do domínio de aplicativo e do assembly no domínio de aplicativo) " - sugeria que a sobrecarga poderia ser compilação JIT, já que é encontrado apenas na primeira execução. Mas eu criei um perfil do seu código em um aplicativo de console C # e ele incorreu apenas em dezenas de ms de compilação JIT. O método estático especificamente levou apenas 0,3 ms para ser JIT. Mas eu não sei nada sobre SQLCLR, então talvez haja mais código envolvido do que eu sei.
Josh Darnell
11
@ jadarnel27 Obrigado por ajudar a investigar. Eu acho que pode ser uma verificação de permissão de alguma coisa. Algo relacionado à geração / validação do plano de consulta.
Solomon Rutzky
4

Vou começar jogando alguns dados de teste em uma tabela. Não tenho ideia de como são seus dados reais, então usei números inteiros sequenciais:

CREATE TABLE APPLY_FUNCTION_TO_ME (
    COL1 VARCHAR(60),
    COL2 VARCHAR(60),
    COL3 VARCHAR(60)
);

INSERT INTO APPLY_FUNCTION_TO_ME WITH (TABLOCK)
SELECT RN, RN, RN
FROM (
    SELECT CAST(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS VARCHAR(60)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) t;

A seleção de todas as linhas com os conjuntos de resultados desativados fornece uma linha de base:

-- CPU time = 1359 ms,  elapsed time = 1434 ms.
SELECT COL1 FROM dbo.APPLY_FUNCTION_TO_ME

Se uma consulta semelhante com a chamada da função levar mais tempo, teremos uma estimativa aproximada da sobrecarga da função. Aqui está o que eu recebo ao chamar seu TVF como é:

-- CPU time = 41703 ms,  elapsed time = 41899 ms.
SELECT t1.AmountVerified
FROM dbo.APPLY_FUNCTION_TO_ME
CROSS APPLY dbo.ConvertAmountVerified_TVF (COL1) t1
OPTION (MAXDOP 1);

Portanto, a função precisa de cerca de 40 segundos de tempo da CPU para 6,5 ​​milhões de linhas. Multiplique isso por 20 e são 800 segundos de tempo de CPU. Notei duas coisas no seu código de função:

  1. Uso desnecessário de OUTER APPLY. CROSS APPLYfornecerá os mesmos resultados e, para essa consulta, evitará muitas junções desnecessárias. Isso pode economizar um pouco de tempo. Depende principalmente se a consulta completa for paralela. Eu não sei nada sobre seus dados ou consulta, então estou apenas testando MAXDOP 1. Nesse caso, estou melhor com CROSS APPLY.

  2. Existem muitas CHARINDEXchamadas quando você está apenas procurando um caractere em uma pequena lista de valores correspondentes. Você pode usar a ASCII()função e um pouco de matemática para evitar todas as comparações de string.

Aqui está uma maneira diferente de escrever a função:

CREATE OR ALTER FUNCTION dbo.ConvertAmountVerified_TVF3
(
    @amt VARCHAR(60)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
    WITH cteLastChar
    AS(
        SELECT LastCharASCIICode =  ASCII(RIGHT(RTRIM(@amt), 1) COLLATE Latin1_General_CS_AS)
    )
    SELECT
        AmountVerified  = CAST(RET.Y AS NUMERIC(18,2))
    FROM cteLastChar
    CROSS APPLY (
        SELECT N =
                CAST(
                    CASE 
                        --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                        --    THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0)-1
                        WHEN LastCharASCIICode = 123 THEN 0
                        WHEN LastCharASCIICode BETWEEN 65 AND 73 THEN LastCharASCIICode - 64
                        WHEN LastCharASCIICode = 125 THEN 10

                        --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0) >0
                        --    THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQR', 0)-1
                        WHEN LastCharASCIICode BETWEEN 74 AND 82 THEN LastCharASCIICode - 74

                        --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0) >0
                        --    THEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'pqrstuvwxy', 0)-1
                        WHEN LastCharASCIICode BETWEEN 112 AND 121 THEN LastCharASCIICode - 112
                        ELSE 
                            NULL
                    END
                AS VARCHAR(1))
        --FROM
        --    cteLastChar L
    ) NUM
    CROSS APPLY (
        SELECT N =
            CASE 
                --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, '{ABCDEFGHI}', 0) >0
                WHEN LastCharASCIICode = 123 OR LastCharASCIICode = 125 OR LastCharASCIICode BETWEEN 65 AND 73
                    THEN 0

                --WHEN CHARINDEX(L.LastChar  COLLATE Latin1_General_CS_AS, 'JKLMNOPQRpqrstuvwxy', 0) >0
                WHEN LastCharASCIICode BETWEEN 74 AND 82 OR LastCharASCIICode BETWEEN 112 AND 121
                    THEN 1
                ELSE 0
            END
        --FROM cteLastChar L
    ) NEG
    CROSS APPLY(
        SELECT Amt= CASE
                        WHEN NUM.N IS NULL
                            THEN @amt 
                        ELSE
                            SUBSTRING(RTRIM(@amt),1, LEN(@amt) - 1) + Num.N
                    END
    ) TP
    CROSS APPLY(
        SELECT Y =  CASE
                        WHEN NEG.N = 0
                            THEN (CAST(TP.Amt AS NUMERIC) / 100)
                        WHEN NEG.N = 1
                            THEN (CAST (TP.Amt AS NUMERIC) /100) * -1
                    END
    ) RET
) ;

GO

Na minha máquina, a nova função é significativamente mais rápida:

-- CPU time = 7813 ms,  elapsed time = 7876 ms.
SELECT t1.AmountVerified
FROM dbo.APPLY_FUNCTION_TO_ME
CROSS APPLY dbo.ConvertAmountVerified_TVF3 (COL1) t1
OPTION (MAXDOP 1);

Provavelmente, também existem algumas otimizações adicionais disponíveis, mas meu intestino diz que não serão muito. Com base no que seu código está fazendo, não consigo ver como você veria melhorias ao chamar sua função de uma maneira diferente. É apenas um monte de operações de string. Chamar a função 20 vezes por linha será mais lento do que apenas uma vez, mas a definição já é incorporada.

Joe Obbish
fonte
Obrigado por isso. Você está dizendo por "definição já está embutida" que a execução do TVF em várias colunas se comportará como uma função embutida?
Mazhar
Vou testar sua função com meus dados.
Mazhar
2

Tente usar o seguinte

-- Get Last Character
SELECT @LastChar = RIGHT(RTRIM(@amt), 1) collate latin1_general_cs_as;

DECLARE @CharPos int=NULLIF(CHARINDEX(@LastChar,'{ABCDEFGHI}JKLMNOPQRpqrstuvwxy'),0)-1
SET @Num = ISNULL(@CharPos%10,''); 
SET @Negative = IIF(@CharPos>9,1,0);

em vez de

SELECT @Num =
    CASE @LastChar  collate latin1_general_cs_as
        WHEN '{'  THEN '0'
...

SELECT @Negative =
    CASE @LastChar collate latin1_general_cs_as
        WHEN '{' THEN 0
...

Uma variante com o uso de uma tabela auxiliar

-- auxiliary table
CREATE TABLE LastCharLink(
  LastChar varchar(1) collate latin1_general_cs_as NOT NULL,
  Num varchar(1) NOT NULL,
  Prefix varchar(1) NOT NULL,
CONSTRAINT PK_LastCharLink PRIMARY KEY(LastChar)
)

INSERT LastCharLink(LastChar,Num,Prefix)VALUES
('{','0',''),
('A','1',''),
('B','2',''),
('C','3',''),
('D','4',''),
('E','5',''),
('F','6',''), 
('G','7',''), 
('H','8',''), 
('I','9',''), 
('}','0','-'), 
('J','1','-'),
('K','2','-'),
('L','3','-'),
('M','4','-'),
('N','5','-'),
('O','6','-'),
('P','7','-'),
('Q','8','-'),
('R','9','-'),                
('p','0','-'),
('q','1','-'),
('r','2','-'),
('s','3','-'),
('t','4','-'),
('u','5','-'),
('v','6','-'),
('w','7','-'),
('x','8','-'),
('y','9','-')

Uma consulta de teste

CREATE TABLE #TestAmounts(Amt varchar(10))
INSERT #TestAmounts(Amt)VALUES('00064170'),('00057600A'),('00066294R'),('00059224}'),('00012345p')

SELECT
  *,
  CAST( -- step 5 - final cast
      CAST( -- step 3 - convert to number
          CONCAT( -- step 2 - add a sign and an additional number
              l.Prefix,
              LEFT(RTRIM(a.Amt),LEN(RTRIM(a.Amt))-IIF(l.LastChar IS NULL,0,1)), -- step 1 - remove last char
              l.Num
            )
          AS numeric(18,3)
        )/100 -- step 4 - divide
      AS numeric(18,3)
    ) ResultAmt
FROM #TestAmounts a
LEFT JOIN LastCharLink l ON RIGHT(RTRIM(a.Amt),1) collate latin1_general_cs_as=l.LastChar

DROP TABLE #TestAmounts

Como variante, você também pode tentar usar uma tabela auxiliar temporária #LastCharLinkou uma tabela variável @LastCharLink(mas pode ser mais lenta que uma tabela real ou temporária)

DECLARE @LastCharLink TABLE(
  LastChar varchar(1) collate latin1_general_cs_as NOT NULL,
  Num varchar(1) NOT NULL,
  Prefix varchar(1) NOT NULL,
PRIMARY KEY(LastChar)
)

INSERT LastCharLink(LastChar,Num,Prefix)VALUES
('{','0',''),
('A','1',''),
('B','2',''),
('C','3',''),
('D','4',''),
('E','5',''),
...

E use-o como

FROM #TestAmounts a
LEFT JOIN #LastCharLink l ON ...

ou

FROM #TestAmounts a
LEFT JOIN @LastCharLink l ON ...

Depois, você também pode criar uma função embutida simples e colocar todas as conversões

CREATE FUNCTION NewConvertAmountVerified(
  @Amt varchar(50),
  @LastChar varchar(1),
  @Num varchar(1),
  @Prefix varchar(1)
)
RETURNS numeric(18,3)
AS
BEGIN
  RETURN CAST( -- step 3 - convert to number
              CONCAT( -- step 2 - add a sign and an additional number
                  @Prefix,
                  LEFT(@Amt,LEN(@Amt)-IIF(@LastChar IS NULL,0,1)), -- step 1 - remove last char
                  @Num
                )
              AS numeric(18,3)
            )/100 -- step 4 - divide
END
GO

E então use esta função como

CREATE TABLE #TestAmounts(Amt varchar(10))
INSERT #TestAmounts(Amt)VALUES('00064170'),('00057600A'),('00066294R'),('00059224}'),('00012345p')

SELECT
  *,
  -- you need to use `RTRIM` here
  dbo.NewConvertAmountVerified(RTRIM(a.Amt),l.LastChar,l.Num,l.Prefix) ResultAmt
FROM #TestAmounts a
LEFT JOIN LastCharLink l ON RIGHT(RTRIM(a.Amt),1) collate latin1_general_cs_as=l.LastChar

DROP TABLE #TestAmounts
Sergey Menshov
fonte
Eu atualizei minha resposta. Tente usar uma tabela auxiliar para fazer o que você deseja. Eu acho que essa variante será mais rápida.
Atualizei minha resposta mais uma vez. Agora ele usa em Prefixvez de Divider.
2

Como alternativa, você pode criar uma tabela permanente. Essa é uma criação única.

CREATE TABLE CharVal (
    charactor CHAR(1) collate latin1_general_cs_as NOT NULL
    ,positiveval INT NOT NULL
    ,negativeval INT NOT NULL
    ,PRIMARY KEY (charactor)
    )

insert into CharVal (charactor,positiveval,negativeval) VALUES

 ( '{' ,'0', 0 ),( 'A' ,'1', 0 ) ,( 'B' ,'2', 0 ) ,( 'C' ,'3', 0 ) ,( 'D' ,'4', 0 )       
                         ,( 'E' ,'5', 0 )  ,( 'F' ,'6', 0 ) ,( 'G' ,'7', 0 ) ,( 'H' ,'8', 0 )       
,( 'I' ,'9', 0 ),( '}' ,'0', 1 ),( 'J' ,'1', 1  ),( 'K' ,'2', 1 ) ,( 'L' ,'3', 1 ) ,( 'M' ,'4', 1 )       
,( 'N' ,'5', 1 )  ,( 'O' ,'6', 1 )  ,( 'P' ,'7', 1 )  ,( 'Q' ,'8', 1 )  ,( 'R' ,'9', 1  )
---ASCII
,( 'p' , '0', '1'),( 'q' , '1', '1'),( 'r' , '2', '1'),( 's' , '3', '1')
,( 't' , '4', '1'),( 'u' , '5', '1'),( 'v' , '6', '1'),( 'w' , '7', '1')
,( 'x' , '8', '1'),( 'y' , '9', '1')

--neg
('{' ,2, 0) ,('A' ,2, 0) ,('B' ,2, 0)  ,('C' ,2, 0) ,('D' ,2, 0)                    
,('E' ,2, 0),('F' ,2, 0)  ,('G' ,2, 0) ,('H' ,2, 0) ,('I' ,2, 0) ,('}' ,2, 1)
,('J' ,2, 1) ,('K' ,2, 1) ,('L' ,2, 1) ,('M' ,2, 1) ,('N' ,2, 1)                    
,('O' ,2, 1)  ,('P' ,2, 1)  ,('Q' ,2, 1) ,('R' ,2, 1)
  ---ASCII
,( 'p' ,2, '1'),( 'q' ,2, '1')
,( 'r' ,2, '1'),( 's' ,2, '1')
,( 't' ,2, '1'),( 'u' ,2, '1')
,( 'v' ,2, '1'),( 'w' ,2, '1')
,( 'x' ,2, '1'),( 'y' ,2, '1')

Então TVF

ALTER FUNCTION dbo.ConvertAmountVerified_TVFHarsh (@amt VARCHAR(60))
RETURNS TABLE
    WITH SCHEMABINDING
AS
RETURN (
        WITH MainCTE AS (
                SELECT TOP 1 
                Amt = CASE 
                        WHEN positiveval IS NULL
                            THEN @amt
                        ELSE SUBSTRING(RTRIM(@amt), 1, LEN(@amt) - 1) + positiveval
                        END
                    ,negativeval
                FROM (
                    SELECT positiveval
                        ,negativeval negativeval
                        ,1 sortorder
                    FROM dbo.CharVal WITH (NOLOCK)
                    WHERE (charactor = RIGHT(RTRIM(@amt), 1))

                    UNION ALL

                    SELECT NULL
                        ,0
                        ,0
                    ) t4
                ORDER BY sortorder DESC
                )

        SELECT AmountVerified = CASE 
                WHEN negativeval = 0
                    THEN (CAST(TP.Amt AS NUMERIC) / 100)
                WHEN negativeval = 1
                    THEN (CAST(TP.Amt AS NUMERIC) / 100) * - 1
                END
        FROM MainCTE TP
        );
GO

Do exemplo @Joe,

- Demora 30 s

SELECT t1.AmountVerified
FROM dbo.APPLY_FUNCTION_TO_ME
CROSS APPLY dbo.ConvertAmountVerified_TVFHarsh (COL1) t1
OPTION (MAXDOP 1);

Se possível, o Valor também pode ser formatado no nível da interface do usuário. Essa é a melhor opção. Caso contrário, você também poderá compartilhar sua consulta original. OU, se possível, mantenha o valor formatado na tabela também.

KumarHarsh
fonte