Reading SET STATISTICS IO and TIME Output: The SQLYARD Statistics Parse

Reading SET STATISTICS IO and TIME Output: The SQLYARD Statistics Parser – SQLYARD

Reading SET STATISTICS IO and TIME Output: The SQLYARD Statistics Parser


You run a query, enable STATISTICS IO and TIME, and SQL Server dumps 40 lines of raw text into the Messages tab. You scroll through it trying to find the table that is killing your query. You are doing mental math, comparing numbers across lines, looking for the one table with 80,000 logical reads buried in the middle of the output. On a complex stored procedure hitting 15 tables that output can run to 200 lines. It is readable, technically. It is just a drag.

The SQLYARD Statistics Parser takes that raw output, parses every line, and gives you a formatted table sorted by logical reads with totals at the bottom. You see your worst offender in two seconds instead of two minutes.

Compatibility: Works with SET STATISTICS IO and SET STATISTICS TIME output from SQL Server 2008 through SQL Server 2025. The tool runs entirely in your browser. Nothing is sent to any server.

1 What SET STATISTICS IO and TIME Actually Do Beginner

These are two separate SET options in SQL Server that you enable before running any query. They add diagnostic output to the Messages tab in SSMS after the query executes.

SET STATISTICS IO ON tells SQL Server to report how many page reads were performed for each table touched by the query. This is the single most useful signal for identifying where a query is spending its I/O budget. A query with a bad index is not just slow in the abstract — it is doing 50,000 logical reads against a table that a proper index would hit in 200.

SET STATISTICS TIME ON reports CPU time and elapsed time for each statement in the batch. When a query is slow but STATISTICS IO looks reasonable, TIME tells you whether the bottleneck is compute or something else.

-- Enable both before running your query
SET STATISTICS IO ON;
SET STATISTICS TIME ON;

-- Run your query
SELECT
    o.OrderID,
    o.OrderDate,
    c.CustomerName,
    SUM(od.Quantity * od.UnitPrice) AS OrderTotal
FROM dbo.Orders       o
JOIN dbo.Customers    c  ON c.CustomerID = o.CustomerID
JOIN dbo.OrderDetails od ON od.OrderID   = o.OrderID
GROUP BY o.OrderID, o.OrderDate, c.CustomerName
ORDER BY o.OrderDate DESC;

-- Disable after (optional but good practice in shared sessions)
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;

The query results appear in the Results tab as normal. The STATISTICS output appears in the Messages tab. Switch to Messages after the query runs to see it.

2 What the Raw Output Looks Like Beginner

Here is what SQL Server actually writes to the Messages tab for the query above. This is a realistic example with three tables:

Table 'OrderDetails'. Scan count 4, logical reads 31204, physical reads 0,
read-ahead reads 0, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.

Table 'Orders'. Scan count 1, logical reads 8842, physical reads 3,
read-ahead reads 120, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.

Table 'Customers'. Scan count 1, logical reads 5640, physical reads 2,
read-ahead reads 44, lob logical reads 0, lob physical reads 0,
lob read-ahead reads 0.

Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0,
read-ahead reads 0, lob logical reads 8400, lob physical reads 0,
lob read-ahead reads 0.

SQL Server parse and compile time:
   CPU time = 0 ms, elapsed time = 14 ms.

SQL Server Execution Times:
   CPU time = 1250 ms,  elapsed time = 2034 ms.

Three tables, one worktable, one compile entry, one execution entry. That is already eight lines to mentally parse before you can answer the question: which table is the problem? Now multiply that by a stored procedure hitting 15 tables with multiple statement boundaries. You are looking at 150 lines of this, and the output is not sorted by anything useful. It comes out in execution order.

3 What Each Column Means Beginner

Understanding what you are looking at makes the output useful and not just noise.

Scan count

How many times SQL Server scanned or sought into this table or index. A count of 1 on an index seek is normal. A high count (4, 10, 50) on a large table usually means a loop join is driving repeated lookups worth investigating.

Logical reads

Total 8KB pages read from the buffer cache to satisfy the query. This is the most important number. It reflects actual work done regardless of whether data was in memory or on disk. High logical reads on a small table means a missing or unused index.

