Skip to content

Commit 2b67f09

Browse files
Merge pull request #855 from erikdarlingdata/fix/854-finops-tde-version-awareness
Fix FinOps TDE recommendation on SQL Server 2019+
2 parents 29e561c + 4235f50 commit 2b67f09

2 files changed

Lines changed: 163 additions & 97 deletions

File tree

Dashboard/Services/DatabaseService.FinOps.cs

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1852,19 +1852,51 @@ public async Task<List<FinOpsRecommendation>> GetFinOpsRecommendationsAsync(deci
18521852
try
18531853
{
18541854
using var editionCmd = new SqlCommand(
1855-
"SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128))", connection);
1855+
"SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)), " +
1856+
"CAST(SERVERPROPERTY('ProductMajorVersion') AS INT)", connection);
18561857
editionCmd.CommandTimeout = 30;
1857-
var edition = (string?)await editionCmd.ExecuteScalarAsync() ?? "";
1858+
using var editionReader = await editionCmd.ExecuteReaderAsync();
1859+
string edition = "";
1860+
int majorVersion = 0;
1861+
if (await editionReader.ReadAsync())
1862+
{
1863+
edition = editionReader.IsDBNull(0) ? "" : editionReader.GetString(0);
1864+
majorVersion = editionReader.IsDBNull(1) ? 0 : editionReader.GetInt32(1);
1865+
}
18581866

18591867
if (edition.Contains("Enterprise", StringComparison.OrdinalIgnoreCase))
18601868
{
1861-
/*
1862-
sys.dm_db_persisted_sku_features is database-scoped on all versions.
1863-
Query across all online user databases for TDE usage — the only feature
1864-
still Enterprise-only since 2016 SP1 (Compression, Partitioning,
1865-
ColumnStoreIndex are all available in Standard).
1866-
*/
1867-
using var featCmd = new SqlCommand(@"
1869+
// SQL Server 2019 (major version 15) moved TDE to Standard Edition.
1870+
// On 2019+, dm_db_persisted_sku_features won't report TDE since it's
1871+
// no longer Enterprise-restricted — so we skip the TDE-specific check
1872+
// and give version-appropriate guidance instead.
1873+
if (majorVersion >= 15)
1874+
{
1875+
// 2019+: Most features that were Enterprise-only moved to Standard
1876+
// in 2016 SP1, and TDE moved in 2019. Very few Enterprise-only
1877+
// features remain (e.g., certain HA configurations).
1878+
recommendations.Add(new FinOpsRecommendation
1879+
{
1880+
Category = "Licensing",
1881+
Severity = "High",
1882+
Confidence = "Medium",
1883+
Finding = "Enterprise Edition may not be required",
1884+
Detail = "Starting with SQL Server 2019, most previously Enterprise-only features " +
1885+
"(including TDE, compression, partitioning, and columnstore) are available " +
1886+
"in Standard Edition. Review whether remaining Enterprise-only features " +
1887+
"(such as Always On availability groups with multiple secondaries) are in use " +
1888+
"before considering a downgrade to Standard Edition.",
1889+
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
1890+
});
1891+
}
1892+
else
1893+
{
1894+
/*
1895+
Pre-2019: TDE is the only commonly-used feature still restricted
1896+
to Enterprise Edition since 2016 SP1. Use dm_db_persisted_sku_features
1897+
to detect it — the DMV correctly reports TDE on these versions.
1898+
*/
1899+
using var featCmd = new SqlCommand(@"
18681900
DECLARE
18691901
@sql nvarchar(max) = N'';
18701902
@@ -1883,42 +1915,42 @@ IF @sql <> N''
18831915
SET @sql = LEFT(@sql, LEN(@sql) - 10);
18841916
EXEC sys.sp_executesql @sql;
18851917
END;", connection);
1886-
featCmd.CommandTimeout = 30;
1918+
featCmd.CommandTimeout = 30;
18871919

1888-
var tdeDbNames = new List<string>();
1889-
using var featReader = await featCmd.ExecuteReaderAsync();
1890-
while (await featReader.ReadAsync())
1891-
{
1892-
if (!featReader.IsDBNull(0))
1893-
tdeDbNames.Add(featReader.GetString(0));
1894-
}
1920+
var tdeDbNames = new List<string>();
1921+
using var featReader = await featCmd.ExecuteReaderAsync();
1922+
while (await featReader.ReadAsync())
1923+
{
1924+
if (!featReader.IsDBNull(0))
1925+
tdeDbNames.Add(featReader.GetString(0));
1926+
}
18951927

1896-
if (tdeDbNames.Count == 0)
1897-
{
1898-
recommendations.Add(new FinOpsRecommendation
1928+
if (tdeDbNames.Count == 0)
18991929
{
1900-
Category = "Licensing",
1901-
Severity = "High",
1902-
Confidence = "High",
1903-
Finding = "Enterprise Edition with no Enterprise-only features detected",
1904-
Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
1905-
"still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
1906-
"Review whether Standard Edition would meet workload requirements for potential license savings.",
1907-
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
1908-
});
1909-
}
1910-
else
1911-
{
1912-
recommendations.Add(new FinOpsRecommendation
1930+
recommendations.Add(new FinOpsRecommendation
1931+
{
1932+
Category = "Licensing",
1933+
Severity = "High",
1934+
Confidence = "High",
1935+
Finding = "Enterprise Edition with no Enterprise-only features detected",
1936+
Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
1937+
"still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
1938+
"Review whether Standard Edition would meet workload requirements for potential license savings.",
1939+
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
1940+
});
1941+
}
1942+
else
19131943
{
1914-
Category = "Licensing",
1915-
Severity = "Low",
1916-
Confidence = "High",
1917-
Finding = "TDE in use — Enterprise Edition downgrade blocker",
1918-
Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" +
1919-
(tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") +
1920-
". TDE must be removed before downgrading to Standard Edition."
1921-
});
1944+
recommendations.Add(new FinOpsRecommendation
1945+
{
1946+
Category = "Licensing",
1947+
Severity = "Low",
1948+
Confidence = "High",
1949+
Finding = "TDE in use — Enterprise Edition downgrade blocker",
1950+
Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" +
1951+
(tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") +
1952+
". TDE must be removed before downgrading to Standard Edition."
1953+
});
19221954

19231955
// Check 10: License cost impact estimate (only when features ARE in use)
19241956
using var cpuInfoCmd = new SqlCommand(
@@ -1941,6 +1973,7 @@ IF @sql <> N''
19411973
});
19421974
}
19431975
}
1976+
}
19441977
}
19451978
}
19461979
catch (Exception ex)

Lite/Services/LocalDataService.FinOps.cs

Lines changed: 89 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -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(@"
15941626
DECLARE
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;
16111643
END;", 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

Comments
 (0)