La forma más eficiente de llamar a la misma función con valores de tabla en varias columnas en una consulta

8

Estoy tratando de ajustar una consulta donde se llama a la misma función con valores de tabla (TVF) en 20 columnas.

Lo primero que hice fue convertir la función escalar en una función en línea con valores de tabla.

¿Está utilizando CROSS APPLYla mejor forma de ejecutar la misma función en varias columnas en una consulta?

Un ejemplo 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

¿Hay mejores alternativas?

Se puede invocar la misma función en múltiples consultas contra X número de columnas.

Aquí está la función:

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

Aquí está la versión de la función escalar que heredé, si alguien está interesado:

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

Datos de prueba de muestra:

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
fuente

Respuestas:

8

PRIMERO: debe mencionarse que el método más rápido para obtener los resultados deseados es hacer lo siguiente:

  1. Migre datos a nuevas columnas o incluso a una nueva tabla:
    1. Nuevo enfoque de columna:
      1. Agregue nuevas columnas {name}_newa la tabla con el DECIMAL(18, 3)tipo de datos
      2. Realice una migración única de los datos de las VARCHARcolumnas antiguas a las DECIMALcolumnas
      3. cambiar el nombre de las columnas antiguas a {name}_old
      4. renombrar nuevas columnas para ser justos {name}
    2. Nuevo enfoque de mesa:
      1. Crear una nueva tabla como {table_name}_newusando DECIMAL(18, 3)datatype
      2. Realice una migración única de los datos de la tabla actual a la nueva DECIMALtabla basada.
      3. cambiar el nombre de la tabla anterior a _old
      4. eliminar _newde la nueva tabla
  2. Actualice la aplicación, etc. para nunca insertar datos codificados de esta manera
  3. después de un ciclo de lanzamiento, si no hay problemas, descarte las columnas o la tabla antiguas
  4. soltar TVF y UDF
  5. ¡Nunca vuelvas a hablar de esto!

Dicho esto: puede deshacerse de gran parte de ese código, ya que es en gran medida una duplicación innecesaria. Además, hay al menos dos errores que hacen que la salida a veces sea incorrecta o que a veces arroje un error. Y esos errores se copiaron en el código de Joe, ya que produce los mismos resultados (incluido el error) que el código del OP. Por ejemplo:

  • Estos valores producen un resultado correcto:

    00062929x
    00021577E
    00000509H
  • Estos valores producen un resultado incorrecto:

    00002020Q
    00016723L
    00009431O
    00017221R
  • Este valor produce un error:

    00062145}
    anything ending with "}"

Comparando las 3 versiones con 448,740 filas usando SET STATISTICS TIME ON;, todas corrieron en poco más de 5000 ms de tiempo transcurrido. Pero para el tiempo de CPU, los resultados fueron:

  • TVF de OP: 7031 ms
  • TVF de Joe: 3734 ms
  • TVF de Salomón: 1407 ms

CONFIGURACIÓN: DATOS

Lo siguiente crea una tabla y la llena. Esto debería crear el mismo conjunto de datos en todos los sistemas que ejecutan SQL Server 2017, ya que tendrán las mismas filas spt_values. Esto ayuda a proporcionar una base de comparación entre otras personas que realizan pruebas en su sistema, ya que los datos generados aleatoriamente tendrían en cuenta las diferencias de tiempo entre los sistemas, o incluso entre las pruebas en el mismo sistema si los datos de la muestra se regeneran. Comencé con la misma tabla de 3 columnas que Joe, pero usé los valores de muestra de la pregunta como una plantilla para obtener una variedad de valores numéricos agregados con cada una de las posibles opciones de caracteres finales (incluido ningún carácter final). Esta es también la razón por la que forcé la colación en las columnas: no quería que el hecho de que esté usando una instancia de colación binaria niegue injustamente el efecto del uso deCOLLATE palabra clave para forzar una colación diferente en el TVF).

La única diferencia está en el orden de las filas en la tabla.

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

CONFIGURACIÓN: 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

