J'ai une procédure stockée:
create proc sp_MyProc(@calcType tinyint) as
begin
-- some stuff collating data into #MyTempTable
if (@calcType = 1) -- sum
select A, B, C, CalcField = sum(Amount)
from #MyTempTable t
join AnotherTable a on t.Field1 = a.Field1;
group by A, B, C
else if (@calcType = 2) -- average
select A, B, C, CalcField = avg(Amount)
from #MyTempTable t
join AnotherTable a on t.Field1 = a.Field1;
group by A, B, C
else if (@calcType = 3) -- some other fancy formula
select A, B, C, CalcField = sum(case when t.Type = 1 then 1 else 0 end) * t.Factor
from #MyTempTable t
join AnotherTable a on t.Field1 = a.Field1;
group by A, B, C
-- plus a whole bunch of other, similar cases
else
select A, B, C, CalcField = 0.0
from #MyTempTable t
join AnotherTable a on t.Field1 = a.Field1;
group by A, B, C
end
Maintenant, tous ces cas différents pour différentes valeurs de @calcType semblent gaspiller beaucoup d'espace et me faire copier et coller partout, ce qui envoie toujours des frissons le long de ma colonne vertébrale.
Existe-t-il un moyen de déclarer une fonction pour CalcField, similaire à la notation lambda en C #, pour rendre mon code plus compact et maintenable? Je voudrais faire quelque chose comme ça:
declare @func FUNCTION(@t #MyTempTable) as real -- i.e. input is of type #MyTempTable and output is of type real
if (@calcType = 1) -- sum
set @func = sum(@t.Amount)
else if (@calcType = 2) -- average
set @func = avg(@t.Amount)
else if (@calcType = 3) -- some other fancy formula
set @func = sum(case when @t.Type = 1 then 1 else 0 end) * @t.Factor
-- plus a whole bunch of other, similar cases
else
set @func = 0;
select A, B, C, CalcField = @func(t)
from #MyTempTable t
join AnotherTable a on t.Field1 = a.Field1;
group by A, B, C
Évidemment, la syntaxe ici ne fonctionne pas, mais y a-t-il quelque chose qui atteindra ce que je veux?
Non, ce n'est pas possible.
Un TVF ou une vue permanente n'est pas une option en raison de la référence à #MyTempTable
J'ai vu une demande d'élément de connexion pour vues temporaires et j'accepte parfois qu'elles soient utiles. Celui-ci a été fermé en tant que doublon d'une requête expressions de table au niveau du module .
Vous pourrez peut-être réécrire
SELECT A,
B,
C,
CASE @calcType
WHEN 1
THEN sum(Amount)
WHEN 2
THEN avg(Amount)
END
FROM #MyTempTable t
JOIN AnotherTable a
ON t.Field1 = a.Field1
GROUP BY A,
B,
C
Ou si vos besoins étaient plus complexes (par exemple, même jeu de résultats de forme et utilisation de la même source mais de conditions de regroupement différentes)
WITH Base
AS (SELECT A,
B,
C,
Factor,
Type
FROM #MyTempTable t
JOIN AnotherTable a
ON t.Field1 = a.Field1)
SELECT A,
B,
C,
sum(Amount)
FROM Base
WHERE @calcType = 1
GROUP BY A,
B,
C
UNION ALL
SELECT A,
B,
C,
CalcField = 0.0 * t.Factor
FROM Base
WHERE @calcType NOT IN ( 1, 2, 3 )
Avec le dernier en particulier, vous pourriez envisager OPTION (RECOMPILE)
pour simplifier le plan. (Il est probable qu'il aurait un filtre avec un prédicat de démarrage sans l'indication et n'exécuterait pas réellement les branches redondantes, mais vous auriez besoin de vérifier. De même, les estimations de ligne pourraient être erronées avec cette approche si le prédicat de démarrage est conservé).
Si ni l'un ni l'autre ne vous convient, vous devrez entrer dans le domaine du SQL dynamique.
Strictement parlant (sous-programme T-SQL): Non.
Techniquement parlant (un moyen d'abstraire des formules à définir une fois): Oui.
Pragmatiquement parlant: cela dépend :).
Voici les problèmes qui vous empêchent actuellement concernant les restrictions sur les fonctions T-SQL:
Cependant, tout cela peut être fait dans SQLCLR (enfin, pas la partie dynamique, mais cela ne semble pas être le point central ici). En utilisant SQLCLR, vous pouvez créer une fonction qui peut accéder à une table temporaire, et il peut même s'agir d'une fonction agrégée. Bien sûr, pour les calculs simplistes tels que SUM
et AVG
, vous pourriez perdre plus de performances que vous ne gagnez à réduire la duplication de code, mais c'est une question de test (d'où une grande partie de la raison pour laquelle "Ça dépend").
Maintenant, dans ce cas spécifique, il ne semble même pas que l'accès à une table temporaire soit nécessaire car les valeurs par ligne seraient naturellement envoyées dans l'agrégat défini par l'utilisateur. En supposant que dbo.DynamicCalc a une signature de DynamicCalc(@CalcType TINYINT, @Amount FLOAT)
:
select A, B, C, CalcField = dbo.DynamicCalc(2, t.Amount)
from #MyTempTable t
join AnotherTable a
on t.Field1 = a.Field1
group by A, B, C;
ou:
select A, B, C, CalcField = dbo.DynamicCalc(3, IIF(t.Type = 1, t.Factor, 0))
from #MyTempTable t
join AnotherTable a
on t.Field1 = a.Field1
group by A, B, C;
Ou vous pourriez passer t.Factor
en soi comme @Amount
param et ajoutez un paramètre supplémentaire pour @Type INT
qui recevra t.Type
qui ne sera utilisé que si @CalcType
= 3.
Encore une fois, l'opportunité d'adopter ou non cette approche est une question d'ordre pratique qui dépend fortement des formules que vous avez. La suggestion de @Martin de faire le CASE @calcType
L'instruction pour basculer entre les formules sera plus efficace si les formules sont assez simples (car elles semblent être basées sur le code affiché dans la question). Mais si ces formules deviennent assez complexes, ou si vous avez vraiment besoin d'accéder à une table temporaire, alors c'est une option à considérer.