Skip to content

Commit d2500c2

Browse files
Merge pull request #433 from erikdarlingdata/feature/lob-compression-dedup
LOB compression + deduplication for query stats tables (#419)
2 parents 0751e51 + 9c04bb2 commit d2500c2

18 files changed

Lines changed: 2074 additions & 108 deletions

Dashboard/Dashboard.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
<UseWPF>true</UseWPF>
77
<AssemblyName>PerformanceMonitorDashboard</AssemblyName>
88
<Product>SQL Server Performance Monitor Dashboard</Product>
9-
<Version>2.1.0</Version>
10-
<AssemblyVersion>2.1.0.0</AssemblyVersion>
11-
<FileVersion>2.1.0.0</FileVersion>
12-
<InformationalVersion>2.1.0</InformationalVersion>
9+
<Version>2.2.0</Version>
10+
<AssemblyVersion>2.2.0.0</AssemblyVersion>
11+
<FileVersion>2.2.0.0</FileVersion>
12+
<InformationalVersion>2.2.0</InformationalVersion>
1313
<Company>Darling Data, LLC</Company>
1414
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
1515
<ApplicationIcon>EDD.ico</ApplicationIcon>

Dashboard/Services/DatabaseService.QueryPerformance.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -739,8 +739,8 @@ WITH per_lifetime AS
739739
total_spills = MAX(qs.total_spills),
740740
min_spills = MIN(qs.min_spills),
741741
max_spills = MAX(qs.max_spills),
742-
query_text = MAX(qs.query_text),
743-
query_plan_text = MAX(qs.query_plan_text),
742+
query_text = CAST(DECOMPRESS(MAX(qs.query_text)) AS nvarchar(max)),
743+
query_plan_text = CAST(DECOMPRESS(MAX(qs.query_plan_text)) AS nvarchar(max)),
744744
query_plan_hash = MAX(qs.query_plan_hash),
745745
sql_handle = MAX(qs.sql_handle),
746746
plan_handle = MAX(qs.plan_handle)
@@ -753,7 +753,7 @@ FROM collect.query_stats AS qs
753753
OR (qs.last_execution_time >= @fromDate AND qs.last_execution_time <= @toDate)
754754
OR (qs.creation_time <= @fromDate AND qs.last_execution_time >= @toDate)))
755755
)
756-
AND qs.query_text NOT LIKE N'WAITFOR%'
756+
AND CAST(DECOMPRESS(qs.query_text) AS nvarchar(max)) NOT LIKE N'WAITFOR%'
757757
GROUP BY
758758
qs.database_name,
759759
qs.query_hash,
@@ -922,7 +922,7 @@ WITH per_lifetime AS
922922
total_spills = MAX(ps.total_spills),
923923
min_spills = MIN(ps.min_spills),
924924
max_spills = MAX(ps.max_spills),
925-
query_plan_text = MAX(ps.query_plan_text),
925+
query_plan_text = CAST(DECOMPRESS(MAX(ps.query_plan_text)) AS nvarchar(max)),
926926
sql_handle = MAX(ps.sql_handle),
927927
plan_handle = MAX(ps.plan_handle)
928928
FROM collect.procedure_stats AS ps
@@ -1101,7 +1101,7 @@ public async Task<List<QueryStoreItem>> GetQueryStoreDataAsync(int hoursBack = 2
11011101
plan_type = MAX(qsd.plan_type),
11021102
is_forced_plan = MAX(CONVERT(tinyint, qsd.is_forced_plan)),
11031103
compatibility_level = MAX(qsd.compatibility_level),
1104-
query_sql_text = CONVERT(nvarchar(max), MAX(qsd.query_sql_text)),
1104+
query_sql_text = CAST(DECOMPRESS(MAX(qsd.query_sql_text)) AS nvarchar(max)),
11051105
query_plan_hash = CONVERT(nvarchar(20), MAX(qsd.query_plan_hash), 1),
11061106
force_failure_count = SUM(qsd.force_failure_count),
11071107
last_force_failure_reason_desc = MAX(qsd.last_force_failure_reason_desc),
@@ -1121,7 +1121,7 @@ FROM collect.query_store_data AS qsd
11211121
OR (qsd.server_last_execution_time >= @fromDate AND qsd.server_last_execution_time <= @toDate)
11221122
OR (qsd.server_first_execution_time <= @fromDate AND qsd.server_last_execution_time >= @toDate)))
11231123
)
1124-
AND qsd.query_sql_text NOT LIKE N'WAITFOR%'
1124+
AND CAST(DECOMPRESS(qsd.query_sql_text) AS nvarchar(max)) NOT LIKE N'WAITFOR%'
11251125
GROUP BY
11261126
qsd.database_name,
11271127
qsd.query_id
@@ -2228,7 +2228,7 @@ ORDER BY
22282228
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
22292229
22302230
SELECT
2231-
qsd.query_plan_text
2231+
CAST(DECOMPRESS(qsd.query_plan_text) AS nvarchar(max)) AS query_plan_text
22322232
FROM collect.query_store_data AS qsd
22332233
WHERE qsd.collection_id = @collection_id;";
22342234

