Skip to content

Commit b8578f1

Browse files
Enrich database_size_stats with volume-level drive space from dm_os_volume_stats
Adds volume_mount_point, volume_total_mb, volume_free_mb to the database_size_stats collector so every file row carries its drive context. On-prem uses CROSS APPLY sys.dm_os_volume_stats(); Azure SQL DB gets NULLs (DMV not available). Tested on sql2022: 39 rows, all with C:\ volume data (409 GB total, 135 GB free). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 35e90ca commit b8578f1

8 files changed

Lines changed: 120 additions & 13 deletions

File tree

Lite/Database/DuckDbInitializer.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void Dispose()
8686
/// <summary>
8787
/// Current schema version. Increment this when schema changes require table rebuilds.
8888
/// </summary>
89-
internal const int CurrentSchemaVersion = 16;
89+
internal const int CurrentSchemaVersion = 17;
9090

9191
private readonly string _archivePath;
9292

@@ -505,6 +505,23 @@ New tables only — no existing table changes needed. Tables created by
505505
GetAllTableStatements() during initialization. */
506506
_logger?.LogInformation("Running migration to v16: adding FinOps tables (database_size_stats, server_properties)");
507507
}
508+
509+
if (fromVersion < 17)
510+
{
511+
/* v17: Added volume-level drive space columns to database_size_stats.
512+
Columns appended at end — safe for DuckDB appender positional writes. */
513+
_logger?.LogInformation("Running migration to v17: adding volume stats columns to database_size_stats");
514+
try
515+
{
516+
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS volume_mount_point VARCHAR");
517+
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS volume_total_mb DECIMAL(19,2)");
518+
await ExecuteNonQueryAsync(connection, "ALTER TABLE database_size_stats ADD COLUMN IF NOT EXISTS volume_free_mb DECIMAL(19,2)");
519+
}
520+
catch
521+
{
522+
/* Table doesn't exist yet — will be created with correct schema below */
523+
}
524+
}
508525
}
509526

510527
/// <summary>

Lite/Database/Schema.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,10 @@ auto_growth_mb DECIMAL(19,2),
609609
max_size_mb DECIMAL(19,2),
610610
recovery_model_desc VARCHAR,
611611
compatibility_level INTEGER,
612-
state_desc VARCHAR
612+
state_desc VARCHAR,
613+
volume_mount_point VARCHAR,
614+
volume_total_mb DECIMAL(19,2),
615+
volume_free_mb DECIMAL(19,2)
613616
)";
614617

615618
public const string CreateDatabaseSizeStatsIndex = @"

Lite/Services/RemoteCollectorService.DatabaseSize.cs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public partial class RemoteCollectorService
2222
{
2323
/// <summary>
2424
/// Collects per-file database sizes for growth trending and capacity planning.
25-
/// On-prem: queries sys.master_files + sys.databases for all online databases.
26-
/// Azure SQL DB: queries sys.database_files for the single database.
25+
/// On-prem: queries sys.master_files + sys.databases + dm_os_volume_stats for file and drive context.
26+
/// Azure SQL DB: queries sys.database_files for the single database (no volume stats available).
2727
/// </summary>
2828
private async Task<int> CollectDatabaseSizeStatsAsync(ServerConnection server, CancellationToken cancellationToken)
2929
{
@@ -63,10 +63,17 @@ ELSE CONVERT(decimal(19,2), mf.max_size * 8.0 / 1024.0)
6363
compatibility_level =
6464
CONVERT(int, d.compatibility_level),
6565
state_desc =
66-
d.state_desc
66+
d.state_desc,
67+
volume_mount_point =
68+
RTRIM(vs.volume_mount_point),
69+
volume_total_mb =
70+
CONVERT(decimal(19,2), vs.total_bytes / 1048576.0),
71+
volume_free_mb =
72+
CONVERT(decimal(19,2), vs.available_bytes / 1048576.0)
6773
FROM sys.master_files AS mf
6874
JOIN sys.databases AS d
6975
ON d.database_id = mf.database_id
76+
CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) AS vs
7077
WHERE d.state_desc = N'ONLINE'
7178
ORDER BY
7279
d.name,
@@ -106,7 +113,13 @@ ELSE CONVERT(decimal(19,2), df.max_size * 8.0 / 1024.0)
106113
compatibility_level =
107114
CONVERT(int, NULL),
108115
state_desc =
109-
N'ONLINE'
116+
N'ONLINE',
117+
volume_mount_point =
118+
CONVERT(nvarchar(256), NULL),
119+
volume_total_mb =
120+
CONVERT(decimal(19,2), NULL),
121+
volume_free_mb =
122+
CONVERT(decimal(19,2), NULL)
110123
FROM sys.database_files AS df
111124
ORDER BY
112125
df.file_id
@@ -123,7 +136,8 @@ ORDER BY
123136
var rows = new List<(string DatabaseName, int DatabaseId, int FileId, string FileTypeDesc,
124137
string FileName, string PhysicalName, decimal TotalSizeMb, decimal? UsedSizeMb,
125138
decimal? AutoGrowthMb, decimal? MaxSizeMb, string? RecoveryModel,
126-
int? CompatibilityLevel, string? StateDesc)>();
139+
int? CompatibilityLevel, string? StateDesc, string? VolumeMountPoint,
140+
decimal? VolumeTotalMb, decimal? VolumeFreeMb)>();
127141