Physical reads

Pages read directly from disk because they were not in the buffer cache. On a warm system you expect zero. Physical reads during peak hours can indicate memory pressure or a cold cache after a restart.

Read-ahead reads

Pages SQL Server read speculatively from disk before they were needed. Some read-ahead is normal on range scans. Very high read-ahead on a point lookup query signals the optimizer chose a scan where a seek would be better.

LOB reads

The same three metrics (logical, physical, read-ahead) for large object columns: varchar(max), nvarchar(max), varbinary(max), text, ntext, image, XML, and CLR types. Unexpected LOB reads can indicate implicit conversions or XML in intermediate results.

CPU vs Elapsed time

CPU time is milliseconds consumed by the statement. Elapsed time is what the user experiences. If elapsed is much higher than CPU, the query is waiting on something: I/O, locks, network, or memory grants. If they are close together, the query is compute-bound.

4 Why It Gets Hard to Read at Scale Intermediate

For a three-table query the raw output is manageable. For anything complex it becomes a problem fast.

A stored procedure joining 15 tables with two temp table spills, a cursor, and three separate SELECT statements produces output like this in the Messages tab:

Table 'SalesOrderHeader'. Scan count 1, logical reads 8842, physical reads 0 ...
Table 'SalesOrderDetail'. Scan count 4, logical reads 31204, physical reads 0 ...
Table 'Product'. Scan count 1, logical reads 112, physical reads 0 ...
Table 'Customer'. Scan count 1, logical reads 5640, physical reads 2 ...
Table 'Address'. Scan count 2, logical reads 290, physical reads 0 ...
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0 ...

SQL Server parse and compile time:
   CPU time = 0 ms, elapsed time = 8 ms.

SQL Server Execution Times:
   CPU time = 47 ms,  elapsed time = 61 ms.

Table 'SalesOrderDetail'. Scan count 1, logical reads 4200, physical reads 0 ...
Table 'Product'. Scan count 1, logical reads 112, physical reads 0 ...
Table 'Worktable'. Scan count 12, logical reads 8800, physical reads 0 ...
Table 'WorkfileHeap'. Scan count 0, logical reads 0, physical reads 0 ...

SQL Server Execution Times:
   CPU time = 312 ms,  elapsed time = 408 ms.

Table 'SalesOrderHeader'. Scan count 1, logical reads 2100, physical reads 3 ...
Table 'Customer'. Scan count 1, logical reads 890, physical reads 0 ...
Table 'Address'. Scan count 4, logical reads 1240, physical reads 0 ...
-- ... continues for another 80 lines

The same table appears multiple times across different statements. The totals are split across the output. There is no sort order that helps you. To answer “what is the total logical read count for SalesOrderHeader across the entire procedure” you have to find every occurrence, pull the numbers, and add them up manually. On a 200-line output that takes time and it is easy to miss a line.

This is the problem the parser solves.

5 Introducing the SQLYARD Statistics Parser Beginner

The SQLYARD Statistics Parser is a free browser-based tool that takes raw SET STATISTICS IO and TIME output and formats it into something you can actually use.

  • Aggregates duplicate table entries. If the same table appears five times across multiple statements, the parser adds up all the reads and shows one row with the total. You see the real cost of each table across the entire batch.
  • Sorts by logical reads by default. Your worst table is always at the top. Click any column header to re-sort by physical reads, scan count, or any other metric.
  • Shows a totals row. Total logical reads, total physical reads, total scans across all tables in the batch. One number that tells you the overall I/O footprint of the query.
  • Parses TIME output alongside IO. If your paste includes both STATISTICS IO and STATISTICS TIME output, both are formatted. CPU time and elapsed time per statement appear in a separate section below the IO table.

Completely private. The tool runs entirely in your browser. You paste in text, hit Parse, and the results display on the page. Nothing is sent anywhere. You can use it on production query output without any data leaving your machine.

Try the tool now at sqlyard.com/tools/statistics-parser

Open the Statistics Parser

6 How to Use It: Step by Step Beginner