@@ -2276,7 +2276,7 @@ FROM collect.procedure_stats AS ps
22762276
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
22772277
22782278
SELECT
2279-
qs.query_plan_text
2279+
CAST(DECOMPRESS(qs.query_plan_text) AS nvarchar(max)) AS query_plan_text
22802280
FROM collect.query_stats AS qs
22812281
WHERE qs.collection_id = @collection_id;";
22822282

Installer/PerformanceMonitorInstaller.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
<!-- Application metadata -->
2121
<AssemblyName>PerformanceMonitorInstaller</AssemblyName>
2222
<Product>SQL Server Performance Monitor Installer</Product>
23-
<Version>2.1.0</Version>
24-
<AssemblyVersion>2.1.0.0</AssemblyVersion>
25-
<FileVersion>2.1.0.0</FileVersion>
26-
<InformationalVersion>2.1.0</InformationalVersion>
23+
<Version>2.2.0</Version>
24+
<AssemblyVersion>2.2.0.0</AssemblyVersion>
25+
<FileVersion>2.2.0.0</FileVersion>
26+
<InformationalVersion>2.2.0</InformationalVersion>
2727
<Company>Darling Data, LLC</Company>
2828
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
2929
<Description>Installation utility for SQL Server Performance Monitor - Supports SQL Server 2016-2025</Description>

InstallerGui/InstallerGui.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
<AssemblyName>PerformanceMonitorInstallerGui</AssemblyName>
99
<RootNamespace>PerformanceMonitorInstallerGui</RootNamespace>
1010
<Product>SQL Server Performance Monitor Installer</Product>
11-
<Version>2.1.0</Version>
12-
<AssemblyVersion>2.1.0.0</AssemblyVersion>
13-
<FileVersion>2.1.0.0</FileVersion>
14-
<InformationalVersion>2.1.0</InformationalVersion>
11+
<Version>2.2.0</Version>
12+
<AssemblyVersion>2.2.0.0</AssemblyVersion>
13+
<FileVersion>2.2.0.0</FileVersion>
14+
<InformationalVersion>2.2.0</InformationalVersion>
1515
<Company>Darling Data, LLC</Company>
1616
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
1717
<ApplicationIcon>EDD.ico</ApplicationIcon>

Lite/PerformanceMonitorLite.csproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77
<AssemblyName>PerformanceMonitorLite</AssemblyName>
88
<RootNamespace>PerformanceMonitorLite</RootNamespace>
99
<Product>SQL Server Performance Monitor Lite</Product>
10-
<Version>2.1.0</Version>
11-
<AssemblyVersion>2.1.0.0</AssemblyVersion>
12-
<FileVersion>2.1.0.0</FileVersion>
13-
<InformationalVersion>2.1.0</InformationalVersion>
10+
<Version>2.2.0</Version>
11+
<AssemblyVersion>2.2.0.0</AssemblyVersion>
12+
<FileVersion>2.2.0.0</FileVersion>
13+
<InformationalVersion>2.2.0</InformationalVersion>
1414
<Company>Darling Data, LLC</Company>
1515
<Copyright>Copyright © 2026 Darling Data, LLC</Copyright>
1616
<Description>Lightweight SQL Server performance monitoring - no installation required on target servers</Description>

install/01_install_database.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ BEGIN
274274
DEFAULT 5,
275275
retention_days integer NOT NULL
276276
DEFAULT 30,
277+
collect_query bit NOT NULL
278+
DEFAULT CONVERT(bit, 'true'),
279+
collect_plan bit NOT NULL
280+
DEFAULT CONVERT(bit, 'true'),
277281
[description] nvarchar(500) NULL,
278282
created_date datetime2(7) NOT NULL
279283
DEFAULT SYSDATETIME(),