128142
var sqlSw = Stopwatch.StartNew();
129143
using var sqlConnection = await CreateConnectionAsync(server, cancellationToken);
@@ -146,7 +160,10 @@ ORDER BY
146160
reader.IsDBNull(9) ? null : reader.GetDecimal(9),
147161
reader.IsDBNull(10) ? null : reader.GetString(10),
148162
reader.IsDBNull(11) ? null : reader.GetInt32(11),
149-
reader.IsDBNull(12) ? null : reader.GetString(12)));
163+
reader.IsDBNull(12) ? null : reader.GetString(12),
164+
reader.IsDBNull(13) ? null : reader.GetString(13),
165+
reader.IsDBNull(14) ? null : reader.GetDecimal(14),
166+
reader.IsDBNull(15) ? null : reader.GetDecimal(15)));
150167
}
151168
sqlSw.Stop();
152169

@@ -178,6 +195,9 @@ ORDER BY
178195
.AppendValue(r.RecoveryModel)
179196
.AppendValue(r.CompatibilityLevel)
180197
.AppendValue(r.StateDesc)
198+
.AppendValue(r.VolumeMountPoint)
199+
.AppendValue(r.VolumeTotalMb)
200+
.AppendValue(r.VolumeFreeMb)
181201
.EndRow();
182202
rowsCollected++;
183203
}

