Le moyen le plus efficace d'appeler la même fonction table sur plusieurs colonnes d'une requête

8

J'essaie de régler une requête dans laquelle la même fonction table (TVF) est appelée sur 20 colonnes.

La première chose que j'ai faite a été de convertir la fonction scalaire en une fonction de valeur de table en ligne.

La méthode CROSS APPLYla plus performante consiste-t-elle à exécuter la même fonction sur plusieurs colonnes dans une requête?

Un exemple simpliste:

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

Existe-t-il de meilleures alternatives?

La même fonction peut être appelée dans plusieurs requêtes sur un nombre X de colonnes.

Voici la fonction:

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

Voici la version de la fonction scalaire dont j'ai hérité, si quelqu'un est intéressé:

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

Exemples de données de test:

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
la source

Réponses:

8

PREMIER: il convient de mentionner que la méthode la plus rapide pour obtenir les résultats souhaités est de procéder comme suit:

  1. Migrez les données dans de nouvelles colonnes ou même dans une nouvelle table:
    1. Nouvelle approche de colonne:
      1. Ajouter de nouvelles colonnes {name}_newau tableau avec le DECIMAL(18, 3)type de données
      2. Effectuer une migration unique des données des anciennes VARCHARcolonnes vers les DECIMALcolonnes
      3. renommer les anciennes colonnes en {name}_old
      4. renommer de nouvelles colonnes pour être juste {name}
    2. Nouvelle approche de table:
      1. Créer une nouvelle table en {table_name}_newutilisant le DECIMAL(18, 3)type de données
      2. Effectuez une migration unique des données de la table actuelle vers la nouvelle DECIMALtable.
      3. renommer l'ancienne table en _old
      4. supprimer _newde la nouvelle table
  2. Mettre à jour l'application, etc. pour ne jamais insérer de données encodées de cette manière
  3. après un cycle de publication, si aucun problème, supprimez les anciennes colonnes ou le tableau
  4. supprimer les TVF et UDF
  5. Ne plus jamais en parler!

CELA ÊTRE DIT: Vous pouvez vous débarrasser d'une grande partie de ce code car il s'agit d'une duplication largement inutile. En outre, il existe au moins deux bogues qui provoquent parfois une sortie incorrecte ou provoquent parfois une erreur. Et ces bogues ont été copiés dans le code de Joe car il produit les mêmes résultats (y compris l'erreur) que le code de l'OP. Par exemple:

  • Ces valeurs produisent un résultat correct:

    00062929x
    00021577E
    00000509H
  • Ces valeurs produisent un résultat incorrect:

    00002020Q
    00016723L
    00009431O
    00017221R
  • Cette valeur produit une erreur:

    00062145}
    anything ending with "}"

En comparant les 3 versions à 448 740 lignes utilisant SET STATISTICS TIME ON;, elles ont toutes fonctionné en un peu plus de 5000 ms de temps écoulé. Mais pour le temps CPU, les résultats étaient:

  • TVF OP: 7031 ms
  • TVF de Joe: 3734 ms
  • TVF de Salomon: 1407 ms

CONFIGURATION: DONNÉES

Ce qui suit crée un tableau et le remplit. Cela devrait créer le même ensemble de données sur tous les systèmes exécutant SQL Server 2017 car ils auront les mêmes lignes spt_values. Cela permet de fournir une base de comparaison entre les autres personnes qui testent sur leur système, car les données générées de manière aléatoire prendraient en compte les différences de synchronisation entre les systèmes, ou même entre les tests sur le même système si les données d'échantillon sont régénérées. J'ai commencé avec le même tableau à 3 colonnes que Joe, mais j'ai utilisé les exemples de valeurs de la question comme modèle pour trouver une variété de valeurs numériques ajoutées à chacune des options de caractère de fin possibles (y compris aucun caractère de fin). C'est aussi pourquoi j'ai forcé le classement sur les colonnes: je ne voulais pas que le fait d'utiliser une instance de classement binaire annule injustement l'effet de l'utilisation duCOLLATE pour forcer un classement différent dans le TVF).

La seule différence réside dans l'ordre des lignes du tableau.

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

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