install/02_create_tables.sql

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,11 @@ BEGIN
168168
total_worker_time_delta /
169169
NULLIF(sample_interval_seconds, 0) / 1000.
170170
),
171-
/*Query text and execution plan*/
172-
query_text nvarchar(MAX) NULL,
173-
query_plan_text nvarchar(MAX) NULL,
174-
query_plan xml NULL,
171+
/*Query text and execution plan (compressed with COMPRESS/DECOMPRESS)*/
172+
query_text varbinary(max) NULL,
173+
query_plan_text varbinary(max) NULL,
174+
/*Deduplication hash for skipping unchanged rows*/
175+
row_hash binary(32) NULL,
175176
CONSTRAINT
176177
PK_query_stats
177178
PRIMARY KEY CLUSTERED
@@ -183,6 +184,34 @@ BEGIN
183184
PRINT 'Created collect.query_stats table';
184185
END;
185186

187+
/*
188+
2b. Query Stats Dedup Tracking
189+
One row per natural key, updated on each collection cycle
190+
*/
191+
IF OBJECT_ID(N'collect.query_stats_latest_hash', N'U') IS NULL
192+
BEGIN
193+
CREATE TABLE
194+
collect.query_stats_latest_hash
195+
(
196+
sql_handle varbinary(64) NOT NULL,
197+
statement_start_offset integer NOT NULL,
198+
statement_end_offset integer NOT NULL,
199+
plan_handle varbinary(64) NOT NULL,
200+
row_hash binary(32) NOT NULL,
201+
last_seen datetime2(7) NOT NULL
202+
DEFAULT SYSDATETIME(),
203+
CONSTRAINT
204+
PK_query_stats_latest_hash
205+
PRIMARY KEY CLUSTERED
206+
(sql_handle, statement_start_offset,
207+
statement_end_offset, plan_handle)
208+
WITH
209+
(DATA_COMPRESSION = PAGE)
210+
);
211+
212+
PRINT 'Created collect.query_stats_latest_hash table';
213+
END;
214+
186215
/*
187216
3. Memory Pressure
188217
*/
@@ -429,9 +458,10 @@ BEGIN
429458
total_worker_time_delta /
430459
NULLIF(sample_interval_seconds, 0) / 1000.
431460
),
432-
/*Execution plan*/
433-
query_plan_text nvarchar(max) NULL,
434-
query_plan xml NULL,
461+
/*Execution plan (compressed with COMPRESS/DECOMPRESS)*/
462+
query_plan_text varbinary(max) NULL,
463+
/*Deduplication hash for skipping unchanged rows*/
464+
row_hash binary(32) NULL,
435465
CONSTRAINT
436466
PK_procedure_stats
437467
PRIMARY KEY CLUSTERED
@@ -443,6 +473,32 @@ BEGIN
443473
PRINT 'Created collect.procedure_stats table';
444474
END;
445475

476+
/*
477+
9b. Procedure Stats Dedup Tracking
478+
One row per natural key, updated on each collection cycle
479+
*/
480+
IF OBJECT_ID(N'collect.procedure_stats_latest_hash', N'U') IS NULL
481+
BEGIN
482+
CREATE TABLE
483+
collect.procedure_stats_latest_hash
484+
(
485+
database_name sysname NOT NULL,
486+
object_id integer NOT NULL,
487+
plan_handle varbinary(64) NOT NULL,
488+
row_hash binary(32) NOT NULL,
489+
last_seen datetime2(7) NOT NULL
490+
DEFAULT SYSDATETIME(),
491+
CONSTRAINT
492+
PK_procedure_stats_latest_hash
493+
PRIMARY KEY CLUSTERED
494+
(database_name, object_id, plan_handle)
495+
WITH
496+
(DATA_COMPRESSION = PAGE)
497+
);
498+
499+
PRINT 'Created collect.procedure_stats_latest_hash table';
500+
END;
501+
446502
/*
447503
10. Currently Executing Query Snapshots
448504
Table is created dynamically by sp_WhoIsActive on first collection
@@ -473,7 +529,7 @@ BEGIN
473529
server_first_execution_time datetime2(7) NOT NULL,
474530
server_last_execution_time datetime2(7) NOT NULL,
475531
module_name nvarchar(261) NULL,
476-
query_sql_text nvarchar(max) NULL,
532+
query_sql_text varbinary(max) NULL,
477533
query_hash binary(8) NULL,
478534
/*Execution count*/
479535
count_executions bigint NOT NULL,
@@ -531,9 +587,11 @@ BEGIN
531587
last_force_failure_reason_desc nvarchar(128) NULL,
532588
plan_forcing_type nvarchar(60) NULL,
533589
compatibility_level smallint NULL,
534-
query_plan_text nvarchar(max) NULL,
535-
compilation_metrics xml NULL,
590+
query_plan_text varbinary(max) NULL,
591+
compilation_metrics varbinary(max) NULL,
536592
query_plan_hash binary(8) NULL,
593+
/*Deduplication hash for skipping unchanged rows*/
594+
row_hash binary(32) NULL,
537595
CONSTRAINT
538596
PK_query_store_data
539597
PRIMARY KEY CLUSTERED
@@ -545,6 +603,32 @@ BEGIN
545603
PRINT 'Created collect.query_store_data table';
546604
END;
547605

