This is page 2 of 8 of the general performance analysis scripts online for the Performance Analyser 1.20 tool. See page 1 for the introduction. Use the links in the table below to navigate between pages.
- General analysis | |
---|---|
Analyse SQL Configuration | Page 1 |
Analyse SQL Indexes | Page 2 |
Analyse SQL Queries | Page 3 |
Analyse Blocking | Page 4 |
Baseline - benchmark queries | Page 5 |
- AX Specific | |
Analyse AX Configuration | Page 6 |
Analyse AX Indexes | Page 7 |
Analyse AX Queries | Page 8 |
Analyse SQL Indexes
INDEXES_BY_SIZE
INDEX_ACTIVITY
COMPRESSED_INDEXES
EXACT_DUPLICATE_INDEXES
SUBSET_DUPLICATE_INDEXES
INCLUDED_COLUMN_INDEXES
UNUSED_INDEXES
TABLES_WITHOUT_CLUSTERED_INDEX
ADJUST_CLUSTERED_INDEXES
INDEXES_BEING_SCANNED
SEARCH_QUERY_PLANS_FOR_INDEX_USAGE
-- --------------------------------------------------------------
--
-- INDEXES_BY_SIZE
--
-- List top 100 Largest Tables, Investigate for data retention
-- purposes or incorrect application configuration such as logging
----------------------------------------------------------------
USE DynamicsPerf
SELECT TOP 100 DATABASE_NAME,
TABLE_NAME,
Sum(CASE
WHEN INDEX_DESCRIPTION LIKE 'CLUSTERED%'
OR INDEX_DESCRIPTION LIKE 'HEAP%' THEN PAGE_COUNT * 8 / 1024
END) AS SIZEMB_DATA,
Sum(CASE
WHEN INDEX_DESCRIPTION LIKE 'NONCLUSTERED%' THEN PAGE_COUNT * 8 / 1024
END) AS SIZEMB_INDEXES,
Count(CASE
WHEN INDEX_DESCRIPTION LIKE 'NONCLUSTERED%' THEN TABLE_NAME
END) AS NO_OF_INDEXES,
Max(CASE
WHEN ( DATA_COMPRESSION > 0 )
AND ( INDEX_DESCRIPTION LIKE 'CLUSTERED%'
OR INDEX_DESCRIPTION LIKE 'HEAP%' ) THEN 'Y'
ELSE 'N'
END) AS DATA_COMPRESSED,
Max(CASE
WHEN ( DATA_COMPRESSION > 0 )
AND ( INDEX_DESCRIPTION LIKE 'NONCLUSTERED%' ) THEN 'Y'
ELSE 'N'
END) AS INDEXES_COMPRESSED
FROM INDEX_STATS_CURR_VW
GROUP BY DATABASE_NAME,
TABLE_NAME
ORDER BY 3 DESC
-- --------------------------------------------------------------
--
-- INDEX_ACTIVITY
--
-- List Activity by table, Investigate for activity
-- purposes or incorrect application configuration such as logging
----------------------------------------------------------------
SELECT DATABASE_NAME,
TABLE_NAME,
CASE
WHEN ( Sum(USER_UPDATES + USER_SEEKS + USER_SCANS
+ USER_LOOKUPS) = 0 ) THEN NULL
ELSE ( Cast(Sum(USER_SEEKS + USER_SCANS + USER_LOOKUPS) AS DECIMAL) / Cast(Sum(USER_UPDATES + USER_SEEKS + USER_SCANS
+ USER_LOOKUPS) AS DECIMAL) )
END AS RatioOfReads,
CASE
WHEN ( Sum(USER_UPDATES + USER_SEEKS + USER_SCANS
+ USER_LOOKUPS) = 0 ) THEN NULL
ELSE ( Cast(Sum(USER_UPDATES) AS DECIMAL) / Cast(Sum(USER_UPDATES + USER_SEEKS + USER_SCANS
+ USER_LOOKUPS) AS DECIMAL) )
END AS RatioOfWrites,
Sum(USER_SEEKS + USER_SCANS + USER_LOOKUPS) AS TotalReadOperations,
Sum(USER_UPDATES) AS TotalWriteOperations,
Sum(USER_UPDATES + USER_SEEKS + USER_SCANS
+ USER_LOOKUPS) AS TotalOperations
FROM INDEX_STATS_CURR_VW /*sys.dm_db_index_usage_stats*/
GROUP BY DATABASE_NAME,
TABLE_NAME
--order by TotalOperations desc
--order by TotalReadOperations desc
ORDER BY TotalWriteOperations DESC
-- --------------------------------------------------------------
--
-- COMPRESSED_INDEXES
--
-- Find indexes that are compressed.
----------------------------------------------------------------
SELECT *
FROM INDEX_STATS_CURR_VW
WHERE DATA_COMPRESSION > 0
ORDER BY USER_UPDATES DESC
-- --------------------------------------------------------------
--
-- EXACT_DUPLICATE_INDEXES
--
-- Tables that have 2 or more indexes with the exact same key
-- Trust me, this happens.
-- NOTE: If you are using included columns the indexes may be unique
----------------------------------------------------------------
SELECT DATABASE_NAME,
TABLE_NAME,
INDEX_KEYS,
Count(*)
FROM INDEX_STATS_CURR_VW
GROUP BY DATABASE_NAME,
TABLE_NAME,
INDEX_KEYS
HAVING Count(INDEX_KEYS) > 1
ORDER BY TABLE_NAME
-- --------------------------------------------------------------
--
-- SUBSET_DUPLICATE_INDEXES
--
-- Just as bad (and even more common) are indexes that are a left key
-- subset of another index on same table. Unless the subsset key is
-- unique, its usefulness is subsumed of the superset key.
-- NOTE: If you are using included columns the indexes may be unique
----------------------------------------------------------------
SELECT O.DATABASE_NAME,
O.TABLE_NAME,
O.INDEX_NAME AS SUBSET_INDEX,
O.INDEX_KEYS AS SUBSET_INDEX_KEYS,
O.INDEX_DESCRIPTION AS SUBSET_INDEX_DESCRIPTION,
O.PAGE_COUNT * 8 / 1024 AS SUBSET_SIZE_MB,
I.INDEX_NAME AS SUPERSET_INDEX,
I.INDEX_KEYS AS SUPERSET_KEYS
FROM INDEX_STATS_CURR_VW O
LEFT JOIN INDEX_STATS_CURR_VW I
ON I.RUN_NAME = O.RUN_NAME
AND I.DATABASE_NAME = O.DATABASE_NAME
AND I.TABLE_NAME = O.TABLE_NAME
AND I.INDEX_KEYS <> O.INDEX_KEYS
AND I.INDEX_KEYS LIKE O.INDEX_KEYS + ',%'
WHERE O.INDEX_DESCRIPTION NOT LIKE '%UNIQUE%'
AND I.INDEX_NAME IS NOT NULL
ORDER BY O.DATABASE_NAME,
I.TABLE_NAME,
I.INDEX_KEYS
-- --------------------------------------------------------------
--
-- INCLUDED_COLUMN_INDEXES
--
-- Find indexes with high number of include columns
-- This can be indication of table that needs a different clustered index
-- or poorly designed query. Will cause table size BLOAT and
-- potential blocking issues as SQL updates the included columns
----------------------------------------------------------------
SELECT TOP 100 *
FROM INDEX_STATS_CURR_VW
WHERE INCLUDED_COLUMNS <> 'N/A'
ORDER BY LEN(INCLUDED_COLUMNS) DESC
-- --------------------------------------------------------------
--
-- UNUSED_INDEXES
--
-- Find indexes that are not being used. If an index enforces
-- a uniqueness constraint, we must retain it.
--
--**************************************************************
-- DO NOT DELETE THESE INDEXES UNLESS YOU ARE SURE YOU HAVE RUN
-- EVERY PROCESS IN YOUR DYNAMICS DATABASE INCLUDING YEAR END !!
--**************************************************************
----------------------------------------------------------------
SELECT PAGE_COUNT * 8 / 1024 AS SIZE_MB,
*
FROM INDEX_HISTORICAL_VW
WHERE
-- criteria for never been used indexes
USER_SEEKS = 0
AND USER_SCANS = 0
-- uncomment next 2 lines if you want to see indexes with very low usages
--AND USER_SEEKS < 100
--AND USER_SCANS < 100
AND INDEX_DESCRIPTION NOT LIKE '%UNIQUE%'
AND INDEX_DESCRIPTION NOT LIKE '%HEAP%'
AND (PAGE_COUNT * 8 / 1024) > 0 -- only show indexes consuming space
ORDER BY 1 DESC
-- --------------------------------------------------------------
--
-- TABLES_WITHOUT_CLUSTERED_INDEX
--
-- Tables missing clustered indexes
-- Heaps with multiple non-clustered indexes.
-- Use the following script to identify a good clustered index
-- based solely on user activity
----------------------------------------------------------------
SELECT CLUS.TABLE_NAME,
CLUS.INDEX_NAME AS HEAP_TABLE,
CLUS.INDEX_KEYS AS CLUSTERED_KEYS,
NONCLUS.INDEX_NAME AS NONCLUSTERED_INDEX,
NONCLUS.INDEX_KEYS,
( NONCLUS.RANGE_SCAN_COUNT - CLUS.RANGE_SCAN_COUNT ) AS NONCLUSTERED_VS_CLUSTERED_RANGE_COUNT,
CLUS.USER_SEEKS AS CLUSTERED_USER_SEEKS,
CLUS.USER_SCANS AS CLUSTERED_USER_SCANS,
CLUS.SINGLETON_LOOKUP_COUNT AS CLUSTERED_SINGLE_LOOKUPS,
CLUS.RANGE_SCAN_COUNT AS CLUSTERED_RANGE_SCAN,
NONCLUS.USER_SEEKS AS NONCLUSTERED_USER_SEEKS,
NONCLUS.USER_SCANS AS NONCLUSTERED_USER_SCANS,
NONCLUS.SINGLETON_LOOKUP_COUNT AS NONCLUSTERED_SINGLE_LOOKUPS,
NONCLUS.RANGE_SCAN_COUNT AS NONCLUSTERED_RANGE_SCANS,
NONCLUS.USER_UPDATES AS NONCLUSTERED_USER_UPDATES
FROM INDEX_STATS_CURR_VW CLUS
INNER JOIN INDEX_STATS_CURR_VW NONCLUS
ON CLUS.TABLE_NAME = NONCLUS.TABLE_NAME
AND CLUS.DATABASE_NAME = NONCLUS.DATABASE_NAME
AND CLUS.INDEX_NAME <> NONCLUS.INDEX_NAME
WHERE CLUS.INDEX_DESCRIPTION LIKE 'HEAP%'
AND ( ( NONCLUS.RANGE_SCAN_COUNT > CLUS.RANGE_SCAN_COUNT )
OR ( NONCLUS.SINGLETON_LOOKUP_COUNT > CLUS.SINGLETON_LOOKUP_COUNT ) )
ORDER BY CLUS.USER_LOOKUPS DESC,
CLUS.TABLE_NAME,
( NONCLUS.RANGE_SCAN_COUNT - CLUS.RANGE_SCAN_COUNT ) DESC
-- ----------------------------------------------------------------------------------------
--
-- ADJUST_CLUSTERED_INDEXES
--
--
-- Find clustered indexes that could be changed
-- to 1 of the non-clustered indexes that has more usage than the clustered index
-- Use the following script to identify the non-clustered index
-- that could be the clustered index based solely on user activity
-- This should be the LAST activty done in a performance tuning session
--
--NOTE - CHANGING CLUSTERED INDEXES WILL TAKE LONG TIME TO DO
-- AND REQUIRES DOWNTIME TO IMPLEMENT
--------------------------------------------------------------------------------------------
SELECT CLUS.TABLE_NAME,
CLUS.INDEX_NAME AS CLUSTERED_INDEX,
CLUS.INDEX_KEYS AS CLUSTERED_KEYS,
NONCLUS.INDEX_NAME AS NONCLUSTERED_INDEX,
NONCLUS.INDEX_KEYS,
( NONCLUS.RANGE_SCAN_COUNT - CLUS.RANGE_SCAN_COUNT ) AS NONCLUSTERED_VS_CLUSTERED_RANGE_COUNT,
CLUS.USER_SEEKS AS CLUSTERED_USER_SEEKS,
CLUS.USER_SCANS AS CLUSTERED_USER_SCANS,
CLUS.SINGLETON_LOOKUP_COUNT AS CLUSTERED_SINGLE_LOOKUPS,
CLUS.RANGE_SCAN_COUNT AS CLUSTERED_RANGE_SCAN,
NONCLUS.USER_SEEKS AS NONCLUSTERED_USER_SEEKS,
NONCLUS.USER_SCANS AS NONCLUSTERED_USER_SCANS,
NONCLUS.SINGLETON_LOOKUP_COUNT AS NONCLUSTERED_SINGLE_LOOKUPS,
NONCLUS.RANGE_SCAN_COUNT AS NONCLUSTERED_RANGE_SCANS,
NONCLUS.USER_UPDATES AS NONCLUSTERED_USER_UPDATES
FROM INDEX_STATS_CURR_VW CLUS
INNER JOIN INDEX_STATS_CURR_VW NONCLUS
ON CLUS.TABLE_NAME = NONCLUS.TABLE_NAME
AND CLUS.DATABASE_NAME = NONCLUS.DATABASE_NAME
AND CLUS.INDEX_NAME <> NONCLUS.INDEX_NAME
WHERE CLUS.INDEX_DESCRIPTION LIKE 'CLUSTERED%' AND (( NONCLUS.RANGE_SCAN_COUNT > CLUS.RANGE_SCAN_COUNT )
OR ( NONCLUS.SINGLETON_LOOKUP_COUNT > CLUS.SINGLETON_LOOKUP_COUNT ))
ORDER BY CLUS.USER_LOOKUPS DESC, CLUS.TABLE_NAME,
( NONCLUS.RANGE_SCAN_COUNT - CLUS.RANGE_SCAN_COUNT ) DESC
-- --------------------------------------------------------------
--
-- INDEXES_BEING_SCANNED
--
-- Find non-clustered indexes that are being scanned. Generally
-- this will indicate that key columns are out of order compared
-- to query predicates
--
----------------------------------------------------------------
SELECT TOP 100 *
FROM INDEX_STATS_CURR_VW
WHERE USER_SCANS > 0
AND INDEX_DESCRIPTION LIKE 'NONCLUSTERED%'
ORDER BY USER_SCANS DESC
-- --------------------------------------------------------------
--
-- SEARCH_QUERY_PLANS_FOR_INDEX_USAGE
--
-- Using indexes identifies in the previous query, list queries
-- whose execution plan references a specific index; order by
-- most expensive (logical reads)
--
----------------------------------------------------------------
SELECT TOP 100 *
FROM QUERY_STATS_CURR_VW
WHERE QUERY_PLAN_TEXT LIKE '%Index_Name%'
ORDER BY TOTAL_LOGICAL_READS DESC