diff --git a/Dashboard/ExcludedDatabasesDialog.xaml b/Dashboard/ExcludedDatabasesDialog.xaml
new file mode 100644
index 0000000..00789a6
--- /dev/null
+++ b/Dashboard/ExcludedDatabasesDialog.xaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Dashboard/ExcludedDatabasesDialog.xaml.cs b/Dashboard/ExcludedDatabasesDialog.xaml.cs
new file mode 100644
index 0000000..6b6bf98
--- /dev/null
+++ b/Dashboard/ExcludedDatabasesDialog.xaml.cs
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows;
+using System.Windows.Media;
+using PerformanceMonitorDashboard.Models;
+using PerformanceMonitorDashboard.Services;
+
+namespace PerformanceMonitorDashboard
+{
+ public partial class ExcludedDatabasesDialog : Window
+ {
+ private readonly ServerManager _serverManager;
+ private readonly ServerConnection _server;
+ private ObservableCollection _items = new();
+
+ public bool ExclusionsModified { get; private set; }
+
+ public ExcludedDatabasesDialog(ServerManager serverManager, ServerConnection server)
+ {
+ InitializeComponent();
+ _serverManager = serverManager;
+ _server = server;
+ HeaderText.Text = $"Excluded Databases — {server.DisplayNameWithIntent}";
+ Loaded += async (_, _) => await LoadAsync();
+ }
+
+ private async System.Threading.Tasks.Task LoadAsync()
+ {
+ StatusText.Text = "Loading databases…";
+ DatabasesItemsControl.ItemsSource = null;
+
+ try
+ {
+ var liveDatabases = await _serverManager.GetUserDatabasesAsync(_server);
+ var existingExclusions = await _serverManager.GetCollectorDatabaseExclusionsAsync(_server);
+
+ var liveSet = new HashSet(liveDatabases, StringComparer.OrdinalIgnoreCase);
+
+ _items = new ObservableCollection();
+
+ /* Live databases: sortable, checkable */
+ foreach (var name in liveDatabases.OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
+ {
+ _items.Add(new DatabaseExclusionItem
+ {
+ Name = name,
+ DisplayName = name,
+ IsExcluded = existingExclusions.Contains(name, StringComparer.OrdinalIgnoreCase),
+ IsEnabled = true,
+ IsStale = false
+ });
+ }
+
+ /* Stale entries: in exclusion list but not present on the server. Show greyed, disabled, pre-checked. */
+ foreach (var name in existingExclusions
+ .Where(n => !liveSet.Contains(n))
+ .OrderBy(n => n, StringComparer.OrdinalIgnoreCase))
+ {
+ _items.Add(new DatabaseExclusionItem
+ {
+ Name = name,
+ DisplayName = $"{name} (missing)",
+ IsExcluded = true,
+ IsEnabled = false,
+ IsStale = true
+ });
+ }
+
+ DatabasesItemsControl.ItemsSource = _items;
+ StatusText.Text = $"{liveDatabases.Count} database(s) on this server, {existingExclusions.Count} currently excluded.";
+ }
+ catch (Exception ex)
+ {
+ StatusText.Text = $"Failed to load: {ex.Message}";
+ MessageBox.Show(this,
+ $"Could not read database list from '{_server.DisplayNameWithIntent}':\n\n{ex.Message}",
+ "Load Failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
+
+ private async void Save_Click(object sender, RoutedEventArgs e)
+ {
+ /* Collect every checked item (live + stale). Stale ones can't be unchecked, so they stay if they were excluded. */
+ var checkedNames = _items
+ .Where(i => i.IsExcluded)
+ .Select(i => i.Name)
+ .ToList();
+
+ Cursor = System.Windows.Input.Cursors.Wait;
+ try
+ {
+ await _serverManager.SaveCollectorDatabaseExclusionsAsync(_server, checkedNames);
+ ExclusionsModified = true;
+ DialogResult = true;
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show(this,
+ $"Failed to save exclusions on '{_server.DisplayNameWithIntent}':\n\n{ex.Message}",
+ "Save Failed",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ finally
+ {
+ Cursor = null;
+ }
+ }
+
+ private void Cancel_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ }
+ }
+
+ public class DatabaseExclusionItem : INotifyPropertyChanged
+ {
+ public string Name { get; set; } = "";
+ public string DisplayName { get; set; } = "";
+ private bool _isExcluded;
+ public bool IsExcluded
+ {
+ get => _isExcluded;
+ set { _isExcluded = value; OnPropertyChanged(nameof(IsExcluded)); }
+ }
+ public bool IsEnabled { get; set; } = true;
+ public bool IsStale { get; set; }
+
+ public Brush ForegroundBrush => IsStale
+ ? (Brush)Application.Current.FindResource("ForegroundMutedBrush")
+ : (Brush)Application.Current.FindResource("ForegroundBrush");
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ private void OnPropertyChanged(string propertyName)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
diff --git a/Dashboard/ManageServersWindow.xaml b/Dashboard/ManageServersWindow.xaml
index c96e986..3389288 100644
--- a/Dashboard/ManageServersWindow.xaml
+++ b/Dashboard/ManageServersWindow.xaml
@@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Manage Servers"
- Height="450" Width="780"
+ Height="450" Width="960"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResizeWithGrip"
Background="{DynamicResource BackgroundBrush}">
@@ -12,6 +12,7 @@
+
@@ -98,6 +99,7 @@
+
diff --git a/Dashboard/ManageServersWindow.xaml.cs b/Dashboard/ManageServersWindow.xaml.cs
index c2ad284..633de30 100644
--- a/Dashboard/ManageServersWindow.xaml.cs
+++ b/Dashboard/ManageServersWindow.xaml.cs
@@ -297,6 +297,25 @@ string Normalize(string v)
}
}
+ private void ExcludedDatabases_Click(object sender, RoutedEventArgs e)
+ {
+ if (ServersDataGrid.SelectedItem is not ServerConnection server)
+ {
+ MessageBox.Show(
+ "Please select a server to configure excluded databases.",
+ "No Server Selected",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+ return;
+ }
+
+ var dialog = new ExcludedDatabasesDialog(_serverManager, server) { Owner = this };
+ if (dialog.ShowDialog() == true && dialog.ExclusionsModified)
+ {
+ ServersModified = true;
+ }
+ }
+
private async void PurgeNow_Click(object sender, RoutedEventArgs e)
{
if (ServersDataGrid.SelectedItem is not ServerConnection server)
diff --git a/Dashboard/Services/ServerManager.cs b/Dashboard/Services/ServerManager.cs
index 29ce934..433a4fd 100644
--- a/Dashboard/Services/ServerManager.cs
+++ b/Dashboard/Services/ServerManager.cs
@@ -284,6 +284,115 @@ FROM config.collection_log AS cl
return result;
}
+ ///
+ /// Returns user database names (excluding system DBs and PerformanceMonitor) on the target server,
+ /// for use in the Excluded Databases dialog.
+ ///
+ public async Task> GetUserDatabasesAsync(ServerConnection server)
+ {
+ var connectionString = server.GetConnectionString(_credentialService);
+ var builder = new SqlConnectionStringBuilder(connectionString)
+ {
+ InitialCatalog = "master",
+ ConnectTimeout = 10
+ };
+
+ using var connection = new SqlConnection(builder.ConnectionString);
+ await connection.OpenAsync();
+
+ using var cmd = new SqlCommand(@"
+SELECT d.name
+FROM sys.databases AS d
+WHERE d.database_id > 4
+AND d.state_desc = N'ONLINE'
+AND d.name <> N'PerformanceMonitor'
+AND d.database_id < 32761 /*exclude contained AG system databases*/
+ORDER BY d.name;", connection);
+ cmd.CommandTimeout = 30;
+
+ var names = new List();
+ using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ names.Add(reader.GetString(0));
+ }
+ return names;
+ }
+
+ ///
+ /// Returns the current per-database exclusion list from config.collector_database_exclusions on the target.
+ ///
+ public async Task> GetCollectorDatabaseExclusionsAsync(ServerConnection server)
+ {
+ var connectionString = server.GetConnectionString(_credentialService);
+ var builder = new SqlConnectionStringBuilder(connectionString)
+ {
+ InitialCatalog = "PerformanceMonitor",
+ ConnectTimeout = 10
+ };
+
+ using var connection = new SqlConnection(builder.ConnectionString);
+ await connection.OpenAsync();
+
+ using var cmd = new SqlCommand(@"
+SELECT e.database_name
+FROM config.collector_database_exclusions AS e
+ORDER BY e.database_name;", connection);
+ cmd.CommandTimeout = 30;
+
+ var names = new List();
+ using var reader = await cmd.ExecuteReaderAsync();
+ while (await reader.ReadAsync())
+ {
+ names.Add(reader.GetString(0));
+ }
+ return names;
+ }
+
+ ///
+ /// Replaces the contents of config.collector_database_exclusions with the supplied list, transactionally.
+ ///
+ public async Task SaveCollectorDatabaseExclusionsAsync(ServerConnection server, IEnumerable databaseNames)
+ {
+ var connectionString = server.GetConnectionString(_credentialService);
+ var builder = new SqlConnectionStringBuilder(connectionString)
+ {
+ InitialCatalog = "PerformanceMonitor",
+ ConnectTimeout = 10
+ };
+
+ using var connection = new SqlConnection(builder.ConnectionString);
+ await connection.OpenAsync();
+
+ using var transaction = connection.BeginTransaction();
+ try
+ {
+ using (var deleteCmd = new SqlCommand("DELETE FROM config.collector_database_exclusions;", connection, transaction))
+ {
+ deleteCmd.CommandTimeout = 30;
+ await deleteCmd.ExecuteNonQueryAsync();
+ }
+
+ foreach (var name in databaseNames.Distinct(StringComparer.OrdinalIgnoreCase))
+ {
+ using var insertCmd = new SqlCommand(
+ "INSERT INTO config.collector_database_exclusions (database_name) VALUES (@name);",
+ connection, transaction);
+ insertCmd.CommandTimeout = 30;
+ insertCmd.Parameters.Add(new SqlParameter("@name", System.Data.SqlDbType.NVarChar, 128) { Value = name });
+ await insertCmd.ExecuteNonQueryAsync();
+ }
+
+ transaction.Commit();
+ Logger.Info($"Saved collector database exclusions on '{server.DisplayName}'");
+ }
+ catch
+ {
+ transaction.Rollback();
+ throw;
+ }
+ }
+
public void UpdateLastConnected(string id)
{
lock (_serversLock)
diff --git a/install/01_install_database.sql b/install/01_install_database.sql
index 0559e3a..5b4b293 100644
--- a/install/01_install_database.sql
+++ b/install/01_install_database.sql
@@ -734,6 +734,32 @@ BEGIN
END;
GO
+/*
+Create per-database exclusions table
+User-configurable list of databases to skip in per-database collectors
+(query_store, file_io_stats, database_size_stats, etc.). System databases
+are always skipped by the collectors themselves and are not represented here.
+*/
+IF OBJECT_ID(N'config.collector_database_exclusions', N'U') IS NULL
+BEGIN
+ CREATE TABLE
+ config.collector_database_exclusions
+ (
+ database_name sysname NOT NULL,
+ excluded_at datetime2(7) NOT NULL DEFAULT SYSDATETIME(),
+ excluded_by sysname NULL DEFAULT SUSER_SNAME(),
+ CONSTRAINT
+ PK_collector_database_exclusions
+ PRIMARY KEY CLUSTERED
+ (database_name)
+ WITH
+ (DATA_COMPRESSION = PAGE)
+ );
+
+ PRINT 'Created config.collector_database_exclusions table';
+END;
+GO
+
/*
Create installation history table
*/
diff --git a/install/03_create_config_tables.sql b/install/03_create_config_tables.sql
index f81960e..b0f65dd 100644
--- a/install/03_create_config_tables.sql
+++ b/install/03_create_config_tables.sql
@@ -629,6 +629,45 @@ BEGIN
);
END;
+ /*
+ Create config.collector_database_exclusions
+ User-configurable list of databases to skip in per-database collectors.
+ System databases (master/tempdb/model/msdb) are filtered by the collectors
+ themselves and aren't represented here.
+ */
+ IF OBJECT_ID(N'config.collector_database_exclusions', N'U') IS NULL
+ BEGIN
+ IF @debug = 1
+ BEGIN
+ RAISERROR(N'Creating config.collector_database_exclusions table', 0, 1) WITH NOWAIT;
+ END;
+
+ CREATE TABLE
+ config.collector_database_exclusions
+ (
+ database_name sysname NOT NULL,
+ excluded_at datetime2(7) NOT NULL DEFAULT SYSDATETIME(),
+ excluded_by sysname NULL DEFAULT SUSER_SNAME(),
+ CONSTRAINT PK_collector_database_exclusions PRIMARY KEY CLUSTERED (database_name) WITH (DATA_COMPRESSION = PAGE)
+ );
+
+ SET @tables_created = @tables_created + 1;
+
+ INSERT INTO
+ config.collection_log
+ (
+ collector_name,
+ collection_status,
+ error_message
+ )
+ VALUES
+ (
+ N'ensure_config_tables',
+ N'TABLE_CREATED',
+ N'Created config.collector_database_exclusions table'
+ );
+ END;
+
/*
Create config.installation_history
*/
diff --git a/install/08_collect_query_stats.sql b/install/08_collect_query_stats.sql
index 2092d55..f01d6cb 100644
--- a/install/08_collect_query_stats.sql
+++ b/install/08_collect_query_stats.sql
@@ -406,6 +406,13 @@ BEGIN
DB_ID(N'PerformanceMonitor')
)
AND pa.dbid < 32761 /*exclude contained AG system databases*/
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
OPTION(RECOMPILE);
/*
diff --git a/install/09_collect_query_store.sql b/install/09_collect_query_store.sql
index d13df2a..1b471d9 100644
--- a/install/09_collect_query_store.sql
+++ b/install/09_collect_query_store.sql
@@ -351,6 +351,13 @@ BEGIN
drs.database_id IS NULL /*not in any AG*/
OR drs.is_primary_replica = 1 /*primary replica*/
)
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
OPTION(RECOMPILE);
OPEN @db_check_cursor;
diff --git a/install/10_collect_procedure_stats.sql b/install/10_collect_procedure_stats.sql
index abe44e9..5d99ad3 100644
--- a/install/10_collect_procedure_stats.sql
+++ b/install/10_collect_procedure_stats.sql
@@ -339,6 +339,13 @@ BEGIN
DB_ID(N'PerformanceMonitor')
)
AND pa.dbid < 32761 /*exclude contained AG system databases*/
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
UNION ALL
@@ -508,6 +515,13 @@ BEGIN
DB_ID(N'PerformanceMonitor')
)
AND pa.dbid < 32761 /*exclude contained AG system databases*/
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
UNION ALL
SELECT
diff --git a/install/20_collect_file_io_stats.sql b/install/20_collect_file_io_stats.sql
index 44b7102..7d0545f 100644
--- a/install/20_collect_file_io_stats.sql
+++ b/install/20_collect_file_io_stats.sql
@@ -176,6 +176,13 @@ BEGIN
DB_ID(N'msdb') /*4*/
)
AND vfs.database_id < 32761 /*exclude resource database and contained AG system databases*/
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
OPTION(RECOMPILE);
SET @rows_collected = ROWCOUNT_BIG();
diff --git a/install/37_collect_waiting_tasks.sql b/install/37_collect_waiting_tasks.sql
index e7564d8..1db92c4 100644
--- a/install/37_collect_waiting_tasks.sql
+++ b/install/37_collect_waiting_tasks.sql
@@ -183,6 +183,13 @@ BEGIN
WHERE iwt.wait_type = wt.wait_type
AND iwt.is_enabled = 1
)
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
OPTION(RECOMPILE);
SET @rows_collected = ROWCOUNT_BIG();
diff --git a/install/39_collect_database_configuration.sql b/install/39_collect_database_configuration.sql
index f6a4202..8927287 100644
--- a/install/39_collect_database_configuration.sql
+++ b/install/39_collect_database_configuration.sql
@@ -144,6 +144,13 @@ BEGIN
AND d.name != DB_NAME()
AND d.state_desc = N'ONLINE'
AND d.database_id < 32761 /*exclude contained AG system databases*/
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
OPTION (RECOMPILE);
IF @debug = 1
@@ -173,6 +180,13 @@ BEGIN
drs.database_id IS NULL /*not in any AG*/
OR drs.is_primary_replica = 1 /*primary replica*/
)
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
ORDER BY
d.name
OPTION (RECOMPILE);
diff --git a/install/52_collect_database_size_stats.sql b/install/52_collect_database_size_stats.sql
index 11e71e0..57c6f14 100644
--- a/install/52_collect_database_size_stats.sql
+++ b/install/52_collect_database_size_stats.sql
@@ -195,6 +195,13 @@ BEGIN
WHERE d.state = 0 /*ONLINE only — skip RESTORING databases (mirroring/AG secondary)*/
AND d.database_id > 0
AND HAS_DBACCESS(d.name) = 1
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
ORDER BY
d.database_id;
diff --git a/install/53_collect_server_properties.sql b/install/53_collect_server_properties.sql
index fb1b1c2..c77ceaf 100644
--- a/install/53_collect_server_properties.sql
+++ b/install/53_collect_server_properties.sql
@@ -136,6 +136,13 @@ BEGIN
WHERE d.state = 0 /*ONLINE only — skip RESTORING databases (mirroring/AG secondary)*/
AND d.database_id > 4 /*Skip system databases*/
AND HAS_DBACCESS(d.name) = 1
+ AND NOT EXISTS
+ (
+ SELECT
+ 1/0
+ FROM config.collector_database_exclusions AS e
+ WHERE e.database_name = d.name
+ )
ORDER BY
d.database_id;