Comme @Habo l'a mentionné, nous avons besoin du plan d'exécution réel (par exemple, exécutez la requête avec "include actual execution plan" activé.) J'ai regardé ce que vous avez posté et il n'y a rien qui explique le problème. La différence entre le plan réel et le plan estimé est que le nombre réel de lignes extraites est enregistré, ce qui est essentiel pour dépanner les requêtes peu performantes.
Cela dit, les deux requêtes posent un énorme problème. C'est un problème qui, une fois corrigé, améliorera les deux requêtes à moins d'une seconde. Votre requête utilise deux fonctions scalaires définies par l'utilisateur (UDF) : dbo.fn_WorkDaysAge et dbo.fn_WorkDate15. Les UDF scalaires ruinent tout. Non seulement elles sont lentes, mais elles imposent un plan d'exécution en série qui ralentit considérablement toutes les requêtes dans lesquelles elles sont utilisées.
Je n'ai pas le code pour dbo.fn_WorkDaysAge ou dbo.fn_WorkDate15. J'ai ma propre fonction "WorkDays" qui est en ligne (code ci-dessous). La syntaxe est un peu différente, mais les avantages en termes de performances en valent la peine. Voici la différence de syntaxe :
-- Scalar
SELECT d.*, workDays = dbo.countWorkDays_scalar(d.StartDate,d.EndDate)
FROM <sometable> AS d;
-- Inline version
SELECT d.*, f.workDays
FROM <sometable> AS d
CROSS APPLY dbo.countWorkDays(d.StartDate,d.EndDate) AS f;
Voici un test de performance que j'ai réalisé pour montrer la différence entre une version inline et une version scalaire :
-- SAMPLE DATA
IF OBJECT_ID('tempdb..#dates') IS NOT NULL DROP TABLE #dates;
WITH E1(x) AS (SELECT 1 FROM (VALUES(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS x(x)),
E3(x) AS (SELECT 1 FROM E1 a, E1 b, E1 c),
iTally AS (SELECT N=ROW_NUMBER() OVER (ORDER BY (SELECT 1)) FROM E3 a, E3 b)
SELECT TOP (100000)
StartDate = CAST(DATEADD(DAY,-ABS(CHECKSUM(NEWID())%1000),GETDATE()) AS DATE),
EndDate = CAST(DATEADD(DAY,+ABS(CHECKSUM(NEWID())%1000),GETDATE()) AS DATE)
INTO #dates
FROM iTally;
-- PERFORMANCE TESTS
PRINT CHAR(10)+'Scalar Version (always serial):'+CHAR(10)+REPLICATE('-',60);
GO
DECLARE @st DATETIME = GETDATE(), @workdays INT;
SELECT @workdays = dbo.countWorkDays_scalar(d.StartDate,d.EndDate)
FROM #dates AS d;
PRINT DATEDIFF(MS,@st,GETDATE());
GO 3
PRINT CHAR(10)+'Inline Version:'+CHAR(10)+REPLICATE('-',60);
GO
DECLARE @st DATETIME = GETDATE(), @workdays INT;
SELECT @workdays = f.workDays
FROM #dates AS d
CROSS APPLY dbo.countWorkDays(d.StartDate,d.EndDate) AS f
PRINT DATEDIFF(MS,@st,GETDATE());
GO 3
Résultats :
Scalar Version (always serial):
------------------------------------------------------------
Beginning execution loop
380
363
350
Batch execution completed 3 times.
Inline Version:
------------------------------------------------------------
Beginning execution loop
47
47
46
Batch execution completed 3 times.
Comme vous pouvez le constater, la version en ligne est environ 8 fois plus rapide que la version scalaire. Remplacer ces UDFs scalaires par une version en ligne accélérera très certainement cette requête, quel que soit le type de jointure.
Les autres problèmes que je rencontre sont les suivants :
-
Je vois beaucoup de balayages d'index, c'est un signe que vous avez besoin de plus de filtrage et/ou de meilleurs index.
-
dbo.tblCrosswalkWghtPhnEffTarget n'a pas d'index, ce qui signifie qu'elle sera toujours analysée.
Fonctions utilisées pour le test de performance :
-- INLINE VERSION
----------------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.countWorkDays') IS NOT NULL DROP FUNCTION dbo.countWorkDays;
GO
CREATE FUNCTION dbo.countWorkDays (@startDate DATETIME, @endDate DATETIME)
/*****************************************************************************************
[Purpose]:
Calculates the number of business days between two dates (Mon-Fri) and excluded weekends.
dates.countWorkDays does not take holidays into considerations; for this you would need a
seperate "holiday table" to perform an antijoin against.
The idea is based on the solution in this article:
https://www.sqlservercentral.com/Forums/Topic153606.aspx?PageIndex=16
[Author]:
Alan Burstein
[Compatibility]:
SQL Server 2005+
[Syntax]:
--===== Autonomous
SELECT f.workDays
FROM dates.countWorkDays(@startdate, @enddate) AS f;
--===== Against a table using APPLY
SELECT t.col1, t.col2, f.workDays
FROM dbo.someTable t
CROSS APPLY dates.countWorkDays(t.col1, t.col2) AS f;
[Parameters]:
@startDate = datetime; first date to compare
@endDate = datetime; date to compare @startDate to
[Returns]:
Inline Table Valued Function returns:
workDays = int; number of work days between @startdate and @enddate
[Dependencies]:
N/A
[Developer Notes]:
1. NULL when either input parameter is NULL,
2. This function is what is referred to as an "inline" scalar UDF." Technically it's an
inline table valued function (iTVF) but performs the same task as a scalar valued user
defined function (UDF); the difference is that it requires the APPLY table operator
to accept column values as a parameter. For more about "inline" scalar UDFs see this
article by SQL MVP Jeff Moden: http://www.sqlservercentral.com/articles/T-SQL/91724/
and for more about how to use APPLY see the this article by SQL MVP Paul White:
http://www.sqlservercentral.com/articles/APPLY/69953/.
Note the above syntax example and usage examples below to better understand how to
use the function. Although the function is slightly more complicated to use than a
scalar UDF it will yield notably better performance for many reasons. For example,
unlike a scalar UDFs or multi-line table valued functions, the inline scalar UDF does
not restrict the query optimizer's ability generate a parallel query execution plan.
3. dates.countWorkDays requires that @enddate be equal to or later than @startDate. Otherwise
a NULL is returned.
4. dates.countWorkDays is NOT deterministic. For more deterministic functions see:
https://msdn.microsoft.com/en-us/library/ms178091.aspx
[Examples]:
--===== 1. Basic Use
SELECT f.workDays
FROM dates.countWorkDays('20180608', '20180611') AS f;
---------------------------------------------------------------------------------------
[Revision History]:
Rev 00 - 20180625 - Initial Creation - Alan Burstein
*****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT workDays =
-- If @startDate or @endDate are NULL then rerturn a NULL
CASE WHEN SIGN(DATEDIFF(dd, @startDate, @endDate)) > -1 THEN
(DATEDIFF(dd, @startDate, @endDate) + 1) --total days including weekends
-(DATEDIFF(wk, @startDate, @endDate) * 2) --Subtact 2 days for each full weekend
-- Subtract 1 when startDate is Sunday and Substract 1 when endDate is Sunday:
-(CASE WHEN DATENAME(dw, @startDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, @endDate) = 'Saturday' THEN 1 ELSE 0 END)
END;
GO
-- SCALAR VERSION
----------------------------------------------------------------------------------------------
IF OBJECT_ID('dbo.countWorkDays_scalar') IS NOT NULL DROP FUNCTION dbo.countWorkDays_scalar;
GO
CREATE FUNCTION dbo.countWorkDays_scalar (@startDate DATETIME, @endDate DATETIME)
RETURNS INT WITH SCHEMABINDING AS
BEGIN
RETURN
(
SELECT workDays =
-- If @startDate or @endDate are NULL then rerturn a NULL
CASE WHEN SIGN(DATEDIFF(dd, @startDate, @endDate)) > -1 THEN
(DATEDIFF(dd, @startDate, @endDate) + 1) --total days including weekends
-(DATEDIFF(wk, @startDate, @endDate) * 2) --Subtact 2 days for each full weekend
-- Subtract 1 when startDate is Sunday and Substract 1 when endDate is Sunday:
-(CASE WHEN DATENAME(dw, @startDate) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, @endDate) = 'Saturday' THEN 1 ELSE 0 END)
END
);
END
GO
MISE À JOUR BASÉE SUR LA QUESTION DE L'OP DANS LES COMMENTAIRES :
D'abord pour la version de chaque fonction valorisée par un tableau en ligne. Notez que j'utilise mes propres tableaux et que je n'ai pas le temps de faire correspondre les noms à votre environnement, mais j'ai fait de mon mieux pour inclure des commentaires dans le code. Notez également que si, dans votre fonction, workingday = '1'
est simplement d'extraire les jours de la semaine, alors vous trouverez que ma fonction ci-dessus est une alternative beaucoup plus rapide à votre fonction dbo.fn_WorkDaysAge. Si la fonction dbo.fn_WorkDaysAge workingday = '1'
filtre également les vacances, il ne fonctionnera pas.
CREATE FUNCTION dbo.fn_WorkDaysAge_itvf
(
@first_date DATETIME,
@second_date DATETIME
)
RETURNS TABLE AS RETURN
SELECT WorkDays = COUNT(*)
FROM dbo.dimdate -- DateDimension
WHERE DateValue -- [date]
BETWEEN @first_date AND @second_date
AND IsWeekend = 0 --workingday = '1'
GO
CREATE FUNCTION dbo.fn_WorkDate15_itvf
(
@TauStartDate DATETIME
)
RETURNS TABLE AS RETURN
WITH DATES AS
(
SELECT
ROW_NUMBER() OVER(Order By DateValue Desc) as RowNum, DateValue
FROM dbo.dimdate -- DateDimension
WHERE DateValue BETWEEN @TauStartDate AND --GETDATE() testing below
CASE WHEN GETDATE() < @TauStartDate + 200 THEN GETDATE() ELSE @TauStartDate + 200 END
AND IsWeekend = 0 --workingday = '1'
)
--Get the 15th businessday from the current date
SELECT DateValue
FROM DATES
WHERE RowNum = 16;
GO
Maintenant, pour remplacer vos UDF scalaires par les fonctions en ligne évaluées par des tables, vous devez faire ceci (notez mes commentaires) :
WITH agent_split_stats AS (
Select
racf,
agent_stats.SkillGroupSkillTargetID,
aht_target.EnterpriseName,
aht_target.target,
Sum(agent_stats.CallsHandled) as n_calls_handled,
CASE WHEN (Sum(agent_stats.TalkInTime) + Sum(agent_stats.IncomingCallsOnHoldTime) + Sum(agent_stats.WorkReadyTime)) = 0 THEN 1 ELSE
(Sum(agent_stats.TalkInTime) + Sum(agent_stats.IncomingCallsOnHoldTime) + Sum(agent_stats.WorkReadyTime)) END
AS total_handle_time
from tblAceyusAgntSklGrp as agent_stats
INNER JOIN tblCrosswalkWghtPhnEffTarget as aht_target
ON aht_target.SgId = agent_stats.SkillGroupSkillTargetID
AND agent_stats.DateTime BETWEEN aht_target.StartDt and aht_target.EndDt
INNER JOIN tblAgentMetricCrosswalk as xwalk
ON xwalk.SkillTargetID = agent_stats.SkillTargetID
INNER JOIN tblTauClassList AS T
ON T.SaRacf = racf
-- INLINE FUNCTIONS HERE:
CROSS APPLY dbo.fn_WorkDaysAge_itvf(TauStart, GETDATE()) AS wd
CROSS APPLY dbo.fn_WorkDate15_itvf(TauStart) AS w15
-- NEW WHERE CLAUSE:
WHERE agent_stats.DateTime >=
CASE WHEN wd.workdays < 15 THEN TauStart ELSE w15.workdays END
And Graduated = 'No'
AND CallsHandled <> 0
AND Target is not null
Group By
racf, agent_stats.SkillGroupSkillTargetID, aht_target.EnterpriseName, aht_target.target
),
agent_split_stats_with_weight AS (
SELECT
agent_split_stats.*,
agent_split_stats.n_calls_handled/SUM(agent_split_stats.n_calls_handled) OVER(PARTITION BY agent_split_stats.racf) AS [weight]
FROM agent_split_stats
),
agent_split_effectiveness AS
(
SELECT
agent_split_stats_with_weight.*,
(((agent_split_stats_with_weight.target * agent_split_stats_with_weight.n_calls_handled) /
agent_split_stats_with_weight.total_handle_time)*100)*
agent_split_stats_with_weight.weight AS effectiveness_sum
FROM agent_split_stats_with_weight
),
agent_effectiveness AS
(
SELECT
racf AS SaRacf,
ROUND(SUM(effectiveness_sum),2) AS WpeScore
FROM agent_split_effectiveness
GROUP BY racf
),
tau AS
(
SELECT L.SaRacf, TauStart, Goal as WpeGoal
,CASE WHEN agent_effectiveness.WpeScore IS NULL THEN 1 ELSE WpeScore END as WpeScore
FROM tblTauClassList AS L
LEFT JOIN agent_effectiveness
ON agent_effectiveness.SaRacf = L.SaRacf
LEFT JOIN tblCrosswalkTauGoal AS G
ON G.Year = TauYear
AND G.Bucket = 'Wpe'
WHERE TermDate IS NULL
AND Graduated = 'No'
)
SELECT tau.*,
-- NEW CASE STATEMENT HERE:
CASE WHEN wd.workdays > 14 AND WpeScore >= WpeGoal THEN 'Pass' ELSE 'Fail' END
from tau
-- INLINE FUNCTIONS HERE:
CROSS APPLY dbo.fn_WorkDaysAge_itvf(TauStart, GETDATE()) AS wd
CROSS APPLY dbo.fn_WorkDate15_itvf(TauStart) AS w15;
Notez que je ne peux pas tester cela pour le moment, mais cela devrait être correct (ou presque).