@@ -1578,19 +1578,51 @@ public async Task<List<RecommendationRow>> GetRecommendationsAsync(int serverId,
15781578 await sqlConn . OpenAsync ( ) ;
15791579
15801580 using var editionCmd = new SqlCommand (
1581- "SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128))" , sqlConn ) ;
1581+ "SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)), " +
1582+ "CAST(SERVERPROPERTY('ProductMajorVersion') AS INT)" , sqlConn ) ;
15821583 editionCmd . CommandTimeout = 30 ;
1583- var edition = ( string ? ) await editionCmd . ExecuteScalarAsync ( ) ?? "" ;
1584+ using var editionReader = await editionCmd . ExecuteReaderAsync ( ) ;
1585+ string edition = "" ;
1586+ int majorVersion = 0 ;
1587+ if ( await editionReader . ReadAsync ( ) )
1588+ {
1589+ edition = editionReader . IsDBNull ( 0 ) ? "" : editionReader . GetString ( 0 ) ;
1590+ majorVersion = editionReader . IsDBNull ( 1 ) ? 0 : editionReader . GetInt32 ( 1 ) ;
1591+ }
15841592
15851593 if ( edition . Contains ( "Enterprise" , StringComparison . OrdinalIgnoreCase ) )
15861594 {
1587- /*
1588- sys.dm_db_persisted_sku_features is database-scoped on all versions.
1589- Query across all online user databases for TDE usage — the only feature
1590- still Enterprise-only since 2016 SP1 (Compression, Partitioning,
1591- ColumnStoreIndex are all available in Standard).
1592- */
1593- using var featCmd = new SqlCommand ( @"
1595+ // SQL Server 2019 (major version 15) moved TDE to Standard Edition.
1596+ // On 2019+, dm_db_persisted_sku_features won't report TDE since it's
1597+ // no longer Enterprise-restricted — so we skip the TDE-specific check
1598+ // and give version-appropriate guidance instead.
1599+ if ( majorVersion >= 15 )
1600+ {
1601+ // 2019+: Most features that were Enterprise-only moved to Standard
1602+ // in 2016 SP1, and TDE moved in 2019. Very few Enterprise-only
1603+ // features remain (e.g., certain HA configurations).
1604+ recommendations . Add ( new RecommendationRow
1605+ {
1606+ Category = "Licensing" ,
1607+ Severity = "High" ,
1608+ Confidence = "Medium" ,
1609+ Finding = "Enterprise Edition may not be required" ,
1610+ Detail = "Starting with SQL Server 2019, most previously Enterprise-only features " +
1611+ "(including TDE, compression, partitioning, and columnstore) are available " +
1612+ "in Standard Edition. Review whether remaining Enterprise-only features " +
1613+ "(such as Always On availability groups with multiple secondaries) are in use " +
1614+ "before considering a downgrade to Standard Edition." ,
1615+ EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
1616+ } ) ;
1617+ }
1618+ else
1619+ {
1620+ /*
1621+ Pre-2019: TDE is the only commonly-used feature still restricted
1622+ to Enterprise Edition since 2016 SP1. Use dm_db_persisted_sku_features
1623+ to detect it — the DMV correctly reports TDE on these versions.
1624+ */
1625+ using var featCmd = new SqlCommand ( @"
15941626DECLARE
15951627 @sql nvarchar(max) = N'';
15961628
@@ -1609,62 +1641,63 @@ IF @sql <> N''
16091641 SET @sql = LEFT(@sql, LEN(@sql) - 10);
16101642 EXEC sys.sp_executesql @sql;
16111643END;" , sqlConn ) ;
1612- featCmd . CommandTimeout = 30 ;
1644+ featCmd . CommandTimeout = 30 ;
16131645
1614- var tdeDbNames = new List < string > ( ) ;
1615- using var featReader = await featCmd . ExecuteReaderAsync ( ) ;
1616- while ( await featReader . ReadAsync ( ) )
1617- {
1618- if ( ! featReader . IsDBNull ( 0 ) )
1619- tdeDbNames . Add ( featReader . GetString ( 0 ) ) ;
1620- }
1621-
1622- if ( tdeDbNames . Count == 0 )
1623- {
1624- recommendations . Add ( new RecommendationRow
1625- {
1626- Category = "Licensing" ,
1627- Severity = "High" ,
1628- Confidence = "High" ,
1629- Finding = "Enterprise Edition with no Enterprise-only features detected" ,
1630- Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
1631- "still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
1632- "Review whether Standard Edition would meet workload requirements for potential license savings." ,
1633- EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
1634- } ) ;
1635- }
1636- else
1637- {
1638- recommendations . Add ( new RecommendationRow
1646+ var tdeDbNames = new List < string > ( ) ;
1647+ using var featReader = await featCmd . ExecuteReaderAsync ( ) ;
1648+ while ( await featReader . ReadAsync ( ) )
16391649 {
1640- Category = "Licensing" ,
1641- Severity = "Low" ,
1642- Confidence = "High" ,
1643- Finding = "TDE in use — Enterprise Edition downgrade blocker" ,
1644- Detail = $ "The following databases use Transparent Data Encryption: { string . Join ( ", " , tdeDbNames . Take ( 20 ) ) } " +
1645- ( tdeDbNames . Count > 20 ? $ " and { tdeDbNames . Count - 20 } more" : "" ) +
1646- ". TDE must be removed before downgrading to Standard Edition."
1647- } ) ;
1650+ if ( ! featReader . IsDBNull ( 0 ) )
1651+ tdeDbNames . Add ( featReader . GetString ( 0 ) ) ;
1652+ }
16481653
1649- // Check 10: License cost impact estimate (only when features ARE in use)
1650- using var cpuInfoCmd = new SqlCommand (
1651- "SELECT cpu_count FROM sys.dm_os_sys_info" , sqlConn ) ;
1652- cpuInfoCmd . CommandTimeout = 30 ;
1653- var cpuCountObj = await cpuInfoCmd . ExecuteScalarAsync ( ) ;
1654- var coreLicenseCount = cpuCountObj != null ? Convert . ToInt32 ( cpuCountObj ) : 0 ;
1655- if ( coreLicenseCount > 0 )
1654+ if ( tdeDbNames . Count == 0 )
1655+ {
1656+ recommendations . Add ( new RecommendationRow
1657+ {
1658+ Category = "Licensing" ,
1659+ Severity = "High" ,
1660+ Confidence = "High" ,
1661+ Finding = "Enterprise Edition with no Enterprise-only features detected" ,
1662+ Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
1663+ "still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
1664+ "Review whether Standard Edition would meet workload requirements for potential license savings." ,
1665+ EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
1666+ } ) ;
1667+ }
1668+ else
16561669 {
1657- var monthlySavings = coreLicenseCount * 5000m / 12m ;
16581670 recommendations . Add ( new RecommendationRow
16591671 {
16601672 Category = "Licensing" ,
16611673 Severity = "Low" ,
1662- Confidence = "Low " ,
1663- Finding = $ "Enterprise to Standard would save ~$ { monthlySavings : N0 } /mo at list pricing ( { coreLicenseCount } cores) ",
1664- Detail = "Based on list pricing differential of ~$5,000/core/year between Enterprise and Standard. " +
1665- "Actual savings depend on your licensing agreement. See Enterprise feature audit for downgrade blockers." ,
1666- EstMonthlySavings = monthlySavings
1674+ Confidence = "High " ,
1675+ Finding = "TDE in use — Enterprise Edition downgrade blocker ",
1676+ Detail = $ "The following databases use Transparent Data Encryption: { string . Join ( ", " , tdeDbNames . Take ( 20 ) ) } " +
1677+ ( tdeDbNames . Count > 20 ? $ " and { tdeDbNames . Count - 20 } more" : "" ) +
1678+ ". TDE must be removed before downgrading to Standard Edition."
16671679 } ) ;
1680+
1681+ // Check 10: License cost impact estimate (only when features ARE in use)
1682+ using var cpuInfoCmd = new SqlCommand (
1683+ "SELECT cpu_count FROM sys.dm_os_sys_info" , sqlConn ) ;
1684+ cpuInfoCmd . CommandTimeout = 30 ;
1685+ var cpuCountObj = await cpuInfoCmd . ExecuteScalarAsync ( ) ;
1686+ var coreLicenseCount = cpuCountObj != null ? Convert . ToInt32 ( cpuCountObj ) : 0 ;
1687+ if ( coreLicenseCount > 0 )
1688+ {
1689+ var monthlySavings = coreLicenseCount * 5000m / 12m ;
1690+ recommendations . Add ( new RecommendationRow
1691+ {
1692+ Category = "Licensing" ,
1693+ Severity = "Low" ,
1694+ Confidence = "Low" ,
1695+ Finding = $ "Enterprise to Standard would save ~${ monthlySavings : N0} /mo at list pricing ({ coreLicenseCount } cores)",
1696+ Detail = "Based on list pricing differential of ~$5,000/core/year between Enterprise and Standard. " +
1697+ "Actual savings depend on your licensing agreement. See Enterprise feature audit for downgrade blockers." ,
1698+ EstMonthlySavings = monthlySavings
1699+ } ) ;
1700+ }
16681701 }
16691702 }
16701703 }
0 commit comments