@@ -236,18 +236,30 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt)
236236 // Rule 5: Large estimate vs actual row gaps (actual plans only)
237237 if ( node . HasActualStats && node . EstimateRows > 0 )
238238 {
239- var ratio = node . ActualRows / node . EstimateRows ;
240- if ( ratio >= 10.0 || ratio <= 0.1 )
239+ if ( node . ActualRows == 0 )
241240 {
242- var direction = ratio >= 10.0 ? "underestimated" : "overestimated" ;
243- var factor = ratio >= 10.0 ? ratio : 1.0 / ratio ;
244241 node . Warnings . Add ( new PlanWarning
245242 {
246243 WarningType = "Row Estimate Mismatch" ,
247- Message = $ "Estimated { node . EstimateRows : N0} rows, actual { node . ActualRows : N0 } ( { factor : F0 } x { direction } ) . May cause poor plan choices.",
248- Severity = factor >= 100 ? PlanWarningSeverity . Critical : PlanWarningSeverity . Warning
244+ Message = $ "Estimated { node . EstimateRows : N0} rows, actual 0 rows returned . May cause poor plan choices.",
245+ Severity = node . EstimateRows >= 100 ? PlanWarningSeverity . Critical : PlanWarningSeverity . Warning
249246 } ) ;
250247 }
248+ else
249+ {
250+ var ratio = node . ActualRows / node . EstimateRows ;
251+ if ( ratio >= 10.0 || ratio <= 0.1 )
252+ {
253+ var direction = ratio >= 10.0 ? "underestimated" : "overestimated" ;
254+ var factor = ratio >= 10.0 ? ratio : 1.0 / ratio ;
255+ node . Warnings . Add ( new PlanWarning
256+ {
257+ WarningType = "Row Estimate Mismatch" ,
258+ Message = $ "Estimated { node . EstimateRows : N0} rows, actual { node . ActualRows : N0} ({ factor : F0} x { direction } ). May cause poor plan choices.",
259+ Severity = factor >= 100 ? PlanWarningSeverity . Critical : PlanWarningSeverity . Warning
260+ } ) ;
261+ }
262+ }
251263 }
252264
253265 // Rule 6: Scalar UDF references (works on estimated plans too)
@@ -270,10 +282,12 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt)
270282 }
271283
272284 // Rule 8: Parallel thread skew (actual plans with per-thread stats)
285+ // Only warn when there are enough rows to meaningfully distribute across threads
273286 if ( node . PerThreadStats . Count > 1 )
274287 {
275288 var totalRows = node . PerThreadStats . Sum ( t => t . ActualRows ) ;
276- if ( totalRows > 0 )
289+ var minRowsForSkew = node . PerThreadStats . Count * 1000 ;
290+ if ( totalRows >= minRowsForSkew )
277291 {
278292 var maxThread = node . PerThreadStats . OrderByDescending ( t => t . ActualRows ) . First ( ) ;
279293 var skewRatio = ( double ) maxThread . ActualRows / totalRows ;
@@ -448,25 +462,37 @@ private static void AnalyzeNode(PlanNode node, PlanStatement stmt)
448462 } ) ;
449463 }
450464
451- // Rule 24: Top above a scan (linear search pattern)
452- if ( node . PhysicalOp == "Top" && node . Children . Count > 0 )
465+ // Rule 24: Top above a scan on the inner side of Nested Loops
466+ // This pattern means the scan executes once per outer row, and the Top
467+ // limits each iteration — but with no supporting index the scan is a
468+ // linear search repeated potentially millions of times.
469+ if ( node . PhysicalOp == "Nested Loops" && node . Children . Count >= 2 )
453470 {
454- // Walk through pass-through operators (Compute Scalar, etc.)
455- var child = node . Children [ 0 ] ;
456- while ( child . PhysicalOp == "Compute Scalar" && child . Children . Count > 0 )
457- child = child . Children [ 0 ] ;
471+ var inner = node . Children [ 1 ] ;
458472
459- if ( IsRowstoreScan ( child ) )
473+ // Walk through pass-through operators to find Top
474+ while ( inner . PhysicalOp == "Compute Scalar" && inner . Children . Count > 0 )
475+ inner = inner . Children [ 0 ] ;
476+
477+ if ( inner . PhysicalOp == "Top" && inner . Children . Count > 0 )
460478 {
461- var predInfo = ! string . IsNullOrEmpty ( child . Predicate )
462- ? " The scan has a residual predicate, so it may read many rows before the Top is satisfied."
463- : "" ;
464- node . Warnings . Add ( new PlanWarning
479+ // Walk through pass-through operators below the Top to find the scan
480+ var scanCandidate = inner . Children [ 0 ] ;
481+ while ( scanCandidate . PhysicalOp == "Compute Scalar" && scanCandidate . Children . Count > 0 )
482+ scanCandidate = scanCandidate . Children [ 0 ] ;
483+
484+ if ( IsRowstoreScan ( scanCandidate ) )
465485 {
466- WarningType = "Top Above Scan" ,
467- Message = $ "Top operator reads from { child . PhysicalOp } (Node { child . NodeId } ).{ predInfo } An index supporting the filter and ordering may convert this to a seek.",
468- Severity = PlanWarningSeverity . Warning
469- } ) ;
486+ var predInfo = ! string . IsNullOrEmpty ( scanCandidate . Predicate )
487+ ? " The scan has a residual predicate, so it may read many rows before the Top is satisfied."
488+ : "" ;
489+ inner . Warnings . Add ( new PlanWarning
490+ {
491+ WarningType = "Top Above Scan" ,
492+ Message = $ "Top operator reads from { scanCandidate . PhysicalOp } (Node { scanCandidate . NodeId } ) on the inner side of Nested Loops (Node { node . NodeId } ).{ predInfo } An index supporting the filter and ordering may convert this to a seek.",
493+ Severity = PlanWarningSeverity . Warning
494+ } ) ;
495+ }
470496 }
471497 }
472498 }
0 commit comments