install/02_create_tables.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,9 @@ BEGIN
14301430
recovery_model_desc nvarchar(12) NULL,
14311431
compatibility_level integer NULL,
14321432
state_desc nvarchar(60) NULL,
1433+
volume_mount_point nvarchar(256) NULL,
1434+
volume_total_mb decimal(19,2) NULL,
1435+
volume_free_mb decimal(19,2) NULL,
14331436
/*Analysis helpers - computed columns*/
14341437
free_space_mb AS
14351438
(

install/06_ensure_collection_table.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,9 @@ BEGIN
11101110
recovery_model_desc nvarchar(12) NULL,
11111111
compatibility_level integer NULL,
11121112
state_desc nvarchar(60) NULL,
1113+
volume_mount_point nvarchar(256) NULL,
1114+
volume_total_mb decimal(19,2) NULL,
1115+
volume_free_mb decimal(19,2) NULL,
11131116
free_space_mb AS
11141117
(
11151118
total_size_mb - used_size_mb

install/52_collect_database_size_stats.sql

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ BEGIN
112112
max_size_mb,
113113
recovery_model_desc,
114114
compatibility_level,
115-
state_desc
115+
state_desc,
116+
volume_mount_point,
117+
volume_total_mb,
118+
volume_free_mb
116119
)
117120
SELECT
118121
collection_time = @start_time,
@@ -147,7 +150,10 @@ BEGIN
147150
recovery_model_desc =
148151
CONVERT(nvarchar(12), DATABASEPROPERTYEX(DB_NAME(), N'Recovery')),
149152
compatibility_level = NULL,
150-
state_desc = N'ONLINE'
153+
state_desc = N'ONLINE',
154+
volume_mount_point = NULL,
155+
volume_total_mb = NULL,
156+
volume_free_mb = NULL
151157
FROM sys.database_files AS df
152158
OPTION(RECOMPILE);
153159

@@ -200,7 +206,10 @@ BEGIN
200206
max_size_mb,
201207
recovery_model_desc,
202208
compatibility_level,
203-
state_desc
209+
state_desc,
210+
volume_mount_point,
211+
volume_total_mb,
212+
volume_free_mb
204213
)
205214
SELECT
206215
collection_time = @start_time,
@@ -234,9 +243,16 @@ BEGIN
234243
END,
235244
recovery_model_desc = d.recovery_model_desc,
236245
compatibility_level = d.compatibility_level,
237-
state_desc = d.state_desc
246+
state_desc = d.state_desc,
247+
volume_mount_point =
248+
RTRIM(vs.volume_mount_point),
249+
volume_total_mb =
250+
CONVERT(decimal(19,2), vs.total_bytes / 1048576.0),
251+
volume_free_mb =
252+
CONVERT(decimal(19,2), vs.available_bytes / 1048576.0)
238253
FROM sys.database_files AS df
239254
CROSS JOIN sys.databases AS d
255+
CROSS APPLY sys.dm_os_volume_stats(DB_ID(), df.file_id) AS vs
240256
WHERE d.database_id = DB_ID();';
241257

242258
EXECUTE sys.sp_executesql
@@ -277,7 +293,10 @@ BEGIN
277293
dss.total_size_mb,
278294
dss.used_size_mb,
279295
dss.free_space_mb,
280-
dss.used_pct
296+
dss.used_pct,
297+
dss.volume_mount_point,
298+
dss.volume_total_mb,
299+
dss.volume_free_mb
281300
FROM collect.database_size_stats AS dss
282301
WHERE dss.collection_time = @start_time
283302
ORDER BY
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2026 Darling Data, LLC
3+
https://www.erikdarling.com/
4+
5+
Upgrade from 2.1.0 to 2.2.0
6+
Adds volume-level drive space columns to database_size_stats.
7+
*/
8+
9+
SET ANSI_NULLS ON;
10+
SET ANSI_PADDING ON;
11+
SET ANSI_WARNINGS ON;
12+
SET ARITHABORT ON;
13+
SET CONCAT_NULL_YIELDS_NULL ON;
14+
SET QUOTED_IDENTIFIER ON;
15+
SET NUMERIC_ROUNDABORT OFF;
16+
SET IMPLICIT_TRANSACTIONS OFF;
17+
SET STATISTICS TIME, IO OFF;
18+
GO
19+
20+
USE PerformanceMonitor;
21+
GO
22+
23+
IF NOT EXISTS
24+
(
25+
SELECT
26+
1/0
27+
FROM sys.columns
28+
WHERE object_id = OBJECT_ID(N'collect.database_size_stats', N'U')
29+
AND name = N'volume_mount_point'
30+
)
31+
BEGIN
32+
ALTER TABLE
33+
collect.database_size_stats
34+
ADD
35+
volume_mount_point nvarchar(256) NULL,
36+
volume_total_mb decimal(19,2) NULL,
37+
volume_free_mb decimal(19,2) NULL;
38+
39+
PRINT 'Added volume_mount_point, volume_total_mb, volume_free_mb to collect.database_size_stats';
40+
END;
41+
GO

upgrades/2.1.0-to-2.2.0/upgrade.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
03_compress_procedure_stats.sql
44
04_create_tracking_tables.sql
55
05_add_finops_collectors.sql
6+
06_add_volume_stats_columns.sql

0 commit comments

Comments
 (0)