Showing posts with label Powershell. Show all posts
Showing posts with label Powershell. Show all posts

Tuesday, January 2, 2018

SQL Server Morning Health Checks

Every now and again as a Microsoft PFE, you get a chance to make a big difference for a customer. One such occasion happened just recently when I was asked to help find a way to automate the daily checks the DBA had to perform every morning. The result was a PowerShell script that reduced that manual task down from an hour to less than a minute.

You can read the full article here on MSDN.
https://blogs.msdn.microsoft.com/samlester/2017/12/29/sql-server-dba-morning-health-checks/

The PowerShell script can be downloaded from here.
https://github.com/PatrickKeisler/SQLMorningHealthChecks

Tuesday, April 18, 2017

Blob Auditing for Azure SQL Database

In February 2017, Microsoft announced the general availability of Blob Auditing for Azure SQL Database. While auditing features were available before in Azure, this is a huge leap forward, especially in having more granular control over what audit records are captured.

Before Blob Auditing, there was Table Auditing. This is something I like to equate to the C2 auditing feature of SQL Server. It’s only configurable options were ON or OFF. In reality, Table Auditing has a few more controls than that, but you get the idea. There was no way to audit actions against one specific table. Blob Auditing provides us with that level of granularity. However, controlling that granularity cannot be accomplished through the Azure Portal; it can only be done with PowerShell or REST API.

Continue reading here...

Thursday, April 13, 2017

AzureRM Module Version

When working with the AzureRM PowerShell module, remember the module is constantly being updated to take advantage of new features added to Azure.

Continue reading here...

Tuesday, August 16, 2016

SQLPSX is Finally Getting Updated

The most current code is now on Github, with the Codeplex version being depreciated. You can read all about the planned updates from Mike Shepard.

The Future of SQLPSX
https://powershellstation.com/2016/07/13/the-future-of-sqlpsx/

SQLPSX Update
https://powershellstation.com/2016/07/31/sqlpsx-update/

Tuesday, September 3, 2013

The Case of the NULL Query_Plan

As a DBA, we're often asked to troubleshoot performance issues for stored procedures.  One of the most common tools at our disposal is the query execution plan cached in memory by SQL Server. Once we have the query plan, we can dissect what SQL Server is doing and hopefully find some places for improvement.

Grabbing the actual XML query plan for a stored procedure from the cache is fairly easy using the following query.

USE AdventureWorks2012;
GO
SELECT qp.query_plan FROM sys.dm_exec_procedure_stats ps
    JOIN sys.objects o ON ps.object_id = o.object_id
    JOIN sys.schemas s ON o.schema_id = s.schema_id
    CROSS APPLY sys.dm_exec_query_plan(ps.plan_handle) qp
WHERE ps.database_id = DB_ID()
    AND s.name = 'dbo'
    AND o.name = 'usp_MonsterStoredProcedure';
GO


From this point, we can open the XML query plan in Management Studio or Plan Explorer to start our investigation. But what happens if SQL Server returns NULL for the query plan?


Let's back up a little bit.  We were pretty sure the query plan is still in cache, right?  Let's verify it.

USE AdventureWorks2012;
GO
SELECT * FROM sys.dm_exec_procedure_stats ps
    JOIN sys.objects o on ps.object_id = o.object_id
WHERE o.name = 'usp_MonsterStoredProcedure';
GO

Sure enough.  The query plan is still cached in memory, and we even can even see the plan_handle.


So why did our first query not return the XML plan?  Let's copy the plan_handle and manually run it through the sys.dm_exec_query_plan function.

SELECT * FROM sys.dm_exec_query_plan(0x05000500DD93100430BFF0750100000001000000000000000000000000000000000000000000000000000000);
GO

Why are we getting NULL returned for the XML query plan when we know is in the cache?  In this case, because the query plan is so large and complex, we're hitting an XML limitation within SQL Server.  "XML datatype instance has too many levels of nested nodes. Maximum allowed depth is 128 levels".  

Let's try to pull the text version of query plan.
SELECT * FROM sys.dm_exec_text_query_plan(0x05000500DD93100430BFF0750100000001000000000000000000000000000000000000000000000000000000,DEFAULT,DEFAULT);
GO


