From eb3c06d04bff886d1a1ae51e740c398f992ea57f Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:03:05 -0500 Subject: [PATCH] Parquet archive visibility and scheduled database compaction (#160, #161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display queries now read from views that UNION hot DuckDB tables with archived parquet files, so users can see the full 90-day retention window instead of only the 7-day hot data window. Views are created at startup and refreshed after each archive cycle. Adds daily database compaction to prevent file bloat from DuckDB's append-only storage. Compaction exports all tables to a fresh database via ATTACH/CREATE TABLE AS, swaps files, and recreates indexes/views. Includes a 1GB size watchdog that logs warnings between compaction cycles. Tested: 24/24 — archive round-trip (export to parquet, delete hot rows, verify view still returns all rows) and compaction (19MB → 5MB, all 1055 rows preserved across 25 tables). Co-Authored-By: Claude Opus 4.6 --- Lite/Database/DuckDbInitializer.cs | 189 ++++++++++++++++++ Lite/MainWindow.xaml.cs | 2 +- Lite/Services/ArchiveService.cs | 3 + Lite/Services/CollectionBackgroundService.cs | 46 ++++- Lite/Services/LocalDataService.Blocking.cs | 10 +- .../LocalDataService.CollectionHealth.cs | 4 +- Lite/Services/LocalDataService.Cpu.cs | 2 +- Lite/Services/LocalDataService.FileIo.cs | 8 +- Lite/Services/LocalDataService.Memory.cs | 8 +- Lite/Services/LocalDataService.Overview.cs | 10 +- Lite/Services/LocalDataService.Perfmon.cs | 8 +- Lite/Services/LocalDataService.QueryStats.cs | 14 +- Lite/Services/LocalDataService.QueryStore.cs | 8 +- Lite/Services/LocalDataService.TempDb.cs | 4 +- Lite/Services/LocalDataService.WaitStats.cs | 12 +- 15 files changed, 282 insertions(+), 46 deletions(-) diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs index 77a441fd..dfaa4f75 100644 --- a/Lite/Database/DuckDbInitializer.cs +++ b/Lite/Database/DuckDbInitializer.cs @@ -21,12 +21,24 @@ public class DuckDbInitializer /// internal const int CurrentSchemaVersion = 10; + private readonly string _archivePath; + public DuckDbInitializer(string databasePath, ILogger? logger = null) { _databasePath = databasePath; _logger = logger; + _archivePath = Path.Combine(Path.GetDirectoryName(databasePath) ?? ".", "archive"); } + /* Tables that have parquet archives — views are created to UNION hot data with archived parquet files */ + private static readonly string[] ArchivableTables = + [ + "wait_stats", "query_stats", "procedure_stats", "query_store_stats", + "query_snapshots", "cpu_utilization_stats", "file_io_stats", "memory_stats", + "memory_clerks", "tempdb_stats", "perfmon_stats", "deadlocks", + "blocked_process_reports", "collection_log" + ]; + /// /// Gets the connection string for the DuckDB database. /// Disables automatic WAL checkpoints to prevent 2-3s stop-the-world stalls @@ -99,6 +111,8 @@ await ExecuteNonQueryAsync(connection, _logger?.LogInformation("Database initialization complete. Schema version: {Version}", CurrentSchemaVersion); } + + await CreateArchiveViewsAsync(); } /// @@ -410,6 +424,58 @@ public DuckDBConnection CreateConnection() return new DuckDBConnection(ConnectionString); } + /// + /// Creates or refreshes views that UNION hot DuckDB tables with archived parquet files. + /// Call at startup and after each archive cycle so newly archived data is queryable. + /// + public async Task CreateArchiveViewsAsync() + { + using var connection = CreateConnection(); + await connection.OpenAsync(); + + foreach (var table in ArchivableTables) + { + try + { + var parquetGlob = Path.Combine(_archivePath, $"*_{table}.parquet"); + var hasParquetFiles = Directory.Exists(_archivePath) + && Directory.GetFiles(_archivePath, $"*_{table}.parquet").Length > 0; + + string viewSql; + if (hasParquetFiles) + { + var globPath = parquetGlob.Replace("\\", "/"); + viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table} UNION ALL SELECT * FROM read_parquet('{globPath}', union_by_name=true)"; + } + else + { + viewSql = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table}"; + } + + using var cmd = connection.CreateCommand(); + cmd.CommandText = viewSql; + await cmd.ExecuteNonQueryAsync(); + } + catch (Exception ex) + { + /* Schema mismatch between hot table and old parquet — fall back to table-only view */ + _logger?.LogWarning(ex, "Failed to create archive view for {Table}, using table-only view", table); + try + { + using var fallbackCmd = connection.CreateCommand(); + fallbackCmd.CommandText = $"CREATE OR REPLACE VIEW v_{table} AS SELECT * FROM {table}"; + await fallbackCmd.ExecuteNonQueryAsync(); + } + catch (Exception fallbackEx) + { + _logger?.LogError(fallbackEx, "Failed to create fallback view for {Table}", table); + } + } + } + + _logger?.LogDebug("Archive views created/refreshed for {Count} tables", ArchivableTables.Length); + } + /// /// Runs a manual WAL checkpoint. Call this between collection cycles /// to flush the WAL during idle time instead of during collector writes. @@ -470,4 +536,127 @@ public double GetDatabaseSizeMb() return fileInfo.Length / (1024.0 * 1024.0); } + /// + /// Compacts the database by exporting all tables to a fresh file and swapping. + /// DuckDB VACUUM does not reclaim space from append-fragmented files — only + /// export/reimport eliminates bloat. Typically takes 2-5 seconds for a 300MB database. + /// + /// True if compaction was performed, false if skipped or failed. + public async Task CompactAsync() + { + if (!DatabaseExists()) + { + return false; + } + + var sizeBefore = GetDatabaseSizeMb(); + var tempPath = _databasePath + ".compact"; + var backupPath = _databasePath + ".precompact"; + + _logger?.LogInformation("Starting database compaction ({SizeMb:F0} MB)", sizeBefore); + + try + { + /* Export all data to a fresh database via ATTACH + CREATE TABLE AS */ + if (File.Exists(tempPath)) File.Delete(tempPath); + + using (var connection = CreateConnection()) + { + await connection.OpenAsync(); + + /* Checkpoint first to flush WAL */ + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "CHECKPOINT"; + await cmd.ExecuteNonQueryAsync(); + } + + /* Attach the new database and copy all tables */ + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = $"ATTACH '{tempPath.Replace("\\", "/")}' AS compact_db"; + await cmd.ExecuteNonQueryAsync(); + } + + /* Get all table names (exclude views) */ + var tableNames = new List(); + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main' AND table_type = 'BASE TABLE'"; + using var reader = await cmd.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + tableNames.Add(reader.GetString(0)); + } + } + + foreach (var table in tableNames) + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = $"CREATE TABLE compact_db.{table} AS SELECT * FROM main.{table}"; + await cmd.ExecuteNonQueryAsync(); + } + + using (var cmd = connection.CreateCommand()) + { + cmd.CommandText = "DETACH compact_db"; + await cmd.ExecuteNonQueryAsync(); + } + } + + /* Swap files: old → backup, compact → primary */ + if (File.Exists(backupPath)) File.Delete(backupPath); + File.Move(_databasePath, backupPath); + + var walPath = _databasePath + ".wal"; + if (File.Exists(walPath)) File.Delete(walPath); + + File.Move(tempPath, _databasePath); + + /* Recreate indexes and views on the fresh database */ + using (var connection = CreateConnection()) + { + await connection.OpenAsync(); + + foreach (var indexStatement in Schema.GetAllIndexStatements()) + { + try + { + using var cmd = connection.CreateCommand(); + cmd.CommandText = indexStatement; + await cmd.ExecuteNonQueryAsync(); + } + catch { /* Index may already exist from CREATE TABLE AS */ } + } + } + + await CreateArchiveViewsAsync(); + + /* Clean up backup */ + File.Delete(backupPath); + + var sizeAfter = GetDatabaseSizeMb(); + _logger?.LogInformation("Compaction complete: {Before:F0} MB -> {After:F0} MB ({Saved:F0} MB reclaimed)", + sizeBefore, sizeAfter, sizeBefore - sizeAfter); + + return true; + } + catch (Exception ex) + { + _logger?.LogError(ex, "Database compaction failed"); + + /* Restore from backup if the primary file was moved */ + if (!File.Exists(_databasePath) && File.Exists(backupPath)) + { + File.Move(backupPath, _databasePath); + _logger?.LogInformation("Restored database from pre-compaction backup"); + } + + /* Clean up temp file */ + if (File.Exists(tempPath)) File.Delete(tempPath); + + return false; + } + } + } diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs index a29d0600..e1aed234 100644 --- a/Lite/MainWindow.xaml.cs +++ b/Lite/MainWindow.xaml.cs @@ -105,7 +105,7 @@ private async void MainWindow_Loaded(object sender, RoutedEventArgs e) var archiveService = new ArchiveService(_databaseInitializer, App.ArchiveDirectory); var retentionService = new RetentionService(App.ArchiveDirectory); - _backgroundService = new CollectionBackgroundService(_collectorService, archiveService, retentionService, _serverManager); + _backgroundService = new CollectionBackgroundService(_collectorService, _databaseInitializer, archiveService, retentionService, _serverManager); // Start background collection _backgroundCts = new CancellationTokenSource(); diff --git a/Lite/Services/ArchiveService.cs b/Lite/Services/ArchiveService.cs index bfcf995b..5b71fac1 100644 --- a/Lite/Services/ArchiveService.cs +++ b/Lite/Services/ArchiveService.cs @@ -128,6 +128,9 @@ UNION ALL _logger?.LogError(ex, "Failed to archive table {Table}", table); } } + + /* Refresh archive views so newly archived parquet files are queryable */ + await _duckDb.CreateArchiveViewsAsync(); } finally { diff --git a/Lite/Services/CollectionBackgroundService.cs b/Lite/Services/CollectionBackgroundService.cs index d52e1429..cbc9ec1e 100644 --- a/Lite/Services/CollectionBackgroundService.cs +++ b/Lite/Services/CollectionBackgroundService.cs @@ -22,6 +22,7 @@ namespace PerformanceMonitorLite.Services; public class CollectionBackgroundService : BackgroundService { private readonly RemoteCollectorService _collectorService; + private readonly DuckDbInitializer? _duckDb; private readonly ServerManager? _serverManager; private readonly ArchiveService? _archiveService; private readonly RetentionService? _retentionService; @@ -30,10 +31,15 @@ public class CollectionBackgroundService : BackgroundService private static readonly TimeSpan CollectionInterval = TimeSpan.FromMinutes(1); private DateTime _lastArchiveTime = DateTime.MinValue; private DateTime _lastRetentionTime = DateTime.MinValue; + private DateTime _lastCompactionTime = DateTime.MinValue; - /* Archive every hour, retention cleanup once per day */ + /* Archive every hour, retention + compaction once per day */ private static readonly TimeSpan ArchiveInterval = TimeSpan.FromHours(1); private static readonly TimeSpan RetentionInterval = TimeSpan.FromHours(24); + private static readonly TimeSpan CompactionInterval = TimeSpan.FromHours(24); + + /* Warn if database exceeds this size between compaction cycles */ + private const double SizeWarningThresholdMb = 1024; public bool IsPaused { get; set; } public DateTime? LastCollectionTime { get; private set; } @@ -41,12 +47,14 @@ public class CollectionBackgroundService : BackgroundService public CollectionBackgroundService( RemoteCollectorService collectorService, + DuckDbInitializer? duckDb = null, ArchiveService? archiveService = null, RetentionService? retentionService = null, ServerManager? serverManager = null, ILogger? logger = null) { _collectorService = collectorService; + _duckDb = duckDb; _serverManager = serverManager; _archiveService = archiveService; _retentionService = retentionService; @@ -108,6 +116,9 @@ stall collectors mid-write with 2-3s stop-the-world pauses */ /* Periodic retention cleanup */ RunRetentionIfDue(); + + /* Periodic database compaction to prevent bloat */ + await RunCompactionIfDueAsync(); } try @@ -158,4 +169,37 @@ private void RunRetentionIfDue() _logger?.LogError(ex, "Retention cleanup failed"); } } + + private async Task RunCompactionIfDueAsync() + { + if (_duckDb == null || DateTime.UtcNow - _lastCompactionTime < CompactionInterval) + { + /* Size watchdog: warn if database is large even between compaction cycles */ + if (_duckDb != null) + { + var sizeMb = _duckDb.GetDatabaseSizeMb(); + if (sizeMb > SizeWarningThresholdMb) + { + _logger?.LogWarning("Database size is {SizeMb:F0} MB (threshold: {Threshold} MB) — compaction will run at next scheduled interval", + sizeMb, SizeWarningThresholdMb); + } + } + return; + } + + try + { + IsPaused = true; + await _duckDb.CompactAsync(); + _lastCompactionTime = DateTime.UtcNow; + } + catch (Exception ex) + { + _logger?.LogError(ex, "Database compaction failed"); + } + finally + { + IsPaused = false; + } + } } diff --git a/Lite/Services/LocalDataService.Blocking.cs b/Lite/Services/LocalDataService.Blocking.cs index 11616d79..e82f781d 100644 --- a/Lite/Services/LocalDataService.Blocking.cs +++ b/Lite/Services/LocalDataService.Blocking.cs @@ -78,7 +78,7 @@ public async Task> GetRecentDeadlocksAsync(int serverId, int h victim_process_id, victim_sql_text, deadlock_graph_xml -FROM deadlocks +FROM v_deadlocks WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -139,7 +139,7 @@ public async Task> GetLatestQuerySnapshotsAsync(int serve query_plan, live_query_plan, collection_time -FROM query_snapshots +FROM v_query_snapshots WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -230,7 +230,7 @@ public async Task> GetRecentBlockedProcessReportsA blocking_last_batch_completed, blocked_priority, blocking_priority -FROM blocked_process_reports +FROM v_blocked_process_reports WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -310,7 +310,7 @@ Group by event_time (when blocking actually occurred) rather than collection_tim SELECT DATE_TRUNC('minute', event_time) AS bucket, COUNT(*) AS incident_count - FROM blocked_process_reports + FROM v_blocked_process_reports WHERE server_id = $1 AND event_time >= $2 AND event_time <= $3 @@ -353,7 +353,7 @@ public async Task> GetDeadlockTrendAsync(int serverId, int hour SELECT DATE_TRUNC('hour', deadlock_time) AS bucket, COUNT(*) AS deadlock_count - FROM deadlocks + FROM v_deadlocks WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 diff --git a/Lite/Services/LocalDataService.CollectionHealth.cs b/Lite/Services/LocalDataService.CollectionHealth.cs index 226a3177..b93bfb76 100644 --- a/Lite/Services/LocalDataService.CollectionHealth.cs +++ b/Lite/Services/LocalDataService.CollectionHealth.cs @@ -34,7 +34,7 @@ public async Task> GetCollectionHealthAsync(int serverI MAX(CASE WHEN status IN ('ERROR', 'PERMISSIONS') THEN error_message END) AS last_error, MAX(CASE WHEN status IN ('ERROR', 'PERMISSIONS') THEN collection_time END) AS last_error_time, SUM(CASE WHEN status = 'PERMISSIONS' THEN 1 ELSE 0 END) AS permission_denied_count -FROM collection_log +FROM v_collection_log WHERE server_id = $1 AND collection_time >= $2 GROUP BY collector_name @@ -83,7 +83,7 @@ public async Task> GetRecentCollectionLogAsync(int server status, error_message, server_name -FROM collection_log +FROM v_collection_log WHERE server_id = $1 AND collection_time >= $2 ORDER BY collection_time DESC diff --git a/Lite/Services/LocalDataService.Cpu.cs b/Lite/Services/LocalDataService.Cpu.cs index e9d6aa61..50e7be8a 100644 --- a/Lite/Services/LocalDataService.Cpu.cs +++ b/Lite/Services/LocalDataService.Cpu.cs @@ -32,7 +32,7 @@ public async Task> GetCpuUtilizationAsync(int serverId, sample_time, sqlserver_cpu_utilization, other_process_cpu_utilization -FROM cpu_utilization_stats +FROM v_cpu_utilization_stats WHERE server_id = $1 AND sample_time >= $2 AND sample_time <= $3 diff --git a/Lite/Services/LocalDataService.FileIo.cs b/Lite/Services/LocalDataService.FileIo.cs index 47185c91..3b1dfbe0 100644 --- a/Lite/Services/LocalDataService.FileIo.cs +++ b/Lite/Services/LocalDataService.FileIo.cs @@ -35,9 +35,9 @@ public async Task> GetLatestFileIoStatsAsync(int serverId) delta_write_bytes, delta_stall_read_ms, delta_stall_write_ms -FROM file_io_stats +FROM v_file_io_stats WHERE server_id = $1 -AND collection_time = (SELECT MAX(collection_time) FROM file_io_stats WHERE server_id = $1) +AND collection_time = (SELECT MAX(collection_time) FROM v_file_io_stats WHERE server_id = $1) ORDER BY (delta_stall_read_ms + delta_stall_write_ms) DESC"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); @@ -81,7 +81,7 @@ public async Task> GetFileIoLatencyTrendAsync(int serverI database_name, CASE WHEN SUM(delta_reads) > 0 THEN SUM(CAST(delta_stall_read_ms AS DOUBLE)) / SUM(delta_reads) ELSE 0 END AS avg_read_latency_ms, CASE WHEN SUM(delta_writes) > 0 THEN SUM(CAST(delta_stall_write_ms AS DOUBLE)) / SUM(delta_writes) ELSE 0 END AS avg_write_latency_ms -FROM file_io_stats +FROM v_file_io_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -124,7 +124,7 @@ public async Task> GetTempDbFileIoTrendAsync(int serverId file_name, CASE WHEN SUM(delta_reads) > 0 THEN SUM(CAST(delta_stall_read_ms AS DOUBLE)) / SUM(delta_reads) ELSE 0 END AS avg_read_latency_ms, CASE WHEN SUM(delta_writes) > 0 THEN SUM(CAST(delta_stall_write_ms AS DOUBLE)) / SUM(delta_writes) ELSE 0 END AS avg_write_latency_ms -FROM file_io_stats +FROM v_file_io_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 diff --git a/Lite/Services/LocalDataService.Memory.cs b/Lite/Services/LocalDataService.Memory.cs index 07612ee7..55869c47 100644 --- a/Lite/Services/LocalDataService.Memory.cs +++ b/Lite/Services/LocalDataService.Memory.cs @@ -35,7 +35,7 @@ public partial class LocalDataService total_server_memory_mb, buffer_pool_mb, plan_cache_mb -FROM memory_stats +FROM v_memory_stats WHERE server_id = $1 ORDER BY collection_time DESC LIMIT 1"; @@ -81,7 +81,7 @@ public async Task> GetMemoryTrendAsync(int serverId, int target_server_memory_mb, buffer_pool_mb, plan_cache_mb -FROM memory_stats +FROM v_memory_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -117,9 +117,9 @@ public async Task> GetLatestMemoryClerksAsync(int serverId) using var command = connection.CreateCommand(); command.CommandText = @" SELECT clerk_type, memory_mb -FROM memory_clerks +FROM v_memory_clerks WHERE server_id = $1 -AND collection_time = (SELECT MAX(collection_time) FROM memory_clerks WHERE server_id = $1) +AND collection_time = (SELECT MAX(collection_time) FROM v_memory_clerks WHERE server_id = $1) ORDER BY memory_mb DESC"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); diff --git a/Lite/Services/LocalDataService.Overview.cs b/Lite/Services/LocalDataService.Overview.cs index 2c9789be..b86f6c4a 100644 --- a/Lite/Services/LocalDataService.Overview.cs +++ b/Lite/Services/LocalDataService.Overview.cs @@ -34,7 +34,7 @@ public partial class LocalDataService { cmd.CommandText = @" SELECT sqlserver_cpu_utilization, sample_time -FROM cpu_utilization_stats +FROM v_cpu_utilization_stats WHERE server_id = $1 ORDER BY sample_time DESC LIMIT 1"; @@ -52,7 +52,7 @@ ORDER BY sample_time DESC { cmd.CommandText = @" SELECT total_server_memory_mb -FROM memory_stats +FROM v_memory_stats WHERE server_id = $1 ORDER BY collection_time DESC LIMIT 1"; @@ -69,7 +69,7 @@ ORDER BY collection_time DESC { cmd.CommandText = @" SELECT COUNT(*) -FROM blocked_process_reports +FROM v_blocked_process_reports WHERE server_id = $1 AND event_time >= $2"; cmd.Parameters.Add(new DuckDBParameter { Value = serverId }); @@ -83,7 +83,7 @@ FROM blocked_process_reports { cmd.CommandText = @" SELECT COUNT(*) -FROM deadlocks +FROM v_deadlocks WHERE server_id = $1 AND deadlock_time >= $2"; cmd.Parameters.Add(new DuckDBParameter { Value = serverId }); @@ -97,7 +97,7 @@ FROM deadlocks { cmd.CommandText = @" SELECT MAX(collection_time) -FROM collection_log +FROM v_collection_log WHERE server_id = $1"; cmd.Parameters.Add(new DuckDBParameter { Value = serverId }); var result = await cmd.ExecuteScalarAsync(); diff --git a/Lite/Services/LocalDataService.Perfmon.cs b/Lite/Services/LocalDataService.Perfmon.cs index 74a07e47..f748846c 100644 --- a/Lite/Services/LocalDataService.Perfmon.cs +++ b/Lite/Services/LocalDataService.Perfmon.cs @@ -28,9 +28,9 @@ public async Task> GetLatestPerfmonStatsAsync(int serverId) instance_name, cntr_value, delta_cntr_value -FROM perfmon_stats +FROM v_perfmon_stats WHERE server_id = $1 -AND collection_time = (SELECT MAX(collection_time) FROM perfmon_stats WHERE server_id = $1) +AND collection_time = (SELECT MAX(collection_time) FROM v_perfmon_stats WHERE server_id = $1) ORDER BY counter_name"; command.Parameters.Add(new DuckDBParameter { Value = serverId }); @@ -63,7 +63,7 @@ public async Task> GetDistinctPerfmonCountersAsync(int serverId, in command.CommandText = @" SELECT DISTINCT counter_name -FROM perfmon_stats +FROM v_perfmon_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -97,7 +97,7 @@ public async Task> GetPerfmonTrendAsync(int serverId, st collection_time, cntr_value, delta_cntr_value -FROM perfmon_stats +FROM v_perfmon_stats WHERE server_id = $1 AND counter_name = $2 AND collection_time >= $3 diff --git a/Lite/Services/LocalDataService.QueryStats.cs b/Lite/Services/LocalDataService.QueryStats.cs index fd4bafaf..0ed8a34c 100644 --- a/Lite/Services/LocalDataService.QueryStats.cs +++ b/Lite/Services/LocalDataService.QueryStats.cs @@ -66,7 +66,7 @@ public async Task> GetTopQueriesByCpuAsync(int serverId, int MAX(plan_handle) AS plan_handle, MAX(query_text) AS query_text, MAX(query_plan_xml) AS query_plan -FROM query_stats +FROM v_query_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -139,7 +139,7 @@ public async Task> GetQueryStatsHistoryAsync(int serv max_elapsed_time, query_plan_xml, query_plan_hash -FROM query_stats +FROM v_query_stats WHERE server_id = $1 AND database_name = $2 AND query_hash = $3 @@ -196,7 +196,7 @@ public async Task> GetProcedureStatsHistoryAsync( delta_logical_reads, delta_logical_writes, delta_physical_reads -FROM procedure_stats +FROM v_procedure_stats WHERE server_id = $1 AND database_name = $2 AND schema_name = $3 @@ -330,7 +330,7 @@ public async Task> GetTopProceduresByCpuAsync(int server SUM(total_spills) AS total_spills, MAX(sql_handle) AS sql_handle, MAX(plan_handle) AS plan_handle -FROM procedure_stats +FROM v_procedure_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -389,7 +389,7 @@ WITH raw AS SUM(delta_elapsed_time) / 1000.0 AS total_elapsed_ms, SUM(delta_execution_count) AS total_executions, date_diff('second', LAG(collection_time) OVER (ORDER BY collection_time), collection_time) AS interval_seconds - FROM query_stats + FROM v_query_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -438,7 +438,7 @@ WITH raw AS SUM(delta_elapsed_time) / 1000.0 AS total_elapsed_ms, SUM(delta_execution_count) AS total_executions, date_diff('second', LAG(collection_time) OVER (ORDER BY collection_time), collection_time) AS interval_seconds - FROM procedure_stats + FROM v_procedure_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -486,7 +486,7 @@ WITH raw AS collection_time, SUM(delta_execution_count) AS total_executions, date_diff('second', LAG(collection_time) OVER (ORDER BY collection_time), collection_time) AS interval_seconds - FROM query_stats + FROM v_query_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 diff --git a/Lite/Services/LocalDataService.QueryStore.cs b/Lite/Services/LocalDataService.QueryStore.cs index b492ae72..d68ae69c 100644 --- a/Lite/Services/LocalDataService.QueryStore.cs +++ b/Lite/Services/LocalDataService.QueryStore.cs @@ -44,7 +44,7 @@ public async Task> GetQueryStoreTopQueriesAsync(int serverId AVG(avg_rowcount) AS avg_rowcount, MAX(last_execution_time) AS last_execution_time, MAX(query_plan_hash) AS query_plan_hash -FROM query_store_stats +FROM v_query_store_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -102,7 +102,7 @@ public async Task> GetQueryStoreHistoryAsync(int serv avg_physical_reads, avg_rowcount, last_execution_time -FROM query_store_stats +FROM v_query_store_stats WHERE server_id = $1 AND database_name = $2 AND query_id = $3 @@ -146,7 +146,7 @@ public async Task> GetQueryStoreDatabasesAsync(int serverId, int ho using var command = connection.CreateCommand(); command.CommandText = @" SELECT DISTINCT database_name -FROM query_store_stats +FROM v_query_store_stats WHERE server_id = $1 AND collection_time >= $2 ORDER BY database_name"; @@ -221,7 +221,7 @@ WITH raw AS SUM(execution_count * avg_duration_ms) AS total_duration_ms, SUM(execution_count) AS total_executions, date_diff('second', LAG(collection_time) OVER (ORDER BY collection_time), collection_time) AS interval_seconds - FROM query_store_stats + FROM v_query_store_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 diff --git a/Lite/Services/LocalDataService.TempDb.cs b/Lite/Services/LocalDataService.TempDb.cs index 0b4e1bba..9b9ea7fe 100644 --- a/Lite/Services/LocalDataService.TempDb.cs +++ b/Lite/Services/LocalDataService.TempDb.cs @@ -36,7 +36,7 @@ public async Task> GetTempDbTrendAsync(int serverId, int hoursBa total_sessions_using_tempdb, top_session_id, top_session_tempdb_mb -FROM tempdb_stats +FROM v_tempdb_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -84,7 +84,7 @@ FROM tempdb_stats version_store_reserved_mb, top_session_tempdb_mb, top_session_id -FROM tempdb_stats +FROM v_tempdb_stats WHERE server_id = $1 ORDER BY collection_time DESC LIMIT 1"; diff --git a/Lite/Services/LocalDataService.WaitStats.cs b/Lite/Services/LocalDataService.WaitStats.cs index 406bdd57..46994844 100644 --- a/Lite/Services/LocalDataService.WaitStats.cs +++ b/Lite/Services/LocalDataService.WaitStats.cs @@ -32,7 +32,7 @@ public async Task> GetWaitStatsAsync(int serverId, int hoursB SUM(delta_wait_time_ms) AS total_wait_time_ms, SUM(delta_signal_wait_time_ms) AS total_signal_wait_time_ms, COUNT(*) AS sample_count -FROM wait_stats +FROM v_wait_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -75,7 +75,7 @@ public async Task> GetDistinctWaitTypesAsync(int serverId, int hour SELECT wait_type, SUM(delta_wait_time_ms) AS total_delta -FROM wait_stats +FROM v_wait_stats WHERE server_id = $1 AND collection_time >= $2 AND collection_time <= $3 @@ -114,7 +114,7 @@ WITH raw AS delta_signal_wait_time_ms, delta_waiting_tasks, date_diff('second', LAG(collection_time) OVER (ORDER BY collection_time), collection_time) AS interval_seconds - FROM wait_stats + FROM v_wait_stats WHERE server_id = $1 AND wait_type = $2 AND collection_time >= $3 @@ -166,7 +166,7 @@ public async Task> GetLatestPoisonWaitAvgsAsync(int server CASE WHEN delta_waiting_tasks > 0 THEN CAST(delta_wait_time_ms AS DOUBLE) / delta_waiting_tasks ELSE 0 END AS avg_ms_per_wait -FROM wait_stats +FROM v_wait_stats WHERE server_id = $1 AND wait_type IN ('THREADPOOL', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE') AND delta_waiting_tasks > 0 @@ -213,9 +213,9 @@ public async Task> GetLongRunningQueriesAsync(int ser writes, wait_type, blocking_session_id -FROM query_snapshots +FROM v_query_snapshots WHERE server_id = $1 -AND collection_time = (SELECT MAX(collection_time) FROM query_snapshots WHERE server_id = $1) +AND collection_time = (SELECT MAX(collection_time) FROM v_query_snapshots WHERE server_id = $1) AND session_id > 50 AND total_elapsed_time_ms >= $2 ORDER BY total_elapsed_time_ms DESC