606+
/*
607+
11b. Query Store Data Dedup Tracking
608+
One row per natural key, updated on each collection cycle
609+
*/
610+
IF OBJECT_ID(N'collect.query_store_data_latest_hash', N'U') IS NULL
611+
BEGIN
612+
CREATE TABLE
613+
collect.query_store_data_latest_hash
614+
(
615+
database_name sysname NOT NULL,
616+
query_id bigint NOT NULL,
617+
plan_id bigint NOT NULL,
618+
row_hash binary(32) NOT NULL,
619+
last_seen datetime2(7) NOT NULL
620+
DEFAULT SYSDATETIME(),
621+
CONSTRAINT
622+
PK_query_store_data_latest_hash
623+
PRIMARY KEY CLUSTERED
624+
(database_name, query_id, plan_id)
625+
WITH
626+
(DATA_COMPRESSION = PAGE)
627+
);
628+
629+
PRINT 'Created collect.query_store_data_latest_hash table';
630+
END;
631+
548632
/*
549633
Trace analysis table - stores processed trace file data
550634
*/

install/06_ensure_collection_table.sql

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -265,10 +265,11 @@ BEGIN
265265
total_worker_time_delta /
266266
NULLIF(sample_interval_seconds, 0) / 1000.
267267
),
268-
/*Query text and execution plan*/
269-
query_text nvarchar(max) NULL,
270-
query_plan_text nvarchar(max) NULL,
271-
query_plan xml NULL,
268+
/*Query text and execution plan (compressed with COMPRESS/DECOMPRESS)*/
269+
query_text varbinary(max) NULL,
270+
query_plan_text varbinary(max) NULL,
271+
/*Deduplication hash for skipping unchanged rows*/
272+
row_hash binary(32) NULL,
272273
CONSTRAINT
273274
PK_query_stats
274275
PRIMARY KEY CLUSTERED
@@ -446,9 +447,10 @@ BEGIN
446447
total_worker_time_delta /
447448
NULLIF(sample_interval_seconds, 0) / 1000.
448449
),
449-
/*Execution plan*/
450-
query_plan_text nvarchar(max) NULL,
451-
query_plan xml NULL,
450+
/*Execution plan (compressed with COMPRESS/DECOMPRESS)*/
451+
query_plan_text varbinary(max) NULL,
452+
/*Deduplication hash for skipping unchanged rows*/
453+
row_hash binary(32) NULL,
452454
CONSTRAINT
453455
PK_procedure_stats
454456
PRIMARY KEY CLUSTERED
@@ -491,7 +493,7 @@ BEGIN
491493
server_first_execution_time datetime2(7) NOT NULL,
492494
server_last_execution_time datetime2(7) NOT NULL,
493495
module_name nvarchar(261) NULL,
494-
query_sql_text nvarchar(max) NULL,
496+
query_sql_text varbinary(max) NULL,
495497
query_hash binary(8) NULL,
496498
/*Execution count*/
497499
count_executions bigint NOT NULL,
@@ -549,9 +551,11 @@ BEGIN
549551
last_force_failure_reason_desc nvarchar(128) NULL,
550552
plan_forcing_type nvarchar(60) NULL,
551553
compatibility_level smallint NULL,
552-
query_plan_text nvarchar(max) NULL,
553-
compilation_metrics xml NULL,
554+
query_plan_text varbinary(max) NULL,
555+
compilation_metrics varbinary(max) NULL,
554556
query_plan_hash binary(8) NULL,
557+
/*Deduplication hash for skipping unchanged rows*/
558+
row_hash binary(32) NULL,
555559
CONSTRAINT
556560
PK_query_store_data
557561
PRIMARY KEY CLUSTERED

0 commit comments

Comments
 (0)