It looks as though we have solved the issue; however, we didn't.  Management Studio has a 65535 character limit in grid results and 8192 character limit in text results.  Our query plan has been truncated far from the end.  Now it seems we are back to square one.  

We still know the query plan is in cache, but we just need a tool other than Management Studio to retrieve it.  This is where Powershell enters the picture.

With Powershell, we can create a simple script to execute the sys.dm_exec_text_query_plan function and then output the data to a file.  All we need is to pass two variables.  The first is the SQL Server name where the plan is cached, and the second is the plan_handle. 

param (    
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]
    $SqlInstance

   ,[Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [string]
    $PlanHandle
)

The script will simply execute a TSQL script and capture the output into a string variable.
$SqlCommand = "SELECT query_plan FROM sys.dm_exec_text_query_plan(" 
    + $PlanHandle + ",DEFAULT,DEFAULT);"
$QueryPlanText = $cmd.ExecuteScalar()

The final step will use System.IO.StreamWriter() to output the data to a file.
$stream = New-Object System.IO.StreamWriter($FileName)
$stream.WriteLine($QueryPlanText)


The Powershell script will save the entire XML query plan to a file named output.sqlplan.  As you can see below, the actual plan was over 5MB.


Finally we're able to view the entire query plan in our favorite tool and see the complexity of the stored procedure.


This is just another example of why  DBAs need to set aside some time to learn Powershell.  The entire script is posted below.  Feel free to modify it as needed to fit your environment.

######################################################################################
#
#   File Name:    Get-QueryPlan.ps1
#
#   Applies to:   SQL Server 2008
#                 SQL Server 2008 R2
#                 SQL Server 2012
#
#   Purpose:      Used to retrieve an XML query plan from cache.
#
#   Prerequisite: Powershell must be installed.
#                 SQL Server components must be installed.
#
#   Parameters:   [string]$SqlInstance - SQL Server name (Ex: SERVER\INSTANCE)
#                 [string]$PlanHandle - Binary query handle
#
#   Author:       Patrick Keisler
#
#   Version:      1.0.0
#
#   Date:         08/30/2013
#
#   Help:         http://www.patrickkeisler.com/
#
######################################################################################

#Define input parameters
param ( 
  [Parameter(Mandatory=$true)]
  [ValidateNotNullOrEmpty()]
  [string]
  $SqlInstance
  
  ,[Parameter(Mandatory=$true)]
  [ValidateNotNullOrEmpty()]
  [string]
  $PlanHandle
  )

Write-Host "Script starting."

#Grab the path where the Powershell script was executed from.
$path = Split-Path $MyInvocation.MyCommand.Path

#Build the SQL Server connection objects
$conn = New-Object System.Data.SqlClient.SqlConnection
$builder = New-Object System.Data.SqlClient.SqlConnectionStringBuilder
$cmd = New-Object System.Data.SqlClient.SqlCommand

#Build the TSQL statement & connection string
$SqlCommand = "SELECT query_plan FROM sys.dm_exec_text_query_plan(" + $PlanHandle + ",DEFAULT,DEFAULT);"
$builder.psBase.DataSource = $SqlInstance
$builder.psBase.InitialCatalog = "master"
$builder.psBase.IntegratedSecurity = $true
$builder.psBase.ApplicationName = "Get-QueryPlan"
$builder.psBase.Pooling = $true
$builder.psBase.ConnectTimeout = 15
$conn.ConnectionString = $builder.ConnectionString
$cmd.Connection = $conn
$cmd.CommandText = $SqlCommand

