Il modo più efficiente per chiamare la stessa funzione con valori di tabella su più colonne in una query


8

Sto cercando di ottimizzare una query in cui la stessa funzione con valori di tabella (TVF) viene chiamata su 20 colonne.

La prima cosa che ho fatto è stata la conversione della funzione scalare in una funzione con valori di tabella incorporata.

Utilizza CROSS APPLYil modo migliore per eseguire la stessa funzione su più colonne in una query?

Un esempio semplicistico:

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

Ci sono alternative migliori?

La stessa funzione può essere chiamata in più query rispetto al numero X di colonne.

Ecco la funzione:

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

Ecco la versione della funzione scalare che ho ereditato, se qualcuno è interessato:

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

Dati di prova di esempio:

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

Risposte:


8

PRIMO: va detto che il metodo assolutamente più veloce per ottenere i risultati desiderati è quello di fare quanto segue:

  1. Migrare i dati in nuove colonne o persino in una nuova tabella:
    1. Nuovo approccio colonna:
      1. Aggiungi nuove colonne {name}_newalla tabella con il DECIMAL(18, 3)tipo di dati
      2. Esegui una migrazione singola dei dati dalle vecchie VARCHARcolonne alle DECIMALcolonne
      3. rinominare le vecchie colonne in {name}_old
      4. rinominare nuove colonne per essere giuste {name}
    2. Nuovo approccio al tavolo:
      1. Crea una nuova tabella come {table_name}_newusando il DECIMAL(18, 3)tipo di dati
      2. Esegui una migrazione singola dei dati dalla tabella corrente alla nuova DECIMALtabella basata.
      3. rinominare la vecchia tabella in _old
      4. rimuovi _newdalla nuova tabella
  2. Aggiorna app, ecc. Per non inserire mai dati codificati in questo modo
  3. dopo un ciclo di rilascio, se non ci sono problemi, elimina le colonne o le tabelle precedenti
  4. rilasciare TVF e UDF
  5. Non parlarne mai più!

DETTO CHE: Puoi liberarti di molto di quel codice in quanto si tratta di una duplicazione in gran parte superflua. Inoltre, ci sono almeno due bug che causano a volte l'output errato o talvolta generano un errore. E quei bug sono stati copiati nel codice di Joe poiché produce gli stessi risultati (incluso l'errore) del codice dell'OP. Per esempio:

  • Questi valori producono un risultato corretto:

    00062929x
    00021577E
    00000509H
  • Questi valori producono un risultato errato:

    00002020Q
    00016723L
    00009431O
    00017221R
  • Questo valore produce un errore:

    00062145}
    anything ending with "}"

Confrontando tutte e 3 le versioni con 448.740 righe usando SET STATISTICS TIME ON;, tutte hanno funzionato in poco più di 5000 ms di tempo trascorso. Ma per il tempo della CPU, i risultati sono stati:

  • TVF di OP: 7031 ms
  • Joe's TVF: 3734 ms
  • TVF di Salomone: 1407 ms

SETUP: DATI

Quanto segue crea una tabella e la popola. Ciò dovrebbe creare lo stesso set di dati su tutti i sistemi che eseguono SQL Server 2017 poiché avranno le stesse righe spt_values. Questo aiuta a fornire una base di confronto tra le altre persone che eseguono test sul proprio sistema poiché i dati generati casualmente determinerebbero differenze di temporizzazione tra i sistemi, o anche tra test sullo stesso sistema se i dati del campione fossero rigenerati. Ho iniziato con la stessa tabella a 3 colonne di Joe, ma ho usato i valori di esempio della domanda come modello per trovare una varietà di valori numerici aggiunti a ciascuna delle possibili opzioni di carattere finale (incluso nessun carattere finale). Questo è anche il motivo per cui ho forzato il confronto sulle colonne: non volevo il fatto che sto usando un'istanza di confronto binario per negare ingiustamente l'effetto dell'uso delCOLLATE parola chiave per forzare un diverso confronto nella TVF).

L'unica differenza è nell'ordinamento delle righe nella tabella.

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

SETUP: 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

