Skip to content

Commit adad5ff

Browse files
authored
Merge pull request #3619 from ReeceGoding/dev
sp_BlitzIndex: Added reports of resumable index operations (closes #3609 )
2 parents 085f739 + 842f6d0 commit adad5ff

File tree

2 files changed

+240
-7
lines changed

2 files changed

+240
-7
lines changed

Documentation/sp_BlitzIndex_Checks_by_Priority.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ Before adding a new check, make sure to add a Github issue for it first, and hav
66

77
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.
88

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

1212
| Priority | FindingsGroup | Finding | URL | CheckID |
1313
| -------- | ----------------------- | --------------------------------------------------------------- | ----------------------------------------------- | ------- |
1414
| 10 | Over-Indexing | Many NC Indexes on a Single Table | https://www.brentozar.com/go/IndexHoarder | 20 |
1515
| 10 | Over-Indexing | Unused NC Index with High Writes | https://www.brentozar.com/go/IndexHoarder | 22 |
16+
| 10 | Resumable Indexing | Resumable Index Operation Paused | https://www.BrentOzar.com/go/resumable | 122 |
17+
| 10 | Resumable Indexing | Resumable Index Operation Running | https://www.BrentOzar.com/go/resumable | 123 |
1618
| 20 | Redundant Indexes | Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 1 |
1719
| 30 | Redundant Indexes | Approximate Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 2 |
1820
| 40 | Index Suggestion | High Value Missing Index | https://www.brentozar.com/go/indexaphobia | 50 |

sp_BlitzIndex.sql

Lines changed: 236 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,11 @@ IF OBJECT_ID('tempdb..#FilteredIndexes') IS NOT NULL
261261
DROP TABLE #FilteredIndexes;
262262

263263
IF OBJECT_ID('tempdb..#Ignore_Databases') IS NOT NULL
264-
DROP TABLE #Ignore_Databases
265-
264+
DROP TABLE #Ignore_Databases;
265+
266+
IF OBJECT_ID('tempdb..#IndexResumableOperations') IS NOT NULL
267+
DROP TABLE #IndexResumableOperations;
268+
266269
IF OBJECT_ID('tempdb..#dm_db_partition_stats_etc') IS NOT NULL
267270
DROP TABLE #dm_db_partition_stats_etc
268271
IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL
@@ -803,6 +806,59 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL
803806
column_name NVARCHAR(128) NULL
804807
);
805808

