Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Documentation/sp_BlitzIndex_Checks_by_Priority.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ Before adding a new check, make sure to add a Github issue for it first, and hav

If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too.

CURRENT HIGH CHECKID: 121
If you want to add a new check, start at 122.
CURRENT HIGH CHECKID: 123
If you want to add a new check, start at 124.

| Priority | FindingsGroup | Finding | URL | CheckID |
| -------- | ----------------------- | --------------------------------------------------------------- | ----------------------------------------------- | ------- |
| 10 | Over-Indexing | Many NC Indexes on a Single Table | https://www.brentozar.com/go/IndexHoarder | 20 |
| 10 | Over-Indexing | Unused NC Index with High Writes | https://www.brentozar.com/go/IndexHoarder | 22 |
| 10 | Resumable Indexing | Resumable Index Operation Paused | https://www.BrentOzar.com/go/resumable | 122 |
| 10 | Resumable Indexing | Resumable Index Operation Running | https://www.BrentOzar.com/go/resumable | 123 |
| 20 | Redundant Indexes | Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 1 |
| 30 | Redundant Indexes | Approximate Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 2 |
| 40 | Index Suggestion | High Value Missing Index | https://www.brentozar.com/go/indexaphobia | 50 |
Expand Down
241 changes: 236 additions & 5 deletions sp_BlitzIndex.sql
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,11 @@ IF OBJECT_ID('tempdb..#FilteredIndexes') IS NOT NULL
DROP TABLE #FilteredIndexes;

IF OBJECT_ID('tempdb..#Ignore_Databases') IS NOT NULL
DROP TABLE #Ignore_Databases

DROP TABLE #Ignore_Databases;

IF OBJECT_ID('tempdb..#IndexResumableOperations') IS NOT NULL
DROP TABLE #IndexResumableOperations;

IF OBJECT_ID('tempdb..#dm_db_partition_stats_etc') IS NOT NULL
DROP TABLE #dm_db_partition_stats_etc
IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL
Expand Down Expand Up @@ -803,6 +806,59 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL
column_name NVARCHAR(128) NULL
);