try
{
 if ($conn.State -eq "Closed")
 {
  #Open a connection to SQL Server
  $conn.Open()
 }
 
 #Execute the TSQL statement
 [string]$QueryPlanText = $cmd.ExecuteScalar()

 #Write the output to a file
 $FileName = $path + "\output.sqlplan"
 $stream = New-Object System.IO.StreamWriter($FileName)
 $stream.WriteLine($QueryPlanText)

 if ($stream.BaseStream -ne $null)
 {
  #Close the stream object
  $stream.close()
 }

 if ($conn.State -eq "Open")
 {
  #Close the SQL Server connection
  $conn.Close()
 }
 
 Write-Host "Script completed successfully."
}
catch
{
 #Capture errors if needed
 if ($_.Exception.InnerException)
 {
  $Host.UI.WriteErrorLine("ERROR: " + $_.Exception.InnerException.Message)
  if ($_.Exception.InnerException.InnerException)
  {
   $Host.UI.WriteErrorLine("ERROR: " + $_.Exception.InnerException.InnerException.Message)
  }
 }
 else
 {
  $Host.UI.WriteErrorLine("ERROR: " + $_.Exception.Message)
 }
 
 Write-Host .
 Write-Host "ERROR: Script failed."
}

Tuesday, March 26, 2013

Use Powershell to Pick Up what Database Mirroring Leaves Behind

Database mirroring has been around since SQL Server 2005, and it's turned out to be an excellent step up from log shipping.  However, like log shipping, it is still only a database-level disaster recovery solution.  Meaning that any logins, server role memberships or server-level permissions will not be mirrored over to the mirror server.  This is where the DBA needs to plan ahead and create their own custom jobs to script and/or document these types of shortcomings.

My solution is to use Powershell.  In this example, I have setup database mirroring for the AdventureWorks2012 database.  For this demo, both instances, TEST1 and TEST2, are on the same physical server.


There are two logins on the principal server that currently do not exist on the mirror server.  One is a SQL login, AWLogin1, and the other is a Windows Authenticated login, TRON2\AWLogin2.

The first step of our Powershell script will need to connect to the principal server to generate a CREATE LOGIN script for those two logins.  To generate the script, we need to grab the login name, the SID, and the hashed password if it's a SQL login.  This is accomplished by running the following code.

SELECT 'USE master; CREATE LOGIN ' + QUOTENAME(p.name) + ' ' +
CASE WHEN p.type in ('U','G')
    THEN 'FROM WINDOWS '
    ELSE ''
    END
+ 'WITH ' +
CASE WHEN p.type = 'S'
    THEN 'PASSWORD = ' + master.sys.fn_varbintohexstr(l.password_hash) + ' HASHED, ' + 'SID = ' + master.sys.fn_varbintohexstr(l.sid) +  ', CHECK_EXPIRATION = ' +
    CASE WHEN l.is_expiration_checked > 0
        THEN 'ON, '
        ELSE 'OFF, '
        END
    + 'CHECK_POLICY = ' +
    CASE WHEN l.is_policy_checked > 0
        THEN 'ON, '
        ELSE 'OFF, '
        END +
    CASE WHEN l.credential_id > 0
        THEN 'CREDENTIAL = ' + c.name + ', '
        ELSE ''
        END
ELSE ''
END
+ 'DEFAULT_DATABASE = ' + p.default_database_name +
CASE WHEN LEN(p.default_language_name) > 0
    THEN ', DEFAULT_LANGUAGE = ' + p.default_language_name
    ELSE ''
    END
+ ';' AS 'LoginScript'
FROM master.sys.server_principals p LEFT JOIN master.sys.sql_logins l
    ON p.principal_id = l.principal_id LEFT JOIN master.sys.credentials c
    ON l.credential_id = c.credential_id
WHERE p.type IN ('S','U','G')
    AND p.name NOT IN ('sa', 'NT AUTHORITY\SYSTEM')
    AND p.name NOT LIKE '##%##'
    AND p.name NOT LIKE 'BUILTIN\%'
    AND p.name NOT LIKE 'NT SERVICE\%'
ORDER BY p.name;

In this example, you can see we have one row for each of the two logins.


The next step of the Powershell script will need to write those two rows of data to a file on the mirror server.  This is done using the System.IO.StreamWriter class.

foreach($row in $commandList.Tables[0].Rows)
{
    try
    {
            $output = $row["LoginScript"].ToString()
            $stream.WriteLine($output)
      }
      catch
      {
            $stream.Close()
            CheckForErrors
      }
}

When there is a need to failover to the mirror server, the DBA can then open this script and run it.  All logins will be created and with their original SID value and password.

