What extra do I need to access SQL Server from PowerShell?
You don’t need anything additional in the way of .NET libraries or PowerShell modules to work with SQL Server, but for many scripting tasks you can get a lot of extra value from using them.
The approach that you choose towards accessing SQL Server depends on the sort of task you need to produce a script for.
- If you need to just perform queries against SQL Server databases and save the results to a file, then we can just run SQLCMD or BCP from PowerShell.
- You can choose to load the .NET data provider. This gives you a lot more versatility, but you are working at a lower level, having to create in code what is already built into SQLCMD
- You can load SMO or even SQLPS, the SQL Server provider that uses SMO behind the scenes. SMO is used primarily to provide all the functionality for SQL Server Management Studio. Anything you can do in SSMS you can also automate in SMO.
- With SQLPS, you can read the output from the cmdlet Invoke-sqlcmd into PowerShell’s native data objects, and you can process the results and save data to file in a number of formats such as XML
The same is true of outputting data from PowerShell. The built-in features of PowerShell are enough to get you a long way, but what you use depends on your task. If you need more …
- A lot can be done with command-line tools such as BCP
- You can use modules or functions to provide extra, such as writing out in Excel spreadsheet format
This article is all about how you get started with all these approaches, and about how you can load and use the modules and assemblies to do this safely and effectively.
The Various methods of working with SQL Server
Running an External command-line Program
You can run external command-line programs such as OSQL , BCP or SQLCMD. You can call those external programs inside PowerShell and then use them to query the SQL Server instance:
This example will query the test table and redirect the output to a txt file
1 2 3 4 5 6 7 8 9 10 |
$ServerInstance = 'DeathStar' $Database = 'Dagobah' $Query = 'Select * from test' $SqlcmdOptions = @" -S"$ServerInstance" -d "$Database" -Q "$Query" "@ Start-Process -FilePath "sqlcmd.exe" -ArgumentList @" $SqlcmdOptions "@ -Wait -NoNewWindow -RedirectStandardOutput C:\temp\SQLCMD.txt -PassThru |
Another approach is to use BCP utility which allows us very fast import or export of a table, view or query. In the following example we are exporting the product table from AdventureWorks2014:
1 2 3 4 5 6 7 8 9 10 11 |
$ServerInstance = 'DeathStar' $Database = 'Dagobah' $TableExport = 'AdventureWorks2014.Production.Product' $FileExport = 'C:\temp\Product.dat' $BCPOptions = @" "$TableExport" out "$FileExport" -S "$ServerInstance" -T -n "@ Start-Process -FilePath "bcp.exe" -ArgumentList @" $BCPOptions "@ -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\temp\BCPError.txt |
Something to be aware of is that, if we get an error using this coding style, it will not be visible. The output will be the same. The difference will be that the BCPError.txt will have the error. Let’s export a table that does not exists called IdontExist so as to deliberately cause an error:
1 2 3 4 5 6 7 8 9 10 11 |
$ServerInstance = 'DeathStar' $Database = 'Dagobah' $TableExport = 'AdventureWorks2014.IDontExist' $FileExport = 'C:\temp\Product.dat' $BCPOptions = @" "$TableExport" out "$FileExport" -S "$ServerInstance" -T -n "@ Start-Process -FilePath "bcp.exe" -ArgumentList @" $BCPOptions "@ -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\temp\BCPError.txt |
‘So Laerte, how can I test in my code if it was successful or not?’
One approach is to use a property that is returned by the start-process called ExitCode. If it is 0, it was running with no errors and 1, of course, an error occurred.
Saving the output in a variable:
1 2 3 4 5 |
$Output = Start-Process -FilePath "bcp.exe" -ArgumentList @" $BCPOptions "@ -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\temp\BCPError.txt $Output.ExitCode |
1 2 3 4 5 6 7 8 9 10 |
PS C:\WINDOWS\system32> . 'C:\temp\Untitled15.ps1' <# script is not saved yet #> Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id SI ProcessName ------- ------ ----- ----- ----- ------ -- -- ----------- 23 3 384 1988 11 0.00 11148 3 bcp PS C:\WINDOWS\system32> . 'C:\temp\Untitled15.ps1' <# script is not saved yet #> 1 |
Saving only the ExitCode property:
1 2 3 4 5 |
$Output = (Start-Process -FilePath "bcp.exe" -ArgumentList @" $BCPOptions "@ -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\temp\BCPError.txt).ExitCode $Output |
Or even, testing the property directly:
1 2 3 4 5 |
if ((Start-Process -FilePath "bcp.exe" -ArgumentList @" $BCPOptions "@ -Wait -NoNewWindow -PassThru -RedirectStandardOutput C:\temp\BCPError.txt).ExitCode) { Write-Error "Some Error ocurred. Check the BCPError.txt file" } |
1 2 3 |
: Some Error ocurred. Check the BCPError.txt file + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException |
You can read more about BCP here: Working with the bcp Command-line Utility
Querying SQL Server via the .NET data provider
If you don’t need SMO for any other reason, you can query SQL metadata or data, or to work with any kind of SQL Server Object, by using the System.Data.SqlClient Namespace – the .NET Framework Data Provider for SQL Server. It will allow you to use T-SQL commands and then manage what you need in much the same way as you would with the SMO database connection methods. It does not require you to load anything extra, because the namespace is already loaded by PowerShell.
In your test database, execute this…
1 2 |
Create table test (id int) insert into test values (1),(2),(3) |
And then, in PowerShell, amend the server instance name and database, and run the script
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$Server = 'DeathStar' $database = 'Dagobah' $Connection = New-Object System.Data.SQLClient.SQLConnection $Connection.ConnectionString = "server=$($Server);database=$($Database);trusted_connection=true;" $Connection.Open() $Command = New-Object System.Data.SQLClient.SQLCommand $Command.Connection = $Connection $Command.CommandText = 'Select * from test' $Reader = $Command.ExecuteReader() $Datatable = New-Object System.Data.DataTable $Datatable.Load($Reader) $Datatable $Connection.Close() |
If your result set returns more than one table, you can use the DataAdapter and Dataset class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Create table test (id int) insert into test values (1),(2),(3) Create table test1 (id int) insert into test1 values (4),(5),(6),(7) $Connection = New-Object System.Data.SQLClient.SQLConnection $Connection.ConnectionString = "server=$($Server);database=$($Database);trusted_connection=true;" $Connection.Open() $Command = New-Object System.Data.SQLClient.SQLCommand $Command.Connection = $Connection $Command.CommandText = 'Select * from test;Select * from test1' $DataAdapter = new-object System.Data.SqlClient.SqlDataAdapter $Command $Dataset = new-object System.Data.Dataset $DataAdapter.Fill($Dataset) $DataSet.Tables[0] $DataSet.Tables[1] $Connection.Close() |
In addition, you may want to execute a stored procedure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Create procedure usp_foo as Select * from test1 $Server = 'DeathStar' $database = 'Dagobah' $Connection = New-Object System.Data.SQLClient.SQLConnection $Connection.ConnectionString = "server=$($Server);database=$($Database);trusted_connection=true;" $Connection.Open() $Command = New-Object System.Data.SQLClient.SQLCommand $Command.Connection = $Connection $Command.CommandText = 'exec usp_foo' $Reader = $Command.ExecuteReader() $Datatable = New-Object System.Data.DataTable $Datatable.Load($Reader) $Datatable.Rows $Connection.Close() |
Using SMO
SMO is a .NET library that enables you to work with SQL Server as objects, classes, methods and so on. When you access a database on a server, the database object provides an open System.Data.SqlClient connection that is exposed by the Database.ExecuteNonQuery and Database.ExecuteWithResults methods that you can use for querying either the server or a database. You can use it in the same way that you use any other SqlClient connection. Check out my Stairway to Server Management Objects (SMO) Level 1: Concepts and Basics on SQLServerCentral.com.
Unlike the previous method, you need to load the SMO assembly before you can use SMO. The traditional way of doing this is to use the .NET method LoadWithPartialName.
1 |
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null |
This isn’t ideal, and we’ll be discussing refinements to this method later on in the article.
Using SQLPS
If you use SQLPS, you will not only be able to use SMO’s own .NET connection for querying databases but you will also have the SQL CmdLet’s. Everytrhing you need is loaded for you under the covers.The Cmdlet to use for executing SQL is Invoke-SQLCMD, but you can get a complete list of CmdLet’s that are available for use once you have loaded SQLPS by using the PowerShell command …
1 |
Get-Command -Module SQLPS |
Invoke-SQLCMD
Invoke-SQLCMD is a compiled cmdlet from SQLPS.
1 2 |
import-module sqlps -DisableNameChecking Invoke-Sqlcmd -query 'Select * from test' -ServerInstance deathstar -Database dagobah |
You can also run several .SQL files.
For that let’s use the amazing SQL Server Diagnostic Information Queries for September 2014 from Glenn Berry, copy some SQL from there and split into .sql files. I´ve created three .sql files: FileNamesPaths.SQL, ServerProperties.SQL and SQLServerAgentjobsCategory.SQL
One approach is using the :r parameter inside a .sql file. First create a .sql file with the .SQL files to run: Let’s call this file SQLFiles.SQL
1 2 3 4 5 6 7 8 9 10 11 |
PRINT 'executing the first file' go :r C:\temp\FileNamesPaths.SQL :On Error exit PRINT 'Executing the second file' go :r C:\temp\ServerProperties.SQL :On Error exit PRINT 'Executing the last file' go :r C:\temp\SQLServerAgentjobsCategory.SQL |
Then it´s just run the .sql file
1 |
Invoke-Sqlcmd -InputFile C:\temp\SQLFiles.sql -ServerInstance deathstar -Database master -Verbose |
It will run all the .SQL files you have specified in the SQLFiles.SQL
You may want to export to a txt file the output of all files ran:
1 2 3 |
import-module sqlps -DisableNameChecking Invoke-Sqlcmd -InputFile C:\temp\SQLFiles.sql -ServerInstance deathstar -Database master -Verbose | out-file C:\temp\output.txt |
This approach can be very useful if you just need to check the output, not generating reports, or even create objects in SQL Server – an full environment for instance.
Another approach is to call invoke-SQLCMD for each file. For that we don’t need the SQLFiles.SQL
1 2 3 4 5 |
import-module sqlps -DisableNameChecking get-childitem c:\temp\*.sql | ForEach-Object { Invoke-Sqlcmd -InputFile $_ -ServerInstance deathstar -Database master } |
You can, of course, export the results to CSV files:
1 2 3 4 5 6 7 8 9 |
import-module sqlps -DisableNameChecking #delete all csv files Remove-Item -Path c:\temp\*.csv -Force -ErrorAction SilentlyContinue get-childitem -path c:\temp\*.sql | ForEach-Object { Invoke-Sqlcmd -InputFile $_ -ServerInstance deathstar -Database master | Export-Csv -Path "c:\temp\$(($_.name -split '[.]')[0]).csv" -NoTypeInformation -NoClobber } |
Even better, you can write an XLSX file from one or more CSV files with each CSV file turned into a Worksheet. For that, we can use the function of my fellow PowerShell MVP Boe Prox called Convert-CSVToExcel (you can download the source from the top of this article)
1 2 3 4 5 6 7 8 9 10 11 |
import-module sqlps -DisableNameChecking #delete all csv files Remove-Item -Path c:\temp\*.csv -Force -ErrorAction SilentlyContinue get-childitem -path c:\temp\*.sql | ForEach-Object { Invoke-Sqlcmd -InputFile $_ -ServerInstance deathstar -Database master | Export-Csv -Path "c:\temp\$(($_.name -split '[.]')[0]).csv" -NoTypeInformation -NoClobber } Convert-CSVToExcel -inputfile (Get-ChildItem c:\temp\*.csv) -output 'c:\temp\DiagnosticInformationQueries.xlsx' |
Loading Community functions
You can also run Community functions such as Invoke-SQLCMD2, a System.Data.SQLClient based function wrote by Chad Miller that extends the functionality of Invoke-SQLCMD. You just need to download it and run it inside your script, save into .ps1 file and call the function as a .ps1 script, load it in your profile or even save the code in a psm1 file (script module file) and save in any path shown by the $env:psmodulepath to load directly when calling it (PowerShell 3.0 and higher)
1 |
Invoke-Sqlcmd2 -query 'Select * from test' -ServerInstance deathstar -Database dagobah -As PSObject -QueryTimeout 30 -ConnectionTimeout 30 |
Loading and handling the extras
Installing Module.
To Install a PowerShell module means nothing more than to copy the module files to a folder in a computer. To load a module in small projects is pretty simple. You just need to use the Import-Module cmdlet. If you don’t specify the path of the module PowerShell will look in the environment variable $ENV:PSMODULEPATH and check if it is there and automatically will load it. In PowerShell 2.0 you need to explicitly the import-module in your profile.
Further reading:
Loading an assembly
Most of the code you will see that uses SMO does not leave the loading of assemblies to SQLPS, but instead uses the loadwithpartialname method. This method is deprecated because it doesn’t give you control of the version of SMO that will be loaded if you have several versions in your system. The choice of file is controlled by GAC*. On the other hand, using the ADD-Type can be a pain because you need to be explicit about the version and change your code. Let’s take a look at some examples:
LoadWithPartialName
The general way:
1 |
[Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') |
But we also can check if there is any SMO installed and return an error if not, otherwise it will be loaded:
1 2 3 |
if ([Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') -eq $null) { Throw 'SMO not avaliable' } |
Add-Type:
General way:
1 |
Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=13.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" |
Testing:
1 2 3 4 5 |
try { Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=13.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" -ErrorAction Stop } catch { Throw 'SMO not avaliable' } |
The recommend way is to use ADD-Type, but what is the best approach? Well, it depends. If you are dealing with several version of SMO and it’s not a problem for your PowerShell environment then loadwithpartialname will be easier; otherwise, use add-type. But always remember LoadwithpartialName is a deprecated method and in some point will be excluded from the framework
Checking which version of SMO or SQLPS was loaded in the session
By default, PowerShell loads some assemblies when you open a session. Of course, it needs some basic assemblies to work properly. SMO is a .NET library and it needs to be loaded. The same happens with SQLPS module. To find out which assemblies were loaded, you can use this code (credits to fellow PowerShell MVP Richard Siddaway – Assemblies loaded in PowerShell:)
1 2 3 |
[appdomain]::currentdomain.getassemblies() | Sort-Object -property fullname | format-table fullname |
For our purposes, we just need to filter for SMO:
1 2 3 |
([appdomain]::currentdomain.getassemblies()).where{$_.fullname -like '*smo*'}| Sort-Object -property fullname | format-table fullname |
1 2 3 4 5 6 |
FullName -------- Microsoft.SqlServer.Smo, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91 Microsoft.SqlServer.Smo, Version=13.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91 Microsoft.SqlServer.SmoExtended, Version=12.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91 Microsoft.SqlServer.SmoExtended, Version=13.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91 |
Finding out the version of the SQLPS module is a little different. PowerShell has a cmdlet called Get-Module that shows all the information on the modules.
1 |
Get-Module |
Also it has a parameter to check whether a specific module was loaded:
1 |
Get-Module -Name SQLPS |
Also you can check what modules are available to load using the parameter – ListAvaliable
1 |
Get-Module -ListAvailable |
The Get-Module cmdlet will search all the modules available in the Environment variable $Env:PSMODULEPATH, a special variable that contains all the paths for the installed modules and will list them for you.
1 2 3 4 |
PS C:\temp> $env:psmodulepath C:\Users\Laerte Junior\Documents\WindowsPowerShell\Modules;C:\Program Files\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\Modules;C:\Program Files (x86)\Microsoft SQL Server\120\Tools\PowerShell\Modules\;C:\Program Files\Microsoft Message Analyzer\PowerShell\;C:\Program Files (x86)\Microsoft SQL Server\110\Tools\Pow erShell\Modules\ |
Determining the version of SQLPS that PowerShell will load
If you use the name parameter, PowerShell will always check in the $env:psmodulepath variable and then will, according fellow PowerShell MVP Joel Bennet, import them in the following order:
Path order, then alphabetic order, then highest version if you have side-by-side modules in the same folder (in PS5)
Loading a specific version of SQLPS or SMO
That´s an interesting question that comes out in the comments on the SMO stairway, and I will give a different answer for SQLPS, which is a module, and SMO which is a group of assemblies.
Loading a particular version of SQLPS
As we saw, if we use the parameter name in the import-module it will load the SQLPS in the order of the environment variable . So in this specific case we use the parameter -FullyQualifiedName
1 |
Import-Module -FullyQualifiedName 'C:\Program Files (x86)\Microsoft SQL Server\110\Tools\PowerShell\Modules\SQLPS\sqlps' |
Or even the parameter -name with the FQDN:
1 |
PS C:\Users\Laerte Junior> Import-Module -name 'C:\Program Files (x86)\Microsoft SQL Server\120\Tools\PowerShell\Modules\SQLPS\sqlps' |
The point is that you need to specify the path for the SQLPS module.
Shawn Melton showed, in the comments, a very cool function to make the job easy:
1 2 3 4 5 6 7 8 |
function Load-SQLModule { [cmdletbinding()] param ( $version = 130 ) Get-Module -ListAvailable -Name SQLPS | where {$_.path -match $version} | Import-module -DisableNameChecking | Out-Null } |
In this function, you can pass as a parameter the version you want to import and the default is 130.
This example will load the highest SQLPS version
1 2 3 4 |
Get-Module -ListAvailable -Name SQLPS | Sort-Object -Property Path -Descending | Select-Object -First 1 -ExpandProperty Path | Import-module -DisableNameChecking |
We ended up with another version of Shawn´s function where you can choose the most recent SQLPS version by using a switch parameter -highest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function Load-SQLModule { [CmdletBinding( DefaultParameterSetName='Version' )] param( [Parameter( Position=0, Mandatory=$false, ParameterSetName = 'Version')] [string] $version, [Parameter( Position=1, Mandatory=$false, ParameterSetName = 'Highest')] [Switch] $HighestOne ) If ($version) { Get-Module -ListAvailable -Name SQLPS | Where-Object {$_.path -match $version} | Import-module -DisableNameChecking $moduleBase = Get-Module -Name SQLPS | Select-Object ModuleBase } else { Get-Module -ListAvailable -Name SQLPS | Sort-Object -Property Path -Descending | Select-Object -First 1 -ExpandProperty Path | Import-module -DisableNameChecking $moduleNBase = Get-Module -Name SQLPS | Select-Object ModuleBase } Write-Verbose "Imported Module: $($modulebase)" } |
Loading a particular version of SMO:
If you use the code that I included earlier on, it will only show you the assemblies that are already loaded in your session. If you use the loadwithpartialname method to load the SMO, this is controlled by the GAC* . If you want to load a specific version of SMO, I recommend that you should use ADD-Type and you can then specify which version will be loaded:
1 |
Add-Type -AssemblyName "Microsoft.SqlServer.SMO, Version=13.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" |
Using the same idea from Shawn, we could think of doing something similar with SMO, but for this we would need to query the GAC: Let’s say we want to load the highest version SMO installed
1 2 3 4 5 6 |
$HighestOne = (Get-ChildItem -path C:\Windows\assembly -recurse | Where-Object {$_.name -eq 'Microsoft.SQLServer.SMO.dll'} | Sort-Object -Property fullname -desc | Select-Object -First 1).fullname Add-Type -LiteralPath $HighestOne |
We could then develop this to speed it up by saving on the pipeline traffic. This is achieved by doing the selection within the Get-ChildItem cmdlet.
1 2 3 |
Add-Type -literalpath (Get-ChildItem -path "$($env:windir)\assembly\GAC_MSIL\Microsoft.SqlServer.Smo" -include 'Microsoft.SQLServer.SMO.dll' -recurse | Sort-Object -Property fullname -desc | Select-Object -First 1).fullname |
You can read more about how modules work in PowerShell here: (Importing a PowerShell Module)
*GAC or Global Assembly Cache is a machine-wide code where the assemblies are installed and stored to be shared with applications on the computer.
Creating a Custom Type Accelerator for SMO
You will sometimes find that when you load an assembly like SMO, that specifying the NET types is laborious. Type accelerators are a great help for getting around this. Type Accelerators are like shortcuts for .NET types. It can simplify your code and make it more intelligible if you use them. For instance [adsi] is a type accelerator for System.DirectoryServices.DirectoryEntry or [pscustomobject] for System.Management.Automation.PSObject. You can do the same for SMO
This code shows the type accelerators currently defined in your session:
1 |
[PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::get |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
PS C:\Users\Laerte Junior> [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::get Key Value --- ----- Alias System.Management.Automation.AliasAttribute AllowEmptyCollection System.Management.Automation.AllowEmptyCollectionAttribute AllowEmptyString System.Management.Automation.AllowEmptyStringAttribute AllowNull System.Management.Automation.AllowNullAttribute ArgumentCompleter System.Management.Automation.ArgumentCompleterAttribute array System.Array bool System.Boolean byte System.Byte char System.Char CmdletBinding System.Management.Automation.CmdletBindingAttribute |
To create a custom type accelerator you just need to add the key and the .net type:
1 |
[PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::add("Key",".Net Type") |
So for instance, to create a type accelerator for the SMO Server:
1 |
[PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')::Add(SMOSERVER','Microsoft.SqlServer.Management.Smo.Server') |
Then I can easily access all properties and methods using the new type accelerator.
1 |
([SMOSERVER]'DeathStar').databases |
Or also call a method:
1 |
([SMOSERVER]'DeathStar').KillAllProcesses('Dagobah') |
Now it´s just play around it and add in your PowerShell profile to load your custom SMO type accelerators
Reference: Boe Prox – Quick Hits: Finding, Creating and Removing PowerShell Type Accelerators
Conclusion
There really isn’t a single best method of accessing SQL Server via PowerShell. What you use depends on what you need to achieve. Every approach has its advantages and disadvantages. In this article, I’ve described how to get up and running with each of these approaches, and I’ve tried to suggest the best ways of approaching the task.
Load comments