MS SQL - diagnostic
1. Running queries
SELECT
sqltext.TEXT,
session_id,
STATUS,
command,
cpu_time,
total_elapsed_time,
wait_type
FROM sys.dm_exec_requests req
CROSS APPLY sys.dm_exec_sql_text(sql_handle) AS sqltext
WHERE req.session_id <> @@SPID AND req.session_id > 50
Big total_elapsed_time with small cpu_time = the query is waiting, not working. Check
wait_type then. Times are in milliseconds.
2. Fragmentation
SELECT
S.name AS [Schema],
T.name AS [Table],
I.name AS [Index],
DDIPS.index_type_desc AS [IndexType],
DDIPS.avg_fragmentation_in_percent AS [FragmentationPercent],
DDIPS.page_count AS [PageCount],
DDIPS.page_count * 8.0 / 1024 AS [SizeMB]
FROM sys.dm_db_index_physical_stats(
DB_ID(),
NULL,
NULL,
NULL,
'LIMITED'
) AS DDIPS
INNER JOIN sys.tables T ON T.object_id = DDIPS.object_id
INNER JOIN sys.schemas S ON T.schema_id = S.schema_id
INNER JOIN sys.indexes I ON I.object_id = DDIPS.object_id AND I.index_id = DDIPS.index_id
WHERE
DDIPS.database_id = DB_ID()
AND T.is_ms_shipped = 0
AND I.index_id > 0
AND I.name IS NOT NULL
AND DDIPS.page_count >= 100
AND DDIPS.avg_fragmentation_in_percent > 10
ORDER BY
DDIPS.avg_fragmentation_in_percent DESC;
| Fragmentation | What to do |
|---|---|
| below 5% | ignore it |
| 5–30% | REORGANIZE |
| above 30% | REBUILD |
page_count >= 100, because smaller indexes sit in memory anyway — rebuilding them wastes
time and log.
Leave it on 'LIMITED'. 'DETAILED' reads every page of the index and can flatten prod.
3. Index sizes
SELECT
SCHEMA_NAME(t.schema_id) AS [SchemaName],
t.name AS [TableName],
i.name AS [IndexName],
i.type_desc AS [IndexType],
SUM(ps.used_page_count) * 8.0 / 1024 AS [IndexSizeMB],
SUM(ps.row_count) AS [RowCount]
FROM sys.dm_db_partition_stats ps
JOIN sys.indexes i ON ps.object_id = i.object_id AND ps.index_id = i.index_id
JOIN sys.tables t ON ps.object_id = t.object_id
WHERE i.index_id > 0
GROUP BY
t.schema_id, t.name, i.object_id, i.index_id, i.name, i.type_desc
HAVING SUM(ps.used_page_count) * 8.0 / 1024 > 1
ORDER BY [IndexSizeMB] DESC;
* 8.0 / 1024 — a page is 8 KB
index_id > 0 drops heaps. index_id = 1 is the clustered index, i.e. the table data itself —
always at the top of the list, nothing to worry about.
4. Indexes and usage
S Tier for debugging
Magnum opus of the ms sql optimizer, I put it last, because only those who took the time to read the previous queries are worthy of it
SELECT
SCHEMA_NAME(T.schema_id) AS Schema_Name,
T.name AS Table_Name,
IX.name AS Index_Name,
IX.type_desc AS Index_Type,
IX.is_unique AS IsUnique,
IX.is_primary_key AS IsPrimaryKey,
IX.is_unique_constraint AS IsUniqueConstraint,
SUM(PS.row_count) AS 'RowCount',
SUM(PS.used_page_count) * 8 AS IndexSizeKB,
SUM(PS.used_page_count) * 8.0 / 1024 AS IndexSizeMB,
ISNULL(IXUS.user_seeks, 0) AS NumOfSeeks,
ISNULL(IXUS.user_scans, 0) AS NumOfScans,
ISNULL(IXUS.user_lookups, 0) AS NumOfLookups,
ISNULL(IXUS.user_updates, 0) AS NumOfUpdates,
IXUS.last_user_seek AS LastSeek,
IXUS.last_user_scan AS LastScan,
IXUS.last_user_lookup AS LastLookup,
IXUS.last_user_update AS LastUpdate
FROM sys.indexes IX
JOIN sys.tables T ON T.object_id = IX.object_id
JOIN sys.dm_db_partition_stats PS ON PS.object_id = IX.object_id AND PS.index_id = IX.index_id
LEFT JOIN sys.dm_db_index_usage_stats IXUS ON IXUS.database_id = DB_ID() AND IXUS.index_id = IX.index_id AND IXUS.object_id = IX.object_id
WHERE T.is_ms_shipped = 0 AND IX.index_id > 0
GROUP BY
T.schema_id, T.name, IX.name, IX.type_desc, IX.is_unique, IX.is_primary_key, IX.is_unique_constraint, IXUS.user_seeks, IXUS.user_scans,
IXUS.user_lookups, IXUS.user_updates, IXUS.last_user_seek, IXUS.last_user_scan, IXUS.last_user_lookup, IXUS.last_user_update
ORDER BY IndexSizeMB DESC;
TL;DR:
lots of seeks → the index is usually pulling its weight
lots of scans → find out why it is scanning
lots of lookups → check whether the indexes are covering
lots of updates → the cost of keeping the index around
In most cases:
| Operation | Typically | What it means |
|---|---|---|
| Index Seek | 🟢 good | SQL Server hits a specific range of the index |
| Index Scan | 🟡 depends | reads a large chunk of the index, or all of it |
| Table Scan / Heap Scan | 🟡/🔴 depends | reads the table with no useful index |
| Key/Bookmark Lookup | 🟡/🔴 | after finding rows in the index it has to fetch the remaining columns from the table/clustered index separately (try INCLUDE) |
The big one:
sys.dm_db_index_usage_statsresets when the service restarts. Zero reads doesn’t mean “unused” — it may mean “the server came up yesterday”.
Check how long it’s even been counting:
SELECT sqlserver_start_time FROM sys.dm_os_sys_info;