Monitoring SQL Server Query Performance in Grafana Using Windows Exporter, Prometheus

Introduction

Monitoring SQL Server query performance is a critical requirement for database administrators and operations teams. Identifying slow queries, high CPU usage, and excessive logical reads in real time can help optimize database performance, prevent bottlenecks, and ensure application reliability.

In this article, we will build a real-time SQL Server Query Performance Dashboard in Grafana using:

  • SQL Server

  • PowerShell

  • Windows Exporter

  • Prometheus

  • Grafana

The final dashboard will display:

  • Query Text

  • Database Name

  • Execution Count

  • Elapsed Time (ms)

  • CPU Time (ms)

  • Logical Reads

Solution Architecture

Step 1: Verify Windows Exporter Installation

Verify that Windows Exporter is running:

cmd

sc query windows_exporter

Expected:

STATE : 4 RUNNING

Step 2: Verify Textfile Collector Configuration

Run:

cmd

sc qc windows_exporter

Example Output:

BINARY_PATH_NAME : “C:\Program Files\windows_exporter\windows_exporter.exe” --config.file=“C:\Program Files\windows_exporter\config.yaml”

Verify that config.yaml contains the textfile collector configuration:

yaml

collectors:
  enabled: cpu,memory,logical_disk,physical_disk,net,os,service,system,textfile


Step 3: Verify SQL Server is Running

Verify SQL Server is running on port 1433:

cmd

docker ps

Expected:

CONTAINER ID PORTS

xxxxxxxxxxxx 0.0.0.0:1433->1433/tcp


Step 4: Create Test Database and Sample Data

Connect to SQL Server and create a test database:

cmd

docker exec -it sqlserver2 /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "YourPassword" -C -Q "CREATE DATABASE TestDB"

Create a sample table with data:

cmd

docker exec -it sqlserver2 /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "YourPassword" -C -d TestDB -Q "CREATE TABLE Orders (OrderID INT PRIMARY KEY, CustomerName NVARCHAR(100), Amount DECIMAL(10,2), OrderDate DATETIME DEFAULT GETDATE()); INSERT INTO Orders VALUES (1,'Alice',500.00,GETDATE()),(2,'Bob',1200.00,GETDATE()),(3,'Charlie',300.00,GETDATE()),(4,'Diana',850.00,GETDATE()),(5,'Eve',2000.00,GETDATE())"

Step 5: Create Textfile Collector Directory

C:\Program Files\windows_exporter\textfile_inputs

This directory will contain Prometheus metrics.


Step 6: Create PowerShell Script

Create the file:

C:\Program Files\windows_exporter\textfile_inputs\sql_queries.ps1

Paste the following script:

powershell

$server = "localhost,1433"
$user = "sa"
$password = "YourPassword"
$query = @"
SELECT TOP 10
    SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
        ((CASE qs.statement_end_offset
            WHEN -1 THEN DATALENGTH(st.text)
            ELSE qs.statement_end_offset
        END - qs.statement_start_offset)/2)+1) AS query_text,
    qs.execution_count,
    qs.total_elapsed_time / 1000 AS total_elapsed_ms,
    qs.total_worker_time / 1000 AS total_cpu_ms,
    qs.total_logical_reads,
    ISNULL(DB_NAME(st.dbid), 'internal') AS database_name

FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
ORDER BY qs.total_elapsed_time DESC
"@
try {
    $connection = New-Object System.Data.SqlClient.SqlConnection
    $connection.ConnectionString = "Server=$server;Database=master;User Id=$user;Password=$password;TrustServerCertificate=True;"
    $connection.Open()
    $cmd = $connection.CreateCommand()
    $cmd.CommandText = $query
    $reader = $cmd.ExecuteReader()
    $metrics = @()
    $metrics += "# HELP sqlserver_query_info SQL Server query performance metrics"
    $metrics += "# TYPE sqlserver_query_info gauge"
    while ($reader.Read()) {
        $queryText = $reader\["query_text"\].ToString().Trim()
        $queryText = $queryText -replace '"', '\\"'
        $queryText = $queryText -replace '\\n', ' '
        $queryText = $queryText -replace '\\r', ' '
        $queryText = ($queryText -replace '\\s+', ' ').Substring(0, \[Math\]::Min(100, $queryText.Length))
        $execCount    = $reader\["execution_count"\]
        $elapsedMs    = $reader\["total_elapsed_ms"\]
        $cpuMs        = $reader\["total_cpu_ms"\]
        $logicalReads = $reader\["total_logical_reads"\]
        $dbName       = $reader\["database_name"\].ToString()
        $label = "query=""$queryText"",database=""$dbName"",execution_count=""$execCount"",elapsed_ms=""$elapsedMs"",cpu_ms=""$cpuMs"",logical_reads=""$logicalReads"""
        $metrics += "sqlserver_query_info{$label} 1"
    }
    $reader.Close()
    $connection.Close()
    $metrics | Out-File "C:\\Program Files\\windows_exporter\\textfile_inputs\\sql_queries.prom" -Encoding ascii
    Write-Host "Metrics written successfully"
}
catch {
    Write-Host "Error: $\_"
}