Notare che:

  1. Ho usato una raccolta binaria (cioè _BIN2) che è più veloce di una raccolta con distinzione tra maiuscole e minuscole in quanto non deve tenere conto di alcuna regola linguistica.
  2. L'unica cosa che conta davvero è la posizione (cioè l '"indice") del carattere più a destra nell'elenco dei caratteri alfa più le due parentesi graffe. Tutto ciò che viene fatto operativamente deriva da quella posizione più del valore del personaggio stesso.
  3. Ho usato il parametro di input e i tipi di dati del valore restituito come indicato nell'UDF originale che è stato riscritto dall'OP A meno che non ci fossero buone ragioni per passare da VARCHAR(50)a VARCHAR(60), e da NUMERIC (18,3)a NUMERIC (18,2)(una buona ragione sarebbe "avevano torto"), quindi mi sarei bloccato con la firma / i tipi originali.
  4. Ho aggiunto un punto periodo / decimale alla fine dei 3 numerici letterali / costanti: 100., -1.e 1.. Questo non era nella mia versione originale di questo TVF (nella storia di questa risposta) ma ho notato alcune CONVERT_IMPLICITchiamate nel piano di esecuzione XML (poiché 100è un INTma l'operazione deve essere NUMERIC/ DECIMAL), quindi mi sono appena preso cura di quello in anticipo .
  5. Creo un carattere stringa usando la CHAR()funzione anziché passare una versione stringa di un numero (ad es. '2') In una CONVERTfunzione (che era quello che stavo facendo originariamente, sempre nella storia). Questo sembra essere leggermente più veloce. Solo pochi millisecondi, ma comunque.

TEST

Si noti che ho dovuto filtrare le righe che finiscono in }quanto ciò ha causato l'errore dei TVF dell'OP e di Joe. Mentre il mio codice gestisce }correttamente, volevo essere coerente con quali righe venivano testate nelle 3 versioni. Questo è il motivo per cui il numero di righe generate dalla query di installazione è leggermente superiore al numero che ho notato sopra i risultati del test per quante righe sono state testate.

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

Il tempo della CPU è solo leggermente inferiore quando si decommenta il --@Dummy =, e la classifica tra i 3 TVF è la stessa. Ma abbastanza interessante, quando si decommenta la variabile, le classifiche cambiano un po ':

  • Joe's TVF: 3295 ms
  • TVF di OP: 2240 ms
  • TVF di Salomone: 1203 ms

Non sono sicuro del perché il codice del PO funzionerebbe molto meglio in questo scenario (mentre il codice mio e di Joe è migliorato solo marginalmente), ma sembra coerente in molti test. E no, non ho esaminato le differenze del piano di esecuzione in quanto non ho tempo per indagare su questo.

ANCORA PIÙ RAPIDAMENTE

Ho completato i test dell'approccio alternativo e fornisce un leggero ma deciso miglioramento a quanto mostrato sopra. Il nuovo approccio utilizza SQLCLR e sembra ridimensionare meglio. Ho scoperto che quando si aggiunge la seconda colonna alla query, l'approccio T-SQL raddoppia nel tempo. Ma, quando si aggiungono colonne aggiuntive usando un UDF scalare SQLCLR, il tempo è aumentato, ma non della stessa quantità dei tempi della singola colonna. Forse c'è un sovraccarico iniziale nell'invocare il metodo SQLCLR (non associato al sovraccarico del caricamento iniziale del dominio app e dell'assembly nel dominio app) perché i tempi erano (tempo trascorso, non tempo CPU):

  • 1 colonna: 1018 ms
  • 2 colonne: 1750 - 1800 ms
  • 3 colonne: 2500 - 2600 ms

Quindi è possibile che il tempo (del dumping su una variabile, non restituendo il set di risultati) abbia un sovraccarico di 200 ms - 250 ms e quindi 750 ms - 800 ms per istanza. I tempi della CPU erano: 950 ms, 1750 ms e 2400 ms per 1, 2 e 3 istanze dell'UDF, rispettivamente.

CODICE 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);
    }
}

Inizialmente ho usato SqlDecimalcome tipo di ritorno, ma vi è una penalità per le prestazioni rispetto a SqlDouble/ FLOAT. A volte FLOAT presenta problemi (a causa del tipo impreciso), ma ho verificato il TVF T-SQL tramite la query seguente e non sono state rilevate differenze:

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]);

TEST

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;

Grazie per questo. Ho intenzione di testare la tua funzione rispetto ai miei dati. In attesa di vedere le tue modifiche per renderlo ancora più veloce e testare i dati.
Mazhar,

1
@Mazhar Grazie per aver accettato :-). Tuttavia, ho completato i miei test sull'approccio alternativo e ho scoperto che è leggermente più veloce di quello che avevo già qui. Usa SQLCLR ma si adatta meglio. È anche tornato ad essere un UDF scalare, quindi un po 'più facile da lavorare (cioè non ha bisogno della CROSS APPLYs).
Solomon Rutzky,

