@@ -18,11 +18,11 @@ public static class BenefitScorer
1818 "Filter Operator" , // Rule 1
1919 "Eager Index Spool" , // Rule 2
2020 "Spill" , // Rule 7
21- "Key Lookup" , // Rule 10
22- "RID Lookup" , // Rule 10 variant
21+ // Key Lookup / RID Lookup (Rule 10) handled separately by ScoreKeyLookupWarning
2322 "Scan With Predicate" , // Rule 11
2423 "Non-SARGable Predicate" , // Rule 12
2524 "Scan Cardinality Misestimate" , // Rule 32
25+ "Bare Scan" , // Rule 34
2626 } ;
2727
2828 public static void Score ( ParsedPlan plan )
@@ -50,44 +50,18 @@ private static void ScoreStatementWarnings(PlanStatement stmt)
5050 {
5151 switch ( warning . WarningType )
5252 {
53- case "Ineffective Parallelism" : // Rule 25
54- case "Parallel Wait Bottleneck" : // Rule 31
55- // These are meta-findings about parallelism efficiency.
56- // The benefit is the gap between actual and ideal elapsed time.
57- if ( elapsedMs > 0 && stmt . QueryTimeStats != null )
58- {
59- var cpu = stmt . QueryTimeStats . CpuTimeMs ;
60- var dop = stmt . DegreeOfParallelism ;
61- if ( dop > 1 && cpu > 0 )
62- {
63- // Ideal elapsed = CPU / DOP. Benefit = (actual - ideal) / actual
64- var idealElapsed = ( double ) cpu / dop ;
65- var benefit = Math . Max ( 0 , ( elapsedMs - idealElapsed ) / elapsedMs * 100 ) ;
66- warning . MaxBenefitPercent = Math . Min ( 100 , Math . Round ( benefit , 1 ) ) ;
67- }
68- }
69- break ;
70-
7153 case "Serial Plan" : // Rule 3
72- // Can't know how fast a parallel plan would be, but estimate:
73- // CPU-bound: benefit up to (1 - 1/maxDOP) * 100%
74- if ( elapsedMs > 0 && stmt . QueryTimeStats != null )
54+ // Per Joe's formula: (cpu * (DOP - 1) / DOP) / elapsed * 100
55+ // Assumes DOP 4 when the plan doesn't tell us. No benefit when cost < 1
56+ // (trivial plans don't gain from parallelism).
57+ if ( elapsedMs > 0 && stmt . QueryTimeStats != null && stmt . StatementSubTreeCost >= 1.0 )
7558 {
7659 var cpu = stmt . QueryTimeStats . CpuTimeMs ;
77- // Assume server max DOP — use a conservative 4 if unknown
7860 var potentialDop = 4 ;
79- if ( cpu >= elapsedMs )
80- {
81- // CPU-bound: parallelism could help significantly
82- var benefit = ( 1.0 - 1.0 / potentialDop ) * 100 ;
83- warning . MaxBenefitPercent = Math . Round ( benefit , 1 ) ;
84- }
85- else
61+ if ( cpu > 0 )
8662 {
87- // Not CPU-bound: parallelism helps less
88- var cpuRatio = ( double ) cpu / elapsedMs ;
89- var benefit = cpuRatio * ( 1.0 - 1.0 / potentialDop ) * 100 ;
90- warning . MaxBenefitPercent = Math . Round ( Math . Min ( 50 , benefit ) , 1 ) ;
63+ var benefit = ( ( double ) cpu * ( potentialDop - 1 ) / potentialDop ) / elapsedMs * 100 ;
64+ warning . MaxBenefitPercent = Math . Round ( Math . Min ( 100 , benefit ) , 1 ) ;
9165 }
9266 }
9367 break ;
@@ -150,6 +124,10 @@ private static void ScoreNodeWarnings(PlanNode node, PlanStatement stmt)
150124 {
151125 ScoreSpillWarning ( warning , node , stmt ) ;
152126 }
127+ else if ( warning . WarningType is "Key Lookup" or "RID Lookup" ) // Rule 10
128+ {
129+ ScoreKeyLookupWarning ( warning , node , stmt ) ;
130+ }
153131 else if ( OperatorTimeRules . Contains ( warning . WarningType ) )
154132 {
155133 ScoreByOperatorTime ( warning , node , stmt ) ;
@@ -159,7 +137,8 @@ private static void ScoreNodeWarnings(PlanNode node, PlanStatement stmt)
159137 ScoreEstimateMismatchWarning ( warning , node , stmt ) ;
160138 }
161139 // Rules that stay null: Scalar UDF (Rule 6, informational reference),
162- // Parallel Skew (Rule 8), Data Type Mismatch (Rule 13),
140+ // Parallel Skew (Rule 8 — will be integrated per-operator later),
141+ // Data Type Mismatch (Rule 13),
163142 // Lazy Spool Ineffective (Rule 14), Join OR Clause (Rule 15),
164143 // Many-to-Many Merge Join (Rule 17), CTE Multiple References (Rule 21),
165144 // Table Variable (Rule 22), Table-Valued Function (Rule 23),
@@ -282,6 +261,61 @@ private static void ScoreByOperatorTime(PlanWarning warning, PlanNode node, Plan
282261 }
283262 }
284263
264+ /// <summary>
265+ /// Rule 10: Key Lookup / RID Lookup — benefit includes the lookup operator's time,
266+ /// plus the parent Nested Loops join when the NL only exists to drive the lookup
267+ /// (inner child is the lookup, outer child is a seek/scan with no subtree).
268+ /// </summary>
269+ private static void ScoreKeyLookupWarning ( PlanWarning warning , PlanNode node , PlanStatement stmt )
270+ {
271+ var stmtMs = stmt . QueryTimeStats ? . ElapsedTimeMs ?? 0 ;
272+
273+ if ( node . HasActualStats && stmtMs > 0 )
274+ {
275+ var operatorMs = PlanAnalyzer . GetOperatorOwnElapsedMs ( node ) ;
276+
277+ // Check if the parent NL join is purely a lookup driver:
278+ // - Parent is Nested Loops
279+ // - Has exactly 2 children
280+ // - This node (the lookup) is the inner child (index 1)
281+ // - The outer child (index 0) is a simple seek/scan with no children
282+ var parent = node . Parent ;
283+ if ( parent != null
284+ && parent . PhysicalOp == "Nested Loops"
285+ && parent . Children . Count == 2
286+ && parent . Children [ 1 ] == node
287+ && parent . Children [ 0 ] . Children . Count == 0 )
288+ {
289+ operatorMs += PlanAnalyzer . GetOperatorOwnElapsedMs ( parent ) ;
290+ }
291+
292+ if ( operatorMs > 0 )
293+ {
294+ var benefit = ( double ) operatorMs / stmtMs * 100 ;
295+ warning . MaxBenefitPercent = Math . Round ( Math . Min ( 100 , benefit ) , 1 ) ;
296+ }
297+ else
298+ {
299+ warning . MaxBenefitPercent = 0 ;
300+ }
301+ }
302+ else if ( ! node . HasActualStats && stmt . StatementSubTreeCost > 0 )
303+ {
304+ var benefit = ( double ) node . CostPercent ;
305+ // Same parent-NL logic for estimated plans
306+ var parent = node . Parent ;
307+ if ( parent != null
308+ && parent . PhysicalOp == "Nested Loops"
309+ && parent . Children . Count == 2
310+ && parent . Children [ 1 ] == node
311+ && parent . Children [ 0 ] . Children . Count == 0 )
312+ {
313+ benefit += parent . CostPercent ;
314+ }
315+ warning . MaxBenefitPercent = Math . Round ( Math . Min ( 100 , benefit ) , 1 ) ;
316+ }
317+ }
318+
285319 /// <summary>
286320 /// Rule 5: Row Estimate Mismatch — benefit is the harmed operator's time.
287321 /// If the mismatch caused a spill, benefit = spilling operator time.
0 commit comments