CREATE TABLE #IndexResumableOperations
(
database_name NVARCHAR(128) NULL,
database_id INT NOT NULL,
schema_name NVARCHAR(128) NOT NULL,
table_name NVARCHAR(128) NOT NULL,
/*
Every following non-computed column has
the same definitions as in
sys.index_resumable_operations.
*/
[object_id] INT NOT NULL,
index_id INT NOT NULL,
[name] NVARCHAR(128) NOT NULL,
/*
We have done nothing to make this query text pleasant
to read. Until somebody has a better idea, we trust
that copying Microsoft's approach is wise.
*/
sql_text NVARCHAR(MAX) NULL,
last_max_dop_used SMALLINT NOT NULL,
partition_number INT NULL,
state TINYINT NOT NULL,
state_desc NVARCHAR(60) NULL,
start_time DATETIME NOT NULL,
last_pause_time DATETIME NULL,
total_execution_time INT NOT NULL,
percent_complete FLOAT NOT NULL,
page_count BIGINT NOT NULL,
/*
sys.indexes will not always have the name of the index
because a resumable CREATE INDEX does not populate
sys.indexes until it is done.
So it is better to work out the full name here
rather than pull it from another temp table.
*/
[db_schema_table_index] AS
[schema_name] + N'.' + [table_name] + N'.' + [name],
/* For convenience. */
reserved_MB_pretty_print AS
CONVERT(NVARCHAR(100), CONVERT(MONEY, page_count * 8. / 1024.))
+ 'MB and '
+ state_desc,
more_info AS
N'New index: SELECT * FROM ' + QUOTENAME(database_name) +
N'.sys.index_resumable_operations WHERE [object_id] = ' +
CONVERT(NVARCHAR(100), [object_id]) +
N'; Old index: ' +
N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],N'''') +
N', @SchemaName=' + QUOTENAME([schema_name],N'''') +
N', @TableName=' + QUOTENAME([table_name],N'''') + N';'
);

CREATE TABLE #Ignore_Databases
(
DatabaseName NVARCHAR(128),
Expand Down Expand Up @@ -2517,8 +2573,53 @@ OPTION (RECOMPILE);';
BEGIN CATCH
RAISERROR (N'Skipping #FilteredIndexes population due to error, typically low permissions.', 0,1) WITH NOWAIT;
END CATCH
END;

IF @Mode NOT IN(1, 2, 3)
/*
The sys.index_resumable_operations view was a 2017 addition, so we need to check for it and go dynamic.
*/
AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'index_resumable_operations')
BEGIN
SET @dsql=N'SELECT @i_DatabaseName AS database_name,
DB_ID(@i_DatabaseName) AS [database_id],
s.name AS schema_name,
t.name AS table_name,
iro.[object_id],
iro.index_id,
iro.name,
iro.sql_text,
iro.last_max_dop_used,
iro.partition_number,
iro.state,
iro.state_desc,
iro.start_time,
iro.last_pause_time,
iro.total_execution_time,
iro.percent_complete,
iro.page_count
FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_resumable_operations AS iro
JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t
ON t.object_id = iro.object_id
JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s
ON t.schema_id = s.schema_id
OPTION(RECOMPILE);'

BEGIN TRY
RAISERROR (N'Inserting data into #IndexResumableOperations',0,1) WITH NOWAIT;
INSERT #IndexResumableOperations
( database_name, database_id, schema_name, table_name,
[object_id], index_id, name, sql_text, last_max_dop_used, partition_number, state, state_desc,
start_time, last_pause_time, total_execution_time, percent_complete, page_count )
EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName;
END TRY
BEGIN CATCH
RAISERROR (N'Skipping #IndexResumableOperations population due to error, typically low permissions', 0,1) WITH NOWAIT;
END CATCH
END;


END;
END;

END;
END TRY
Expand Down Expand Up @@ -2967,7 +3068,8 @@ BEGIN
SELECT '#ComputedColumns' AS table_name, * FROM #ComputedColumns;
SELECT '#TraceStatus' AS table_name, * FROM #TraceStatus;
SELECT '#CheckConstraints' AS table_name, * FROM #CheckConstraints;
SELECT '#FilteredIndexes' AS table_name, * FROM #FilteredIndexes;
SELECT '#FilteredIndexes' AS table_name, * FROM #FilteredIndexes;
SELECT '#IndexResumableOperations' AS table_name, * FROM #IndexResumableOperations;
END


Expand Down Expand Up @@ -3185,7 +3287,50 @@ BEGIN
ORDER BY s.auto_created, s.user_created, s.name, hist.step_number;';
EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID;
END
END

/* Check for resumable index operations. */
IF (SELECT TOP (1) [object_id] FROM #IndexResumableOperations WHERE [object_id] = @ObjectID AND database_id = @DatabaseID) IS NOT NULL
BEGIN
SELECT
N'Resumable Index Operation' AS finding,
N'This may invalidate your analysis!' AS warning,
iro.state_desc + ' on ' + iro.db_schema_table_index +
CASE iro.state
WHEN 0 THEN
' at MAXDOP ' + CONVERT(NVARCHAR(30), iro.last_max_dop_used) +
'. First started ' + CONVERT(NVARCHAR(50), iro.start_time, 120) + '. ' +
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete after ' +
CONVERT(NVARCHAR(30), iro.total_execution_time) +
' minute(s). This blocks DDL and can pile up ghosts.'
WHEN 1 THEN
' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + '. ' +
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete' +
/*
At 100% completion, resumable indexes open up a transaction and go back to paused for what ought to be a moment.
Updating statistics is one of the things that it can do in this false paused state.
Updating stats can take a while, so we point it out as a likely delay.
It seems that any of the normal operations that happen at the very end of an index build can cause this.
*/
CASE WHEN iro.percent_complete > 99.9
THEN '. It is probably still running, perhaps updating statistics.'
ELSE ' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time)
+ ' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts.'
END
ELSE ' which is an undocumented resumable index state description.'
END AS details,
N'https://www.BrentOzar.com/go/resumable' AS URL,
iro.more_info AS [More Info]
FROM #IndexResumableOperations AS iro
WHERE iro.database_id = @DatabaseID
AND iro.[object_id] = @ObjectID
OPTION ( RECOMPILE );
END
ELSE
BEGIN
SELECT N'No resumable index operations.' AS finding;
END;

END /* END @ShowColumnstoreOnly = 0 */

/* Visualize columnstore index contents. More info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2584 */
IF 2 = (SELECT SUM(1) FROM sys.all_objects WHERE name IN ('column_store_row_groups','column_store_segments'))
Expand Down Expand Up @@ -3566,6 +3711,92 @@ BEGIN
ORDER BY ips.total_rows DESC, ip.[schema_name], ip.[object_name], ip.key_column_names, ip.include_column_names
OPTION ( RECOMPILE );

----------------------------------------
--Resumable Indexing: Check_id 122-123
----------------------------------------
/*
This is more complicated than you would expect!
As of SQL Server 2022, I am aware of 6 cases that we need to check:
1) A resumable rowstore CREATE INDEX that is currently running
2) A resumable rowstore CREATE INDEX that is currently paused
3) A resumable rowstore REBUILD that is currently running
4) A resumable rowstore REBUILD that is currently paused
5) A resumable rowstore CREATE INDEX [...] DROP_EXISTING = ON that is currently running
6) A resumable rowstore CREATE INDEX [...] DROP_EXISTING = ON that is currently paused
In cases 1 and 2, sys.indexes has no data at all about the index in question.
This makes #IndexSanity much harder to use, since it depends on sys.indexes.
We must therefore get as much from #IndexResumableOperations as possible.
*/
RAISERROR(N'check_id 122: Resumable Index Operation Paused', 0,1) WITH NOWAIT;
INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding,
[database_name], URL, details, index_definition, secret_columns,
index_usage_summary, index_size_summary, create_tsql, more_info )
SELECT 122 AS check_id,
i.index_sanity_id,
10 AS Priority,
N'Resumable Indexing' AS findings_group,
N'Resumable Index Operation Paused' AS finding,
iro.[database_name] AS [Database Name],
N'https://www.BrentOzar.com/go/resumable' AS URL,
iro.state_desc + ' on ' + iro.db_schema_table_index +
' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + '. ' +
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete' +
/*
At 100% completion, resumable indexes open up a transaction and go back to paused for what ought to be a moment.
Updating statistics is one of the things that it can do in this false paused state.
Updating stats can take a while, so we point it out as a likely delay.
It seems that any of the normal operations that happen at the very end of an index build can cause this.
*/
CASE WHEN iro.percent_complete > 99.9
THEN '. It is probably still running, perhaps updating statistics.'
ELSE ' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time)
+ ' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts.'
END AS details,
'Old index: ' + ISNULL(i.index_definition, 'not found. Either the index is new or you need @IncludeInactiveIndexes = 1') AS index_definition,
i.secret_columns,
i.index_usage_summary,
'New index: ' + iro.reserved_MB_pretty_print + '; Old index: ' + ISNULL(sz.index_size_summary,'not found.') AS index_size_summary,
'New index: ' + iro.sql_text AS create_tsql,
iro.more_info
FROM #IndexResumableOperations iro
LEFT JOIN #IndexSanity AS i ON i.database_id = iro.database_id
AND i.[object_id] = iro.[object_id]
AND i.index_id = iro.index_id
LEFT JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id
WHERE iro.state = 1
OPTION ( RECOMPILE );

RAISERROR(N'check_id 123: Resumable Index Operation Running', 0,1) WITH NOWAIT;
INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding,
[database_name], URL, details, index_definition, secret_columns,
index_usage_summary, index_size_summary, create_tsql, more_info )
SELECT 123 AS check_id,
i.index_sanity_id,
10 AS Priority,
N'Resumable Indexing' AS findings_group,
N'Resumable Index Operation Running' AS finding,
iro.[database_name] AS [Database Name],
N'https://www.BrentOzar.com/go/resumable' AS URL,
iro.state_desc + ' on ' + iro.db_schema_table_index +
' at MAXDOP ' + CONVERT(NVARCHAR(30), iro.last_max_dop_used) +
'. First started ' + CONVERT(NVARCHAR(50), iro.start_time, 120) + '. ' +
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete after ' +
CONVERT(NVARCHAR(30), iro.total_execution_time) +
' minute(s). This blocks DDL and can pile up ghosts.' AS details,
'Old index: ' + ISNULL(i.index_definition, 'not found. Either the index is new or you need @IncludeInactiveIndexes = 1') AS index_definition,
i.secret_columns,
i.index_usage_summary,
'New index: ' + iro.reserved_MB_pretty_print + '; Old index: ' + ISNULL(sz.index_size_summary,'not found.') AS index_size_summary,
'New index: ' + iro.sql_text AS create_tsql,
iro.more_info
FROM #IndexResumableOperations iro
LEFT JOIN #IndexSanity AS i ON i.database_id = iro.database_id
AND i.[object_id] = iro.[object_id]
AND i.index_id = iro.index_id
LEFT JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id
WHERE iro.state = 0
OPTION ( RECOMPILE );

----------------------------------------
--Aggressive Indexes: Check_id 10-19
----------------------------------------
Expand Down
Loading