@@ -24,11 +24,6 @@ public static class PlanAnalyzer
2424 @"\bCASE\s+(WHEN\b|$)" ,
2525 RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
2626
27- // Matches CTE definitions: WITH name AS ( or , name AS (
28- private static readonly Regex CteDefinitionRegex = new (
29- @"(?:\bWITH\s+|\,\s*)(\w+)\s+AS\s*\(" ,
30- RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
31-
3227 public static void Analyze ( ParsedPlan plan , AnalyzerConfig ? config = null )
3328 {
3429 var cfg = config ?? AnalyzerConfig . Default ;
@@ -40,6 +35,8 @@ public static void Analyze(ParsedPlan plan, AnalyzerConfig? config = null)
4035
4136 if ( stmt . RootNode != null )
4237 AnalyzeNodeTree ( stmt . RootNode , stmt , cfg ) ;
38+
39+ MarkLegacyWarnings ( stmt ) ;
4340 }
4441 }
4542
@@ -48,6 +45,59 @@ public static void Analyze(ParsedPlan plan, AnalyzerConfig? config = null)
4845 ApplySeverityOverrides ( plan , cfg ) ;
4946 }
5047
48+ /// <summary>
49+ /// Rule types that predate the benefit-scoring framework (#215) and haven't
50+ /// been folded into A/B/C/D categorization yet. Tagged so reviewers can hold
51+ /// new-framework items to a higher bar vs known-legacy items that will be
52+ /// reworked later. Remove entries from this set as rules migrate.
53+ /// </summary>
54+ private static readonly HashSet < string > LegacyWarningTypes = new ( StringComparer . OrdinalIgnoreCase )
55+ {
56+ "Excessive Memory Grant" ,
57+ "Large Memory Grant" ,
58+ "Compile Memory Exceeded" ,
59+ "Local Variables" ,
60+ "Optimize For Unknown" ,
61+ "Low Impact Index" ,
62+ "Wide Index Suggestion" ,
63+ "Duplicate Index Suggestions" ,
64+ "Table Variable" ,
65+ "Scalar UDF" ,
66+ "Parallel Skew" ,
67+ "Estimated Plan CE Guess" ,
68+ "Data Type Mismatch" ,
69+ "Lazy Spool Ineffective" ,
70+ "Join OR Clause" ,
71+ "Many-to-Many Merge Join" ,
72+ "Table-Valued Function" ,
73+ "Top Above Scan" ,
74+ "Row Goal" ,
75+ "NOT IN with Nullable Column" ,
76+ "Implicit Conversion" ,
77+ } ;
78+
79+ private static void MarkLegacyWarnings ( PlanStatement stmt )
80+ {
81+ foreach ( var w in stmt . PlanWarnings )
82+ {
83+ if ( LegacyWarningTypes . Contains ( w . WarningType ) )
84+ w . IsLegacy = true ;
85+ }
86+ if ( stmt . RootNode != null )
87+ MarkLegacyWarningsOnTree ( stmt . RootNode ) ;
88+ }
89+
90+ private static void MarkLegacyWarningsOnTree ( PlanNode node )
91+ {
92+ foreach ( var w in node . Warnings )
93+ {
94+ if ( LegacyWarningTypes . Contains ( w . WarningType ) )
95+ w . IsLegacy = true ;
96+ }
97+ foreach ( var child in node . Children )
98+ MarkLegacyWarningsOnTree ( child ) ;
99+ }
100+
51101 // Rule number → WarningType mapping for severity overrides
52102 private static readonly Dictionary < int , string > RuleWarningTypes = new ( )
53103 {
@@ -58,7 +108,7 @@ public static void Analyze(ParsedPlan plan, AnalyzerConfig? config = null)
58108 [ 13 ] = "Data Type Mismatch" , [ 14 ] = "Lazy Spool Ineffective" , [ 15 ] = "Join OR Clause" ,
59109 [ 16 ] = "Nested Loops High Executions" , [ 17 ] = "Many-to-Many Merge Join" ,
60110 [ 18 ] = "Compile Memory Exceeded" , [ 19 ] = "High Compile CPU" , [ 20 ] = "Local Variables" ,
61- [ 21 ] = "CTE Multiple References" , [ 22 ] = "Table Variable" , [ 23 ] = "Table-Valued Function" ,
111+ [ 22 ] = "Table Variable" , [ 23 ] = "Table-Valued Function" ,
62112 [ 24 ] = "Top Above Scan" , [ 25 ] = "Ineffective Parallelism" , [ 26 ] = "Row Goal" ,
63113 [ 27 ] = "Optimize For Unknown" , [ 28 ] = "NOT IN with Nullable Column" ,
64114 [ 29 ] = "Implicit Conversion" , [ 30 ] = "Wide Index Suggestion" ,
@@ -367,11 +417,9 @@ private static void AnalyzeStatement(PlanStatement stmt, AnalyzerConfig cfg)
367417 }
368418 }
369419
370- // Rule 21: CTE referenced multiple times
371- if ( ! cfg . IsRuleDisabled ( 21 ) && ! string . IsNullOrEmpty ( stmt . StatementText ) )
372- {
373- DetectMultiReferenceCte ( stmt ) ;
374- }
420+ // Rule 21 (CTE referenced multiple times) removed per Joe's #215 feedback:
421+ // for actual plans, SQL Server runtime stats show exactly where time was
422+ // spent, so a statement-text-pattern warning about CTE reuse is guessing.
375423
376424 // Rule 27: OPTIMIZE FOR UNKNOWN in statement text
377425 if ( ! cfg . IsRuleDisabled ( 27 ) && ! string . IsNullOrEmpty ( stmt . StatementText ) &&
@@ -1445,41 +1493,6 @@ private static bool IsFunctionOnColumnSide(string predicate, Match funcMatch)
14451493 return Regex . IsMatch ( side , @"\[[^\]@]+\]\.\[" ) ;
14461494 }
14471495
1448- /// <summary>
1449- /// Detects CTEs that are referenced more than once in the statement text.
1450- /// Each reference re-executes the CTE since SQL Server does not materialize them.
1451- /// </summary>
1452- private static void DetectMultiReferenceCte ( PlanStatement stmt )
1453- {
1454- var text = stmt . StatementText ;
1455- var cteMatches = CteDefinitionRegex . Matches ( text ) ;
1456- if ( cteMatches . Count == 0 )
1457- return ;
1458-
1459- foreach ( Match match in cteMatches )
1460- {
1461- var cteName = match . Groups [ 1 ] . Value ;
1462- if ( string . IsNullOrEmpty ( cteName ) )
1463- continue ;
1464-
1465- // Count references as FROM/JOIN targets after the CTE definition
1466- var refPattern = new Regex (
1467- $@ "\b(FROM|JOIN)\s+{ Regex . Escape ( cteName ) } \b",
1468- RegexOptions . IgnoreCase ) ;
1469- var refCount = refPattern . Matches ( text ) . Count ;
1470-
1471- if ( refCount > 1 )
1472- {
1473- stmt . PlanWarnings . Add ( new PlanWarning
1474- {
1475- WarningType = "CTE Multiple References" ,
1476- Message = $ "CTE \" { cteName } \" is referenced { refCount } times. SQL Server re-executes the entire CTE each time — it does not materialize the results. Materialize into a #temp table instead.",
1477- Severity = PlanWarningSeverity . Warning
1478- } ) ;
1479- }
1480- }
1481- }
1482-
14831496 /// <summary>
14841497 /// Verifies the OR expansion chain walking up from a Concatenation node:
14851498 /// Nested Loops → Merge Interval → TopN Sort → [Compute Scalar] → Concatenation
0 commit comments