1

Enable STATISTICS IO and TIME in SSMS

Run these in the same query window before executing your query. You can also enable them via the SSMS menu under Query > Query Options > Advanced > Set Statistics, but the T-SQL approach works in any session and any tool.

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
2

Run your query and switch to the Messages tab

After the query completes, click the Messages tab in the results pane. You will see the raw STATISTICS output. It starts with the table lines and ends with the execution time lines.

3

Select all the Messages output and copy it

Click anywhere in the Messages tab, press Ctrl+A to select all, then Ctrl+C to copy. You want everything: the table lines, the parse and compile time line, and the execution times line. The parser handles all of it.

4

Paste into the parser and hit Parse

Open the Statistics Parser, paste into the input box, and click Parse. Results appear immediately below. If you want to try it first without using your own output, click Load Sample to populate the input with a realistic example.

7 Reading the Results: What to Look For Intermediate

The parser gives you the numbers. Here is how to use them to find the actual problem.

Start with the Summary Cards

Total logical reads tells you the overall I/O footprint of the batch. If you are running a query that should be a quick lookup and you are seeing 500,000 total logical reads, that number alone tells you something is wrong before you even look at the table breakdown.

Check Physical Reads Immediately

On a warm production system physical reads should be zero or close to it. Any physical reads means data was not in the buffer cache when the query ran. A few physical reads on a large table is not alarming. Thousands of physical reads on a table that gets hit constantly is a memory pressure signal worth investigating separately.

Work Down the Logical Reads Column

The table is sorted highest to lowest by default. For each high-read table ask: is the scan count 1 (probably a seek or a single scan, which may be expected) or is it 4, 10, 50 (a loop join driving repeated lookups, which usually means a missing index on the inner side of the join)?

Flag Non-Zero Physical Reads

The parser highlights physical read rows. A table showing 3 physical reads out of 8,842 logical reads is fine. A table showing 3,000 physical reads means the buffer cache is not holding that data between executions.

Compare CPU to Elapsed Time

In the TIME section, the gap between elapsed and CPU is your wait time. If elapsed is 2,034ms and CPU is 1,250ms, the gap is 784ms. The query was ready to run but waiting on something. Common causes include memory grant waits (RESOURCE_SEMAPHORE), I/O waits (PAGEIOLATCH), or lock waits (LCK_M_S). If elapsed equals CPU you are compute-bound and the focus is reducing logical reads. If elapsed is much larger than CPU you are waiting, and the focus shifts to wait stat analysis.

A Concrete Example

-- Raw output, hard to read across 15 tables:
Table 'OrderDetails'. Scan count 4, logical reads 31204 ...
Table 'Orders'. Scan count 1, logical reads 8842 ...
Table 'Customers'. Scan count 1, logical reads 5640 ...
Table 'Worktable'. Scan count 0, logical reads 0 ...

SQL Server Execution Times:
   CPU time = 1250 ms,  elapsed time = 2034 ms.

After pasting into the parser you see this immediately:

TableScan CountLogical ReadsPhysical ReadsAssessment
OrderDetails431,2040Investigate
Orders18,8423Monitor
Customers15,6402Normal
Worktable000OK
Total645,6865

The action item is clear without any mental arithmetic. OrderDetails is the target. Scan count 4 with 31,204 logical reads means the optimizer is doing 4 passes through that table. Check the index on the join column between Orders and OrderDetails. If there is no index on OrderDetails.OrderID that is the fix. The 784ms gap between CPU and elapsed (1,250ms vs 2,034ms) tells you there is also wait time alongside the compute work, so after fixing the index, check wait statistics for any secondary bottleneck.

You could get to the same conclusion from the raw output. The parser gets you there faster, especially when you are dealing with 15 tables instead of 3.

All SQLYARD tools are free, run in the browser, and live on the SQLYARD Tools page.
The Execution Plan Splitter, for when SSMS chokes on plans over 10MB, is coming next.

Try the SQLYARD Statistics Parser

References


Discover more from SQLYARD

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from SQLYARD

Subscribe now to keep reading and get access to the full archive.

Continue reading