Skip to content

Commit 62923eb

Browse files
Uwe Jankeclaude
andcommitted
Get-sqmDiskSpaceReport: backup-history bootstrap forecast (method B2) (1.7.1.0)
Adds -SeedFromBackupHistory: while the snapshot history (B1) has fewer than -MinDataPoints points for a volume, derive a fallback growth rate from msdb.dbo.backupset. Per database the data-growth trend is fitted from full-backup sizes (linear regression, >=3 points) and distributed across volumes in proportion to the database's data-file size on each volume. Reported as ForecastBasis='BackupHistory' (confidence Low, report column "Boot"); B1 takes over once enough snapshots exist. Opt-in switch; needs read access to msdb.dbo.backupset. Verified: helper runs cleanly against real msdb (empty when no multi-day trend), distribution math and the in-function seed override branch validated synthetically, cold start falls back to "collecting". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cbe34c5 commit 62923eb

3 files changed

Lines changed: 141 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# sqmSQLTool — Changelog
22

3+
## [1.7.1.0] — 2026-06-22
4+
5+
### ✨ Neu
6+
7+
- **Get-sqmDiskSpaceReport — Bootstrap aus Backup-Historie (Methode B2)**: Neuer Schalter
8+
`-SeedFromBackupHistory`. Solange die Snapshot-Historie (B1) für ein Volume noch < `-MinDataPoints`
9+
Punkte hat, wird ersatzweise eine Wachstumsrate aus `msdb.dbo.backupset` abgeleitet: je Datenbank
10+
der Daten-Wachstumstrend aus den Full-Backup-Größen (lineare Regression, ab 3 Punkten) und
11+
proportional zur Datendatei-Größe auf die Volumes verteilt. Überbrückt die ~5-Läufe-Anlaufzeit von
12+
B1. Ausgewiesen mit `ForecastBasis='BackupHistory'`, Konfidenz `Low` (Report-Spalte `Boot`). Sobald
13+
B1 genug Snapshots hat, übernimmt wieder B1. Greift nur bei gesetztem Schalter; benötigt Lesezugriff
14+
auf `msdb.dbo.backupset`.
15+
316
## [1.7.0.0] — 2026-06-22
417

518
### ✨ Neu

Public/Get-sqmDiskSpaceReport.ps1

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@
4848
.PARAMETER NoHistory
4949
Do not append the current run to the history (forecast still uses whatever history exists).
5050
51+
.PARAMETER SeedFromBackupHistory
52+
Bootstrap (method B2): while the snapshot history has fewer than -MinDataPoints points for a volume,
53+
derive a fallback growth rate from msdb full-backup sizes (data growth trend per database, mapped to
54+
volumes by data-file location). Used only as long as B1 is insufficient; reported with
55+
ForecastBasis='BackupHistory' and confidence Low. Requires read access to msdb.dbo.backupset.
56+
5157
.PARAMETER ContinueOnError
5258
Continue on error for an instance (otherwise the error is thrown).
5359
@@ -99,6 +105,8 @@ function Get-sqmDiskSpaceReport
99105
[Parameter(Mandatory = $false)]
100106
[switch]$NoHistory,
101107
[Parameter(Mandatory = $false)]
108+
[switch]$SeedFromBackupHistory,
109+
[Parameter(Mandatory = $false)]
102110
[switch]$ContinueOnError,
103111
[Parameter(Mandatory = $false)]
104112
[switch]$EnableException,
@@ -277,6 +285,23 @@ ORDER BY vs.volume_mount_point;
277285
}
278286
}
279287

