Collecting Performance Metrics Using SQL Server DMV

If you cannot afford a third-party tool that can capture database performance metrics or don’t want to roll your own solution, then using T-SQL to collect some performance metrics for SQL Server may be your solution. .

What method do you use to determine how well your server is performing overtime? Do you know what your batch request per Second looks like over the last 30 days, or year? Can you track your page life expectancy over the day? As a DBA, there are always many things to do, and sometimes tracking your database performance is one of the last things you have time to work on. There are many different ways to collect performance related numbers for SQL Server. If you work in an organization that can afford it, I would suggest you buy a third-party tool that can capture performance metrics. If you cannot afford an expensive tool, or like rolling your own solution, then I will explain in this article how to use T-SQL to collect some performance metrics for SQL Server.

The Method

There are many different methods you might consider when collecting performance metric. However, I am only going to focus in and explain one method. That method is to use sys.dm_os_performanace_counters, which is a dynamic management view (DMV).

The sys.dm_os_performanace_counters DMVs was introduction in SQL Server 2005, and is still available in SQL Server 2008. This DMV displays one record for every counter that the server collects. You need “VIEW SERVER STATE” permissions to use this DMV. Unless your login is in the sysadmin server role, or someone has specifically provided you “VIEW SERVER STATE” permissions you might not be able to use this DMV. If you do not have permissions to query this DMV then I would just ask your administrator, to GRANT you “VIEW SERVER STATE” permission. I hope that they will not have an issue allowing view permissions to the server state.

Using sys.dm_os_peformance_counters DMV

It is not exactly straightforward to understand how to get each performance counter out of this DMV. Some performance counters can be extracted from a single record, while other counters require you to use two different records to gain the performance counter value. Others require you to run this DMV twice and then compare the counters between both executions to obtain a useful performance metric.

To see all the counters available run the following command:

SELECT * FROM sys.dm_os_performance_counters;

When you run this command, you should get a list of many counters, as displayed below:
(Note, below I have selectively only provided a few somewhat random rows of the output from the command above).

object_name                    counter_name                instance_name   cntr_value  cntr_type
------------------------------ --------------------------- --------------- ----------- ----------
MSSQL$SQL2008:Buffer Manager   Buffer cache hit ratio                      1055        537003264
MSSQL$SQL2008:Buffer Manager   Buffer cache hit ratio base                 1055        1073939712
MSSQL$SQL2008:Buffer Manager   Page lookups/sec                            6057902     272696576
MSSQL$SQL2008:Latches          Average Latch Wait Time(ms)                 74743       1073874176
MSSQL$SQL2008:Databases        Data File(s) Size (KB)      AventureWorksLT 5312        65792

If you examine the output above, you will see each row has a different “cntr_type” value. You need to understand each of these cntr_value’s to understand how to interpret this output. Below I will explain how to use the cntr_value column for each of these different cntr_types to obtain a meaningful performance metric.

If you look at the first two lines, you can see that there is first a cntr_type of “537003264”, which has a name of “Buffer cache hit ratio” and then in the second line there is a cntr_type of “1073939712” with a name of “Buffer cache hit ratio base”. These two types of counters have to be used in conjunction with each other to determine the actual buffer cache hit ratio performance measurement. In the example output above, I show the buffer cache hit ratio counter pairing. There are many pairings of a counter with a base when you look at all the performance counters exposed by the sys.dm_os_performance_counters DMV. To calculate the real counter value, or in my case the buffer cache hit ratio you need to take the cntr_value “1055” of the counter_name “Buffer cache hit ratio” and divide it by the “Buffer cache hit ratio base” counter_value, which in this case is also “1055” and then multiple by 100. Here is an example that does that in a single statement:

SELECT (a.cntr_value * 1.0 / b.cntr_value) * 100.0 [BufferCacheHitRatio]
FROM (SELECT * FROM sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) +
':Buffer Manager' END ) a
CROSS JOIN
(SELECT * from sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio base'
and object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) +
':Buffer Manager' END ) b;