The second half of the Powershell script will use the same procedures to script out any server role memberships or server-level permissions these two logins may have on the principal server.  This is done using the following block of code.

-- BUILD SERVER ROLE MEMBERSHIPS
SELECT 'USE master; EXEC sp_addsrvrolemember @loginame = '+QUOTENAME(s.name)+', @rolename = '+QUOTENAME(s2.name)+';'  AS 'ServerPermission'
FROM master.sys.server_role_members r INNER JOIN master.sys.server_principals s
    ON s.principal_id = r.member_principal_id INNER JOIN master.sys.server_principals s2
    ON s2.principal_id = r.role_principal_id
WHERE s2.type = 'R'
    AND s.is_disabled = 0
    AND s.name NOT IN ('sa','NT AUTHORITY\SYSTEM')
    AND s.name NOT LIKE '##%##'
    AND s.name NOT LIKE 'NT SERVICE\%'
UNION ALL
-- BUILD SERVER-LEVEL PERMISSIONS
SELECT 'USE master; '+sp.state_desc+' '+sp.permission_name+' TO '+QUOTENAME(s.name) COLLATE Latin1_General_CI_AS+';'  AS 'ServerPermission'
FROM sys.server_permissions sp JOIN sys.server_principals s
    ON sp.grantee_principal_id = s.principal_id
WHERE s.type IN ('S','G','U')
    AND sp.type NOT IN ('CO','COSQ')
    AND s.is_disabled = 0
    AND s.name NOT IN ('sa','NT AUTHORITY\SYSTEM')
    AND s.name NOT LIKE '##%##'
    AND s.name NOT LIKE 'NT SERVICE\%';

From the output, you can see the TRON\AWLogin2 is a member of the BULKADMIN server role and has the VIEW SERVER STATE permission.  These two rows will be written to a file in the same file share as the previous file.  


As before, once the database is failed over to the mirror server, the DBA can run this script to apply any missing permissions.

Finally, this Powershell script can be scheduled to run from any server; however, I choose to setup this job on the principal server.  I schedule it to run once a day through SQL Agent.  Each run of the script will overwrite the existing file, so if there are any logins or permissions that have been added or removed, it will show up in the latest version of the files.


Using this Powershell script can make it very easy to script out logins and permissions.  While this example was used with database mirroring, then same strategy will work for log shipping.  The entire Powershell script is below.

Tuesday, February 12, 2013

T-SQL Tuesday - Use Powershell to Restore a Database on a Different Server

T-SQL Tuesday - This month's party is hosted by Wayne Sheffield (blog|twitter), and the topic is about Powershell and how to use it for anything SQL Server.

With that challenge, I'd like to share a script I've written that takes a backup file from one server, copies to another server, and and then restores it.  That may sound pretty easy, but I've added in a few requirements to the restore.





Here's the scenario:
We have two SQL Servers, one production (TRON2\R2PROD) and one test (TRON3\R2TEST), and we have one user database (AdventureWorks2008R2) on each of the production and test servers.  The test server is used by a developer.  The developer send us a request to "refresh the development database with a copy of production".  This translates into: he needs the most recent backup of that production database copied from the production server over to the test server, then restored to it by overwriting the existing database, all while preserving his existing dbo level permissions.

The manual approach to completing this task.
  1. Figure out which full database backup file is the most recent for AdventureWorks2008R2.
  2. Copy the file from TRON2 to TRON3.
  3. On TRON3\R2TEST, script out all existing user permissions for the AdventureWorks2008R2 database.
  4. Restore the backup.
  5. Run the script from step 3 to reapply the developers permissions.
  6. Delete the backup file from TRON3.
Total time to execute this task manually: ~ 10 minutes.

That many not seem like much time out of your entire workday, but what if that same developer wants you to complete this task each morning at 8AM.  Now you're up to 10 minutes per day.  And what if he asked you to do it several times a day, every day of the week.  That 10 minutes can really add up.

The Powershell approach to completing this task.
  1. Run the AutoDatabaseRefresh.ps1 script.
Total time to execute this task using Powershell: < 30 seconds.

How's that for performance improvement?

