Yesterday I had the honor and pleasure of giving a session on managing your enterprise environment with PowerShell.  Thanks to all that have joined, and the scripts can be found here: http://sdrv.ms/18WJhSM

These examples and this code is provided to show an example and to illustrate a few methods in managing database servers with PowerShell and is not meant to be run in a production environment.

This Sample Code is provided for the purpose of illustration only
and is not intended to be used in a production environment.
THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED “AS IS”
WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING
BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a nonexclusive,
royalty-free right to use and modify the Sample Code and to reproduce
and distribute the object code form of the Sample Code, provided
that You agree: (i) to not use Our name, logo, or trademarks
to market Your software product in which the Sample Code is embedded; 
(ii) to include a valid copyright notice on Your software product
in which the Sample Code is embedded; and (iii) to indemnify,
hold harmless, and defend Us and Our suppliers from and against  any claims or lawsuits, including attorneys’ fees, that arise
or result from the use or distribution of the Sample Code.

Enjoy, and thanks for joining!

One of our greatest tools for troubleshooting poor performance across the board for an instance is through the use of sys.dm_os_wait_stats.  But it can be a bit cumbersome, as the counters in this DMV only get reset upon service restart (or manual clearing).  A common practice is to grab a starting/reference point of the wait stats, wait a certain amount of time, and then get the wait stats again.  With a little simple math, you’re able to calculate the wait counts and durations during that period of time.

But let’s take that a step further.  What if you don’t want one single point of reference (i.e. time A to time B)?  What if you want to have a flexible and disconnected way to control excluded and benign wait types?  What if you want to output that data to a CSV file with ease for a little Excel graphing and visualization?  Let’s try this through PowerShell.

I wrote a function in order to grab the wait stats of any given instance.

Function Get-WaitStats {
    param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [String]$SqlServerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false, Position = 1)]
        [Switch]$SqlAuth,

        [Parameter(Mandatory = $false, Position = 2)]
        [pscredential]$Credentials
    )

    [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") |
        Out-Null

    try {
        $SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlServerName)
        if ($SqlAuth) {
            if (!$Credentials) {
                Write-Error "You must supply credentials for SQL Server authentication."
            }
            $SqlServer.ConnectionContext.LoginSecure = $false
            $SqlServer.ConnectionContext.Login = $Credentials.UserName
            $SqlServer.ConnectionContext.SecurePassword = $Credentials.Password
        }

        $WaitStatsQuery = "
            select
	            wait_type,
	            waiting_tasks_count,
	            wait_time_ms,
	            max_wait_time_ms,
	            signal_wait_time_ms,
                getdate() as current_datetime
            from sys.dm_os_wait_stats;"

        $SqlServer.Databases["master"].ExecuteWithResults($WaitStatsQuery).Tables[0]
    }

    catch {
        Write-Error $_.Exception
    }
}

All that function really does is pull the wait stats for a particular instance (as supplied by the appropriate parameter). The calculating, iterating, and all around leg work is done in the script’s surrounding code.