Notez s'il vous plaît:

  1. J'ai utilisé un classement binaire (c'est-à-dire _BIN2) qui est plus rapide qu'un classement sensible à la casse car il n'a pas besoin de tenir compte des règles linguistiques.
  2. La seule chose qui compte vraiment, c'est l'emplacement (c'est-à-dire "l'index") du caractère le plus à droite dans la liste des caractères alpha plus les deux accolades. Tout ce qui est fait opérationnellement est plus dérivé de cette position que la valeur du caractère lui-même.
  3. J'ai utilisé le paramètre d'entrée et les types de données de valeur de retour comme indiqué dans l'UDF d'origine qui a été réécrit par l'OP Sauf s'il y avait une bonne raison d'aller de VARCHAR(50)à VARCHAR(60), et de NUMERIC (18,3)à NUMERIC (18,2)(une bonne raison serait "ils ont eu tort"), alors je resterais avec la signature / les types originaux.
  4. J'ai ajouté une période / point décimal jusqu'à la fin des 3 littéraux / constantes numériques: 100., -1.et 1.. Ce n'était pas dans ma version originale de ce TVF (dans l'historique de cette réponse) mais j'ai remarqué quelques CONVERT_IMPLICITappels dans le plan d'exécution XML (car 100c'est un INTmais l'opération doit être NUMERIC/ DECIMAL) alors j'ai juste pris soin de cela à l'avance .
  5. Je crée un caractère de chaîne en utilisant la CHAR()fonction plutôt que de passer une version chaîne d'un nombre (par exemple '2') dans une CONVERTfonction (ce que je faisais à l'origine, toujours dans l'histoire). Cela semble être un peu plus rapide. Seulement quelques millisecondes, mais quand même.

TESTER

Veuillez noter que j'ai dû filtrer les lignes se terminant par }car cela provoquait une erreur dans les TVF de l'OP et de Joe. Bien que mon code gère }correctement, je voulais être cohérent avec les lignes testées dans les 3 versions. C'est pourquoi le nombre de lignes générées par la requête de configuration est légèrement supérieur au nombre que j'ai noté au-dessus des résultats du test pour le nombre de lignes testées.

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

Le temps CPU n'est que légèrement inférieur lorsque vous ne commentez pas le --@Dummy =, et le classement parmi les 3 TVF est le même. Mais chose intéressante, lorsque vous ne commentez pas la variable, le classement change un peu:

  • TVF de Joe: 3295 ms
  • TVF OP: 2240 ms
  • TVF de Salomon: 1203 ms

Je ne sais pas pourquoi le code de l'OP fonctionnerait tellement mieux dans ce scénario (alors que mon code et celui de Joe ne s'est amélioré que légèrement), mais il semble cohérent dans de nombreux tests. Et non, je n'ai pas examiné les différences de plan d'exécution car je n'ai pas le temps d'enquêter.

ENCORE PLUS RAPIDE

