Here's the code:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; DECLARE @IndexName AS NVARCHAR(128) = N'YourIndexName'; --Handle the case where the left or right was quoted manually but not the opposite side IF LEFT(@IndexName, 1) <> '[' SET @IndexName = '[' + @IndexName; IF RIGHT(@IndexName, 1) <> ']' SET @IndexName = @IndexName + ']'; ;WITH XMLNAMESPACES (DEFAULT 'http://schemas.microsoft.com/sqlserver/2004/07/showplan') , cte AS (SELECT plan_handle, query_plan_hash, statement_start_offset, statement_end_offset, MIN(creation_time) OVER(PARTITION BY qs.query_plan_hash ORDER BY qs.query_plan_hash) creation_time, MAX(last_execution_time) OVER(PARTITION BY qs.query_plan_hash ORDER BY qs.query_plan_hash) last_execution_time, SUM(total_worker_time) OVER(PARTITION BY qs.query_plan_hash ORDER BY qs.query_plan_hash) total_worker_time, SUM(total_logical_reads) OVER(PARTITION BY qs.query_plan_hash ORDER BY qs.query_plan_hash) total_logical_reads, SUM(execution_count) OVER(PARTITION BY qs.query_plan_hash ORDER BY qs.query_plan_hash) execution_count, ROW_NUMBER() OVER(PARTITION BY qs.query_plan_hash ORDER BY qs.query_plan_hash) row_num FROM sys.dm_exec_query_stats AS qs ) , cte2 AS (SELECT * FROM cte OUTER APPLY sys.dm_exec_query_plan(cte.plan_handle) AS qp WHERE (query_plan.exist('//IndexScan[not(@Lookup)]/Object[@Index=sql:variable("@IndexName")]') = 1 OR query_plan IS NULL) AND cte.row_num = 1 ) , cte3 AS (SELECT ISNULL( SUBSTRING(qt.text, (cte2.statement_start_offset / 2) + 1, ((CASE cte2.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text) ELSE cte2.statement_end_offset END - cte2.statement_start_offset) / 2) + 1), qt.text) AS TSQL, TRY_CAST(qp.query_plan AS XML) query_plan, cte2.creation_time, cte2.last_execution_time, cte2.total_worker_time, cte2.total_logical_reads, cte2.execution_count FROM cte2 CROSS APPLY sys.dm_exec_text_query_plan(cte2.plan_handle, cte2.statement_start_offset, cte2.statement_end_offset) AS qp CROSS APPLY sys.dm_exec_sql_text(cte2.plan_handle) AS qt) SELECT * FROM cte3 WHERE query_plan.exist('//IndexScan[not(@Lookup)]/Object[@Index=sql:variable("@IndexName")]') = 1 ORDER BY cte3.execution_count DESC OPTION (RECOMPILE);
I admit it's quite messy, but gets me relatively clean data and runs in 2 minutes. Quite nice improvement over original.
Before using this script three warnings:
- Remember that plan cache doesn't necessary hold all execution plans, especially after instance restart
- This script won't catch index usage, if plan is not parse-able as XML.
- Considering the above be careful when performing index maintenance based on script results.