Step 7: Execute the Script

Run:

powershell

powershell.exe -ExecutionPolicy Bypass -File "C:\\Program Files\\windows_exporter\\textfile_inputs\\sql_queries.ps1"

Expected output:

Metrics written successfully


Step 8: Verify Generated Metric File

Open:

C:\Program Files\windows_exporter\textfile_inputs\sql_queries.prom

Example:

sqlserver_query_info{query=“SELECT * FROM Orders”,database=“TestDB”,execution_count=“1”,elapsed_ms=“301”,cpu_ms=“301”,logical_reads=“0”} 1


Step 9: Verify Windows Exporter Metrics

Open:

localhost:9182/metrics

Search for sqlserver_query_info

Expected:

sqlserver_query_info{cpu_ms=“301”,database=“internal”,elapsed_ms=“301”,…}


Step 10: Configure Task Scheduler

To update the metric automatically every minute:

General Tab

  • Name: sqlserver_metrics

  • Run only when user is logged on

  • Run with highest privileges

Trigger Tab

  • Daily

  • Repeat task every: 1 minute

  • Duration: Indefinitely

Action Tab

  • Program: powershell.exe

  • Arguments: -ExecutionPolicy Bypass -File “C:\Program Files\windows_exporter\textfile_inputs\sql_queries.ps1”

Conditions Tab

  • Uncheck all power conditions

Settings Tab

  • Enable: Allow task to be run on demand

  • Enable: Run task as soon as possible after a scheduled start is missed

Save the task.


Step 11: Verify Prometheus

Open Prometheus:

localhost:9091

Execute:

sqlserver_query_info

Expected result:

sqlserver_query_info{cpu_ms=“301”,database=“internal”,…}


Step 12: Create Grafana Dashboard

Create a new panel in Grafana with these settings:

  • Visualization: Table

  • Data source: Prometheus

  • Query: sqlserver_query_info

  • Format: Table

Add Organize Fields transformation and configure:
FieldAction
database Rename → Database
query Rename → Query
execution_count Rename → Execution Count
elapsed_ms Rename → Elapsed Time (ms)
cpu_ms Rename → CPU Time (ms)
logical_reads Rename → Logical Reads
Time, _name_, instance, job, Value → Hide


Dashboard Output

The dashboard successfully displays:
Database
Query
Execution Count
Elapsed Time (ms)
CPU Time (ms)
Logical Reads

DBAs and operations teams can instantly identify the slowest queries, most executed queries, and highest resource consuming queries in real time.


Benefits

  • Real-time SQL Server query performance monitoring

  • No additional agents required

  • Uses existing Prometheus and Windows Exporter infrastructure

  • Identifies slow queries, high CPU queries, and excessive logical reads

  • Easily scalable across multiple SQL Server instances

  • Customizable Grafana dashboards

  • Real-time auto-updating metrics via Task Scheduler → refreshes every minute automatically


Conclusion

This solution reads from sys.dm_exec_query_stats which reflects the current SQL Server plan cache. Metrics will reset if SQL Server restarts or the plan cache is cleared. For historical query performance tracking, consider enabling SQL Server Query Store as a complementary solution

By combining PowerShell, Windows Exporter Textfile Collector, Prometheus, and Grafana, we created a lightweight and efficient solution for monitoring SQL Server query performance. This approach provides real-time visibility into query execution and can be extended to include additional metrics such as wait statistics, index usage, and connection counts.