Tenga en cuenta:

  1. Utilicé una intercalación binaria (es decir, _BIN2que es más rápida que una intercalación entre mayúsculas y minúsculas, ya que no necesita tener en cuenta ninguna regla lingüística).
  2. Lo único que realmente importa es la ubicación (es decir, el "índice") del carácter más a la derecha dentro de la lista de caracteres alfabéticos más los dos corchetes. Todo lo que se hace operacionalmente se deriva de esa posición más que el valor del personaje en sí.
  3. He utilizado los tipos de datos de parámetros de entrada y el valor de retorno como se indica en la UDF original que fue reescrito por el OP A menos que haya una buena razón para ir de VARCHAR(50)a VARCHAR(60), y desde NUMERIC (18,3)a NUMERIC (18,2)(razón buena sería "que estaban equivocados"), entonces me gustaría seguir con la firma / tipos originales.
  4. Añadí un punto período / decimal al final de los 3 numéricos literales / constantes: 100., -1., y 1.. Esto no estaba en mi versión original de este TVF (en el historial de esta respuesta) pero noté algunas CONVERT_IMPLICITllamadas en el plan de ejecución XML (ya que 100es un INTpero la operación debe ser NUMERIC/ DECIMAL), así que me ocupé de eso con anticipación .
  5. Creo un carácter de cadena usando la CHAR()función en lugar de pasar una versión de cadena de un número (por ejemplo '2') a una CONVERTfunción (que era lo que estaba haciendo originalmente, nuevamente en la historia). Esto parece ser un poco más rápido. Solo unos pocos milisegundos, pero aún así.

PRUEBA

Tenga en cuenta que tuve que filtrar las filas que terminan en, }ya que eso causó un error en los TVF de OP y Joe. Si bien mi código maneja }correctamente, quería ser coherente con las filas que se estaban probando en las 3 versiones. Es por eso que el número de filas generadas por la consulta de configuración es ligeramente mayor que el número que anoté arriba de los resultados de la prueba para cuántas filas se probaron.

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

El tiempo de CPU es solo un poco más bajo cuando se descomenta el --@Dummy =, y la clasificación entre los 3 TVF es la misma. Pero curiosamente, al descomentar la variable, las clasificaciones cambian un poco:

  • TVF de Joe: 3295 ms
  • TVF de OP: 2240 ms
  • TVF de Salomón: 1203 ms

No estoy seguro de por qué el código del OP funcionaría mucho mejor en este escenario (mientras que el código de mi y Joe solo mejoró marginalmente), pero parece consistente en muchas pruebas. Y no, no miré las diferencias en el plan de ejecución ya que no tengo tiempo para investigar eso.

INCLUSO MÁS RÁPIDO

He completado las pruebas del enfoque alternativo y proporciona una mejora leve pero definitiva a lo que se muestra arriba. El nuevo enfoque utiliza SQLCLR y parece escalar mejor. Descubrí que al agregar en la segunda columna a la consulta, el enfoque T-SQL se duplicaba en el tiempo. Pero, al agregar columnas adicionales usando un UDF escalar SQLCLR, el tiempo aumentó, pero no en la misma cantidad que el tiempo de una sola columna. Tal vez haya una sobrecarga inicial al invocar el método SQLCLR (no asociado con la sobrecarga de la carga inicial del dominio de la aplicación y del ensamblaje en el dominio de la aplicación) porque los tiempos fueron (tiempo transcurrido, no tiempo de CPU):

  • 1 columna: 1018 ms
  • 2 columnas: 1750 - 1800 ms
  • 3 columnas: 2500 - 2600 ms

Por lo tanto, es posible que el tiempo (de volcar a una variable, no devolver el conjunto de resultados) tenga una sobrecarga de 200 ms - 250 ms y luego un tiempo de 750 ms - 800 ms por instancia. Los tiempos de CPU fueron: 950 ms, 1750 ms y 2400 ms para 1, 2 y 3 instancias de la 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);
    }
}

Originalmente lo usé SqlDecimalcomo tipo de retorno, pero hay una penalización de rendimiento por usar eso en lugar de SqlDouble/ FLOAT. A veces, FLOAT tiene problemas (debido a que es un tipo impreciso), pero verifiqué con el T-SQL TVF a través de la siguiente consulta y no se detectaron diferencias:

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

