@@ -261,8 +261,11 @@ IF OBJECT_ID('tempdb..#FilteredIndexes') IS NOT NULL
261261 DROP TABLE #FilteredIndexes;
262262
263263IF 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+
266269IF OBJECT_ID (' tempdb..#dm_db_partition_stats_etc' ) IS NOT NULL
267270 DROP TABLE #dm_db_partition_stats_etc
268271IF 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
25232624END ;
25242625END 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;
29713073END
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