Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions samples/features/shrink/shrink-driver/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Changelog

All notable changes to the ShrinkDriver sample are documented in this file.

## [1.1.0] - 2026-08-27

### Changed

- A shrink blocked by the database low watermark not advancing because of an open
transaction or other reasons (error 49537) now ends shrink for that file with a distinct
"run shrink again later" outcome, instead of retrying it as a generic transient error.
The shrink of other files in the same run continues.

## [1.0.0] - 2026-07-16

### Added

- `Invoke-ShrinkDriver`: reclaims allocated but unused space from a database's `ROWS`
data files by running `DBCC SHRINKFILE` on multiple sessions in parallel.
- `Report` mode (default) and `Shrink` mode. `Report` needs only a connection; `Shrink`
needs `db_owner` or `sysadmin`.
- Incremental, step-based shrinking toward an optional per-file target size
(`-FileTargetSizeGiB`, `-StepGiB`), skipping files with less than `-MinReclaimGiB` of
reclaimable space.
- `-TruncateOnly` (release tail free space only) and `-NoTruncate` (repack only) modes.
- `WAIT_AT_LOW_PRIORITY` support (`-WaitAtLowPriority`, `-AbortAfterWait`).
- Transient-failure retries with exponential backoff and full jitter, plus a
connection-level retry provider and an outer reconnect loop that rides out an Azure SQL
restart or failover.
- Stuck detection: a shrink blocked or making no progress for `-StuckWindowSeconds` is
cancelled and retried.
- Graceful shutdown: two-stage Ctrl+C and an optional `-MaxRuntimeMinutes`, both of which
reassess in-flight files to real outcomes.
- Per-file outcome buckets and an end-of-run advisory listing files that still have
reclaimable space, so a follow-up run can recover more.
- A periodic status report written to the console and a log file, with a structured
result object available via `-PassThru`.
- Entra ID, Windows, and SQL authentication; validate-first connections with an opt-in
`-TrustServerCertificate` fallback; a secure `SecureString` password prompt.
- Support for SQL Server 2022 or later, Azure SQL Managed Instance, and Azure SQL Database.
53 changes: 49 additions & 4 deletions samples/features/shrink/shrink-driver/src/ShrinkDriver.ps1
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
<#PSScriptInfo
.VERSION 1.0.0
.VERSION 1.1.0
.GUID 1b801c08-f374-45df-ba3e-34d9983e9133
.AUTHOR Microsoft
.COMPANYNAME Microsoft
.COPYRIGHT (c) Microsoft Corporation. Licensed under the MIT License.
.TAGS SQL SQLServer AzureSQL DBCC SHRINKFILE shrink maintenance
.LICENSEURI https://github.com/microsoft/sql-server-samples/blob/master/license.txt
.PROJECTURI https://github.com/microsoft/sql-server-samples
.RELEASENOTES
Initial version.
#>

<#
Expand Down Expand Up @@ -557,6 +555,39 @@ SELECT CAST(SERVERPROPERTY('EngineEdition') AS int) AS engine_edition,
try { if ($rd.Read()) { @{ Alloc = [long]$rd['a']; Used = [long]$rd['u'] } } else { $null } }
finally { $rd.Dispose(); $cmd.Dispose() }
}
function Get-ShrinkOldestTran($conn) {
# Oldest active transaction in this database; on Hyperscale it can hold the low watermark
# that blocks shrink.
$cmd = $conn.CreateCommand()
$cmd.CommandText = @'
SELECT TOP (1) s.session_id AS spid,
DATEDIFF(SECOND, dt.database_transaction_begin_time, SYSDATETIME()) AS age_s,
s.program_name AS prog,
snap.is_snapshot
FROM sys.dm_tran_database_transactions AS dt
JOIN sys.dm_tran_session_transactions AS st ON st.transaction_id = dt.transaction_id
JOIN sys.dm_exec_sessions AS s ON s.session_id = st.session_id
LEFT JOIN sys.dm_tran_active_snapshot_database_transactions AS snap ON snap.transaction_id = dt.transaction_id
WHERE dt.database_id = DB_ID() AND dt.database_transaction_begin_time IS NOT NULL
ORDER BY dt.database_transaction_begin_time ASC;
'@
try {
$rd = $cmd.ExecuteReader()
try {
if ($rd.Read()) {
$prog = if ($rd['prog'] -is [DBNull]) { '' } else { [string]$rd['prog'] }
$who = if ($prog) { "session $([int]$rd['spid']), $prog" } else { "session $([int]$rd['spid'])" }
$snapTag = ''
if (-not ($rd['is_snapshot'] -is [DBNull])) {
$snapTag = if ([int]$rd['is_snapshot'] -eq 1) { '; snapshot isolation transaction holding row versions' } else { '; transaction holding row versions' }
}
$age = Format-ShrinkDuration -TimeSpan ([TimeSpan]::FromSeconds([int]$rd['age_s']))
"oldest active transaction in the database is $age old ($who)$snapTag"
} else { $null }
} finally { $rd.Dispose() }
} catch { $null }
finally { $cmd.Dispose() }
}
function Get-WConnResilient {
# Re-establish the worker connection, tolerating a server that is briefly offline (for
# example during an Azure SQL restart or failover). New-WConn's connection-level retry
Expand Down Expand Up @@ -756,13 +787,27 @@ SELECT CAST(SERVERPROPERTY('EngineEdition') AS int) AS engine_edition,
if ($num -eq 5201) {
if ($before -lt $startAlloc) {
$bucket = 'Shrunk'
Emit "File $($file.FileId) shrunk to $(Format-ShrinkSize $before) (MSSQL 5201: no more reclaimable space)"
Emit "File $($file.FileId) shrunk to $(Format-ShrinkSize $before) (MSSQL error 5201: no more reclaimable space)"
} else {
$bucket = 'AlreadyMinimal'
Emit "File $($file.FileId) cannot be shrunk (MSSQL error 5201: no reclaimable space)"
}
break
}
if ($num -eq 49537) {
# 49537: a page shrink must relocate is pinned by the database low watermark - a
# long-running transaction on the primary (possibly another concurrent shrink moving
# pages) or a lagging/long-queried secondary. The engine has already exhausted its
# own retries and the watermark rarely advances within a client retry, so stop and let
# a later run reclaim the space once the watermark moves.
$after = if ($conn.State -eq 'Open') { (Get-Size $conn $file.FileId).Alloc } else { $before }
$held = if ($conn.State -eq 'Open') { Get-ShrinkOldestTran $conn } else { $null }
$bucket = Get-ShrinkGaveUpBucket -StartAllocMB $startAlloc -FinalAllocMB $after
$bucketReason = 'blocked by the database low watermark (MSSQL error 49537): a page to be moved is pinned by a long-running transaction or a lagging secondary; run shrink again later'
if ($held) { $bucketReason += "; $held" }
Emit ("File $($file.FileId) blocked by the low watermark (MSSQL error 49537); run shrink again later" + $(if ($held) { " ($held)" } else { '' }))
break
}
if ($conn.State -ne 'Open') {
# The connection dropped (server restart, failover, or the client machine
# sleeping). Reconnecting is not a per-file failure, so it does not count
Expand Down