Understanding and Using PowerShell Support in SQL Server 2008
by Allen White; SQL Server MVP and Trainer/Consultant. Visit Allen’s blog at http://sqlblog.com/blogs/allen_white/default.aspx.
Introduction
In this paper we’ll walk through an introduction to Windows PowerShell and its key elements. We’ll then look at the special PowerShell executable program provided with SQL Server 2008, sqlps.exe. Sqlps.exe incorporated a set of cmdlets specific to using PowerShell with SQL Server, and adds a PowerShell drive (PSDrive) allowing us to navigate SQL Server as though it were a filesystem. We’ll explore these features, then look at some scripts we can use to administer SQL Server as well as to extract data as needed.
Introduction to the PowerShell language
To provide a framework for understanding the basics of PowerShell we’ll look at cmdlets, variables, the pipeline, flow control, functions, error handling, and security.
Using Cmdlets
PowerShell’s cmdlets provide the core functionality necessary for any shell environment. To provide consistency they’re named using a standard verb-noun naming convention. So, to get a list of the processes running on the system use the Get-Process cmdlet. For example:
PS> Get-Process
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
780 6 2532 5052 31 117.91 328 csrss
97 4 544 3052 21 44.94 3716 ctfmon
313 11 8388 13112 61 247.17 3632 explorer
0 0 0 16 0 0 Idle
479 30 9524 13128 89 34.73 1192 inetinfo
729 27 8592 9312 44 36.47 412 lsass
150 6 1744 4132 23 1.73 1012 msdtc
159 4 12256 13320 104 1.95 1256 MsDtsSrvr
716 46 36820 28084 211 149.56 1352 msmdsrv
152 10 2268 5564 37 1.26 1756 pop3svc
201 5 24396 21128 126 2.17 2736 powershell
To stop a service running on the system, use the Stop-Service cmdlet. Help is available on any cmdlet using the Get-Help cmdlet followed by the name of the cmdlet. To get help on the Get-Help command type this:
PS> Get-Help Get-Help
NAME
Get-Help
SYNOPSIS
Displays information about Windows PowerShell cmdlets and concepts.
SYNTAX
Get-Help [[-name] <string>] [-component <string[]>] [-functionality <string[]>] [-role <string[]>] [-category <stri
ng[]>] [-full] [<CommonParameters>]
Get-Help [[-name] <string>] [-component <string[]>] [-functionality <string[]>] [-role <string[]>] [-category <stri
ng[]>] [-detailed] [<CommonParameters>]
Get-Help [[-name] <string>] [-component <string[]>] [-functionality <string[]>] [-role <string[]>] [-category <stri
ng[]>] [-examples] [<CommonParameters>]
Get-Help [[-name] <string>] [-component <string[]>] [-functionality <string[]>] [-role <string[]>] [-category <stri
ng[]>] [-parameter <string>] [<CommonParameters>]
DETAILED DESCRIPTION
The Get-Help cmdlet displays information about Windows PowerShell cmdlets and concepts. You can also use "Help {<cmdlet name> | <topic-name>" or "<cmdlet-name> /?". "Help" displays the help topics one page at a time. The "/?" displays help for cmdlets on a single page.
RELATED LINKS
Get-Command
Get-PSDrive
Get-Member
REMARKS
For more information, type: "get-help Get-Help -detailed".
For technical information, type: "get-help Get-Help -full".
As you can see there are a number of options available including detailed help (-detailed), technical information (-full), or one of the best options, to see examples of how to use the cmdlet, use the –examples option. In one of the related links listed in our example, the Get-Command cmdlet returns a list of all the available cmdlets.
Storing Variables
No language is complete without the ability to store interim results in some sort of variable, and PowerShell provides this capability as well. Variables are defined by using the dollar sign character as the first character of the variable name. Variables are not just a reference to a stored value, though, they store an object. Objects are used extensively in the .NET Framework, and are well supported in PowerShell. Objects have methods and properties so casting a variable as a string object, for example, enables all the methods and properties of a string object for that variable. A quick example follows:
PS> $lit = 'Cleveland Rocks!'
PS>
PS> $lit
Cleveland Rocks!
PS> $lit.Length
16
PS>
The Length property for the literal value assigned to the $lit variable contains the value 16. You can find out the members and properties for any variable by piping the variable to the Get-Member cmdlet like this (results are condensed):
PS> $lit | Get-Member
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Clone Method System.Object Clone()
...
Length Property System.Int32 Length {get;}
There are times where PowerShell may not choose the correct type of object for a variable assignment. A case in point is when a numeric value is assigned to a variable but the variable needs to be string object. To control this you can cast the variable as the type you need, like this:
PS> $strval = [string]'7'
Another benefit of objects is the ability to group objects into collections of objects. Collections are like an array without a predefined limit. You can create a collection like this:
PS> $col = 1,3,4,6,7,9
The individual members of the collection can be accessed by their ordinal number, where an ordinal number of 0 represents the first member of the collection. So, to retrieve the value of 4 out of our collection we can use this command:
PS> $col[2]
4
The power of collections will become evident as we examine flow control. One really useful cmdlet that creates a collection is Get-Contents. This cmdlet makes it easy to load a text file into a variable, which then contains a collection of string objects:
PS> $servers = Get-Content 'servers.txt'
This will load the contents of the servers.txt file in the local directory into the $servers variable.
The Pipeline
In the introduction we discussed using the pipeline to send the results of one cmdlet to the next, and demonstrated that briefly in looking at the Get-Member cmdlet. The pipeline, invoked by using the vertical bar character (‘|’), takes the results from one cmdlet and sends it to the next. This ability creates the power that Unix administrators became used to with the shell scripting environments on that platform. An example that provides some interesting results when examining our systems for performance problems is this:
PS> get-process | sort-object workingset -descending | select-object
-first 10
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
637 82 163668 157312 1228 232.92 2132 sqlservr
535 80 120208 117256 1225 261.53 1344 sqlservr
562 18 99972 77364 357 457.14 3580 Ssms
598 11 52048 50352 179 57.86 4012 powershell
308 73 61612 45740 1155 156.54 728 sqlservr
602 17 57452 37956 255 298.60 1400 ReportingServicesService
494 10 26636 33144 155 5.93 3308 SQLPS
713 46 36704 27984 210 241.31 1264 msmdsrv
1011 42 12872 19556 80 144.29 808 svchost
158 4 12248 13272 104 2.22 1204 MsDtsSrvr
This takes the output of the Get-Process cmdlet we examined earlier and pipes the output to the sort-object cmdlet, which allows us to sort the results in a particular order. In this case we’re sorting the results on the WorkingSet property in descending order, so the process using the most memory will be returned first, followed by the process using the next most amount of memory, and so on. The results of this are then piped into the select-object cmdlet, which allows us to limit the resultset, and in this case we’re selecting just the first 10. So, we’re returning the top ten processes in terms of memory use on our system.
To make things more convenient, PowerShell provides aliases for most of the cmdlets. The aliases use well-known command names for certain functions. For example, to view the items in directory PowerShell provides the Get-ChildItem cmdlet, but this cmdlet is aliased as “dir”, which is familiar to Windows users, and “ls”, which is familiar to Unix users, and provides the same functionality. You can find out all the defined aliases by using the Get-Alias cmdlet. Another great feature in PowerShell is the fact that either the slash (/) or the backslash (\) can be used in specifying directory trees, making PowerShell a more comfortable experience for both Windows and Unix admins.
Where Unix shell scripts pipe text from one command to another, PowerShell pipes objects from one cmdlet to the next. This eliminates the need to parse the output of one command before sending it to the next. Thus, our sort-object cmdlet understands that WorkingSet is a property in the resultset of the Get-Process cmdlet.
Flow Control
Flow control is important in any language to allow conditional and iterative processing, and PowerShell provides the constructs necessary for flow control. Comparison operators are a necessary component of flow control, and PowerShell supports this set:
Operator |
Description |
-lt |
less than |
-le |
less than or equal to |
-gt |
greater than |
-ge |
greater than or equal to |
-eq |
equal to |
-ne |
not equal to |
-like |
like wildcard pattern matching |
-and |
logical and |
-or |
logical or |
Table 1: PowerShell Comparison Operators
Flow control is then handled using this set of commands:
Control |
Example Code |
If |
if ($val -eq "target") { #work } |
For |
For ($i=0; $i -lt 10; $i++) { #work } |
ForEach |
Foreach ($obj in $objects) { #work } |
Switch |
Switch ($val) { "Val1" { #work } "Val2" { #work } } |
Do Until |
Do { #work } Until ($val -eq "target") |
Do While |
Do { #work } While ($val -eq "target") |
While |
While ($val -eq "target") { #work } |
Table 2: Flow Control Operators and Examples
Notice that each of these commands, once the condition is met, is followed by a pair of braces({}). This set of braces, along with the commands contained within them, is referred to as a scriptblock. Scriptblocks can be created anywhere on a command line or in a script, and can be nested.
In addition, PowerShell provides these cmdlets for controlling flow:
Control Cmdlet |
Description |
ForEach-Object |
Executes once for each member in the collection |
Where-Object |
Filters objects based on conditions |
Select-Object |
Pipes only the specified properties |
Sort-Object |
Sorts the objects |
Tee-Object |
Sends the objects in two directions |
Table 3: Flow Control Cmdlets
Assembling calls to cmdlets using the tools shown here into a script you can use whenever needed allows you to automate the processes you use regularly, allowing you to be more efficient. Scripts can be built to perform any number of administrative functions, from creating a database to extracting HR data and importing it into Active Directory Domain Services (AD DS) to keep your organization running smoothly.
Functions
Sets of script code you may need to run multiple times within the script can be grouped together into a function. Functions can be defined with zero or more parameters to provide flexibility. The basic form of a function is as follows:
Function MyFunction {
#work
}
Within the scriptblock you can write the code necessary for your script to perform its desired function. You can add parameters in one of two ways. The first uses a single parameter, like this:
Function MyFunction ($param) {
#work
}
The second, and more preferred way is like this:
Function MyFunction {
param (
[int]$x = 7,
[int]$y = 9
)
#work
}
Since PowerShell is an interpreted language, functions must be placed in the script before they’re called in the main part of the script. Best practices include putting all of your functions at the beginning of the script for this reason.
Error Handling
Errors in PowerShell 1.0 are best handled using a Trap function. Here’s an example:
Function Err_Handler {
$err = "Error Category: " + $error[0].CategoryInfo.Category
$err = $err + ". Error Object: " + $error[0].TargetObject
$err = $err + " Error Message: " + $error[0].Exception.Message
$err = $err + " Error Message: " + $error[0].FullyQualifiedErrorId
$log = New-Object System.Diagnostics.EventLog('Application')
$log.set_source("MyScript")
$log.WriteEntry($err)
}
Trap {
# Handle the error
Err_Handler
# End the program.
break;
}
# Your Code Here …
The error handler will write the error to the Windows system Application log, which can be viewed by an administrator to diagnose the problem.
Security
One of the important issues in any application is security. PowerShell 1.0, as installed by default, doesn’t allow scripts to run. The Get-ExecutionPolicy cmdlet will return the current security setting for PowerShell, and by default it’s set to Restricted. To allow scripts which are on the local machine to run use the Set-ExecutionPolicy cmdlet to set the property to RemoteSigned. This will allow local scripts to be run, but scripts anywhere else must be digitally signed. For more information about execution policy get help on “about_signing”. Also, in order to prevent command “hijacking”, script location must be qualified to run, so to run a script in the current directory called myscript.ps1 the command to use is:
PS> ./myscript.ps1
The full path can be used as well, and the slash or backslash character can be used in the path name.
SQL Server PowerShell Extensions
Microsoft has incorporated PowerShell into its Common Engineering Criteria for server products, and SQL Server 2008 has included PowerShell in its management toolset. PowerShell by design allows the inclusion of “snap-ins” that provide access to the application that wants to allow a PowerShell interface into the application. The most notable of these applications is Microsoft Exchange 2007, which rebuilt the application from the ground up with administrative interfaces built for PowerShell, including hundreds of cmdlets for various functions, and the Exchange administrative application executes the PowerShell cmdlets.
Shell scripting environments address everything on a server as though it were a file system. PowerShell Drives provide this capability in PowerShell, and the Get-PSDrive cmdlet will list the available drives in your session. PSDrives included in basic PowerShell include (besides real file system drives):
- ENV: (Environment variables)
- HKCU: (HKEY_CURRENT_USER Registry tree)
- HKLM: (HKEY_LOCAL_MACHINE)
You can actually navigate the Windows Registry using filesystem commands like cd (Set-Location cmdlet) and dir (Get-ChildItem).
The SQLSERVER: Drive
SQL Server 2008 adds its own PSDrive for the environment in the form of the SQLSERVER: drive. (For detailed steps on loading the SQL PowerShell snap-ins into native PowerShell, visit Michiel Wories’ blog post: https://blogs.msdn.com/mwories/archive/2008/06/14/SQL2008_5F00_Powershell.aspx.) With the SQL Server snap-in you can navigate SQL Server objects through one of four directories under SQLSERVER:
- The SQL folder accesses the database engine, SQL Server Agent, Service Broker, and Database Mail.
- The SQLPolicy folder accesses Policy-Based Management.
- The SQLRegistration folder accesses the Registered Servers and the new Central Management Server.
- The DataCollection folder accesses the Data Collector objects provided with Management Data Warehouse.
These folders can be navigated like the filesystem, as is shown in this screen capture:
Figure 1: Navigating the SQLServer: PowerShell Drive
Now, by navigating to the Databases folder under an instance of SQL Server, you can use the following command to send the list of databases to a web page for anyone in your organization to view:
PS> dir | select name, CompatibilityLevel, RecoveryModel, Size, SpaceAvailable | convertto-html > c:\inetpub\wwwroot\databases.html
If you then navigate to the Tables folder under a specific database you can use the following command to send the list of tables in that database, in descending order by the number of rows in the table, showing the largest (in rows) tables first, to another web page for your management tool set.
PS> dir | select schema, name, rowcount, filegroup | sort rowcount -descending | convertto-html > c:\inetpub\wwwroot\DBTables.html
SQL Server Cmdlets
These are basic web pages, but can be enhanced for your needs. The SQL Server PowerShell extensions also provide new cmdlets. The cmdlets are:
- Invoke-Sqlcmd
- Invoke-PolicyEvaluation
- Encode-SqlName
- Decode-SqlName
- Convert-UrnToPath
These cmdlets provide functionality not available through other means:
- Invoke-Sqlcmd takes a query in text form and sends it to SQL Server for processing. The results are returned in object form and standard PowerShell functions can be used to manipulate the data as necessary. It takes either a standard Transact-SQL query or an XQuery statement as input.
- Invoke-PolicyEvaluation uses the SQL Server 2008 Policy-Based Management feature. It evaluates policies defined for one or more servers to determine whether or not the servers are in compliance. It can also reset object settings to comply with the policy. Lara Rubbelke wrote a set of blog posts about this cmdlet at http://sqlblog.com/blogs/lara\_rubbelke/archive/2008/06/19/evaluating-policies-on-demand-through-powershell.aspx.
There are characters acceptable for use within SQL Server that are not acceptable in PowerShell, such as the backslash between the server and instance names in defining a SQL Server instance.
· The Encode-SqlName allows conversion from a SQL Server acceptable name (such as HOME\MyInstance) to one usable by PowerShell (HOME%5CMyInstance).
· The Decode-SqlName cmdlet reverses the above process.
SMO uses Uniform Resource Names (URN) for its objects.
· Convert-URNToPath cmdlet is provided to convert those URN values to path names. That path name can then be used in a Set-Location cmdlet, to navigate SQL Server. The URN for the Sales.Order table in AdventureWorks on HOME\MyInstance is
Server[@Name='HOME\MyInstance']\Database[@Name='AdventureWorks']\Table[@Name='Order' and @Schema='Sales']
That URN will be converted by the cmdlet to: SQLSERVER:\SQL\HOME\MyInstance\Databases\AdventureWorks\Tables\Sales.Order
These five cmdlets, in conjunction with the objects in the SMO object library, provide full access to the management features in SQL Server Management Studio.
SQLPS.exe – The SQL Server Mini-Shell
To make things easier for administrators using PowerShell for SQL Server, Microsoft created an executable - a mini-shell - which automatically loads the SQL Server PowerShell snap-ins. The program is called sqlps.exe and is included when you install SQL Server 2008. Besides automatically loading the snap-ins the sqlps.exe environment enables script execution automatically by setting the execution policy to RemoteSigned.
This mini-shell can be invoked in a number of ways. You can use the Windows Start, Run menu item and type in sqlps and start it that way. SQL Server Agent jobs can use the SQL Server Agent PowerShell subsystem in a step to run a script by selecting the step type of PowerShell. Each time a step runs the PowerShell step it loads a separate copy of sqlps.exe into memory, each taking about 40MB, so caution is advised when using this feature.
The most interesting method is within SQL Server Management Studio. In Object Explorer you can right-click on any object in the “tree” under an instance and select “Start PowerShell”, and sqlps.exe will start up, and the Set-Location cmdlet is used to set the current location in the shell window to the object you pointed to. This provides a very nice ability to do some ad-hoc browsing of SQL Server objects as necessary.
Using the .NET Framework with SQL Server
There are two perspectives for working with SQL Server, administration and data access. SQL Server Management Objects (SMO) contains the objects we’ll use to manage SQL Server, and ADO.NET allows us access to the data.
SQL Server Management Objects - SMO
SQL Server Management Objects (SMO) is the object library provided by Microsoft to perform administrative actions against SQL Server. The library is built on top of the .NET Framework (2.0) so applications written using SMO are using managed code and benefit from the features available within the Framework, including the security and cleanup features. SMO was introduced with SQL Server 2005 and allows you to manage servers running SQL Server 2000, SQL Server 2005, and SQL Server 2008.
To use SMO within PowerShell (if you’re not using sqlps.exe) you’ll need to load the SMO DLLs. This is easy to do at the beginning of your scripts using these commands:
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMOExtended') | out-null
The Out-Null cmdlet at the end of the command simply ignores the version information returned when the DLL is loaded.
The SMO object library is best envisioned as a tree-type structure, starting with the Server object. You can connect to a server using the following command:
$srv = new-object ('Microsoft.SqlServer.Management.Smo.Server') 'HOME\MyInstance'
The new-object cmdlet creates an object of the type specified within parentheses, in this case a new Server object. Once you’ve set a variable to the server object you can navigate through the various collections and properties available. For example the Information collection contains properties about this instance, like Edition, Version, location of the system data and log files (MasterDBData and MasterDBLogData), and more. The Settings collection contains useful information such as the BackupDirectory and default data and log directory for user databases (DefaultFile and DefaultLog).
Figure 2: SMO Server Information and Settings objects.
The databases defined on the instance are identified in the Databases collection. For example, details on the physical files can be accessed by the following structure:
Figure 3: SMO Database File and Log File objects.
So, how do we make use of this information? Here’s a script to create a database, using the server’s default file and log locations:
#createsimpledb.ps1
#Creates a new database using defaults
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$s = new-object ('Microsoft.SqlServer.Management.Smo.Server') 'HOME\MyInstance'
$dbname = 'SMOSimple_DB'
$db = new-object ('Microsoft.SqlServer.Management.Smo.Database') ($s, $dbname)
$db.Create()
This creates a database with all the default sizes and locations, using the settings in the model database. If you want to have more control over the creation of the database you’ll need to set the properties in the SMO objects defined for creating databases, as shown in this more complete script:
#createdb.ps1
#Creates a new database using our specifications
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$s = new-object ('Microsoft.SqlServer.Management.Smo.Server') 'HOME\MyInstance'
$dbname = 'SMO_DB'
$syslogname = $dbname + '_SysData'
$applogname = $dbname + '_AppData'
$loglogname = $dbname + '_Log'
$fileloc = $s.Settings.DefaultFile
$logloc = $s.Settings.DefaultLog
if ($fileloc.Length = 0) {
$fileloc = $s.Information.MasterDBPath
}
if ($logloc.Length = 0) {
$logloc = $s.Information.MasterDBLogPath
}
$dbsysfile = $fileloc + '\' + $syslogname + '.mdf'
$dbappfile = $fileloc + '\' + $applogname + '.ndf'
$dblogfile = $logloc + '\' + $loglogname + '.ldf'
# Instantiate the database object and add the filegroups
$db = new-object ('Microsoft.SqlServer.Management.Smo.Database') ($s, $dbname)
$sysfg = new-object ('Microsoft.SqlServer.Management.Smo.FileGroup') ($db, 'PRIMARY')
$db.FileGroups.Add($sysfg)
$appfg = new-object ('Microsoft.SqlServer.Management.Smo.FileGroup') ($db, 'AppFG')
$db.FileGroups.Add($appfg)
# Create the file for the system tables
$dbdsysfile = new-object ('Microsoft.SqlServer.Management.Smo.DataFile') ($sysfg, $syslogname)
$sysfg.Files.Add($dbdsysfile)
$dbdsysfile.FileName = $dbsysfile
$dbdsysfile.Size = [double](5.0 * 1024.0)
$dbdsysfile.GrowthType = 'Percent'
$dbdsysfile.Growth = 25.0
$dbdsysfile.IsPrimaryFile = 'True'
# Create the file for the Application tables
$dbdappfile = new-object ('Microsoft.SqlServer.Management.Smo.DataFile') ($appfg, $applogname)
$appfg.Files.Add($dbdappfile)
$dbdappfile.FileName = $dbappfile
$dbdappfile.Size = [double](25.0 * 1024.0)
$dbdappfile.GrowthType = 'Percent'
$dbdappfile.Growth = 25.0
$dbdappfile.MaxSize = [double](100.0 * 1024.0)
# Create the file for the log
$dblfile = new-object ('Microsoft.SqlServer.Management.Smo.LogFile') ($db, $loglogname)
$db.LogFiles.Add($dblfile)
$dblfile.FileName = $dblogfile
$dblfile.Size = [double](10.0 * 1024.0)
$dblfile.GrowthType = 'Percent'
$dblfile.Growth = 25.0
# Create the database
$db.Create()
# Set the default filegroup to AppFG
$appfg = $db.FileGroups['AppFG']
$appfg.IsDefault = $true
$appfg.Alter()
$db.Alter()
This database now has two filegroups, PRIMARY and App_FG, which is now the default, so new objects aren’t placed in the PRIMARY filegroup with the database metadata.
Objects used in another key function, Backup, can be used to script regular backups. Here are the objects:
Figure 4:SMO Backup Objects.
These are used in the following script to back up user databases:
#backup.ps1
#Performs a Full backup on all user databases
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SmoExtended') | out-null
$s = new-object ('Microsoft.SqlServer.Management.Smo.Server') 'HOME\MyInstance'
$bkdir = $s.Settings.BackupDirectory
$dbs = $s.Databases
# Iterate through all databases and backup each user database
$dbs | foreach-object {
$db = $_
if ($db.IsSystemObject -eq $False) {
$dbname = $db.Name
$dt = get-date -format yyyyMMddHHmmss
$dbbk = new-object ('Microsoft.SqlServer.Management.Smo.Backup')
$dbbk.Action = 'Database'
$dbbk.BackupSetDescription = "Full backup of " + $dbname
$dbbk.BackupSetName = $dbname + " Backup"
$dbbk.Database = $dbname
$dbbk.MediaDescription = "Disk"
$dbbk.Devices.AddDevice($bkdir + "\" + $dbname + "_db_" + $dt + ".bak", 'File')
$dbbk.SqlBackup($s)
}
}
Notice that the Get-Date cmdlet is used to extract the current system date and incorporate that value into the name of the backup file, so you know exactly when the backup was created.
Books Online documents all of the SMO objects and provides many examples of basic administrative tasks, and the Get-Member cmdlet is useful to determine the properties and methods available for each SMO object.
SMO allows us to build scripts to manage SQL Server, but frequently we need to work with the data within our databases. The Invoke-Sqlcmd cmdlet allows us to query the database from PowerShell, but a more powerful method to return data is through the use of ADO.NET.
Using ADO.NET for Queries
ADO.NET has two sets of objects, a connected set that allows you to connect with a data source, run queries and return result sets, and a disconnected set of objects that allow you to work with the data after it’s been collected. Here’s a list of the objects.
ADO.NET Object |
Description |
Connection Object |
A connection to the data source |
Command Object |
Can represent a query against a database, a call to a stored procedure, or a direct request to return the contents of a specific table |
DataReader Object |
Designed to return query results as quickly as possible |
Transaction Object |
Groups a number of changes to a database and treats them as a single unit of work |
The Connection object has a BeginTransaction method that can be used to create Transaction objects |
|
Parameter Object |
Allows the specification of parameters for stored procedures or parameterized queries |
DataAdapter Object |
Acts as a bridge between the database and the disconnected objects in the ADO.NET object model |
Table 4: ADO.NET Connected Objects
ADO.NET Object |
Description |
DataTable Object |
Allows the examination of data through collections of rows and columns |
DataColumn Object |
Corresponds to a column in a table |
Constraint Object |
Defines and enforces column constraints |
DataRow Object |
Provides access to the DataTable's Rows collection |
DataSet Object |
The container for a number of DataTable objects |
DataRelation Object |
Defines the relations between DataTables in the DataSet object |
DataView Object |
Allows the examination of DataTable data in different ways |
Table 5: ADO.NET Disconnected Objects
Here’s a script which will return a comma-separated list from the AdventureWorks database based on the results of a query which returns product inventory counts where the current inventory level is below the reorder level:
#reorder.ps1
#This script pulls info from the Production Product and Inventory tables
# in AdventureWorks and presents it to the user
$cn = new-object system.data.SqlClient.SqlConnection("Data Source=HOME\MyInstance;Integrated Security=SSPI;Initial Catalog=AdventureWorks");
$ds = new-object "System.Data.DataSet" "dsInventoryData"
$q = "SELECT p.[ProductID]"
$q = $q + " ,p.[Name]"
$q = $q + " ,p.[ProductNumber]"
$q = $q + " ,p.[SafetyStockLevel]"
$q = $q + " ,p.[ReorderPoint]"
$q = $q + " ,pi.QOH"
$q = $q + " FROM [Production].[Product] p"
$q = $q + " JOIN (SELECT [ProductID] ,Sum([Quantity]) as QOH"
$q = $q + " FROM [Production].[ProductInventory]"
$q = $q + " GROUP BY [ProductID]) pi"
$q = $q + " ON pi.ProductID = p.ProductID"
$q = $q + " WHERE pi.QOH < p.ReorderPoint"
$da = new-object "System.Data.SqlClient.SqlDataAdapter" ($q, $cn)
$da.Fill($ds)
$dtInventory = new-object "System.Data.DataTable" "dsInventoryData"
$dtInventory = $ds.Tables[0]
$dtInventory | FOREACH-OBJECT {[string]$_.ProductID + "," + $_.Name + "," + $_.ProductNumber + "," + $_.SafetyStockLevel + "," + $_.ReorderPoint + "," + $_.QOH
}
To connect with SQL Server we need a standard connection string. You can get an appropriate connection string for your script at http://www.connectionstrings.com/sql-server-2008. Once the connection is established with the server you build your query and run it using your query string and the connection object as parameters. The script does this by creating a DataSet object to store the results, then, after building the query string, creating a DataAdapter object with the query and connection objects. The Fill method of the DataAdapter object runs the query and loads the results into the DataSet. The DataSet is an in-memory database of the results of the query, and contains DataTable objects. Because we only submitted one query the DataSet contains only one DataTable. We create a DataTable object and load the table from the DataSet into our DataTable, and then pipe that to a ForEach-Object cmdlet to iterate through the results. The $_ variable indicates the current object in the set we’re working through, so within the ForEach-Object scriptblock we’re building a string containing a comma-separated list of the results. Here are the results from our script:
7
386,Hex Nut 1,HN-4402,1000,750,725
462,Lower Head Race,LR-8520,1000,750,701
853,Women's Tights, M,TG-W091-M,4,3,0
859,Half-Finger Gloves, M,GL-H102-M,4,3,0
876,Hitch Rack - 4-Bike,RA-H123,4,3,0
882,Short-Sleeve Classic Jersey, M,SJ-0194-M,4,3,0
910,HL Mountain Seat/Saddle,SE-M940,500,375,355
The first row in the results contains the number of rows in our set of results.
We can achieve similar results to this method using the new Invoke-Sqlcmd cmdlet by first creating a variable (say $q) with our query, as we did in the script, and using the Invoke-Sqlcmd cmdlet, then piping the results to the export-csv cmdlet. Here’s the updated script:
#reorder2.ps1
#This script pulls info from the Production Product and Inventory tables
# in AdventureWorks and exports it to a csv file
$q = "SELECT p.[ProductID]"
$q = $q + " ,p.[Name]"
$q = $q + " ,p.[ProductNumber]"
$q = $q + " ,p.[SafetyStockLevel]"
$q = $q + " ,p.[ReorderPoint]"
$q = $q + " ,pi.QOH"
$q = $q + " FROM [Production].[Product] p"
$q = $q + " JOIN (SELECT [ProductID] ,Sum([Quantity]) as QOH"
$q = $q + " FROM [Production].[ProductInventory]"
$q = $q + " GROUP BY [ProductID]) pi"
$q = $q + " ON pi.ProductID = p.ProductID"
$q = $q + " WHERE pi.QOH < p.ReorderPoint"
invoke-sqlcmd -ServerInstance 'HOME\MyInstance' -Query $q -Database 'AdventureWorks' | export-csv reorder.csv
And the contents of the output file:
#TYPE System.Data.DataRow
ProductID,Name,ProductNumber,SafetyStockLevel,ReorderPoint,QOH
386,"Hex Nut 1",HN-4402,1000,750,725
462,"Lower Head Race",LR-8520,1000,750,701
853,"Women's Tights, M",TG-W091-M,4,3,0
859,"Half-Finger Gloves, M",GL-H102-M,4,3,0
876,"Hitch Rack - 4-Bike",RA-H123,4,3,0
882,"Short-Sleeve Classic Jersey, M",SJ-0194-M,4,3,0
910,"HL Mountain Seat/Saddle",SE-M940,500,375,355
As you can see the results are almost identical, and this example shows you have many different ways to accomplish any task using PowerShell with SQL Server.
Putting it Together
We’ve already seen scripts that create and back up our databases. Just to show one more example, we can use PowerShell to create a SQL Server Agent job to execute our backup.ps1 script. The SMO objects needed for this script are as follows:
Figure 5:SMO SQL Server Agent Job Objects.
So, we can use the following script to execute the backup.ps1 script we created earlier:
#fullbackupjob.ps1
#This script creates a SQL Server Agent job which will run a PowerShell script once a day to backup user databases.
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null
$s = new-object ('Microsoft.SqlServer.Management.Smo.Server') 'HOME\MyInstance'
$j = new-object ('Microsoft.SqlServer.Management.Smo.Agent.Job') ($s.JobServer, 'FullBackup')
$j.Description = 'Backup User Databases'
$j.Category = '[Uncategorized (Local)]'
$j.OwnerLoginName = 'sa'
$j.Create()
$jid = $j.JobID
$js = new-object ('Microsoft.SqlServer.Management.Smo.Agent.JobStep') ($j, 'Step 01')
$js.SubSystem = 'CmdExec'
$js.Command = 'powershell "& E:\Scripts\backup.ps1"'
$js.OnSuccessAction = 'QuitWithSuccess'
$js.OnFailAction = 'QuitWithFailure'
$js.Create()
$jsid = $js.ID
$j.ApplyToTargetServer($s.Name)
$j.StartStepID = $jsid
$j.Alter()
$jsch = new-object ('Microsoft.SqlServer.Management.Smo.Agent.JobSchedule') ($j, 'Sched 01')
$jsch.FrequencyTypes = 'Daily'
$jsch.FrequencySubDayTypes = 'Once'
$startts = new-object System.Timespan(2, 0, 0)
$jsch.ActiveStartTimeOfDay = $startts
$endts = new-object System.Timespan(23, 59, 59)
$jsch.ActiveEndTimeOfDay = $endts
$jsch.FrequencyInterval = 1
$jsch.ActiveStartDate = get-date
$jsch.Create()
This script will run the powershell.exe executable, not sqlps.exe. If the server where this job is to run is running SQL Server 2008 you can change the JobStep’s SubSystem property to ‘PowerShell’, and the Command property to ‘e:\scripts\backup.ps1’. The job is created by using the new-object cmdlet to create a Job object, and setting its properties. Then a JobStep object is created, and its properties set as well. Once the step properties have been set and the step created, the JobStep.ID property is then used to set the StartStepID property of the job, so Agent knows where to start the job. Finally a schedule is needed, and in this case the job is set up using the properties in the JobSchedule object to run the job once daily at 2am, starting today.
Another powerful feature of PowerShell is the ability to easily retrieve information from the Windows Management Instrumentation (WMI) objects. SMO provides access to WMI, which allows you to manage the SQL Server services. An example would be the following script which returns the IP port number used by the instances created on the local system:
#tcpport.ps1
#Evaluates the SQL Server instances on a Windows server and returns the TCP port number used by each instance
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.SMO") | Out-Null
$m = New-Object ('Microsoft.SqlServer.Management.Smo.WMI.ManagedComputer') 'HOME'
$m.ServerInstances | ForEach-Object { $m.Name + '\' + $_.Name + ', ' +
$m.ServerInstances[$_.Name].ServerProtocols['Tcp'].IPAddresses['IP1'].IPAddress.IPAddressToString + ':' +
$m.ServerInstances[$_.Name].ServerProtocols['Tcp'].IPAddresses['IPAll'].IPAddressProperties['TcpDynamicPorts'].Value
}
In addition to the SMO WMI objects, PowerShell provides the Get-WMIObject cmdlet (aliased as gwmi) to return information from these objects. We can return information about our current system by executing the following statement:
gwmi -query "select * from Win32_ComputerSystem" | select Name, Model,
Manufacturer, Description, DNSHostName, Domain, DomainRole,
NumberOfProcessors, SystemType, TotalPhysicalMemory,
UserName, Workgroup
This will return an object with the properties listed after the select statement - basic information about the physical machine we’re working with.
If we’re interested in finding out about the disk drives on our server, this query will help:
gwmi -query "select * from Win32_LogicalDisk where
DriveType=3" | select Name, FreeSpace, Size
For each locally attached drive (DriveType=3) the properties returned are the drive letter (Name) the amount of free space and the size of the drive, in bytes.
We can even retrieve performance counters, as we can see with this query:
gwmi -query "Select * from Win32_perfFormattedData_PerfOs_System" |
select contextSwitchesPerSec, ProcessorQueueLength
There is a great deal of information available through WMI, and you can get the details on the WMI classes at this link: https://msdn.microsoft.com/en-us/library/aa394554(VS.85).aspx.
Conclusion
Scripting has been a powerful tool for Unix administrators for a long time. Windows administrators have had fewer and less capable options for automating administrative processes until the introduction of PowerShell.
There are a great many sites providing quality information on using PowerShell to automate administrative tasks. The addition of PowerShell support to SQL Server 2008 tool set adds an extra dimension of manageability to the Windows Server environment. You should review the available books and online material to develop your skills in using this powerful tool, and by doing so automate and streamline the processes in your own environment.
About the author. Allen White* *is a SQL Server MVP based in Cleveland, Ohio. He specializes in DBA Automation methods and enjoys teaching SQL Server classes and helping companies use SQL Server more effectively.