809+
CREATE TABLE #IndexResumableOperations
810+
(
811+
database_name NVARCHAR(128) NULL,
812+
database_id INT NOT NULL,
813+
schema_name NVARCHAR(128) NOT NULL,
814+
table_name NVARCHAR(128) NOT NULL,
815+
/*
816+
Every following non-computed column has
817+
the same definitions as in
818+
sys.index_resumable_operations.
819+
*/
820+
[object_id] INT NOT NULL,
821+
index_id INT NOT NULL,
822+
[name] NVARCHAR(128) NOT NULL,
823+
/*
824+
We have done nothing to make this query text pleasant
825+
to read. Until somebody has a better idea, we trust
826+
that copying Microsoft's approach is wise.
827+
*/
828+
sql_text NVARCHAR(MAX) NULL,
829+
last_max_dop_used SMALLINT NOT NULL,
830+
partition_number INT NULL,
831+
state TINYINT NOT NULL,
832+
state_desc NVARCHAR(60) NULL,
833+
start_time DATETIME NOT NULL,
834+
last_pause_time DATETIME NULL,
835+
total_execution_time INT NOT NULL,
836+
percent_complete FLOAT NOT NULL,
837+
page_count BIGINT NOT NULL,
838+
/*
839+
sys.indexes will not always have the name of the index
840+
because a resumable CREATE INDEX does not populate
841+
sys.indexes until it is done.
842+
So it is better to work out the full name here
843+
rather than pull it from another temp table.
844+
*/
845+
[db_schema_table_index] AS
846+
[schema_name] + N'.' + [table_name] + N'.' + [name],
847+
/* For convenience. */
848+
reserved_MB_pretty_print AS
849+
CONVERT(NVARCHAR(100), CONVERT(MONEY, page_count * 8. / 1024.))
850+
+ 'MB and '
851+
+ state_desc,
852+
more_info AS
853+
N'New index: SELECT * FROM ' + QUOTENAME(database_name) +
854+
N'.sys.index_resumable_operations WHERE [object_id] = ' +
855+
CONVERT(NVARCHAR(100), [object_id]) +
856+
N'; Old index: ' +
857+
N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],N'''') +
858+
N', @SchemaName=' + QUOTENAME([schema_name],N'''') +
859+
N', @TableName=' + QUOTENAME([table_name],N'''') + N';'
860+
);
861+
806862
CREATE TABLE #Ignore_Databases
807863
(
808864
DatabaseName NVARCHAR(128),
@@ -2517,8 +2573,53 @@ OPTION (RECOMPILE);';
25172573
BEGIN CATCH
25182574
RAISERROR (N'Skipping #FilteredIndexes population due to error, typically low permissions.', 0,1) WITH NOWAIT;
25192575
END CATCH
2576+
END;
2577+
2578+
IF @Mode NOT IN(1, 2, 3)
2579+
/*
2580+
The sys.index_resumable_operations view was a 2017 addition, so we need to check for it and go dynamic.
2581+
*/
2582+
AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'index_resumable_operations')
2583+
BEGIN
2584+
SET @dsql=N'SELECT @i_DatabaseName AS database_name,
2585+
DB_ID(@i_DatabaseName) AS [database_id],
2586+
s.name AS schema_name,
2587+
t.name AS table_name,
2588+
iro.[object_id],
2589+
iro.index_id,
2590+
iro.name,
2591+
iro.sql_text,
2592+
iro.last_max_dop_used,
2593+
iro.partition_number,
2594+
iro.state,
2595+
iro.state_desc,
2596+
iro.start_time,
2597+
iro.last_pause_time,
2598+
iro.total_execution_time,
2599+
iro.percent_complete,
2600+
iro.page_count
2601+
FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_resumable_operations AS iro
2602+
JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t
2603+
ON t.object_id = iro.object_id
2604+
JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s
2605+
ON t.schema_id = s.schema_id
2606+
OPTION(RECOMPILE);'
2607+
2608+
BEGIN TRY
2609+
RAISERROR (N'Inserting data into #IndexResumableOperations',0,1) WITH NOWAIT;
2610+
INSERT #IndexResumableOperations
2611+
( database_name, database_id, schema_name, table_name,
2612+
[object_id], index_id, name, sql_text, last_max_dop_used, partition_number, state, state_desc,
2613+
start_time, last_pause_time, total_execution_time, percent_complete, page_count )
2614+
EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName;
2615+
END TRY
2616+
BEGIN CATCH
2617+
RAISERROR (N'Skipping #IndexResumableOperations population due to error, typically low permissions', 0,1) WITH NOWAIT;
2618+
END CATCH
2619+
END;
2620+
2621+
25202622
END;
2521-
END;
25222623

25232624
END;
25242625
END TRY
@@ -2967,7 +3068,8 @@ BEGIN
29673068
SELECT '#ComputedColumns' AS table_name, * FROM #ComputedColumns;
29683069
SELECT '#TraceStatus' AS table_name, * FROM #TraceStatus;
29693070
SELECT '#CheckConstraints' AS table_name, * FROM #CheckConstraints;
2970-
SELECT '#FilteredIndexes' AS table_name, * FROM #FilteredIndexes;
3071+
SELECT '#FilteredIndexes' AS table_name, * FROM #FilteredIndexes;
3072+
SELECT '#IndexResumableOperations' AS table_name, * FROM #IndexResumableOperations;
29713073
END
29723074

29733075

@@ -3185,7 +3287,50 @@ BEGIN
31853287
ORDER BY s.auto_created, s.user_created, s.name, hist.step_number;';
31863288
EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID;
31873289
END
3188-
END
3290+
3291+
/* Check for resumable index operations. */
3292+
IF (SELECT TOP (1) [object_id] FROM #IndexResumableOperations WHERE [object_id] = @ObjectID AND database_id = @DatabaseID) IS NOT NULL
3293+
BEGIN
3294+
SELECT
3295+
N'Resumable Index Operation' AS finding,
3296+
N'This may invalidate your analysis!' AS warning,
3297+
iro.state_desc + ' on ' + iro.db_schema_table_index +
3298+
CASE iro.state
3299+
WHEN 0 THEN
3300+
' at MAXDOP ' + CONVERT(NVARCHAR(30), iro.last_max_dop_used) +
3301+
'. First started ' + CONVERT(NVARCHAR(50), iro.start_time, 120) + '. ' +
3302+
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete after ' +
3303+
CONVERT(NVARCHAR(30), iro.total_execution_time) +
3304+
' minute(s). This blocks DDL and can pile up ghosts.'
3305+
WHEN 1 THEN
3306+
' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + '. ' +
3307+
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete' +
3308+
/*
3309+
At 100% completion, resumable indexes open up a transaction and go back to paused for what ought to be a moment.
3310+
Updating statistics is one of the things that it can do in this false paused state.
3311+
Updating stats can take a while, so we point it out as a likely delay.
3312+
It seems that any of the normal operations that happen at the very end of an index build can cause this.
3313+
*/
3314+
CASE WHEN iro.percent_complete > 99.9
3315+
THEN '. It is probably still running, perhaps updating statistics.'
3316+
ELSE ' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time)
3317+
+ ' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts.'
3318+
END
3319+
ELSE ' which is an undocumented resumable index state description.'
3320+
END AS details,
3321+
N'https://www.BrentOzar.com/go/resumable' AS URL,
3322+
iro.more_info AS [More Info]
3323+
FROM #IndexResumableOperations AS iro
3324+
WHERE iro.database_id = @DatabaseID
3325+
AND iro.[object_id] = @ObjectID
3326+
OPTION ( RECOMPILE );
3327+
END
3328+
ELSE
3329+
BEGIN
3330+
SELECT N'No resumable index operations.' AS finding;
3331+
END;
3332+
3333+
END /* END @ShowColumnstoreOnly = 0 */
31893334

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

3714+
----------------------------------------
3715+
--Resumable Indexing: Check_id 122-123
3716+
----------------------------------------
3717+
/*
3718+
This is more complicated than you would expect!
3719+
As of SQL Server 2022, I am aware of 6 cases that we need to check:
3720+
1) A resumable rowstore CREATE INDEX that is currently running
3721+
2) A resumable rowstore CREATE INDEX that is currently paused
3722+
3) A resumable rowstore REBUILD that is currently running
3723+
4) A resumable rowstore REBUILD that is currently paused
3724+
5) A resumable rowstore CREATE INDEX [...] DROP_EXISTING = ON that is currently running
3725+
6) A resumable rowstore CREATE INDEX [...] DROP_EXISTING = ON that is currently paused
3726+
In cases 1 and 2, sys.indexes has no data at all about the index in question.
3727+
This makes #IndexSanity much harder to use, since it depends on sys.indexes.
3728+
We must therefore get as much from #IndexResumableOperations as possible.
3729+
*/
3730+
RAISERROR(N'check_id 122: Resumable Index Operation Paused', 0,1) WITH NOWAIT;
3731+
INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding,
3732+
[database_name], URL, details, index_definition, secret_columns,
3733+
index_usage_summary, index_size_summary, create_tsql, more_info )
3734+
SELECT 122 AS check_id,
3735+
i.index_sanity_id,
3736+
10 AS Priority,
3737+
N'Resumable Indexing' AS findings_group,
3738+
N'Resumable Index Operation Paused' AS finding,
3739+
iro.[database_name] AS [Database Name],
3740+
N'https://www.BrentOzar.com/go/resumable' AS URL,
3741+
iro.state_desc + ' on ' + iro.db_schema_table_index +
3742+
' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + '. ' +
3743+
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete' +
3744+
/*
3745+
At 100% completion, resumable indexes open up a transaction and go back to paused for what ought to be a moment.
3746+
Updating statistics is one of the things that it can do in this false paused state.
3747+
Updating stats can take a while, so we point it out as a likely delay.
3748+
It seems that any of the normal operations that happen at the very end of an index build can cause this.
3749+
*/
3750+
CASE WHEN iro.percent_complete > 99.9
3751+
THEN '. It is probably still running, perhaps updating statistics.'
3752+
ELSE ' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time)
3753+
+ ' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts.'
3754+
END AS details,
3755+
'Old index: ' + ISNULL(i.index_definition, 'not found. Either the index is new or you need @IncludeInactiveIndexes = 1') AS index_definition,
3756+
i.secret_columns,
3757+
i.index_usage_summary,
3758+
'New index: ' + iro.reserved_MB_pretty_print + '; Old index: ' + ISNULL(sz.index_size_summary,'not found.') AS index_size_summary,
3759+
'New index: ' + iro.sql_text AS create_tsql,
3760+
iro.more_info
3761+
FROM #IndexResumableOperations iro
3762+
LEFT JOIN #IndexSanity AS i ON i.database_id = iro.database_id
3763+
AND i.[object_id] = iro.[object_id]
3764+
AND i.index_id = iro.index_id
3765+
LEFT JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id
3766+
WHERE iro.state = 1
3767+
OPTION ( RECOMPILE );
3768+
3769+
RAISERROR(N'check_id 123: Resumable Index Operation Running', 0,1) WITH NOWAIT;
3770+
INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding,
3771+
[database_name], URL, details, index_definition, secret_columns,
3772+
index_usage_summary, index_size_summary, create_tsql, more_info )
3773+
SELECT 123 AS check_id,
3774+
i.index_sanity_id,
3775+
10 AS Priority,
3776+
N'Resumable Indexing' AS findings_group,
3777+
N'Resumable Index Operation Running' AS finding,
3778+
iro.[database_name] AS [Database Name],
3779+
N'https://www.BrentOzar.com/go/resumable' AS URL,
3780+
iro.state_desc + ' on ' + iro.db_schema_table_index +
3781+
' at MAXDOP ' + CONVERT(NVARCHAR(30), iro.last_max_dop_used) +
3782+
'. First started ' + CONVERT(NVARCHAR(50), iro.start_time, 120) + '. ' +
3783+
CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + '% complete after ' +
3784+
CONVERT(NVARCHAR(30), iro.total_execution_time) +
3785+
' minute(s). This blocks DDL and can pile up ghosts.' AS details,
3786+
'Old index: ' + ISNULL(i.index_definition, 'not found. Either the index is new or you need @IncludeInactiveIndexes = 1') AS index_definition,
3787+
i.secret_columns,
3788+
i.index_usage_summary,
3789+
'New index: ' + iro.reserved_MB_pretty_print + '; Old index: ' + ISNULL(sz.index_size_summary,'not found.') AS index_size_summary,
3790+
'New index: ' + iro.sql_text AS create_tsql,
3791+
iro.more_info
3792+
FROM #IndexResumableOperations iro
3793+
LEFT JOIN #IndexSanity AS i ON i.database_id = iro.database_id
3794+
AND i.[object_id] = iro.[object_id]
3795+
AND i.index_id = iro.index_id
3796+
LEFT JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id
3797+
WHERE iro.state = 0
3798+
OPTION ( RECOMPILE );
3799+
35693800
----------------------------------------
35703801
--Aggressive Indexes: Check_id 10-19
35713802
----------------------------------------

0 commit comments

Comments
 (0)