" Forse c'è un sovraccarico iniziale nell'invocare il metodo SQLCLR (non associato al sovraccarico del caricamento iniziale del dominio App e dell'Assemblea nel dominio App) " - Stavo per suggerire che l'overhead potrebbe essere una compilazione JIT, poiché viene rilevato solo alla prima esecuzione. Ma ho profilato il tuo codice in un'app console C #, e sono stati necessari solo 10 ms di compilazione JIT. Il metodo statico in particolare ha impiegato solo .3 ms per essere JIT. Ma non so nulla di SQLCLR, quindi forse c'è più codice coinvolto di quello che sono a conoscenza.
Josh Darnell,

1
@ jadarnel27 Grazie per l'aiuto nell'indagare. Penso che potrebbe essere un controllo di autorizzazione di qualcosa. Qualcosa correlato alla generazione / convalida del piano di query.
Solomon Rutzky,

4

Inizierò lanciando alcuni dati di test in una tabella. Non ho idea di come siano i tuoi dati reali, quindi ho usato solo numeri interi sequenziali:

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;

La selezione di tutte le righe con i set di risultati disattivati ​​fornisce una linea di base:

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

Se una query simile con la chiamata di funzione richiede più tempo, abbiamo una stima approssimativa del sovraccarico della funzione. Ecco cosa ottengo chiamando il tuo TVF così com'è:

-- 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);

Quindi la funzione richiede circa 40 secondi di tempo CPU per 6,5 milioni di righe. Moltiplicalo per 20 ed è 800 secondi di tempo CPU. Ho notato due cose nel tuo codice funzione:

  1. Uso non necessario di OUTER APPLY. CROSS APPLYti darà gli stessi risultati e per questa query eviterà un sacco di join non necessari. Ciò può farti risparmiare un po 'di tempo. Dipende principalmente se la query completa diventa parallela. Non so nulla dei tuoi dati o query, quindi sto solo testando MAXDOP 1. In quel caso sto meglio con CROSS APPLY.

  2. Ci sono molte CHARINDEXchiamate quando stai solo cercando un personaggio in un piccolo elenco di valori corrispondenti. È possibile utilizzare la ASCII()funzione e un po 'di matematica per evitare tutti i confronti tra stringhe.

Ecco un modo diverso di scrivere la funzione:

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

Sulla mia macchina, la nuova funzione è significativamente più veloce:

-- 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);

Probabilmente ci sono anche alcune ottimizzazioni aggiuntive disponibili, ma il mio istinto dice che non equivalgono a molto. Sulla base di ciò che sta facendo il tuo codice non riesco a vedere come vedresti un ulteriore miglioramento chiamando in qualche modo la tua funzione in un modo diverso. Sono solo un mucchio di operazioni sulle stringhe. Chiamare la funzione 20 volte per riga sarà più lento di una sola volta, ma la definizione viene già incorporata.


Grazie per questo. Stai dicendo che "la definizione è già inline" che l'esecuzione del TVF su più colonne si comporterà come una funzione incorporata?
Mazhar,

Ho intenzione di testare la tua funzione rispetto ai miei dati.
Mazhar,

2

Prova a usare quanto segue

-- 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);

anziché

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

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

Una variante con l'utilizzo di una tabella ausiliaria

-- 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','-')

Una query di prova

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

Come variante puoi anche provare a usare una tabella ausiliaria temporanea #LastCharLinko una tabella variabile @LastCharLink(ma può essere più lenta di una tabella reale o temporanea)

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 usalo come

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

o

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

Quindi puoi anche creare una semplice funzione incorporata e inserire tutte le conversioni

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 quindi usa questa funzione come

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

Ho aggiornato la mia risposta. Prova a utilizzare una tabella ausiliaria per fare ciò che desideri. Penso che questa variante sarà più veloce.

Ho aggiornato la mia risposta ancora una volta. Ora utilizza Prefixinvece di Divider.

2

In alternativa puoi creare una tabella permanente. Questa è la creazione di una volta.

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')

Quindi 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

Dall'esempio @Joe,

- Ci vogliono 30 s

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

Se possibile, l'importo può essere formattato anche a livello di interfaccia utente. Questa è l'opzione migliore. Altrimenti puoi anche condividere la tua query originale. O se possibile, mantenere anche il valore formattato nella tabella.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.