The great thing about Powershell is that it allows you to connect to different systems, such as Windows and SQL Server, all from a single programming language.  The entire script is written using the SQL Management Objects (SMO).  It does not use any of the SQL Server cmdlets, so there are no modules to import.  Let's take a closer look.

For this script you need to pass 6 parameters to this script.
  1. $sourceInstance - Source SQL Server name
    • Example: "TRON2\R2PROD"
  2. $sourceDbName - Source database
    • Example: "AdventureWorks2008R2"
  3. $sourcePath - Source share where the file exists (UNC Path)
    • Example: "\\TRON2\BACKUP\R2PROD\AdventureWorks2008R2"
  4. $destinationInstance - Destination SQL Server name
    • Example: "TRON3\R2TEST"
  5. $destinationDbName - Database to be refreshed on destination server
    • Example: "AdventureWorks2008R2"
  6. $destinationPath - Destination share to copy backup file to (UNC Path)
    • Example: "\\TRON3\BACKUP"
The script needs to know both the source and destination SQL Servers (#1 and #4), and the source and destination database names (#2 and #5).  The other two parameters are the source paths (#3 and #6) and they must be UNC file shares.  This is so the Powershell script can be executed from any server or from any DBA's workstation.

The basic workflow of the Powershell script is as follows:

Step 1:  Validate the input parameters.  All connectivity to the SQL Servers and to the file shares use Windows Authentication.  Tests for blank parameters.  Tests the connectivity to each SQL Server. Test that each file share exists.  If any of these validation tests fail, the script will halt.

if([String]::IsNullOrEmpty($sourceInstance))
{
    Write-Host "ERROR"
    $errorMessage = "Source server name is not valid."
    throw $errorMessage
}

Step 2:  Connect to $sourceInstance to get the name of the most recent backup file for $sourceDbName.  This is accomplished by running this TSQL script.

$server = GetServer($serverInstance)
$db = $server.Databases["msdb"]
$fileList = $db.ExecuteWithResults(
    @"
DECLARE
     @BackupId int
    ,@DatabaseName nvarchar(255);

SET @DatabaseName = '$sourceDbName';

-- Get the most recent full backup for this database
SELECT TOP 1
     @DatabaseName AS DatabaseName
    ,m.physical_device_name
    ,RIGHT(m.physical_device_name, CHARINDEX('\',REVERSE(physical_device_name),1) - 1) AS 'FileName'
    ,b.backup_finish_date
    ,b.type AS 'BackupType'
FROM msdb.dbo.backupset b JOIN msdb.dbo.backupmediafamily m
ON b.media_set_id = m.media_set_id
WHERE b.database_name = @DatabaseName
    AND b.type = 'D'
    AND b.is_snapshot = 0
    AND b.is_copy_only = 0
    AND b.backup_finish_date IS NOT NULL
ORDER BY b.database_backup_lsn DESC;
    "@

This give us the following output.


Step 3:  Copy the file from $sourcePath to $destinationPath.  From the output above, the physical file, AdventureWorks2008R2_db_201302060836.BAK, is located in D:\Backup\R2PROD\AdventureWorks2008R2, so the $sourcePath must match this location.  Our UNC path is \\TRON2\BACKUP\R2PROD\AdventureWorks2008R2.  This step uses the Copy-Item cmdlet.  In my testing I have seen this cmdlet outperform the regular Windows copy and even Robocopy.

$source = $sourcePath + "\" + $backupFile
Write-Host "Copying file..."
copy-item $source -destination $destinationpPath

Step 4:  Connect to $destinationInstance and script out all user-level permissions and database roles for the $destinationDbName.  The is accomplished by using the following script.

$server = GetServer($serverInstance)
$db = $server.Databases["$destinationDbName"]
if(-not $db)
{
    Write-Host "Database does not exist on: $serverInstance"
}
else
{
    Write-Host "Saving permissions on $destinationDbName..." -NoNewline
    $commandList = $db.ExecuteWithResults(
        @"
IF OBJECT_ID('tempdb..#Commands') IS NOT NULL
    DROP TABLE #Commands;
CREATE TABLE #Commands(
     RowId int identity(1,1)
    ,Cmd varchar(2000));

INSERT #Commands(Cmd)
SELECT 'USE [$destinationDbName];IF EXISTS (SELECT * FROM sys.database_principals WHERE name = N'+QUOTENAME(d.name,CHAR(39))+') ALTER USER ' + QUOTENAME(d.name) + ' WITH LOGIN = ' + QUOTENAME(s.name) + ';'
FROM [$destinationDbName].sys.database_principals d LEFT OUTER JOIN master.sys.server_principals s
    ON d.sid = s.sid
WHERE s.name IS NOT NULL
    AND d.type = 'S'
    AND d.name <> 'dbo';

INSERT #Commands(Cmd)
SELECT 'USE [$destinationDbName];IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'+QUOTENAME(d.name,CHAR(39))+') CREATE USER ' + QUOTENAME(d.name) + ' FOR LOGIN ' + QUOTENAME(s.name) + ' WITH DEFAULT_SCHEMA = ' + QUOTENAME(d.default_schema_name) + ';'
FROM [$destinationDbName].sys.database_principals d LEFT OUTER JOIN master.sys.server_principals s
    ON d.sid = s.sid
WHERE s.name IS NOT NULL
    AND d.type = 'S'
    AND d.name <> 'dbo';

INSERT #Commands(Cmd)
SELECT 'USE [$destinationDbName];IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'+QUOTENAME(d.name,CHAR(39))+') CREATE USER ' + QUOTENAME(d.name) + ' FOR LOGIN ' + QUOTENAME(s.name) + ';'
FROM [$destinationDbName].sys.database_principals d LEFT OUTER JOIN master.sys.server_principals s
    ON d.sid = s.sid
WHERE s.name IS NOT NULL
    AND d.type IN ('U','G');

INSERT #Commands(Cmd)
SELECT 'USE [$destinationDbName];IF NOT EXISTS (SELECT * FROM sys.database_principals WHERE name = N'+QUOTENAME(p.name,CHAR(39))+') CREATE ROLE ' + QUOTENAME(p.name) + ' AUTHORIZATION '+QUOTENAME(o.name)+';'
FROM [$destinationDbName].sys.database_principals p JOIN [$destinationDbName].sys.database_principals o
    ON o.principal_id = p.owning_principal_id
WHERE p.type = 'R'
    AND p.is_fixed_role = 0
    AND p.principal_id <> 0;

INSERT #Commands(Cmd)
SELECT 'USE [$destinationDbName];EXEC sp_addrolemember N' + QUOTENAME(d.name,'''') + ', N' + QUOTENAME(m.name,CHAR(39)) + ';'
FROM [$destinationDbName].sys.database_role_members r JOIN [$destinationDbName].sys.database_principals d
    ON r.role_principal_id = d.principal_id JOIN [$destinationDbName].sys.database_principals m
    ON r.member_principal_id = m.principal_id
WHERE m.principal_id > 5;

INSERT #Commands(Cmd)
SELECT 'USE [$destinationDbName];' + dp.state_desc + ' ' + dp.permission_name + ' TO ' + QUOTENAME(d.name) COLLATE Latin1_General_CI_AS + ';'
FROM [$destinationDbName].sys.database_permissions dp JOIN [$destinationDbName].sys.database_principals d
    ON dp.grantee_principal_id = d.principal_id
WHERE dp.major_id = 0
    AND dp.state <> 'W'
    AND dp.permission_name <> 'CONNECT'
ORDER BY d.name, dp.permission_name ASC, dp.state_desc ASC;

INSERT #Commands(Cmd)
SELECT 'USE [$destinationDbName];GRANT ' + dp.permission_name + ' TO ' + QUOTENAME(d.name) COLLATE Latin1_General_CI_AS + ' WITH GRANT OPTION;'
FROM [$destinationDbName].sys.database_permissions dp JOIN [$destinationDbName].sys.database_principals d
    ON dp.grantee_principal_id = d.principal_id
WHERE dp.major_id = 0
    AND dp.state = 'W'
    AND dp.permission_name <> 'CONNECT'
ORDER BY d.name, dp.permission_name ASC, dp.state_desc ASC;

SELECT Cmd FROM #Commands
ORDER BY RowId;
        "@
}

This gives us the existing permissions that we'll re-apply later in step 6.  You can see we're creating code to resync logins (ALTER USER...WITH LOGIN), create the user if it doesn't exist, create database roles if they don't exist, and add users to those database roles.


Step 5:  Restore the backup file to $destinationInstance using the $destinationDbName name.  This is the real meat and potatoes of the script.  

$restore = new-object ('Microsoft.SqlServer.Management.Smo.Restore')
$restore.Database = $destinationDbName
$restore.NoRecovery = $false
$restore.PercentCompleteNotification = 10
$restore.Devices.AddDevice($backupDataFile, [Microsoft.SqlServer.Management.Smo.DeviceType]::File)

First it checks $destinationInstance to see if $destinationDbName already exists.  If it does, then it just restores over it.  If $destinationDbName does not exist, then the script will create it using the RESTORE...WITH MOVE command.  Since the source and destination SQL Servers have different instance names, the file folders for the physical MDF & LDF files will be different.  The script uses the default folder locations to store the data and log files.  This folders were specified when you installed SQL Server.  If the $sourceDbName has several NDF files, all of them will be placed in the default data folder.

$defaultMdf = $server.Settings.DefaultFile
$defaultLdf = $server.Settings.DefaultLog


Before the restore, the script will set the recovery mode of $destinationDbName to SIMPLE.  This is avoid the "backup tail log" error message in case the database is in FULL recovery mode.  It sets the database to single-user mode to kill any existing connections before the restore.  And after the restore is complete, it sets the recovery mode back to SIMPLE.

$db.RecoveryModel = [Microsoft.SqlServer.Management.Smo.RecoveryModel]::Simple
$db.UserAccess = "Single"
$db.Alter(
    [Microsoft.SqlServer.Management.Smo.TerminationClause]
    "RollbackTransactionsImmediately")

Step 6:  Apply the saved permissions from step 4 to $destinationDbName.  These are the permissions that were scripted out from step 4.  They are applied to the $destinationDbName one line at a time.

foreach($Row in $commandList.Tables[0].Rows)
{
    $db.ExecuteNonQuery($Row["Cmd"])
}

Step 7:  Delete the backup from $destinationPath.  This is the cleanup step.

remove-item $backupFile

When running the script from a console, the output will look like this.

=============================================================
 1: Perform Initial Checks & Validate Input Parameters
=============================================================
Validating parameters...OK
Verifying source SQL Server connectivity...OK
Verifying source database exists...OK
Verifying destination SQL Server connectivity...OK
Verifying source file share exists...OK
Verifying destination file share exists...OK

=============================================================
 2: Get Source Backup for the Restore
=============================================================
Connecting to TRON2\R2PROD to find a restore file...
Selected file:  D:\Backup\R2PROD\AdventureWorks2008R2\AdventureWorks2008R2_db_201302060836.BAK
Verifying file: \\TRON2\BACKUP\R2PROD\AdventureWorks2008R2\AdventureWorks2008R2_db_201302060836.BAK exists...
Source file existence: OK

=============================================================
 3: Copy Backup File to the Destination
=============================================================
Copying file...
Copy file: OK

=============================================================
 4: Get Current Permissions on the Destination Database
=============================================================
Saving permissions on AdventureWorks2008R2...OK

=============================================================
 5: Restore Backup File to the Destination Server
=============================================================
Restoring database...
Database Restore: OK

=============================================================
 6: Restore Permissions to the Destination Database
=============================================================
Restoring existing permissions...
Existing permissions restored: OK

=============================================================
 7: Delete Backup File from the Destination Server
=============================================================
Deleting file...
Delete file: OK

=============================================================
    Database refresh completed successfully
=============================================================

The best part about using the Powershell script, is you can setup a SQL Agent job to call the script with the parameters already specified.  That way when the developer asks you refresh the same database then all you have to do is run the job, or you can work the developer to schedule the job to run automatically each day.


The SQL Agent job will need to setup as an "Operating system (CmdExec)" job type.  This is because it uses Powershell components that are outside the normal SQLPS group of commands.

The entire script is below.  Feel free to modify it as you see fit for your environment.