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.
Utilise CROSS APPLY
la meilleure façon d'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
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:
{name}_new
À la table avec le type de données DECIMAL(18, 3)
VARCHAR
vers les colonnes DECIMAL
{name}_old
{name}
{table_name}_new
En utilisant DECIMAL(18, 3)
type de donnéesDECIMAL
._old
_new
de la nouvelle tableCELA Ê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'O.P. 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 contre 448 740 lignes en 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:
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 dans 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 du mot clé COLLATE
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:
_BIN2
) Qui est plus rapide qu'un classement sensible à la casse car il n'a besoin de prendre en compte aucune règle linguistique.VARCHAR(50)
à VARCHAR(60)
, et de NUMERIC (18,3)
à NUMERIC (18,2)
(la bonne raison serait "ils se sont trompés"), alors je m'en tiendrai à la signature/types d'origine.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 appels CONVERT_IMPLICIT
Dans le plan d'exécution XML (puisque 100
Est un INT
mais l'opération doit être NUMERIC
/DECIMAL
) donc j'ai juste pris soin de cela à l'avance.CHAR()
plutôt que de passer une version chaîne d'un nombre (par exemple '2'
) Dans une fonction CONVERT
(ce que je faisais à l'origine , encore une fois dans l'histoire). Cela semble être un peu plus rapide. Seulement quelques millisecondes, mais quand même.TEST
Veuillez noter que j'ai dû filtrer les lignes se terminant par }
Car cela provoquait une erreur dans les TVF d'O.P.et de Joe. Bien que mon code gère correctement le }
, 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:
Je ne sais pas pourquoi le code O.P. 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 légère mais nette amélioration à ce qui est indiqué ci-dessus. La nouvelle approche utilise SQLCLR et elle semble mieux évoluer. J'ai trouvé que lors de l'ajout dans la deuxième colonne de 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 temporisations étaient (temps écoulé, pas temps CPU):
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é à l'origine SqlDecimal
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 TVF T-SQL 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]);
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;
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:
Utilisation inutile de OUTER APPLY
. CROSS APPLY
Vous 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 préfère CROSS APPLY
.
Il y a beaucoup d'appels CHARINDEX
lorsque vous recherchez juste un caractère par rapport à une petite liste de valeurs correspondantes. Vous pouvez utiliser la fonction ASCII()
et un peu de calcul 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 quelques 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. Appeler la fonction 20 fois par ligne sera plus lent qu'une seule fois, mais la définition est déjà intégrée.
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. OR si possible, conservez également la valeur formatée dans le tableau.
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 #LastCharLink
ou une table de variables @LastCharLink
(mais il peut être plus lent 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