diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index b9465696d..83c596c58 100644 --- a/Install-All-Scripts.sql +++ b/Install-All-Scripts.sql @@ -38,7 +38,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -4076,7 +4076,7 @@ AS ''Informational'' AS FindingGroup , ''Backup Compression Default Off'' AS Finding , ''https://www.brentozar.com/go/backup'' AS URL , - ''Uncompressed full backups have happened recently, and backup compression is not turned on at the server level. Backup compression is included with SQL Server 2008R2 & newer, even in Standard Edition. We recommend turning backup compression on by default so that ad-hoc backups will get compressed.'' + ''Uncompressed full backups have happened recently, and backup compression is not turned on at the server level. Backup compression is included with Standard Edition. We recommend turning backup compression on by default so that ad-hoc backups will get compressed.'' FROM sys.configurations WHERE configuration_id = 1579 AND CAST(value_in_use AS INT) = 0 AND EXISTS (SELECT * FROM msdb.dbo.backupset WHERE backup_size = compressed_backup_size AND type = ''D'' AND backup_finish_date >= DATEADD(DD, -14, GETDATE())) OPTION (RECOMPILE);'; @@ -7749,52 +7749,44 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] through [%d] and [%d] through [%d].', 0, 1, 194, 197, 237, 255) WITH NOWAIT; INSERT INTO #DatabaseScopedConfigurationDefaults (configuration_id, [name], default_value, default_value_for_secondary, CheckID) - SELECT 1, 'MAXDOP', '0', NULL, 194 - UNION ALL - SELECT 2, 'LEGACY_CARDINALITY_ESTIMATION', '0', NULL, 195 - UNION ALL - SELECT 3, 'PARAMETER_SNIFFING', '1', NULL, 196 - UNION ALL - SELECT 4, 'QUERY_OPTIMIZER_HOTFIXES', '0', NULL, 197 - UNION ALL - SELECT 6, 'IDENTITY_CACHE', '1', NULL, 237 - UNION ALL - SELECT 7, 'INTERLEAVED_EXECUTION_TVF', '1', NULL, 238 - UNION ALL - SELECT 8, 'BATCH_MODE_MEMORY_GRANT_FEEDBACK', '1', NULL, 239 - UNION ALL - SELECT 9, 'BATCH_MODE_ADAPTIVE_JOINS', '1', NULL, 240 - UNION ALL - SELECT 10, 'TSQL_SCALAR_UDF_INLINING', '1', NULL, 241 - UNION ALL - SELECT 11, 'ELEVATE_ONLINE', 'OFF', NULL, 242 - UNION ALL - SELECT 12, 'ELEVATE_RESUMABLE', 'OFF', NULL, 243 - UNION ALL - SELECT 13, 'OPTIMIZE_FOR_AD_HOC_WORKLOADS', '0', NULL, 244 - UNION ALL - SELECT 14, 'XTP_PROCEDURE_EXECUTION_STATISTICS', '0', NULL, 245 - UNION ALL - SELECT 15, 'XTP_QUERY_EXECUTION_STATISTICS', '0', NULL, 246 - UNION ALL - SELECT 16, 'ROW_MODE_MEMORY_GRANT_FEEDBACK', '1', NULL, 247 - UNION ALL - SELECT 17, 'ISOLATE_SECURITY_POLICY_CARDINALITY', '0', NULL, 248 - UNION ALL - SELECT 18, 'BATCH_MODE_ON_ROWSTORE', '1', NULL, 249 - UNION ALL - SELECT 19, 'DEFERRED_COMPILATION_TV', '1', NULL, 250 - UNION ALL - SELECT 20, 'ACCELERATED_PLAN_FORCING', '1', NULL, 251 - UNION ALL - SELECT 21, 'GLOBAL_TEMPORARY_TABLE_AUTO_DROP', '1', NULL, 252 - UNION ALL - SELECT 22, 'LIGHTWEIGHT_QUERY_PROFILING', '1', NULL, 253 - UNION ALL - SELECT 23, 'VERBOSE_TRUNCATION_WARNINGS', '1', NULL, 254 - UNION ALL - SELECT 24, 'LAST_QUERY_PLAN_STATS', '0', NULL, 255; - EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + VALUES + (1, 'MAXDOP', '0', NULL, 194), + (2, 'LEGACY_CARDINALITY_ESTIMATION', '0', NULL, 195), + (3, 'PARAMETER_SNIFFING', '1', NULL, 196), + (4, 'QUERY_OPTIMIZER_HOTFIXES', '0', NULL, 197), + (6, 'IDENTITY_CACHE', '1', NULL, 237), + (7, 'INTERLEAVED_EXECUTION_TVF', '1', NULL, 238), + (8, 'BATCH_MODE_MEMORY_GRANT_FEEDBACK', '1', NULL, 239), + (9, 'BATCH_MODE_ADAPTIVE_JOINS', '1', NULL, 240), + (10, 'TSQL_SCALAR_UDF_INLINING', '1', NULL, 241), + (11, 'ELEVATE_ONLINE', 'OFF', NULL, 242), + (12, 'ELEVATE_RESUMABLE', 'OFF', NULL, 243), + (13, 'OPTIMIZE_FOR_AD_HOC_WORKLOADS', '0', NULL, 244), + (14, 'XTP_PROCEDURE_EXECUTION_STATISTICS', '0', NULL, 245), + (15, 'XTP_QUERY_EXECUTION_STATISTICS', '0', NULL, 246), + (16, 'ROW_MODE_MEMORY_GRANT_FEEDBACK', '1', NULL, 247), + (17, 'ISOLATE_SECURITY_POLICY_CARDINALITY', '0', NULL, 248), + (18, 'BATCH_MODE_ON_ROWSTORE', '1', NULL, 249), + (19, 'DEFERRED_COMPILATION_TV', '1', NULL, 250), + (20, 'ACCELERATED_PLAN_FORCING', '1', NULL, 251), + (21, 'GLOBAL_TEMPORARY_TABLE_AUTO_DROP', '1', NULL, 252), + (22, 'LIGHTWEIGHT_QUERY_PROFILING', '1', NULL, 253), + (23, 'VERBOSE_TRUNCATION_WARNINGS', '1', NULL, 254), + (24, 'LAST_QUERY_PLAN_STATS', '0', NULL, 255), + (25, 'PAUSED_RESUMABLE_INDEX_ABORT_DURATION_MINUTES', '1440', NULL, 267), + (26, 'DW_COMPATIBILITY_LEVEL', '0', NULL, 267), + (27, 'EXEC_QUERY_STATS_FOR_SCALAR_FUNCTIONS', '1', NULL, 267), + (28, 'PARAMETER_SENSITIVE_PLAN_OPTIMIZATION', '1', NULL, 267), + (29, 'ASYNC_STATS_UPDATE_WAIT_AT_LOW_PRIORITY', '0', NULL, 267), + (31, 'CE_FEEDBACK', '1', NULL, 267), + (33, 'MEMORY_GRANT_FEEDBACK_PERSISTENCE', '1', NULL, 267), + (34, 'MEMORY_GRANT_FEEDBACK_PERCENTILE_GRANT', '1', NULL, 267), + (35, 'OPTIMIZED_PLAN_FORCING', '1', NULL, 267), + (37, 'DOP_FEEDBACK', '0', NULL, 267), + (38, 'LEDGER_DIGEST_STORAGE_ENDPOINT', 'OFF', NULL, 267), + (39, 'FORCE_SHOWPLAN_RUNTIME_PARAMETER_COLLECTION', '0', NULL, 267); + +EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT def1.CheckID, DB_NAME(), 210, ''Non-Default Database Scoped Config'', dsc.[name], ''https://www.brentozar.com/go/dbscope'', (''Set value: '' + COALESCE(CAST(dsc.value AS NVARCHAR(100)),''Empty'') + '' Default: '' + COALESCE(CAST(def1.default_value AS NVARCHAR(100)),''Empty'') + '' Set value for secondary: '' + COALESCE(CAST(dsc.value_for_secondary AS NVARCHAR(100)),''Empty'') + '' Default value for secondary: '' + COALESCE(CAST(def1.default_value_for_secondary AS NVARCHAR(100)),''Empty'')) FROM [?].sys.database_scoped_configurations dsc INNER JOIN #DatabaseScopedConfigurationDefaults def1 ON dsc.configuration_id = def1.configuration_id @@ -10565,7 +10557,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN @@ -11443,7 +11435,7 @@ AS SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN @@ -13217,7 +13209,8 @@ ALTER PROCEDURE dbo.sp_BlitzCache @MinutesBack INT = NULL, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, - @VersionCheckMode BIT = 0 + @VersionCheckMode BIT = 0, + @KeepCRLF BIT = 0 WITH RECOMPILE AS BEGIN @@ -13225,7 +13218,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -13427,7 +13420,12 @@ IF @Help = 1 UNION ALL SELECT N'@VersionCheckMode', N'BIT', - N'Setting this to 1 will make the procedure stop after setting @Version and @VersionDate.'; + N'Setting this to 1 will make the procedure stop after setting @Version and @VersionDate.' + + UNION ALL + SELECT N'@KeepCRLF', + N'BIT', + N'Retain CR/LF in query text to avoid issues caused by line comments.'; /* Column definitions */ @@ -15704,7 +15702,10 @@ SET PercentCPU = y.PercentCPU, /* Strip newlines and tabs. Tabs are replaced with multiple spaces so that the later whitespace trim will completely eliminate them */ - QueryText = REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + QueryText = CASE WHEN @KeepCRLF = 1 + THEN REPLACE(QueryText, @tab, ' ') + ELSE REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + END FROM ( SELECT PlanHandle, CASE @total_cpu WHEN 0 THEN 0 @@ -15760,7 +15761,10 @@ SET PercentCPU = y.PercentCPU, /* Strip newlines and tabs. Tabs are replaced with multiple spaces so that the later whitespace trim will completely eliminate them */ - QueryText = REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + QueryText = CASE WHEN @KeepCRLF = 1 + THEN REPLACE(QueryText, @tab, ' ') + ELSE REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + END FROM ( SELECT DatabaseName, SqlHandle, @@ -20600,7 +20604,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -20682,6 +20686,7 @@ DECLARE @ColumnList NVARCHAR(MAX); DECLARE @ColumnListWithApostrophes NVARCHAR(MAX); DECLARE @PartitionCount INT; DECLARE @OptimizeForSequentialKey BIT = 0; +DECLARE @ResumableIndexesDisappearAfter INT = 0; DECLARE @StringToExecute NVARCHAR(MAX); /* If user was lazy and just used @ObjectName with a fully qualified table name, then lets parse out the various parts */ @@ -20812,8 +20817,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 @@ -21324,7 +21332,8 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL history_schema_name NVARCHAR(128) NOT NULL, start_column_name NVARCHAR(128) NOT NULL, end_column_name NVARCHAR(128) NOT NULL, - period_name NVARCHAR(128) NOT NULL + period_name NVARCHAR(128) NOT NULL, + history_table_object_id INT NULL ); CREATE TABLE #CheckConstraints @@ -21354,6 +21363,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), @@ -22976,7 +23038,8 @@ OPTION (RECOMPILE);'; oa.htn AS history_table_name, c1.name AS start_column_name, c2.name AS end_column_name, - p.name AS period_name + p.name AS period_name, + t.history_table_id AS history_table_object_id FROM ' + QUOTENAME(@DatabaseName) + N'.sys.periods AS p INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t ON p.object_id = t.object_id @@ -23002,7 +23065,7 @@ OPTION (RECOMPILE);'; RAISERROR('@dsql is null',16,1); INSERT #TemporalTables ( database_name, database_id, schema_name, table_name, history_schema_name, - history_table_name, start_column_name, end_column_name, period_name ) + history_table_name, start_column_name, end_column_name, period_name, history_table_object_id ) EXEC sp_executesql @dsql; END; @@ -23068,8 +23131,63 @@ 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; + + SET @dsql=N'SELECT @ResumableIndexesDisappearAfter = CAST(value AS INT) + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.database_scoped_configurations + WHERE name = ''PAUSED_RESUMABLE_INDEX_ABORT_DURATION_MINUTES'' + AND value > 0;' + EXEC sp_executesql @dsql, N'@ResumableIndexesDisappearAfter INT OUT', @ResumableIndexesDisappearAfter out; + + IF @ResumableIndexesDisappearAfter IS NULL + SET @ResumableIndexesDisappearAfter = 0; + + 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 @@ -23516,9 +23634,11 @@ BEGIN SELECT '#Statistics' AS table_name, * FROM #Statistics; SELECT '#PartitionCompressionInfo' AS table_name, * FROM #PartitionCompressionInfo; SELECT '#ComputedColumns' AS table_name, * FROM #ComputedColumns; - SELECT '#TraceStatus' AS table_name, * FROM #TraceStatus; + SELECT '#TraceStatus' AS table_name, * FROM #TraceStatus; + SELECT '#TemporalTables' AS table_name, * FROM #TemporalTables; 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 @@ -23736,7 +23856,55 @@ 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 + N' on ' + iro.db_schema_table_index + + CASE iro.state + WHEN 0 THEN + N' at MAXDOP ' + CONVERT(NVARCHAR(30), iro.last_max_dop_used) + + N'. First started ' + CONVERT(NVARCHAR(50), iro.start_time, 120) + N'. ' + + CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + N'% complete after ' + + CONVERT(NVARCHAR(30), iro.total_execution_time) + + N' minute(s). ' + + CASE WHEN @ResumableIndexesDisappearAfter > 0 + THEN N' Will be automatically removed by the database server at ' + CONVERT(NVARCHAR(50), (DATEADD(mi, @ResumableIndexesDisappearAfter, iro.last_pause_time)), 121) + N'. ' + ELSE N' Will not be automatically removed by the database server. ' + END + + N'This blocks DDL and can pile up ghosts.' + WHEN 1 THEN + N' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + N'. ' + + CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + N'% 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 N'. It is probably still running, perhaps updating statistics.' + ELSE N' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time) + + N' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts.' + END + ELSE N' 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')) @@ -24117,6 +24285,96 @@ 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 + N' on ' + iro.db_schema_table_index + + N' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + N'. ' + + CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + N'% 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 N'. It is probably still running, perhaps updating statistics.' + ELSE N' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time) + + N' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts. ' + END + + CASE WHEN @ResumableIndexesDisappearAfter > 0 + THEN N' Will be automatically removed by the database server at ' + CONVERT(NVARCHAR(50), (DATEADD(mi, @ResumableIndexesDisappearAfter, iro.last_pause_time)), 121) + N'. ' + ELSE N' Will not be automatically removed by the database server. ' + END AS details, + N'Old index: ' + ISNULL(i.index_definition, N'not found. Either the index is new or you need @IncludeInactiveIndexes = 1') AS index_definition, + i.secret_columns, + i.index_usage_summary, + N'New index: ' + iro.reserved_MB_pretty_print + N'; Old index: ' + ISNULL(sz.index_size_summary,'not found.') AS index_size_summary, + N'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 ---------------------------------------- @@ -24274,7 +24532,38 @@ BEGIN WHERE i.filter_columns_not_in_index IS NOT NULL ORDER BY i.db_schema_object_indexid OPTION ( RECOMPILE ); - + + RAISERROR(N'check_id 124: History Table With NonClustered Index', 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 ) + SELECT 124 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'History Table With NonClustered Index' AS finding, + i.[database_name] AS [Database Name], + N'https://sqlserverfast.com/blog/hugo/2023/09/an-update-on-merge/' AS URL, + N'The history table ' + + QUOTENAME(hist.history_schema_name) + + '.' + + QUOTENAME(hist.history_table_name) + + ' has a non-clustered index. This can cause MERGEs on the main table to fail! See item 8 on the URL.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + JOIN #TemporalTables hist + ON i.[object_id] = hist.history_table_object_id + AND i.[database_id] = hist.[database_id] + WHERE hist.history_table_object_id IS NOT NULL + AND i.index_type = 2 /* NC only */ + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); + ---------------------------------------- --Self Loathing Indexes : Check_id 40-49 ---------------------------------------- @@ -27063,7 +27352,7 @@ BEGIN CATCH GO IF OBJECT_ID('dbo.sp_BlitzLock') IS NULL BEGIN - EXEC ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); + EXECUTE ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); END; GO @@ -27082,6 +27371,11 @@ ALTER PROCEDURE @TargetSessionType sysname = NULL, @VictimsOnly bit = 0, @DeadlockType nvarchar(20) = NULL, + @TargetDatabaseName sysname = NULL, + @TargetSchemaName sysname = NULL, + @TargetTableName sysname = NULL, + @TargetColumnName sysname = NULL, + @TargetTimestampColumnName sysname = NULL, @Debug bit = 0, @Help bit = 0, @Version varchar(30) = NULL OUTPUT, @@ -27100,7 +27394,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF @VersionCheckMode = 1 BEGIN @@ -27117,6 +27411,7 @@ BEGIN Variables you can use: + /*Filtering parameters*/ @DatabaseName: If you want to filter to a specific database @StartDate: The date you want to start searching on, defaults to last 7 days @@ -27135,16 +27430,32 @@ BEGIN @LoginName: If you want to filter to a specific login + @DeadlockType: Search for regular or parallel deadlocks specifically + + /*Extended Event session details*/ @EventSessionName: If you want to point this at an XE session rather than the system health session. - @TargetSessionType: Can be ''ring_buffer'' or ''event_file''. Leave NULL to auto-detect. + @TargetSessionType: Can be ''ring_buffer'', ''event_file'', or ''table''. Leave NULL to auto-detect. + /*Output to a table*/ @OutputDatabaseName: If you want to output information to a specific database @OutputSchemaName: Specify a schema name to output information to a specific Schema @OutputTableName: Specify table name to to output information to a specific table + /*Point at a table containing deadlock XML*/ + @TargetDatabaseName: The database that contains the table with deadlock report XML + + @TargetSchemaName: The schema of the table containing deadlock report XML + + @TargetTableName: The name of the table containing deadlock report XML + + @TargetColumnName: The name of the XML column that contains the deadlock report + + @TargetTimestampColumnName: The name of the datetime column for filtering by date range (optional) + + To learn more, visit http://FirstResponderKit.org where you can download new versions for free, watch training videos on how it works, get more info on the findings, contribute your own code, and more. @@ -27262,7 +27573,11 @@ BEGIN @StartDateOriginal datetime = @StartDate, @EndDateOriginal datetime = @EndDate, @StartDateUTC datetime, - @EndDateUTC datetime;; + @EndDateUTC datetime, + @extract_sql nvarchar(MAX), + @validation_sql nvarchar(MAX), + @xe bit, + @xd bit; /*Temporary objects used in the procedure*/ DECLARE @@ -27387,7 +27702,185 @@ BEGIN @TargetSessionType = N'ring_buffer'; END; + IF ISNULL(@TargetDatabaseName, DB_NAME()) IS NOT NULL + AND ISNULL(@TargetSchemaName, N'dbo') IS NOT NULL + AND @TargetTableName IS NOT NULL + AND @TargetColumnName IS NOT NULL + BEGIN + SET @TargetSessionType = N'table'; + END; + + /* Add this after the existing parameter validations */ + IF @TargetSessionType = N'table' + BEGIN + IF @TargetDatabaseName IS NULL + BEGIN + SET @TargetDatabaseName = DB_NAME(); + END; + + IF @TargetSchemaName IS NULL + BEGIN + SET @TargetSchemaName = N'dbo'; + END; + + IF @TargetTableName IS NULL + OR @TargetColumnName IS NULL + BEGIN + RAISERROR(N' + When using a table as a source, you must specify @TargetTableName, and @TargetColumnName. + When @TargetDatabaseName or @TargetSchemaName is NULL, they default to DB_NAME() AND dbo', + 11, 1) WITH NOWAIT; + RETURN; + END; + + /* Check if target database exists */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name = @TargetDatabaseName + ) + BEGIN + RAISERROR(N'The specified @TargetDatabaseName %s does not exist.', 11, 1, @TargetDatabaseName) WITH NOWAIT; + RETURN; + END; + + /* Use dynamic SQL to validate schema, table, and column existence */ + SET @validation_sql = N' + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + WHERE s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetSchemaName %s does not exist in database %s.'', 11, 1, @schema, @database) WITH NOWAIT; + RETURN; + END; + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE t.name = @table + AND s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetTableName %s does not exist in schema %s in database %s.'', 11, 1, @table, @schema, @database) WITH NOWAIT; + RETURN; + END; + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @column + AND t.name = @table + AND s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetColumnName %s does not exist in table %s.%s in database %s.'', 11, 1, @column, @schema, @table, @database) WITH NOWAIT; + RETURN; + END; + + /* Validate column is XML type */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.types AS ty + ON c.user_type_id = ty.user_type_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @column + AND t.name = @table + AND s.name = @schema + AND ty.name = N''xml'' + ) + BEGIN + RAISERROR(N''The specified @TargetColumnName %s must be of XML data type.'', 11, 1, @column) WITH NOWAIT; + RETURN; + END;'; + + /* Validate timestamp_column if specified */ + IF @TargetTimestampColumnName IS NOT NULL + BEGIN + SET @validation_sql = @validation_sql + N' + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @timestamp_column + AND t.name = @table + AND s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetTimestampColumnName %s does not exist in table %s.%s in database %s.'', 11, 1, @timestamp_column, @schema, @table, @database) WITH NOWAIT; + RETURN; + END; + + /* Validate timestamp column is datetime type */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.types AS ty + ON c.user_type_id = ty.user_type_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @timestamp_column + AND t.name = @table + AND s.name = @schema + AND ty.name LIKE ''%date%'' + ) + BEGIN + RAISERROR(N''The specified @TargetTimestampColumnName %s must be of datetime data type.'', 11, 1, @timestamp_column) WITH NOWAIT; + RETURN; + END;'; + END; + + IF @Debug = 1 BEGIN PRINT @validation_sql; END; + + EXECUTE sys.sp_executesql + @validation_sql, + N' + @database sysname, + @schema sysname, + @table sysname, + @column sysname, + @timestamp_column sysname + ', + @TargetDatabaseName, + @TargetSchemaName, + @TargetTableName, + @TargetColumnName, + @TargetTimestampColumnName; + END; + + IF @Azure = 0 + AND LOWER(@TargetSessionType) <> N'table' BEGIN IF NOT EXISTS ( @@ -27404,8 +27897,9 @@ BEGIN RETURN; END; END; - + IF @Azure = 1 + AND LOWER(@TargetSessionType) <> N'table' BEGIN IF NOT EXISTS ( @@ -27464,7 +27958,7 @@ BEGIN N'@r sysname OUTPUT'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute, @StringToExecuteParams, @r OUTPUT; @@ -27504,7 +27998,7 @@ BEGIN N' ADD spid smallint NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new wait_resource column, add it. See Github #3101. */ @@ -27520,7 +28014,7 @@ BEGIN N' ADD wait_resource nvarchar(MAX) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new client option column, add it. See Github #3101. */ @@ -27536,7 +28030,7 @@ BEGIN N' ADD client_option_1 varchar(500) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new client option column, add it. See Github #3101. */ @@ -27552,7 +28046,7 @@ BEGIN N' ADD client_option_2 varchar(500) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new lock mode column, add it. See Github #3101. */ @@ -27568,7 +28062,7 @@ BEGIN N' ADD lock_mode nvarchar(256) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new status column, add it. See Github #3101. */ @@ -27584,7 +28078,7 @@ BEGIN N' ADD status nvarchar(256) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; ELSE /* end if @r is not null. if it is null there is no table, create it from above execution */ @@ -27642,7 +28136,7 @@ BEGIN )'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /*table created.*/ @@ -27657,7 +28151,7 @@ BEGIN N'@r sysname OUTPUT'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute, @StringToExecuteParams, @r OUTPUT; @@ -27685,7 +28179,7 @@ BEGIN );'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; END; @@ -27714,7 +28208,7 @@ BEGIN @OutputTableFindings; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /*create synonym for deadlock table.*/ @@ -27741,7 +28235,7 @@ BEGIN @OutputTableName; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; END; @@ -27813,6 +28307,7 @@ BEGIN ( @Azure = 1 AND @TargetSessionType IS NULL + AND LOWER(@TargetSessionType) <> N'table' ) BEGIN RAISERROR('@TargetSessionType is NULL, assigning for Azure instance', 0, 1) WITH NOWAIT; @@ -28111,6 +28606,148 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; END; + /* If table target */ + IF @TargetSessionType = 'table' + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data from table source %s', 0, 1, @d) WITH NOWAIT; + + /* + First, we need to heck the XML structure. + Depending on the data source, the XML could + contain either the /event or /deadlock nodes. + When the /event nodes are not present, there + is no @name attribute to evaluate. + */ + + SELECT + @extract_sql = N' + SELECT TOP (1) + @xe = xe.e.exist(''.''), + @xd = xd.e.exist(''.'') + FROM [master].[dbo].[bpr] AS x + OUTER APPLY x.[bpr].nodes(''/event'') AS xe(e) + OUTER APPLY x.[bpr].nodes(''/deadlock'') AS xd(e) + OPTION(RECOMPILE); + '; + + IF @Debug = 1 BEGIN PRINT @extract_sql; END; + + EXECUTE sys.sp_executesql + @extract_sql, + N' + @xe bit OUTPUT, + @xd bit OUTPUT + ', + @xe OUTPUT, + @xd OUTPUT; + + + /* Build dynamic SQL to extract the XML */ + IF @xe = 1 + AND @xd IS NULL + BEGIN + SET @extract_sql = N' + SELECT + deadlock_xml = ' + + QUOTENAME(@TargetColumnName) + + N' + FROM ' + + QUOTENAME(@TargetDatabaseName) + + N'.' + + QUOTENAME(@TargetSchemaName) + + N'.' + + QUOTENAME(@TargetTableName) + + N' AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/event'') AS e(x) + WHERE + ( + e.x.exist(''@name[ .= "xml_deadlock_report"]'') = 1 + OR e.x.exist(''@name[ .= "database_xml_deadlock_report"]'') = 1 + OR e.x.exist(''@name[ .= "xml_deadlock_report_filtered"]'') = 1 + )'; + END; + + IF @xe IS NULL + AND @xd = 1 + BEGIN + SET @extract_sql = N' + SELECT + deadlock_xml = ' + + QUOTENAME(@TargetColumnName) + + N' + FROM ' + + QUOTENAME(@TargetDatabaseName) + + N'.' + + QUOTENAME(@TargetSchemaName) + + N'.' + + QUOTENAME(@TargetTableName) + + N' AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/deadlock'') AS e(x) + WHERE 1 = 1'; + END; + + /* Add timestamp filtering if specified */ + IF @TargetTimestampColumnName IS NOT NULL + BEGIN + SET @extract_sql = @extract_sql + N' + AND x.' + QUOTENAME(@TargetTimestampColumnName) + N' >= @StartDate + AND x.' + QUOTENAME(@TargetTimestampColumnName) + N' < @EndDate'; + END; + + /* If no timestamp column but date filtering is needed, handle XML-based filtering when possible */ + IF @TargetTimestampColumnName IS NULL + AND @xe = 1 + AND @xd IS NULL + BEGIN + SET @extract_sql = @extract_sql + N' + AND e.x.exist(''@timestamp[. >= sql:variable("@StartDate") and . < sql:variable("@EndDate")]'') = 1'; + END; + + /*Woof*/ + IF @TargetTimestampColumnName IS NULL + AND @xe IS NULL + AND @xd = 1 + BEGIN + SET @extract_sql = @extract_sql + N' + AND e.x.exist(''(/deadlock/process-list/process/@lasttranstarted)[. >= sql:variable("@StartDate") and . < sql:variable("@EndDate")]'') = 1'; + END; + + SET @extract_sql += N' + OPTION(RECOMPILE); + '; + + IF @Debug = 1 BEGIN PRINT @extract_sql; END; + + /* Execute the dynamic SQL */ + INSERT + #deadlock_data + WITH + (TABLOCKX) + ( + deadlock_xml + ) + EXECUTE sys.sp_executesql + @extract_sql, + N' + @StartDate datetime, + @EndDate datetime + ', + @StartDate, + @EndDate; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + /*Parse process and input buffer xml*/ SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Initial Parse process and input buffer xml %s', 0, 1, @d) WITH NOWAIT; @@ -28126,6 +28763,21 @@ BEGIN FROM #deadlock_data AS d1 LEFT JOIN #t AS t ON 1 = 1 + WHERE @xe = 1 + + UNION ALL + + SELECT + d1.deadlock_xml, + event_date = d1.deadlock_xml.value('(/deadlock/process-list/process/@lasttranstarted)[1]', 'datetime2'), + victim_id = d1.deadlock_xml.value('(/deadlock/victim-list/victimProcess/@id)[1]', 'nvarchar(256)'), + is_parallel = d1.deadlock_xml.exist('/deadlock/resource-list/exchangeEvent'), + is_parallel_batch = d1.deadlock_xml.exist('/deadlock/resource-list/SyncPoint'), + deadlock_graph = d1.deadlock_xml.query('.') + FROM #deadlock_data AS d1 + LEFT JOIN #t AS t + ON 1 = 1 + WHERE @xd = 1 OPTION(RECOMPILE); SET @d = CONVERT(varchar(40), GETDATE(), 109); @@ -28798,7 +29450,7 @@ BEGIN '; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; @@ -28947,7 +29599,7 @@ BEGIN COUNT_BIG(DISTINCT dp.event_date) ) + N' deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -29002,7 +29654,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s) between read queries and modification queries.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -29064,7 +29716,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s).', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -29107,7 +29759,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s).', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -29156,7 +29808,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s).', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -29205,7 +29857,7 @@ BEGIN COUNT_BIG(DISTINCT dp.event_date) ) + N' instances of Serializable deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -29248,7 +29900,7 @@ BEGIN COUNT_BIG(DISTINCT dp.event_date) ) + N' instances of Repeatable Read deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -29310,7 +29962,7 @@ BEGIN N'UNKNOWN' ) + N'.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -29422,7 +30074,7 @@ BEGIN 1, N'' ) + N' locks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY CONVERT(bigint, lt.lock_count) DESC) FROM lock_types AS lt @@ -29492,7 +30144,7 @@ BEGIN dow.database_name, object_name = ds.proc_name, finding_group = N'More Info - Query', - finding = N'EXEC sp_BlitzCache ' + + finding = N'EXECUTE sp_BlitzCache ' + CASE WHEN ds.proc_name = N'adhoc' THEN N'@OnlySqlHandles = ' + ds.sql_handle_csv @@ -29548,7 +30200,7 @@ BEGIN object_name = ds.proc_name, finding_group = N'More Info - Query', finding = - N'EXEC sp_BlitzQueryStore ' + + N'EXECUTE sp_BlitzQueryStore ' + N'@DatabaseName = ' + QUOTENAME(ds.database_name, N'''') + N', ' + @@ -29601,7 +30253,7 @@ BEGIN COUNT_BIG(DISTINCT ds.id) ) + N' deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT ds.id) DESC) FROM #deadlock_stack AS ds @@ -29661,7 +30313,7 @@ BEGIN bi.object_name, finding_group = N'More Info - Table', finding = - N'EXEC sp_BlitzIndex ' + + N'EXECUTE sp_BlitzIndex ' + N'@DatabaseName = ' + QUOTENAME(bi.database_name, N'''') + N', @SchemaName = ' + @@ -29702,19 +30354,19 @@ BEGIN ) ), wait_time_hms = - /*the more wait time you rack up the less accurate this gets, + /*the more wait time you rack up the less accurate this gets, it's either that or erroring out*/ - CASE - WHEN + CASE + WHEN SUM ( CONVERT ( - bigint, + bigint, dp.wait_time ) )/1000 > 2147483647 - THEN + THEN CONVERT ( nvarchar(30), @@ -29727,7 +30379,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, dp.wait_time ) ) @@ -29738,16 +30390,16 @@ BEGIN ), 14 ) - WHEN + WHEN SUM ( CONVERT ( - bigint, + bigint, dp.wait_time ) ) BETWEEN 2147483648 AND 2147483647000 - THEN + THEN CONVERT ( nvarchar(30), @@ -29760,7 +30412,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, dp.wait_time ) ) @@ -29842,7 +30494,7 @@ BEGIN 14 ) + N' [dd hh:mm:ss:ms] of deadlock wait time.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY cs.total_waits DESC) FROM chopsuey AS cs @@ -29916,19 +30568,19 @@ BEGIN ) ) + N' ' + - /*the more wait time you rack up the less accurate this gets, + /*the more wait time you rack up the less accurate this gets, it's either that or erroring out*/ - CASE - WHEN + CASE + WHEN SUM ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) )/1000 > 2147483647 - THEN + THEN CONVERT ( nvarchar(30), @@ -29941,7 +30593,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) ) @@ -29952,16 +30604,16 @@ BEGIN ), 14 ) - WHEN + WHEN SUM ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) ) BETWEEN 2147483648 AND 2147483647000 - THEN + THEN CONVERT ( nvarchar(30), @@ -29974,7 +30626,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) ) @@ -30007,7 +30659,7 @@ BEGIN 14 ) END + N' [dd hh:mm:ss:ms] of deadlock wait time.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY SUM(CONVERT(bigint, wt.total_wait_time_ms)) DESC) FROM wait_time AS wt @@ -30045,7 +30697,7 @@ BEGIN N'There have been ' + RTRIM(COUNT_BIG(DISTINCT aj.event_date)) + N' deadlocks from this Agent Job and Step.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT aj.event_date) DESC) FROM #agent_job AS aj @@ -30738,9 +31390,9 @@ BEGIN SET STATISTICS XML ON; END; - SET @StringToExecute = N' + SET @StringToExecute = N' - INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadLockTbl + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadLockTbl ( ServerName, deadlock_type, @@ -30783,9 +31435,9 @@ BEGIN waiter_waiting_to_close, deadlock_graph ) - EXEC sys.sp_executesql - @deadlock_result;' - EXEC sys.sp_executesql @StringToExecute, N'@deadlock_result NVARCHAR(MAX)', @deadlock_result; + EXECUTE sys.sp_executesql + @deadlock_result;'; + EXECUTE sys.sp_executesql @StringToExecute, N'@deadlock_result NVARCHAR(MAX)', @deadlock_result; IF @Debug = 1 BEGIN @@ -30801,7 +31453,7 @@ BEGIN SET @StringToExecute = N' - INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadlockFindings + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadlockFindings ( ServerName, check_id, @@ -30819,8 +31471,8 @@ BEGIN df.finding FROM #deadlock_findings AS df ORDER BY df.check_id - OPTION(RECOMPILE);' - EXEC sys.sp_executesql @StringToExecute; + OPTION(RECOMPILE);'; + EXECUTE sys.sp_executesql @StringToExecute; RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; @@ -30830,23 +31482,23 @@ BEGIN BEGIN SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Results to client %s', 0, 1, @d) WITH NOWAIT; - + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; - - EXEC sys.sp_executesql + + EXECUTE sys.sp_executesql @deadlock_result; - + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; PRINT @deadlock_result; END; - + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Getting available execution plans for deadlocks %s', 0, 1, @d) WITH NOWAIT; - + SELECT DISTINCT available_plans = 'available_plans', @@ -30913,15 +31565,15 @@ BEGIN min_used_grant_mb = deqs.min_used_grant_kb * 8. / 1024., max_used_grant_mb = - deqs.max_used_grant_kb * 8. / 1024., + deqs.max_used_grant_kb * 8. / 1024., deqs.min_reserved_threads, deqs.max_reserved_threads, deqs.min_used_threads, deqs.max_used_threads, deqs.total_rows, - max_worker_time_ms = + max_worker_time_ms = deqs.max_worker_time / 1000., - max_elapsed_time_ms = + max_elapsed_time_ms = deqs.max_elapsed_time / 1000. INTO #dm_exec_query_stats FROM sys.dm_exec_query_stats AS deqs @@ -30933,7 +31585,7 @@ BEGIN WHERE ap.sql_handle = deqs.sql_handle ) AND deqs.query_hash IS NOT NULL; - + CREATE CLUSTERED INDEX deqs ON #dm_exec_query_stats @@ -30941,7 +31593,7 @@ BEGIN sql_handle, plan_handle ); - + SELECT ap.available_plans, ap.database_name, @@ -30975,7 +31627,7 @@ BEGIN ap.statement_end_offset FROM ( - + SELECT ap.*, c.statement_start_offset, @@ -31027,10 +31679,10 @@ BEGIN OPTION(RECOMPILE, LOOP JOIN, HASH JOIN); RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - + SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Returning findings %s', 0, 1, @d) WITH NOWAIT; - + SELECT df.check_id, df.database_name, @@ -31042,7 +31694,7 @@ BEGIN df.check_id, df.sort_order OPTION(RECOMPILE); - + SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; END; /*done with output to client app.*/ @@ -31126,7 +31778,7 @@ BEGIN END; IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL - BEGIN + BEGIN SELECT table_name = N'#dm_exec_query_stats', * @@ -31161,6 +31813,16 @@ BEGIN @VictimsOnly, DeadlockType = @DeadlockType, + TargetDatabaseName = + @TargetDatabaseName, + TargetSchemaName = + @TargetSchemaName, + TargetTableName = + @TargetTableName, + TargetColumnName = + @TargetColumnName, + TargetTimestampColumnName = + @TargetTimestampColumnName, Debug = @Debug, Help = @@ -31265,7 +31927,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN @@ -32678,7 +33340,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN @@ -32860,7 +33522,7 @@ END; BEGIN TRY DECLARE @CurrentDatabaseContext AS VARCHAR(128) = (SELECT DB_NAME()); -DECLARE @CommandExecuteCheck VARCHAR(315) +DECLARE @CommandExecuteCheck VARCHAR(400); SET @CommandExecuteCheck = 'IF NOT EXISTS (SELECT name FROM ' +@CurrentDatabaseContext+'.sys.objects WHERE type = ''P'' AND name = ''CommandExecute'') BEGIN @@ -34347,7 +35009,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN @@ -34710,6 +35372,8 @@ INSERT INTO dbo.SqlServerVersions (MajorVersionNumber, MinorVersionNumber, Branch, [Url], ReleaseDate, MainstreamSupportEndDate, ExtendedSupportEndDate, MajorVersionName, MinorVersionName) VALUES /*2022*/ + (16, 4185, 'CU18', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate18', '2025-03-13', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 18'), + (16, 4175, 'CU17', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate17', '2025-01-16', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 17'), (16, 4165, 'CU16', 'https://support.microsoft.com/en-us/help/5048033', '2024-11-14', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 16'), (16, 4150, 'CU15 GDR', 'https://support.microsoft.com/en-us/help/5046059', '2024-10-08', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 15 GDR'), (16, 4145, 'CU15', 'https://support.microsoft.com/en-us/help/5041321', '2024-09-25', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 15'), @@ -34732,6 +35396,8 @@ VALUES (16, 1050, 'RTM GDR', 'https://support.microsoft.com/kb/5021522', '2023-02-14', '2028-01-11', '2033-01-11', 'SQL Server 2022 GDR', 'RTM'), (16, 1000, 'RTM', '', '2022-11-15', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'RTM'), /*2019*/ + (15, 4420, 'CU32', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2019/cumulativeupdate32', '2025-02-27', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32'), + (15, 4430, 'CU31', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2019/cumulativeupdate31', '2025-02-13', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 31'), (15, 4415, 'CU30', 'https://support.microsoft.com/kb/5049235', '2024-12-13', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 30'), (15, 4405, 'CU29', 'https://support.microsoft.com/kb/5046365', '2024-10-31', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 29'), (15, 4395, 'CU28 GDR', 'https://support.microsoft.com/kb/5046060', '2024-10-08', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 28 GDR'), @@ -35189,7 +35855,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN diff --git a/Install-Azure.sql b/Install-Azure.sql index 0ca1ddc29..bed3ddb87 100644 --- a/Install-Azure.sql +++ b/Install-Azure.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN @@ -1164,7 +1164,8 @@ ALTER PROCEDURE dbo.sp_BlitzCache @MinutesBack INT = NULL, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, - @VersionCheckMode BIT = 0 + @VersionCheckMode BIT = 0, + @KeepCRLF BIT = 0 WITH RECOMPILE AS BEGIN @@ -1172,7 +1173,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -1374,7 +1375,12 @@ IF @Help = 1 UNION ALL SELECT N'@VersionCheckMode', N'BIT', - N'Setting this to 1 will make the procedure stop after setting @Version and @VersionDate.'; + N'Setting this to 1 will make the procedure stop after setting @Version and @VersionDate.' + + UNION ALL + SELECT N'@KeepCRLF', + N'BIT', + N'Retain CR/LF in query text to avoid issues caused by line comments.'; /* Column definitions */ @@ -3651,7 +3657,10 @@ SET PercentCPU = y.PercentCPU, /* Strip newlines and tabs. Tabs are replaced with multiple spaces so that the later whitespace trim will completely eliminate them */ - QueryText = REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + QueryText = CASE WHEN @KeepCRLF = 1 + THEN REPLACE(QueryText, @tab, ' ') + ELSE REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + END FROM ( SELECT PlanHandle, CASE @total_cpu WHEN 0 THEN 0 @@ -3707,7 +3716,10 @@ SET PercentCPU = y.PercentCPU, /* Strip newlines and tabs. Tabs are replaced with multiple spaces so that the later whitespace trim will completely eliminate them */ - QueryText = REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + QueryText = CASE WHEN @KeepCRLF = 1 + THEN REPLACE(QueryText, @tab, ' ') + ELSE REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + END FROM ( SELECT DatabaseName, SqlHandle, @@ -8545,7 +8557,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN @@ -13555,7 +13567,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -13637,6 +13649,7 @@ DECLARE @ColumnList NVARCHAR(MAX); DECLARE @ColumnListWithApostrophes NVARCHAR(MAX); DECLARE @PartitionCount INT; DECLARE @OptimizeForSequentialKey BIT = 0; +DECLARE @ResumableIndexesDisappearAfter INT = 0; DECLARE @StringToExecute NVARCHAR(MAX); /* If user was lazy and just used @ObjectName with a fully qualified table name, then lets parse out the various parts */ @@ -13767,8 +13780,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 @@ -14279,7 +14295,8 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL history_schema_name NVARCHAR(128) NOT NULL, start_column_name NVARCHAR(128) NOT NULL, end_column_name NVARCHAR(128) NOT NULL, - period_name NVARCHAR(128) NOT NULL + period_name NVARCHAR(128) NOT NULL, + history_table_object_id INT NULL ); CREATE TABLE #CheckConstraints @@ -14309,6 +14326,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), @@ -15931,7 +16001,8 @@ OPTION (RECOMPILE);'; oa.htn AS history_table_name, c1.name AS start_column_name, c2.name AS end_column_name, - p.name AS period_name + p.name AS period_name, + t.history_table_id AS history_table_object_id FROM ' + QUOTENAME(@DatabaseName) + N'.sys.periods AS p INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t ON p.object_id = t.object_id @@ -15957,7 +16028,7 @@ OPTION (RECOMPILE);'; RAISERROR('@dsql is null',16,1); INSERT #TemporalTables ( database_name, database_id, schema_name, table_name, history_schema_name, - history_table_name, start_column_name, end_column_name, period_name ) + history_table_name, start_column_name, end_column_name, period_name, history_table_object_id ) EXEC sp_executesql @dsql; END; @@ -16023,8 +16094,63 @@ 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; + + SET @dsql=N'SELECT @ResumableIndexesDisappearAfter = CAST(value AS INT) + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.database_scoped_configurations + WHERE name = ''PAUSED_RESUMABLE_INDEX_ABORT_DURATION_MINUTES'' + AND value > 0;' + EXEC sp_executesql @dsql, N'@ResumableIndexesDisappearAfter INT OUT', @ResumableIndexesDisappearAfter out; + + IF @ResumableIndexesDisappearAfter IS NULL + SET @ResumableIndexesDisappearAfter = 0; + + 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 @@ -16471,9 +16597,11 @@ BEGIN SELECT '#Statistics' AS table_name, * FROM #Statistics; SELECT '#PartitionCompressionInfo' AS table_name, * FROM #PartitionCompressionInfo; SELECT '#ComputedColumns' AS table_name, * FROM #ComputedColumns; - SELECT '#TraceStatus' AS table_name, * FROM #TraceStatus; + SELECT '#TraceStatus' AS table_name, * FROM #TraceStatus; + SELECT '#TemporalTables' AS table_name, * FROM #TemporalTables; 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 @@ -16691,7 +16819,55 @@ 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 + N' on ' + iro.db_schema_table_index + + CASE iro.state + WHEN 0 THEN + N' at MAXDOP ' + CONVERT(NVARCHAR(30), iro.last_max_dop_used) + + N'. First started ' + CONVERT(NVARCHAR(50), iro.start_time, 120) + N'. ' + + CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + N'% complete after ' + + CONVERT(NVARCHAR(30), iro.total_execution_time) + + N' minute(s). ' + + CASE WHEN @ResumableIndexesDisappearAfter > 0 + THEN N' Will be automatically removed by the database server at ' + CONVERT(NVARCHAR(50), (DATEADD(mi, @ResumableIndexesDisappearAfter, iro.last_pause_time)), 121) + N'. ' + ELSE N' Will not be automatically removed by the database server. ' + END + + N'This blocks DDL and can pile up ghosts.' + WHEN 1 THEN + N' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + N'. ' + + CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + N'% 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 N'. It is probably still running, perhaps updating statistics.' + ELSE N' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time) + + N' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts.' + END + ELSE N' 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')) @@ -17072,6 +17248,96 @@ 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 + N' on ' + iro.db_schema_table_index + + N' since ' + CONVERT(NVARCHAR(50), iro.last_pause_time, 120) + N'. ' + + CONVERT(NVARCHAR(6), CONVERT(MONEY, iro.percent_complete)) + N'% 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 N'. It is probably still running, perhaps updating statistics.' + ELSE N' after ' + CONVERT(NVARCHAR(30), iro.total_execution_time) + + N' minute(s). This blocks DDL, fails transactions needing table-level X locks, and can pile up ghosts. ' + END + + CASE WHEN @ResumableIndexesDisappearAfter > 0 + THEN N' Will be automatically removed by the database server at ' + CONVERT(NVARCHAR(50), (DATEADD(mi, @ResumableIndexesDisappearAfter, iro.last_pause_time)), 121) + N'. ' + ELSE N' Will not be automatically removed by the database server. ' + END AS details, + N'Old index: ' + ISNULL(i.index_definition, N'not found. Either the index is new or you need @IncludeInactiveIndexes = 1') AS index_definition, + i.secret_columns, + i.index_usage_summary, + N'New index: ' + iro.reserved_MB_pretty_print + N'; Old index: ' + ISNULL(sz.index_size_summary,'not found.') AS index_size_summary, + N'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 ---------------------------------------- @@ -17229,7 +17495,38 @@ BEGIN WHERE i.filter_columns_not_in_index IS NOT NULL ORDER BY i.db_schema_object_indexid OPTION ( RECOMPILE ); - + + RAISERROR(N'check_id 124: History Table With NonClustered Index', 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 ) + SELECT 124 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'History Table With NonClustered Index' AS finding, + i.[database_name] AS [Database Name], + N'https://sqlserverfast.com/blog/hugo/2023/09/an-update-on-merge/' AS URL, + N'The history table ' + + QUOTENAME(hist.history_schema_name) + + '.' + + QUOTENAME(hist.history_table_name) + + ' has a non-clustered index. This can cause MERGEs on the main table to fail! See item 8 on the URL.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + JOIN #TemporalTables hist + ON i.[object_id] = hist.history_table_object_id + AND i.[database_id] = hist.[database_id] + WHERE hist.history_table_object_id IS NOT NULL + AND i.index_type = 2 /* NC only */ + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); + ---------------------------------------- --Self Loathing Indexes : Check_id 40-49 ---------------------------------------- @@ -20018,7 +20315,7 @@ BEGIN CATCH GO IF OBJECT_ID('dbo.sp_BlitzLock') IS NULL BEGIN - EXEC ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); + EXECUTE ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); END; GO @@ -20037,6 +20334,11 @@ ALTER PROCEDURE @TargetSessionType sysname = NULL, @VictimsOnly bit = 0, @DeadlockType nvarchar(20) = NULL, + @TargetDatabaseName sysname = NULL, + @TargetSchemaName sysname = NULL, + @TargetTableName sysname = NULL, + @TargetColumnName sysname = NULL, + @TargetTimestampColumnName sysname = NULL, @Debug bit = 0, @Help bit = 0, @Version varchar(30) = NULL OUTPUT, @@ -20055,7 +20357,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF @VersionCheckMode = 1 BEGIN @@ -20072,6 +20374,7 @@ BEGIN Variables you can use: + /*Filtering parameters*/ @DatabaseName: If you want to filter to a specific database @StartDate: The date you want to start searching on, defaults to last 7 days @@ -20090,16 +20393,32 @@ BEGIN @LoginName: If you want to filter to a specific login + @DeadlockType: Search for regular or parallel deadlocks specifically + + /*Extended Event session details*/ @EventSessionName: If you want to point this at an XE session rather than the system health session. - @TargetSessionType: Can be ''ring_buffer'' or ''event_file''. Leave NULL to auto-detect. + @TargetSessionType: Can be ''ring_buffer'', ''event_file'', or ''table''. Leave NULL to auto-detect. + /*Output to a table*/ @OutputDatabaseName: If you want to output information to a specific database @OutputSchemaName: Specify a schema name to output information to a specific Schema @OutputTableName: Specify table name to to output information to a specific table + /*Point at a table containing deadlock XML*/ + @TargetDatabaseName: The database that contains the table with deadlock report XML + + @TargetSchemaName: The schema of the table containing deadlock report XML + + @TargetTableName: The name of the table containing deadlock report XML + + @TargetColumnName: The name of the XML column that contains the deadlock report + + @TargetTimestampColumnName: The name of the datetime column for filtering by date range (optional) + + To learn more, visit http://FirstResponderKit.org where you can download new versions for free, watch training videos on how it works, get more info on the findings, contribute your own code, and more. @@ -20217,7 +20536,11 @@ BEGIN @StartDateOriginal datetime = @StartDate, @EndDateOriginal datetime = @EndDate, @StartDateUTC datetime, - @EndDateUTC datetime;; + @EndDateUTC datetime, + @extract_sql nvarchar(MAX), + @validation_sql nvarchar(MAX), + @xe bit, + @xd bit; /*Temporary objects used in the procedure*/ DECLARE @@ -20342,7 +20665,185 @@ BEGIN @TargetSessionType = N'ring_buffer'; END; + IF ISNULL(@TargetDatabaseName, DB_NAME()) IS NOT NULL + AND ISNULL(@TargetSchemaName, N'dbo') IS NOT NULL + AND @TargetTableName IS NOT NULL + AND @TargetColumnName IS NOT NULL + BEGIN + SET @TargetSessionType = N'table'; + END; + + /* Add this after the existing parameter validations */ + IF @TargetSessionType = N'table' + BEGIN + IF @TargetDatabaseName IS NULL + BEGIN + SET @TargetDatabaseName = DB_NAME(); + END; + + IF @TargetSchemaName IS NULL + BEGIN + SET @TargetSchemaName = N'dbo'; + END; + + IF @TargetTableName IS NULL + OR @TargetColumnName IS NULL + BEGIN + RAISERROR(N' + When using a table as a source, you must specify @TargetTableName, and @TargetColumnName. + When @TargetDatabaseName or @TargetSchemaName is NULL, they default to DB_NAME() AND dbo', + 11, 1) WITH NOWAIT; + RETURN; + END; + + /* Check if target database exists */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name = @TargetDatabaseName + ) + BEGIN + RAISERROR(N'The specified @TargetDatabaseName %s does not exist.', 11, 1, @TargetDatabaseName) WITH NOWAIT; + RETURN; + END; + + /* Use dynamic SQL to validate schema, table, and column existence */ + SET @validation_sql = N' + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + WHERE s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetSchemaName %s does not exist in database %s.'', 11, 1, @schema, @database) WITH NOWAIT; + RETURN; + END; + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE t.name = @table + AND s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetTableName %s does not exist in schema %s in database %s.'', 11, 1, @table, @schema, @database) WITH NOWAIT; + RETURN; + END; + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @column + AND t.name = @table + AND s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetColumnName %s does not exist in table %s.%s in database %s.'', 11, 1, @column, @schema, @table, @database) WITH NOWAIT; + RETURN; + END; + + /* Validate column is XML type */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.types AS ty + ON c.user_type_id = ty.user_type_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @column + AND t.name = @table + AND s.name = @schema + AND ty.name = N''xml'' + ) + BEGIN + RAISERROR(N''The specified @TargetColumnName %s must be of XML data type.'', 11, 1, @column) WITH NOWAIT; + RETURN; + END;'; + + /* Validate timestamp_column if specified */ + IF @TargetTimestampColumnName IS NOT NULL + BEGIN + SET @validation_sql = @validation_sql + N' + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @timestamp_column + AND t.name = @table + AND s.name = @schema + ) + BEGIN + RAISERROR(N''The specified @TargetTimestampColumnName %s does not exist in table %s.%s in database %s.'', 11, 1, @timestamp_column, @schema, @table, @database) WITH NOWAIT; + RETURN; + END; + + /* Validate timestamp column is datetime type */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@TargetDatabaseName) + N'.sys.columns AS c + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.types AS ty + ON c.user_type_id = ty.user_type_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.tables AS t + ON c.object_id = t.object_id + JOIN ' + QUOTENAME(@TargetDatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + WHERE c.name = @timestamp_column + AND t.name = @table + AND s.name = @schema + AND ty.name LIKE ''%date%'' + ) + BEGIN + RAISERROR(N''The specified @TargetTimestampColumnName %s must be of datetime data type.'', 11, 1, @timestamp_column) WITH NOWAIT; + RETURN; + END;'; + END; + + IF @Debug = 1 BEGIN PRINT @validation_sql; END; + + EXECUTE sys.sp_executesql + @validation_sql, + N' + @database sysname, + @schema sysname, + @table sysname, + @column sysname, + @timestamp_column sysname + ', + @TargetDatabaseName, + @TargetSchemaName, + @TargetTableName, + @TargetColumnName, + @TargetTimestampColumnName; + END; + + IF @Azure = 0 + AND LOWER(@TargetSessionType) <> N'table' BEGIN IF NOT EXISTS ( @@ -20359,8 +20860,9 @@ BEGIN RETURN; END; END; - + IF @Azure = 1 + AND LOWER(@TargetSessionType) <> N'table' BEGIN IF NOT EXISTS ( @@ -20419,7 +20921,7 @@ BEGIN N'@r sysname OUTPUT'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute, @StringToExecuteParams, @r OUTPUT; @@ -20459,7 +20961,7 @@ BEGIN N' ADD spid smallint NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new wait_resource column, add it. See Github #3101. */ @@ -20475,7 +20977,7 @@ BEGIN N' ADD wait_resource nvarchar(MAX) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new client option column, add it. See Github #3101. */ @@ -20491,7 +20993,7 @@ BEGIN N' ADD client_option_1 varchar(500) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new client option column, add it. See Github #3101. */ @@ -20507,7 +21009,7 @@ BEGIN N' ADD client_option_2 varchar(500) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new lock mode column, add it. See Github #3101. */ @@ -20523,7 +21025,7 @@ BEGIN N' ADD lock_mode nvarchar(256) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /* If the table doesn't have the new status column, add it. See Github #3101. */ @@ -20539,7 +21041,7 @@ BEGIN N' ADD status nvarchar(256) NULL;'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; ELSE /* end if @r is not null. if it is null there is no table, create it from above execution */ @@ -20597,7 +21099,7 @@ BEGIN )'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /*table created.*/ @@ -20612,7 +21114,7 @@ BEGIN N'@r sysname OUTPUT'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute, @StringToExecuteParams, @r OUTPUT; @@ -20640,7 +21142,7 @@ BEGIN );'; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; END; @@ -20669,7 +21171,7 @@ BEGIN @OutputTableFindings; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; /*create synonym for deadlock table.*/ @@ -20696,7 +21198,7 @@ BEGIN @OutputTableName; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; END; @@ -20768,6 +21270,7 @@ BEGIN ( @Azure = 1 AND @TargetSessionType IS NULL + AND LOWER(@TargetSessionType) <> N'table' ) BEGIN RAISERROR('@TargetSessionType is NULL, assigning for Azure instance', 0, 1) WITH NOWAIT; @@ -21066,6 +21569,148 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; END; + /* If table target */ + IF @TargetSessionType = 'table' + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data from table source %s', 0, 1, @d) WITH NOWAIT; + + /* + First, we need to heck the XML structure. + Depending on the data source, the XML could + contain either the /event or /deadlock nodes. + When the /event nodes are not present, there + is no @name attribute to evaluate. + */ + + SELECT + @extract_sql = N' + SELECT TOP (1) + @xe = xe.e.exist(''.''), + @xd = xd.e.exist(''.'') + FROM [master].[dbo].[bpr] AS x + OUTER APPLY x.[bpr].nodes(''/event'') AS xe(e) + OUTER APPLY x.[bpr].nodes(''/deadlock'') AS xd(e) + OPTION(RECOMPILE); + '; + + IF @Debug = 1 BEGIN PRINT @extract_sql; END; + + EXECUTE sys.sp_executesql + @extract_sql, + N' + @xe bit OUTPUT, + @xd bit OUTPUT + ', + @xe OUTPUT, + @xd OUTPUT; + + + /* Build dynamic SQL to extract the XML */ + IF @xe = 1 + AND @xd IS NULL + BEGIN + SET @extract_sql = N' + SELECT + deadlock_xml = ' + + QUOTENAME(@TargetColumnName) + + N' + FROM ' + + QUOTENAME(@TargetDatabaseName) + + N'.' + + QUOTENAME(@TargetSchemaName) + + N'.' + + QUOTENAME(@TargetTableName) + + N' AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/event'') AS e(x) + WHERE + ( + e.x.exist(''@name[ .= "xml_deadlock_report"]'') = 1 + OR e.x.exist(''@name[ .= "database_xml_deadlock_report"]'') = 1 + OR e.x.exist(''@name[ .= "xml_deadlock_report_filtered"]'') = 1 + )'; + END; + + IF @xe IS NULL + AND @xd = 1 + BEGIN + SET @extract_sql = N' + SELECT + deadlock_xml = ' + + QUOTENAME(@TargetColumnName) + + N' + FROM ' + + QUOTENAME(@TargetDatabaseName) + + N'.' + + QUOTENAME(@TargetSchemaName) + + N'.' + + QUOTENAME(@TargetTableName) + + N' AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/deadlock'') AS e(x) + WHERE 1 = 1'; + END; + + /* Add timestamp filtering if specified */ + IF @TargetTimestampColumnName IS NOT NULL + BEGIN + SET @extract_sql = @extract_sql + N' + AND x.' + QUOTENAME(@TargetTimestampColumnName) + N' >= @StartDate + AND x.' + QUOTENAME(@TargetTimestampColumnName) + N' < @EndDate'; + END; + + /* If no timestamp column but date filtering is needed, handle XML-based filtering when possible */ + IF @TargetTimestampColumnName IS NULL + AND @xe = 1 + AND @xd IS NULL + BEGIN + SET @extract_sql = @extract_sql + N' + AND e.x.exist(''@timestamp[. >= sql:variable("@StartDate") and . < sql:variable("@EndDate")]'') = 1'; + END; + + /*Woof*/ + IF @TargetTimestampColumnName IS NULL + AND @xe IS NULL + AND @xd = 1 + BEGIN + SET @extract_sql = @extract_sql + N' + AND e.x.exist(''(/deadlock/process-list/process/@lasttranstarted)[. >= sql:variable("@StartDate") and . < sql:variable("@EndDate")]'') = 1'; + END; + + SET @extract_sql += N' + OPTION(RECOMPILE); + '; + + IF @Debug = 1 BEGIN PRINT @extract_sql; END; + + /* Execute the dynamic SQL */ + INSERT + #deadlock_data + WITH + (TABLOCKX) + ( + deadlock_xml + ) + EXECUTE sys.sp_executesql + @extract_sql, + N' + @StartDate datetime, + @EndDate datetime + ', + @StartDate, + @EndDate; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + /*Parse process and input buffer xml*/ SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Initial Parse process and input buffer xml %s', 0, 1, @d) WITH NOWAIT; @@ -21081,6 +21726,21 @@ BEGIN FROM #deadlock_data AS d1 LEFT JOIN #t AS t ON 1 = 1 + WHERE @xe = 1 + + UNION ALL + + SELECT + d1.deadlock_xml, + event_date = d1.deadlock_xml.value('(/deadlock/process-list/process/@lasttranstarted)[1]', 'datetime2'), + victim_id = d1.deadlock_xml.value('(/deadlock/victim-list/victimProcess/@id)[1]', 'nvarchar(256)'), + is_parallel = d1.deadlock_xml.exist('/deadlock/resource-list/exchangeEvent'), + is_parallel_batch = d1.deadlock_xml.exist('/deadlock/resource-list/SyncPoint'), + deadlock_graph = d1.deadlock_xml.query('.') + FROM #deadlock_data AS d1 + LEFT JOIN #t AS t + ON 1 = 1 + WHERE @xd = 1 OPTION(RECOMPILE); SET @d = CONVERT(varchar(40), GETDATE(), 109); @@ -21753,7 +22413,7 @@ BEGIN '; IF @Debug = 1 BEGIN PRINT @StringToExecute; END; - EXEC sys.sp_executesql + EXECUTE sys.sp_executesql @StringToExecute; END; @@ -21902,7 +22562,7 @@ BEGIN COUNT_BIG(DISTINCT dp.event_date) ) + N' deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -21957,7 +22617,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s) between read queries and modification queries.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -22019,7 +22679,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s).', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -22062,7 +22722,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s).', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -22111,7 +22771,7 @@ BEGIN COUNT_BIG(DISTINCT dow.event_date) ) + N' deadlock(s).', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) FROM #deadlock_owner_waiter AS dow @@ -22160,7 +22820,7 @@ BEGIN COUNT_BIG(DISTINCT dp.event_date) ) + N' instances of Serializable deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -22203,7 +22863,7 @@ BEGIN COUNT_BIG(DISTINCT dp.event_date) ) + N' instances of Repeatable Read deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -22265,7 +22925,7 @@ BEGIN N'UNKNOWN' ) + N'.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) FROM #deadlock_process AS dp @@ -22377,7 +23037,7 @@ BEGIN 1, N'' ) + N' locks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY CONVERT(bigint, lt.lock_count) DESC) FROM lock_types AS lt @@ -22447,7 +23107,7 @@ BEGIN dow.database_name, object_name = ds.proc_name, finding_group = N'More Info - Query', - finding = N'EXEC sp_BlitzCache ' + + finding = N'EXECUTE sp_BlitzCache ' + CASE WHEN ds.proc_name = N'adhoc' THEN N'@OnlySqlHandles = ' + ds.sql_handle_csv @@ -22503,7 +23163,7 @@ BEGIN object_name = ds.proc_name, finding_group = N'More Info - Query', finding = - N'EXEC sp_BlitzQueryStore ' + + N'EXECUTE sp_BlitzQueryStore ' + N'@DatabaseName = ' + QUOTENAME(ds.database_name, N'''') + N', ' + @@ -22556,7 +23216,7 @@ BEGIN COUNT_BIG(DISTINCT ds.id) ) + N' deadlocks.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT ds.id) DESC) FROM #deadlock_stack AS ds @@ -22616,7 +23276,7 @@ BEGIN bi.object_name, finding_group = N'More Info - Table', finding = - N'EXEC sp_BlitzIndex ' + + N'EXECUTE sp_BlitzIndex ' + N'@DatabaseName = ' + QUOTENAME(bi.database_name, N'''') + N', @SchemaName = ' + @@ -22657,19 +23317,19 @@ BEGIN ) ), wait_time_hms = - /*the more wait time you rack up the less accurate this gets, + /*the more wait time you rack up the less accurate this gets, it's either that or erroring out*/ - CASE - WHEN + CASE + WHEN SUM ( CONVERT ( - bigint, + bigint, dp.wait_time ) )/1000 > 2147483647 - THEN + THEN CONVERT ( nvarchar(30), @@ -22682,7 +23342,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, dp.wait_time ) ) @@ -22693,16 +23353,16 @@ BEGIN ), 14 ) - WHEN + WHEN SUM ( CONVERT ( - bigint, + bigint, dp.wait_time ) ) BETWEEN 2147483648 AND 2147483647000 - THEN + THEN CONVERT ( nvarchar(30), @@ -22715,7 +23375,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, dp.wait_time ) ) @@ -22797,7 +23457,7 @@ BEGIN 14 ) + N' [dd hh:mm:ss:ms] of deadlock wait time.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY cs.total_waits DESC) FROM chopsuey AS cs @@ -22871,19 +23531,19 @@ BEGIN ) ) + N' ' + - /*the more wait time you rack up the less accurate this gets, + /*the more wait time you rack up the less accurate this gets, it's either that or erroring out*/ - CASE - WHEN + CASE + WHEN SUM ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) )/1000 > 2147483647 - THEN + THEN CONVERT ( nvarchar(30), @@ -22896,7 +23556,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) ) @@ -22907,16 +23567,16 @@ BEGIN ), 14 ) - WHEN + WHEN SUM ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) ) BETWEEN 2147483648 AND 2147483647000 - THEN + THEN CONVERT ( nvarchar(30), @@ -22929,7 +23589,7 @@ BEGIN ( CONVERT ( - bigint, + bigint, wt.total_wait_time_ms ) ) @@ -22962,7 +23622,7 @@ BEGIN 14 ) END + N' [dd hh:mm:ss:ms] of deadlock wait time.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY SUM(CONVERT(bigint, wt.total_wait_time_ms)) DESC) FROM wait_time AS wt @@ -23000,7 +23660,7 @@ BEGIN N'There have been ' + RTRIM(COUNT_BIG(DISTINCT aj.event_date)) + N' deadlocks from this Agent Job and Step.', - sort_order = + sort_order = ROW_NUMBER() OVER (ORDER BY COUNT_BIG(DISTINCT aj.event_date) DESC) FROM #agent_job AS aj @@ -23693,9 +24353,9 @@ BEGIN SET STATISTICS XML ON; END; - SET @StringToExecute = N' + SET @StringToExecute = N' - INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadLockTbl + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadLockTbl ( ServerName, deadlock_type, @@ -23738,9 +24398,9 @@ BEGIN waiter_waiting_to_close, deadlock_graph ) - EXEC sys.sp_executesql - @deadlock_result;' - EXEC sys.sp_executesql @StringToExecute, N'@deadlock_result NVARCHAR(MAX)', @deadlock_result; + EXECUTE sys.sp_executesql + @deadlock_result;'; + EXECUTE sys.sp_executesql @StringToExecute, N'@deadlock_result NVARCHAR(MAX)', @deadlock_result; IF @Debug = 1 BEGIN @@ -23756,7 +24416,7 @@ BEGIN SET @StringToExecute = N' - INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadlockFindings + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadlockFindings ( ServerName, check_id, @@ -23774,8 +24434,8 @@ BEGIN df.finding FROM #deadlock_findings AS df ORDER BY df.check_id - OPTION(RECOMPILE);' - EXEC sys.sp_executesql @StringToExecute; + OPTION(RECOMPILE);'; + EXECUTE sys.sp_executesql @StringToExecute; RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; @@ -23785,23 +24445,23 @@ BEGIN BEGIN SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Results to client %s', 0, 1, @d) WITH NOWAIT; - + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; - - EXEC sys.sp_executesql + + EXECUTE sys.sp_executesql @deadlock_result; - + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; PRINT @deadlock_result; END; - + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Getting available execution plans for deadlocks %s', 0, 1, @d) WITH NOWAIT; - + SELECT DISTINCT available_plans = 'available_plans', @@ -23868,15 +24528,15 @@ BEGIN min_used_grant_mb = deqs.min_used_grant_kb * 8. / 1024., max_used_grant_mb = - deqs.max_used_grant_kb * 8. / 1024., + deqs.max_used_grant_kb * 8. / 1024., deqs.min_reserved_threads, deqs.max_reserved_threads, deqs.min_used_threads, deqs.max_used_threads, deqs.total_rows, - max_worker_time_ms = + max_worker_time_ms = deqs.max_worker_time / 1000., - max_elapsed_time_ms = + max_elapsed_time_ms = deqs.max_elapsed_time / 1000. INTO #dm_exec_query_stats FROM sys.dm_exec_query_stats AS deqs @@ -23888,7 +24548,7 @@ BEGIN WHERE ap.sql_handle = deqs.sql_handle ) AND deqs.query_hash IS NOT NULL; - + CREATE CLUSTERED INDEX deqs ON #dm_exec_query_stats @@ -23896,7 +24556,7 @@ BEGIN sql_handle, plan_handle ); - + SELECT ap.available_plans, ap.database_name, @@ -23930,7 +24590,7 @@ BEGIN ap.statement_end_offset FROM ( - + SELECT ap.*, c.statement_start_offset, @@ -23982,10 +24642,10 @@ BEGIN OPTION(RECOMPILE, LOOP JOIN, HASH JOIN); RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - + SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Returning findings %s', 0, 1, @d) WITH NOWAIT; - + SELECT df.check_id, df.database_name, @@ -23997,7 +24657,7 @@ BEGIN df.check_id, df.sort_order OPTION(RECOMPILE); - + SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; END; /*done with output to client app.*/ @@ -24081,7 +24741,7 @@ BEGIN END; IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL - BEGIN + BEGIN SELECT table_name = N'#dm_exec_query_stats', * @@ -24116,6 +24776,16 @@ BEGIN @VictimsOnly, DeadlockType = @DeadlockType, + TargetDatabaseName = + @TargetDatabaseName, + TargetSchemaName = + @TargetSchemaName, + TargetTableName = + @TargetTableName, + TargetColumnName = + @TargetColumnName, + TargetTimestampColumnName = + @TargetTimestampColumnName, Debug = @Debug, Help = @@ -24220,7 +24890,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_Blitz.sql b/sp_Blitz.sql index e568f5886..43de4bbfe 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -38,7 +38,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzAnalysis.sql b/sp_BlitzAnalysis.sql index 74d290709..96216408c 100644 --- a/sp_BlitzAnalysis.sql +++ b/sp_BlitzAnalysis.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzBackups.sql b/sp_BlitzBackups.sql index f3285ece8..5f33342e4 100755 --- a/sp_BlitzBackups.sql +++ b/sp_BlitzBackups.sql @@ -24,7 +24,7 @@ AS SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index db4d24e4f..9c6453f70 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -282,7 +282,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index e32fa33f6..bdbcda1d4 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -47,7 +47,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index f7624172d..365cbe6c6 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -49,7 +49,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index 79029b7ca..3b4f65248 100644 --- a/sp_BlitzLock.sql +++ b/sp_BlitzLock.sql @@ -42,7 +42,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF @VersionCheckMode = 1 BEGIN diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index 2f3ceadd9..6bb2df72f 100644 --- a/sp_BlitzWho.sql +++ b/sp_BlitzWho.sql @@ -33,7 +33,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_DatabaseRestore.sql b/sp_DatabaseRestore.sql index f478a9d8c..b685a0d08 100755 --- a/sp_DatabaseRestore.sql +++ b/sp_DatabaseRestore.sql @@ -58,7 +58,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.23', @VersionDate = '20241228'; +SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index 0d2a85057..16df3b7c5 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -36,7 +36,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.23', @VersionDate = '20241228'; + SELECT @Version = '8.24', @VersionDate = '20250407'; IF(@VersionCheckMode = 1) BEGIN