J'ai terminé les tests de l'approche alternative et elle apporte une amélioration légère mais définitive à ce qui est indiqué ci-dessus. La nouvelle approche utilise SQLCLR et elle semble mieux évoluer. J'ai trouvé qu'en ajoutant dans la deuxième colonne à la requête, l'approche T-SQL double dans le temps. Mais, lors de l'ajout de colonnes supplémentaires à l'aide d'un UDF scalaire SQLCLR, le temps a augmenté, mais pas autant que le timing d'une seule colonne. Peut-être y a-t-il une surcharge initiale lors de l'invocation de la méthode SQLCLR (non associée à la surcharge du chargement initial du domaine d'application et de l'assembly dans le domaine d'application) parce que les synchronisations étaient (temps écoulé, pas temps CPU):

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

Il est donc possible que le timing (du dumping vers une variable, sans retour du jeu de résultats) ait une surcharge de 200 ms - 250 ms, puis de 750 ms - 800 ms par temps d'instance. Les temporisations du processeur étaient respectivement de 950 ms, 1750 ms et 2400 ms pour 1, 2 et 3 instances de l'UDF.

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

J'ai utilisé SqlDecimalà l' origine comme type de retour, mais il y a une pénalité de performance pour l'utiliser par opposition à SqlDouble/ FLOAT. Parfois, FLOAT a des problèmes (car il s'agit d'un type imprécis), mais j'ai vérifié par rapport au T-SQL TVF via la requête suivante et aucune différence n'a été détectée:

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

TESTER

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
la source
Merci pour cela. Je vais tester votre fonction par rapport à mes données. Au plaisir de voir vos modifications pour les rendre encore plus rapides et tester les données.
Mazhar
1
@Mazhar Merci d'avoir accepté :-). Cependant, j'ai terminé mes tests sur l'approche alternative et j'ai constaté qu'elle est légèrement plus rapide que ce que j'avais déjà ici. Il utilise SQLCLR mais évolue mieux. Il est également redevenu un UDF scalaire, donc un peu plus facile à travailler (c'est-à-dire qu'il n'a pas besoin des CROSS APPLYs).
Solomon Rutzky
" Peut-être qu'il y a une surcharge initiale dans l'invocation de la méthode SQLCLR (non associée à la surcharge du chargement initial du domaine d'application et de l'assembly dans le domaine d'application) " - j'allais suggérer que la surcharge pourrait être une compilation JIT, car il n'est rencontré que lors de la première exécution. Mais j'ai profilé votre code dans une application console C #, et cela n'a entraîné que 10 ms de compilation JIT. La méthode statique n'a spécifiquement pris que 0,3 ms pour être JIT. Mais je ne sais rien de SQLCLR, alors peut-être qu'il y a plus de code impliqué que je ne le pense.
Josh Darnell
1
@ jadarnel27 Merci d'avoir aidé à enquêter. Je pense que ce pourrait être une vérification d'autorisation de quelque chose. Quelque chose lié à la génération / validation du plan de requête.
Solomon Rutzky
4

Je vais commencer par jeter quelques données de test dans une table. Je n'ai aucune idée de l'apparence de vos données réelles, j'ai donc utilisé des entiers séquentiels:

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 sélection de toutes les lignes dont les jeux de résultats sont désactivés fournit une ligne de base:

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

Si une requête similaire avec l'appel de fonction prend plus de temps, nous avons une estimation approximative de la surcharge de la fonction. Voici ce que j'obtiens en appelant votre TVF tel quel:

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

La fonction a donc besoin d'environ 40 secondes de temps processeur pour 6,5 millions de lignes. Multipliez cela par 20 et c'est 800 secondes de temps CPU. J'ai remarqué deux choses dans votre code de fonction:

  1. Utilisation inutile de OUTER APPLY. CROSS APPLYvous donnera les mêmes résultats, et pour cette requête, cela évitera un tas de jointures inutiles. Cela peut vous faire gagner un peu de temps. Cela dépend principalement si la requête complète va en parallèle. Je ne sais rien de vos données ou de votre requête, je teste donc simplement avec MAXDOP 1. Dans ce cas, je suis mieux avec CROSS APPLY.

  2. Il y a beaucoup d' CHARINDEXappels lorsque vous recherchez simplement un caractère par rapport à une petite liste de valeurs correspondantes. Vous pouvez utiliser la ASCII()fonction et un peu de maths pour éviter toutes les comparaisons de chaînes.

Voici une manière différente d'écrire la fonction:

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

Sur ma machine, la nouvelle fonction est nettement plus rapide:

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

Il y a probablement aussi des optimisations supplémentaires disponibles, mais mon instinct dit qu'elles ne seront pas très importantes. Sur la base de ce que fait votre code, je ne vois pas comment vous verriez une amélioration supplémentaire en appelant votre fonction d'une manière différente. C'est juste un tas d'opérations de chaîne. L'appel de la fonction 20 fois par ligne sera plus lent qu'une seule fois, mais la définition est déjà intégrée.

Joe Obbish
la source
Merci pour cela. Êtes-vous en train de dire par «la définition est déjà en ligne» que l'exécution du TVF sur plusieurs colonnes se comportera comme une fonction en ligne?
Mazhar
Je vais tester votre fonction par rapport à mes données.
Mazhar
2

Essayez d'utiliser ce qui suit

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

au lieu

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

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

Une variante avec l'utilisation d'une table auxiliaire

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

Une requête de test

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

En variante, vous pouvez également essayer d'utiliser une table auxiliaire temporaire #LastCharLinkou une table variable @LastCharLink(mais elle peut être plus lente qu'une table réelle ou temporaire)

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

Et utilisez-le comme

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

ou

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

Ensuite, vous pouvez également créer une fonction en ligne simple et y mettre toutes les conversions

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

Et puis utilisez cette fonction comme

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
la source
J'ai mis à jour ma réponse. Essayez d'utiliser une table auxiliaire pour faire ce que vous voulez. Je pense que cette variante sera plus rapide.
J'ai mis à jour ma réponse une fois de plus. Maintenant, il utilise Prefixau lieu de Divider.
2

Alternativement, vous pouvez créer une table permanente, c'est une création unique.

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

Puis 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

De l'exemple @Joe,

- Il faut 30 s

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

Si cela est possible, le montant peut également être formaté au niveau de l'interface utilisateur. C'est la meilleure solution. Sinon, vous pouvez également partager votre requête d'origine. OU si possible, conservez également la valeur formatée dans le tableau.

KumarHarsh
la source