Microsoft Dynamics AX general performance analysis scripts page 2
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