288+
# 2b. Bootstrap (B2): Wachstumsrate aus msdb-Backup-Historie, solange B1 noch zu wenige
289+
# Snapshots hat. Greift in der Detailschleife nur, wenn der B1-Forecast 'Insufficient' ist.
290+
$backupSeed = @{ }
291+
if ($SeedFromBackupHistory)
292+
{
293+
try
294+
{
295+
$backupSeed = Get-sqmBackupGrowthSeed -ConnParams $connParams -Days ([math]::Max($HistoryDays, 90)) -Volumes $volRows
296+
Invoke-sqmLogging -Message "[$instance] Backup-Historie-Seed: $($backupSeed.Keys.Count) Volume(s) mit Trend." -FunctionName $functionName -Level "VERBOSE"
297+
}
298+
catch
299+
{
300+
Invoke-sqmLogging -Message "[$instance] Backup-Historie-Seed nicht ermittelbar: $($_.Exception.Message)" -FunctionName $functionName -Level "WARNING"
301+
$backupSeed = @{ }
302+
}
303+
}
304+
280305
# 3. Detailzeilen aufbereiten (Prognose je Volume aus der Historie)
281306
foreach ($vol in $volRows)
282307
{
@@ -289,10 +314,24 @@ ORDER BY vs.volume_mount_point;
289314
$volHist = @($histForecast | Where-Object { $_.MountPoint -eq $mount })
290315
$fc = Get-sqmVolumeForecast -History $volHist -FreeGB $freeGB -MinDataPoints $MinDataPoints
291316

317+
$basis = $fc.Basis
318+
$conf = $fc.Confidence
319+
$dpoints = $fc.DataPoints
292320
$growthPerDay = if ($fc.Basis -eq 'History' -and $fc.SlopePerDayGB -gt 0) { $fc.SlopePerDayGB } else { 0 }
293321
$daysUntilFull = if ($fc.Basis -eq 'History') { $fc.DaysUntilFull } else { $null }
294322
$growthGB = if ($fc.Basis -eq 'History') { $fc.GrowthWindowGB } else { $null }
295323

324+
# Bootstrap (B2) nur solange B1 noch nicht greift.
325+
if ($fc.Basis -ne 'History' -and $backupSeed.ContainsKey($mount) -and $backupSeed[$mount].SlopePerDayGB -gt 0)
326+
{
327+
$seedSlope = $backupSeed[$mount].SlopePerDayGB
328+
$growthPerDay = $seedSlope
329+
$daysUntilFull = [math]::Round($freeGB / $seedSlope, 0)
330+
$growthGB = [math]::Round($seedSlope * $HistoryDays, 2)
331+
$basis = 'BackupHistory'
332+
$conf = 'Low'
333+
}
334+
296335
$status = if ($freePct -le $CriticalThresholdPct) { 'Critical' }
297336
elseif ($freePct -le $WarnThresholdPct) { 'Warning' }
298337
elseif ($daysUntilFull -and $daysUntilFull -le 30) { 'Warning' }
@@ -310,26 +349,28 @@ ORDER BY vs.volume_mount_point;
310349
GrowthPerDayGB = if ($growthPerDay -gt 0) { $growthPerDay } else { $null }
311350
DaysUntilFull = $daysUntilFull
312351
HistoryDays = $HistoryDays
313-
DataPoints = $fc.DataPoints
314-
ForecastConfidence = $fc.Confidence
315-
ForecastBasis = $fc.Basis
352+
DataPoints = $dpoints
353+
ForecastConfidence = $conf
354+
ForecastBasis = $basis
316355
Status = $status
317356
Message = switch ($status)
318357
{
319358
'Critical' { "Kritisch: nur $freePct% frei ($freeGB GB)!" }
320359
'Warning' {
321360
if ($daysUntilFull -and $daysUntilFull -le 30)
322361
{
323-
"Warnung: voll in ca. $daysUntilFull Tagen (Konfidenz $($fc.Confidence))."
362+
$src = if ($basis -eq 'BackupHistory') { 'Bootstrap Backup-Historie' } else { "Konfidenz $conf" }
363+
"Warnung: voll in ca. $daysUntilFull Tagen ($src)."
324364
}
325365
else { "Warnung: nur $freePct% frei ($freeGB GB)." }
326366
}
327367
default {
328-
if ($fc.Basis -ne 'History')
368+
switch ($basis)
329369
{
330-
"OK: $freePct% frei ($freeGB GB). Prognose sammelt noch Daten ($($fc.DataPoints) von $MinDataPoints Laeufen)."
370+
'BackupHistory' { "OK: $freePct% frei ($freeGB GB). Prognose (Bootstrap Backup-Historie): voll in ca. $daysUntilFull Tagen." }
371+
'History' { "OK: $freePct% frei ($freeGB GB)." }
372+
default { "OK: $freePct% frei ($freeGB GB). Prognose sammelt noch Daten ($dpoints von $MinDataPoints Laeufen)." }
331373
}
332-
else { "OK: $freePct% frei ($freeGB GB)." }
333374
}
334375
}
335376
})
@@ -379,7 +420,7 @@ ORDER BY vs.volume_mount_point;
379420
$daysDisplay = if ($e.DaysUntilFull) { $e.DaysUntilFull }
380421
elseif ($e.ForecastBasis -ne 'History') { "$($e.DataPoints)/$MinDataPoints" }
381422
else { '-' }
382-
$confDisplay = if ($e.ForecastBasis -eq 'History') { $e.ForecastConfidence } else { '-' }
423+
$confDisplay = switch ($e.ForecastBasis) { 'History' { $e.ForecastConfidence } 'BackupHistory' { 'Boot' } default { '-' } }
383424
$lines.Add(("{0,-6} {1,-20} {2,-8} {3,-8} {4,-8} {5,-7} {6,-10} {7,-9} {8,-6} {9}" -f
384425
$e.Status, $e.MountPoint, $e.TotalGB, $e.UsedGB, $e.FreeGB,
385426
"$($e.FreePct)%", $perDayDisplay, $daysDisplay, $confDisplay, $e.Message))
@@ -396,7 +437,7 @@ ORDER BY vs.volume_mount_point;
396437
$cls = switch ($e.Status) { 'Critical' { 'crit' } 'Warning' { 'warn' } default { 'ok' } }
397438
$perDayDisplay = if ($e.GrowthPerDayGB) { $e.GrowthPerDayGB } elseif ($e.ForecastBasis -ne 'History') { 'sammelt' } else { 'stabil' }
398439
$daysDisplay = if ($e.DaysUntilFull) { $e.DaysUntilFull } elseif ($e.ForecastBasis -ne 'History') { "$($e.DataPoints)/$MinDataPoints" } else { '-' }
399-
$confDisplay = if ($e.ForecastBasis -eq 'History') { $e.ForecastConfidence } else { '-' }
440+
$confDisplay = switch ($e.ForecastBasis) { 'History' { $e.ForecastConfidence } 'BackupHistory' { 'Boot' } default { '-' } }
400441
$mp = [string]$e.MountPoint -replace '&', '&amp;' -replace '<', '&lt;' -replace '>', '&gt;'
401442
$rowsHtml += "<tr><td class='$cls'>$($e.Status)</td><td>$mp</td><td>$($e.TotalGB)</td><td>$($e.UsedGB)</td><td>$($e.FreeGB)</td><td>$($e.FreePct)%</td><td>$perDayDisplay</td><td>$daysDisplay</td><td>$confDisplay</td></tr>`n"
402443
}
@@ -530,3 +571,79 @@ function Get-sqmVolumeForecast
530571
Basis = 'History'
531572
}
532573
}
574+
575+
# ---------------------------------------------------------------------------
576+
# Private Hilfsfunktion: Bootstrap-Wachstumsrate je Volume aus msdb-Backup-Historie (Methode B2)
577+
# Leitet pro DB den Daten-Wachstumstrend aus Full-Backup-Groessen ab und verteilt die Rate
578+
# proportional zur Datendatei-Groesse auf die Volumes (volume_mount_point). Rueckgabe:
579+
# Hashtable mountpoint -> @{ SlopePerDayGB; Dbs }. Fehler werden an den Aufrufer durchgereicht.
580+
# ---------------------------------------------------------------------------
581+
function Get-sqmBackupGrowthSeed
582+
{
583+
param (
584+
[hashtable]$ConnParams,
585+
[int]$Days = 90,
586+
[object[]]$Volumes
587+
)
588+
589+
$seed = @{ }
590+
591+
$backupQuery = @"
592+
SELECT bs.database_name AS DatabaseName,
593+
bs.backup_finish_date AS FinishDate,
594+
bs.backup_size / 1073741824.0 AS BackupGB
595+
FROM msdb.dbo.backupset bs
596+
WHERE bs.type = 'D' AND bs.is_copy_only = 0 AND bs.backup_size > 0
597+
AND bs.backup_finish_date >= DATEADD(DAY, -$Days, GETDATE())
598+
ORDER BY bs.database_name, bs.backup_finish_date;
599+
"@
600+
$backups = @(Invoke-DbaQuery @ConnParams -Query $backupQuery -EnableException:$true)
601+
if ($backups.Count -eq 0) { return $seed }
602+
603+
# Datendateien (ROWS) -> Volume ueber laengsten Pfad-Praefix der Mount Points.
604+
$fileRows = @(Invoke-DbaQuery @ConnParams -Query "SELECT DB_NAME(database_id) AS DatabaseName, physical_name AS PhysicalName, size * 8.0 / 1024 / 1024 AS FileGB FROM sys.master_files WHERE type = 0" -EnableException:$true)
605+
$mounts = @($Volumes | ForEach-Object { [string]$_.MountPoint } | Sort-Object { $_.Length } -Descending)
606+
607+
$dbMountGB = @{ } # "$db|$mount" -> GB
608+
$dbTotalGB = @{ } # "$db" -> GB
609+
foreach ($fl in $fileRows)
610+
{
611+
$path = [string]$fl.PhysicalName
612+
$mt = $null
613+
foreach ($m in $mounts) { if ($path -and $m -and $path.StartsWith($m, [System.StringComparison]::OrdinalIgnoreCase)) { $mt = $m; break } }
614+
if (-not $mt) { continue }
615+
$g = [double]$fl.FileGB
616+
$db = [string]$fl.DatabaseName
617+
$dbMountGB["$db|$mt"] = [double]($dbMountGB["$db|$mt"]) + $g
618+
$dbTotalGB[$db] = [double]($dbTotalGB[$db]) + $g
619+
}
620+
621+
# Pro DB die Daten-Wachstumsrate (GB/Tag) aus dem Backup-Groessen-Trend bestimmen
622+
# (min. 3 Punkte genuegen fuer einen Bootstrap) und proportional auf die Volumes verteilen.
623+
foreach ($grp in ($backups | Group-Object DatabaseName))
624+
{
625+
$db = [string]$grp.Name
626+
$total = [double]($dbTotalGB[$db])
627+
if ($total -le 0) { continue }
628+
629+
$pts = @($grp.Group | ForEach-Object { [PSCustomObject]@{ Timestamp = [datetime]$_.FinishDate; UsedGB = [double]$_.BackupGB } })
630+
$dbFc = Get-sqmVolumeForecast -History $pts -FreeGB 0 -MinDataPoints 3
631+
if ($dbFc.Basis -ne 'History' -or -not $dbFc.SlopePerDayGB -or $dbFc.SlopePerDayGB -le 0) { continue }
632+
$slope = [double]$dbFc.SlopePerDayGB
633+
634+
foreach ($m in $mounts)
635+
{
636+
$key = "$db|$m"
637+
if ($dbMountGB.ContainsKey($key))
638+
{
639+
$frac = [double]($dbMountGB[$key]) / $total
640+
if (-not $seed.ContainsKey($m)) { $seed[$m] = @{ SlopePerDayGB = 0.0; Dbs = 0 } }
641+
$seed[$m].SlopePerDayGB += $slope * $frac
642+
$seed[$m].Dbs += 1
643+
}
644+
}
645+
}
646+
647+
foreach ($k in @($seed.Keys)) { $seed[$k].SlopePerDayGB = [math]::Round($seed[$k].SlopePerDayGB, 3) }
648+
return $seed
649+
}

sqmSQLTool.psd1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
RootModule = 'sqmSQLTool.psm1'
1818

1919
# Version number of this module.
20-
ModuleVersion = '1.7.0.0'
20+
ModuleVersion = '1.7.1.0'
2121

2222
# ID used to uniquely identify this module
2323
GUID = 'c4b10ba2-aee2-4d8d-ad86-a6e97c346ba6'
@@ -243,7 +243,7 @@
243243
# IconUri = ''
244244

245245
# ReleaseNotes of this module
246-
ReleaseNotes = 'See CHANGELOG.md and GitHub: https://github.com/JankeUwe/sqmSQLTool/releases/tag/v1.7.0.0'
246+
ReleaseNotes = 'See CHANGELOG.md and GitHub: https://github.com/JankeUwe/sqmSQLTool/releases/tag/v1.7.1.0'
247247

248248
# External module dependencies
249249
ExternalModuleDependencies = @('dbatools')

0 commit comments

Comments
 (0)