Getting perfmon data into SQL Server, Part III

I didn’t think about this post to be honest, and I believed the series was complete with Part I and Part II, but when it was time to implement and deploy the solution to multiple servers, I realised that it would be useful to deploy programatically and not going one server after another clicking and clicking and clicking… you get it, right?


As I said, when it’s time to deploy the solution explained in my previous posts to a number of servers it might get very tedious, specially if we have servers running multiple instances, since each have different counter names because the instance name is part of that name, and if we create one template, that won’t apply to all cases, so a lot of manual intervention.

So I decided to do what I like the most and got to write some queries that combined with some powershell will do the job for me.

The logic is easy, we have three tables where we are going to insert the different SQL Servers we want to deploy to (Server\Instance format), then we will have one table for windows counters and another for SQL Server counters.

To get a list of all counters available, you can run this command

typeperf -q > c:\temp\all_server_counters.txt

This will bring the whole list of performance counters, you need to run this in a SQL Server with a default instance installed to get SQL Server performance counters

Then you can review them and choose those you are most interested.

The query will generate just one row per server but it will contain all server counters plus SQL Server counters for each instance, so we will have the whole picture in one output file and we’ll be able to correlate the data easier.

After that, I use logman to remotely create the different Data Collectors on each server, be sure you have enough administrative privileges.

Import-Module sqlps -DisableNameChecking
$cmssrv = "localhost\MSSQL2016";
$templatePath = "c:\temp\templates\"; 
$outputPath = "c:\Perfmon\"
$serverlist = invoke-sqlcmd -ServerInstance $cmssrv -Database "master" -query "
IF OBJECT_ID('tempdb..#SQLServers')         IS NOT NULL DROP TABLE #SQLServers
IF OBJECT_ID('tempdb..#ServerCounters')     IS NOT NULL DROP TABLE #ServerCounters
IF OBJECT_ID('tempdb..#SQLServerCounters')  IS NOT NULL DROP TABLE #SQLServerCounters
CREATE TABLE #ServerCounters    (ID INT NOT NULL IDENTITY, counter_name SYSNAME)
VALUES ('localhost')
, ('localhost\MSSQL2014')
, ('localhost\MSSQL2016')
, ('localhost\MSSQL2017')
INSERT INTO #ServerCounters ( counter_name )
VALUES ('\Memory\Available MBytes')
, ('\NUMA Node Memory(*)\Available MBytes')
, ('\Paging File(*)\% Usage')
, ('\PhysicalDisk(*)\Avg. Disk Queue Length')
, ('\PhysicalDisk(*)\Current Disk Queue Length')
, ('\PhysicalDisk(*)\Disk Reads/sec')
, ('\PhysicalDisk(*)\Disk Writes/sec')
, ('\PhysicalDisk(*)\Avg. Disk sec/Read')
, ('\PhysicalDisk(*)\Avg. Disk sec/Write')
, ('\PhysicalDisk(*)\Avg. Disk Read Queue Length')
, ('\PhysicalDisk(*)\Avg. Disk Write Queue Length')
, ('\System\Processor Queue Length')
, ('\Processor(*)\% Processor Time')
, ('\Network Interface(*)\Bytes Total/sec')
, ('\Network Interface(*)\Packets/sec')
, ('\Network Interface(*)\Packets Received/sec')
, ('\Network Interface(*)\Packets Sent/sec')
INSERT INTO #SQLServerCounters ( counter_name )
VALUES ('\SQLServer:Buffer Manager\Page life expectancy')
, ('\SQLServer:Buffer Node(*)\Page life expectancy')
, ('\SQLServer:General Statistics\User Connections')
, ('\SQLServer:Memory Manager\Memory Grants Pending')
, ('\SQLServer:SQL Statistics\Batch Requests/sec')
, ('\SQLServer:SQL Statistics\SQL Compilations/sec')
, ('\SQLServer:SQL Statistics\SQL Re-Compilations/sec')
            WHEN CHARINDEX('\', s.instance_name) > 0 
                THEN LEFT(s.instance_name, CHARINDEX('\', s.instance_name) - 1) 
            ELSE s.instance_name 
        END AS server_name
        , STUFF((SELECT CHAR(10) + counter_name FROM #ServerCounters AS c ORDER BY ID FOR XML PATH('')), 1,1,'') 
        (SELECT CHAR(10) + 
                    CASE WHEN CHARINDEX('\', s1.instance_name) = 0  
                        THEN c.counter_name 
                        ELSE REPLACE(c.counter_name, 'SQLServer', 'MSSQL$' + CONVERT(SYSNAME, RIGHT(s1.instance_name, LEN(s1.instance_name) - CHARINDEX('\', s1.instance_name))))
             FROM #SQLServerCounters AS c 
             CROSS APPLY #SQLServers AS s1
             ORDER BY s1.instance_name, c.ID 
             FOR XML PATH('')
        ) AS template
    FROM #SQLServers AS s
    ORDER BY server_name"
foreach ($servername in $serverlist) {
    $OutputExists = Get-Item -Path $templatePath -ErrorAction SilentlyContinue;
    if ($OutputExists -eq $null) {
        Write-Output "Creating directory $templatePath";
        New-Item -Path $templatePath -Type Directory;
    $templateFile = "$($templatePath)\$($servername.server_name)_DBA_collector.txt";
    $servername.template | Out-File -FilePath $templateFile;
    $serverOutputPath = "$($outputPath)$($servername.server_name)";
    $OutputExists = Get-Item -Path $serverOutputPath -ErrorAction SilentlyContinue;
    if ($OutputExists -eq $null) {
        Write-Output "Creating directory $serverOutputPath";
        New-Item -Path $serverOutputPath -Type Directory;
        Write-Output "Trying to create counter in $($servername.server_name)";
        logman create counter -s $servername.server_name -cf $templateFile -n "DBA_perfmonCollector" -f "csv" -si 15 -o "$($serverOutputPath)\$($servername.server_name)" -a -cnf "24:00:00" -v "nnnnnn";
        logman start -s $servername.server_name -n "DBA_perfmonCollector";
        Write-Output "Failed to create counter in $($servername.server_name)";

After successfully running this powershell script you will have your Data collectors created on each server.

I’d recommend to use a shared folder to centralize all the outputs in one location and also avoid filling the c:\ drive which can be a real pain in the back.


As I said, I didn’t expect to write this post, but I felt something was missing with just the two previous posts.

I am currently working on some reports to make the data a bit more usable because the amount of information can be huge and really defeat the purpose of having it.

Hope you like it and stay tuned!



  1. Valeriano Pari Torres says:

    Excelente artículo, con lenguaje sencillo y sin trucos ni tecnicismos.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.