PRUEBA

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
fuente
Gracias por esto. Voy a probar su función con mis datos. Esperamos ver sus cambios para hacerlo aún más rápido y probar datos.
Mazhar
1
@Mazhar Gracias por aceptar :-). Sin embargo, he completado mis pruebas en el enfoque alternativo y descubrí que es un poco más rápido de lo que ya tenía aquí. Utiliza SQLCLR pero escala mejor. También ha vuelto a ser un UDF escalar, por lo que es un poco más fácil trabajar con él (es decir, no necesita el CROSS APPLYs).
Solomon Rutzky
" Tal vez haya una sobrecarga inicial al invocar el método SQLCLR (no asociado con la sobrecarga de la carga inicial del dominio de la aplicación y del ensamblaje en el dominio de la aplicación) " - Iba a sugerir que la sobrecarga podría ser la compilación JIT, ya que solo se encuentra en la primera ejecución. Pero perfilé su código en una aplicación de consola C #, y solo incurrió en 10 de ms de compilación JIT. El método estático específicamente solo tomó .3 ms para ser JIT'd. Pero no sé nada sobre SQLCLR, por lo que quizás haya más código involucrado de lo que sé.
Josh Darnell
1
@ jadarnel27 Gracias por ayudar a investigar. Creo que podría ser una verificación de permiso de algo. Algo relacionado con la generación / validación del plan de consulta.
Solomon Rutzky
4

Comenzaré arrojando algunos datos de prueba en una tabla. No tengo idea de cómo se ven sus datos reales, así que solo utilicé enteros secuenciales:

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;

Seleccionar todas las filas con conjuntos de resultados desactivados proporciona una línea base:

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

Si una consulta similar con la llamada a la función lleva más tiempo, entonces tenemos una estimación aproximada de la sobrecarga de la función. Esto es lo que obtengo al llamar a su TVF tal como está:

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

Por lo tanto, la función necesita aproximadamente 40 segundos de tiempo de CPU para 6.5 millones de filas. Multiplique eso por 20 y son 800 segundos de tiempo de CPU. Noté dos cosas en su código de función:

  1. Uso innecesario de OUTER APPLY. CROSS APPLYle dará los mismos resultados, y para esta consulta evitará un montón de uniones innecesarias. Eso puede ahorrar un poco de tiempo. Depende principalmente de si la consulta completa va paralela. No sé nada sobre sus datos o consulta, así que solo estoy probando con MAXDOP 1. En ese caso estoy mejor con CROSS APPLY.

  2. Hay muchas CHARINDEXllamadas cuando solo busca un carácter en una pequeña lista de valores coincidentes. Puede usar la ASCII()función y un poco de matemática para evitar todas las comparaciones de cadenas.

Aquí hay una forma diferente de escribir la función:

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

En mi máquina, la nueva función es significativamente más 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);

Probablemente también hay algunas optimizaciones adicionales disponibles, pero mi instinto dice que no serán demasiado. Según lo que está haciendo su código, no puedo ver cómo vería una mejora adicional al llamar de alguna manera a su función de una manera diferente. Es solo un montón de operaciones de cadena. Llamar a la función 20 veces por fila será más lento que solo una vez, pero la definición ya está en línea.

Joe Obbish
fuente
Gracias por esto. ¿Está diciendo "por definición ya está en línea" que la ejecución de TVF en varias columnas se comportará como una función en línea?
Mazhar
Voy a probar su función con mis datos.
Mazhar
2

Intenta usar lo siguiente

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

en lugar

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 el uso de una mesa 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','-')

Una consulta de prueba

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, también puede intentar usar una tabla auxiliar temporal #LastCharLinko una tabla variable @LastCharLink(pero puede ser más lenta que una tabla real o temporal)

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

Y úsalo como

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

o

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

Entonces también puede crear una función en línea simple y poner todas las conversiones

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

Y luego use esta función 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
fuente
He actualizado mi respuesta. Intenta usar una mesa auxiliar para hacer lo que quieras. Creo que esta variante será más rápida.
He actualizado mi respuesta una vez más. Ahora usa en Prefixlugar de Divider.
2

Alternativamente, puede crear una tabla permanente. Esta es una creación ú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')

Entonces 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

Del ejemplo de @Joe,

- Tarda 30 s

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

Si es posible, Amount también se puede formatear a nivel de IU. Esta es la mejor opcion. De lo contrario, también puede compartir su consulta original. O si es posible, mantenga el valor formateado en la tabla también.

KumarHarsh
fuente