Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 74 additions & 41 deletions Dashboard/Services/DatabaseService.FinOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1852,19 +1852,51 @@ public async Task<List<FinOpsRecommendation>> GetFinOpsRecommendationsAsync(deci
try
{
using var editionCmd = new SqlCommand(
"SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128))", connection);
"SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)), " +
"CAST(SERVERPROPERTY('ProductMajorVersion') AS INT)", connection);
editionCmd.CommandTimeout = 30;
var edition = (string?)await editionCmd.ExecuteScalarAsync() ?? "";
using var editionReader = await editionCmd.ExecuteReaderAsync();
string edition = "";
int majorVersion = 0;
if (await editionReader.ReadAsync())
{
edition = editionReader.IsDBNull(0) ? "" : editionReader.GetString(0);
majorVersion = editionReader.IsDBNull(1) ? 0 : editionReader.GetInt32(1);
}

if (edition.Contains("Enterprise", StringComparison.OrdinalIgnoreCase))
{
/*
sys.dm_db_persisted_sku_features is database-scoped on all versions.
Query across all online user databases for TDE usage — the only feature
still Enterprise-only since 2016 SP1 (Compression, Partitioning,
ColumnStoreIndex are all available in Standard).
*/
using var featCmd = new SqlCommand(@"
// SQL Server 2019 (major version 15) moved TDE to Standard Edition.
// On 2019+, dm_db_persisted_sku_features won't report TDE since it's
// no longer Enterprise-restricted — so we skip the TDE-specific check
// and give version-appropriate guidance instead.
if (majorVersion >= 15)
{
// 2019+: Most features that were Enterprise-only moved to Standard
// in 2016 SP1, and TDE moved in 2019. Very few Enterprise-only
// features remain (e.g., certain HA configurations).
recommendations.Add(new FinOpsRecommendation
{
Category = "Licensing",
Severity = "High",
Confidence = "Medium",
Finding = "Enterprise Edition may not be required",
Detail = "Starting with SQL Server 2019, most previously Enterprise-only features " +
"(including TDE, compression, partitioning, and columnstore) are available " +
"in Standard Edition. Review whether remaining Enterprise-only features " +
"(such as Always On availability groups with multiple secondaries) are in use " +
"before considering a downgrade to Standard Edition.",
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
});
}
else
{
/*
Pre-2019: TDE is the only commonly-used feature still restricted
to Enterprise Edition since 2016 SP1. Use dm_db_persisted_sku_features
to detect it — the DMV correctly reports TDE on these versions.
*/
using var featCmd = new SqlCommand(@"
DECLARE
@sql nvarchar(max) = N'';

Expand All @@ -1883,42 +1915,42 @@ IF @sql <> N''
SET @sql = LEFT(@sql, LEN(@sql) - 10);
EXEC sys.sp_executesql @sql;
END;", connection);
featCmd.CommandTimeout = 30;
featCmd.CommandTimeout = 30;

var tdeDbNames = new List<string>();
using var featReader = await featCmd.ExecuteReaderAsync();
while (await featReader.ReadAsync())
{
if (!featReader.IsDBNull(0))
tdeDbNames.Add(featReader.GetString(0));
}
var tdeDbNames = new List<string>();
using var featReader = await featCmd.ExecuteReaderAsync();
while (await featReader.ReadAsync())
{
if (!featReader.IsDBNull(0))
tdeDbNames.Add(featReader.GetString(0));
}

if (tdeDbNames.Count == 0)
{
recommendations.Add(new FinOpsRecommendation
if (tdeDbNames.Count == 0)
{
Category = "Licensing",
Severity = "High",
Confidence = "High",
Finding = "Enterprise Edition with no Enterprise-only features detected",
Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
"still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
"Review whether Standard Edition would meet workload requirements for potential license savings.",
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
});
}
else
{
recommendations.Add(new FinOpsRecommendation
recommendations.Add(new FinOpsRecommendation
{
Category = "Licensing",
Severity = "High",
Confidence = "High",
Finding = "Enterprise Edition with no Enterprise-only features detected",
Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
"still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
"Review whether Standard Edition would meet workload requirements for potential license savings.",
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
});
}
else
{
Category = "Licensing",
Severity = "Low",
Confidence = "High",
Finding = "TDE in use — Enterprise Edition downgrade blocker",
Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" +
(tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") +
". TDE must be removed before downgrading to Standard Edition."
});
recommendations.Add(new FinOpsRecommendation
{
Category = "Licensing",
Severity = "Low",
Confidence = "High",
Finding = "TDE in use — Enterprise Edition downgrade blocker",
Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" +
(tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") +
". TDE must be removed before downgrading to Standard Edition."
});

// Check 10: License cost impact estimate (only when features ARE in use)
using var cpuInfoCmd = new SqlCommand(
Expand All @@ -1941,6 +1973,7 @@ IF @sql <> N''
});
}
}
}
}
}
catch (Exception ex)
Expand Down
145 changes: 89 additions & 56 deletions Lite/Services/LocalDataService.FinOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1578,19 +1578,51 @@ public async Task<List<RecommendationRow>> GetRecommendationsAsync(int serverId,
await sqlConn.OpenAsync();

using var editionCmd = new SqlCommand(
"SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128))", sqlConn);
"SELECT CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)), " +
"CAST(SERVERPROPERTY('ProductMajorVersion') AS INT)", sqlConn);
editionCmd.CommandTimeout = 30;
var edition = (string?)await editionCmd.ExecuteScalarAsync() ?? "";
using var editionReader = await editionCmd.ExecuteReaderAsync();
string edition = "";
int majorVersion = 0;
if (await editionReader.ReadAsync())
{
edition = editionReader.IsDBNull(0) ? "" : editionReader.GetString(0);
majorVersion = editionReader.IsDBNull(1) ? 0 : editionReader.GetInt32(1);
}

if (edition.Contains("Enterprise", StringComparison.OrdinalIgnoreCase))
{
/*
sys.dm_db_persisted_sku_features is database-scoped on all versions.
Query across all online user databases for TDE usage — the only feature
still Enterprise-only since 2016 SP1 (Compression, Partitioning,
ColumnStoreIndex are all available in Standard).
*/
using var featCmd = new SqlCommand(@"
// SQL Server 2019 (major version 15) moved TDE to Standard Edition.
// On 2019+, dm_db_persisted_sku_features won't report TDE since it's
// no longer Enterprise-restricted — so we skip the TDE-specific check
// and give version-appropriate guidance instead.
if (majorVersion >= 15)
{
// 2019+: Most features that were Enterprise-only moved to Standard
// in 2016 SP1, and TDE moved in 2019. Very few Enterprise-only
// features remain (e.g., certain HA configurations).
recommendations.Add(new RecommendationRow
{
Category = "Licensing",
Severity = "High",
Confidence = "Medium",
Finding = "Enterprise Edition may not be required",
Detail = "Starting with SQL Server 2019, most previously Enterprise-only features " +
"(including TDE, compression, partitioning, and columnstore) are available " +
"in Standard Edition. Review whether remaining Enterprise-only features " +
"(such as Always On availability groups with multiple secondaries) are in use " +
"before considering a downgrade to Standard Edition.",
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
});
}
else
{
/*
Pre-2019: TDE is the only commonly-used feature still restricted
to Enterprise Edition since 2016 SP1. Use dm_db_persisted_sku_features
to detect it — the DMV correctly reports TDE on these versions.
*/
using var featCmd = new SqlCommand(@"
DECLARE
@sql nvarchar(max) = N'';

Expand All @@ -1609,62 +1641,63 @@ IF @sql <> N''
SET @sql = LEFT(@sql, LEN(@sql) - 10);
EXEC sys.sp_executesql @sql;
END;", sqlConn);
featCmd.CommandTimeout = 30;
featCmd.CommandTimeout = 30;

var tdeDbNames = new List<string>();
using var featReader = await featCmd.ExecuteReaderAsync();
while (await featReader.ReadAsync())
{
if (!featReader.IsDBNull(0))
tdeDbNames.Add(featReader.GetString(0));
}

if (tdeDbNames.Count == 0)
{
recommendations.Add(new RecommendationRow
{
Category = "Licensing",
Severity = "High",
Confidence = "High",
Finding = "Enterprise Edition with no Enterprise-only features detected",
Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
"still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
"Review whether Standard Edition would meet workload requirements for potential license savings.",
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
});
}
else
{
recommendations.Add(new RecommendationRow
var tdeDbNames = new List<string>();
using var featReader = await featCmd.ExecuteReaderAsync();
while (await featReader.ReadAsync())
{
Category = "Licensing",
Severity = "Low",
Confidence = "High",
Finding = "TDE in use — Enterprise Edition downgrade blocker",
Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" +
(tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") +
". TDE must be removed before downgrading to Standard Edition."
});
if (!featReader.IsDBNull(0))
tdeDbNames.Add(featReader.GetString(0));
}

// Check 10: License cost impact estimate (only when features ARE in use)
using var cpuInfoCmd = new SqlCommand(
"SELECT cpu_count FROM sys.dm_os_sys_info", sqlConn);
cpuInfoCmd.CommandTimeout = 30;
var cpuCountObj = await cpuInfoCmd.ExecuteScalarAsync();
var coreLicenseCount = cpuCountObj != null ? Convert.ToInt32(cpuCountObj) : 0;
if (coreLicenseCount > 0)
if (tdeDbNames.Count == 0)
{
recommendations.Add(new RecommendationRow
{
Category = "Licensing",
Severity = "High",
Confidence = "High",
Finding = "Enterprise Edition with no Enterprise-only features detected",
Detail = "No databases use Transparent Data Encryption (TDE), the only feature " +
"still restricted to Enterprise Edition since SQL Server 2016 SP1. " +
"Review whether Standard Edition would meet workload requirements for potential license savings.",
EstMonthlySavings = monthlyCost > 0 ? monthlyCost * 0.40m : null
});
}
else
{
var monthlySavings = coreLicenseCount * 5000m / 12m;
recommendations.Add(new RecommendationRow
{
Category = "Licensing",
Severity = "Low",
Confidence = "Low",
Finding = $"Enterprise to Standard would save ~${monthlySavings:N0}/mo at list pricing ({coreLicenseCount} cores)",
Detail = "Based on list pricing differential of ~$5,000/core/year between Enterprise and Standard. " +
"Actual savings depend on your licensing agreement. See Enterprise feature audit for downgrade blockers.",
EstMonthlySavings = monthlySavings
Confidence = "High",
Finding = "TDE in use — Enterprise Edition downgrade blocker",
Detail = $"The following databases use Transparent Data Encryption: {string.Join(", ", tdeDbNames.Take(20))}" +
(tdeDbNames.Count > 20 ? $" and {tdeDbNames.Count - 20} more" : "") +
". TDE must be removed before downgrading to Standard Edition."
});

// Check 10: License cost impact estimate (only when features ARE in use)
using var cpuInfoCmd = new SqlCommand(
"SELECT cpu_count FROM sys.dm_os_sys_info", sqlConn);
cpuInfoCmd.CommandTimeout = 30;
var cpuCountObj = await cpuInfoCmd.ExecuteScalarAsync();
var coreLicenseCount = cpuCountObj != null ? Convert.ToInt32(cpuCountObj) : 0;
if (coreLicenseCount > 0)
{
var monthlySavings = coreLicenseCount * 5000m / 12m;
recommendations.Add(new RecommendationRow
{
Category = "Licensing",
Severity = "Low",
Confidence = "Low",
Finding = $"Enterprise to Standard would save ~${monthlySavings:N0}/mo at list pricing ({coreLicenseCount} cores)",
Detail = "Based on list pricing differential of ~$5,000/core/year between Enterprise and Standard. " +
"Actual savings depend on your licensing agreement. See Enterprise feature audit for downgrade blockers.",
EstMonthlySavings = monthlySavings
});
}
}
}
}
Expand Down
Loading