param (
    [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
    [String]$SqlServerName = $env:COMPUTERNAME,

    [Parameter(Mandatory = $false, Position = 1)]
    [int]$SampleIntervalSeconds = 10,

    [Parameter(Mandatory = $false, Position = 2)]
    [int]$Iterations = 1,

    [Parameter(Mandatory = $false, Position = 3)]
    [String[]]$WaitTypesToExclude,

    [Parameter(Mandatory = $false, Position = 4)]
    [Switch]$SqlAuth,

    [Parameter(Mandatory = $false, Position = 5)]
    [pscredential]$Credentials
)

# see above function for full definition
Function Get-WaitStats {...}

while ($Iterations -gt 0) {
    try {
        $WaitStatsBefore = Get-WaitStats -SqlServerName $SqlServerName -SqlAuth:$SqlAuth -Credentials $Credentials -ErrorAction Stop
        Start-Sleep -Seconds $SampleIntervalSeconds
        $WaitStatsAfter = Get-WaitStats -SqlServerName $SqlServerName -SqlAuth:$SqlAuth -Credentials $Credentials -ErrorAction Stop

        $WaitStatsOutput = foreach ($WaitStat in $WaitStatsAfter) {
            if (($WaitStat.wait_time_ms -gt 0) -and ($WaitTypesToExclude -notcontains $WaitStat.wait_type)) {
                $WaitStat |
                    Select-Object wait_type,
                        @{Name = "wait_time_ms"; Expression = {$WaitStat.wait_time_ms - ($WaitStatsBefore | Where-Object {$_.wait_type -eq $WaitStat.wait_type}).wait_time_ms}},
                        @{Name = "waiting_tasks_count"; Expression = {$WaitStat.waiting_tasks_count - ($WaitStatsBefore | Where-Object {$_.wait_type -eq $WaitStat.wait_type}).waiting_tasks_count}},
                        @{Name = "start_time"; Expression = {($WaitStatsBefore | Where-Object {$_.wait_type -eq $WaitStat.wait_type}).current_datetime}},
                        @{Name = "end_time"; Expression = {$WaitStat.current_datetime}} |
                            Where-Object {$_.wait_time_ms -gt 0}
            }
        }

        $WaitStatsOutput |
            Sort-Object wait_time_ms -Descending

        $Iterations--
    }

    catch {
        Write-Error $_.Exception
        break
    }
}

This PowerShell code simply iterates for the amount of specified iterations (default 1 if not specified) and grabs the wait stats initially, waits for the specified time (default 10 seconds if not specified. Feel free to alter the parameter defaults or slice and dice this script to fit your needs), and then pulls wait stats again. The calculations are made, and then outputted.

What I really like about this approach is the ability to specify wait types to ignore. The parameter that defines this is $WaitTypesToExclude, a string array. What I do is have a list of text files that on each line have a wait type that I don’t care about. Maybe a text file per version of SQL Server. Then for this parameter, we can do a simple Get-Content to read that text file into the string array for wait types to exclude.

Below is one way that you can consume this script:

.\WaitStatsCollector.ps1 -SqlServerName "YourSqlServerName" -SampleIntervalSeconds 60 -Iterations 10 -WaitTypesToExclude (Get-Content -Path "C:\YourDir\Benign_Waits_List.txt") |
    Export-Csv -Path "C:\YourDir\WaitStatsCollector.csv" -NoTypeInformation

This code isn’t complicated, and it isn’t doing rocket science. But if you’re looking for a flexible way to get wait stats, then this code can hopefully be of use to you. As always, test this code in a non-production environment and ensure that you understand it, and that it performs how you hope it does.

If you have any questions, comments, or suggestions please feel to leave a comment below or email me at sqlsalt@outlook.com.

In the first installment of my mini-series for adding a database file to an Availability Group database, I wrote about the right way to do this operation if there were disparate database file directory structures between the primary replica and the secondary replica(s).

But let’s be honest, we don’t always get it right.  Sometimes things don’t happen the way you planned for them to happen, or maybe it was just a simple oversight.  But, alas, all is not lost.

If you were to add a database file to the primary replica of an Availability Group database (for my testing purposes, the AG database is AdventureWorks2012 and my Availability Group is AgTest1) and the same directory structure does not exist on the secondary replica(s), then you might find an undesirable outcome to this.  Looking at the secondary replica, you would see a similar “red flag” in Object Explorer:

NotSynchronizing_Suspect

That’s not good!  Just by looking here we would see the AG database on the secondary replica is now not synchronizing and the database state is now suspect.  Let’s run a quick query to just confirm:

-- run on the secondary replica(s)

select
	db_name(drs.database_id) as database_name,
	ag.name as ag_name,
	drs.synchronization_state_desc,
	drs.synchronization_health_desc,
	drs.database_state_desc,
	drs.suspend_reason_desc
from sys.dm_hadr_database_replica_states drs
inner join sys.availability_groups ag
on drs.group_id = ag.group_id;

-- OUTPUT
/*
database_name			ag_name		synchronization_state_desc	synchronization_health_desc		database_state_desc		suspend_reason_desc
AdventureWorks2012		AgTest1		NOT SYNCHRONIZING			NOT_HEALTHY						SUSPECT					SUSPEND_FROM_REDO
*/

You can see from the output of that diagnostic query against sys.dm_hadr_database_replica_states shows that the database isn’t synchronizing, it’s not healthy, and it’s suspect.  It may or may not be totally evident right off the bat, but if the cause is because of the added database file you will see a handful of the following error in the SQL Server error log of the secondary replica:

CREATE FILE encountered operating system error 3(The system cannot find the path specified.) while attempting to open or create the physical file ‘C:\SqlServer\AlwaysOn1Dir\AwData3.ndf’.

This basically means the redo attempt of the CREATE FILE operation wasn’t successful on that particular replica.  So at this point, you should have a pretty good idea of what suspended our data synchronization and put the database in a suspect state, if you didn’t already know.

So now we know what the problem is, and it’s time to fix it.  The first step is to take the AG database out of the Availability Group on the secondary replica(s).  It can be done through the following T-SQL:

-- run on the secondary replica(s)

alter database AdventureWorks2012
set hadr off;
go

Now that the database is removed from the Availability Group, we need to do a file backup of the file that we created on the primary replica.

-- run on the primary replica

backup database AdventureWorks2012
file = 'AwData3'
to disk = '\\<Backup_Share>\AW2012_AwData3.bak';
go

Now we will take this file backup and restore it on the secondary replica (WITH NORECOVERY), ensuring to specify the MOVE clause to place the database file in an appropriate and existing directory on the particular replica.

-- run on the secondary replica(s)

restore database AdventureWorks2012
file = 'AwData3'
from disk = '\\<Backup_Share>\AW2012_AwData3.bak'
with
	move 'AwData3' to 'C:\SqlServer\AlwaysOn2Dir\AwData3.ndf',
	norecovery;
go

Now in order to get these databases synced back up, we will run a transaction log backup on the primary replica and restore this transaction log backup on the secondary replica.

-- run on the primary replica

backup log AdventureWorks2012
to disk = '\\<Backup_Share>\AW2012_AddDataFile.trn';
go

-- run on the secondary replica(s)

restore log AdventureWorks2012
from disk = '\\<Backup_Share>\AW2012_AddDataFile.trn'
with norecovery;
go

All we need to do now is simply join the database on the secondary replica back to the Availability Group.

-- run on the secondary replica(s)

alter database AdventureWorks2012
set hadr availability group = AgTest1;
go

At this point we should have a synchronized/synchronizing AG database on the secondary replica in a healthy state. I have illustrated one way to getting back to a healthy state for the Availability Group database after mistakenly adding a database file on the primary replica with a disparate directory structure on participating secondary replica(s). Instead of destroying the Availability Group, or having to work with full backups (which could take a significant amount of time depending on the database size), we can massage the new database file into the secondary replica.

If there are any comments, questions, or suggestions please feel free to leave a comment below or email me at sqlsalt@outlook.com.

When you are working with a database that is joined to an availability group, there are a handful of considerations and “extra steps” to take when making changes.  In this post I am going to be talking about one particular operation:  Adding a database file to an availability group database.

Let’s face it, one of the powerful and flexibility parts of availability groups is to have high availability across disparate environments.  This can include a different directory structure housing the database files of an AG (availability group) database.  You may have run into this when you are setting up the availability group and the participating databases, and had to utilize the MOVE clause when doing the database restore on the secondary replica(s).

No problem, all is working well and your AG database is…always on.  But now you need to add another database file; say a data file.  If your directory structure isn’t symmetric on the secondary replica(s) for the new location of the file then you could run into a high availability-halting scenario (the fix for this will be blogged about in Part 2 of this mini-series).  Documented below is the right way to add a database file to an availability group database.

On the secondary replica(s), remove the database from the AG.  This can be done through SQL Server Management Studio (SSMS) by drilling down into AlwaysOn High Availability -> Availability Groups -> <Your AG Name> -> Availability Databases -> [Right-Click] <Your AG Database Name> and selecting Remove Database from Availability Group.  This can also be done through T-SQL (and I will be using the T-SQL route for the rest of the post for brevity):

-- run on the secondary replica(s)
use master;
go

alter database AdventureWorks2012
set hadr off;
go

Now with transactions not being synced to this replica for the database (in my case, the database is AdventureWorks2012 and my availability group is named AgTest1) I can add the database file to the availability group database on the primary replica:

-- run on the primary replica
alter database AdventureWorks2012
add file
(
	name = AwData2,
	filename = 'C:\SqlServer\AlwaysOn1Dir\AwData2.ndf',
	size = 100MB
);
go

The reason I’m having to go through this trouble is because the directory C:\SqlServer\AlwaysOn1Dir\ does not live on my secondary replica.  With the database file added, I now need to run a transaction log backup against the primary replica’s database:

-- run on the primary replica
backup log AdventureWorks2012
to disk = '\\MyShareDir\AwAdFile_Test1.trn';
go

Going back to the secondary replica(s), I will apply this transaction log backup with the MOVE clause to ensure the added database file gets put in a valid and usable location:

-- run on the secondary replica(s)
restore log AdventureWorks2012
from disk = '\\MyShareDir\AwAdFile_Test1.trn'
with
	move 'AwData2' to 'C:\SqlServer\AlwaysOn2Dir\AwData2.ndf',
	norecovery;
go

The original database file’s location didn’t exist on this secondary replica, so I used an existing and valid directory structure (note:  The child directory is now named AlwaysOn2Dir).  Once that transaction log has been restored on the secondary replica(s), I can rejoin the database to the availability group:

-- run on the secondary replica(s)
use master;
go

alter database AdventureWorks2012
set hadr availability group = AgTest1;
go

Now at this point we have a synchronized (or synchronizing, depending on the commit mode) availability group database without any interference due to this database file operation.

In Part 2 of this mini-series I will be talking about what to do if you attempt to add a database file without a symmetric directory structure on the secondary replica(s) and you don’t remove the AG database.  There are a few extra steps to get back to a healthy state.  If there are any questions or comments please feel free to leave a comment below or email me at sqlsalt@outlook.com.

T-SQL Tuesday #40

This month’s T-SQL Tuesday is hosted by Jen McCown, and the topic is filegroups.  I chose to write about one way to find out how many filegroups a particular database has, as well as if the PRIMARY filegroup is default.  There are a few reasons why you may want to do this check.  For instance, if filegroup restoring is part of your Disaster Recovery plan in order to get the database online as quickly as possible then there are a few basic necessities to ensure this is possible and streamlined.

One of the checks is to see if there are more than one filegroup.  After all, the power of a filegroup restore is to trickle the restores instead of waiting for the entire database to restore and bring the database online.

The second check is to see if the PRIMARY filegroup is the default filegroup.  One of the reasons why this may not be desirable is because when database objects are created without specifying a destination filegroup, they will be created in the default filegroup.  This may bloat the PRIMARY filegroup if it is the default filegroup (which it is…by default), and delay the database from being brought online with a filegroup restore.  More information regarding the default filegroup can be found on Books Online.

One way to get this information is through PowerShell and the utilization of SMO.  Below is the sample code to grab this configuration and display it to the host:

[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") |
    Out-Null

##### VARIABLES #####
$SqlServerName = "YourInstanceName"
$SqlDatabaseName = "YourDatabaseName"
### END VARIABLES ###

$SqlServer = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlServerName)

$DbFileGroups = $SqlServer.Databases[$SqlDatabaseName].FileGroups

# check to see if there is only one filegroup on the specified database
if ($DbFileGroups.Count -eq 1) {
    Write-Host "Warning!!!  $SqlDatabaseName contains only 1 filegroup" -ForegroundColor Red
}
else {
    Write-Host "$SqlDatabaseName contains $($DbFileGroups.Count) filegroups" -ForegroundColor Green
}

# check to see if the PRIMARY filegroup is default
if ($DbFileGroups | Where-Object {$_.Name -like "PRIMARY"} |
        Select-Object -ExpandProperty IsDefault) {
    Write-Host "Warning!!!  The PRIMARY filegroup is default" -ForegroundColor Red
}
else {
    Write-Host "The PRIMARY filegroup is not default" -ForegroundColor Green
}

Ultimately, there are just two conditional checks on the FileGroups property of the Database object.  There are two variables that will need to be modified:  $SqlServerName and $SqlDatabaseName.  They are respectively the instance name and the database name to run the check against.

This check is not intended for all databases across the board.  But if you expect a database to have multiple filegroups, and the PRIMARY filegroup not to be default then the above method is a quick way to find this using PowerShell.  If there are any comments, questions, or suggestions please feel free to leave a comment below or email me at sqlsalt@outlook.com.

I am a firm believer that in order to progress in the IT industry all forms of documentation are completely necessary.  Whether we’re talking about Books Online, technical white papers, tutorials, wikis, blogs, or any other form of documentation they are the ways to pass off the baton from memory to written word.  How many times have we all said “hmmmm I remember seeing that before but I can’t quite recall how I fixed that”.  That is the catch-phrase of an event you should’ve documented.

In my opinion, the day-in-day-out type of documentation will most likely be a local copy of your notes (or in some enterprises, an active wiki).  This is the equivalent of having that virtual notebook where you jot down things that you don’t want to forget, or you list how to resolve an issue that you just fixed.  There are a handful of products out there that can facilitate this, and my personal choice is Microsoft OneNote.  Below are some tips that I find useful when I locally document for later personal use.

1. Get notes down quickly, then organize later

Books on dream interpretation suggest that when you wake up you immediately you should start writing about what your dream was.  A sloppy, disorganized list.  It is when you remember the most vivid details about what you should permanently persist.  When I am working on a problem (often times with much haste) I will have OneNote open on a separate monitor and simply type my steps and findings.  I don’t care about spelling, I don’t care about format, I don’t care about anything pretty yet.  I’m just trying to get the facts on paper.  Why?  Because when you are immersed in the situation at hand, that is going to be when the steps/symptoms/surrounding facts are most vivid and easily understandable.  As time goes on (as in, minutes passing) you will start forgetting those nitty gritty details.  I am definitely a supporter of attractive documentation, though.  So when all is said and done, and you have all of the facts written that is when it is time to make it look good.  Format the documentation so that it is presentable, easy to read, and accents the important parts.

2. Be detailed….very detailed

Something that seems like a “given” today might not be so evident tomorrow (or a year from now).  When you are documenting your steps, don’t assume that you will remember anything… because most likely your memory will have purged most details of the issue.  Dumb it down for yourself when you document to a degree that leaves nothing up to guessing or memory.

3. Include screenshots, but not too many screenshots

I am a believer that a picture is worth a thousand words.  There’s nothing more reaffirming then seeing a dialog on run #2 and having that exact same dialog as a screenshot with fully configured options and values ready to proceed.  You know exactly what to do and what not to do because, after all, it is clearly shown in your documentation with a nifty screenshot.  But don’t overdo the screenshots.  If it is a no-brainer part of troubleshooting or configuration that simply requires another “Next” click then it would be overdoing it to include that as a screenshot.  You want enough screenshots to show the right way to do it, but not too many that bloat your documentation into being a long scroll of unnecessary pictures.

4. Ensure notes are globally accessible

You never want to be caught in a situation where the next sentence that comes out of your mouth is “if I had my laptop I could pull up some quick documentation.”  Even if your notes are just for you, ensure that they aren’t limited to a single device.  Not only is that a single point of failure for your hard-earned written knowledge, but that also means you are relying on a single device to be with you 100% of the time when you run into that same issue again.  Utilize cloud storage like SkyDrive, and as long as you have your note-taking client on a handy device you should be able to access it.

5. Be consistent

Taking one day’s worth of good notes wouldn’t be very valuable.  This is one of those daily tasks that needs to be on the forefront of your mind until note-taking is second nature.  It will eventually become something that you will continuously think about when you are working on those issues.

Do yourself and the “future you” a favor.  Take notes!  Any questions, comments, or suggestions please feel free to leave a comment below or email me at sqlsalt@outlook.com.

By default, when the SQL Server service starts up it will attempt to register the Server Principal Name (referred to as SPN for the rest of the article).  And when the service is stopping it will attempt to unregister the SPN.  Provided the service has the appropriate permissions, this will happen flawlessly allowing client connections to use Windows Authentication with Kerberos.  More information on automatic SPN registration can be found on BOL.

But if you are utilizing an AlwaysOn Availability Group Listener to connect to a replica, the same does not hold true.  As per BOL on the topic:

A Server Principal Name (SPN) must be configured in Active Directory by a domain administrator for each availability group listener name in order to enable Kerberos for the client connection to the availability group listener.

In other words, manual creation of the SPN needs to take place in order to Kerberos to work when connecting through the listener.  Let’s see this in action.  The first test will be the default (with the automatically registered SPN on the instance, but not for the listener).  What I will do is connect from a PowerShell session to the instance using Windows authentication, first directly to the primary replica (i.e. the instance hosting the primary replica of the Availability Group.  In this case, the primary replica is ALWAYSON1).  And then I will connect to the primary replica through the Availability Group Listener (in this case, the listener is AG_List_Test1).

PoShConnectWithoutSPN

I make a get request to the Status property just to open the connection.  I use the below query to find out the auth_scheme of each current connection:

select
	c.session_id,
	c.net_transport,
	c.protocol_type,
	c.auth_scheme,
	s.program_name
from sys.dm_exec_connections c
inner join sys.dm_exec_sessions s
on c.session_id = s.session_id;

Here are my initial results:

SQLConnectWithoutSPN

As you can see above, the direct replica/instance connection uses Kerberos whereas the connection to the listener uses NTLM.  Now, as per BOL, let’s register an SPN for the Listener using setspn:

PoShRegisterListenerSPN

Now I will re-open the two connections:  The first to the primary replica, and the second to Availability Group Listener.  Below are the results after running the query to see the connections’ auth_scheme:

SQLConnectWithSPN

As you can see from the experiment above, in order to utilize Kerberos auth for an Availability Group Listener, an SPN needs to be manually registered for the FQDN of the listener.  If there are any comments, questions, or suggestions please feel free to leave a comment below or email me at sqlsalt@outlook.com.

There is probably a very infrequent use-case for this requirement, but say you needed to find out how the data of a table is distributed across the data files in your database.  I have written a script that will calculate how many pages are on each data file (as well as the percentage of the table that lives on that data file, and the space used).

Note:  This script uses the undocumented and unsupported DBCC IND command.  Use at your own risk.

This code basically looks at each page that a table owns (heap, clustered index, nonclustered index; leaf and non-leaf level pages) and aggregates a count based off of the pages.  You’ll end up with output that looks like the following:

TableAcrossDataFiles_resultset

Here is the code to pull this data:

/******************************************************************************
Description:
	this script will provide data distribution of all pages (index and data)
	across the containing data file(s)

Note:
	change 'dbo.YourTable' to your respective table name

	change 'YourDatabase' to your respective database
******************************************************************************/
use YourDatabase;
go

declare
	@database_name sysname,
	@table_name sysname;
select
	@database_name = db_name(),
	@table_name = 'dbo.YourTable';

create table #DbccInd
(
	PageFID	int null,
	PagePID	int null,
	IAMFID int null,
	IAMPID int null,
	ObjectID int null,
	IndexID int null,
	PartitionNumber int null,
	PartitionID bigint null,
	iam_chain_type varchar(32) null,
	PageType int null,
	IndexLevel int null,
	NextPageFID int null,
	NextPagePID int null,
	PrevPageFID int null,
	PrevPagePID int null
);

insert into #DbccInd
exec('dbcc ind(''' + @database_name + ''', ''' + @table_name + ''', -1);');

;with file_dist as
(
	select
		dind.PageFID as file_id,
		file_name(dind.PageFID) as file_name,
		df.physical_name,
		count(*) as page_count,
		count(*) * 8 as space_used_kb
	from #DbccInd dind
	inner join sys.database_files df
	on dind.PageFID = df.file_id
	group by dind.PageFID, df.physical_name
)
select
	@table_name as table_name,
	file_id,
	file_name,
	physical_name,
	page_count,
	convert (
		decimal(5, 2),
		page_count * 1.0 / (select sum(page_count) from file_dist) * 100
	) as percent_on_file,
	space_used_kb
from file_dist fd;

I hope this comes in handy!  If there are any comments, questions, or suggestions please feel free to leave a comment below or email me at sqlsalt@outlook.com.

T-SQL Tuesday #39

This week’s T-SQL Tuesday is being hosted by Wayne Sheffield ( blog | twitter ), and the topic is to blog about anything PowerShell and SQL Server related.  As a SQL Server professional, I use PowerShell day-in-day-out and I love every second of it.  Today I am going to write about how to use SMO (via PowerShell) to get both server and database role members for a list of SQL Server instances.

When would this come in handy?  Say you just found out the implications of membership in the securityadmin fixed server role.  Wide-eyed, your brain starts racing on how you can reach across all 1,000 database servers in your environment to get a list of all members in the securityadmin fixed server role.  There are a couple of ways to do this, and my tool of choice for this type of requirement would be PowerShell.  Note: the below code is meant to show one way to accomplish this task, and it is not to be copy-and-pasted in production without thorough testing.

There are a total of four cmdlets that I utilize for this purpose.  They are as follows:

Get-SqlServerRoleMembersgets a list of members for a particular server role (or roles)

Get-SqlServerDbRoleMembersgets a list of members for a particular database role (or roles)

Is-SqlServerRoletakes a Server object and role name as parameters and outputs a Boolean value whether or not the principal is a role (this is consumed by Get-SqlServerRoleMembers)

Is-SqlServerDbRoletakes a Database object and role name as parameters and outputs a Boolean value whether or not the principal is a role (this is consumed by Get-SqlServerDbRoleMembers)

Now getting to the code.  The last two cmdlets (Is-SqlServerRole and Is-SqlServerDbRole) are really just worker functions for the two main cmdlets and don’t require direct calls.  All they really do is find out whether a member is a role.  The reason I had to write these is because the SMO objects didn’t readily provide this information, and with a huge dump of data it would be easier if that was provided.  Not to mention you want to find out if a login has the nested membership, so these cmdlets allowed me to get recursive with the Get-SqlServer[Db]RoleMembers cmdlets.

Is-SqlServerRole

Function Is-SqlServerRole {
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.SqlServer.Management.Smo.Server] $SqlServerInstance,

        [Parameter(Mandatory = $true, Position = 1)]
        [String] $RoleName
    )

    $SqlCommand = "
        declare @principal_name sysname;
        set @principal_name = '$RoleName';

        select 1
        from sys.server_principals
        where type = 'R'
        and name = @principal_name"

    try {
        if ($SqlServerInstance.Status -eq [Microsoft.SqlServer.Management.Smo.ServerStatus]::Online) {
            if ($SqlServerInstance.Databases["master"].ExecuteWithResults($SqlCommand).Tables[0].Rows.Count -gt 0) {
                return $true
            }
            else {
                return $false
            }
        }
        else {
            return $false
        }
    }
    catch {
        Write-Error $_.Exception
    }
}

Is-SqlServerDbRole

Function Is-SqlServerDbRole {
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Microsoft.SqlServer.Management.Smo.Database] $SqlDatabase,

        [Parameter(Mandatory = $true, Position = 1)]
        [String] $RoleName
    )

    $SqlCommand = "
        declare @principal_name sysname;
        set @principal_name = '$RoleName';

        select 1
        from sys.database_principals
        where type = 'R'
        and name = @principal_name;"

    try {
        if ($SqlDatabase.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) {
            if ($SqlDatabase.ExecuteWithResults($SqlCommand).Tables[0].Rows.Count -gt 0) {
                return $true
            }
            else {
                return $false;
            }
        }
        else {
            return $false
        }
    }
    catch {
        Write-Error $_.Exception
    }
}

The real work is done with the following two cmdlets:  Get-SqlServerRoleMembers and Get-SqlServerDbRoleMembers.  Below are a couple of calls to these cmdlets, and you can see that they take a list of SQL Server instances and that is then piped to the aforementioned cmdlets.  By specifying the role list as a string array, we are able to pull the role (or roles) members that we are concerned with.  And, as always, if you need to use SQL Server authentication there is that capability.

Usage

"sqlbox1", "sqlbox2" |
    Get-SqlServerRoleMembers -SqlServerRoleList "sysadmin", "securityadmin" -SqlAuth -SqlCredential $Creds

"sqlbox1", "sqlbox2" |
    Get-SqlServerDbRoleMembers -SqlServerRoleList "db_owner"

Now getting into the code.  All we really do here is loop through the server roles in Get-SqlServerRoleMembers, and the database roles in Get-SqlServerDbRoleMembers.  Not rocket science, just using the ever-so-flexible and easy-to-utilize SMO namespace.

Get-SqlServerRoleMembers

Function Get-SqlServerRoleMembers {
    param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [String[]] $SqlInstanceNameList = $env:COMPUTERNAME,

        [Parameter(Mandatory = $true, Position = 1)]
        [String[]] $SqlServerRoleList,

        [Parameter(Mandatory = $false, Position = 2)]
        [Switch] $SqlAuth,

        [Parameter(Mandatory = $false, Position = 3)]
        [PSCredential] $SqlCredential
    )

    begin {
        [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") |
            Out-Null
    }

    process {
        try {
            foreach ($SqlInstanceName in $SqlInstanceNameList) {
                $SqlInstance = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlInstanceName)
                if ($SqlAuth) {
                    $SqlInstance.ConnectionContext.LoginSecure = $false
                    $SqlInstance.ConnectionContext.Login = $SqlCredential.UserName
                    $SqlInstance.ConnectionContext.SecurePassword = $SqlCredential.Password
                }

                $SqlInstance.Roles |
                    Where-Object {$_.Name -in $SqlServerRoleList} |
                    ForEach-Object {
                        foreach($RoleMember in $_.EnumMemberNames()) {
                            $RoleMemberObj = New-Object System.Object
                            $RoleMemberObj |
                                Add-Member -MemberType NoteProperty -Name "ServerName" -Value $SqlInstance.Name
                            $RoleMemberObj |
                                Add-Member -MemberType NoteProperty -Name "RoleName" -Value $_.Name
                            $RoleMemberObj |
                                Add-Member -MemberType NoteProperty -Name "MemberName" -Value $RoleMember

                            if (Is-SqlServerRole -SqlServerInstance $SqlInstance -RoleName $RoleMember) {
                                Get-SqlServerRoleMembers -SqlInstanceNameList $SqlInstanceName -SqlServerRoleList $RoleMember -SqlAuth:$SqlAuth -SqlCredential $SqlCredential
                                $RoleMemberObj |
                                    Add-Member -MemberType NoteProperty -Name "MemberType" -Value "Role"
                            }
                            else {
                                $RoleMemberObj |
                                    Add-Member -MemberType NoteProperty -Name "MemberType" -Value "Login"
                            }

                            Write-Output $RoleMemberObj
                        }
                    }
            }
        }
        catch {
            Write-Error $_.Exception
        }
    }
}

Get-SqlServerDbRoleMembers

Function Get-SqlServerDbRoleMembers {
    param (
        [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [String[]] $SqlInstanceNameList = $env:COMPUTERNAME,

        [Parameter(Mandatory = $true, Position = 1)]
        [String[]] $SqlServerRoleList,

        [Parameter(Mandatory = $false, Position = 2)]
        [Switch] $SqlAuth,

        [Parameter(Mandatory = $false, Position = 3)]
        [PSCredential] $SqlCredential
    )

    begin {
        [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.Smo") |
            Out-Null
    }

    process {
        try {
            foreach ($SqlInstanceName in $SqlInstanceNameList) {
                $SqlInstance = New-Object Microsoft.SqlServer.Management.Smo.Server($SqlInstanceName)
                if ($SqlAuth) {
                    $SqlInstance.ConnectionContext.LoginSecure = $false
                    $SqlInstance.ConnectionContext.Login = $SqlCredential.UserName
                    $SqlInstance.ConnectionContext.SecurePassword = $SqlCredential.Password
                }

                foreach ($SqlDatabase in $SqlInstance.Databases) {
                    try {
                        $SqlDatabase.Roles |
                            Where-Object {$_.Name -in $SqlServerRoleList} |
                                ForEach-Object {
                                    foreach($DbRoleMember in $_.EnumMembers()) {
                                        $DbRoleMemberObj = New-Object System.Object
                                        $DbRoleMemberObj |
                                            Add-Member -MemberType NoteProperty -Name "ServerName" -Value $SqlInstance.Name
                                        $DbRoleMemberObj |
                                            Add-Member -MemberType NoteProperty -Name "DatabaseName" -Value $SqlDatabase.Name
                                        $DbRoleMemberObj |
                                            Add-Member -MemberType NoteProperty -Name "RoleName" -Value $_.Name
                                        $DbRoleMemberObj |
                                            Add-Member -MemberType NoteProperty -Name "MemberName" -Value $DbRoleMember

                                        if (Is-SqlServerDbRole -SqlDatabase $SqlDatabase -RoleName $DbRoleMember) {
                                            $DbRoleMemberObj |
                                                Add-Member -MemberType NoteProperty -Name "MemberType" -Value "Role"
                                        }
                                        else {
                                            $DbRoleMemberObj |
                                                Add-Member -MemberType NoteProperty -Name "MemberType" -Value "User"
                                        }

                                        Write-Output $DbRoleMemberObj
                                    }
                                }
                        }
                    catch {
                        Write-Error $_.Exception
                    }
                }
            }
        }
        catch {
            Write-Error $_.Exception
        }
    }
}

Voila!  This is a good starting point.  It shows just how powerful our beloved PowerShell can be, turning an impossible task into nothing with just a quick script.

If there are any comments, questions, or suggestions please feel free to leave a comment below or email me at sqlsalt@outlook.com.

I ran into an issue this week that I thought worth blogging about.  When we setup an AlwaysOn Availability Group, what do we like to do?  We want to test it out.  Especially automatic failover, something that we will lean on during a disaster.

So like any good DBA, I started causing failovers in my test environment.  But then I ran into a snag:  On the primary replica I stopped the SQL Server service and I noticed that the Availability Group was not automatically failing over to the secondary replica’s node (as seen through the Failover Cluster Manager) stuck in a failed state, and the secondary replica was stuck in a “Resolving” state (as SQL Server sees it).

After much troubleshooting and head-scratching, I reached out to Sr. Escalation Engineer, friend, and AlwaysOn AG expert Curt Mathews.  He pointed me in the right direction on one big “gotcha” with WSFC.  It turned out my problem was with the WSFC failure threshold that was causing the prevention of automatic failover.  As defined here on BOL:

If an availability group exceeds its WSFC failure threshold, the WSFC cluster will not attempt an automatic failover for the availability group. Furthermore, the WSFC resource group of the availability group remains in a failed state until either the cluster administrator manually brings the failed resource group online or the database administrator performs a manual failover of the availability group. The WSFC failure threshold is defined as the maximum number of failures supported for the availability group during a given time period. The default time period is six hours, and the default value for the maximum number of failures during this period is n-1, where n is the number of WSFC nodes. To change the failure-threshold values for a given availability group, use the WSFC Failover Manager Console.

As you can see here, the default time period is six hours and the maximum number of failures is an equation (n – 1, where n is the number of cluster nodes).  In my case with a two node cluster, that value was one.  Therefore, my threshold was one failure in a span of six hours.

Here is where you can see this configuration option for the Availability Group within the Failover Cluster Manager:

  1. In the Failover Cluster Manager right-click on the Availability Group in Services and applications
  2. Select Properties
  3. Select the Failover tab
  4. View/alter the failure count and/or period

FCM_failurethreshold

Even the dialog box explains this to us: “If the service or application fails more than the maximum in the specified period, it will be left in the failed state.”  Hopefully this will help others caught up with this same threshold limit.  If there are any comments, questions, or suggestions please feel free to leave a comment below or email me at sqlsalt@outlook.com.