All the per-second performance counters have a cntr_type = 272696576. In my example above, the per-second counter I displayed was “Page Lookups / sec”. For these types of counters the cntr_value contains a cumulative number. Therefore, in order to calculate the actual per-second rate you need to capture the per-second cntr_value twice and then calculate the per-second amount based on the two cntr_value’s and the number of seconds between the two samplings. Here is an example of how to calculate the per-second counter value for the number of “Page reads/sec”:

-- Collect first sample
DECLARE @old_cntr_value INT;
DECLARE @first_sample_date DATETIME;
SELECT @old_cntr_value = cntr_value,
@first_sample_date = getdate()
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page reads/sec';
-- Time frame to wait before collecting second sample
WAITFOR DELAY '00:00:10'
-- Collect second sample and calculate per-second counter
SELECT (cntr_value - @old_cntr_value) /
DATEDIFF(ss,@first_sample_date, GETDATE()) as PageReadPerSec
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page reads/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END

You can see that I started this code by first capturing a sample of the cntr_value for the “Page reads/sec” counter, and the time of the sampling. I saved the counter value and sampling time values in local variables. Next, I used the “WAITFOR” statement to wait 10 seconds before capturing the second sample. While I captured the second sample, I calculated the actual per-second value. I did this by first subtracting my saved cntr_value (@old_cntr_value) from the cntr_value provided by the second sampling. By doing this, I calculated the total number of page reads performed between the two samples. I then took the total number of page reads performed and divide by the number of seconds between the two samplings. The time difference between the two samplings was calculated using the date DIFF function. If you want to capture per-second counter values using the sys.dm_os_performance_counters you will need to use a method similar to what I describe above for each per-second counter you want to capture, to determine the actual per-second value.

The cntr_type ” 1073874176″ and the “65792” values, like the “Average Latch Wait Time (ms)” and “Data Files(s) Size (KB)” counters stand by themselves, meaning no calculation is needed to calculate the actual value. You can grab the cntr_value directly for these cntr_type’s and they will represent the value associated with the counter_name. If you look at the counter_name “Data File(s) Size (KB)” in my output above you will see that the size of my “AdventureWorksLT” database is 5,312 KB.

As you probably already noticed the cntr_type “65792” in my example has a value under the “instance_name” column, in this example that value is “AdventureWorksLT”. This column not only shows the names of databases, but many different other values like, “Application”, “Page”, “_ Total”, depending on the particular counter being displayed.

Script to collect Some Performance Metrics

One of the things that you can use the sys.dm_os_performanace_counters for is to gather perform metrics over time. Below is a script that can be used to display a number different performance metrics. Capturing these performance metrics periodically allows you to produce trend reports over time for each counter:

