Last active
August 25, 2026 12:09
-
-
Save Digiover/e6a49daf927e68baf0ecb03c3f144b2d to your computer and use it in GitHub Desktop.
Install or update SQL Server Management Studio automatically using this fire-and-forget method
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Installs or updates SQL Server Management Studio (SSMS) 21 or later, unattended. | |
| .DESCRIPTION | |
| Starting with SSMS 21, SQL Server Management Studio ships as part of the Visual Studio | |
| installer family. There is no standalone MSI any more. The download is a ~5 MB stub | |
| installer (bootstrapper) named vs_SSMS.exe, which hands the actual work to the Visual | |
| Studio Installer. | |
| That breaks every script written for SSMS 20 and earlier: | |
| - The old setup accepted /Install /Quiet. The bootstrapper wants --quiet --wait --norestart. | |
| - Without --wait the bootstrapper returns immediately while the install continues in the | |
| background, so a zero exit code means nothing. | |
| - SSMS 21 and later register in the uninstall registry as "SQL Server Management Studio 21", | |
| without the "Microsoft " prefix that SSMS 17 to 20 used. A filter on | |
| "Microsoft SQL Server Management Studio*" silently finds nothing. | |
| - VersionMajor is empty for those entries, and the install location moved from | |
| "C:\Program Files (x86)\Microsoft SQL Server Management Studio <n>" to | |
| "C:\Program Files\Microsoft SQL Server Management Studio <n>\Release". | |
| - UninstallString points at the Visual Studio Installer (setup.exe) with its own | |
| "uninstall --installPath" arguments, which are not /uninstall /quiet. | |
| This script handles both worlds. It detects what is installed, updates an existing | |
| SSMS 21+ installation in place, installs it side by side when the requested major | |
| version is missing, and can optionally remove a legacy SSMS 20 or earlier install. | |
| Uninstall-then-install, the approach that was needed in the MSI era, is no longer | |
| required: SSMS 21+ supports side-by-side installs and has an "update" verb. | |
| .PARAMETER Major | |
| Major SSMS version to install or update. Defaults to 22. | |
| .PARAMETER Channel | |
| Release channel to pull the bootstrapper from. Defaults to release. | |
| .PARAMETER BootstrapperUri | |
| Overrides the download URI completely. Use this to pin a specific servicing release from | |
| the SSMS release history page. | |
| .PARAMETER InstallPath | |
| Optional install directory, passed as --installPath. Can only be set on first install. | |
| .PARAMETER Add | |
| Optional workload or component IDs to add, for example | |
| Microsoft.SqlServer.Workload.SSMS.CodeTools. Repeatable. | |
| .PARAMETER IncludeRecommended | |
| Adds --includeRecommended, pulling in the recommended components of the selected workloads. | |
| .PARAMETER RemoveLegacy | |
| Also uninstall any SSMS 20 or earlier (MSI era) installation that is found. | |
| .PARAMETER Force | |
| Passes --force, which closes a running SSMS instead of failing with exit code 1003 or 8006. | |
| This can lose unsaved work. | |
| .PARAMETER KeepBootstrapper | |
| Keeps the downloaded vs_SSMS.exe instead of deleting it afterwards. | |
| .EXAMPLE | |
| .\Update-SqlServerManagementStudio.ps1 | |
| Installs or updates SSMS 22 silently. | |
| .EXAMPLE | |
| .\Update-SqlServerManagementStudio.ps1 -Major 22 -RemoveLegacy -Force | |
| Removes an old SSMS 20 or earlier, then installs SSMS 22, closing SSMS if it is running. | |
| .EXAMPLE | |
| .\Update-SqlServerManagementStudio.ps1 -Add Microsoft.SqlServer.Workload.SSMS.CodeTools -IncludeRecommended | |
| Installs SSMS 22 including the Code Tools workload and its recommended components. | |
| .NOTES | |
| Update of: https://www.saotn.org/windows-server/update-sql-server-management-studio-ssms-automatically/ | |
| Requires an elevated session. Visual Studio Installer operations need administrator rights. | |
| Twitter / X: @Jan_Reilink | |
| #> | |
| #Requires -Version 5.1 | |
| #Requires -RunAsAdministrator | |
| [CmdletBinding(SupportsShouldProcess)] | |
| param( | |
| [ValidateRange(21, 99)] | |
| [int]$Major = 22, | |
| [ValidateSet('release', 'preview')] | |
| [string]$Channel = 'release', | |
| [uri]$BootstrapperUri, | |
| [string]$InstallPath, | |
| [string[]]$Add, | |
| [switch]$IncludeRecommended, | |
| [switch]$RemoveLegacy, | |
| [switch]$Force, | |
| [switch]$KeepBootstrapper | |
| ) | |
| Set-StrictMode -Version Latest | |
| $ErrorActionPreference = 'Stop' | |
| # Documented Visual Studio Installer error codes. See the SSMS command-line parameters page. | |
| $script:ExitCodes = @{ | |
| 0 = 'Operation completed successfully' | |
| 740 = 'Elevation required' | |
| 1001 = 'Visual Studio installer process is running' | |
| 1003 = 'SSMS is in use' | |
| 1602 = 'Operation was canceled' | |
| 1618 = 'Another installation running' | |
| 1641 = 'Operation completed successfully, and reboot was initiated' | |
| 3010 = 'Operation completed successfully, but install requires reboot before it can be used' | |
| 5003 = 'Bootstrapper failed to download installer' | |
| 5004 = 'Operation was canceled' | |
| 5005 = 'Bootstrapper command-line parse error' | |
| 5007 = "Operation was blocked - the computer doesn't meet the requirements" | |
| 8001 = 'Arm machine check failure' | |
| 8002 = 'Background download precheck failure' | |
| 8003 = 'Out of support selectable failure' | |
| 8004 = 'Target directory failure' | |
| 8005 = 'Verifying source payloads failure' | |
| 8006 = 'SSMS processes running' | |
| 8010 = 'Operating system not supported' | |
| -1073720687 = 'Connectivity failure' | |
| -1073741510 = 'Microsoft Visual Studio Installer was terminated' | |
| } | |
| # 0 = done, 1641 = done and rebooting, 3010 = done but needs a reboot first. | |
| $script:SuccessCodes = @(0, 1641, 3010) | |
| function Get-RegistryValue { | |
| <# | |
| Reads one property off a registry object, returning $null when it is absent. | |
| Get-ItemProperty only materialises properties for values that actually exist, so plenty of | |
| uninstall keys have no DisplayName, InstallLocation or UninstallString at all. Under | |
| Set-StrictMode -Version Latest a direct $_.DisplayName on such a key is a terminating error, | |
| not an empty string. | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)][AllowNull()]$InputObject, | |
| [Parameter(Mandatory)][string]$Name | |
| ) | |
| if ($null -eq $InputObject) { return $null } | |
| $property = $InputObject.PSObject.Properties[$Name] | |
| if ($null -eq $property) { return $null } | |
| return $property.Value | |
| } | |
| function Get-SsmsInstallation { | |
| <# | |
| Returns every SQL Server Management Studio installation found in the uninstall registry. | |
| InstallerType tells the two generations apart: | |
| VisualStudio = SSMS 21 and later, driven by the Visual Studio Installer | |
| Legacy = SSMS 20 and earlier, the standalone SSMS-Setup-ENU.exe installer | |
| Note the DisplayName filter: it deliberately matches with and without the "Microsoft " | |
| prefix, because SSMS 21+ dropped it. It does not use a wildcard on "SSMS", which would | |
| also catch unrelated entries such as vs_ssmsprotocolselectormsi. | |
| #> | |
| [CmdletBinding()] | |
| param() | |
| $uninstallKeys = @( | |
| 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' | |
| 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' | |
| ) | |
| $entries = @( | |
| Get-ItemProperty -Path $uninstallKeys -ErrorAction SilentlyContinue | | |
| Where-Object { (Get-RegistryValue -InputObject $_ -Name 'DisplayName') -like '*SQL Server Management Studio*' } | |
| ) | |
| foreach ($entry in $entries) { | |
| $displayName = Get-RegistryValue -InputObject $entry -Name 'DisplayName' | |
| $displayVersion = Get-RegistryValue -InputObject $entry -Name 'DisplayVersion' | |
| $installLocation = Get-RegistryValue -InputObject $entry -Name 'InstallLocation' | |
| $uninstallString = Get-RegistryValue -InputObject $entry -Name 'UninstallString' | |
| $isVisualStudio = $uninstallString -match 'Microsoft Visual Studio\\Installer\\setup\.exe' | |
| # VersionMajor is empty for the Visual Studio Installer entries, so derive the major | |
| # version from the display name first and fall back to the install location. | |
| $major = $null | |
| if ($displayName -match 'Management Studio\D*(\d+)') { | |
| $major = [int]$Matches[1] | |
| } | |
| elseif ($installLocation -and $installLocation -match 'Management Studio\D*(\d+)') { | |
| $major = [int]$Matches[1] | |
| } | |
| [pscustomobject]@{ | |
| DisplayName = $displayName | |
| Major = $major | |
| Version = $displayVersion | |
| InstallLocation = $installLocation | |
| UninstallString = $uninstallString | |
| InstallerType = if ($isVisualStudio) { 'VisualStudio' } else { 'Legacy' } | |
| } | |
| } | |
| } | |
| function Get-SsmsBootstrapper { | |
| <# | |
| Downloads the vs_SSMS.exe stub installer and returns the path to it. | |
| aka.ms/ssmsfullsetup redirects to the current major version, which makes it a moving | |
| target for a script that has to decide between install and update. The per-version URI | |
| is deterministic, so that is the default. | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)][uri]$Uri, | |
| [Parameter(Mandatory)][string]$Destination | |
| ) | |
| Write-Host "Downloading stub installer from $Uri" | |
| $previousProgress = $ProgressPreference | |
| $ProgressPreference = 'SilentlyContinue' | |
| try { | |
| Invoke-WebRequest -Uri $Uri -OutFile $Destination -UseBasicParsing | |
| } | |
| finally { | |
| $ProgressPreference = $previousProgress | |
| } | |
| $item = Get-Item -LiteralPath $Destination | |
| # The product version of the bootstrapper tells you which channel and version it installs. | |
| $productVersion = $item.VersionInfo.ProductVersion | |
| Write-Host ("Downloaded {0} ({1:N1} MB, product version {2})" -f $item.Name, ($item.Length / 1MB), $productVersion) | |
| return $item.FullName | |
| } | |
| function Invoke-SsmsSetup { | |
| <# | |
| Runs the bootstrapper or the Visual Studio Installer and reports the documented exit code. | |
| Returns $true when the operation succeeded. | |
| --wait matters: it is the only reason the exit code means anything. Without it the | |
| bootstrapper returns as soon as it has handed off to the Visual Studio Installer. | |
| Note that --wait is a bootstrapper-only parameter; setup.exe does not support it, which | |
| is why the caller relies on Start-Process -Wait there. | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)][string]$FilePath, | |
| [Parameter(Mandatory)][string[]]$Arguments | |
| ) | |
| Write-Host "Running: `"$FilePath`" $($Arguments -join ' ')" | |
| $process = Start-Process -FilePath $FilePath -ArgumentList $Arguments -Wait -PassThru | |
| $code = $process.ExitCode | |
| $meaning = if ($script:ExitCodes.ContainsKey($code)) { $script:ExitCodes[$code] } else { 'Failure condition occurred - check the logs in %TEMP% (ssms_bootstrapper*, ssms_client*, ssms_setup*)' } | |
| if ($script:SuccessCodes -contains $code) { | |
| Write-Host "Exit code ${code}: $meaning" | |
| return $true | |
| } | |
| Write-Warning "Exit code ${code}: $meaning" | |
| return $false | |
| } | |
| function Remove-LegacySsms { | |
| <# | |
| Uninstalls an SSMS 20 or earlier installation. Those still use the standalone installer | |
| that accepts single-dash switches. Returns $true when everything was removed. | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [Parameter(Mandatory)][pscustomobject[]]$Installation | |
| ) | |
| $allRemoved = $true | |
| foreach ($ssms in $Installation) { | |
| Write-Host "Removing legacy installation: $($ssms.DisplayName)" | |
| if ([string]::IsNullOrWhiteSpace($ssms.UninstallString)) { | |
| Write-Warning "No UninstallString for $($ssms.DisplayName), skipping." | |
| $allRemoved = $false | |
| continue | |
| } | |
| # Workaround kept from the original script: the uninstall of some older SSMS builds | |
| # trips over this leftover binary. VersionMajor is unreliable, so glob the folder. | |
| $deploymentExe = @( | |
| Get-ChildItem -Path 'C:\Program Files (x86)\Microsoft SQL Server Management Studio *\Common7\IDE\Microsoft.AnalysisServices.Deployment.exe' -ErrorAction SilentlyContinue | |
| ) | |
| foreach ($exe in $deploymentExe) { | |
| Write-Host "Removing $($exe.FullName)" | |
| Remove-Item -LiteralPath $exe.FullName -Force -ErrorAction SilentlyContinue | |
| } | |
| $executable = $ssms.UninstallString.Split('"')[1] | |
| if ([string]::IsNullOrWhiteSpace($executable)) { | |
| $executable = $ssms.UninstallString | |
| } | |
| if (-not (Invoke-SsmsSetup -FilePath $executable -Arguments @('/uninstall', '/quiet', '/norestart'))) { | |
| $allRemoved = $false | |
| } | |
| } | |
| return $allRemoved | |
| } | |
| # --------------------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------------------- | |
| if (-not $BootstrapperUri) { | |
| $BootstrapperUri = "https://aka.ms/ssms/$Major/$Channel/vs_SSMS.exe" | |
| } | |
| # @() around the call: a single match would otherwise unroll into a bare object and the | |
| # .Count and array operations below would behave differently. | |
| $installations = @(Get-SsmsInstallation) | |
| if ($installations.Count -eq 0) { | |
| Write-Host 'No SQL Server Management Studio installation found.' | |
| } | |
| else { | |
| Write-Host 'Found the following SQL Server Management Studio installations:' | |
| $installations | | |
| Select-Object DisplayName, Version, InstallerType, InstallLocation | | |
| Format-Table -AutoSize | | |
| Out-String | | |
| Write-Host | |
| } | |
| $legacy = @($installations | Where-Object { $_.InstallerType -eq 'Legacy' }) | |
| $target = @($installations | Where-Object { $_.InstallerType -eq 'VisualStudio' -and $_.Major -eq $Major }) | |
| if ($RemoveLegacy -and $legacy.Count -gt 0) { | |
| if ($PSCmdlet.ShouldProcess(($legacy.DisplayName -join ', '), 'Uninstall legacy SSMS')) { | |
| if (-not (Remove-LegacySsms -Installation $legacy)) { | |
| Write-Warning 'Removing the legacy installation did not fully succeed. Continuing anyway.' | |
| } | |
| } | |
| } | |
| elseif ($legacy.Count -gt 0) { | |
| Write-Host "Legacy installation present ($($legacy.DisplayName -join ', ')). It can stay side by side; pass -RemoveLegacy to uninstall it." | |
| } | |
| $bootstrapperPath = Join-Path -Path $env:TEMP -ChildPath 'vs_SSMS.exe' | |
| try { | |
| if (-not $PSCmdlet.ShouldProcess("SSMS $Major ($Channel)", $(if ($target.Count -gt 0) { 'Update' } else { 'Install' }))) { | |
| return | |
| } | |
| $bootstrapperPath = Get-SsmsBootstrapper -Uri $BootstrapperUri -Destination $bootstrapperPath | |
| # An existing install of this major version gets the update verb. A different major | |
| # version (SSMS 21 next to 22) is a separate product and lands side by side. | |
| $arguments = [System.Collections.Generic.List[string]]::new() | |
| if ($target.Count -gt 0) { | |
| Write-Host "SSMS $Major is already installed ($($target[0].Version)). Updating in place." | |
| $arguments.Add('update') | |
| # For update the install path is required to identify the instance to act upon. | |
| if ($target[0].InstallLocation) { | |
| $arguments.Add('--installPath') | |
| $arguments.Add($target[0].InstallLocation) | |
| } | |
| } | |
| else { | |
| Write-Host "SSMS $Major is not installed. Performing a fresh install." | |
| if ($InstallPath) { | |
| $arguments.Add('--installPath') | |
| $arguments.Add($InstallPath) | |
| } | |
| foreach ($id in $Add) { | |
| $arguments.Add('--add') | |
| $arguments.Add($id) | |
| } | |
| if ($IncludeRecommended) { | |
| $arguments.Add('--includeRecommended') | |
| } | |
| } | |
| $arguments.Add('--quiet') | |
| $arguments.Add('--wait') | |
| $arguments.Add('--norestart') | |
| if ($Force) { | |
| $arguments.Add('--force') | |
| } | |
| if (Invoke-SsmsSetup -FilePath $bootstrapperPath -Arguments $arguments.ToArray()) { | |
| $result = @(Get-SsmsInstallation | Where-Object { $_.InstallerType -eq 'VisualStudio' -and $_.Major -eq $Major }) | |
| if ($result.Count -gt 0) { | |
| Write-Host "SQL Server Management Studio $Major is now at version $($result[0].Version)." | |
| } | |
| else { | |
| Write-Warning "Setup reported success but SSMS $Major was not found in the registry afterwards." | |
| } | |
| } | |
| else { | |
| Write-Warning 'SQL Server Management Studio setup did not complete successfully.' | |
| } | |
| } | |
| finally { | |
| if (-not $KeepBootstrapper -and (Test-Path -LiteralPath $bootstrapperPath)) { | |
| Remove-Item -LiteralPath $bootstrapperPath -Force -ErrorAction SilentlyContinue | |
| } | |
| } |
Author
Author
Since SQL Server Management Studio 21, the URL https://aka.ms/ssmsfullsetup redirects to a stub installer file vs_SSMS.exe, breaking the installation part of the previous version of this script. Here is an updated version.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The fire-and-forget method to update SQL Server Management Studio (SSMS) automatically by uninstalling any installed version and downloading & installing the newest available version from Microsoft.
Blogpost: https://www.saotn.org/windows-server/update-sql-server-management-studio-ssms-automatically/