bin/Public/Invoke-sqmLogShrink.ps1
|
<#
.SYNOPSIS Shrinkt die Transaktions-Logdatei (LDF) einer oder mehrerer Datenbanken. .DESCRIPTION Fuehrt DBCC SHRINKFILE auf die Log-Datei(en) aus. Berechnet die Zielgroesse aus einem Prozentsatz der aktuellen Groesse (ShrinkTargetPercent) mit einem unteren Schwellwert (MinTargetMB). Beruecksichtigt Always On AGs (leitet automatisch zum Primary um). Systemdatenbanken und Offline-Datenbanken werden uebersprungen. Wichtige Hinweise: - Shrink kann nur bis zum aeltesten aktiven VLF verkleinern. - Im FULL Recovery Modell ist vorher ein Log-Backup sinnvoll. - Haeufiges Shrinken fragmentiert die VLFs. .PARAMETER SqlInstance SQL Server-Instanz (Standard: aktueller Computername). Bei AG-Mitglied wird automatisch zum Primary umgeleitet. .PARAMETER SqlCredential Optionales PSCredential fuer die Verbindung. .PARAMETER Database Name(n) der Zieldatenbank(en) (Wildcards erlaubt). Ohne Angabe werden alle Benutzerdatenbanken verarbeitet (entspricht -All). .PARAMETER All Verarbeitet alle Benutzerdatenbanken (exkl. Systemdatenbanken, nur Online). Wird auch implizit verwendet, wenn weder -Database noch -All angegeben wird. .PARAMETER ShrinkTargetPercent Zielgroesse in Prozent der aktuellen Log-Groesse (1-99). Standard: 10. .PARAMETER MinTargetMB Minimale Zielgroesse in MB (Standard: 64 MB). .PARAMETER ContinueOnError Bei Fehler mit naechster Datenbank fortfahren. .PARAMETER EnableException Ausnahmen sofort ausloesen (ueberschreibt ContinueOnError). .PARAMETER Confirm Fordert Bestaetigung vor dem Shrink an. Standardmaessig deaktiviert. .PARAMETER WhatIf Zeigt, was passieren wuerde, ohne den Shrink auszufuehren. .EXAMPLE Invoke-sqmLogShrink -Database "MyDB" -ShrinkTargetPercent 20 .EXAMPLE Invoke-sqmLogShrink -SqlInstance "SQL01" -All -WhatIf .NOTES Voraussetzungen: dbatools, Invoke-sqmLogging. Bei Always On AGs wird automatisch zum Primary umgeleitet. #> function Invoke-sqmLogShrink { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $false, Position = 0)] [string]$SqlInstance = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential]$SqlCredential, [Parameter(Mandatory = $false)] [Alias('DatabaseName')] [string[]]$Database, [Parameter(Mandatory = $false)] [switch]$All, [Parameter(Mandatory = $false)] [ValidateRange(1, 99)] [int]$ShrinkTargetPercent = 10, [Parameter(Mandatory = $false)] [ValidateRange(1, [int]::MaxValue)] [int]$MinTargetMB = 64, [Parameter(Mandatory = $false)] [switch]$ContinueOnError, [Parameter(Mandatory = $false)] [switch]$EnableException ) begin { $functionName = $MyInvocation.MyCommand.Name if (-not $script:dbatoolsAvailable) { $errMsg = "dbatools-Modul nicht gefunden. Bitte installieren: Install-Module dbatools" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" throw $errMsg } $systemDatabases = @('master', 'model', 'msdb', 'tempdb', 'distribution') $results = [System.Collections.Generic.List[PSCustomObject]]::new() $effectiveInstance = $SqlInstance $connParams = @{ SqlInstance = $SqlInstance } if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential } } process { try { # ---- 1. Primary-Validierung bei AGs ---- Invoke-sqmLogging -Message "Pruefe AG-Mitgliedschaft und Primary-Status von '$SqlInstance' ..." -FunctionName $functionName -Level "INFO" $agsOnEntry = Get-DbaAvailabilityGroup @connParams -ErrorAction SilentlyContinue if ($agsOnEntry) { $primaryAgs = @($agsOnEntry | Where-Object { $_.PrimaryReplica -eq $SqlInstance }) $nonPrimaryAgs = @($agsOnEntry | Where-Object { $_.PrimaryReplica -ne $SqlInstance }) if ($primaryAgs.Count -eq 0) { $firstAg = $nonPrimaryAgs[0] $effectiveInstance = $firstAg.PrimaryReplica if (-not $effectiveInstance) { throw "Primary-Replikat fuer AG '$($firstAg.Name)' konnte nicht ermittelt werden." } Invoke-sqmLogging -Message "'$SqlInstance' ist kein Primary. Redirect zu Primary '$effectiveInstance' (AG: '$($firstAg.Name)')." -FunctionName $functionName -Level "WARNING" $connParams = @{ SqlInstance = $effectiveInstance } if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential } } elseif ($nonPrimaryAgs.Count -gt 0) { Invoke-sqmLogging -Message "'$SqlInstance' ist Primary fuer $($primaryAgs.Count) AG(s), aber Secondary fuer $($nonPrimaryAgs.Count) weitere AG(s). SHRINKFILE wird auf '$SqlInstance' ausgefuehrt." -FunctionName $functionName -Level "WARNING" } else { Invoke-sqmLogging -Message "'$SqlInstance' ist Primary fuer alle $($primaryAgs.Count) AG(s)." -FunctionName $functionName -Level "INFO" } } else { Invoke-sqmLogging -Message "'$SqlInstance' ist kein AG-Mitglied (Standalone)." -FunctionName $functionName -Level "INFO" } Invoke-sqmLogging -Message "Effektive Instanz fuer SHRINKFILE: '$effectiveInstance'" -FunctionName $functionName -Level "INFO" # ---- 2. Zieldatenbanken ermitteln ---- $allDatabases = Get-DbaDatabase @connParams -ErrorAction Stop if (-not $allDatabases) { throw "Keine Datenbanken auf '$effectiveInstance' gefunden oder Verbindung fehlgeschlagen." } # Ohne -Database und ohne -All: alle Benutzerdatenbanken (implizites -All) $useAll = $All -or (-not $Database -or $Database.Count -eq 0) $targetDatabases = if ($useAll) { $allDatabases | Where-Object { $_.Name -notin $systemDatabases -and $_.Status -eq 'Normal' } } else { $resolved = [System.Collections.Generic.List[object]]::new() foreach ($pattern in $Database) { if ($pattern -like '*[*?]*') { $matched = $allDatabases | Where-Object { $_.Name -like $pattern } if (-not $matched) { Invoke-sqmLogging -Message "Kein Treffer fuer Muster '$pattern' auf '$effectiveInstance'." -FunctionName $functionName -Level "WARNING" } else { $matched | ForEach-Object { $resolved.Add($_) } } } else { $db = $allDatabases | Where-Object { $_.Name -eq $pattern } if (-not $db) { Invoke-sqmLogging -Message "Datenbank '$pattern' nicht auf '$effectiveInstance' gefunden." -FunctionName $functionName -Level "WARNING" } else { $resolved.Add($db) } } } $resolved | Select-Object -Unique } if (-not $targetDatabases) { throw "Keine Zieldatenbanken zur Verarbeitung gefunden." } Invoke-sqmLogging -Message "$($targetDatabases.Count) Datenbank(en) zur Verarbeitung." -FunctionName $functionName -Level "INFO" # ---- 3. Pro Datenbank: Log-Datei shrinken ---- foreach ($db in $targetDatabases) { $dbName = $db.Name try { if ($dbName -in $systemDatabases) { $msg = "Systemdatenbank '$dbName' wird uebersprungen." Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "WARNING" $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = 'n/a' SizeBefore_MB = 0 TargetSize_MB = 0 SizeAfter_MB = 0 SpaceSaved_MB = 0 ShrinkPercent = 0 Status = 'Skipped' Message = $msg }) continue } if ($db.Status -ne 'Normal') { $msg = "Datenbank '$dbName' ist nicht Online (Status: $($db.Status)). uebersprungen." Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "WARNING" $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = 'n/a' SizeBefore_MB = 0 TargetSize_MB = 0 SizeAfter_MB = 0 SpaceSaved_MB = 0 ShrinkPercent = 0 Status = 'Skipped' Message = $msg }) continue } $logFiles = Get-DbaDbFile @connParams -Database $dbName -ErrorAction Stop | Where-Object { $_.TypeDescription -eq 'LOG' } if (-not $logFiles) { $msg = "Keine Log-Datei fuer Datenbank '$dbName' gefunden." Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "WARNING" $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = 'n/a' SizeBefore_MB = 0 TargetSize_MB = 0 SizeAfter_MB = 0 SpaceSaved_MB = 0 ShrinkPercent = 0 Status = 'Skipped' Message = $msg }) continue } foreach ($logFile in $logFiles) { $logFileName = $logFile.LogicalName $sizeBefore = [math]::Round($logFile.Size.Megabyte, 2) $targetByPct = [math]::Round($sizeBefore * ($ShrinkTargetPercent / 100), 0) $targetSizeMB = [math]::Max($targetByPct, $MinTargetMB) Invoke-sqmLogging -Message "[$dbName / $logFileName] Groesse: ${sizeBefore} MB ? Ziel: ${targetSizeMB} MB ($ShrinkTargetPercent%, Min: ${MinTargetMB} MB)" -FunctionName $functionName -Level "VERBOSE" if ($targetSizeMB -ge $sizeBefore) { $msg = "Log-Datei '$logFileName' ist bereits ${sizeBefore} MB - Zielgroesse ${targetSizeMB} MB erreicht oder ueberschritten. Kein Shrink erforderlich." Invoke-sqmLogging -Message "[$dbName] $msg" -FunctionName $functionName -Level "VERBOSE" $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = $logFileName SizeBefore_MB = $sizeBefore TargetSize_MB = $targetSizeMB SizeAfter_MB = $sizeBefore SpaceSaved_MB = 0 ShrinkPercent = 0 Status = 'NoChangeNeeded' Message = $msg }) continue } $actionMsg = "DBCC SHRINKFILE auf '$logFileName' von ${sizeBefore} MB auf ${targetSizeMB} MB" if ($PSCmdlet.ShouldProcess("$effectiveInstance / $dbName / $logFileName", $actionMsg)) { try { $shrinkQuery = "DBCC SHRINKFILE (N'$logFileName', $targetSizeMB) WITH NO_INFOMSGS;" Invoke-DbaQuery @connParams -Database $dbName -Query $shrinkQuery -EnableException $logFileAfter = Get-DbaDbFile @connParams -Database $dbName -ErrorAction Stop | Where-Object { $_.LogicalName -eq $logFileName } $sizeAfter = [math]::Round($logFileAfter.Size.Megabyte, 2) $spaceSaved = [math]::Round($sizeBefore - $sizeAfter, 2) $shrinkPct = if ($sizeBefore -gt 0) { [math]::Round((($sizeBefore - $sizeAfter) / $sizeBefore) * 100, 1) } else { 0 } $msg = "SHRINKFILE abgeschlossen: ${sizeBefore} MB ? ${sizeAfter} MB (${spaceSaved} MB / ${shrinkPct}% Reduktion)." if ($sizeAfter -gt $targetSizeMB) { $msg += " Zielgroesse ${targetSizeMB} MB wurde nicht erreicht (aktive VLFs verhindern weitere Verkleinerung)." } Invoke-sqmLogging -Message "[$dbName / $logFileName] $msg" -FunctionName $functionName -Level "INFO" $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = $logFileName SizeBefore_MB = $sizeBefore TargetSize_MB = $targetSizeMB SizeAfter_MB = $sizeAfter SpaceSaved_MB = $spaceSaved ShrinkPercent = $shrinkPct Status = 'Shrunk' Message = $msg }) } catch { $errMsg = "Fehler beim Shrink: $($_.Exception.Message)" Invoke-sqmLogging -Message "[$dbName / $logFileName] $errMsg" -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = $logFileName SizeBefore_MB = $sizeBefore TargetSize_MB = $targetSizeMB SizeAfter_MB = $sizeBefore SpaceSaved_MB = 0 ShrinkPercent = 0 Status = 'Failed' Message = $errMsg }) if (-not $ContinueOnError) { throw } } } else { $msg = "WhatIf: Shrink uebersprungen." Invoke-sqmLogging -Message "[$dbName / $logFileName] $msg" -FunctionName $functionName -Level "VERBOSE" $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = $logFileName SizeBefore_MB = $sizeBefore TargetSize_MB = $targetSizeMB SizeAfter_MB = $sizeBefore SpaceSaved_MB = 0 ShrinkPercent = 0 Status = 'WhatIfSkipped' Message = "WhatIf: Shrink wuerde ausgefuehrt werden." }) } } } catch { $errMsg = "Fehler bei Datenbank '$dbName': $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" $results.Add([PSCustomObject]@{ SqlInstance = $effectiveInstance Database = $dbName LogFile = 'n/a' SizeBefore_MB = 0 TargetSize_MB = 0 SizeAfter_MB = 0 SpaceSaved_MB = 0 ShrinkPercent = 0 Status = 'Failed' Message = $errMsg }) if ($EnableException) { throw } if (-not $ContinueOnError) { throw } } } $cntShrunk = ($results | Where-Object Status -eq 'Shrunk').Count $cntNoChange = ($results | Where-Object Status -eq 'NoChangeNeeded').Count $cntSkipped = ($results | Where-Object Status -eq 'Skipped').Count $cntFailed = ($results | Where-Object Status -eq 'Failed').Count $totalSaved = [math]::Round(($results | Measure-Object SpaceSaved_MB -Sum).Sum, 2) Invoke-sqmLogging -Message "$functionName abgeschlossen. Shrunk: $cntShrunk, NoChange: $cntNoChange, Skipped: $cntSkipped, Failed: $cntFailed, Gesamt eingespart: ${totalSaved} MB" -FunctionName $functionName -Level "INFO" } catch { $errMsg = "Schwerer Fehler: $($_.Exception.Message)" Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR" if ($EnableException) { throw } throw $errMsg } } end { return $results } } |