DECLARE @SQLProcessUtilization int;
DECLARE @PageReadsPerSecond bigint
DECLARE @PageWritesPerSecond bigint
DECLARE @CheckpointPagesPerSecond bigint
DECLARE @LazyWritesPerSecond bigint
DECLARE @BatchRequestsPerSecond bigint
DECLARE @CompilationsPerSecond bigint
DECLARE @ReCompilationsPerSecond bigint
DECLARE @PageLookupsPerSecond bigint
DECLARE @TransactionsPerSecond bigint
DECLARE @stat_date datetime
-- Table for First Sample
DECLARE @RatioStatsX TAbLE(
[object_name] varchar(128)
,[counter_name] varchar(128)
,[instance_name] varchar(128)
,[cntr_value] bigint
,[cntr_type] int
)
-- Table for Second Sample
DECLARE @RatioStatsY TAbLE(
[object_name] varchar(128)
,[counter_name] varchar(128)
,[instance_name] varchar(128)
,[cntr_value] bigint
,[cntr_type] int
)
INSERT INTO @RatioStatsX (
[object_name]
,[counter_name]
,[instance_name]
,[cntr_value]
,[cntr_type] )
SELECT [object_name]
,[counter_name]
,[instance_name]
,[cntr_value]
,[cntr_type] FROM sys.dm_os_performance_counters
SET @stat_date = getdate()
SELECT TOP 1 @PageReadsPerSecond=cntr_value
FROM @RatioStatsX
WHERE counter_name = 'Page reads/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
SELECT TOP 1 @PageWritesPerSecond= cntr_value
FROM @RatioStatsX
WHERE counter_name = 'Page writes/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
SELECT TOP 1 @CheckpointPagesPerSecond = cntr_value
FROM @RatioStatsX
WHERE counter_name = 'Checkpoint pages/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
SELECT TOP 1 @LazyWritesPerSecond = cntr_value
FROM @RatioStatsX
WHERE counter_name = 'Lazy writes/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
SELECT TOP 1 @BatchRequestsPerSecond = cntr_value
FROM @RatioStatsX
WHERE counter_name = 'Batch Requests/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:SQL Statistics'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':SQL Statistics' END
SELECT TOP 1 @CompilationsPerSecond = cntr_value
FROM @RatioStatsX
WHERE counter_name = 'SQL Compilations/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:SQL Statistics'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':SQL Statistics' END
SELECT TOP 1 @ReCompilationsPerSecond = cntr_value
FROM @RatioStatsX
WHERE counter_name = 'SQL Re-Compilations/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:SQL Statistics'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':SQL Statistics' END
SELECT TOP 1 @PageLookupsPerSecond=cntr_value
FROM @RatioStatsX
WHERE counter_name = 'Page lookups/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Buffer Manager'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
SELECT TOP 1 @TransactionsPerSecond=cntr_value
FROM @RatioStatsX
WHERE counter_name = 'Transactions/sec' AND instance_name = '_Total'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Databases'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Databases' END
-- Wait for 5 seconds before taking second sample
WAITFOR DELAY '00:00:05'
-- Table for second sample
INSERT INTO @RatioStatsY (
[object_name]
,[counter_name]
,[instance_name]
,[cntr_value]
,[cntr_type] )
SELECT [object_name]
,[counter_name]
,[instance_name]
,[cntr_value]
,[cntr_type] FROM sys.dm_os_performance_counters
SELECT (a.cntr_value * 1.0 / b.cntr_value) * 100.0 [BufferCacheHitRatio]
,c.[PageReadPerSec] [PageReadsPerSec]
,d.[PageWritesPerSecond] [PageWritesPerSecond]
,e.cntr_value [UserConnections]
,f.cntr_value [PageLifeExpectency]
,g.[CheckpointPagesPerSecond] [CheckpointPagesPerSecond]
,h.[LazyWritesPerSecond] [LazyWritesPerSecond]
,i.cntr_value [FreeSpaceInTempdbKB]
,j.[BatchRequestsPerSecond] [BatchRequestsPerSecond]
,k.[SQLCompilationsPerSecond] [SQLCompilationsPerSecond]
,l.[SQLReCompilationsPerSecond] [SQLReCompilationsPerSecond]
,m.cntr_value [Target Server Memory (KB)]
,n.cntr_value [Total Server Memory (KB)]
,GETDATE() AS [MeasurementTime]
,o.[AvgTaskCount]
,o.[AvgRunnableTaskCount]
,o.[AvgPendingDiskIOCount]
,p.PercentSignalWait AS [PercentSignalWait]
,q.PageLookupsPerSecond As [PageLookupsPerSecond]
,r.TransactionsPerSecond AS [TransactionsPerSecond]
,s.cntr_value [MemoryGrantsPending]
FROM (SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'Buffer cache hit ratio'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END ) a
join
(SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'Buffer cache hit ratio base'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END ) b
on a.x = b.x
join
(SELECT (cntr_value - @PageReadsPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [PageReadPerSec], 1 x
FROM @RatioStatsY
WHERE counter_name = 'Page reads/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
)c on a.x = c.x
join
(SELECT (cntr_value - @PageWritesPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [PageWritesPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'Page writes/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
) d on a.x = d.x
join
(SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'User Connections'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:General Statistics' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':General Statistics' END ) e
on a.x = e.x
join
(SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'Page life expectancy '
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END ) f
on a.x = f.x
join
(SELECT (cntr_value - @CheckpointPagesPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [CheckpointPagesPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'Checkpoint pages/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
) g on a.x = g.x
join
(SELECT (cntr_value - @LazyWritesPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [LazyWritesPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'Lazy writes/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
) h
on a.x = h.x
join
(SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'Free Space in tempdb (KB)'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Transactions' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Transactions' end) i
on a.x = i.x
join
(SELECT (cntr_value - @BatchRequestsPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [BatchRequestsPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'Batch Requests/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:SQL Statistics' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':SQL Statistics' END
) j
on a.x = j.x
join
(SELECT (cntr_value - @CompilationsPerSecond) / (CASE WHEN datediff(ss,@stat_date,getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [SQLCompilationsPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'SQL Compilations/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:SQL Statistics' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':SQL Statistics' END
) k on a.x = k.x
join
(SELECT (cntr_value - @ReCompilationsPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [SQLReCompilationsPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'SQL Re-Compilations/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:SQL Statistics' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':SQL Statistics' END
) l
on a.x = l.x
join
(SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'Target Server Memory (KB)'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Memory Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Memory Manager' END ) m
on a.x = m.x
join
(SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'Total Server Memory (KB)'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Memory Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Memory Manager' END ) n
on a.x = n.x
JOIN
(SELECT 1 AS x
, AVG(current_tasks_count)AS [AvgTaskCount]
, AVG(runnable_tasks_count)AS [AvgRunnableTaskCount]
, AVG(pending_disk_io_count) AS [AvgPendingDiskIOCount]
FROM sys.dm_os_schedulers
WHERE scheduler_id < 255) o
on a.x = o.x
JOIN
( SELECT 1 AS x, SUM(signal_wait_time_ms) / sum (wait_time_ms) AS PercentSignalWait
FROM sys.dm_os_wait_stats) p
ON a.x = p.x
join
(SELECT (cntr_value - @PageLookupsPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [PageLookupsPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'Page Lookups/sec'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Buffer Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Buffer Manager' END
) q
on a.x = q.x
join
(SELECT (cntr_value - @TransactionsPerSecond) / (CASE WHEN datediff(ss,@stat_date, getdate()) = 0 THEN 1 
	ELSE datediff(ss,@stat_date, getdate()) end) as [TransactionsPerSecond], 1 x
FROM @RatioStatsY
WHERE counter_name = 'Transactions/sec' AND instance_name = '_Total'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER'
THEN 'SQLServer:Databases'
ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Databases' END ) r
on a.x = r.x
join
(SELECT *, 1 x FROM @RatioStatsY
WHERE counter_name = 'Memory Grants Pending'
AND object_name = CASE WHEN @@SERVICENAME = 'MSSQLSERVER' THEN 'SQLServer:Memory Manager' 
	ELSE 'MSSQL$' + rtrim(@@SERVICENAME) + ':Memory Manager' END ) s
on a.x = s.x

By looking at this code, or running it on your system you will find that it displays one record. In the output of that one record, you will find a number of different performance counter metrics. To obtain these metrics you can see that I created a couple of temporary tables, @RatioStatsX and @RatioStatsY. These tables were used to collect two different samples of statistics from sys.dm_os_performance_counters DMV. Between collecting the two samples, I waited 5 seconds. This wait was performed so I could calculate those per-second counter values.

I have incorporated this code into a stored procedure that inserts this data into a physical table. This stored procedure is then run periodically (via SQL Agent) so I can capture performance metrics over time. By doing this, I was able to save the output of each run of my SQL Agent job in a SQL Server table. Now that I have my performance metrics stored in a table, I am able to create trend reports for any one of these collected performance statistics over time

Monitoring Performance with sys.dm_os_performance_counters DMV

Using the sys.dm_os_performance_counters DMV provides you a wealth of performance data that is only a T-SQL query away. By looking at different counters, you get a glimpse at the current performance status of your SQL Server instance. Collecting these performance counters overtime allows you to measure your performance trends overtime. Being able to understand your SQL Server performance trend allows you to you better manage your SQL Server instance performance.

» See All Articles by Columnist Gregory A. Larsen

Gregory Larsen
Gregory Larsen
Gregory A. Larsen is a DBA at Washington State Department of Health (DOH). Greg is responsible for maintaining SQL Server and other database management software. Greg works with customers and developers to design and implement database changes, and solve database/application related problems. Greg builds homegrown solutions to simplify and streamline common database management tasks, such as capacity management.

Get the Free Newsletter!

Subscribe to Cloud Insider for top news, trends & analysis

Latest Articles