From f30c75a1ce6c13249e3a43ff9c92d016559f8a1e Mon Sep 17 00:00:00 2001 From: steve boeckmann Date: Thu, 20 Apr 2023 16:00:04 -0500 Subject: [PATCH 01/76] Remove nonstriped full backup files after @StopAt --- sp_DatabaseRestore.sql | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sp_DatabaseRestore.sql b/sp_DatabaseRestore.sql index ce6f076a1..7adfb74e0 100755 --- a/sp_DatabaseRestore.sql +++ b/sp_DatabaseRestore.sql @@ -672,6 +672,14 @@ BEGIN WHERE BackupFile LIKE N'%[_][0-9][0-9].bak' AND BackupFile LIKE N'%' + @Database + N'%' AND (REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 18 ), '_', '' ) > @StopAt); + + DELETE + FROM @FileList + WHERE BackupFile LIKE N'%.bak' /*delete backups that aren't striped and new "enough"*/ + AND BackupFile NOT LIKE N'%[_][0-9].bak' /*skip remaining striped backups since those were deleted above*/ + AND BackupFile NOT LIKE N'%[_][0-9][0-9].bak' + AND BackupFile LIKE N'%' + @Database + N'%' + AND (REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) > @StopAt); END; -- Find latest full backup From 7c8c31c3100042b42da199a405ff6e6d8ee3d0ea Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 18 Apr 2024 14:38:47 -0700 Subject: [PATCH 02/76] Update README.md Clarify that sp_BlitzLock won't always work in Azure SQL DB. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 343d5b96b..58e55f7d0 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,7 @@ Parameters you can use: Known issues: +* In Azure SQL DB, Microsoft doesn't seem to be running system health sessions for you. If you get the error "A session with the name system_health does not exist or is not currently active", you'll need to start your own Extended Events session to capture deadlocks. We don't have documentation on that, unfortunately, but we'd love to get help from someone else writing that! * If your database has periods in the name, the deadlock report itself doesn't report the database name correctly. [More info in closed issue 2452.](https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2452) From 273ed2938b48e9ee4a9beccbdefcd57abcf7ebae Mon Sep 17 00:00:00 2001 From: RG Date: Sun, 6 Apr 2025 17:47:07 +0100 Subject: [PATCH 03/76] Added warning for non-clustered indexes on history tables. These cause MERGEs on the main table to fail. Also put #TemportalTables in the debug output. It was missing. --- .../sp_BlitzIndex_Checks_by_Priority.md | 129 +++++++++--------- sp_BlitzIndex.sql | 44 +++++- 2 files changed, 104 insertions(+), 69 deletions(-) diff --git a/Documentation/sp_BlitzIndex_Checks_by_Priority.md b/Documentation/sp_BlitzIndex_Checks_by_Priority.md index d6af3eec2..619438d0f 100644 --- a/Documentation/sp_BlitzIndex_Checks_by_Priority.md +++ b/Documentation/sp_BlitzIndex_Checks_by_Priority.md @@ -6,68 +6,69 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 123 -If you want to add a new check, start at 124. +CURRENT HIGH CHECKID: 124 +If you want to add a new check, start at 125. -| Priority | FindingsGroup | Finding | URL | CheckID | -| -------- | ----------------------- | --------------------------------------------------------------- | ----------------------------------------------- | ------- | -| 10 | Over-Indexing | Many NC Indexes on a Single Table | https://www.brentozar.com/go/IndexHoarder | 20 | -| 10 | Over-Indexing | Unused NC Index with High Writes | https://www.brentozar.com/go/IndexHoarder | 22 | -| 10 | Resumable Indexing | Resumable Index Operation Paused | https://www.BrentOzar.com/go/resumable | 122 | -| 10 | Resumable Indexing | Resumable Index Operation Running | https://www.BrentOzar.com/go/resumable | 123 | -| 20 | Redundant Indexes | Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 1 | -| 30 | Redundant Indexes | Approximate Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 2 | -| 40 | Index Suggestion | High Value Missing Index | https://www.brentozar.com/go/indexaphobia | 50 | -| 70 | Locking-Prone Indexes | Total Lock Time with Long Average Waits | https://www.brentozar.com/go/aggressiveindexes | 11 | -| 70 | Locking-Prone Indexes | Total Lock Time with Short Average Waits | https://www.brentozar.com/go/aggressiveindexes | 12 | -| 80 | Abnormal Design Pattern | Columnstore Indexes with Trace Flag 834 | https://support.microsoft.com/en-us/kb/3210239 | 72 | -| 80 | Abnormal Design Pattern | Identity Column Near End of Range | https://www.brentozar.com/go/AbnormalPsychology | 68 | -| 80 | Abnormal Design Pattern | Filter Columns Not In Index Definition | https://www.brentozar.com/go/IndexFeatures | 34 | -| 90 | Statistics Warnings | Low Sampling Rates | https://www.brentozar.com/go/stats | 91 | -| 90 | Statistics Warnings | Statistics Not Updated Recently | https://www.brentozar.com/go/stats | 90 | -| 90 | Statistics Warnings | Statistics with NO RECOMPUTE | https://www.brentozar.com/go/stats | 92 | -| 100 | Over-Indexing | NC index with High Writes:Reads | https://www.brentozar.com/go/IndexHoarder | 48 | -| 100 | Indexes Worth Reviewing | Heap with a Nonclustered Primary Key | https://www.brentozar.com/go/SelfLoathing | 47 | -| 100 | Indexes Worth Reviewing | Heap with Forwarded Fetches | https://www.brentozar.com/go/SelfLoathing | 43 | -| 100 | Indexes Worth Reviewing | Large Active Heap | https://www.brentozar.com/go/SelfLoathing | 44 | -| 100 | Indexes Worth Reviewing | Low Fill Factor on Clustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | -| 100 | Indexes Worth Reviewing | Low Fill Factor on Nonclustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | -| 100 | Indexes Worth Reviewing | Medium Active Heap | https://www.brentozar.com/go/SelfLoathing | 45 | -| 100 | Indexes Worth Reviewing | Small Active Heap | https://www.brentozar.com/go/SelfLoathing | 46 | -| 100 | Forced Serialization | Computed Column with Scalar UDF | https://www.brentozar.com/go/serialudf | 99 | -| 100 | Forced Serialization | Check Constraint with Scalar UDF | https://www.brentozar.com/go/computedscalar | 94 | -| 150 | Abnormal Design Pattern | Cascading Updates or Deletes | https://www.brentozar.com/go/AbnormalPsychology | 71 | -| 150 | Abnormal Design Pattern | Unindexed Foreign Keys | https://www.brentozar.com/go/AbnormalPsychology | 72 | -| 150 | Abnormal Design Pattern | Columnstore Index | https://www.brentozar.com/go/AbnormalPsychology | 61 | -| 150 | Abnormal Design Pattern | Column Collation Does Not Match Database Collation | https://www.brentozar.com/go/AbnormalPsychology | 69 | -| 150 | Abnormal Design Pattern | Compressed Index | https://www.brentozar.com/go/AbnormalPsychology | 63 | -| 150 | Abnormal Design Pattern | In-Memory OLTP | https://www.brentozar.com/go/AbnormalPsychology | 73 | -| 150 | Abnormal Design Pattern | Non-Aligned Index on a Partitioned Table | https://www.brentozar.com/go/AbnormalPsychology | 65 | -| 150 | Abnormal Design Pattern | Partitioned Index | https://www.brentozar.com/go/AbnormalPsychology | 64 | -| 150 | Abnormal Design Pattern | Spatial Index | https://www.brentozar.com/go/AbnormalPsychology | 62 | -| 150 | Abnormal Design Pattern | XML Index | https://www.brentozar.com/go/AbnormalPsychology | 60 | -| 150 | Over-Indexing | Approximate: Wide Indexes (7 or More Columns) | https://www.brentozar.com/go/IndexHoarder | 23 | -| 150 | Over-Indexing | More Than 5 Percent NC Indexes Are Unused | https://www.brentozar.com/go/IndexHoarder | 21 | -| 150 | Over-Indexing | Non-Unique Clustered Index | https://www.brentozar.com/go/IndexHoarder | 28 | -| 150 | Over-Indexing | Unused NC Index with Low Writes | https://www.brentozar.com/go/IndexHoarder | 29 | -| 150 | Over-Indexing | Wide Clustered Index (>3 columns or >16 bytes) | https://www.brentozar.com/go/IndexHoarder | 24 | -| 150 | Indexes Worth Reviewing | Disabled Index | https://www.brentozar.com/go/SelfLoathing | 42 | -| 150 | Indexes Worth Reviewing | Hypothetical Index | https://www.brentozar.com/go/SelfLoathing | 41 | -| 200 | Abnormal Design Pattern | Identity Column Using a Negative Seed or Increment Other Than 1 | https://www.brentozar.com/go/AbnormalPsychology | 74 | -| 200 | Abnormal Design Pattern | Recently Created Tables/Indexes (1 week) | https://www.brentozar.com/go/AbnormalPsychology | 66 | -| 200 | Abnormal Design Pattern | Recently Modified Tables/Indexes (2 days) | https://www.brentozar.com/go/AbnormalPsychology | 67 | -| 200 | Abnormal Design Pattern | Replicated Columns | https://www.brentozar.com/go/AbnormalPsychology | 70 | -| 200 | Abnormal Design Pattern | Temporal Tables | https://www.brentozar.com/go/AbnormalPsychology | 110 | -| 200 | Repeated Calculations | Computed Columns Not Persisted | https://www.brentozar.com/go/serialudf | 100 | -| 200 | Statistics Warnings | Statistics With Filters | https://www.brentozar.com/go/stats | 93 | -| 200 | Over-Indexing | High Ratio of Nulls | https://www.brentozar.com/go/IndexHoarder | 25 | -| 200 | Over-Indexing | High Ratio of Strings | https://www.brentozar.com/go/IndexHoarder | 27 | -| 200 | Over-Indexing | Wide Tables: 35+ cols or > 2000 non-LOB bytes | https://www.brentozar.com/go/IndexHoarder | 26 | -| 200 | Indexes Worth Reviewing | Heaps with Deletes | https://www.brentozar.com/go/SelfLoathing | 49 | -| 200 | High Workloads | Scan-a-lots (index-usage-stats) | https://www.brentozar.com/go/Workaholics | 80 | -| 200 | High Workloads | Top Recent Accesses (index-op-stats) | https://www.brentozar.com/go/Workaholics | 81 | -| 250 | Omitted Index Features | Few Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 31 | -| 250 | Omitted Index Features | No Filtered Indexes or Indexed Views | https://www.brentozar.com/go/IndexFeatures | 32 | -| 250 | Omitted Index Features | No Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 30 | -| 250 | Omitted Index Features | Potential Filtered Index (Based on Column Name) | https://www.brentozar.com/go/IndexFeatures | 33 | -| 250 | Specialized Indexes | Optimized For Sequential Keys | | 121 | +| Priority | FindingsGroup | Finding | URL | CheckID | +| -------- | ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- | ------- | +| 10 | Over-Indexing | Many NC Indexes on a Single Table | https://www.brentozar.com/go/IndexHoarder | 20 | +| 10 | Over-Indexing | Unused NC Index with High Writes | https://www.brentozar.com/go/IndexHoarder | 22 | +| 10 | Resumable Indexing | Resumable Index Operation Paused | https://www.BrentOzar.com/go/resumable | 122 | +| 10 | Resumable Indexing | Resumable Index Operation Running | https://www.BrentOzar.com/go/resumable | 123 | +| 20 | Redundant Indexes | Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 1 | +| 30 | Redundant Indexes | Approximate Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 2 | +| 40 | Index Suggestion | High Value Missing Index | https://www.brentozar.com/go/indexaphobia | 50 | +| 70 | Locking-Prone Indexes | Total Lock Time with Long Average Waits | https://www.brentozar.com/go/aggressiveindexes | 11 | +| 70 | Locking-Prone Indexes | Total Lock Time with Short Average Waits | https://www.brentozar.com/go/aggressiveindexes | 12 | +| 80 | Abnormal Design Pattern | Columnstore Indexes with Trace Flag 834 | https://support.microsoft.com/en-us/kb/3210239 | 72 | +| 80 | Abnormal Design Pattern | Identity Column Near End of Range | https://www.brentozar.com/go/AbnormalPsychology | 68 | +| 80 | Abnormal Design Pattern | Filter Columns Not In Index Definition | https://www.brentozar.com/go/IndexFeatures | 34 | +| 80 | Abnormal Design Pattern | History Table With NonClustered Index | https://sqlserverfast.com/blog/hugo/2023/09/an-update-on-merge/ | 124 | +| 90 | Statistics Warnings | Low Sampling Rates | https://www.brentozar.com/go/stats | 91 | +| 90 | Statistics Warnings | Statistics Not Updated Recently | https://www.brentozar.com/go/stats | 90 | +| 90 | Statistics Warnings | Statistics with NO RECOMPUTE | https://www.brentozar.com/go/stats | 92 | +| 100 | Over-Indexing | NC index with High Writes:Reads | https://www.brentozar.com/go/IndexHoarder | 48 | +| 100 | Indexes Worth Reviewing | Heap with a Nonclustered Primary Key | https://www.brentozar.com/go/SelfLoathing | 47 | +| 100 | Indexes Worth Reviewing | Heap with Forwarded Fetches | https://www.brentozar.com/go/SelfLoathing | 43 | +| 100 | Indexes Worth Reviewing | Large Active Heap | https://www.brentozar.com/go/SelfLoathing | 44 | +| 100 | Indexes Worth Reviewing | Low Fill Factor on Clustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | +| 100 | Indexes Worth Reviewing | Low Fill Factor on Nonclustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | +| 100 | Indexes Worth Reviewing | Medium Active Heap | https://www.brentozar.com/go/SelfLoathing | 45 | +| 100 | Indexes Worth Reviewing | Small Active Heap | https://www.brentozar.com/go/SelfLoathing | 46 | +| 100 | Forced Serialization | Computed Column with Scalar UDF | https://www.brentozar.com/go/serialudf | 99 | +| 100 | Forced Serialization | Check Constraint with Scalar UDF | https://www.brentozar.com/go/computedscalar | 94 | +| 150 | Abnormal Design Pattern | Cascading Updates or Deletes | https://www.brentozar.com/go/AbnormalPsychology | 71 | +| 150 | Abnormal Design Pattern | Unindexed Foreign Keys | https://www.brentozar.com/go/AbnormalPsychology | 72 | +| 150 | Abnormal Design Pattern | Columnstore Index | https://www.brentozar.com/go/AbnormalPsychology | 61 | +| 150 | Abnormal Design Pattern | Column Collation Does Not Match Database Collation | https://www.brentozar.com/go/AbnormalPsychology | 69 | +| 150 | Abnormal Design Pattern | Compressed Index | https://www.brentozar.com/go/AbnormalPsychology | 63 | +| 150 | Abnormal Design Pattern | In-Memory OLTP | https://www.brentozar.com/go/AbnormalPsychology | 73 | +| 150 | Abnormal Design Pattern | Non-Aligned Index on a Partitioned Table | https://www.brentozar.com/go/AbnormalPsychology | 65 | +| 150 | Abnormal Design Pattern | Partitioned Index | https://www.brentozar.com/go/AbnormalPsychology | 64 | +| 150 | Abnormal Design Pattern | Spatial Index | https://www.brentozar.com/go/AbnormalPsychology | 62 | +| 150 | Abnormal Design Pattern | XML Index | https://www.brentozar.com/go/AbnormalPsychology | 60 | +| 150 | Over-Indexing | Approximate: Wide Indexes (7 or More Columns) | https://www.brentozar.com/go/IndexHoarder | 23 | +| 150 | Over-Indexing | More Than 5 Percent NC Indexes Are Unused | https://www.brentozar.com/go/IndexHoarder | 21 | +| 150 | Over-Indexing | Non-Unique Clustered Index | https://www.brentozar.com/go/IndexHoarder | 28 | +| 150 | Over-Indexing | Unused NC Index with Low Writes | https://www.brentozar.com/go/IndexHoarder | 29 | +| 150 | Over-Indexing | Wide Clustered Index (>3 columns or >16 bytes) | https://www.brentozar.com/go/IndexHoarder | 24 | +| 150 | Indexes Worth Reviewing | Disabled Index | https://www.brentozar.com/go/SelfLoathing | 42 | +| 150 | Indexes Worth Reviewing | Hypothetical Index | https://www.brentozar.com/go/SelfLoathing | 41 | +| 200 | Abnormal Design Pattern | Identity Column Using a Negative Seed or Increment Other Than 1 | https://www.brentozar.com/go/AbnormalPsychology | 74 | +| 200 | Abnormal Design Pattern | Recently Created Tables/Indexes (1 week) | https://www.brentozar.com/go/AbnormalPsychology | 66 | +| 200 | Abnormal Design Pattern | Recently Modified Tables/Indexes (2 days) | https://www.brentozar.com/go/AbnormalPsychology | 67 | +| 200 | Abnormal Design Pattern | Replicated Columns | https://www.brentozar.com/go/AbnormalPsychology | 70 | +| 200 | Abnormal Design Pattern | Temporal Tables | https://www.brentozar.com/go/AbnormalPsychology | 110 | +| 200 | Repeated Calculations | Computed Columns Not Persisted | https://www.brentozar.com/go/serialudf | 100 | +| 200 | Statistics Warnings | Statistics With Filters | https://www.brentozar.com/go/stats | 93 | +| 200 | Over-Indexing | High Ratio of Nulls | https://www.brentozar.com/go/IndexHoarder | 25 | +| 200 | Over-Indexing | High Ratio of Strings | https://www.brentozar.com/go/IndexHoarder | 27 | +| 200 | Over-Indexing | Wide Tables: 35+ cols or > 2000 non-LOB bytes | https://www.brentozar.com/go/IndexHoarder | 26 | +| 200 | Indexes Worth Reviewing | Heaps with Deletes | https://www.brentozar.com/go/SelfLoathing | 49 | +| 200 | High Workloads | Scan-a-lots (index-usage-stats) | https://www.brentozar.com/go/Workaholics | 80 | +| 200 | High Workloads | Top Recent Accesses (index-op-stats) | https://www.brentozar.com/go/Workaholics | 81 | +| 250 | Omitted Index Features | Few Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 31 | +| 250 | Omitted Index Features | No Filtered Indexes or Indexed Views | https://www.brentozar.com/go/IndexFeatures | 32 | +| 250 | Omitted Index Features | No Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 30 | +| 250 | Omitted Index Features | Potential Filtered Index (Based on Column Name) | https://www.brentozar.com/go/IndexFeatures | 33 | +| 250 | Specialized Indexes | Optimized For Sequential Keys | | 121 | diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 63519ff4a..f7624172d 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -777,7 +777,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 @@ -2482,7 +2483,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 @@ -2508,7 +2510,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; @@ -3077,7 +3079,8 @@ 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 '#IndexResumableOperations' AS table_name, * FROM #IndexResumableOperations; @@ -3974,7 +3977,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 ---------------------------------------- From d217ac6b043c3b774ff075364ac6d9ee66687bf1 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Mon, 7 Apr 2025 04:10:22 -0700 Subject: [PATCH 04/76] 2025-04-07 sqlserverversions Adding latest SQL Server 2022 and 2019 builds. --- SqlServerVersions.sql | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SqlServerVersions.sql b/SqlServerVersions.sql index 01c4d63af..4866d6e22 100644 --- a/SqlServerVersions.sql +++ b/SqlServerVersions.sql @@ -42,6 +42,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'), @@ -64,6 +66,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'), From 19841c8c7076d0a68d1553bb1924b5a91be76f0b Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Mon, 7 Apr 2025 04:22:56 -0700 Subject: [PATCH 05/76] 2025-04-07 release prep Bumping version numbers and dates. --- Install-All-Scripts.sql | 964 +++++++++++++++++++++++++++++++++------- Install-Azure.sql | 864 +++++++++++++++++++++++++++++++---- sp_Blitz.sql | 2 +- sp_BlitzAnalysis.sql | 2 +- sp_BlitzBackups.sql | 2 +- sp_BlitzCache.sql | 2 +- sp_BlitzFirst.sql | 2 +- sp_BlitzIndex.sql | 2 +- sp_BlitzLock.sql | 2 +- sp_BlitzWho.sql | 2 +- sp_DatabaseRestore.sql | 2 +- sp_ineachdb.sql | 2 +- 12 files changed, 1592 insertions(+), 256 deletions(-) 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 From f7ec30f1f2c0ebd5b63d1724f28b1b4d09ba8892 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Tue, 15 Apr 2025 08:48:04 -0400 Subject: [PATCH 06/76] #3627 sp_Blitz 7745 warnings Remove duplicate warnings for trace flag 7745. Closes #3627. --- sp_Blitz.sql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 43de4bbfe..385a73ebd 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -8558,8 +8558,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT 'Informational' AS FindingsGroup , 'Recommended Trace Flag Off' AS Finding , 'https://www.sqlskills.com/blogs/erin/query-store-trace-flags/' AS URL , - 'Trace Flag 7745 not enabled globally. It makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. It is recommended, but it loses you the non-flushed Query Store data.' AS Details - FROM #TraceStatus T + 'Trace Flag 7745 not enabled globally. It makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. It is recommended, but it loses you the non-flushed Query Store data.' AS Details; END; IF NOT EXISTS ( SELECT 1 From dcfc4ec92aeb80c5ed9e8479b2f00c2c726e9ed1 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Tue, 15 Apr 2025 09:18:00 -0400 Subject: [PATCH 07/76] #3629 sp_BlitzFirst move avg ms To the left in result sets. Closes #3629. --- sp_BlitzFirst.sql | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index bdbcda1d4..f36f5fee7 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -4654,13 +4654,13 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, CAST(c.[Wait Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Wait Time (Hours)], - CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], - (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 THEN CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait] + ELSE 0 END AS [Avg ms Per Wait], + CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], + (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits] FROM max_batch b JOIN #WaitStats wd2 ON wd2.SampleTime =b.SampleTime @@ -4799,17 +4799,17 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, CAST(c.[Wait Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Wait Time (Hours)], + CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + THEN + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ + (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) + ELSE 0 END AS [Avg ms Per Wait], CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], CAST(c.[Signal Wait Time (Seconds)] / 60.0 / 60 AS DECIMAL(18,1)) AS [Signal Wait Time (Hours)], CASE WHEN c.[Wait Time (Seconds)] > 0 THEN CAST(100.*(c.[Signal Wait Time (Seconds)]/c.[Wait Time (Seconds)]) AS NUMERIC(4,1)) ELSE 0 END AS [Percent Signal Waits], (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], - CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - THEN - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ - (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait], N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON @@ -4843,17 +4843,17 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, c.[Wait Time (Seconds)], + CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + THEN + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ + (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) + ELSE 0 END AS [Avg ms Per Wait], CAST((CAST(wd2.wait_time_ms - wd1.wait_time_ms AS MONEY)) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Second], c.[Signal Wait Time (Seconds)], CASE WHEN c.[Wait Time (Seconds)] > 0 THEN CAST(100.*(c.[Signal Wait Time (Seconds)]/c.[Wait Time (Seconds)]) AS NUMERIC(4,1)) ELSE 0 END AS [Percent Signal Waits], (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], - CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - THEN - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ - (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait], N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON From b6ca85e4ff2904ce92272e7cafdecf71b2e4f962 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Tue, 15 Apr 2025 12:11:20 -0400 Subject: [PATCH 08/76] #3632 sp_BlitzCache readable replicas Lets you query the plan cache on secondaries. Closes #3632. --- README.md | 1 + sp_BlitzCache.sql | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e874b611c..9698711a6 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ Other common parameters include: * @ExportToExcel = 1 - turn this on, and it doesn't return XML fields that would hinder you from copy/pasting the data into Excel. * @ExpertMode = 1 - turn this on, and you get more columns with more data. Doesn't take longer to run though. * @IgnoreSystemDBs = 0 - if you want to show queries in master/model/msdb. By default we hide these. Additionally hides queries from databases named `dbadmin`, `dbmaintenance`, and `dbatools`. +* @IgnoreReadableReplicaDBs = 0 - if you want to analyze the plan cache on an Availability Group readable replica. You will also have to connect to the replica using ApplicationIntent = ReadOnly, since SQL Server itself will abort queries that try to do work in readable secondaries. * @MinimumExecutionCount = 0 - in servers like data warehouses where lots of queries only run a few times, you can set a floor number for examination. [*Back to top*](#header1) diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index 9c6453f70..655f855b5 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -256,6 +256,7 @@ ALTER PROCEDURE dbo.sp_BlitzCache @DurationFilter DECIMAL(38,4) = NULL , @HideSummary BIT = 0 , @IgnoreSystemDBs BIT = 1 , + @IgnoreReadableReplicaDBs BIT = 1 , @OnlyQueryHashes VARCHAR(MAX) = NULL , @IgnoreQueryHashes VARCHAR(MAX) = NULL , @OnlySqlHandles VARCHAR(MAX) = NULL , @@ -1405,7 +1406,7 @@ CREATE TABLE #plan_usage ); -IF EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') +IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') BEGIN RAISERROR('Checking for Read intent databases to exclude',0,0) WITH NOWAIT; @@ -1824,7 +1825,7 @@ IF @VersionShowsAirQuoteActualPlans = 1 SET @body += N' WHERE 1 = 1 ' + @nl ; - IF EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') + IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') BEGIN RAISERROR(N'Ignoring readable secondaries databases by default', 0, 1) WITH NOWAIT; SET @body += N' AND CAST(xpa.value AS INT) NOT IN (SELECT database_id FROM #ReadableDBs)' + @nl ; From 5b95670eb3187f9175edac89d8111b7d40acce0e Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Tue, 15 Apr 2025 15:59:57 -0400 Subject: [PATCH 09/76] #3631 sp_BlitzFirst add thread time To headline news result set. Closes #3631. --- .../sp_BlitzFirst_Checks_by_Priority.md | 5 ++-- sp_BlitzFirst.sql | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Documentation/sp_BlitzFirst_Checks_by_Priority.md b/Documentation/sp_BlitzFirst_Checks_by_Priority.md index 053423541..2b7a0e25d 100644 --- a/Documentation/sp_BlitzFirst_Checks_by_Priority.md +++ b/Documentation/sp_BlitzFirst_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 49 -If you want to add a new check, start at 50. +CURRENT HIGH CHECKID: 50 +If you want to add a new check, start at 51. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|---------------------------------|---------------------------------------|-------------------------------------------------|----------| @@ -58,4 +58,5 @@ If you want to add a new check, start at 50. | 251 | Server Info | Database Count | | 22 | | 251 | Server Info | Database Size, Total GB | | 21 | | 251 | Server Info | Memory Grant/Workspace info | | 40 | +| 251 | Server Info | Thread Time | https://www.brentozar.com/go/threadtime | 50 | | 254 | Informational | Thread Time Inaccurate | | 48 | diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index f36f5fee7..72004d33c 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -2021,6 +2021,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, FROM sys.databases WHERE database_id > 4; + /* Server Info - Memory Grants pending - CheckID 39 */ IF (@Debug = 1) BEGIN @@ -3325,6 +3326,34 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, OR max_session_percent >= 90); END + /* Server Info - Thread Time - CheckID 50 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 50',10,1) WITH NOWAIT; + END + + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT TOP 1 50 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Thread Time' AS Finding, + CAST(CAST(c.[Total Thread Time (Seconds)] AS DECIMAL(18,1)) AS VARCHAR(100)) AS Details, + CAST(c.[Total Thread Time (Seconds)] AS DECIMAL(18,1)) AS DetailsInt, + 'https://www.brentozar.com/go/threadtime' AS URL + FROM max_batch b + JOIN #WaitStats wd2 ON + wd2.SampleTime =b.SampleTime + JOIN #WaitStats wd1 ON + wd1.wait_type=wd2.wait_type AND + wd2.SampleTime > wd1.SampleTime + CROSS APPLY (SELECT + CAST((wd2.thread_time_ms - wd1.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c; + /* Server Info - Batch Requests per Sec - CheckID 19 */ IF (@Debug = 1) BEGIN From 04c60f8fced3821d1553abd17d34069ff2cafc11 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Tue, 15 Apr 2025 17:22:12 -0400 Subject: [PATCH 10/76] #3635 sp_Blitz AG latency warning Closes #3635. --- Documentation/sp_Blitz_Checks_by_Priority.md | 5 ++- sp_Blitz.sql | 39 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Documentation/sp_Blitz_Checks_by_Priority.md b/Documentation/sp_Blitz_Checks_by_Priority.md index 8f964ffe5..c803f7d43 100644 --- a/Documentation/sp_Blitz_Checks_by_Priority.md +++ b/Documentation/sp_Blitz_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 267. -If you want to add a new one, start at 268. +CURRENT HIGH CHECKID: 268. +If you want to add a new one, start at 269. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|-----------------------------|---------------------------------------------------------|------------------------------------------------------------------------|----------| @@ -32,6 +32,7 @@ If you want to add a new one, start at 268. | 1 | Security | Dangerous Service Account | https://vladdba.com/SQLServerSvcAccount | 259 | | 1 | Security | Dangerous Service Account | https://vladdba.com/SQLServerSvcAccount | 260 | | 1 | Security | Dangerous Service Account | https://vladdba.com/SQLServerSvcAccount | 261 | +| 5 | Availability | AG Replica Falling Behind | https://www.BrentOzar.com/go/ag | 268 | | 5 | Monitoring | Disabled Internal Monitoring Features | https://msdn.microsoft.com/en-us/library/ms190737.aspx | 177 | | 5 | Reliability | Dangerous Third Party Modules | https://support.microsoft.com/en-us/kb/2033238 | 179 | | 5 | Reliability | Priority Boost Enabled | https://www.BrentOzar.com/go/priorityboost | 126 | diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 385a73ebd..0782feb7a 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -6706,6 +6706,45 @@ IF @ProductVersionMajor >= 10 + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 268 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 268) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + DatabaseName , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 268 AS CheckID, + 5 AS Priority, + DB_NAME(ps.database_id), + 'Availability' AS FindingsGroup, + 'AG Replica Falling Behind' AS Finding, + 'https://www.BrentOzar.com/go/ag' AS URL, + ag.name + N' AG replica server ' + + ar.replica_server_name + N' is ' + + CASE WHEN DATEDIFF(SECOND, drs.last_commit_time, ps.last_commit_time) < 200 THEN (CAST(DATEDIFF(SECOND, drs.last_commit_time, ps.last_commit_time) AS NVARCHAR(10)) + N' seconds ') + ELSE (CAST(DATEDIFF(MINUTE, drs.last_commit_time, ps.last_commit_time) AS NVARCHAR(10)) + N' minutes ') END + + N' behind the primary.' + AS details + FROM sys.dm_hadr_database_replica_states AS drs + JOIN sys.availability_replicas AS ar ON drs.replica_id = ar.replica_id + JOIN sys.availability_groups AS ag ON ar.group_id = ag.group_id + JOIN sys.dm_hadr_database_replica_states AS ps + ON drs.group_id = ps.group_id + AND drs.database_id = ps.database_id + AND ps.is_local = 1 /* Primary */ + WHERE drs.is_local = 0 /* Secondary */ + AND DATEDIFF(SECOND, drs.last_commit_time, ps.last_commit_time) > 60; + END; + IF @CheckUserDatabaseObjects = 1 BEGIN From 126a60882dbcfabd88b16b93f9d266abae252f30 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Tue, 15 Apr 2025 17:37:52 -0400 Subject: [PATCH 11/76] #3637 sp_BlitzFirst deadlock warning Add warning if deadlocks are happening now. Closes #3637. --- .../sp_BlitzFirst_Checks_by_Priority.md | 9 ++++---- sp_BlitzFirst.sql | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Documentation/sp_BlitzFirst_Checks_by_Priority.md b/Documentation/sp_BlitzFirst_Checks_by_Priority.md index 2b7a0e25d..b81ea5505 100644 --- a/Documentation/sp_BlitzFirst_Checks_by_Priority.md +++ b/Documentation/sp_BlitzFirst_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 50 -If you want to add a new check, start at 51. +CURRENT HIGH CHECKID: 51 +If you want to add a new check, start at 52. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|---------------------------------|---------------------------------------|-------------------------------------------------|----------| @@ -42,10 +42,11 @@ If you want to add a new check, start at 51. | 50 | Server Performance | Too Much Free Memory | https://www.brentozar.com/go/freememory | 34 | | 50 | Server Performance | Memory Grants pending | https://www.brentozar.com/blitz/memory-grants | 39 | | 100 | In-Memory OLTP | Transactions aborted | https://www.brentozar.com/go/aborted | 32 | -| 100 | Query Problems | Suboptimal Plans/Sec High | https://www.brentozar.com/go/suboptimal | 33 | | 100 | Query Problems | Bad Estimates | https://www.brentozar.com/go/skewedup | 42 | -| 100 | Query Problems | Skewed Parallelism | https://www.brentozar.com/go/skewedup | 43 | +| 100 | Query Problems | Deadlocks | https://www.brentozar.com/go/deadlocks | 51 | | 100 | Query Problems | Query with a memory grant exceeding @MemoryGrantThresholdPct | https://www.brentozar.com/memory-grants-sql-servers-public-toilet/ | 46 | +| 100 | Query Problems | Skewed Parallelism | https://www.brentozar.com/go/skewedup | 43 | +| 100 | Query Problems | Suboptimal Plans/Sec High | https://www.brentozar.com/go/suboptimal | 33 | | 200 | Wait Stats | (One per wait type) | https://www.brentozar.com/sql/wait-stats/#(waittype) | 6 | | 210 | Potential Upcoming Problems | High Number of Connections |https://www.brentozar.com/archive/2014/05/connections-slow-sql-server-threadpool/ | 49 | | 210 | Query Stats | Plan Cache Analysis Skipped | https://www.brentozar.com/go/topqueries | 18 | diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 72004d33c..5ea9f4472 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -3081,6 +3081,28 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END; + /* Query Problems - Deadlocks - CheckID 51 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 51',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 51 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Deadlocks' AS Finding, + ' https://www.brentozar.com/go/deadlocks' AS URL, + 'Number of deadlocks during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + + 'Determined by sampling Perfmon counter ' + ps.object_name + ' - ' + ps.counter_name + @LineFeed AS Details, + 'Check sp_BlitzLock to find which indexes and queries to tune.' AS HowToStopIt + FROM #PerfmonStats ps + WHERE ps.Pass = 2 + AND counter_name = 'Number of Deadlocks/sec' + AND instance_name LIKE '_Total%' + AND value_delta > 0; + + /* SQL Server Internal Maintenance - Log File Growing - CheckID 13 */ IF (@Debug = 1) BEGIN From 5aae39864bb84482a434e3535573f81e27953ef8 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Sat, 26 Apr 2025 07:09:58 -0700 Subject: [PATCH 12/76] Update README.md Remove sp_BlitzLock @Top parameter. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9698711a6..bfcf2470d 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,6 @@ In addition to the [parameters common to many of the stored procedures](#paramet Checks either the System Health session or a specific Extended Event session that captures deadlocks and parses out all the XML for you. Parameters you can use: -* @Top: Use if you want to limit the number of deadlocks to return. This is ordered by event date ascending. * @DatabaseName: If you want to filter to a specific database * @StartDate: The date you want to start searching on. * @EndDate: The date you want to stop searching on. From 77ebf85fc31b5a6b92bf1fa117664242c07b4a93 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Tue, 29 Apr 2025 11:23:02 -0400 Subject: [PATCH 13/76] Issue 3640 Closes #3640 Fixes hardcoded table and column names Fixes #dd temp table not populating when not in "table" mode. --- sp_BlitzLock.sql | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index 3b4f65248..52361d583 100644 --- a/sp_BlitzLock.sql +++ b/sp_BlitzLock.sql @@ -1255,7 +1255,7 @@ BEGIN END; /* If table target */ - IF @TargetSessionType = 'table' + IF LOWER(@TargetSessionType) = N'table' BEGIN SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Inserting to #deadlock_data from table source %s', 0, 1, @d) WITH NOWAIT; @@ -1273,9 +1273,19 @@ BEGIN 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) + FROM ' + + QUOTENAME(@TargetDatabaseName) + + N'.' + + QUOTENAME(@TargetSchemaName) + + N'.' + + QUOTENAME(@TargetTableName) + + N' AS x + OUTER APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/event'') AS xe(e) + OUTER APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/deadlock'') AS xd(e) OPTION(RECOMPILE); '; @@ -1412,6 +1422,7 @@ BEGIN LEFT JOIN #t AS t ON 1 = 1 WHERE @xe = 1 + OR LOWER(@TargetSessionType) <> N'table' UNION ALL From 5d8d560d801fd21015a243b2822b82a671910b3c Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Mon, 12 May 2025 07:47:38 -0700 Subject: [PATCH 14/76] #3643 - sp_BlitzFirst RDS Restores Now show up. Closes #3643. --- sp_BlitzFirst.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 5ea9f4472..fe1301bc4 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -1638,7 +1638,7 @@ BEGIN 'Maintenance Tasks Running' AS FindingGroup, 'Restore Running' AS Finding, 'https://www.brentozar.com/askbrent/backups/' AS URL, - 'Restore of ' + DB_NAME(db.resource_database_id) + ' database (' + (SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id) + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, + 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, pl.query_plan AS QueryPlan, r.start_time AS StartTime, @@ -1646,14 +1646,14 @@ BEGIN s.nt_user_name AS NTUserName, s.[program_name] AS ProgramName, s.[host_name] AS HostName, - db.[resource_database_id] AS DatabaseID, - DB_NAME(db.resource_database_id) AS DatabaseName, + COALESCE(db.[resource_database_id],0) AS DatabaseID, + COALESCE(DB_NAME(db.resource_database_id), 'Unknown') AS DatabaseName, 0 AS OpenTransactionCount, r.query_hash FROM sys.dm_exec_requests r INNER JOIN sys.dm_exec_connections c ON r.session_id = c.session_id INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id - INNER JOIN ( + LEFT OUTER JOIN ( SELECT DISTINCT request_session_id, resource_database_id FROM sys.dm_tran_locks WHERE resource_type = N'DATABASE' From d354ad4f7d6c4b78070ff3ba054f619f419ae6c9 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Tue, 20 May 2025 06:59:15 -0400 Subject: [PATCH 15/76] #3646 sp_Blitz 2025 db scoped Adds new database scoped configurations. Closes #3646. --- sp_Blitz.sql | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 0782feb7a..2d0f72f05 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -7821,9 +7821,15 @@ IF @ProductVersionMajor >= 10 (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), + (37, 'DOP_FEEDBACK', CASE WHEN @ProductVersionMajor >= 17 THEN '1' ELSE '0' END, NULL, 267), (38, 'LEDGER_DIGEST_STORAGE_ENDPOINT', 'OFF', NULL, 267), - (39, 'FORCE_SHOWPLAN_RUNTIME_PARAMETER_COLLECTION', '0', NULL, 267); + (39, 'FORCE_SHOWPLAN_RUNTIME_PARAMETER_COLLECTION', '0', NULL, 267), + (40, 'READABLE_SECONDARY_TEMPORARY_STATS_AUTO_CREATE', '1', NULL, 267), + (41, 'READABLE_SECONDARY_TEMPORARY_STATS_AUTO_UPDATE', '1', NULL, 267), + (42, 'OPTIMIZED_SP_EXECUTESQL', '0', NULL, 267), + (43, 'OPTIMIZED_HALLOWEEN_PROTECTION', '1', NULL, 267), + (44, 'FULLTEXT_INDEX_VERSION', '2', NULL, 267), + (47, 'OPTIONAL_PARAMETER_OPTIMIZATION', '1', 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 de328c66c5c0f3c1258eb44dd3e2a612f8b27577 Mon Sep 17 00:00:00 2001 From: Ben <60398078+bwiggin10@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:04:10 -0400 Subject: [PATCH 16/76] Add check for Query Store to sp_ineachdb --- sp_ineachdb.sql | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index 16df3b7c5..b156bcf58 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -29,7 +29,8 @@ ALTER PROCEDURE [dbo].[sp_ineachdb] @Version varchar(30) = NULL OUTPUT, @VersionDate datetime = NULL OUTPUT, @VersionCheckMode bit = 0, - @is_ag_writeable_copy bit = 0 + @is_ag_writeable_copy bit = 0, + @is_query_store_on bit = 0 -- WITH EXECUTE AS OWNER – maybe not a great idea, depending on the security of your system AS BEGIN @@ -235,6 +236,23 @@ OPTION (MAXRECURSION 0); ) ); + -- delete any databases that don't match query store criteria + IF @SQLVersion >= 13 + BEGIN + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 + FROM sys.databases AS d + WHERE d.database_id = dbs.id + AND NOT + ( + is_query_store_on = COALESCE(@is_query_store_on, is_query_store_on) + AND NOT (@is_query_store_on = 1 AND d.database_id = 3) OR (@is_query_store_on = 0 AND d.database_id = 3) -- Excluding the model database which shows QS enabled in SQL2022+ + ) + ); + END + -- if a user access is specified, remove any that are NOT in that state IF @user_access IN (N'SINGLE_USER', N'MULTI_USER', N'RESTRICTED_USER') BEGIN From 470d55126071d5a8cafef5de5590ee6101e21faf Mon Sep 17 00:00:00 2001 From: Ben <60398078+bwiggin10@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:06:33 -0400 Subject: [PATCH 17/76] Formatting --- sp_ineachdb.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index b156bcf58..bf9b147b4 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -241,7 +241,7 @@ OPTION (MAXRECURSION 0); BEGIN DELETE dbs FROM #ineachdb AS dbs WHERE EXISTS - ( + ( SELECT 1 FROM sys.databases AS d WHERE d.database_id = dbs.id From e85173dd2af8356ea0c9d79eebb84857a161bb81 Mon Sep 17 00:00:00 2001 From: Jane Palmer Date: Thu, 19 Jun 2025 10:57:47 +0100 Subject: [PATCH 18/76] Update sp_Blitz.sql Extend details for Invalid Logins - empty AD Groups will occasionally show up here (it's an AD thing), and it's a pain to delete that group and then have to reinstate it in SQL when it turns out you do need it. --- sp_Blitz.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 2d0f72f05..c8cf8c65d 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -1862,7 +1862,7 @@ AS 'Security' AS FindingsGroup , 'Invalid login defined with Windows Authentication' AS Finding , 'https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-validatelogins-transact-sql' AS URL , - ( 'Windows user or group ' + QUOTENAME(LoginName) + ' is mapped to a SQL Server principal but no longer exists in the Windows environment.') AS Details + ( 'Windows user or group ' + QUOTENAME(LoginName) + ' is mapped to a SQL Server principal but no longer exists in the Windows environment. Sometimes empty AD groups can show up here so check thoroughly.') AS Details FROM #InvalidLogins ; END; From 39337ba705f892d18eaab5bf310f0138ccef99aa Mon Sep 17 00:00:00 2001 From: Ben <60398078+bwiggin10@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:33:13 -0400 Subject: [PATCH 19/76] Exclude Model Database From @QueryStoreInUse Check The model database always returns 1 for is_query_store_on in sys.databases in SQL 2022, so this will always show query store is in use even if no user databases have it enabled. --- sp_Blitz.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 2d0f72f05..0f37fb1c4 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -7777,7 +7777,7 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS(SELECT * FROM sys.databases WHERE is_query_store_on = 1) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS(SELECT * FROM sys.databases WHERE is_query_store_on = 1 AND database_id <> 3) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @QueryStoreInUse = 1; END; From 0e3be2ff0c1fdc126e0670306e87701f6bb9441c Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 3 Jul 2025 03:51:27 -0700 Subject: [PATCH 20/76] #3655 sp_Blitz add 2025 dsc Adds preview_features database_scoped_configuration. Closes #3655. --- sp_Blitz.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 2d0f72f05..68fe4bca0 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -7829,7 +7829,8 @@ IF @ProductVersionMajor >= 10 (42, 'OPTIMIZED_SP_EXECUTESQL', '0', NULL, 267), (43, 'OPTIMIZED_HALLOWEEN_PROTECTION', '1', NULL, 267), (44, 'FULLTEXT_INDEX_VERSION', '2', NULL, 267), - (47, 'OPTIONAL_PARAMETER_OPTIMIZATION', '1', NULL, 267); + (47, 'OPTIONAL_PARAMETER_OPTIMIZATION', '1', NULL, 267), + (48, 'PREVIEW_FEATURES', '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 572f71c97bf3dbbf8d90372b4439736e278bb815 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 3 Jul 2025 05:44:01 -0700 Subject: [PATCH 21/76] #3657 sp_Blitz 2025 configurations And switched to a table value constructor for shorter code. Closes #3657. --- Documentation/sp_Blitz_Checks_by_Priority.md | 5 +- sp_Blitz.sql | 304 ++++++++----------- 2 files changed, 137 insertions(+), 172 deletions(-) diff --git a/Documentation/sp_Blitz_Checks_by_Priority.md b/Documentation/sp_Blitz_Checks_by_Priority.md index c803f7d43..c81a97708 100644 --- a/Documentation/sp_Blitz_Checks_by_Priority.md +++ b/Documentation/sp_Blitz_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 268. -If you want to add a new one, start at 269. +CURRENT HIGH CHECKID: 269. +If you want to add a new one, start at 270. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|-----------------------------|---------------------------------------------------------|------------------------------------------------------------------------|----------| @@ -242,6 +242,7 @@ If you want to add a new one, start at 269. | 200 | Non-Default Server Config | user options | https://www.BrentOzar.com/go/conf | 1063 | | 200 | Non-Default Server Config | Web Assistant Procedures | https://www.BrentOzar.com/go/conf | 1064 | | 200 | Non-Default Server Config | xp_cmdshell | https://www.BrentOzar.com/go/conf | 1065 | +| 200 | Non-Default Server Config | Configuration Changed | https://www.BrentOzar.com/go/conf | 269 | | 200 | Performance | Buffer Pool Extensions Enabled | https://www.BrentOzar.com/go/bpe | 174 | | 200 | Performance | Default Parallelism Settings | https://www.BrentOzar.com/go/cxpacket | 188 | | 200 | Performance | In-Memory OLTP (Hekaton) In Use | https://www.BrentOzar.com/go/hekaton | 146 | diff --git a/sp_Blitz.sql b/sp_Blitz.sql index b2433cf1f..58c97a514 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -2320,177 +2320,141 @@ AS IF @Debug IN (1, 2) RAISERROR('Generating default configuration values', 0, 1) WITH NOWAIT; - INSERT INTO #ConfigurationDefaults - VALUES ( 'access check cache bucket count', 0, 1001 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'access check cache quota', 0, 1002 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Ad Hoc Distributed Queries', 0, 1003 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity I/O mask', 0, 1004 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity mask', 0, 1005 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity64 mask', 0, 1066 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity64 I/O mask', 0, 1067 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Agent XPs', 0, 1071 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'allow updates', 0, 1007 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'awe enabled', 0, 1008 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'backup checksum default', 0, 1070 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'backup compression default', 0, 1073 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'blocked process threshold', 0, 1009 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'blocked process threshold (s)', 0, 1009 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'c2 audit mode', 0, 1010 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'clr enabled', 0, 1011 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'common criteria compliance enabled', 0, 1074 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'contained database authentication', 0, 1068 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'cost threshold for parallelism', 5, 1012 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'cross db ownership chaining', 0, 1013 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'cursor threshold', -1, 1014 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Database Mail XPs', 0, 1072 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'default full-text language', 1033, 1016 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'default language', 0, 1017 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'default trace enabled', 1, 1018 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'disallow results from triggers', 0, 1019 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'EKM provider enabled', 0, 1075 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'filestream access level', 0, 1076 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'fill factor (%)', 0, 1020 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft crawl bandwidth (max)', 100, 1021 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft crawl bandwidth (min)', 0, 1022 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft notify bandwidth (max)', 100, 1023 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft notify bandwidth (min)', 0, 1024 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'index create memory (KB)', 0, 1025 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'in-doubt xact resolution', 0, 1026 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'lightweight pooling', 0, 1027 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'locks', 0, 1028 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max degree of parallelism', 0, 1029 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max full-text crawl range', 4, 1030 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max server memory (MB)', 2147483647, 1031 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max text repl size (B)', 65536, 1032 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max worker threads', 0, 1033 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'media retention', 0, 1034 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'min memory per query (KB)', 1024, 1035 ); - /* Accepting both 0 and 16 below because both have been seen in the wild as defaults. */ - IF EXISTS ( SELECT * - FROM sys.configurations - WHERE name = 'min server memory (MB)' - AND value_in_use IN ( 0, 16 ) ) - INSERT INTO #ConfigurationDefaults - SELECT 'min server memory (MB)' , - CAST(value_in_use AS BIGINT), 1036 - FROM sys.configurations - WHERE name = 'min server memory (MB)'; - ELSE - INSERT INTO #ConfigurationDefaults - VALUES ( 'min server memory (MB)', 0, 1036 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'nested triggers', 1, 1037 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'network packet size (B)', 4096, 1038 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Ole Automation Procedures', 0, 1039 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'open objects', 0, 1040 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'optimize for ad hoc workloads', 0, 1041 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'PH timeout (s)', 60, 1042 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'precompute rank', 0, 1043 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'priority boost', 0, 1044 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'query governor cost limit', 0, 1045 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'query wait (s)', -1, 1046 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'recovery interval (min)', 0, 1047 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote access', 1, 1048 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote admin connections', 0, 1049 ); - /* SQL Server 2012 changes a configuration default */ - IF @@VERSION LIKE '%Microsoft SQL Server 2005%' - OR @@VERSION LIKE '%Microsoft SQL Server 2008%' - BEGIN - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote login timeout (s)', 20, 1069 ); - END; + INSERT INTO #ConfigurationDefaults + VALUES + ( 'access check cache bucket count', 0, 1001 ), + ( 'access check cache quota', 0, 1002 ), + ( 'Ad Hoc Distributed Queries', 0, 1003 ), + ( 'affinity I/O mask', 0, 1004 ), + ( 'affinity mask', 0, 1005 ), + ( 'affinity64 mask', 0, 1066 ), + ( 'affinity64 I/O mask', 0, 1067 ), + ( 'Agent XPs', 0, 1071 ), + ( 'allow updates', 0, 1007 ), + ( 'awe enabled', 0, 1008 ), + ( 'backup checksum default', 0, 1070 ), + ( 'backup compression default', 0, 1073 ), + ( 'blocked process threshold', 0, 1009 ), + ( 'blocked process threshold (s)', 0, 1009 ), + ( 'c2 audit mode', 0, 1010 ), + ( 'clr enabled', 0, 1011 ), + ( 'common criteria compliance enabled', 0, 1074 ), + ( 'contained database authentication', 0, 1068 ), + ( 'cost threshold for parallelism', 5, 1012 ), + ( 'cross db ownership chaining', 0, 1013 ), + ( 'cursor threshold', -1, 1014 ), + ( 'Database Mail XPs', 0, 1072 ), + ( 'default full-text language', 1033, 1016 ), + ( 'default language', 0, 1017 ), + ( 'default trace enabled', 1, 1018 ), + ( 'disallow results from triggers', 0, 1019 ), + ( 'EKM provider enabled', 0, 1075 ), + ( 'filestream access level', 0, 1076 ), + ( 'fill factor (%)', 0, 1020 ), + ( 'ft crawl bandwidth (max)', 100, 1021 ), + ( 'ft crawl bandwidth (min)', 0, 1022 ), + ( 'ft notify bandwidth (max)', 100, 1023 ), + ( 'ft notify bandwidth (min)', 0, 1024 ), + ( 'index create memory (KB)', 0, 1025 ), + ( 'in-doubt xact resolution', 0, 1026 ), + ( 'lightweight pooling', 0, 1027 ), + ( 'locks', 0, 1028 ), + ( 'max degree of parallelism', 0, 1029 ), + ( 'max full-text crawl range', 4, 1030 ), + ( 'max server memory (MB)', 2147483647, 1031 ), + ( 'max text repl size (B)', 65536, 1032 ), + ( 'max worker threads', 0, 1033 ), + ( 'media retention', 0, 1034 ), + ( 'min memory per query (KB)', 1024, 1035 ), + ( 'nested triggers', 1, 1037 ), + ( 'network packet size (B)', 4096, 1038 ), + ( 'Ole Automation Procedures', 0, 1039 ), + ( 'open objects', 0, 1040 ), + ( 'optimize for ad hoc workloads', 0, 1041 ), + ( 'PH timeout (s)', 60, 1042 ), + ( 'precompute rank', 0, 1043 ), + ( 'priority boost', 0, 1044 ), + ( 'query governor cost limit', 0, 1045 ), + ( 'query wait (s)', -1, 1046 ), + ( 'recovery interval (min)', 0, 1047 ), + ( 'remote access', 1, 1048 ), + ( 'remote admin connections', 0, 1049 ), + ( 'remote login timeout (s)', CASE + WHEN @@VERSION LIKE '%Microsoft SQL Server 2005%' + OR @@VERSION LIKE '%Microsoft SQL Server 2008%' THEN 20 + ELSE 10 + END, 1069 ), + ( 'remote proc trans', 0, 1050 ), + ( 'remote query timeout (s)', 600, 1051 ), + ( 'Replication XPs', 0, 1052 ), + ( 'RPC parameter data validation', 0, 1053 ), + ( 'scan for startup procs', 0, 1054 ), + ( 'server trigger recursion', 1, 1055 ), + ( 'set working set size', 0, 1056 ), + ( 'show advanced options', 0, 1057 ), + ( 'SMO and DMO XPs', 1, 1058 ), + ( 'SQL Mail XPs', 0, 1059 ), + ( 'transform noise words', 0, 1060 ), + ( 'two digit year cutoff', 2049, 1061 ), + ( 'user connections', 0, 1062 ), + ( 'user options', 0, 1063 ), + ( 'Web Assistant Procedures', 0, 1064 ), + ( 'xp_cmdshell', 0, 1065 ), + ( 'automatic soft-NUMA disabled', 0, 269), + ( 'external scripts enabled', 0, 269), + ( 'clr strict security', 1, 269), + ( 'column encryption enclave type', 0, 269), + ( 'tempdb metadata memory-optimized', 0, 269), + ( 'ADR cleaner retry timeout (min)', 15, 269), + ( 'ADR Preallocation Factor', 4, 269), + ( 'version high part of SQL Server', 1114112, 269), + ( 'version low part of SQL Server', 52428803, 269), + ( 'Data processed daily limit in TB', 2147483647, 269), + ( 'Data processed weekly limit in TB', 2147483647, 269), + ( 'Data processed monthly limit in TB', 2147483647, 269), + ( 'ADR Cleaner Thread Count', 1, 269), + ( 'hardware offload enabled', 0, 269), + ( 'hardware offload config', 0, 269), + ( 'hardware offload mode', 0, 269), + ( 'backup compression algorithm', 0, 269), + ( 'ADR cleaner lock timeout (s)', 5, 269), + ( 'SLOG memory quota (%)', 75, 269), + ( 'max RPC request params (KB)', 0, 269), + ( 'max UCS send boxcars', 256, 269), + ( 'availability group commit time (ms)', 0, 269), + ( 'tiered memory enabled', 0, 269), + ( 'max server tiered memory (MB)', 2147483647, 269), + ( 'hadoop connectivity', 0, 269), + ( 'polybase network encryption', 1, 269), + ( 'remote data archive', 0, 269), + ( 'allow polybase export', 0, 269), + ( 'allow filesystem enumeration', 1, 269), + ( 'polybase enabled', 0, 269), + ( 'suppress recovery model errors', 0, 269), + ( 'openrowset auto_create_statistics', 1, 269), + ( 'external rest endpoint enabled', 0, 269), + ( 'external xtp dll gen util enabled', 0, 269), + ( 'external AI runtimes enabled', 0, 269), + ( 'allow server scoped db credentials', 0, 269); + + /* Either 0 or 16 is fine here */ + IF EXISTS ( + SELECT * FROM sys.configurations + WHERE name = 'min server memory (MB)' + AND value_in_use IN (0, 16) + ) + BEGIN + INSERT INTO #ConfigurationDefaults + SELECT 'min server memory (MB)', CAST(value_in_use AS BIGINT), 1036 + FROM sys.configurations + WHERE name = 'min server memory (MB)'; + END ELSE - BEGIN - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote login timeout (s)', 10, 1069 ); - END; - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote proc trans', 0, 1050 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote query timeout (s)', 600, 1051 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Replication XPs', 0, 1052 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'RPC parameter data validation', 0, 1053 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'scan for startup procs', 0, 1054 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'server trigger recursion', 1, 1055 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'set working set size', 0, 1056 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'show advanced options', 0, 1057 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'SMO and DMO XPs', 1, 1058 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'SQL Mail XPs', 0, 1059 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'transform noise words', 0, 1060 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'two digit year cutoff', 2049, 1061 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'user connections', 0, 1062 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'user options', 0, 1063 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Web Assistant Procedures', 0, 1064 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'xp_cmdshell', 0, 1065 ); + BEGIN + INSERT INTO #ConfigurationDefaults + VALUES ('min server memory (MB)', 0, 1036); + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks From 628c147614db124fa337ba851396e55cd0d06968 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Fri, 4 Jul 2025 06:38:33 -0700 Subject: [PATCH 22/76] #3660 sp_BlitzFirst thread time Adds units of measure, updates release dates and versions for 2025-07-04 release. Closes #3660. --- Install-All-Scripts.sql | 533 ++++++++++++++++++++++++---------------- Install-Azure.sql | 140 ++++++++--- SqlServerVersions.sql | 4 + sp_Blitz.sql | 2 +- sp_BlitzAnalysis.sql | 2 +- sp_BlitzBackups.sql | 2 +- sp_BlitzCache.sql | 2 +- sp_BlitzFirst.sql | 45 +++- sp_BlitzIndex.sql | 2 +- sp_BlitzLock.sql | 2 +- sp_BlitzWho.sql | 2 +- sp_DatabaseRestore.sql | 2 +- sp_ineachdb.sql | 2 +- 13 files changed, 479 insertions(+), 261 deletions(-) diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index 83c596c58..5f310717a 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.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -1862,7 +1862,7 @@ AS 'Security' AS FindingsGroup , 'Invalid login defined with Windows Authentication' AS Finding , 'https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-validatelogins-transact-sql' AS URL , - ( 'Windows user or group ' + QUOTENAME(LoginName) + ' is mapped to a SQL Server principal but no longer exists in the Windows environment.') AS Details + ( 'Windows user or group ' + QUOTENAME(LoginName) + ' is mapped to a SQL Server principal but no longer exists in the Windows environment. Sometimes empty AD groups can show up here so check thoroughly.') AS Details FROM #InvalidLogins ; END; @@ -2320,177 +2320,141 @@ AS IF @Debug IN (1, 2) RAISERROR('Generating default configuration values', 0, 1) WITH NOWAIT; - INSERT INTO #ConfigurationDefaults - VALUES ( 'access check cache bucket count', 0, 1001 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'access check cache quota', 0, 1002 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Ad Hoc Distributed Queries', 0, 1003 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity I/O mask', 0, 1004 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity mask', 0, 1005 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity64 mask', 0, 1066 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'affinity64 I/O mask', 0, 1067 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Agent XPs', 0, 1071 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'allow updates', 0, 1007 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'awe enabled', 0, 1008 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'backup checksum default', 0, 1070 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'backup compression default', 0, 1073 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'blocked process threshold', 0, 1009 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'blocked process threshold (s)', 0, 1009 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'c2 audit mode', 0, 1010 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'clr enabled', 0, 1011 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'common criteria compliance enabled', 0, 1074 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'contained database authentication', 0, 1068 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'cost threshold for parallelism', 5, 1012 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'cross db ownership chaining', 0, 1013 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'cursor threshold', -1, 1014 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Database Mail XPs', 0, 1072 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'default full-text language', 1033, 1016 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'default language', 0, 1017 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'default trace enabled', 1, 1018 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'disallow results from triggers', 0, 1019 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'EKM provider enabled', 0, 1075 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'filestream access level', 0, 1076 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'fill factor (%)', 0, 1020 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft crawl bandwidth (max)', 100, 1021 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft crawl bandwidth (min)', 0, 1022 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft notify bandwidth (max)', 100, 1023 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'ft notify bandwidth (min)', 0, 1024 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'index create memory (KB)', 0, 1025 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'in-doubt xact resolution', 0, 1026 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'lightweight pooling', 0, 1027 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'locks', 0, 1028 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max degree of parallelism', 0, 1029 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max full-text crawl range', 4, 1030 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max server memory (MB)', 2147483647, 1031 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max text repl size (B)', 65536, 1032 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'max worker threads', 0, 1033 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'media retention', 0, 1034 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'min memory per query (KB)', 1024, 1035 ); - /* Accepting both 0 and 16 below because both have been seen in the wild as defaults. */ - IF EXISTS ( SELECT * - FROM sys.configurations - WHERE name = 'min server memory (MB)' - AND value_in_use IN ( 0, 16 ) ) - INSERT INTO #ConfigurationDefaults - SELECT 'min server memory (MB)' , - CAST(value_in_use AS BIGINT), 1036 - FROM sys.configurations - WHERE name = 'min server memory (MB)'; - ELSE - INSERT INTO #ConfigurationDefaults - VALUES ( 'min server memory (MB)', 0, 1036 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'nested triggers', 1, 1037 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'network packet size (B)', 4096, 1038 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Ole Automation Procedures', 0, 1039 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'open objects', 0, 1040 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'optimize for ad hoc workloads', 0, 1041 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'PH timeout (s)', 60, 1042 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'precompute rank', 0, 1043 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'priority boost', 0, 1044 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'query governor cost limit', 0, 1045 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'query wait (s)', -1, 1046 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'recovery interval (min)', 0, 1047 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote access', 1, 1048 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote admin connections', 0, 1049 ); - /* SQL Server 2012 changes a configuration default */ - IF @@VERSION LIKE '%Microsoft SQL Server 2005%' - OR @@VERSION LIKE '%Microsoft SQL Server 2008%' - BEGIN - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote login timeout (s)', 20, 1069 ); - END; + INSERT INTO #ConfigurationDefaults + VALUES + ( 'access check cache bucket count', 0, 1001 ), + ( 'access check cache quota', 0, 1002 ), + ( 'Ad Hoc Distributed Queries', 0, 1003 ), + ( 'affinity I/O mask', 0, 1004 ), + ( 'affinity mask', 0, 1005 ), + ( 'affinity64 mask', 0, 1066 ), + ( 'affinity64 I/O mask', 0, 1067 ), + ( 'Agent XPs', 0, 1071 ), + ( 'allow updates', 0, 1007 ), + ( 'awe enabled', 0, 1008 ), + ( 'backup checksum default', 0, 1070 ), + ( 'backup compression default', 0, 1073 ), + ( 'blocked process threshold', 0, 1009 ), + ( 'blocked process threshold (s)', 0, 1009 ), + ( 'c2 audit mode', 0, 1010 ), + ( 'clr enabled', 0, 1011 ), + ( 'common criteria compliance enabled', 0, 1074 ), + ( 'contained database authentication', 0, 1068 ), + ( 'cost threshold for parallelism', 5, 1012 ), + ( 'cross db ownership chaining', 0, 1013 ), + ( 'cursor threshold', -1, 1014 ), + ( 'Database Mail XPs', 0, 1072 ), + ( 'default full-text language', 1033, 1016 ), + ( 'default language', 0, 1017 ), + ( 'default trace enabled', 1, 1018 ), + ( 'disallow results from triggers', 0, 1019 ), + ( 'EKM provider enabled', 0, 1075 ), + ( 'filestream access level', 0, 1076 ), + ( 'fill factor (%)', 0, 1020 ), + ( 'ft crawl bandwidth (max)', 100, 1021 ), + ( 'ft crawl bandwidth (min)', 0, 1022 ), + ( 'ft notify bandwidth (max)', 100, 1023 ), + ( 'ft notify bandwidth (min)', 0, 1024 ), + ( 'index create memory (KB)', 0, 1025 ), + ( 'in-doubt xact resolution', 0, 1026 ), + ( 'lightweight pooling', 0, 1027 ), + ( 'locks', 0, 1028 ), + ( 'max degree of parallelism', 0, 1029 ), + ( 'max full-text crawl range', 4, 1030 ), + ( 'max server memory (MB)', 2147483647, 1031 ), + ( 'max text repl size (B)', 65536, 1032 ), + ( 'max worker threads', 0, 1033 ), + ( 'media retention', 0, 1034 ), + ( 'min memory per query (KB)', 1024, 1035 ), + ( 'nested triggers', 1, 1037 ), + ( 'network packet size (B)', 4096, 1038 ), + ( 'Ole Automation Procedures', 0, 1039 ), + ( 'open objects', 0, 1040 ), + ( 'optimize for ad hoc workloads', 0, 1041 ), + ( 'PH timeout (s)', 60, 1042 ), + ( 'precompute rank', 0, 1043 ), + ( 'priority boost', 0, 1044 ), + ( 'query governor cost limit', 0, 1045 ), + ( 'query wait (s)', -1, 1046 ), + ( 'recovery interval (min)', 0, 1047 ), + ( 'remote access', 1, 1048 ), + ( 'remote admin connections', 0, 1049 ), + ( 'remote login timeout (s)', CASE + WHEN @@VERSION LIKE '%Microsoft SQL Server 2005%' + OR @@VERSION LIKE '%Microsoft SQL Server 2008%' THEN 20 + ELSE 10 + END, 1069 ), + ( 'remote proc trans', 0, 1050 ), + ( 'remote query timeout (s)', 600, 1051 ), + ( 'Replication XPs', 0, 1052 ), + ( 'RPC parameter data validation', 0, 1053 ), + ( 'scan for startup procs', 0, 1054 ), + ( 'server trigger recursion', 1, 1055 ), + ( 'set working set size', 0, 1056 ), + ( 'show advanced options', 0, 1057 ), + ( 'SMO and DMO XPs', 1, 1058 ), + ( 'SQL Mail XPs', 0, 1059 ), + ( 'transform noise words', 0, 1060 ), + ( 'two digit year cutoff', 2049, 1061 ), + ( 'user connections', 0, 1062 ), + ( 'user options', 0, 1063 ), + ( 'Web Assistant Procedures', 0, 1064 ), + ( 'xp_cmdshell', 0, 1065 ), + ( 'automatic soft-NUMA disabled', 0, 269), + ( 'external scripts enabled', 0, 269), + ( 'clr strict security', 1, 269), + ( 'column encryption enclave type', 0, 269), + ( 'tempdb metadata memory-optimized', 0, 269), + ( 'ADR cleaner retry timeout (min)', 15, 269), + ( 'ADR Preallocation Factor', 4, 269), + ( 'version high part of SQL Server', 1114112, 269), + ( 'version low part of SQL Server', 52428803, 269), + ( 'Data processed daily limit in TB', 2147483647, 269), + ( 'Data processed weekly limit in TB', 2147483647, 269), + ( 'Data processed monthly limit in TB', 2147483647, 269), + ( 'ADR Cleaner Thread Count', 1, 269), + ( 'hardware offload enabled', 0, 269), + ( 'hardware offload config', 0, 269), + ( 'hardware offload mode', 0, 269), + ( 'backup compression algorithm', 0, 269), + ( 'ADR cleaner lock timeout (s)', 5, 269), + ( 'SLOG memory quota (%)', 75, 269), + ( 'max RPC request params (KB)', 0, 269), + ( 'max UCS send boxcars', 256, 269), + ( 'availability group commit time (ms)', 0, 269), + ( 'tiered memory enabled', 0, 269), + ( 'max server tiered memory (MB)', 2147483647, 269), + ( 'hadoop connectivity', 0, 269), + ( 'polybase network encryption', 1, 269), + ( 'remote data archive', 0, 269), + ( 'allow polybase export', 0, 269), + ( 'allow filesystem enumeration', 1, 269), + ( 'polybase enabled', 0, 269), + ( 'suppress recovery model errors', 0, 269), + ( 'openrowset auto_create_statistics', 1, 269), + ( 'external rest endpoint enabled', 0, 269), + ( 'external xtp dll gen util enabled', 0, 269), + ( 'external AI runtimes enabled', 0, 269), + ( 'allow server scoped db credentials', 0, 269); + + /* Either 0 or 16 is fine here */ + IF EXISTS ( + SELECT * FROM sys.configurations + WHERE name = 'min server memory (MB)' + AND value_in_use IN (0, 16) + ) + BEGIN + INSERT INTO #ConfigurationDefaults + SELECT 'min server memory (MB)', CAST(value_in_use AS BIGINT), 1036 + FROM sys.configurations + WHERE name = 'min server memory (MB)'; + END ELSE - BEGIN - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote login timeout (s)', 10, 1069 ); - END; - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote proc trans', 0, 1050 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'remote query timeout (s)', 600, 1051 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Replication XPs', 0, 1052 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'RPC parameter data validation', 0, 1053 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'scan for startup procs', 0, 1054 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'server trigger recursion', 1, 1055 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'set working set size', 0, 1056 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'show advanced options', 0, 1057 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'SMO and DMO XPs', 1, 1058 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'SQL Mail XPs', 0, 1059 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'transform noise words', 0, 1060 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'two digit year cutoff', 2049, 1061 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'user connections', 0, 1062 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'user options', 0, 1063 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'Web Assistant Procedures', 0, 1064 ); - INSERT INTO #ConfigurationDefaults - VALUES ( 'xp_cmdshell', 0, 1065 ); + BEGIN + INSERT INTO #ConfigurationDefaults + VALUES ('min server memory (MB)', 0, 1036); + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -6706,6 +6670,45 @@ IF @ProductVersionMajor >= 10 + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 268 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 268) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + DatabaseName , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 268 AS CheckID, + 5 AS Priority, + DB_NAME(ps.database_id), + 'Availability' AS FindingsGroup, + 'AG Replica Falling Behind' AS Finding, + 'https://www.BrentOzar.com/go/ag' AS URL, + ag.name + N' AG replica server ' + + ar.replica_server_name + N' is ' + + CASE WHEN DATEDIFF(SECOND, drs.last_commit_time, ps.last_commit_time) < 200 THEN (CAST(DATEDIFF(SECOND, drs.last_commit_time, ps.last_commit_time) AS NVARCHAR(10)) + N' seconds ') + ELSE (CAST(DATEDIFF(MINUTE, drs.last_commit_time, ps.last_commit_time) AS NVARCHAR(10)) + N' minutes ') END + + N' behind the primary.' + AS details + FROM sys.dm_hadr_database_replica_states AS drs + JOIN sys.availability_replicas AS ar ON drs.replica_id = ar.replica_id + JOIN sys.availability_groups AS ag ON ar.group_id = ag.group_id + JOIN sys.dm_hadr_database_replica_states AS ps + ON drs.group_id = ps.group_id + AND drs.database_id = ps.database_id + AND ps.is_local = 1 /* Primary */ + WHERE drs.is_local = 0 /* Secondary */ + AND DATEDIFF(SECOND, drs.last_commit_time, ps.last_commit_time) > 60; + END; + IF @CheckUserDatabaseObjects = 1 BEGIN @@ -7738,7 +7741,7 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS(SELECT * FROM sys.databases WHERE is_query_store_on = 1) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS(SELECT * FROM sys.databases WHERE is_query_store_on = 1 AND database_id <> 3) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @QueryStoreInUse = 1; END; @@ -7782,9 +7785,16 @@ IF @ProductVersionMajor >= 10 (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), + (37, 'DOP_FEEDBACK', CASE WHEN @ProductVersionMajor >= 17 THEN '1' ELSE '0' END, NULL, 267), (38, 'LEDGER_DIGEST_STORAGE_ENDPOINT', 'OFF', NULL, 267), - (39, 'FORCE_SHOWPLAN_RUNTIME_PARAMETER_COLLECTION', '0', NULL, 267); + (39, 'FORCE_SHOWPLAN_RUNTIME_PARAMETER_COLLECTION', '0', NULL, 267), + (40, 'READABLE_SECONDARY_TEMPORARY_STATS_AUTO_CREATE', '1', NULL, 267), + (41, 'READABLE_SECONDARY_TEMPORARY_STATS_AUTO_UPDATE', '1', NULL, 267), + (42, 'OPTIMIZED_SP_EXECUTESQL', '0', NULL, 267), + (43, 'OPTIMIZED_HALLOWEEN_PROTECTION', '1', NULL, 267), + (44, 'FULLTEXT_INDEX_VERSION', '2', NULL, 267), + (47, 'OPTIONAL_PARAMETER_OPTIMIZATION', '1', NULL, 267), + (48, 'PREVIEW_FEATURES', '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'')) @@ -8558,8 +8568,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT 'Informational' AS FindingsGroup , 'Recommended Trace Flag Off' AS Finding , 'https://www.sqlskills.com/blogs/erin/query-store-trace-flags/' AS URL , - 'Trace Flag 7745 not enabled globally. It makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. It is recommended, but it loses you the non-flushed Query Store data.' AS Details - FROM #TraceStatus T + 'Trace Flag 7745 not enabled globally. It makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. It is recommended, but it loses you the non-flushed Query Store data.' AS Details; END; IF NOT EXISTS ( SELECT 1 @@ -10557,7 +10566,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -11435,7 +11444,7 @@ AS SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -13192,6 +13201,7 @@ ALTER PROCEDURE dbo.sp_BlitzCache @DurationFilter DECIMAL(38,4) = NULL , @HideSummary BIT = 0 , @IgnoreSystemDBs BIT = 1 , + @IgnoreReadableReplicaDBs BIT = 1 , @OnlyQueryHashes VARCHAR(MAX) = NULL , @IgnoreQueryHashes VARCHAR(MAX) = NULL , @OnlySqlHandles VARCHAR(MAX) = NULL , @@ -13218,7 +13228,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -14341,7 +14351,7 @@ CREATE TABLE #plan_usage ); -IF EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') +IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') BEGIN RAISERROR('Checking for Read intent databases to exclude',0,0) WITH NOWAIT; @@ -14760,7 +14770,7 @@ IF @VersionShowsAirQuoteActualPlans = 1 SET @body += N' WHERE 1 = 1 ' + @nl ; - IF EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') + IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') BEGIN RAISERROR(N'Ignoring readable secondaries databases by default', 0, 1) WITH NOWAIT; SET @body += N' AND CAST(xpa.value AS INT) NOT IN (SELECT database_id FROM #ReadableDBs)' + @nl ; @@ -20604,7 +20614,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -27394,7 +27404,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF @VersionCheckMode = 1 BEGIN @@ -28607,7 +28617,7 @@ BEGIN END; /* If table target */ - IF @TargetSessionType = 'table' + IF LOWER(@TargetSessionType) = N'table' BEGIN SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Inserting to #deadlock_data from table source %s', 0, 1, @d) WITH NOWAIT; @@ -28625,9 +28635,19 @@ BEGIN 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) + FROM ' + + QUOTENAME(@TargetDatabaseName) + + N'.' + + QUOTENAME(@TargetSchemaName) + + N'.' + + QUOTENAME(@TargetTableName) + + N' AS x + OUTER APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/event'') AS xe(e) + OUTER APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/deadlock'') AS xd(e) OPTION(RECOMPILE); '; @@ -28764,6 +28784,7 @@ BEGIN LEFT JOIN #t AS t ON 1 = 1 WHERE @xe = 1 + OR LOWER(@TargetSessionType) <> N'table' UNION ALL @@ -31927,7 +31948,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -33340,7 +33361,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -35002,14 +35023,15 @@ ALTER PROCEDURE [dbo].[sp_ineachdb] @Version varchar(30) = NULL OUTPUT, @VersionDate datetime = NULL OUTPUT, @VersionCheckMode bit = 0, - @is_ag_writeable_copy bit = 0 + @is_ag_writeable_copy bit = 0, + @is_query_store_on bit = 0 -- WITH EXECUTE AS OWNER – maybe not a great idea, depending on the security of your system AS BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -35208,6 +35230,23 @@ OPTION (MAXRECURSION 0); ) ); + -- delete any databases that don't match query store criteria + IF @SQLVersion >= 13 + BEGIN + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 + FROM sys.databases AS d + WHERE d.database_id = dbs.id + AND NOT + ( + is_query_store_on = COALESCE(@is_query_store_on, is_query_store_on) + AND NOT (@is_query_store_on = 1 AND d.database_id = 3) OR (@is_query_store_on = 0 AND d.database_id = 3) -- Excluding the model database which shows QS enabled in SQL2022+ + ) + ); + END + -- if a user access is specified, remove any that are NOT in that state IF @user_access IN (N'SINGLE_USER', N'MULTI_USER', N'RESTRICTED_USER') BEGIN @@ -35372,6 +35411,10 @@ INSERT INTO dbo.SqlServerVersions (MajorVersionNumber, MinorVersionNumber, Branch, [Url], ReleaseDate, MainstreamSupportEndDate, ExtendedSupportEndDate, MajorVersionName, MinorVersionName) VALUES /*2022*/ + (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.1'), + (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.0'), + /*2022*/ + (16, 4195, 'CU19', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate19', '2025-05-19', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19'), (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'), @@ -35855,7 +35898,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -37446,7 +37489,7 @@ BEGIN 'Maintenance Tasks Running' AS FindingGroup, 'Restore Running' AS Finding, 'https://www.brentozar.com/askbrent/backups/' AS URL, - 'Restore of ' + DB_NAME(db.resource_database_id) + ' database (' + (SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id) + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, + 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, pl.query_plan AS QueryPlan, r.start_time AS StartTime, @@ -37454,14 +37497,14 @@ BEGIN s.nt_user_name AS NTUserName, s.[program_name] AS ProgramName, s.[host_name] AS HostName, - db.[resource_database_id] AS DatabaseID, - DB_NAME(db.resource_database_id) AS DatabaseName, + COALESCE(db.[resource_database_id],0) AS DatabaseID, + COALESCE(DB_NAME(db.resource_database_id), 'Unknown') AS DatabaseName, 0 AS OpenTransactionCount, r.query_hash FROM sys.dm_exec_requests r INNER JOIN sys.dm_exec_connections c ON r.session_id = c.session_id INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id - INNER JOIN ( + LEFT OUTER JOIN ( SELECT DISTINCT request_session_id, resource_database_id FROM sys.dm_tran_locks WHERE resource_type = N'DATABASE' @@ -37829,6 +37872,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, FROM sys.databases WHERE database_id > 4; + /* Server Info - Memory Grants pending - CheckID 39 */ IF (@Debug = 1) BEGIN @@ -38888,6 +38932,28 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END; + /* Query Problems - Deadlocks - CheckID 51 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 51',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 51 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Deadlocks' AS Finding, + ' https://www.brentozar.com/go/deadlocks' AS URL, + 'Number of deadlocks during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + + 'Determined by sampling Perfmon counter ' + ps.object_name + ' - ' + ps.counter_name + @LineFeed AS Details, + 'Check sp_BlitzLock to find which indexes and queries to tune.' AS HowToStopIt + FROM #PerfmonStats ps + WHERE ps.Pass = 2 + AND counter_name = 'Number of Deadlocks/sec' + AND instance_name LIKE '_Total%' + AND value_delta > 0; + + /* SQL Server Internal Maintenance - Log File Growing - CheckID 13 */ IF (@Debug = 1) BEGIN @@ -39133,6 +39199,53 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, OR max_session_percent >= 90); END + /* Server Info - Thread Time - CheckID 50 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 50',10,1) WITH NOWAIT; + END + + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT TOP 1 + 50 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Thread Time' AS Finding, + LTRIM( + CASE + WHEN c.[TotalThreadTimeSeconds] >= 86400 THEN + CAST(c.[TotalThreadTimeSeconds] / 86400 AS VARCHAR) + 'd ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 86400 >= 3600 THEN + CAST((c.[TotalThreadTimeSeconds] % 86400) / 3600 AS VARCHAR) + 'h ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 3600 >= 60 THEN + CAST((c.[TotalThreadTimeSeconds] % 3600) / 60 AS VARCHAR) + 'm ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 60 > 0 OR c.[TotalThreadTimeSeconds] = 0 THEN + CAST(c.[TotalThreadTimeSeconds] % 60 AS VARCHAR) + 's' + ELSE '' + END + ) AS Details, + CAST(c.[TotalThreadTimeSeconds] AS DECIMAL(18,1)) AS DetailsInt, + 'https://www.brentozar.com/go/threadtime' AS URL + FROM max_batch b + JOIN #WaitStats wd2 ON wd2.SampleTime = b.SampleTime + JOIN #WaitStats wd1 ON wd1.wait_type = wd2.wait_type AND wd2.SampleTime > wd1.SampleTime + CROSS APPLY ( + SELECT CAST((wd2.thread_time_ms - wd1.thread_time_ms) / 1000 AS INT) AS TotalThreadTimeSeconds + ) AS c; + /* Server Info - Batch Requests per Sec - CheckID 19 */ IF (@Debug = 1) BEGIN @@ -40462,13 +40575,13 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, CAST(c.[Wait Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Wait Time (Hours)], - CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], - (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 THEN CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait] + ELSE 0 END AS [Avg ms Per Wait], + CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], + (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits] FROM max_batch b JOIN #WaitStats wd2 ON wd2.SampleTime =b.SampleTime @@ -40607,17 +40720,17 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, CAST(c.[Wait Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Wait Time (Hours)], + CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + THEN + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ + (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) + ELSE 0 END AS [Avg ms Per Wait], CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], CAST(c.[Signal Wait Time (Seconds)] / 60.0 / 60 AS DECIMAL(18,1)) AS [Signal Wait Time (Hours)], CASE WHEN c.[Wait Time (Seconds)] > 0 THEN CAST(100.*(c.[Signal Wait Time (Seconds)]/c.[Wait Time (Seconds)]) AS NUMERIC(4,1)) ELSE 0 END AS [Percent Signal Waits], (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], - CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - THEN - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ - (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait], N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON @@ -40651,17 +40764,17 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, c.[Wait Time (Seconds)], + CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + THEN + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ + (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) + ELSE 0 END AS [Avg ms Per Wait], CAST((CAST(wd2.wait_time_ms - wd1.wait_time_ms AS MONEY)) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Second], c.[Signal Wait Time (Seconds)], CASE WHEN c.[Wait Time (Seconds)] > 0 THEN CAST(100.*(c.[Signal Wait Time (Seconds)]/c.[Wait Time (Seconds)]) AS NUMERIC(4,1)) ELSE 0 END AS [Percent Signal Waits], (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], - CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - THEN - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ - (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait], N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON diff --git a/Install-Azure.sql b/Install-Azure.sql index bed3ddb87..fbbe2acf5 100644 --- a/Install-Azure.sql +++ b/Install-Azure.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -1147,6 +1147,7 @@ ALTER PROCEDURE dbo.sp_BlitzCache @DurationFilter DECIMAL(38,4) = NULL , @HideSummary BIT = 0 , @IgnoreSystemDBs BIT = 1 , + @IgnoreReadableReplicaDBs BIT = 1 , @OnlyQueryHashes VARCHAR(MAX) = NULL , @IgnoreQueryHashes VARCHAR(MAX) = NULL , @OnlySqlHandles VARCHAR(MAX) = NULL , @@ -1173,7 +1174,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -2296,7 +2297,7 @@ CREATE TABLE #plan_usage ); -IF EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') +IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') BEGIN RAISERROR('Checking for Read intent databases to exclude',0,0) WITH NOWAIT; @@ -2715,7 +2716,7 @@ IF @VersionShowsAirQuoteActualPlans = 1 SET @body += N' WHERE 1 = 1 ' + @nl ; - IF EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') + IF @IgnoreReadableReplicaDBs = 1 AND EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') BEGIN RAISERROR(N'Ignoring readable secondaries databases by default', 0, 1) WITH NOWAIT; SET @body += N' AND CAST(xpa.value AS INT) NOT IN (SELECT database_id FROM #ReadableDBs)' + @nl ; @@ -8557,7 +8558,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -10148,7 +10149,7 @@ BEGIN 'Maintenance Tasks Running' AS FindingGroup, 'Restore Running' AS Finding, 'https://www.brentozar.com/askbrent/backups/' AS URL, - 'Restore of ' + DB_NAME(db.resource_database_id) + ' database (' + (SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id) + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, + 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, pl.query_plan AS QueryPlan, r.start_time AS StartTime, @@ -10156,14 +10157,14 @@ BEGIN s.nt_user_name AS NTUserName, s.[program_name] AS ProgramName, s.[host_name] AS HostName, - db.[resource_database_id] AS DatabaseID, - DB_NAME(db.resource_database_id) AS DatabaseName, + COALESCE(db.[resource_database_id],0) AS DatabaseID, + COALESCE(DB_NAME(db.resource_database_id), 'Unknown') AS DatabaseName, 0 AS OpenTransactionCount, r.query_hash FROM sys.dm_exec_requests r INNER JOIN sys.dm_exec_connections c ON r.session_id = c.session_id INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id - INNER JOIN ( + LEFT OUTER JOIN ( SELECT DISTINCT request_session_id, resource_database_id FROM sys.dm_tran_locks WHERE resource_type = N'DATABASE' @@ -10531,6 +10532,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, FROM sys.databases WHERE database_id > 4; + /* Server Info - Memory Grants pending - CheckID 39 */ IF (@Debug = 1) BEGIN @@ -11590,6 +11592,28 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END; + /* Query Problems - Deadlocks - CheckID 51 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 51',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 51 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Deadlocks' AS Finding, + ' https://www.brentozar.com/go/deadlocks' AS URL, + 'Number of deadlocks during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + + 'Determined by sampling Perfmon counter ' + ps.object_name + ' - ' + ps.counter_name + @LineFeed AS Details, + 'Check sp_BlitzLock to find which indexes and queries to tune.' AS HowToStopIt + FROM #PerfmonStats ps + WHERE ps.Pass = 2 + AND counter_name = 'Number of Deadlocks/sec' + AND instance_name LIKE '_Total%' + AND value_delta > 0; + + /* SQL Server Internal Maintenance - Log File Growing - CheckID 13 */ IF (@Debug = 1) BEGIN @@ -11835,6 +11859,53 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, OR max_session_percent >= 90); END + /* Server Info - Thread Time - CheckID 50 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 50',10,1) WITH NOWAIT; + END + + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT TOP 1 + 50 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Thread Time' AS Finding, + LTRIM( + CASE + WHEN c.[TotalThreadTimeSeconds] >= 86400 THEN + CAST(c.[TotalThreadTimeSeconds] / 86400 AS VARCHAR) + 'd ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 86400 >= 3600 THEN + CAST((c.[TotalThreadTimeSeconds] % 86400) / 3600 AS VARCHAR) + 'h ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 3600 >= 60 THEN + CAST((c.[TotalThreadTimeSeconds] % 3600) / 60 AS VARCHAR) + 'm ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 60 > 0 OR c.[TotalThreadTimeSeconds] = 0 THEN + CAST(c.[TotalThreadTimeSeconds] % 60 AS VARCHAR) + 's' + ELSE '' + END + ) AS Details, + CAST(c.[TotalThreadTimeSeconds] AS DECIMAL(18,1)) AS DetailsInt, + 'https://www.brentozar.com/go/threadtime' AS URL + FROM max_batch b + JOIN #WaitStats wd2 ON wd2.SampleTime = b.SampleTime + JOIN #WaitStats wd1 ON wd1.wait_type = wd2.wait_type AND wd2.SampleTime > wd1.SampleTime + CROSS APPLY ( + SELECT CAST((wd2.thread_time_ms - wd1.thread_time_ms) / 1000 AS INT) AS TotalThreadTimeSeconds + ) AS c; + /* Server Info - Batch Requests per Sec - CheckID 19 */ IF (@Debug = 1) BEGIN @@ -13164,13 +13235,13 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, CAST(c.[Wait Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Wait Time (Hours)], - CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], - (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 THEN CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait] + ELSE 0 END AS [Avg ms Per Wait], + CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], + (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits] FROM max_batch b JOIN #WaitStats wd2 ON wd2.SampleTime =b.SampleTime @@ -13309,17 +13380,17 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, CAST(c.[Wait Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Wait Time (Hours)], + CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + THEN + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ + (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) + ELSE 0 END AS [Avg ms Per Wait], CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Hour], CAST(c.[Signal Wait Time (Seconds)] / 60.0 / 60 AS DECIMAL(18,1)) AS [Signal Wait Time (Hours)], CASE WHEN c.[Wait Time (Seconds)] > 0 THEN CAST(100.*(c.[Signal Wait Time (Seconds)]/c.[Wait Time (Seconds)]) AS NUMERIC(4,1)) ELSE 0 END AS [Percent Signal Waits], (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], - CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - THEN - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ - (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait], N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON @@ -13353,17 +13424,17 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, c.[Wait Time (Seconds)], + CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + THEN + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ + (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) + ELSE 0 END AS [Avg ms Per Wait], CAST((CAST(wd2.wait_time_ms - wd1.wait_time_ms AS MONEY)) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Second], c.[Signal Wait Time (Seconds)], CASE WHEN c.[Wait Time (Seconds)] > 0 THEN CAST(100.*(c.[Signal Wait Time (Seconds)]/c.[Wait Time (Seconds)]) AS NUMERIC(4,1)) ELSE 0 END AS [Percent Signal Waits], (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], - CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - THEN - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ - (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) - ELSE 0 END AS [Avg ms Per Wait], N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON @@ -13567,7 +13638,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -20357,7 +20428,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF @VersionCheckMode = 1 BEGIN @@ -21570,7 +21641,7 @@ BEGIN END; /* If table target */ - IF @TargetSessionType = 'table' + IF LOWER(@TargetSessionType) = N'table' BEGIN SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Inserting to #deadlock_data from table source %s', 0, 1, @d) WITH NOWAIT; @@ -21588,9 +21659,19 @@ BEGIN 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) + FROM ' + + QUOTENAME(@TargetDatabaseName) + + N'.' + + QUOTENAME(@TargetSchemaName) + + N'.' + + QUOTENAME(@TargetTableName) + + N' AS x + OUTER APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/event'') AS xe(e) + OUTER APPLY x.' + + QUOTENAME(@TargetColumnName) + + N'.nodes(''/deadlock'') AS xd(e) OPTION(RECOMPILE); '; @@ -21727,6 +21808,7 @@ BEGIN LEFT JOIN #t AS t ON 1 = 1 WHERE @xe = 1 + OR LOWER(@TargetSessionType) <> N'table' UNION ALL @@ -24890,7 +24972,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN diff --git a/SqlServerVersions.sql b/SqlServerVersions.sql index 4866d6e22..94ae4a0b0 100644 --- a/SqlServerVersions.sql +++ b/SqlServerVersions.sql @@ -42,6 +42,10 @@ INSERT INTO dbo.SqlServerVersions (MajorVersionNumber, MinorVersionNumber, Branch, [Url], ReleaseDate, MainstreamSupportEndDate, ExtendedSupportEndDate, MajorVersionName, MinorVersionName) VALUES /*2022*/ + (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.1'), + (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.0'), + /*2022*/ + (16, 4195, 'CU19', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate19', '2025-05-19', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19'), (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'), diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 58c97a514..e15695fe4 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -38,7 +38,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzAnalysis.sql b/sp_BlitzAnalysis.sql index 96216408c..b30dedd22 100644 --- a/sp_BlitzAnalysis.sql +++ b/sp_BlitzAnalysis.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzBackups.sql b/sp_BlitzBackups.sql index 5f33342e4..822d79f53 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.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index 655f855b5..19e02afe3 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -283,7 +283,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index fe1301bc4..28a20f9e6 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.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN @@ -3359,22 +3359,41 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, FROM #WaitStats ) INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT TOP 1 50 AS CheckID, + SELECT TOP 1 + 50 AS CheckID, 251 AS Priority, 'Server Info' AS FindingGroup, 'Thread Time' AS Finding, - CAST(CAST(c.[Total Thread Time (Seconds)] AS DECIMAL(18,1)) AS VARCHAR(100)) AS Details, - CAST(c.[Total Thread Time (Seconds)] AS DECIMAL(18,1)) AS DetailsInt, + LTRIM( + CASE + WHEN c.[TotalThreadTimeSeconds] >= 86400 THEN + CAST(c.[TotalThreadTimeSeconds] / 86400 AS VARCHAR) + 'd ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 86400 >= 3600 THEN + CAST((c.[TotalThreadTimeSeconds] % 86400) / 3600 AS VARCHAR) + 'h ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 3600 >= 60 THEN + CAST((c.[TotalThreadTimeSeconds] % 3600) / 60 AS VARCHAR) + 'm ' + ELSE '' + END + + CASE + WHEN c.[TotalThreadTimeSeconds] % 60 > 0 OR c.[TotalThreadTimeSeconds] = 0 THEN + CAST(c.[TotalThreadTimeSeconds] % 60 AS VARCHAR) + 's' + ELSE '' + END + ) AS Details, + CAST(c.[TotalThreadTimeSeconds] AS DECIMAL(18,1)) AS DetailsInt, 'https://www.brentozar.com/go/threadtime' AS URL - FROM max_batch b - JOIN #WaitStats wd2 ON - wd2.SampleTime =b.SampleTime - JOIN #WaitStats wd1 ON - wd1.wait_type=wd2.wait_type AND - wd2.SampleTime > wd1.SampleTime - CROSS APPLY (SELECT - CAST((wd2.thread_time_ms - wd1.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] - ) AS c; + FROM max_batch b + JOIN #WaitStats wd2 ON wd2.SampleTime = b.SampleTime + JOIN #WaitStats wd1 ON wd1.wait_type = wd2.wait_type AND wd2.SampleTime > wd1.SampleTime + CROSS APPLY ( + SELECT CAST((wd2.thread_time_ms - wd1.thread_time_ms) / 1000 AS INT) AS TotalThreadTimeSeconds + ) AS c; /* Server Info - Batch Requests per Sec - CheckID 19 */ IF (@Debug = 1) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 365cbe6c6..629cf39ca 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.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index 52361d583..c0a4589d6 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.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF @VersionCheckMode = 1 BEGIN diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index 6bb2df72f..5c49699e9 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.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_DatabaseRestore.sql b/sp_DatabaseRestore.sql index b685a0d08..f4b5cb858 100755 --- a/sp_DatabaseRestore.sql +++ b/sp_DatabaseRestore.sql @@ -58,7 +58,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.24', @VersionDate = '20250407'; +SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index bf9b147b4..a477eb9c3 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -37,7 +37,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.24', @VersionDate = '20250407'; + SELECT @Version = '8.25', @VersionDate = '20250704'; IF(@VersionCheckMode = 1) BEGIN From d2f338068d17fdbb4eadaf7cbe6f36bcf04f56cb Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Fri, 4 Jul 2025 06:41:41 -0700 Subject: [PATCH 23/76] #3662 remove unit tests Closes #3662. --- tests/run-tests.ps1 | 14 ----------- tests/sp_Blitz.tests.ps1 | 10 -------- tests/sp_BlitzAnalysis.tests.ps1 | 15 ------------ tests/sp_BlitzBackups.tests.ps1 | 20 ---------------- tests/sp_BlitzCache.tests.ps1 | 13 ----------- tests/sp_BlitzFirst.tests.ps1 | 40 -------------------------------- tests/sp_BlitzIndex.tests.ps1 | 10 -------- tests/sp_BlitzLock.tests.ps1 | 11 --------- tests/sp_BlitzWho.tests.ps1 | 15 ------------ 9 files changed, 148 deletions(-) delete mode 100644 tests/run-tests.ps1 delete mode 100644 tests/sp_Blitz.tests.ps1 delete mode 100644 tests/sp_BlitzAnalysis.tests.ps1 delete mode 100644 tests/sp_BlitzBackups.tests.ps1 delete mode 100644 tests/sp_BlitzCache.tests.ps1 delete mode 100644 tests/sp_BlitzFirst.tests.ps1 delete mode 100644 tests/sp_BlitzIndex.tests.ps1 delete mode 100644 tests/sp_BlitzLock.tests.ps1 delete mode 100644 tests/sp_BlitzWho.tests.ps1 diff --git a/tests/run-tests.ps1 b/tests/run-tests.ps1 deleted file mode 100644 index c16ea15f5..000000000 --- a/tests/run-tests.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -# Assign default values if script-scoped variables are not set -$ServerInstance = if ($null -ne $script:ServerInstance) { $script:ServerInstance } else { "localhost" } -$UserName = if ($null -ne $script:UserName) { $script:UserName } else { "sa" } -$Password = if ($null -ne $script:Password) { $script:Password } else { "dbatools.I0" } -$TrustServerCertificate = if ($null -ne $script:TrustServerCertificate) { $script:TrustServerCertificate } else { $true } - -$PSDefaultParameterValues = @{ - "*:ServerInstance" = $ServerInstance - "*:UserName" = $UserName - "*:Password" = $Password - "*:TrustServerCertificate" = $TrustServerCertificate -} - -Invoke-Pester -PassThru \ No newline at end of file diff --git a/tests/sp_Blitz.tests.ps1 b/tests/sp_Blitz.tests.ps1 deleted file mode 100644 index 341b9c348..000000000 --- a/tests/sp_Blitz.tests.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -Describe "sp_Blitz Tests" { - - It "sp_Blitz Check" { - $results = Invoke-SqlCmd -Query "EXEC dbo.sp_Blitz" -OutputAs DataSet - $results.Tables.Count | Should -Be 1 - $results.Tables[0].Columns.Count | Should -Be 9 - $results.Tables[0].Rows.Count | Should -BeGreaterThan 0 - } - -} diff --git a/tests/sp_BlitzAnalysis.tests.ps1 b/tests/sp_BlitzAnalysis.tests.ps1 deleted file mode 100644 index f346b6197..000000000 --- a/tests/sp_BlitzAnalysis.tests.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -Describe "sp_BlitzAnalysis Tests" { - - It "sp_BlitzAnalysis Check" { - - # Run sp_BlitzFirst to populate the tables used by sp_BlitzAnalysis - Invoke-SqlCmd -Query "EXEC dbo.sp_BlitzFirst @OutputDatabaseName = 'tempdb', @OutputSchemaName = N'dbo', @OutputTableName = N'BlitzFirst', @OutputTableNameFileStats = N'BlitzFirst_FileStats',@OutputTableNamePerfmonStats = N'BlitzFirst_PerfmonStats', - @OutputTableNameWaitStats = N'BlitzFirst_WaitStats', - @OutputTableNameBlitzCache = N'BlitzCache', - @OutputTableNameBlitzWho= N'BlitzWho'" - - $results = Invoke-SqlCmd -Query "EXEC dbo.sp_BlitzAnalysis @OutputDatabaseName = 'tempdb'" -OutputAs DataSet - $results.Tables.Count | Should -BeGreaterThan 6 - } - -} diff --git a/tests/sp_BlitzBackups.tests.ps1 b/tests/sp_BlitzBackups.tests.ps1 deleted file mode 100644 index 162e869fb..000000000 --- a/tests/sp_BlitzBackups.tests.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -Describe "sp_BlitzBackups Tests" { - - It "sp_BlitzBackups Check" { - # Give sp_BlitzBackups something to capture by performing a dummy backup of model DB - # Test to be run in GitHub action but backing up model to NUL should be safe on most systems - Invoke-SqlCmd -Query "BACKUP DATABASE model TO DISK='NUL'" - $results = Invoke-SqlCmd -Query "EXEC dbo.sp_BlitzBackups" -OutputAs DataSet - $results.Tables.Count | Should -Be 3 - - $results.Tables[0].Columns.Count | Should -Be 39 - $results.Tables[0].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[1].Columns.Count | Should -Be 32 - $results.Tables[1].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[2].Columns.Count | Should -Be 5 - $results.Tables[2].Rows.Count | Should -BeGreaterThan 0 - } - -} diff --git a/tests/sp_BlitzCache.tests.ps1 b/tests/sp_BlitzCache.tests.ps1 deleted file mode 100644 index 4483e090b..000000000 --- a/tests/sp_BlitzCache.tests.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -Describe "sp_BlitzCache Tests" { - - It "sp_BlitzCache Check" { - # Note: Added 'SELECT 1 AS A' as an empty first resultset causes issues returning the full DataSet - $results = Invoke-SqlCmd -Query "SELECT 1 AS A;EXEC dbo.sp_BlitzCache" -OutputAs DataSet - # Adjust table count to get the actual tables returned from sp_BlitzCache (So reporting isn't confusing) - $tableCount = $results.Tables.Count -1 - $tableCount | Should -Be 2 - $results.Tables[1].Columns.Count | Should -Be 43 - $results.Tables[2].Columns.Count | Should -Be 6 - $results.Tables[2].Rows.Count | Should -BeGreaterThan 0 - } -} \ No newline at end of file diff --git a/tests/sp_BlitzFirst.tests.ps1 b/tests/sp_BlitzFirst.tests.ps1 deleted file mode 100644 index d2bb42d3f..000000000 --- a/tests/sp_BlitzFirst.tests.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -Describe "sp_BlitzFirst Tests" { - - It "sp_BlitzFirst Check" { - # Give sp_BlitzFirst something to capture - Start-Job -ScriptBlock { - Invoke-SqlCmd -Query "WAITFOR DELAY '00:00:15'" -ServerInstance $using:ServerInstance -Username $using:UserName -Password $using:Password -TrustServerCertificate:$using:TrustServerCertificate - } - Start-Sleep -Milliseconds 1000 - $results = Invoke-SqlCmd -Query "EXEC dbo.sp_BlitzFirst" -OutputAs DataSet - $results.Tables.Count | Should -Be 1 - $results.Tables[0].Columns.Count | Should -Be 8 - $results.Tables[0].Rows.Count | Should -BeGreaterThan 0 - - $results = Invoke-SqlCmd -Query "EXEC dbo.sp_BlitzFirst @ExpertMode=1" -OutputAs DataSet - $results.Tables.Count | Should -Be 7 - - $results.Tables[0].Columns.Count | Should -Be 21 - $results.Tables[0].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[1].Columns.Count | Should -Be 40 - $results.Tables[1].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[2].Columns.Count | Should -Be 13 - $results.Tables[2].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[3].Columns.Count | Should -Be 11 - $results.Tables[3].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[4].Columns.Count | Should -Be 10 - $results.Tables[4].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[5].Columns.Count | Should -Be 4 - $results.Tables[5].Rows.Count | Should -BeGreaterThan 0 - - $results.Tables[6].Columns.Count | Should -Be 21 - $results.Tables[6].Rows.Count | Should -BeGreaterThan 0 - - } - -} \ No newline at end of file diff --git a/tests/sp_BlitzIndex.tests.ps1 b/tests/sp_BlitzIndex.tests.ps1 deleted file mode 100644 index 63c479ad3..000000000 --- a/tests/sp_BlitzIndex.tests.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -Describe "sp_BlitzIndex Tests" { - - It "sp_BlitzIndex Check" { - $results = Invoke-SqlCmd -Query "EXEC dbo.sp_BlitzIndex" -OutputAs DataSet - $results.Tables.Count | Should -Be 1 - $results.Tables[0].Columns.Count | Should -Be 12 - $results.Tables[0].Rows.Count | Should -BeGreaterThan 0 - } - -} \ No newline at end of file diff --git a/tests/sp_BlitzLock.tests.ps1 b/tests/sp_BlitzLock.tests.ps1 deleted file mode 100644 index 5368e3283..000000000 --- a/tests/sp_BlitzLock.tests.ps1 +++ /dev/null @@ -1,11 +0,0 @@ -Describe "sp_BlitzLock Tests" { - - It "sp_BlitzLock Check" { - # Note: Added 'SELECT 1 AS A' as an empty first resultset causes issues returning the full DataSet - $results = Invoke-SqlCmd -Query "SELECT 1 AS A;EXEC dbo.sp_BlitzLock" -OutputAs DataSet - # Adjust table count to get the actual tables returned from sp_BlitzLock (So reporting isn't confusing) - $tableCount = $results.Tables.Count - 1 - $tableCount | Should -Be 3 - } - -} \ No newline at end of file diff --git a/tests/sp_BlitzWho.tests.ps1 b/tests/sp_BlitzWho.tests.ps1 deleted file mode 100644 index 3e9febd56..000000000 --- a/tests/sp_BlitzWho.tests.ps1 +++ /dev/null @@ -1,15 +0,0 @@ -Describe "sp_BlitzWho Tests" { - - It "sp_BlitzWho Check" { - # Give sp_BlitzWho something to capture - Start-Job -ScriptBlock { - Invoke-SqlCmd -Query "WAITFOR DELAY '00:00:15'" -ServerInstance $using:ServerInstance -Username $using:UserName -Password $using:Password -TrustServerCertificate:$using:TrustServerCertificate - } - Start-Sleep -Milliseconds 1000 - $results = Invoke-SqlCmd -Query "EXEC dbo.sp_BlitzWho" -OutputAs DataSet - $results.Tables.Count | Should -Be 1 - $results.Tables[0].Columns.Count | Should -Be 21 - $results.Tables[0].Rows.Count | Should -BeGreaterThan 0 - } - -} \ No newline at end of file From 60cee45c443a166a70712ed759d3341b6c39709b Mon Sep 17 00:00:00 2001 From: rodik Date: Wed, 16 Jul 2025 14:52:31 +0200 Subject: [PATCH 24/76] look for existing output table in sys.schemas --- sp_BlitzLock.sql | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index c0a4589d6..bf97f1977 100644 --- a/sp_BlitzLock.sql +++ b/sp_BlitzLock.sql @@ -589,19 +589,17 @@ BEGIN @StringToExecute = N'SELECT @r = o.name FROM ' + @OutputDatabaseName + - N'.sys.objects AS o WHERE o.type_desc = N''USER_TABLE'' AND o.name = ' + + N'.sys.objects AS o inner join ' + + @OutputDatabaseName + + N'.sys.schemas as s on o.schema_id = s.schema_id WHERE o.type_desc = N''USER_TABLE'' AND o.name = ' + QUOTENAME ( @OutputTableName, N'''' ) + - N' AND o.schema_id = SCHEMA_ID(' + - QUOTENAME - ( - @OutputSchemaName, - N'''' - ) + - N');', + N' AND s.name =''' + + @OutputSchemaName + + N''';', @StringToExecuteParams = N'@r sysname OUTPUT'; From 802f35f718b84486e5d011bb8e9f55c8b3afde1c Mon Sep 17 00:00:00 2001 From: rodik Date: Wed, 16 Jul 2025 14:55:05 +0200 Subject: [PATCH 25/76] add explicit schema to synonym --- sp_BlitzLock.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index bf97f1977..bd280b174 100644 --- a/sp_BlitzLock.sql +++ b/sp_BlitzLock.sql @@ -841,12 +841,12 @@ BEGIN ) BEGIN RAISERROR('Found synonym DeadlockFindings, dropping', 0, 1) WITH NOWAIT; - DROP SYNONYM DeadlockFindings; + DROP SYNONYM dbo.DeadlockFindings; END; RAISERROR('Creating synonym DeadlockFindings', 0, 1) WITH NOWAIT; SET @StringToExecute = - N'CREATE SYNONYM DeadlockFindings FOR ' + + N'CREATE SYNONYM dbo.DeadlockFindings FOR ' + @OutputDatabaseName + N'.' + @OutputSchemaName + @@ -868,12 +868,12 @@ BEGIN ) BEGIN RAISERROR('Found synonym DeadLockTbl, dropping', 0, 1) WITH NOWAIT; - DROP SYNONYM DeadLockTbl; + DROP SYNONYM dbo.DeadLockTbl; END; RAISERROR('Creating synonym DeadLockTbl', 0, 1) WITH NOWAIT; SET @StringToExecute = - N'CREATE SYNONYM DeadLockTbl FOR ' + + N'CREATE SYNONYM dbo.DeadLockTbl FOR ' + @OutputDatabaseName + N'.' + @OutputSchemaName + @@ -4103,7 +4103,7 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - DROP SYNONYM DeadLockTbl; + DROP SYNONYM dbo.DeadLockTbl; SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Findings to table %s', 0, 1, @d) WITH NOWAIT; @@ -4133,7 +4133,7 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - DROP SYNONYM DeadlockFindings; /*done with inserting.*/ + DROP SYNONYM dbo.DeadlockFindings; /*done with inserting.*/ END; ELSE /*Output to database is not set output to client app*/ BEGIN From b7e32756da2518b36a6ccc87eaa594a1c0b8c078 Mon Sep 17 00:00:00 2001 From: Ben <60398078+bwiggin10@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:17:40 -0400 Subject: [PATCH 26/76] Set @is_query_store_on Default Value to NULL Changes the default value of parameter @is_query_store_on from 0 to NULL to avoid excluding non-query store databases by default. --- sp_ineachdb.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index a477eb9c3..5f81ee422 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -30,7 +30,7 @@ ALTER PROCEDURE [dbo].[sp_ineachdb] @VersionDate datetime = NULL OUTPUT, @VersionCheckMode bit = 0, @is_ag_writeable_copy bit = 0, - @is_query_store_on bit = 0 + @is_query_store_on bit = NULL -- WITH EXECUTE AS OWNER – maybe not a great idea, depending on the security of your system AS BEGIN From 2aa89ef0a18b94cdd8e9c8289a306eda2cf87392 Mon Sep 17 00:00:00 2001 From: Dirk Hondong Date: Tue, 22 Jul 2025 15:47:16 +0200 Subject: [PATCH 27/76] additional wildcard in where condition check 260 / 261 see https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/3673 in rare cases we can get "SQL Server-Agent" as a result back instead of "SQL Server Agent" This will then cause the error "Subquery returned more than 1 value. This is not permitted when the subquery follows = .. " --- sp_Blitz.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index e15695fe4..f1e92f4c1 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -9867,7 +9867,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) FROM [sys].[dm_server_services] WHERE [servicename] LIKE 'SQL Server%' - AND [servicename] NOT LIKE 'SQL Server Agent%' + AND [servicename] NOT LIKE 'SQL Server%Agent%' AND [servicename] NOT LIKE 'SQL Server Launchpad%')) BEGIN INSERT INTO #BlitzResults @@ -9913,7 +9913,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 FROM #localadmins WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) FROM [sys].[dm_server_services] - WHERE [servicename] LIKE 'SQL Server Agent%' + WHERE [servicename] LIKE 'SQL Server%Agent%' AND [servicename] NOT LIKE 'SQL Server Launchpad%')) BEGIN INSERT INTO #BlitzResults @@ -9946,7 +9946,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 FROM #localadmins WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) FROM [sys].[dm_server_services] - WHERE [servicename] LIKE 'SQL Server Agent%' + WHERE [servicename] LIKE 'SQL Server%Agent%' AND [servicename] NOT LIKE 'SQL Server Launchpad%')) BEGIN INSERT INTO #BlitzResults From f712b794aab846e1075259cd155cdf78eb7ea4e9 Mon Sep 17 00:00:00 2001 From: Dirk Hondong Date: Tue, 22 Jul 2025 16:05:21 +0200 Subject: [PATCH 28/76] check 261 fix temp table for "can't piggyback" as mentioned in comment here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/3673#issuecomment-3102778402 if the script has to "piggyback" here, it now uses the proper temp table --- sp_Blitz.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index f1e92f4c1..4e3dbaccf 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -9939,11 +9939,11 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 /*had to use a different table name because SQL Server/SSMS complains when parsing that the table still exists when it gets to the create part*/ IF OBJECT_ID('tempdb..#localadminsag') IS NOT NULL DROP TABLE #localadminsag; CREATE TABLE #localadminsag (cmdshell_output NVARCHAR(1000)); - INSERT INTO #localadmins + INSERT INTO #localadminsag EXEC /**/xp_cmdshell/**/ N'net localgroup administrators' /* added comments around command since some firewalls block this string TL 20210221 */ IF EXISTS (SELECT 1 - FROM #localadmins + FROM #localadminsag WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) FROM [sys].[dm_server_services] WHERE [servicename] LIKE 'SQL Server%Agent%' From 77157606af458601ac572d6fe4d4b6bedd6068d3 Mon Sep 17 00:00:00 2001 From: Dirk Hondong Date: Wed, 23 Jul 2025 13:23:14 +0200 Subject: [PATCH 29/76] determine German OS for xp_cmdshell call Check 260 and 261 this commit adresses issue 3673 where the "net localgroups administrators" command will not return the proper information since the naming is different in an German OS --- sp_Blitz.sql | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 4e3dbaccf..521b16f14 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -9939,8 +9939,17 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 /*had to use a different table name because SQL Server/SSMS complains when parsing that the table still exists when it gets to the create part*/ IF OBJECT_ID('tempdb..#localadminsag') IS NOT NULL DROP TABLE #localadminsag; CREATE TABLE #localadminsag (cmdshell_output NVARCHAR(1000)); - INSERT INTO #localadminsag - EXEC /**/xp_cmdshell/**/ N'net localgroup administrators' /* added comments around command since some firewalls block this string TL 20210221 */ + /* language specific call of xp cmdshell */ + IF (SELECT os_language_version FROM sys.dm_os_windows_info) = 1031 /* os language code for German. Again, this is a very specific fix, see #3673 */ + BEGIN + INSERT INTO #localadminsag + EXEC /**/xp_cmdshell/**/ N'net localgroup Administratoren' /* german */ + END + ELSE + BEGIN + INSERT INTO #localadminsag + EXEC /**/xp_cmdshell/**/ N'net localgroup administrators' /* added comments around command since some firewalls block this string TL 20210221 */ + END IF EXISTS (SELECT 1 FROM #localadminsag From cee92d720c15d4051497dba4fbdbbf601befde3c Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Fri, 25 Jul 2025 11:19:04 -0700 Subject: [PATCH 30/76] #3677 versions updates Fix 2019 CU31/32 swap, add new GDRs. Closes #3677. --- SqlServerVersions.sql | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/SqlServerVersions.sql b/SqlServerVersions.sql index 94ae4a0b0..cb1bb869a 100644 --- a/SqlServerVersions.sql +++ b/SqlServerVersions.sql @@ -45,6 +45,8 @@ VALUES (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.1'), (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.0'), /*2022*/ + (16, 4205, 'CU20', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate20', '2025-07-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 20'), + (16, 4200, 'CU19 GDR', 'https://support.microsoft.com/en-us/help/5058721', '2025-07-08', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19 GDR'), (16, 4195, 'CU19', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate19', '2025-05-19', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19'), (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'), @@ -70,8 +72,9 @@ 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, 4435, 'CU32 GDR', 'https://support.microsoft.com/kb/5058722', '2025-07-08', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32 GDR'), + (15, 4430, '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, 4420, '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'), @@ -110,6 +113,7 @@ VALUES (15, 2070, 'GDR', 'https://support.microsoft.com/en-us/help/4517790', '2019-11-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'RTM GDR '), (15, 2000, 'RTM ', '', '2019-11-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'RTM '), /*2017*/ + (14, 3495, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5058714', '2025-07-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3485, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5046858', '2024-11-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3480, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5046061', '2024-10-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3475, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5042215', '2024-09-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), @@ -151,6 +155,7 @@ VALUES (14, 3006, 'RTM CU1', 'https://support.microsoft.com/en-us/help/4038634', '2017-10-24', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 1'), (14, 1000, 'RTM ', '', '2017-10-02', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM '), /*2016*/ + (13, 7055, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5058717', '2025-07-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7045, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5046062', '2024-10-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7040, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5042209', '2024-09-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7037, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5040944', '2024-07-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), From 7fdf6025b612fda4cf7dd5c46fa3b5a5482318a7 Mon Sep 17 00:00:00 2001 From: DForck42 Date: Thu, 7 Aug 2025 13:15:33 -0500 Subject: [PATCH 31/76] fix for sp conversion info length --- sp_BlitzCache.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index 19e02afe3..7d4175fdd 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -4072,12 +4072,12 @@ SELECT @@SPID AS SPID, AND ci.comma_paren_charindex > 0 THEN SUBSTRING(ci.expression, ci.paren_charindex, ci.comma_paren_charindex) END AS converted_to, - CASE WHEN ci.at_charindex = 0 + LEFT(CASE WHEN ci.at_charindex = 0 AND ci.convert_implicit_charindex = 0 AND ci.proc_name = 'Statement' THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) ELSE '**idk_man**' - END AS compile_time_value + END, 258) AS compile_time_value FROM #conversion_info AS ci OPTION (RECOMPILE); From f919198708712fccb7db2407518c06d847e44d07 Mon Sep 17 00:00:00 2001 From: Klaas Date: Tue, 19 Aug 2025 14:56:01 +0200 Subject: [PATCH 32/76] Update sp_Blitz.sql add checks to skip on MI skip IFI, failsafe operator and sql agent alerts on managed instance --- sp_Blitz.sql | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 521b16f14..41eceb780 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -864,12 +864,17 @@ AS INSERT INTO #SkipChecks (CheckID) VALUES (6); /* Security - Jobs Owned By Users per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ INSERT INTO #SkipChecks (CheckID) VALUES (21); /* Informational - Database Encrypted per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ INSERT INTO #SkipChecks (CheckID) VALUES (24); /* File Configuration - System Database on C Drive per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + INSERT INTO #SkipChecks (CheckID) VALUES (30); /* SQL Agent Alerts cannot be configured on MI */ INSERT INTO #SkipChecks (CheckID) VALUES (50); /* Max Server Memory Set Too High - because they max it out */ INSERT INTO #SkipChecks (CheckID) VALUES (55); /* Security - Database Owner <> sa per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + INSERT INTO #SkipChecks (CheckID) VALUES (61); /* SQL Agent Alerts cannot be configured on MI */ + INSERT INTO #SkipChecks (CheckID) VALUES (73); /* SQL Agent Failsafe Operator cannot be configured on MI */ INSERT INTO #SkipChecks (CheckID) VALUES (74); /* TraceFlag On - because Azure Managed Instances go wild and crazy with the trace flags */ + INSERT INTO #SkipChecks (CheckID) VALUES (96); /* SQL Agent Alerts cannot be configured on MI */ INSERT INTO #SkipChecks (CheckID) VALUES (97); /* Unusual SQL Server Edition */ INSERT INTO #SkipChecks (CheckID) VALUES (100); /* Remote DAC disabled - but it's working anyway, details here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ INSERT INTO #SkipChecks (CheckID) VALUES (186); /* MSDB Backup History Purged Too Frequently */ + INSERT INTO #SkipChecks (CheckID) VALUES (192); /* IFI can not be set for data files and is always used for log files in MI */ INSERT INTO #SkipChecks (CheckID) VALUES (199); /* Default trace, details here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ INSERT INTO #SkipChecks (CheckID) VALUES (211); /*Power Plan */ INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'master'); /* Max file size set */ From 6a2a133a80eda566150fe518b56f3aa300cd397f Mon Sep 17 00:00:00 2001 From: James Davis Date: Tue, 19 Aug 2025 11:30:37 -0500 Subject: [PATCH 33/76] Added @UsualOwnerOfJobs to be used in CheckID 6 Jobs Owned By Users to set your default SA user. I did not use @UsualDBOwner you do not have these the same. Amend --- sp_Blitz.sql | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 521b16f14..97bcf0645 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -26,6 +26,7 @@ ALTER PROCEDURE [dbo].[sp_Blitz] @SummaryMode TINYINT = 0 , @BringThePain TINYINT = 0 , @UsualDBOwner sysname = NULL , + @UsualOwnerOfJobs sysname = NULL , -- This is to set the owner of Jobs is you have a different account than SA that you use as Default @SkipBlockingChecks TINYINT = 1 , @Debug TINYINT = 0 , @Version VARCHAR(30) = NULL OUTPUT, @@ -1932,7 +1933,11 @@ AS BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 6) WITH NOWAIT; + + IF @UsualOwnerOfJobs IS NULL + SET @UsualOwnerOfJobs = SUSER_SNAME(0x01); + INSERT INTO #BlitzResults ( CheckID , Priority , @@ -1951,7 +1956,7 @@ AS + '] - meaning if their login is disabled or not available due to Active Directory problems, the job will stop working.' ) AS Details FROM msdb.dbo.sysjobs j WHERE j.enabled = 1 - AND SUSER_SNAME(j.owner_sid) <> SUSER_SNAME(0x01); + AND SUSER_SNAME(j.owner_sid) <> @UsualOwnerOfJobs; END; /* --TOURSTOP06-- */ From 4ae548e1718de5f25f3103e76c418c6f74f5de2a Mon Sep 17 00:00:00 2001 From: James Davis Date: Wed, 20 Aug 2025 07:31:39 -0500 Subject: [PATCH 34/76] Changed CheckID 183 TempDB Unevenly Sized Data Files to Percent Difference Instead of a fixed 1GB difference I use a over default or passed in percent. I went with 10% as DDefault but do not mind that being changed. --- sp_Blitz.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 97bcf0645..0b6403a08 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -27,6 +27,7 @@ ALTER PROCEDURE [dbo].[sp_Blitz] @BringThePain TINYINT = 0 , @UsualDBOwner sysname = NULL , @UsualOwnerOfJobs sysname = NULL , -- This is to set the owner of Jobs is you have a different account than SA that you use as Default + @MaxPercentTembdbFileVariation DECIMAL(3,2) = 0.10, -- set to a decent Default percent, I went with ten as a good start @SkipBlockingChecks TINYINT = 1 , @Debug TINYINT = 0 , @Version VARCHAR(30) = NULL OUTPUT, @@ -3207,11 +3208,10 @@ AS BEGIN - IF ( SELECT COUNT (distinct [size]) + IF (SELECT (MAX((size * 8)) * 1.0)/(MIN((size * 8)) *1.0) - 1.0 FROM tempdb.sys.database_files WHERE type_desc = 'ROWS' - HAVING MAX((size * 8) / (1024. * 1024)) - MIN((size * 8) / (1024. * 1024)) > 1. - ) <> 1 + ) > @MaxPercentTembdbFileVariation BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 183) WITH NOWAIT; From 9ad73060fc07c84b8b1a8301eae3a02e1cd6045b Mon Sep 17 00:00:00 2001 From: James Davis Date: Wed, 20 Aug 2025 14:30:45 -0500 Subject: [PATCH 35/76] Removing other change that got declined. --- sp_Blitz.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 0b6403a08..97bcf0645 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -27,7 +27,6 @@ ALTER PROCEDURE [dbo].[sp_Blitz] @BringThePain TINYINT = 0 , @UsualDBOwner sysname = NULL , @UsualOwnerOfJobs sysname = NULL , -- This is to set the owner of Jobs is you have a different account than SA that you use as Default - @MaxPercentTembdbFileVariation DECIMAL(3,2) = 0.10, -- set to a decent Default percent, I went with ten as a good start @SkipBlockingChecks TINYINT = 1 , @Debug TINYINT = 0 , @Version VARCHAR(30) = NULL OUTPUT, @@ -3208,10 +3207,11 @@ AS BEGIN - IF (SELECT (MAX((size * 8)) * 1.0)/(MIN((size * 8)) *1.0) - 1.0 + IF ( SELECT COUNT (distinct [size]) FROM tempdb.sys.database_files WHERE type_desc = 'ROWS' - ) > @MaxPercentTembdbFileVariation + HAVING MAX((size * 8) / (1024. * 1024)) - MIN((size * 8) / (1024. * 1024)) > 1. + ) <> 1 BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 183) WITH NOWAIT; From 47b5482e162c05886ebf385ca552eb2a6d3132fe Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Wed, 27 Aug 2025 17:41:04 +0000 Subject: [PATCH 36/76] #3690 sp_Blitz memory pressure Add warning about low memory recently. Closes #3690. --- Documentation/sp_Blitz_Checks_by_Priority.md | 5 +-- sp_Blitz.sql | 32 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Documentation/sp_Blitz_Checks_by_Priority.md b/Documentation/sp_Blitz_Checks_by_Priority.md index c81a97708..d59eeb858 100644 --- a/Documentation/sp_Blitz_Checks_by_Priority.md +++ b/Documentation/sp_Blitz_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 269. -If you want to add a new one, start at 270. +CURRENT HIGH CHECKID: 270. +If you want to add a new one, start at 271. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|-----------------------------|---------------------------------------------------------|------------------------------------------------------------------------|----------| @@ -26,6 +26,7 @@ If you want to add a new one, start at 270. | 1 | Corruption | Database Corruption Detected | https://www.BrentOzar.com/go/repair | 90 | | 1 | Performance | Memory Dangerously Low | https://www.BrentOzar.com/go/max | 51 | | 1 | Performance | Memory Dangerously Low in NUMA Nodes | https://www.BrentOzar.com/go/max | 159 | +| 1 | Performance | Memory Dangerously Low Recently | https://www.BrentOzar.com/go/memhistory | 270 | | 1 | Reliability | Evaluation Edition | https://www.BrentOzar.com/go/workgroup | 229 | | 1 | Reliability | Last good DBCC CHECKDB over 2 weeks old | https://www.BrentOzar.com/go/checkdb | 68 | | 1 | Security | Dangerous Service Account | https://vladdba.com/SQLServerSvcAccount | 258 | diff --git a/sp_Blitz.sql b/sp_Blitz.sql index e48ee2b7d..48ab2efe0 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -3936,6 +3936,38 @@ AS AND SUM([wait_time_ms]) > 60000; END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 270 ) + AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_os_memory_health_history') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 270) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 270 AS CheckID , + 1 AS Priority , + 'Performance' AS FindingGroup , + 'Memory Dangerous Low Recently' AS Finding , + 'https://www.brentozar.com/go/memhist' AS URL , + CAST(SUM(1) AS NVARCHAR(10)) + N' instances of ' + CAST(severity_level_desc AS NVARCHAR(100)) + + N' severity level memory issues reported in the last 4 hours in sys.dm_os_memory_health_history.' + FROM sys.dm_os_memory_health_history + WHERE severity_level > 1 + GROUP BY severity_level, severity_level_desc; + END; + + + + + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 121 ) From dd66b83311ad80fc63536d8fd8570c05cb375a53 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Wed, 27 Aug 2025 17:59:32 +0000 Subject: [PATCH 37/76] #3692 sp_BlitzFirst memory pressure Adds new warning for sys.dm_os_memory_health_history. Closes #3692. --- .../sp_BlitzFirst_Checks_by_Priority.md | 5 +++-- sp_BlitzFirst.sql | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Documentation/sp_BlitzFirst_Checks_by_Priority.md b/Documentation/sp_BlitzFirst_Checks_by_Priority.md index b81ea5505..36d2bbe7d 100644 --- a/Documentation/sp_BlitzFirst_Checks_by_Priority.md +++ b/Documentation/sp_BlitzFirst_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 51 -If you want to add a new check, start at 52. +CURRENT HIGH CHECKID: 52 +If you want to add a new check, start at 53. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|---------------------------------|---------------------------------------|-------------------------------------------------|----------| @@ -23,6 +23,7 @@ If you want to add a new check, start at 52. | 1 | SQL Server Internal Maintenance | Data File Growing | https://www.brentozar.com/go/instant | 4 | | 1 | SQL Server Internal Maintenance | Log File Growing | https://www.brentozar.com/go/logsize | 13 | | 1 | SQL Server Internal Maintenance | Log File Shrinking | https://www.brentozar.com/go/logsize | 14 | +| 10 | Server Performance | Memory Dangerously Low Recently | https://www.brentozar.com/go/memhist | 52 | | 10 | Server Performance | Poison Wait Detected | https://www.brentozar.com/go/poison | 30 | | 10 | Server Performance | Target Memory Lower Than Max | https://www.brentozar.com/go/target | 35 | | 10 | Azure Performance | Database is Maxed Out | https://www.brentozar.com/go/maxedout | 41 | diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 28a20f9e6..5e08c1289 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -2617,6 +2617,27 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END END + + + /* Server Performance - Memory Dangerously Low Recently - CheckID 52 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 52',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_os_memory_health_history') + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 52 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Memory Dangerously Low Recently' AS Finding, + 'https://www.brentozar.com/go/memhist' AS URL, + N'As recently as ' + CONVERT(NVARCHAR(19), snapshot_time, 120) + N', memory health issues are being reported in sys.dm_os_memory_health, indicating extreme memory pressure.' AS Details + FROM sys.dm_os_memory_health_history + WHERE severity_level > 1; + END + + RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; From 6f0e2043f56bf1b61d8fa761f2e90f94c35f9f5d Mon Sep 17 00:00:00 2001 From: ReeceGoding <67124261+ReeceGoding@users.noreply.github.com> Date: Sun, 3 Aug 2025 14:46:43 +0100 Subject: [PATCH 38/76] sp_BlitzIndex: Added warning for persisted sample rates. --- .../sp_BlitzIndex_Checks_by_Priority.md | 5 ++- sp_BlitzIndex.sql | 41 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/Documentation/sp_BlitzIndex_Checks_by_Priority.md b/Documentation/sp_BlitzIndex_Checks_by_Priority.md index 619438d0f..1b64b5d3c 100644 --- a/Documentation/sp_BlitzIndex_Checks_by_Priority.md +++ b/Documentation/sp_BlitzIndex_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 124 -If you want to add a new check, start at 125. +CURRENT HIGH CHECKID: 125 +If you want to add a new check, start at 126. | Priority | FindingsGroup | Finding | URL | CheckID | | -------- | ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- | ------- | @@ -25,6 +25,7 @@ If you want to add a new check, start at 125. | 80 | Abnormal Design Pattern | Filter Columns Not In Index Definition | https://www.brentozar.com/go/IndexFeatures | 34 | | 80 | Abnormal Design Pattern | History Table With NonClustered Index | https://sqlserverfast.com/blog/hugo/2023/09/an-update-on-merge/ | 124 | | 90 | Statistics Warnings | Low Sampling Rates | https://www.brentozar.com/go/stats | 91 | +| 90 | Statistics Warnings | Persisted Sampling Rates | https://www.youtube.com/watch?v=V5illj_KOJg&t=758s | 125 | | 90 | Statistics Warnings | Statistics Not Updated Recently | https://www.brentozar.com/go/stats | 90 | | 90 | Statistics Warnings | Statistics with NO RECOMPUTE | https://www.brentozar.com/go/stats | 92 | | 100 | Over-Indexing | NC index with High Writes:Reads | https://www.brentozar.com/go/IndexHoarder | 48 | diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 629cf39ca..c37e755a9 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -738,7 +738,8 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL table_modify_date DATETIME NULL, no_recompute BIT NULL, has_filter BIT NULL, - filter_definition NVARCHAR(MAX) NULL + filter_definition NVARCHAR(MAX) NULL, + has_persisted_sample BIT NULL ); CREATE TABLE #ComputedColumns @@ -2279,7 +2280,7 @@ OPTION (RECOMPILE);'; INSERT #Statistics ( database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition) + no_recompute, has_filter, filter_definition, has_persisted_sample) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName AS database_name, obj.name AS table_name, @@ -2306,7 +2307,12 @@ OPTION (RECOMPILE);'; CONVERT(DATETIME, obj.modify_date) AS table_modify_date, s.no_recompute, s.has_filter, - s.filter_definition + s.filter_definition, + ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) >= 15) + THEN N's.has_persisted_sample' + ELSE N'NULL AS has_persisted_sample' END + + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj ON s.object_id = obj.object_id @@ -2354,7 +2360,7 @@ OPTION (RECOMPILE);'; INSERT #Statistics(database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition) + no_recompute, has_filter, filter_definition, has_persisted_sample) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName AS database_name, obj.name AS table_name, @@ -2379,9 +2385,11 @@ OPTION (RECOMPILE);'; ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N's.has_filter, - s.filter_definition' + s.filter_definition,' ELSE N'NULL AS has_filter, - NULL AS filter_definition' END + NULL AS filter_definition,' END + /* If we are on this branch, then we cannot have the has_persisted_sample column (it is a 2019+ column). */ + + N'NULL AS has_persisted_sample' + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si @@ -4454,7 +4462,7 @@ BEGIN END; ---------------------------------------- - --Statistics Info: Check_id 90-99 + --Statistics Info: Check_id 90-99, as well as 125 ---------------------------------------- RAISERROR(N'check_id 90: Outdated statistics', 0,1) WITH NOWAIT; @@ -4503,6 +4511,24 @@ BEGIN OR (s.rows > 1000000 AND s.percent_sampled < 1) OPTION ( RECOMPILE ); + RAISERROR(N'check_id 125: Statistics with a persisted sample rate', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 125 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Persisted Sampling Rates', + s.database_name, + 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, + 'The statistics sample rate/amount has been persisted here. ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may indicate that somebody is doing statistics rocket surgery. Perhaps you would be better off updating statistics more frequently?' AS details, + QUOTENAME(database_name) + '.' + QUOTENAME(s.schema_name) + '.' + QUOTENAME(s.table_name) + '.' + QUOTENAME(s.index_name) + '.' + QUOTENAME(s.statistics_name) + '.' + QUOTENAME(s.column_names) AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #Statistics AS s + WHERE s.has_persisted_sample = 1 + OPTION ( RECOMPILE ); + RAISERROR(N'check_id 92: Statistics with NO RECOMPUTE', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) @@ -4521,7 +4547,6 @@ BEGIN WHERE s.no_recompute = 1 OPTION ( RECOMPILE ); - RAISERROR(N'check_id 94: Check Constraints That Reference Functions', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) From c689a58110c191b46f2e64d8a8fe42b560c00884 Mon Sep 17 00:00:00 2001 From: ReeceGoding <67124261+ReeceGoding@users.noreply.github.com> Date: Wed, 27 Aug 2025 21:45:44 +0100 Subject: [PATCH 39/76] Added @UsualStatisticsSamplingPercent parameter and comparison to match. Also replaced version check with a column-existence check. Both of these were as Brent suggested. This introduces the complexity of float comparison, since we now care about the float persisted_sample_percent column and have to compare with it. Also added a new check to distinguish the persisted sample rate being surprising and it being unsurprising. --- .../sp_BlitzIndex_Checks_by_Priority.md | 7 +- README.md | 1 + sp_BlitzIndex.sql | 72 +++++++++++++++---- 3 files changed, 64 insertions(+), 16 deletions(-) diff --git a/Documentation/sp_BlitzIndex_Checks_by_Priority.md b/Documentation/sp_BlitzIndex_Checks_by_Priority.md index 1b64b5d3c..4da92c722 100644 --- a/Documentation/sp_BlitzIndex_Checks_by_Priority.md +++ b/Documentation/sp_BlitzIndex_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 125 -If you want to add a new check, start at 126. +CURRENT HIGH CHECKID: 126 +If you want to add a new check, start at 127. | Priority | FindingsGroup | Finding | URL | CheckID | | -------- | ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- | ------- | @@ -25,7 +25,7 @@ If you want to add a new check, start at 126. | 80 | Abnormal Design Pattern | Filter Columns Not In Index Definition | https://www.brentozar.com/go/IndexFeatures | 34 | | 80 | Abnormal Design Pattern | History Table With NonClustered Index | https://sqlserverfast.com/blog/hugo/2023/09/an-update-on-merge/ | 124 | | 90 | Statistics Warnings | Low Sampling Rates | https://www.brentozar.com/go/stats | 91 | -| 90 | Statistics Warnings | Persisted Sampling Rates | https://www.youtube.com/watch?v=V5illj_KOJg&t=758s | 125 | +| 90 | Statistics Warnings | Persisted Sampling Rates (Unexpected) | `https://www.youtube.com/watch?v=V5illj_KOJg&t=758s` | 125 | | 90 | Statistics Warnings | Statistics Not Updated Recently | https://www.brentozar.com/go/stats | 90 | | 90 | Statistics Warnings | Statistics with NO RECOMPUTE | https://www.brentozar.com/go/stats | 92 | | 100 | Over-Indexing | NC index with High Writes:Reads | https://www.brentozar.com/go/IndexHoarder | 48 | @@ -62,6 +62,7 @@ If you want to add a new check, start at 126. | 200 | Abnormal Design Pattern | Temporal Tables | https://www.brentozar.com/go/AbnormalPsychology | 110 | | 200 | Repeated Calculations | Computed Columns Not Persisted | https://www.brentozar.com/go/serialudf | 100 | | 200 | Statistics Warnings | Statistics With Filters | https://www.brentozar.com/go/stats | 93 | +| 200 | Statistics Warnings | Persisted Sampling Rates (Expected) | `https://www.youtube.com/watch?v=V5illj_KOJg&t=758s` | 126 | | 200 | Over-Indexing | High Ratio of Nulls | https://www.brentozar.com/go/IndexHoarder | 25 | | 200 | Over-Indexing | High Ratio of Strings | https://www.brentozar.com/go/IndexHoarder | 27 | | 200 | Over-Indexing | Wide Tables: 35+ cols or > 2000 non-LOB bytes | https://www.brentozar.com/go/IndexHoarder | 26 | diff --git a/README.md b/README.md index bfcf2470d..9490300eb 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,7 @@ In addition to the [parameters common to many of the stored procedures](#paramet * @SkipPartitions = 1 - add this if you want to analyze large partitioned tables. We skip these by default for performance reasons. * @SkipStatistics = 0 - right now, by default, we skip statistics analysis because we've had some performance issues on this. +* @UsualStatisticsSamplingPercent = 100 (default) - By default, @SkipStatistics = 0 with either @Mode = 0 or @Mode = 4 does not inform you of persisted statistics sample rates if that rate is 100. Use a different float if you usually persist a different sample percentage and do not want to be warned about it. Use NULL if you want to hear about every persisted sample rate. * @Filter = 0 (default) - 1=No low-usage warnings for objects with 0 reads. 2=Only warn for objects >= 500MB * @OutputDatabaseName, @OutputSchemaName, @OutputTableName - these only work for @Mode = 2, index usage detail. diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index c37e755a9..4b63b7f86 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -23,6 +23,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex /*Note:@Filter doesn't do anything unless @Mode=0*/ @SkipPartitions BIT = 0, @SkipStatistics BIT = 1, + @UsualStatisticsSamplingPercent FLOAT = 100, /* FLOAT to match sys.dm_db_stats_properties. More detail later. 100 by default because Brent suggests that if people are persisting statistics at all, they are probably doing 100 in lots of places and not filtering that out would produce noise. */ @GetAllDatabases BIT = 0, @ShowColumnstoreOnly BIT = 0, /* Will show only the Row Group and Segment details for a table with a columnstore index. */ @BringThePain BIT = 0, @@ -739,7 +740,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL no_recompute BIT NULL, has_filter BIT NULL, filter_definition NVARCHAR(MAX) NULL, - has_persisted_sample BIT NULL + persisted_sample_percent FLOAT NULL ); CREATE TABLE #ComputedColumns @@ -2280,7 +2281,7 @@ OPTION (RECOMPILE);'; INSERT #Statistics ( database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition, has_persisted_sample) + no_recompute, has_filter, filter_definition, persisted_sample_percent) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName AS database_name, obj.name AS table_name, @@ -2309,9 +2310,15 @@ OPTION (RECOMPILE);'; s.has_filter, s.filter_definition, ' - + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) >= 15) - THEN N's.has_persisted_sample' - ELSE N'NULL AS has_persisted_sample' END + + CASE WHEN EXISTS + ( + /* We cannot trust checking version numbers, like we did above, because Azure disagrees. */ + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.dm_db_stats_properties', N'IF') AND all_cols.[name] = N'persisted_sample_percent' + ) + THEN N'ddsp.persisted_sample_percent' + ELSE N'NULL AS persisted_sample_percent' END + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj @@ -2360,7 +2367,7 @@ OPTION (RECOMPILE);'; INSERT #Statistics(database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition, has_persisted_sample) + no_recompute, has_filter, filter_definition, persisted_sample_percent) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName AS database_name, obj.name AS table_name, @@ -2388,8 +2395,8 @@ OPTION (RECOMPILE);'; s.filter_definition,' ELSE N'NULL AS has_filter, NULL AS filter_definition,' END - /* If we are on this branch, then we cannot have the has_persisted_sample column (it is a 2019+ column). */ - + N'NULL AS has_persisted_sample' + /* Certainly NULL. This branch does not even join on the table that this column comes from. */ + + N'NULL AS persisted_sample_percent' + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si @@ -4462,7 +4469,7 @@ BEGIN END; ---------------------------------------- - --Statistics Info: Check_id 90-99, as well as 125 + --Statistics Info: Check_id 90-99, as well as 125-126 ---------------------------------------- RAISERROR(N'check_id 90: Outdated statistics', 0,1) WITH NOWAIT; @@ -4511,22 +4518,61 @@ BEGIN OR (s.rows > 1000000 AND s.percent_sampled < 1) OPTION ( RECOMPILE ); - RAISERROR(N'check_id 125: Statistics with a persisted sample rate', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 125: Persisted Sampling Rates (Unexpected)', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) SELECT 125 AS check_id, 90 AS Priority, 'Statistics Warnings' AS findings_group, - 'Persisted Sampling Rates', + 'Persisted Sampling Rates (Unexpected)', s.database_name, 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, - 'The statistics sample rate/amount has been persisted here. ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may indicate that somebody is doing statistics rocket surgery. Perhaps you would be better off updating statistics more frequently?' AS details, + 'The persisted statistics sample rate is ' + CONVERT(NVARCHAR(100), s.persisted_sample_percent) + '%' + + CASE WHEN @UsualStatisticsSamplingPercent IS NOT NULL + THEN (N' rather than your expected @UsualStatisticsSamplingPercent value of ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + '%') + ELSE '' + END + + N'. This may indicate that somebody is doing statistics rocket surgery. If not, consider updating statistics more frequently.' AS details, QUOTENAME(database_name) + '.' + QUOTENAME(s.schema_name) + '.' + QUOTENAME(s.table_name) + '.' + QUOTENAME(s.index_name) + '.' + QUOTENAME(s.statistics_name) + '.' + QUOTENAME(s.column_names) AS index_definition, 'N/A' AS secret_columns, 'N/A' AS index_usage_summary, 'N/A' AS index_size_summary FROM #Statistics AS s - WHERE s.has_persisted_sample = 1 + /* + We have to do float comparison here, so it is time to explain why @UsualStatisticsSamplingPercent is a float. + The foremost reason is that it is a float because we are comparing it to the persisted_sample_percent column in sys.dm_db_stats_properties and that column is a float. + You may correctly object that CREATE STATISTICS with a decimal as your WITH SAMPLE [...] PERCENT is a syntax error and conclude that integers are enough. + However, `WITH SAMPLE [...] ROWS` is allowed with PERSIST_SAMPLE_PERCENT = ON and you can use that to persist a non-integer sample rate. + So, yes, we really have to use floats. + */ + WHERE + /* persisted_sample_percent is either zero or NULL when the statistic is not persisted. */ + s.persisted_sample_percent > 0.0001 + AND + ( + ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) > 0.1 + OR @UsualStatisticsSamplingPercent IS NULL + ) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 126: Persisted Sampling Rates (Expected)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 126 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Persisted Sampling Rates (Expected)', + s.database_name, + 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, + CONVERT(NVARCHAR(100), COUNT(*)) + ' statistic(s) with a persisted sample rate matching your desired persisted sample rate, ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + N'%. Set @UsualStatisticsSamplingPercent to NULL if you want to see all of them in this result set. Its default value is 100.' AS details, + s.database_name + N' (Entire database)' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #Statistics AS s + WHERE ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) <= 0.1 + AND @UsualStatisticsSamplingPercent IS NOT NULL + GROUP BY s.database_name OPTION ( RECOMPILE ); RAISERROR(N'check_id 92: Statistics with NO RECOMPUTE', 0,1) WITH NOWAIT; From 320e778c2218c56082b1834b068c3f49aa78c5a7 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 28 Aug 2025 20:40:07 +0000 Subject: [PATCH 40/76] #3695 sp_BlitzFirst clarify restore name When restoring a new database. Closes #3695. --- sp_BlitzFirst.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 5e08c1289..bfcb66bd6 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -1638,7 +1638,11 @@ BEGIN 'Maintenance Tasks Running' AS FindingGroup, 'Restore Running' AS Finding, 'https://www.brentozar.com/askbrent/backups/' AS URL, - 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, + 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), + (SELECT db1.name FROM sys.databases db1 + LEFT OUTER JOIN sys.databases db2 ON db1.name <> db2.name AND db1.state_desc = db2.state_desc + WHERE db1.state_desc = 'RESTORING' AND db2.name IS NULL), + 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown ') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' AS Details, 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, pl.query_plan AS QueryPlan, r.start_time AS StartTime, From f81d59901d0348e6b0a95925ef8ddc74ffc611b2 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 28 Aug 2025 20:52:59 +0000 Subject: [PATCH 41/76] #3694 sp_BlitzWho live plans On by default in 2022 & newer and Azure. --- sp_BlitzWho.sql | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index 5c49699e9..0267ff3cf 100644 --- a/sp_BlitzWho.sql +++ b/sp_BlitzWho.sql @@ -22,7 +22,7 @@ ALTER PROCEDURE dbo.sp_BlitzWho @CheckDateOverride DATETIMEOFFSET = NULL, @ShowActualParameters BIT = 0, @GetOuterCommand BIT = 0, - @GetLiveQueryPlan BIT = 0, + @GetLiveQueryPlan BIT = NULL, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0, @@ -85,7 +85,8 @@ RETURN; END; /* @Help = 1 */ /* Get the major and minor build numbers */ -DECLARE @ProductVersion NVARCHAR(128) +DECLARE @ProductVersion NVARCHAR(128) = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) + ,@EngineEdition INT = CAST(SERVERPROPERTY('EngineEdition') AS INT) ,@ProductVersionMajor DECIMAL(10,2) ,@ProductVersionMinor DECIMAL(10,2) ,@Platform NVARCHAR(8) /* Azure or NonAzure are acceptable */ = (SELECT CASE WHEN @@VERSION LIKE '%Azure%' THEN N'Azure' ELSE N'NonAzure' END AS [Platform]) @@ -122,7 +123,6 @@ DECLARE @ProductVersion NVARCHAR(128) /* Let's get @SortOrder set to lower case here for comparisons later */ SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) @@ -133,6 +133,14 @@ SELECT @OutputTableName = QUOTENAME(@OutputTableName), @LineFeed = CHAR(13) + CHAR(10); +IF @GetLiveQueryPlan IS NULL + BEGIN + IF @ProductVersionMajor >= 16 OR @EngineEdition NOT IN (1, 2, 3, 4) + SET @GetLiveQueryPlan = 1; + ELSE + SET @GetLiveQueryPlan = 0; + END + IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL AND EXISTS ( SELECT * FROM sys.databases @@ -920,7 +928,7 @@ IF @ProductVersionMajor >= 11 END+N' derp.query_plan , CAST(COALESCE(qs_live.Query_Plan, ' + CASE WHEN @GetLiveQueryPlan=1 - THEN '''''' + THEN '''''' ELSE '''''' END +') AS XML From 40ef60c3fe813cf66accb0ea4575bfb5222478ef Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 28 Aug 2025 21:50:24 +0000 Subject: [PATCH 42/76] #3680 sp_BlitzIndex stats sampling Continued work on #3680. Moves 200-priority down to Mode 4, throws error if they pass in an invalid stats sampling rate. --- sp_BlitzIndex.sql | 48 +++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 4b63b7f86..feb81a434 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -175,6 +175,12 @@ BEGIN RETURN; END; +IF(@UsualStatisticsSamplingPercent <= 0 OR @UsualStatisticsSamplingPercent > 100) +BEGIN + RAISERROR('Invalid value for parameter @UsualStatisticsSamplingPercent. Expected: 1 to 100',12,1); + RETURN; +END; + IF(@OutputType = 'TABLE' AND NOT (@OutputTableName IS NULL AND @OutputSchemaName IS NULL AND @OutputDatabaseName IS NULL AND @OutputServerName IS NULL)) BEGIN RAISERROR(N'One or more output parameters specified in combination with TABLE output, changing to NONE output mode', 0,1) WITH NOWAIT; @@ -4555,26 +4561,6 @@ BEGIN ) OPTION ( RECOMPILE ); - RAISERROR(N'check_id 126: Persisted Sampling Rates (Expected)', 0,1) WITH NOWAIT; - INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, - secret_columns, index_usage_summary, index_size_summary ) - SELECT 126 AS check_id, - 200 AS Priority, - 'Statistics Warnings' AS findings_group, - 'Persisted Sampling Rates (Expected)', - s.database_name, - 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, - CONVERT(NVARCHAR(100), COUNT(*)) + ' statistic(s) with a persisted sample rate matching your desired persisted sample rate, ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + N'%. Set @UsualStatisticsSamplingPercent to NULL if you want to see all of them in this result set. Its default value is 100.' AS details, - s.database_name + N' (Entire database)' AS index_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #Statistics AS s - WHERE ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) <= 0.1 - AND @UsualStatisticsSamplingPercent IS NOT NULL - GROUP BY s.database_name - OPTION ( RECOMPILE ); - RAISERROR(N'check_id 92: Statistics with NO RECOMPUTE', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) @@ -5666,6 +5652,28 @@ BEGIN + RAISERROR(N'check_id 126: Persisted Sampling Rates (Expected)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 126 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Persisted Sampling Rates (Expected)', + s.database_name, + 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, + CONVERT(NVARCHAR(100), COUNT(*)) + ' statistic(s) with a persisted sample rate matching your desired persisted sample rate, ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + N'%. Set @UsualStatisticsSamplingPercent to NULL if you want to see all of them in this result set. Its default value is 100.' AS details, + s.database_name + N' (Entire database)' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #Statistics AS s + WHERE ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) <= 0.1 + AND @UsualStatisticsSamplingPercent IS NOT NULL + GROUP BY s.database_name + OPTION ( RECOMPILE ); + + + END /* IF @Mode = 4 */ From 81ca079677d9b711d923105ea508b577f79497ff Mon Sep 17 00:00:00 2001 From: ReeceGoding <67124261+ReeceGoding@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:42:01 +0100 Subject: [PATCH 43/76] Edited comments to reflect check id 126 being moved. --- sp_BlitzIndex.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index feb81a434..56d83b58d 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -4475,7 +4475,7 @@ BEGIN END; ---------------------------------------- - --Statistics Info: Check_id 90-99, as well as 125-126 + --Statistics Info: Check_id 90-99, as well as 125 ---------------------------------------- RAISERROR(N'check_id 90: Outdated statistics', 0,1) WITH NOWAIT; @@ -5651,7 +5651,7 @@ BEGIN OPTION ( RECOMPILE ); - + /* See check_id 125. */ RAISERROR(N'check_id 126: Persisted Sampling Rates (Expected)', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) From 20c22572c226769f56970b3c90fd3e0f4751cc0b Mon Sep 17 00:00:00 2001 From: ReeceGoding <67124261+ReeceGoding@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:46:20 +0100 Subject: [PATCH 44/76] sp_BlitzIndex: Added check for partitioned tables without incremental statistics. Closes #3699. --- .../sp_BlitzIndex_Checks_by_Priority.md | 135 +++++++++--------- sp_BlitzIndex.sql | 87 +++++++++-- 2 files changed, 143 insertions(+), 79 deletions(-) diff --git a/Documentation/sp_BlitzIndex_Checks_by_Priority.md b/Documentation/sp_BlitzIndex_Checks_by_Priority.md index 4da92c722..04ecd4666 100644 --- a/Documentation/sp_BlitzIndex_Checks_by_Priority.md +++ b/Documentation/sp_BlitzIndex_Checks_by_Priority.md @@ -6,71 +6,72 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 126 -If you want to add a new check, start at 127. +CURRENT HIGH CHECKID: 127 +If you want to add a new check, start at 128. -| Priority | FindingsGroup | Finding | URL | CheckID | -| -------- | ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------- | ------- | -| 10 | Over-Indexing | Many NC Indexes on a Single Table | https://www.brentozar.com/go/IndexHoarder | 20 | -| 10 | Over-Indexing | Unused NC Index with High Writes | https://www.brentozar.com/go/IndexHoarder | 22 | -| 10 | Resumable Indexing | Resumable Index Operation Paused | https://www.BrentOzar.com/go/resumable | 122 | -| 10 | Resumable Indexing | Resumable Index Operation Running | https://www.BrentOzar.com/go/resumable | 123 | -| 20 | Redundant Indexes | Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 1 | -| 30 | Redundant Indexes | Approximate Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 2 | -| 40 | Index Suggestion | High Value Missing Index | https://www.brentozar.com/go/indexaphobia | 50 | -| 70 | Locking-Prone Indexes | Total Lock Time with Long Average Waits | https://www.brentozar.com/go/aggressiveindexes | 11 | -| 70 | Locking-Prone Indexes | Total Lock Time with Short Average Waits | https://www.brentozar.com/go/aggressiveindexes | 12 | -| 80 | Abnormal Design Pattern | Columnstore Indexes with Trace Flag 834 | https://support.microsoft.com/en-us/kb/3210239 | 72 | -| 80 | Abnormal Design Pattern | Identity Column Near End of Range | https://www.brentozar.com/go/AbnormalPsychology | 68 | -| 80 | Abnormal Design Pattern | Filter Columns Not In Index Definition | https://www.brentozar.com/go/IndexFeatures | 34 | -| 80 | Abnormal Design Pattern | History Table With NonClustered Index | https://sqlserverfast.com/blog/hugo/2023/09/an-update-on-merge/ | 124 | -| 90 | Statistics Warnings | Low Sampling Rates | https://www.brentozar.com/go/stats | 91 | -| 90 | Statistics Warnings | Persisted Sampling Rates (Unexpected) | `https://www.youtube.com/watch?v=V5illj_KOJg&t=758s` | 125 | -| 90 | Statistics Warnings | Statistics Not Updated Recently | https://www.brentozar.com/go/stats | 90 | -| 90 | Statistics Warnings | Statistics with NO RECOMPUTE | https://www.brentozar.com/go/stats | 92 | -| 100 | Over-Indexing | NC index with High Writes:Reads | https://www.brentozar.com/go/IndexHoarder | 48 | -| 100 | Indexes Worth Reviewing | Heap with a Nonclustered Primary Key | https://www.brentozar.com/go/SelfLoathing | 47 | -| 100 | Indexes Worth Reviewing | Heap with Forwarded Fetches | https://www.brentozar.com/go/SelfLoathing | 43 | -| 100 | Indexes Worth Reviewing | Large Active Heap | https://www.brentozar.com/go/SelfLoathing | 44 | -| 100 | Indexes Worth Reviewing | Low Fill Factor on Clustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | -| 100 | Indexes Worth Reviewing | Low Fill Factor on Nonclustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | -| 100 | Indexes Worth Reviewing | Medium Active Heap | https://www.brentozar.com/go/SelfLoathing | 45 | -| 100 | Indexes Worth Reviewing | Small Active Heap | https://www.brentozar.com/go/SelfLoathing | 46 | -| 100 | Forced Serialization | Computed Column with Scalar UDF | https://www.brentozar.com/go/serialudf | 99 | -| 100 | Forced Serialization | Check Constraint with Scalar UDF | https://www.brentozar.com/go/computedscalar | 94 | -| 150 | Abnormal Design Pattern | Cascading Updates or Deletes | https://www.brentozar.com/go/AbnormalPsychology | 71 | -| 150 | Abnormal Design Pattern | Unindexed Foreign Keys | https://www.brentozar.com/go/AbnormalPsychology | 72 | -| 150 | Abnormal Design Pattern | Columnstore Index | https://www.brentozar.com/go/AbnormalPsychology | 61 | -| 150 | Abnormal Design Pattern | Column Collation Does Not Match Database Collation | https://www.brentozar.com/go/AbnormalPsychology | 69 | -| 150 | Abnormal Design Pattern | Compressed Index | https://www.brentozar.com/go/AbnormalPsychology | 63 | -| 150 | Abnormal Design Pattern | In-Memory OLTP | https://www.brentozar.com/go/AbnormalPsychology | 73 | -| 150 | Abnormal Design Pattern | Non-Aligned Index on a Partitioned Table | https://www.brentozar.com/go/AbnormalPsychology | 65 | -| 150 | Abnormal Design Pattern | Partitioned Index | https://www.brentozar.com/go/AbnormalPsychology | 64 | -| 150 | Abnormal Design Pattern | Spatial Index | https://www.brentozar.com/go/AbnormalPsychology | 62 | -| 150 | Abnormal Design Pattern | XML Index | https://www.brentozar.com/go/AbnormalPsychology | 60 | -| 150 | Over-Indexing | Approximate: Wide Indexes (7 or More Columns) | https://www.brentozar.com/go/IndexHoarder | 23 | -| 150 | Over-Indexing | More Than 5 Percent NC Indexes Are Unused | https://www.brentozar.com/go/IndexHoarder | 21 | -| 150 | Over-Indexing | Non-Unique Clustered Index | https://www.brentozar.com/go/IndexHoarder | 28 | -| 150 | Over-Indexing | Unused NC Index with Low Writes | https://www.brentozar.com/go/IndexHoarder | 29 | -| 150 | Over-Indexing | Wide Clustered Index (>3 columns or >16 bytes) | https://www.brentozar.com/go/IndexHoarder | 24 | -| 150 | Indexes Worth Reviewing | Disabled Index | https://www.brentozar.com/go/SelfLoathing | 42 | -| 150 | Indexes Worth Reviewing | Hypothetical Index | https://www.brentozar.com/go/SelfLoathing | 41 | -| 200 | Abnormal Design Pattern | Identity Column Using a Negative Seed or Increment Other Than 1 | https://www.brentozar.com/go/AbnormalPsychology | 74 | -| 200 | Abnormal Design Pattern | Recently Created Tables/Indexes (1 week) | https://www.brentozar.com/go/AbnormalPsychology | 66 | -| 200 | Abnormal Design Pattern | Recently Modified Tables/Indexes (2 days) | https://www.brentozar.com/go/AbnormalPsychology | 67 | -| 200 | Abnormal Design Pattern | Replicated Columns | https://www.brentozar.com/go/AbnormalPsychology | 70 | -| 200 | Abnormal Design Pattern | Temporal Tables | https://www.brentozar.com/go/AbnormalPsychology | 110 | -| 200 | Repeated Calculations | Computed Columns Not Persisted | https://www.brentozar.com/go/serialudf | 100 | -| 200 | Statistics Warnings | Statistics With Filters | https://www.brentozar.com/go/stats | 93 | -| 200 | Statistics Warnings | Persisted Sampling Rates (Expected) | `https://www.youtube.com/watch?v=V5illj_KOJg&t=758s` | 126 | -| 200 | Over-Indexing | High Ratio of Nulls | https://www.brentozar.com/go/IndexHoarder | 25 | -| 200 | Over-Indexing | High Ratio of Strings | https://www.brentozar.com/go/IndexHoarder | 27 | -| 200 | Over-Indexing | Wide Tables: 35+ cols or > 2000 non-LOB bytes | https://www.brentozar.com/go/IndexHoarder | 26 | -| 200 | Indexes Worth Reviewing | Heaps with Deletes | https://www.brentozar.com/go/SelfLoathing | 49 | -| 200 | High Workloads | Scan-a-lots (index-usage-stats) | https://www.brentozar.com/go/Workaholics | 80 | -| 200 | High Workloads | Top Recent Accesses (index-op-stats) | https://www.brentozar.com/go/Workaholics | 81 | -| 250 | Omitted Index Features | Few Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 31 | -| 250 | Omitted Index Features | No Filtered Indexes or Indexed Views | https://www.brentozar.com/go/IndexFeatures | 32 | -| 250 | Omitted Index Features | No Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 30 | -| 250 | Omitted Index Features | Potential Filtered Index (Based on Column Name) | https://www.brentozar.com/go/IndexFeatures | 33 | -| 250 | Specialized Indexes | Optimized For Sequential Keys | | 121 | +| Priority | FindingsGroup | Finding | URL | CheckID | +| -------- | ----------------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ------- | +| 10 | Over-Indexing | Many NC Indexes on a Single Table | https://www.brentozar.com/go/IndexHoarder | 20 | +| 10 | Over-Indexing | Unused NC Index with High Writes | https://www.brentozar.com/go/IndexHoarder | 22 | +| 10 | Resumable Indexing | Resumable Index Operation Paused | https://www.BrentOzar.com/go/resumable | 122 | +| 10 | Resumable Indexing | Resumable Index Operation Running | https://www.BrentOzar.com/go/resumable | 123 | +| 20 | Redundant Indexes | Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 1 | +| 30 | Redundant Indexes | Approximate Duplicate Keys | https://www.brentozar.com/go/duplicateindex | 2 | +| 40 | Index Suggestion | High Value Missing Index | https://www.brentozar.com/go/indexaphobia | 50 | +| 70 | Locking-Prone Indexes | Total Lock Time with Long Average Waits | https://www.brentozar.com/go/aggressiveindexes | 11 | +| 70 | Locking-Prone Indexes | Total Lock Time with Short Average Waits | https://www.brentozar.com/go/aggressiveindexes | 12 | +| 80 | Abnormal Design Pattern | Columnstore Indexes with Trace Flag 834 | https://support.microsoft.com/en-us/kb/3210239 | 72 | +| 80 | Abnormal Design Pattern | Identity Column Near End of Range | https://www.brentozar.com/go/AbnormalPsychology | 68 | +| 80 | Abnormal Design Pattern | Filter Columns Not In Index Definition | https://www.brentozar.com/go/IndexFeatures | 34 | +| 80 | Abnormal Design Pattern | History Table With NonClustered Index | https://sqlserverfast.com/blog/hugo/2023/09/an-update-on-merge/ | 124 | +| 90 | Statistics Warnings | Low Sampling Rates | https://www.brentozar.com/go/stats | 91 | +| 90 | Statistics Warnings | Persisted Sampling Rates (Unexpected) | `https://www.youtube.com/watch?v=V5illj_KOJg&t=758s` | 125 | +| 90 | Statistics Warnings | Statistics Not Updated Recently | https://www.brentozar.com/go/stats | 90 | +| 90 | Statistics Warnings | Statistics with NO RECOMPUTE | https://www.brentozar.com/go/stats | 92 | +| 100 | Over-Indexing | NC index with High Writes:Reads | https://www.brentozar.com/go/IndexHoarder | 48 | +| 100 | Indexes Worth Reviewing | Heap with a Nonclustered Primary Key | https://www.brentozar.com/go/SelfLoathing | 47 | +| 100 | Indexes Worth Reviewing | Heap with Forwarded Fetches | https://www.brentozar.com/go/SelfLoathing | 43 | +| 100 | Indexes Worth Reviewing | Large Active Heap | https://www.brentozar.com/go/SelfLoathing | 44 | +| 100 | Indexes Worth Reviewing | Low Fill Factor on Clustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | +| 100 | Indexes Worth Reviewing | Low Fill Factor on Nonclustered Index | https://www.brentozar.com/go/SelfLoathing | 40 | +| 100 | Indexes Worth Reviewing | Medium Active Heap | https://www.brentozar.com/go/SelfLoathing | 45 | +| 100 | Indexes Worth Reviewing | Small Active Heap | https://www.brentozar.com/go/SelfLoathing | 46 | +| 100 | Forced Serialization | Computed Column with Scalar UDF | https://www.brentozar.com/go/serialudf | 99 | +| 100 | Forced Serialization | Check Constraint with Scalar UDF | https://www.brentozar.com/go/computedscalar | 94 | +| 150 | Abnormal Design Pattern | Cascading Updates or Deletes | https://www.brentozar.com/go/AbnormalPsychology | 71 | +| 150 | Abnormal Design Pattern | Unindexed Foreign Keys | https://www.brentozar.com/go/AbnormalPsychology | 72 | +| 150 | Abnormal Design Pattern | Columnstore Index | https://www.brentozar.com/go/AbnormalPsychology | 61 | +| 150 | Abnormal Design Pattern | Column Collation Does Not Match Database Collation | https://www.brentozar.com/go/AbnormalPsychology | 69 | +| 150 | Abnormal Design Pattern | Compressed Index | https://www.brentozar.com/go/AbnormalPsychology | 63 | +| 150 | Abnormal Design Pattern | In-Memory OLTP | https://www.brentozar.com/go/AbnormalPsychology | 73 | +| 150 | Abnormal Design Pattern | Non-Aligned Index on a Partitioned Table | https://www.brentozar.com/go/AbnormalPsychology | 65 | +| 150 | Abnormal Design Pattern | Partitioned Index | https://www.brentozar.com/go/AbnormalPsychology | 64 | +| 150 | Abnormal Design Pattern | Spatial Index | https://www.brentozar.com/go/AbnormalPsychology | 62 | +| 150 | Abnormal Design Pattern | XML Index | https://www.brentozar.com/go/AbnormalPsychology | 60 | +| 150 | Over-Indexing | Approximate: Wide Indexes (7 or More Columns) | https://www.brentozar.com/go/IndexHoarder | 23 | +| 150 | Over-Indexing | More Than 5 Percent NC Indexes Are Unused | https://www.brentozar.com/go/IndexHoarder | 21 | +| 150 | Over-Indexing | Non-Unique Clustered Index | https://www.brentozar.com/go/IndexHoarder | 28 | +| 150 | Over-Indexing | Unused NC Index with Low Writes | https://www.brentozar.com/go/IndexHoarder | 29 | +| 150 | Over-Indexing | Wide Clustered Index (>3 columns or >16 bytes) | https://www.brentozar.com/go/IndexHoarder | 24 | +| 150 | Indexes Worth Reviewing | Disabled Index | https://www.brentozar.com/go/SelfLoathing | 42 | +| 150 | Indexes Worth Reviewing | Hypothetical Index | https://www.brentozar.com/go/SelfLoathing | 41 | +| 200 | Abnormal Design Pattern | Identity Column Using a Negative Seed or Increment Other Than 1 | https://www.brentozar.com/go/AbnormalPsychology | 74 | +| 200 | Abnormal Design Pattern | Recently Created Tables/Indexes (1 week) | https://www.brentozar.com/go/AbnormalPsychology | 66 | +| 200 | Abnormal Design Pattern | Recently Modified Tables/Indexes (2 days) | https://www.brentozar.com/go/AbnormalPsychology | 67 | +| 200 | Abnormal Design Pattern | Replicated Columns | https://www.brentozar.com/go/AbnormalPsychology | 70 | +| 200 | Abnormal Design Pattern | Temporal Tables | https://www.brentozar.com/go/AbnormalPsychology | 110 | +| 200 | Repeated Calculations | Computed Columns Not Persisted | https://www.brentozar.com/go/serialudf | 100 | +| 200 | Statistics Warnings | Statistics With Filters | https://www.brentozar.com/go/stats | 93 | +| 200 | Statistics Warnings | Persisted Sampling Rates (Expected) | `https://www.youtube.com/watch?v=V5illj_KOJg&t=758s` | 126 | +| 200 | Statistics Warnings | Partitioned Table Without Incremental Statistics | https://sqlperformance.com/2015/05/sql-statistics/improving-maintenance-incremental-statistics | 127 | +| 200 | Over-Indexing | High Ratio of Nulls | https://www.brentozar.com/go/IndexHoarder | 25 | +| 200 | Over-Indexing | High Ratio of Strings | https://www.brentozar.com/go/IndexHoarder | 27 | +| 200 | Over-Indexing | Wide Tables: 35+ cols or > 2000 non-LOB bytes | https://www.brentozar.com/go/IndexHoarder | 26 | +| 200 | Indexes Worth Reviewing | Heaps with Deletes | https://www.brentozar.com/go/SelfLoathing | 49 | +| 200 | High Workloads | Scan-a-lots (index-usage-stats) | https://www.brentozar.com/go/Workaholics | 80 | +| 200 | High Workloads | Top Recent Accesses (index-op-stats) | https://www.brentozar.com/go/Workaholics | 81 | +| 250 | Omitted Index Features | Few Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 31 | +| 250 | Omitted Index Features | No Filtered Indexes or Indexed Views | https://www.brentozar.com/go/IndexFeatures | 32 | +| 250 | Omitted Index Features | No Indexes Use Includes | https://www.brentozar.com/go/IndexFeatures | 30 | +| 250 | Omitted Index Features | Potential Filtered Index (Based on Column Name) | https://www.brentozar.com/go/IndexFeatures | 33 | +| 250 | Specialized Indexes | Optimized For Sequential Keys | | 121 | diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 56d83b58d..078d5c5df 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -726,6 +726,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL CREATE TABLE #Statistics ( database_id INT NOT NULL, database_name NVARCHAR(256) NOT NULL, + object_id INT NOT NULL, table_name NVARCHAR(128) NULL, schema_name NVARCHAR(128) NULL, index_name NVARCHAR(128) NULL, @@ -746,7 +747,8 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL no_recompute BIT NULL, has_filter BIT NULL, filter_definition NVARCHAR(MAX) NULL, - persisted_sample_percent FLOAT NULL + persisted_sample_percent FLOAT NULL, + is_incremental BIT NULL ); CREATE TABLE #ComputedColumns @@ -2284,14 +2286,15 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Statistics Info With Newer Syntax.',0,1) WITH NOWAIT; SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - INSERT #Statistics ( database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, + INSERT #Statistics ( database_id, database_name, object_id, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition, persisted_sample_percent) + no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - @i_DatabaseName AS database_name, - obj.name AS table_name, - sch.name AS schema_name, + @i_DatabaseName AS database_name, + obj.object_id, + obj.name AS table_name, + sch.name AS schema_name, ISNULL(i.name, ''System Or User Statistic'') AS index_name, ca.column_names AS column_names, s.name AS statistics_name, @@ -2323,8 +2326,16 @@ OPTION (RECOMPILE);'; FROM sys.all_columns AS all_cols WHERE all_cols.[object_id] = OBJECT_ID(N'sys.dm_db_stats_properties', N'IF') AND all_cols.[name] = N'persisted_sample_percent' ) - THEN N'ddsp.persisted_sample_percent' - ELSE N'NULL AS persisted_sample_percent' END + THEN N'ddsp.persisted_sample_percent,' + ELSE N'NULL AS persisted_sample_percent,' END + + CASE WHEN EXISTS + ( + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.stats', N'V') AND all_cols.[name] = N'is_incremental' + ) + THEN N's.is_incremental' + ELSE N'NULL AS is_incremental' END + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj @@ -2370,12 +2381,13 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Statistics Info With Older Syntax.',0,1) WITH NOWAIT; SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - INSERT #Statistics(database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, + INSERT #Statistics(database_id, database_name, object_id, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition, persisted_sample_percent) + no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName AS database_name, + obj.object_id, obj.name AS table_name, sch.name AS schema_name, ISNULL(i.name, ''System Or User Statistic'') AS index_name, @@ -2402,7 +2414,16 @@ OPTION (RECOMPILE);'; ELSE N'NULL AS has_filter, NULL AS filter_definition,' END /* Certainly NULL. This branch does not even join on the table that this column comes from. */ - + N'NULL AS persisted_sample_percent' + + N'NULL AS persisted_sample_percent, + ' + + CASE WHEN EXISTS + ( + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.stats', N'V') AND all_cols.[name] = N'is_incremental' + ) + THEN N's.is_incremental' + ELSE N'NULL AS is_incremental' END + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si @@ -5672,7 +5693,49 @@ BEGIN GROUP BY s.database_name OPTION ( RECOMPILE ); - + RAISERROR(N'check_id 127: Partitioned Table Without Incremental Statistics', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary, more_info ) + SELECT 127 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Partitioned Table Without Incremental Statistics', + partitioned_tables.database_name, + 'https://sqlperformance.com/2015/05/sql-statistics/improving-maintenance-incremental-statistics' AS URL, + 'The table ' + QUOTENAME(partitioned_tables.schema_name) + '.' + QUOTENAME(partitioned_tables.object_name) + ' is partitioned, but ' + + CONVERT(NVARCHAR(100), incremental_stats_counts.not_incremental_stats_count) + ' of its ' + CONVERT(NVARCHAR(100), incremental_stats_counts.stats_count) + + ' statistics are not incremental. If this is a sliding/rolling window table, then consider making the statistics incremental. If not, then investigate why this table is partitioned.' AS details, + partitioned_tables.object_name + N' (Entire table)' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary, + partitioned_tables.more_info + FROM + ( + SELECT s.database_id, + s.object_id, + COUNT(CASE WHEN s.is_incremental = 0 THEN 1 END) AS not_incremental_stats_count, + COUNT(*) AS stats_count + FROM #Statistics AS s + GROUP BY s.database_id, s.object_id + HAVING COUNT(CASE WHEN s.is_incremental = 0 THEN 1 END) > 0 + ) AS incremental_stats_counts + JOIN + ( + /* Just get the tables. We do not need the indexes. */ + SELECT DISTINCT i.database_name, + i.database_id, + i.object_id, + i.schema_name, + i.object_name, + /* This is a little bit dishonest, since it tells us nothing about if the statistics are incremental. */ + i.more_info + FROM #IndexSanity AS i + WHERE i.partition_key_column_name IS NOT NULL + ) AS partitioned_tables + ON partitioned_tables.database_id = incremental_stats_counts.database_id AND partitioned_tables.object_id = incremental_stats_counts.object_id + /* No need for a GROUP BY. What we are joining on has exactly one row in each sub-query. */ + OPTION ( RECOMPILE ); END /* IF @Mode = 4 */ From b32c61ad255064ed2198600791a4da73b1d4e2c6 Mon Sep 17 00:00:00 2001 From: Eilandor Date: Thu, 4 Sep 2025 16:55:52 +0300 Subject: [PATCH 45/76] minor typo in sp_BlitzFirst.sql noticed a small typo in your last video. --- sp_BlitzFirst.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index bfcb66bd6..df1dd7d00 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -2636,7 +2636,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, 'Server Performance' AS FindingGroup, 'Memory Dangerously Low Recently' AS Finding, 'https://www.brentozar.com/go/memhist' AS URL, - N'As recently as ' + CONVERT(NVARCHAR(19), snapshot_time, 120) + N', memory health issues are being reported in sys.dm_os_memory_health, indicating extreme memory pressure.' AS Details + N'As recently as ' + CONVERT(NVARCHAR(19), snapshot_time, 120) + N', memory health issues are being reported in sys.dm_os_memory_health_history, indicating extreme memory pressure.' AS Details FROM sys.dm_os_memory_health_history WHERE severity_level > 1; END From ee7c4d31bb4b82ba573848f19ff8a6e37fb90812 Mon Sep 17 00:00:00 2001 From: ReeceGoding <67124261+ReeceGoding@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:59:38 +0100 Subject: [PATCH 46/76] Made modifications_before_auto_update a BIGINT. Closes #3701 but I cannot test it. --- sp_BlitzIndex.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 078d5c5df..67eb3c7db 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -740,7 +740,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL histogram_steps INT NULL, modification_counter BIGINT NULL, percent_modifications DECIMAL(18, 1) NULL, - modifications_before_auto_update INT NULL, + modifications_before_auto_update BIGINT NULL, index_type_desc NVARCHAR(128) NULL, table_create_date DATETIME NULL, table_modify_date DATETIME NULL, @@ -2310,7 +2310,7 @@ OPTION (RECOMPILE);'; ELSE ddsp.modification_counter END AS percent_modifications, CASE WHEN ddsp.rows < 500 THEN 500 - ELSE CAST(( ddsp.rows * .20 ) + 500 AS INT) + ELSE CAST(( ddsp.rows * .20 ) + 500 AS BIGINT) END AS modifications_before_auto_update, ISNULL(i.type_desc, ''System Or User Statistic - N/A'') AS index_type_desc, CONVERT(DATETIME, obj.create_date) AS table_create_date, @@ -2401,7 +2401,7 @@ OPTION (RECOMPILE);'; ELSE si.rowmodctr END AS percent_modifications, CASE WHEN si.rowcnt < 500 THEN 500 - ELSE CAST(( si.rowcnt * .20 ) + 500 AS INT) + ELSE CAST(( si.rowcnt * .20 ) + 500 AS BIGINT) END AS modifications_before_auto_update, ISNULL(i.type_desc, ''System Or User Statistic - N/A'') AS index_type_desc, CONVERT(DATETIME, obj.create_date) AS table_create_date, From 3cff8339eea16144616341bf44a390a3dba02bac Mon Sep 17 00:00:00 2001 From: Bruce Wilson Date: Mon, 8 Sep 2025 14:18:16 -0500 Subject: [PATCH 47/76] Tests in sp_BlitzIndex to exclude indexes created within 7 days or modified within 2 days were reversed, resulting in ONLY reporting on very recent indexes. --- sp_BlitzIndex.sql | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 078d5c5df..40044171b 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -4671,9 +4671,9 @@ BEGIN JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE index_id NOT IN ( 0, 1 ) AND i.is_unique = 0 - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) OPTION ( RECOMPILE ); IF @percent_NC_indexes_unused >= 5 INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -4704,9 +4704,9 @@ BEGIN WHERE index_id NOT IN ( 0, 1 ) AND i.is_unique = 0 AND total_reads = 0 - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) GROUP BY i.database_name OPTION ( RECOMPILE ); @@ -4957,9 +4957,9 @@ BEGIN AND i.index_id NOT IN (0,1) /*NCs only*/ AND i.is_unique = 0 AND sz.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE sz.total_reserved_MB END - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) ORDER BY i.db_schema_object_indexid OPTION ( RECOMPILE ); From 0e3c131f62047068487d163c1f6d613e814cf417 Mon Sep 17 00:00:00 2001 From: Tom Willwerth Date: Sun, 14 Sep 2025 15:47:39 -0400 Subject: [PATCH 48/76] A few improvements for Linux --- sp_Blitz.sql | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 48ab2efe0..b0d360120 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -5013,7 +5013,7 @@ IF @ProductVersionMajor >= 10 END; END; /* CheckID 258 - Security - SQL Server Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 258 ) @@ -5050,7 +5050,7 @@ IF @ProductVersionMajor >= 10 END; /* CheckID 259 - Security - SQL Server Agent Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 259 ) @@ -5123,7 +5123,7 @@ IF @ProductVersionMajor >= 10 END; /*This checks which service account SQL Server is running as.*/ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 169 ) @@ -5163,7 +5163,7 @@ IF @ProductVersionMajor >= 10 END; /*This checks which service account SQL Agent is running as.*/ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 170 ) @@ -9233,7 +9233,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 ''Server Info'' AS FindingsGroup , ''Services'' AS Finding , '''' AS URL , - N''Service: '' + servicename + N'' runs under service account '' + service_account + N''. Last startup time: '' + COALESCE(CAST(CASE WHEN YEAR(last_startup_time) <= 1753 THEN CAST(''17530101'' as datetime) ELSE CAST(last_startup_time AS DATETIME) END AS VARCHAR(50)), ''not shown.'') + ''. Startup type: '' + startup_type_desc + N'', currently '' + status_desc + ''.'' + N''Service: '' + servicename + ISNULL((N'' runs under service account '' + service_account),'''') + N''. Last startup time: '' + COALESCE(CAST(CASE WHEN YEAR(last_startup_time) <= 1753 THEN CAST(''17530101'' as datetime) ELSE CAST(last_startup_time AS DATETIME) END AS VARCHAR(50)), ''not shown.'') + ''. Startup type: '' + startup_type_desc + N'', currently '' + status_desc + ''.'' FROM sys.dm_server_services OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; From 0f824b0e0afd967cbf2a14ff541e20ae270e4df7 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 25 Sep 2025 04:54:51 -0700 Subject: [PATCH 49/76] 3708_BlitzFirst_MI_operations Added check 53 for Azure operations ongoing. --- .../sp_BlitzFirst_Checks_by_Priority.md | 5 +++-- sp_BlitzFirst.sql | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Documentation/sp_BlitzFirst_Checks_by_Priority.md b/Documentation/sp_BlitzFirst_Checks_by_Priority.md index 36d2bbe7d..433d096fe 100644 --- a/Documentation/sp_BlitzFirst_Checks_by_Priority.md +++ b/Documentation/sp_BlitzFirst_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 52 -If you want to add a new check, start at 53. +CURRENT HIGH CHECKID: 53 +If you want to add a new check, start at 54. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|---------------------------------|---------------------------------------|-------------------------------------------------|----------| @@ -36,6 +36,7 @@ If you want to add a new check, start at 53. | 50 | Query Problems | Re-Compilations/Sec High | https://www.brentozar.com/go/recompile | 16 | | 50 | Query Problems | Statistics Updated Recently | https://www.brentozar.com/go/stats | 44 | | 50 | Query Problems | High Percentage Of Runnable Queries | https://erikdarlingdata.com/go/RunnableQueue/ | 47 | +| 50 | Server Performance | Azure Operation Ongoing | https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-operation-status-azure-sql-database | 53 | | 50 | Server Performance | High CPU Utilization | https://www.brentozar.com/go/cpu | 24 | | 50 | Server Performance | High CPU Utilization - Non SQL Processes | https://www.brentozar.com/go/cpu | 28 | | 50 | Server Performance | Slow Data File Reads | https://www.brentozar.com/go/slow | 11 | diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index df1dd7d00..912452296 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -2592,6 +2592,27 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END + /* Server Performance - Azure Operation Ongoing - CheckID 53 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 53',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_operation_status') + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 53 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Azure Operation ' + CASE WHEN state IN (2, 3, 5) THEN 'Ended Recently' ELSE 'Ongoing' END AS Finding, + 'https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-operation-status-azure-sql-database' AS URL, + N'Operation: ' + operation + N' State: ' + state_desc + N' Percent Complete: ' + CAST(percent_complete AS NVARCHAR(10)) + @LineFeed + + N' On: ' + CAST(resource_type_desc AS NVARCHAR(100)) + N':' + CAST(major_resource_id AS NVARCHAR(100)) + @LineFeed + + N' Started: ' + CAST(start_time AS NVARCHAR(100)) + N' Last Modified Time: ' + CAST(last_modify_time AS NVARCHAR(100)) + @LineFeed + + N' For more information, query SELECT * FROM sys.dm_operation_status; ' AS Details + FROM sys.dm_operation_status + END + + /* Potential Upcoming Problems - High Number of Connections - CheckID 49 */ IF (@Debug = 1) BEGIN From 864d05a21c42c2c6d09da1ccc4e85d5985c16635 Mon Sep 17 00:00:00 2001 From: Yoni Sade <103036539+yoni-yad2@users.noreply.github.com> Date: Mon, 29 Sep 2025 19:48:29 +0300 Subject: [PATCH 50/76] Exclude checks for RDS objects and rdsadmin DB See: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/3712 --- sp_Blitz.sql | 89 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 62 insertions(+), 27 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index b0d360120..1ba7e1a8f 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -570,8 +570,7 @@ AS SELECT DB_NAME(d.database_id) FROM sys.databases AS d - WHERE (DB_NAME(d.database_id) LIKE 'rdsadmin%' - OR LOWER(d.name) IN ('dbatools', 'dbadmin', 'dbmaintenance')) + WHERE LOWER(d.name) IN ('dbatools', 'dbadmin', 'dbmaintenance', 'rdsadmin') OPTION(RECOMPILE); /*Skip checks for database where we don't have read permissions*/ @@ -2047,7 +2046,9 @@ AS ''Performance'' AS FindingsGroup, ''Server Triggers Enabled'' AS Finding, ''https://www.brentozar.com/go/logontriggers/'' AS URL, - (''Server Trigger ['' + [name] ++ ''] is enabled. Make sure you understand what that trigger is doing - the less work it does, the better.'') AS Details FROM sys.server_triggers WHERE is_disabled = 0 AND is_ms_shipped = 0 OPTION (RECOMPILE);'; + (''Server Trigger ['' + [name] ++ ''] is enabled. Make sure you understand what that trigger is doing - the less work it does, the better.'') AS Details + FROM sys.server_triggers + WHERE is_disabled = 0 AND is_ms_shipped = 0 AND name NOT LIKE ''rds^_%'' ESCAPE ''^'' OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; IF @Debug = 2 AND @StringToExecute IS NULL PRINT '@StringToExecute has gone NULL, for some reason.'; @@ -2706,7 +2707,8 @@ AS + '. Tables in the master database may not be restored in the event of a disaster.' ) AS Details FROM master.sys.tables WHERE is_ms_shipped = 0 - AND name NOT IN ('CommandLog','SqlServerVersions','$ndo$srvproperty'); + AND name NOT IN ('CommandLog','SqlServerVersions','$ndo$srvproperty') + AND name NOT LIKE 'rds^_%' ESCAPE '^'; /* That last one is the Dynamics NAV licensing table: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2426 */ END; @@ -4883,12 +4885,12 @@ AS SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT ' + CAST(@CurrentCheckID AS NVARCHAR(200)) + ', d.[name], ' + CAST(@CurrentPriority AS NVARCHAR(200)) + ', ''Non-Default Database Config'', ''' + @CurrentFinding + ''',''' + @CurrentURL + ''',''' + COALESCE(@CurrentDetails, 'This database setting is not the default.') + ''' FROM sys.databases d - WHERE d.database_id > 4 AND d.state = 0 AND (d.[' + @CurrentName + '] NOT IN (0, 60) OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; + WHERE d.database_id > 4 AND DB_NAME(d.database_id) != ''rdsadmin'' AND d.state = 0 AND (d.[' + @CurrentName + '] NOT IN (0, 60) OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; ELSE SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT ' + CAST(@CurrentCheckID AS NVARCHAR(200)) + ', d.[name], ' + CAST(@CurrentPriority AS NVARCHAR(200)) + ', ''Non-Default Database Config'', ''' + @CurrentFinding + ''',''' + @CurrentURL + ''',''' + COALESCE(@CurrentDetails, 'This database setting is not the default.') + ''' FROM sys.databases d - WHERE d.database_id > 4 AND d.state = 0 AND (d.[' + @CurrentName + '] <> ' + @CurrentDefaultValue + ' OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; + WHERE d.database_id > 4 AND DB_NAME(d.database_id) != ''rdsadmin'' AND d.state = 0 AND (d.[' + @CurrentName + '] <> ' + @CurrentDefaultValue + ' OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; IF @Debug = 2 AND @StringToExecute IS NULL PRINT '@StringToExecute has gone NULL, for some reason.'; @@ -6776,7 +6778,12 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) + IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; END; /* Note that by using sp_MSforeachdb, we're running the query in all @@ -6813,7 +6820,7 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/querystore'', (''The new SQL Server 2016 Query Store feature has not been enabled on this database.'') FROM [?].sys.database_query_store_options WHERE desired_state = 0 - AND N''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; + AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -6845,6 +6852,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 AND wait_stats_capture_mode = 0 + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6876,6 +6884,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 AND actual_state <> 2 + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6907,6 +6916,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 AND desired_state <> actual_state + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6942,6 +6952,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 /* No point in checking this if Query Store is off. */ AND query_capture_mode_desc <> ''AUTO'' + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6972,7 +6983,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/cleanup'', (''SQL 2016 RTM has a bug involving dumps that happen every time Query Store cleanup jobs run. This is fixed in CU1 and later: https://sqlserverupdates.com/sql-server-2016-updates/'') FROM sys.databases AS d - WHERE d.is_query_store_on = 1 OPTION (RECOMPILE);'; + WHERE d.is_query_store_on = 1 + AND d.name != ''rdsadmin'' + OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; IF @Debug = 2 AND @StringToExecute IS NULL PRINT '@StringToExecute has gone NULL, for some reason.'; @@ -7008,7 +7021,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options dqso join master.sys.databases D on D.name = N''?'' WHERE ((dqso.actual_state = 0 AND D.is_query_store_on = 1) OR (dqso.actual_state <> 0 AND D.is_query_store_on = 0)) - AND N''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; + AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -7035,11 +7048,12 @@ IF @ProductVersionMajor >= 10 ''Multiple Log Files on One Drive'', ''https://www.brentozar.com/go/manylogs'', (''The ['' + DB_NAME() + ''] database has multiple log files on the '' + LEFT(physical_name, 1) + '' drive. This is not a performance booster because log file access is sequential, not parallel.'') - FROM [?].sys.database_files WHERE type_desc = ''LOG'' - AND N''?'' <> ''[tempdb]'' + FROM [?].sys.database_files + WHERE type_desc = ''LOG'' + AND ''?'' NOT IN (''rdsadmin'',''tempdb'') GROUP BY LEFT(physical_name, 1) - HAVING COUNT(*) > 1 - AND SUM(size) < 268435456 OPTION (RECOMPILE);'; + HAVING COUNT(*) > 1 AND SUM(size) < 268435456 + OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 @@ -7068,6 +7082,7 @@ IF @ProductVersionMajor >= 10 (''The ['' + DB_NAME() + ''] database has multiple data files in one filegroup, but they are not all set up to grow in identical amounts. This can lead to uneven file activity inside the filegroup.'') FROM [?].sys.database_files WHERE type_desc = ''ROWS'' + AND ''?'' != ''rdsadmin'' GROUP BY data_space_id HAVING COUNT(DISTINCT growth) > 1 OR COUNT(DISTINCT is_percent_growth) > 1 OPTION (RECOMPILE);'; END; @@ -7096,7 +7111,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/percentgrowth'' AS URL, ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' has grown to '' + CONVERT(NVARCHAR(20), CONVERT(NUMERIC(38, 2), (f.size / 128.) / 1024.)) + '' GB, and is using percent filegrowth settings. This can lead to slow performance during growths if Instant File Initialization is not enabled.'' FROM [?].sys.database_files f - WHERE is_percent_growth = 1 and size > 128000 OPTION (RECOMPILE);'; + WHERE is_percent_growth = 1 and size > 128000 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; /* addition by Henrik Staun Poulsen, Stovi Software */ @@ -7124,7 +7141,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/percentgrowth'' AS URL, ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' is using 1MB filegrowth settings, but it has grown to '' + CAST((CAST(f.size AS BIGINT) * 8 / 1000000) AS NVARCHAR(10)) + '' GB. Time to up the growth amount.'' FROM [?].sys.database_files f - WHERE is_percent_growth = 0 and growth=128 and size > 128000 OPTION (RECOMPILE);'; + WHERE is_percent_growth = 0 and growth=128 and size > 128000 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 @@ -7154,7 +7173,9 @@ IF @ProductVersionMajor >= 10 ''Enterprise Edition Features In Use'', ''https://www.brentozar.com/go/ee'', (''The ['' + DB_NAME() + ''] database is using '' + feature_name + ''. If this database is restored onto a Standard Edition server, the restore will fail on versions prior to 2016 SP1.'') - FROM [?].sys.dm_db_persisted_sku_features OPTION (RECOMPILE);'; + FROM [?].sys.dm_db_persisted_sku_features + WHERE ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; END; @@ -7212,8 +7233,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/repl'', (''['' + DB_NAME() + ''] has MSreplication_objects tables in it, indicating it is a replication subscriber.'') FROM [?].sys.tables - WHERE name = ''dbo.MSreplication_objects'' AND ''?'' <> ''master'' OPTION (RECOMPILE)'; - + WHERE name = ''dbo.MSreplication_objects'' + AND ''?'' NOT IN (''master'', ''rdsadmin'') + OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -7241,7 +7263,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/trig'', (''The ['' + DB_NAME() + ''] database has '' + CAST(SUM(1) AS NVARCHAR(50)) + '' triggers.'') FROM [?].sys.triggers t INNER JOIN [?].sys.objects o ON t.parent_id = o.object_id - INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id WHERE t.is_ms_shipped = 0 AND DB_NAME() != ''ReportServer'' + INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id + WHERE t.is_ms_shipped = 0 + AND ''?'' NOT IN (''rdsadmin'', ''ReportServer'') HAVING SUM(1) > 0 OPTION (RECOMPILE)'; END; @@ -7271,7 +7295,9 @@ IF @ProductVersionMajor >= 10 ''Plan Guides Failing'', ''https://www.brentozar.com/go/misguided'', (''The ['' + DB_NAME() + ''] database has plan guides that are no longer valid, so the queries involved may be failing silently.'') - FROM [?].sys.plan_guides g CROSS APPLY fn_validate_plan_guide(g.plan_guide_id) OPTION (RECOMPILE)'; + FROM [?].sys.plan_guides g CROSS APPLY fn_validate_plan_guide(g.plan_guide_id) + WHERE ''?'' != ''rdsadmin'' + OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -7299,7 +7325,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/hypo'', (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is a leftover hypothetical index from the Index Tuning Wizard or Database Tuning Advisor. This index is not actually helping performance and should be removed.'') from [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id - WHERE i.is_hypothetical = 1 OPTION (RECOMPILE);'; + WHERE i.is_hypothetical = 1 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 @@ -7355,7 +7383,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/trust'', (''The ['' + DB_NAME() + ''] database has foreign keys that were probably disabled, data was changed, and then the key was enabled again. Simply enabling the key is not enough for the optimizer to use this key - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') from [?].sys.foreign_keys i INNER JOIN [?].sys.objects o ON i.parent_object_id = o.object_id INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id - WHERE i.is_not_trusted = 1 AND i.is_not_for_replication = 0 AND i.is_disabled = 0 AND N''?'' NOT IN (''master'', ''model'', ''msdb'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE);'; + WHERE i.is_not_trusted = 1 AND i.is_not_for_replication = 0 AND i.is_disabled = 0 AND ''?'' + NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''ReportServer'', ''ReportServerTempDB'') + OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 @@ -7844,8 +7874,10 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT INNER JOIN #DatabaseScopedConfigurationDefaults def1 ON dsc.configuration_id = def1.configuration_id LEFT OUTER JOIN #DatabaseScopedConfigurationDefaults def ON dsc.configuration_id = def.configuration_id AND (cast(dsc.value as nvarchar(100)) = cast(def.default_value as nvarchar(100)) OR dsc.value IS NULL) AND (dsc.value_for_secondary = def.default_value_for_secondary OR dsc.value_for_secondary IS NULL) LEFT OUTER JOIN #SkipChecks sk ON (sk.CheckID IS NULL OR def.CheckID = sk.CheckID) AND (sk.DatabaseName IS NULL OR sk.DatabaseName = DB_NAME()) - WHERE def.configuration_id IS NULL AND sk.CheckID IS NULL ORDER BY 1 - OPTION (RECOMPILE);'; + WHERE def.configuration_id IS NULL AND sk.CheckID IS NULL + AND ''?'' != ''rdsadmin'' + ORDER BY 1 + OPTION (RECOMPILE);'; END; /* Check 218 - Show me the dodgy SET Options */ @@ -7882,6 +7914,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT OR sm.uses_quoted_identifier <> 1 ) AND o.is_ms_shipped = 0 + AND ''?'' != ''rdsadmin'' HAVING COUNT(1) > 0;'; END; --of Check 218. @@ -7913,7 +7946,9 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT + CAST(iro.sql_text AS NVARCHAR(1000)) AS Details FROM sys.index_resumable_operations iro JOIN sys.objects o ON iro.[object_id] = o.[object_id] - WHERE iro.state <> 0;'; + WHERE iro.state <> 0 + AND ''?'' != ''rdsadmin'' + ;'; END; --of Check 225. --/* Check 220 - Statistics Without Histograms */ @@ -7947,7 +7982,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT -- WHERE o.is_ms_shipped = 0 AND o.type_desc = ''USER_TABLE'' -- AND h.object_id IS NULL -- AND 0 < (SELECT SUM(row_count) FROM sys.dm_db_partition_stats ps WHERE ps.object_id = o.object_id) - -- AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'') + -- AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'') -- HAVING COUNT(DISTINCT o.object_id) > 0;'; --END; --of Check 220. From ccfafbc63db567d47c7771e170e400633b190b7e Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Thu, 2 Oct 2025 09:44:23 -0400 Subject: [PATCH 51/76] 2025-10-02 Release Bumping version numbers and dates, adding releases. --- Install-All-Scripts.sql | 404 +++++++++++++++++++++++++++++++++------- Install-Azure.sql | 298 +++++++++++++++++++++++------ SqlServerVersions.sql | 12 +- sp_Blitz.sql | 2 +- sp_BlitzAnalysis.sql | 2 +- sp_BlitzBackups.sql | 2 +- sp_BlitzCache.sql | 2 +- sp_BlitzFirst.sql | 2 +- sp_BlitzIndex.sql | 2 +- sp_BlitzLock.sql | 2 +- sp_BlitzWho.sql | 2 +- sp_DatabaseRestore.sql | 2 +- sp_ineachdb.sql | 2 +- 13 files changed, 599 insertions(+), 135 deletions(-) diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index 5f310717a..f1ec7b551 100644 --- a/Install-All-Scripts.sql +++ b/Install-All-Scripts.sql @@ -26,6 +26,7 @@ ALTER PROCEDURE [dbo].[sp_Blitz] @SummaryMode TINYINT = 0 , @BringThePain TINYINT = 0 , @UsualDBOwner sysname = NULL , + @UsualOwnerOfJobs sysname = NULL , -- This is to set the owner of Jobs is you have a different account than SA that you use as Default @SkipBlockingChecks TINYINT = 1 , @Debug TINYINT = 0 , @Version VARCHAR(30) = NULL OUTPUT, @@ -38,7 +39,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -864,12 +865,17 @@ AS INSERT INTO #SkipChecks (CheckID) VALUES (6); /* Security - Jobs Owned By Users per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ INSERT INTO #SkipChecks (CheckID) VALUES (21); /* Informational - Database Encrypted per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ INSERT INTO #SkipChecks (CheckID) VALUES (24); /* File Configuration - System Database on C Drive per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + INSERT INTO #SkipChecks (CheckID) VALUES (30); /* SQL Agent Alerts cannot be configured on MI */ INSERT INTO #SkipChecks (CheckID) VALUES (50); /* Max Server Memory Set Too High - because they max it out */ INSERT INTO #SkipChecks (CheckID) VALUES (55); /* Security - Database Owner <> sa per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + INSERT INTO #SkipChecks (CheckID) VALUES (61); /* SQL Agent Alerts cannot be configured on MI */ + INSERT INTO #SkipChecks (CheckID) VALUES (73); /* SQL Agent Failsafe Operator cannot be configured on MI */ INSERT INTO #SkipChecks (CheckID) VALUES (74); /* TraceFlag On - because Azure Managed Instances go wild and crazy with the trace flags */ + INSERT INTO #SkipChecks (CheckID) VALUES (96); /* SQL Agent Alerts cannot be configured on MI */ INSERT INTO #SkipChecks (CheckID) VALUES (97); /* Unusual SQL Server Edition */ INSERT INTO #SkipChecks (CheckID) VALUES (100); /* Remote DAC disabled - but it's working anyway, details here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ INSERT INTO #SkipChecks (CheckID) VALUES (186); /* MSDB Backup History Purged Too Frequently */ + INSERT INTO #SkipChecks (CheckID) VALUES (192); /* IFI can not be set for data files and is always used for log files in MI */ INSERT INTO #SkipChecks (CheckID) VALUES (199); /* Default trace, details here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ INSERT INTO #SkipChecks (CheckID) VALUES (211); /*Power Plan */ INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'master'); /* Max file size set */ @@ -1932,7 +1938,11 @@ AS BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 6) WITH NOWAIT; + + IF @UsualOwnerOfJobs IS NULL + SET @UsualOwnerOfJobs = SUSER_SNAME(0x01); + INSERT INTO #BlitzResults ( CheckID , Priority , @@ -1951,7 +1961,7 @@ AS + '] - meaning if their login is disabled or not available due to Active Directory problems, the job will stop working.' ) AS Details FROM msdb.dbo.sysjobs j WHERE j.enabled = 1 - AND SUSER_SNAME(j.owner_sid) <> SUSER_SNAME(0x01); + AND SUSER_SNAME(j.owner_sid) <> @UsualOwnerOfJobs; END; /* --TOURSTOP06-- */ @@ -3926,6 +3936,38 @@ AS AND SUM([wait_time_ms]) > 60000; END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 270 ) + AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_os_memory_health_history') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 270) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 270 AS CheckID , + 1 AS Priority , + 'Performance' AS FindingGroup , + 'Memory Dangerous Low Recently' AS Finding , + 'https://www.brentozar.com/go/memhist' AS URL , + CAST(SUM(1) AS NVARCHAR(10)) + N' instances of ' + CAST(severity_level_desc AS NVARCHAR(100)) + + N' severity level memory issues reported in the last 4 hours in sys.dm_os_memory_health_history.' + FROM sys.dm_os_memory_health_history + WHERE severity_level > 1 + GROUP BY severity_level, severity_level_desc; + END; + + + + + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 121 ) @@ -4971,7 +5013,7 @@ IF @ProductVersionMajor >= 10 END; END; /* CheckID 258 - Security - SQL Server Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 258 ) @@ -5008,7 +5050,7 @@ IF @ProductVersionMajor >= 10 END; /* CheckID 259 - Security - SQL Server Agent Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 259 ) @@ -5081,7 +5123,7 @@ IF @ProductVersionMajor >= 10 END; /*This checks which service account SQL Server is running as.*/ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 169 ) @@ -5121,7 +5163,7 @@ IF @ProductVersionMajor >= 10 END; /*This checks which service account SQL Agent is running as.*/ -IF @ProductVersionMajor >= 10 +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 170 ) @@ -9191,7 +9233,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 ''Server Info'' AS FindingsGroup , ''Services'' AS Finding , '''' AS URL , - N''Service: '' + servicename + N'' runs under service account '' + service_account + N''. Last startup time: '' + COALESCE(CAST(CASE WHEN YEAR(last_startup_time) <= 1753 THEN CAST(''17530101'' as datetime) ELSE CAST(last_startup_time AS DATETIME) END AS VARCHAR(50)), ''not shown.'') + ''. Startup type: '' + startup_type_desc + N'', currently '' + status_desc + ''.'' + N''Service: '' + servicename + ISNULL((N'' runs under service account '' + service_account),'''') + N''. Last startup time: '' + COALESCE(CAST(CASE WHEN YEAR(last_startup_time) <= 1753 THEN CAST(''17530101'' as datetime) ELSE CAST(last_startup_time AS DATETIME) END AS VARCHAR(50)), ''not shown.'') + ''. Startup type: '' + startup_type_desc + N'', currently '' + status_desc + ''.'' FROM sys.dm_server_services OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; @@ -9867,7 +9909,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) FROM [sys].[dm_server_services] WHERE [servicename] LIKE 'SQL Server%' - AND [servicename] NOT LIKE 'SQL Server Agent%' + AND [servicename] NOT LIKE 'SQL Server%Agent%' AND [servicename] NOT LIKE 'SQL Server Launchpad%')) BEGIN INSERT INTO #BlitzResults @@ -9913,7 +9955,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 FROM #localadmins WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) FROM [sys].[dm_server_services] - WHERE [servicename] LIKE 'SQL Server Agent%' + WHERE [servicename] LIKE 'SQL Server%Agent%' AND [servicename] NOT LIKE 'SQL Server Launchpad%')) BEGIN INSERT INTO #BlitzResults @@ -9939,14 +9981,23 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 /*had to use a different table name because SQL Server/SSMS complains when parsing that the table still exists when it gets to the create part*/ IF OBJECT_ID('tempdb..#localadminsag') IS NOT NULL DROP TABLE #localadminsag; CREATE TABLE #localadminsag (cmdshell_output NVARCHAR(1000)); - INSERT INTO #localadmins - EXEC /**/xp_cmdshell/**/ N'net localgroup administrators' /* added comments around command since some firewalls block this string TL 20210221 */ + /* language specific call of xp cmdshell */ + IF (SELECT os_language_version FROM sys.dm_os_windows_info) = 1031 /* os language code for German. Again, this is a very specific fix, see #3673 */ + BEGIN + INSERT INTO #localadminsag + EXEC /**/xp_cmdshell/**/ N'net localgroup Administratoren' /* german */ + END + ELSE + BEGIN + INSERT INTO #localadminsag + EXEC /**/xp_cmdshell/**/ N'net localgroup administrators' /* added comments around command since some firewalls block this string TL 20210221 */ + END IF EXISTS (SELECT 1 - FROM #localadmins + FROM #localadminsag WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) FROM [sys].[dm_server_services] - WHERE [servicename] LIKE 'SQL Server Agent%' + WHERE [servicename] LIKE 'SQL Server%Agent%' AND [servicename] NOT LIKE 'SQL Server Launchpad%')) BEGIN INSERT INTO #BlitzResults @@ -10566,7 +10617,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -11444,7 +11495,7 @@ AS SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -13228,7 +13279,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -17017,12 +17068,12 @@ SELECT @@SPID AS SPID, AND ci.comma_paren_charindex > 0 THEN SUBSTRING(ci.expression, ci.paren_charindex, ci.comma_paren_charindex) END AS converted_to, - CASE WHEN ci.at_charindex = 0 + LEFT(CASE WHEN ci.at_charindex = 0 AND ci.convert_implicit_charindex = 0 AND ci.proc_name = 'Statement' THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) ELSE '**idk_man**' - END AS compile_time_value + END, 258) AS compile_time_value FROM #conversion_info AS ci OPTION (RECOMPILE); @@ -20588,6 +20639,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex /*Note:@Filter doesn't do anything unless @Mode=0*/ @SkipPartitions BIT = 0, @SkipStatistics BIT = 1, + @UsualStatisticsSamplingPercent FLOAT = 100, /* FLOAT to match sys.dm_db_stats_properties. More detail later. 100 by default because Brent suggests that if people are persisting statistics at all, they are probably doing 100 in lots of places and not filtering that out would produce noise. */ @GetAllDatabases BIT = 0, @ShowColumnstoreOnly BIT = 0, /* Will show only the Row Group and Segment details for a table with a columnstore index. */ @BringThePain BIT = 0, @@ -20614,7 +20666,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -20739,6 +20791,12 @@ BEGIN RETURN; END; +IF(@UsualStatisticsSamplingPercent <= 0 OR @UsualStatisticsSamplingPercent > 100) +BEGIN + RAISERROR('Invalid value for parameter @UsualStatisticsSamplingPercent. Expected: 1 to 100',12,1); + RETURN; +END; + IF(@OutputType = 'TABLE' AND NOT (@OutputTableName IS NULL AND @OutputSchemaName IS NULL AND @OutputDatabaseName IS NULL AND @OutputServerName IS NULL)) BEGIN RAISERROR(N'One or more output parameters specified in combination with TABLE output, changing to NONE output mode', 0,1) WITH NOWAIT; @@ -21284,6 +21342,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL CREATE TABLE #Statistics ( database_id INT NOT NULL, database_name NVARCHAR(256) NOT NULL, + object_id INT NOT NULL, table_name NVARCHAR(128) NULL, schema_name NVARCHAR(128) NULL, index_name NVARCHAR(128) NULL, @@ -21297,13 +21356,15 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL histogram_steps INT NULL, modification_counter BIGINT NULL, percent_modifications DECIMAL(18, 1) NULL, - modifications_before_auto_update INT NULL, + modifications_before_auto_update BIGINT NULL, index_type_desc NVARCHAR(128) NULL, table_create_date DATETIME NULL, table_modify_date DATETIME NULL, no_recompute BIT NULL, has_filter BIT NULL, - filter_definition NVARCHAR(MAX) NULL + filter_definition NVARCHAR(MAX) NULL, + persisted_sample_percent FLOAT NULL, + is_incremental BIT NULL ); CREATE TABLE #ComputedColumns @@ -22841,14 +22902,15 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Statistics Info With Newer Syntax.',0,1) WITH NOWAIT; SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - INSERT #Statistics ( database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, + INSERT #Statistics ( database_id, database_name, object_id, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition) + no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - @i_DatabaseName AS database_name, - obj.name AS table_name, - sch.name AS schema_name, + @i_DatabaseName AS database_name, + obj.object_id, + obj.name AS table_name, + sch.name AS schema_name, ISNULL(i.name, ''System Or User Statistic'') AS index_name, ca.column_names AS column_names, s.name AS statistics_name, @@ -22864,14 +22926,33 @@ OPTION (RECOMPILE);'; ELSE ddsp.modification_counter END AS percent_modifications, CASE WHEN ddsp.rows < 500 THEN 500 - ELSE CAST(( ddsp.rows * .20 ) + 500 AS INT) + ELSE CAST(( ddsp.rows * .20 ) + 500 AS BIGINT) END AS modifications_before_auto_update, ISNULL(i.type_desc, ''System Or User Statistic - N/A'') AS index_type_desc, CONVERT(DATETIME, obj.create_date) AS table_create_date, CONVERT(DATETIME, obj.modify_date) AS table_modify_date, s.no_recompute, s.has_filter, - s.filter_definition + s.filter_definition, + ' + + CASE WHEN EXISTS + ( + /* We cannot trust checking version numbers, like we did above, because Azure disagrees. */ + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.dm_db_stats_properties', N'IF') AND all_cols.[name] = N'persisted_sample_percent' + ) + THEN N'ddsp.persisted_sample_percent,' + ELSE N'NULL AS persisted_sample_percent,' END + + CASE WHEN EXISTS + ( + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.stats', N'V') AND all_cols.[name] = N'is_incremental' + ) + THEN N's.is_incremental' + ELSE N'NULL AS is_incremental' END + + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj ON s.object_id = obj.object_id @@ -22916,12 +22997,13 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Statistics Info With Older Syntax.',0,1) WITH NOWAIT; SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - INSERT #Statistics(database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, + INSERT #Statistics(database_id, database_name, object_id, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition) + no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName AS database_name, + obj.object_id, obj.name AS table_name, sch.name AS schema_name, ISNULL(i.name, ''System Or User Statistic'') AS index_name, @@ -22935,7 +23017,7 @@ OPTION (RECOMPILE);'; ELSE si.rowmodctr END AS percent_modifications, CASE WHEN si.rowcnt < 500 THEN 500 - ELSE CAST(( si.rowcnt * .20 ) + 500 AS INT) + ELSE CAST(( si.rowcnt * .20 ) + 500 AS BIGINT) END AS modifications_before_auto_update, ISNULL(i.type_desc, ''System Or User Statistic - N/A'') AS index_type_desc, CONVERT(DATETIME, obj.create_date) AS table_create_date, @@ -22944,9 +23026,20 @@ OPTION (RECOMPILE);'; ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N's.has_filter, - s.filter_definition' + s.filter_definition,' ELSE N'NULL AS has_filter, - NULL AS filter_definition' END + NULL AS filter_definition,' END + /* Certainly NULL. This branch does not even join on the table that this column comes from. */ + + N'NULL AS persisted_sample_percent, + ' + + CASE WHEN EXISTS + ( + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.stats', N'V') AND all_cols.[name] = N'is_incremental' + ) + THEN N's.is_incremental' + ELSE N'NULL AS is_incremental' END + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si @@ -25019,7 +25112,7 @@ BEGIN END; ---------------------------------------- - --Statistics Info: Check_id 90-99 + --Statistics Info: Check_id 90-99, as well as 125 ---------------------------------------- RAISERROR(N'check_id 90: Outdated statistics', 0,1) WITH NOWAIT; @@ -25068,6 +25161,43 @@ BEGIN OR (s.rows > 1000000 AND s.percent_sampled < 1) OPTION ( RECOMPILE ); + RAISERROR(N'check_id 125: Persisted Sampling Rates (Unexpected)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 125 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Persisted Sampling Rates (Unexpected)', + s.database_name, + 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, + 'The persisted statistics sample rate is ' + CONVERT(NVARCHAR(100), s.persisted_sample_percent) + '%' + + CASE WHEN @UsualStatisticsSamplingPercent IS NOT NULL + THEN (N' rather than your expected @UsualStatisticsSamplingPercent value of ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + '%') + ELSE '' + END + + N'. This may indicate that somebody is doing statistics rocket surgery. If not, consider updating statistics more frequently.' AS details, + QUOTENAME(database_name) + '.' + QUOTENAME(s.schema_name) + '.' + QUOTENAME(s.table_name) + '.' + QUOTENAME(s.index_name) + '.' + QUOTENAME(s.statistics_name) + '.' + QUOTENAME(s.column_names) AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #Statistics AS s + /* + We have to do float comparison here, so it is time to explain why @UsualStatisticsSamplingPercent is a float. + The foremost reason is that it is a float because we are comparing it to the persisted_sample_percent column in sys.dm_db_stats_properties and that column is a float. + You may correctly object that CREATE STATISTICS with a decimal as your WITH SAMPLE [...] PERCENT is a syntax error and conclude that integers are enough. + However, `WITH SAMPLE [...] ROWS` is allowed with PERSIST_SAMPLE_PERCENT = ON and you can use that to persist a non-integer sample rate. + So, yes, we really have to use floats. + */ + WHERE + /* persisted_sample_percent is either zero or NULL when the statistic is not persisted. */ + s.persisted_sample_percent > 0.0001 + AND + ( + ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) > 0.1 + OR @UsualStatisticsSamplingPercent IS NULL + ) + OPTION ( RECOMPILE ); + RAISERROR(N'check_id 92: Statistics with NO RECOMPUTE', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) @@ -25086,7 +25216,6 @@ BEGIN WHERE s.no_recompute = 1 OPTION ( RECOMPILE ); - RAISERROR(N'check_id 94: Check Constraints That Reference Functions', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) @@ -25158,9 +25287,9 @@ BEGIN JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE index_id NOT IN ( 0, 1 ) AND i.is_unique = 0 - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) OPTION ( RECOMPILE ); IF @percent_NC_indexes_unused >= 5 INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -25191,9 +25320,9 @@ BEGIN WHERE index_id NOT IN ( 0, 1 ) AND i.is_unique = 0 AND total_reads = 0 - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) GROUP BY i.database_name OPTION ( RECOMPILE ); @@ -25444,9 +25573,9 @@ BEGIN AND i.index_id NOT IN (0,1) /*NCs only*/ AND i.is_unique = 0 AND sz.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE sz.total_reserved_MB END - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) ORDER BY i.db_schema_object_indexid OPTION ( RECOMPILE ); @@ -26159,6 +26288,70 @@ BEGIN OPTION ( RECOMPILE ); + /* See check_id 125. */ + RAISERROR(N'check_id 126: Persisted Sampling Rates (Expected)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 126 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Persisted Sampling Rates (Expected)', + s.database_name, + 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, + CONVERT(NVARCHAR(100), COUNT(*)) + ' statistic(s) with a persisted sample rate matching your desired persisted sample rate, ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + N'%. Set @UsualStatisticsSamplingPercent to NULL if you want to see all of them in this result set. Its default value is 100.' AS details, + s.database_name + N' (Entire database)' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #Statistics AS s + WHERE ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) <= 0.1 + AND @UsualStatisticsSamplingPercent IS NOT NULL + GROUP BY s.database_name + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 127: Partitioned Table Without Incremental Statistics', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary, more_info ) + SELECT 127 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Partitioned Table Without Incremental Statistics', + partitioned_tables.database_name, + 'https://sqlperformance.com/2015/05/sql-statistics/improving-maintenance-incremental-statistics' AS URL, + 'The table ' + QUOTENAME(partitioned_tables.schema_name) + '.' + QUOTENAME(partitioned_tables.object_name) + ' is partitioned, but ' + + CONVERT(NVARCHAR(100), incremental_stats_counts.not_incremental_stats_count) + ' of its ' + CONVERT(NVARCHAR(100), incremental_stats_counts.stats_count) + + ' statistics are not incremental. If this is a sliding/rolling window table, then consider making the statistics incremental. If not, then investigate why this table is partitioned.' AS details, + partitioned_tables.object_name + N' (Entire table)' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary, + partitioned_tables.more_info + FROM + ( + SELECT s.database_id, + s.object_id, + COUNT(CASE WHEN s.is_incremental = 0 THEN 1 END) AS not_incremental_stats_count, + COUNT(*) AS stats_count + FROM #Statistics AS s + GROUP BY s.database_id, s.object_id + HAVING COUNT(CASE WHEN s.is_incremental = 0 THEN 1 END) > 0 + ) AS incremental_stats_counts + JOIN + ( + /* Just get the tables. We do not need the indexes. */ + SELECT DISTINCT i.database_name, + i.database_id, + i.object_id, + i.schema_name, + i.object_name, + /* This is a little bit dishonest, since it tells us nothing about if the statistics are incremental. */ + i.more_info + FROM #IndexSanity AS i + WHERE i.partition_key_column_name IS NOT NULL + ) AS partitioned_tables + ON partitioned_tables.database_id = incremental_stats_counts.database_id AND partitioned_tables.object_id = incremental_stats_counts.object_id + /* No need for a GROUP BY. What we are joining on has exactly one row in each sub-query. */ + OPTION ( RECOMPILE ); END /* IF @Mode = 4 */ @@ -27404,7 +27597,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF @VersionCheckMode = 1 BEGIN @@ -27951,19 +28144,17 @@ BEGIN @StringToExecute = N'SELECT @r = o.name FROM ' + @OutputDatabaseName + - N'.sys.objects AS o WHERE o.type_desc = N''USER_TABLE'' AND o.name = ' + + N'.sys.objects AS o inner join ' + + @OutputDatabaseName + + N'.sys.schemas as s on o.schema_id = s.schema_id WHERE o.type_desc = N''USER_TABLE'' AND o.name = ' + QUOTENAME ( @OutputTableName, N'''' ) + - N' AND o.schema_id = SCHEMA_ID(' + - QUOTENAME - ( - @OutputSchemaName, - N'''' - ) + - N');', + N' AND s.name =''' + + @OutputSchemaName + + N''';', @StringToExecuteParams = N'@r sysname OUTPUT'; @@ -28205,12 +28396,12 @@ BEGIN ) BEGIN RAISERROR('Found synonym DeadlockFindings, dropping', 0, 1) WITH NOWAIT; - DROP SYNONYM DeadlockFindings; + DROP SYNONYM dbo.DeadlockFindings; END; RAISERROR('Creating synonym DeadlockFindings', 0, 1) WITH NOWAIT; SET @StringToExecute = - N'CREATE SYNONYM DeadlockFindings FOR ' + + N'CREATE SYNONYM dbo.DeadlockFindings FOR ' + @OutputDatabaseName + N'.' + @OutputSchemaName + @@ -28232,12 +28423,12 @@ BEGIN ) BEGIN RAISERROR('Found synonym DeadLockTbl, dropping', 0, 1) WITH NOWAIT; - DROP SYNONYM DeadLockTbl; + DROP SYNONYM dbo.DeadLockTbl; END; RAISERROR('Creating synonym DeadLockTbl', 0, 1) WITH NOWAIT; SET @StringToExecute = - N'CREATE SYNONYM DeadLockTbl FOR ' + + N'CREATE SYNONYM dbo.DeadLockTbl FOR ' + @OutputDatabaseName + N'.' + @OutputSchemaName + @@ -31467,7 +31658,7 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - DROP SYNONYM DeadLockTbl; + DROP SYNONYM dbo.DeadLockTbl; SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Findings to table %s', 0, 1, @d) WITH NOWAIT; @@ -31497,7 +31688,7 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - DROP SYNONYM DeadlockFindings; /*done with inserting.*/ + DROP SYNONYM dbo.DeadlockFindings; /*done with inserting.*/ END; ELSE /*Output to database is not set output to client app*/ BEGIN @@ -31937,7 +32128,7 @@ ALTER PROCEDURE dbo.sp_BlitzWho @CheckDateOverride DATETIMEOFFSET = NULL, @ShowActualParameters BIT = 0, @GetOuterCommand BIT = 0, - @GetLiveQueryPlan BIT = 0, + @GetLiveQueryPlan BIT = NULL, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0, @@ -31948,7 +32139,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -32000,7 +32191,8 @@ RETURN; END; /* @Help = 1 */ /* Get the major and minor build numbers */ -DECLARE @ProductVersion NVARCHAR(128) +DECLARE @ProductVersion NVARCHAR(128) = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) + ,@EngineEdition INT = CAST(SERVERPROPERTY('EngineEdition') AS INT) ,@ProductVersionMajor DECIMAL(10,2) ,@ProductVersionMinor DECIMAL(10,2) ,@Platform NVARCHAR(8) /* Azure or NonAzure are acceptable */ = (SELECT CASE WHEN @@VERSION LIKE '%Azure%' THEN N'Azure' ELSE N'NonAzure' END AS [Platform]) @@ -32037,7 +32229,6 @@ DECLARE @ProductVersion NVARCHAR(128) /* Let's get @SortOrder set to lower case here for comparisons later */ SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) @@ -32048,6 +32239,14 @@ SELECT @OutputTableName = QUOTENAME(@OutputTableName), @LineFeed = CHAR(13) + CHAR(10); +IF @GetLiveQueryPlan IS NULL + BEGIN + IF @ProductVersionMajor >= 16 OR @EngineEdition NOT IN (1, 2, 3, 4) + SET @GetLiveQueryPlan = 1; + ELSE + SET @GetLiveQueryPlan = 0; + END + IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL AND EXISTS ( SELECT * FROM sys.databases @@ -32835,7 +33034,7 @@ IF @ProductVersionMajor >= 11 END+N' derp.query_plan , CAST(COALESCE(qs_live.Query_Plan, ' + CASE WHEN @GetLiveQueryPlan=1 - THEN '''''' + THEN '''''' ELSE '''''' END +') AS XML @@ -33361,7 +33560,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -35024,14 +35223,14 @@ ALTER PROCEDURE [dbo].[sp_ineachdb] @VersionDate datetime = NULL OUTPUT, @VersionCheckMode bit = 0, @is_ag_writeable_copy bit = 0, - @is_query_store_on bit = 0 + @is_query_store_on bit = NULL -- WITH EXECUTE AS OWNER – maybe not a great idea, depending on the security of your system AS BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -35410,10 +35609,15 @@ DELETE FROM dbo.SqlServerVersions; INSERT INTO dbo.SqlServerVersions (MajorVersionNumber, MinorVersionNumber, Branch, [Url], ReleaseDate, MainstreamSupportEndDate, ExtendedSupportEndDate, MajorVersionName, MinorVersionName) VALUES - /*2022*/ + /*2025*/ + (17, 925, 'RC1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-09-17', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC1'), + (17, 900, 'RC0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-08-20', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC0'), (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.1'), (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.0'), /*2022*/ + (16, 4215, 'CU21', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate21', '2025-09-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 21'), + (16, 4205, 'CU20', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate20', '2025-07-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 20'), + (16, 4200, 'CU19 GDR', 'https://support.microsoft.com/en-us/help/5058721', '2025-07-08', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19 GDR'), (16, 4195, 'CU19', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate19', '2025-05-19', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19'), (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'), @@ -35439,8 +35643,11 @@ 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, 4445, 'CU32 GDR', 'https://support.microsoft.com/kb/5065222', '2025-09-09', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32 GDR'), + (15, 4440, 'CU32 GDR', 'https://support.microsoft.com/kb/5063757', '2025-08-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32 GDR'), + (15, 4435, 'CU32 GDR', 'https://support.microsoft.com/kb/5058722', '2025-07-08', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32 GDR'), + (15, 4430, '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, 4420, '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'), @@ -35479,6 +35686,9 @@ VALUES (15, 2070, 'GDR', 'https://support.microsoft.com/en-us/help/4517790', '2019-11-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'RTM GDR '), (15, 2000, 'RTM ', '', '2019-11-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'RTM '), /*2017*/ + (14, 3505, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5065225', '2025-09-09', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3500, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5063759', '2025-08-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3495, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5058714', '2025-07-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3485, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5046858', '2024-11-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3480, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5046061', '2024-10-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3475, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5042215', '2024-09-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), @@ -35520,6 +35730,7 @@ VALUES (14, 3006, 'RTM CU1', 'https://support.microsoft.com/en-us/help/4038634', '2017-10-24', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 1'), (14, 1000, 'RTM ', '', '2017-10-02', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM '), /*2016*/ + (13, 7055, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5058717', '2025-07-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7045, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5046062', '2024-10-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7040, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5042209', '2024-09-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7037, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5040944', '2024-07-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), @@ -35527,6 +35738,9 @@ VALUES (13, 7024, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5021128', '2023-02-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7016, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5015371', '2022-06-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7000, 'SP3 Azure Feature Pack', 'https://support.microsoft.com/en-us/help/5014242', '2022-05-19', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack'), + (13, 6470, 'SP3 GDR', 'https://support.microsoft.com/kb/5065226', '2025-09-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6465, 'SP3 GDR', 'https://support.microsoft.com/kb/5063762', '2025-08-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6460, 'SP3 GDR', 'https://support.microsoft.com/kb/5058718', '2025-07-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), (13, 6455, 'SP3 GDR', 'https://support.microsoft.com/kb/5046855', '2024-11-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), (13, 6450, 'SP3 GDR', 'https://support.microsoft.com/kb/5046063', '2024-10-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), (13, 6445, 'SP3 GDR', 'https://support.microsoft.com/kb/5042207', '2024-09-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), @@ -35898,7 +36112,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -37489,7 +37703,11 @@ BEGIN 'Maintenance Tasks Running' AS FindingGroup, 'Restore Running' AS Finding, 'https://www.brentozar.com/askbrent/backups/' AS URL, - 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, + 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), + (SELECT db1.name FROM sys.databases db1 + LEFT OUTER JOIN sys.databases db2 ON db1.name <> db2.name AND db1.state_desc = db2.state_desc + WHERE db1.state_desc = 'RESTORING' AND db2.name IS NULL), + 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown ') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' AS Details, 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, pl.query_plan AS QueryPlan, r.start_time AS StartTime, @@ -38439,6 +38657,27 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END + /* Server Performance - Azure Operation Ongoing - CheckID 53 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 53',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_operation_status') + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 53 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Azure Operation ' + CASE WHEN state IN (2, 3, 5) THEN 'Ended Recently' ELSE 'Ongoing' END AS Finding, + 'https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-operation-status-azure-sql-database' AS URL, + N'Operation: ' + operation + N' State: ' + state_desc + N' Percent Complete: ' + CAST(percent_complete AS NVARCHAR(10)) + @LineFeed + + N' On: ' + CAST(resource_type_desc AS NVARCHAR(100)) + N':' + CAST(major_resource_id AS NVARCHAR(100)) + @LineFeed + + N' Started: ' + CAST(start_time AS NVARCHAR(100)) + N' Last Modified Time: ' + CAST(last_modify_time AS NVARCHAR(100)) + @LineFeed + + N' For more information, query SELECT * FROM sys.dm_operation_status; ' AS Details + FROM sys.dm_operation_status + END + + /* Potential Upcoming Problems - High Number of Connections - CheckID 49 */ IF (@Debug = 1) BEGIN @@ -38468,6 +38707,27 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END END + + + /* Server Performance - Memory Dangerously Low Recently - CheckID 52 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 52',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_os_memory_health_history') + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 52 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Memory Dangerously Low Recently' AS Finding, + 'https://www.brentozar.com/go/memhist' AS URL, + N'As recently as ' + CONVERT(NVARCHAR(19), snapshot_time, 120) + N', memory health issues are being reported in sys.dm_os_memory_health_history, indicating extreme memory pressure.' AS Details + FROM sys.dm_os_memory_health_history + WHERE severity_level > 1; + END + + RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; diff --git a/Install-Azure.sql b/Install-Azure.sql index fbbe2acf5..5f0a15cdf 100644 --- a/Install-Azure.sql +++ b/Install-Azure.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -1174,7 +1174,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -4963,12 +4963,12 @@ SELECT @@SPID AS SPID, AND ci.comma_paren_charindex > 0 THEN SUBSTRING(ci.expression, ci.paren_charindex, ci.comma_paren_charindex) END AS converted_to, - CASE WHEN ci.at_charindex = 0 + LEFT(CASE WHEN ci.at_charindex = 0 AND ci.convert_implicit_charindex = 0 AND ci.proc_name = 'Statement' THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) ELSE '**idk_man**' - END AS compile_time_value + END, 258) AS compile_time_value FROM #conversion_info AS ci OPTION (RECOMPILE); @@ -8558,7 +8558,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -10149,7 +10149,11 @@ BEGIN 'Maintenance Tasks Running' AS FindingGroup, 'Restore Running' AS Finding, 'https://www.brentozar.com/askbrent/backups/' AS URL, - 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' AS Details, + 'Restore of ' + COALESCE(DB_NAME(db.resource_database_id), + (SELECT db1.name FROM sys.databases db1 + LEFT OUTER JOIN sys.databases db2 ON db1.name <> db2.name AND db1.state_desc = db2.state_desc + WHERE db1.state_desc = 'RESTORING' AND db2.name IS NULL), + 'Unknown Database') + ' database (' + COALESCE((SELECT CAST(CAST(SUM(size * 8.0 / 1024 / 1024) AS BIGINT) AS NVARCHAR) FROM #MasterFiles WHERE database_id = db.resource_database_id), 'Unknown ') + 'GB) is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' AS Details, 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, pl.query_plan AS QueryPlan, r.start_time AS StartTime, @@ -11099,6 +11103,27 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END + /* Server Performance - Azure Operation Ongoing - CheckID 53 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 53',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_operation_status') + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 53 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Azure Operation ' + CASE WHEN state IN (2, 3, 5) THEN 'Ended Recently' ELSE 'Ongoing' END AS Finding, + 'https://learn.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-operation-status-azure-sql-database' AS URL, + N'Operation: ' + operation + N' State: ' + state_desc + N' Percent Complete: ' + CAST(percent_complete AS NVARCHAR(10)) + @LineFeed + + N' On: ' + CAST(resource_type_desc AS NVARCHAR(100)) + N':' + CAST(major_resource_id AS NVARCHAR(100)) + @LineFeed + + N' Started: ' + CAST(start_time AS NVARCHAR(100)) + N' Last Modified Time: ' + CAST(last_modify_time AS NVARCHAR(100)) + @LineFeed + + N' For more information, query SELECT * FROM sys.dm_operation_status; ' AS Details + FROM sys.dm_operation_status + END + + /* Potential Upcoming Problems - High Number of Connections - CheckID 49 */ IF (@Debug = 1) BEGIN @@ -11128,6 +11153,27 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END END + + + /* Server Performance - Memory Dangerously Low Recently - CheckID 52 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 52',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_os_memory_health_history') + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 52 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Memory Dangerously Low Recently' AS Finding, + 'https://www.brentozar.com/go/memhist' AS URL, + N'As recently as ' + CONVERT(NVARCHAR(19), snapshot_time, 120) + N', memory health issues are being reported in sys.dm_os_memory_health_history, indicating extreme memory pressure.' AS Details + FROM sys.dm_os_memory_health_history + WHERE severity_level > 1; + END + + RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; @@ -13612,6 +13658,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex /*Note:@Filter doesn't do anything unless @Mode=0*/ @SkipPartitions BIT = 0, @SkipStatistics BIT = 1, + @UsualStatisticsSamplingPercent FLOAT = 100, /* FLOAT to match sys.dm_db_stats_properties. More detail later. 100 by default because Brent suggests that if people are persisting statistics at all, they are probably doing 100 in lots of places and not filtering that out would produce noise. */ @GetAllDatabases BIT = 0, @ShowColumnstoreOnly BIT = 0, /* Will show only the Row Group and Segment details for a table with a columnstore index. */ @BringThePain BIT = 0, @@ -13638,7 +13685,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -13763,6 +13810,12 @@ BEGIN RETURN; END; +IF(@UsualStatisticsSamplingPercent <= 0 OR @UsualStatisticsSamplingPercent > 100) +BEGIN + RAISERROR('Invalid value for parameter @UsualStatisticsSamplingPercent. Expected: 1 to 100',12,1); + RETURN; +END; + IF(@OutputType = 'TABLE' AND NOT (@OutputTableName IS NULL AND @OutputSchemaName IS NULL AND @OutputDatabaseName IS NULL AND @OutputServerName IS NULL)) BEGIN RAISERROR(N'One or more output parameters specified in combination with TABLE output, changing to NONE output mode', 0,1) WITH NOWAIT; @@ -14308,6 +14361,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL CREATE TABLE #Statistics ( database_id INT NOT NULL, database_name NVARCHAR(256) NOT NULL, + object_id INT NOT NULL, table_name NVARCHAR(128) NULL, schema_name NVARCHAR(128) NULL, index_name NVARCHAR(128) NULL, @@ -14321,13 +14375,15 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL histogram_steps INT NULL, modification_counter BIGINT NULL, percent_modifications DECIMAL(18, 1) NULL, - modifications_before_auto_update INT NULL, + modifications_before_auto_update BIGINT NULL, index_type_desc NVARCHAR(128) NULL, table_create_date DATETIME NULL, table_modify_date DATETIME NULL, no_recompute BIT NULL, has_filter BIT NULL, - filter_definition NVARCHAR(MAX) NULL + filter_definition NVARCHAR(MAX) NULL, + persisted_sample_percent FLOAT NULL, + is_incremental BIT NULL ); CREATE TABLE #ComputedColumns @@ -15865,14 +15921,15 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Statistics Info With Newer Syntax.',0,1) WITH NOWAIT; SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - INSERT #Statistics ( database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, + INSERT #Statistics ( database_id, database_name, object_id, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition) + no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - @i_DatabaseName AS database_name, - obj.name AS table_name, - sch.name AS schema_name, + @i_DatabaseName AS database_name, + obj.object_id, + obj.name AS table_name, + sch.name AS schema_name, ISNULL(i.name, ''System Or User Statistic'') AS index_name, ca.column_names AS column_names, s.name AS statistics_name, @@ -15888,14 +15945,33 @@ OPTION (RECOMPILE);'; ELSE ddsp.modification_counter END AS percent_modifications, CASE WHEN ddsp.rows < 500 THEN 500 - ELSE CAST(( ddsp.rows * .20 ) + 500 AS INT) + ELSE CAST(( ddsp.rows * .20 ) + 500 AS BIGINT) END AS modifications_before_auto_update, ISNULL(i.type_desc, ''System Or User Statistic - N/A'') AS index_type_desc, CONVERT(DATETIME, obj.create_date) AS table_create_date, CONVERT(DATETIME, obj.modify_date) AS table_modify_date, s.no_recompute, s.has_filter, - s.filter_definition + s.filter_definition, + ' + + CASE WHEN EXISTS + ( + /* We cannot trust checking version numbers, like we did above, because Azure disagrees. */ + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.dm_db_stats_properties', N'IF') AND all_cols.[name] = N'persisted_sample_percent' + ) + THEN N'ddsp.persisted_sample_percent,' + ELSE N'NULL AS persisted_sample_percent,' END + + CASE WHEN EXISTS + ( + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.stats', N'V') AND all_cols.[name] = N'is_incremental' + ) + THEN N's.is_incremental' + ELSE N'NULL AS is_incremental' END + + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj ON s.object_id = obj.object_id @@ -15940,12 +16016,13 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Statistics Info With Older Syntax.',0,1) WITH NOWAIT; SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - INSERT #Statistics(database_id, database_name, table_name, schema_name, index_name, column_names, statistics_name, + INSERT #Statistics(database_id, database_name, object_id, table_name, schema_name, index_name, column_names, statistics_name, last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, - no_recompute, has_filter, filter_definition) + no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName AS database_name, + obj.object_id, obj.name AS table_name, sch.name AS schema_name, ISNULL(i.name, ''System Or User Statistic'') AS index_name, @@ -15959,7 +16036,7 @@ OPTION (RECOMPILE);'; ELSE si.rowmodctr END AS percent_modifications, CASE WHEN si.rowcnt < 500 THEN 500 - ELSE CAST(( si.rowcnt * .20 ) + 500 AS INT) + ELSE CAST(( si.rowcnt * .20 ) + 500 AS BIGINT) END AS modifications_before_auto_update, ISNULL(i.type_desc, ''System Or User Statistic - N/A'') AS index_type_desc, CONVERT(DATETIME, obj.create_date) AS table_create_date, @@ -15968,9 +16045,20 @@ OPTION (RECOMPILE);'; ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N's.has_filter, - s.filter_definition' + s.filter_definition,' ELSE N'NULL AS has_filter, - NULL AS filter_definition' END + NULL AS filter_definition,' END + /* Certainly NULL. This branch does not even join on the table that this column comes from. */ + + N'NULL AS persisted_sample_percent, + ' + + CASE WHEN EXISTS + ( + SELECT 1 + FROM sys.all_columns AS all_cols + WHERE all_cols.[object_id] = OBJECT_ID(N'sys.stats', N'V') AND all_cols.[name] = N'is_incremental' + ) + THEN N's.is_incremental' + ELSE N'NULL AS is_incremental' END + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si @@ -18043,7 +18131,7 @@ BEGIN END; ---------------------------------------- - --Statistics Info: Check_id 90-99 + --Statistics Info: Check_id 90-99, as well as 125 ---------------------------------------- RAISERROR(N'check_id 90: Outdated statistics', 0,1) WITH NOWAIT; @@ -18092,6 +18180,43 @@ BEGIN OR (s.rows > 1000000 AND s.percent_sampled < 1) OPTION ( RECOMPILE ); + RAISERROR(N'check_id 125: Persisted Sampling Rates (Unexpected)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 125 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Persisted Sampling Rates (Unexpected)', + s.database_name, + 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, + 'The persisted statistics sample rate is ' + CONVERT(NVARCHAR(100), s.persisted_sample_percent) + '%' + + CASE WHEN @UsualStatisticsSamplingPercent IS NOT NULL + THEN (N' rather than your expected @UsualStatisticsSamplingPercent value of ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + '%') + ELSE '' + END + + N'. This may indicate that somebody is doing statistics rocket surgery. If not, consider updating statistics more frequently.' AS details, + QUOTENAME(database_name) + '.' + QUOTENAME(s.schema_name) + '.' + QUOTENAME(s.table_name) + '.' + QUOTENAME(s.index_name) + '.' + QUOTENAME(s.statistics_name) + '.' + QUOTENAME(s.column_names) AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #Statistics AS s + /* + We have to do float comparison here, so it is time to explain why @UsualStatisticsSamplingPercent is a float. + The foremost reason is that it is a float because we are comparing it to the persisted_sample_percent column in sys.dm_db_stats_properties and that column is a float. + You may correctly object that CREATE STATISTICS with a decimal as your WITH SAMPLE [...] PERCENT is a syntax error and conclude that integers are enough. + However, `WITH SAMPLE [...] ROWS` is allowed with PERSIST_SAMPLE_PERCENT = ON and you can use that to persist a non-integer sample rate. + So, yes, we really have to use floats. + */ + WHERE + /* persisted_sample_percent is either zero or NULL when the statistic is not persisted. */ + s.persisted_sample_percent > 0.0001 + AND + ( + ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) > 0.1 + OR @UsualStatisticsSamplingPercent IS NULL + ) + OPTION ( RECOMPILE ); + RAISERROR(N'check_id 92: Statistics with NO RECOMPUTE', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) @@ -18110,7 +18235,6 @@ BEGIN WHERE s.no_recompute = 1 OPTION ( RECOMPILE ); - RAISERROR(N'check_id 94: Check Constraints That Reference Functions', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, secret_columns, index_usage_summary, index_size_summary ) @@ -18182,9 +18306,9 @@ BEGIN JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE index_id NOT IN ( 0, 1 ) AND i.is_unique = 0 - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) OPTION ( RECOMPILE ); IF @percent_NC_indexes_unused >= 5 INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -18215,9 +18339,9 @@ BEGIN WHERE index_id NOT IN ( 0, 1 ) AND i.is_unique = 0 AND total_reads = 0 - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) GROUP BY i.database_name OPTION ( RECOMPILE ); @@ -18468,9 +18592,9 @@ BEGIN AND i.index_id NOT IN (0,1) /*NCs only*/ AND i.is_unique = 0 AND sz.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE sz.total_reserved_MB END - /*Skipping tables created in the last week, or modified in past 2 days*/ - AND i.create_date >= DATEADD(dd,-7,GETDATE()) - AND i.modify_date > DATEADD(dd,-2,GETDATE()) + /*Skipping tables created in the last week, or modified in past 2 days*/ + AND i.create_date < DATEADD(dd,-7,GETDATE()) + AND i.modify_date < DATEADD(dd,-2,GETDATE()) ORDER BY i.db_schema_object_indexid OPTION ( RECOMPILE ); @@ -19183,6 +19307,70 @@ BEGIN OPTION ( RECOMPILE ); + /* See check_id 125. */ + RAISERROR(N'check_id 126: Persisted Sampling Rates (Expected)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 126 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Persisted Sampling Rates (Expected)', + s.database_name, + 'https://www.youtube.com/watch?v=V5illj_KOJg&t=758s' AS URL, + CONVERT(NVARCHAR(100), COUNT(*)) + ' statistic(s) with a persisted sample rate matching your desired persisted sample rate, ' + CONVERT(NVARCHAR(100), @UsualStatisticsSamplingPercent) + N'%. Set @UsualStatisticsSamplingPercent to NULL if you want to see all of them in this result set. Its default value is 100.' AS details, + s.database_name + N' (Entire database)' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #Statistics AS s + WHERE ABS(@UsualStatisticsSamplingPercent - s.persisted_sample_percent) <= 0.1 + AND @UsualStatisticsSamplingPercent IS NOT NULL + GROUP BY s.database_name + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 127: Partitioned Table Without Incremental Statistics', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary, more_info ) + SELECT 127 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Partitioned Table Without Incremental Statistics', + partitioned_tables.database_name, + 'https://sqlperformance.com/2015/05/sql-statistics/improving-maintenance-incremental-statistics' AS URL, + 'The table ' + QUOTENAME(partitioned_tables.schema_name) + '.' + QUOTENAME(partitioned_tables.object_name) + ' is partitioned, but ' + + CONVERT(NVARCHAR(100), incremental_stats_counts.not_incremental_stats_count) + ' of its ' + CONVERT(NVARCHAR(100), incremental_stats_counts.stats_count) + + ' statistics are not incremental. If this is a sliding/rolling window table, then consider making the statistics incremental. If not, then investigate why this table is partitioned.' AS details, + partitioned_tables.object_name + N' (Entire table)' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary, + partitioned_tables.more_info + FROM + ( + SELECT s.database_id, + s.object_id, + COUNT(CASE WHEN s.is_incremental = 0 THEN 1 END) AS not_incremental_stats_count, + COUNT(*) AS stats_count + FROM #Statistics AS s + GROUP BY s.database_id, s.object_id + HAVING COUNT(CASE WHEN s.is_incremental = 0 THEN 1 END) > 0 + ) AS incremental_stats_counts + JOIN + ( + /* Just get the tables. We do not need the indexes. */ + SELECT DISTINCT i.database_name, + i.database_id, + i.object_id, + i.schema_name, + i.object_name, + /* This is a little bit dishonest, since it tells us nothing about if the statistics are incremental. */ + i.more_info + FROM #IndexSanity AS i + WHERE i.partition_key_column_name IS NOT NULL + ) AS partitioned_tables + ON partitioned_tables.database_id = incremental_stats_counts.database_id AND partitioned_tables.object_id = incremental_stats_counts.object_id + /* No need for a GROUP BY. What we are joining on has exactly one row in each sub-query. */ + OPTION ( RECOMPILE ); END /* IF @Mode = 4 */ @@ -20428,7 +20616,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF @VersionCheckMode = 1 BEGIN @@ -20975,19 +21163,17 @@ BEGIN @StringToExecute = N'SELECT @r = o.name FROM ' + @OutputDatabaseName + - N'.sys.objects AS o WHERE o.type_desc = N''USER_TABLE'' AND o.name = ' + + N'.sys.objects AS o inner join ' + + @OutputDatabaseName + + N'.sys.schemas as s on o.schema_id = s.schema_id WHERE o.type_desc = N''USER_TABLE'' AND o.name = ' + QUOTENAME ( @OutputTableName, N'''' ) + - N' AND o.schema_id = SCHEMA_ID(' + - QUOTENAME - ( - @OutputSchemaName, - N'''' - ) + - N');', + N' AND s.name =''' + + @OutputSchemaName + + N''';', @StringToExecuteParams = N'@r sysname OUTPUT'; @@ -21229,12 +21415,12 @@ BEGIN ) BEGIN RAISERROR('Found synonym DeadlockFindings, dropping', 0, 1) WITH NOWAIT; - DROP SYNONYM DeadlockFindings; + DROP SYNONYM dbo.DeadlockFindings; END; RAISERROR('Creating synonym DeadlockFindings', 0, 1) WITH NOWAIT; SET @StringToExecute = - N'CREATE SYNONYM DeadlockFindings FOR ' + + N'CREATE SYNONYM dbo.DeadlockFindings FOR ' + @OutputDatabaseName + N'.' + @OutputSchemaName + @@ -21256,12 +21442,12 @@ BEGIN ) BEGIN RAISERROR('Found synonym DeadLockTbl, dropping', 0, 1) WITH NOWAIT; - DROP SYNONYM DeadLockTbl; + DROP SYNONYM dbo.DeadLockTbl; END; RAISERROR('Creating synonym DeadLockTbl', 0, 1) WITH NOWAIT; SET @StringToExecute = - N'CREATE SYNONYM DeadLockTbl FOR ' + + N'CREATE SYNONYM dbo.DeadLockTbl FOR ' + @OutputDatabaseName + N'.' + @OutputSchemaName + @@ -24491,7 +24677,7 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - DROP SYNONYM DeadLockTbl; + DROP SYNONYM dbo.DeadLockTbl; SET @d = CONVERT(varchar(40), GETDATE(), 109); RAISERROR('Findings to table %s', 0, 1, @d) WITH NOWAIT; @@ -24521,7 +24707,7 @@ BEGIN RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - DROP SYNONYM DeadlockFindings; /*done with inserting.*/ + DROP SYNONYM dbo.DeadlockFindings; /*done with inserting.*/ END; ELSE /*Output to database is not set output to client app*/ BEGIN @@ -24961,7 +25147,7 @@ ALTER PROCEDURE dbo.sp_BlitzWho @CheckDateOverride DATETIMEOFFSET = NULL, @ShowActualParameters BIT = 0, @GetOuterCommand BIT = 0, - @GetLiveQueryPlan BIT = 0, + @GetLiveQueryPlan BIT = NULL, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0, @@ -24972,7 +25158,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN @@ -25024,7 +25210,8 @@ RETURN; END; /* @Help = 1 */ /* Get the major and minor build numbers */ -DECLARE @ProductVersion NVARCHAR(128) +DECLARE @ProductVersion NVARCHAR(128) = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) + ,@EngineEdition INT = CAST(SERVERPROPERTY('EngineEdition') AS INT) ,@ProductVersionMajor DECIMAL(10,2) ,@ProductVersionMinor DECIMAL(10,2) ,@Platform NVARCHAR(8) /* Azure or NonAzure are acceptable */ = (SELECT CASE WHEN @@VERSION LIKE '%Azure%' THEN N'Azure' ELSE N'NonAzure' END AS [Platform]) @@ -25061,7 +25248,6 @@ DECLARE @ProductVersion NVARCHAR(128) /* Let's get @SortOrder set to lower case here for comparisons later */ SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) @@ -25072,6 +25258,14 @@ SELECT @OutputTableName = QUOTENAME(@OutputTableName), @LineFeed = CHAR(13) + CHAR(10); +IF @GetLiveQueryPlan IS NULL + BEGIN + IF @ProductVersionMajor >= 16 OR @EngineEdition NOT IN (1, 2, 3, 4) + SET @GetLiveQueryPlan = 1; + ELSE + SET @GetLiveQueryPlan = 0; + END + IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL AND EXISTS ( SELECT * FROM sys.databases @@ -25859,7 +26053,7 @@ IF @ProductVersionMajor >= 11 END+N' derp.query_plan , CAST(COALESCE(qs_live.Query_Plan, ' + CASE WHEN @GetLiveQueryPlan=1 - THEN '''''' + THEN '''''' ELSE '''''' END +') AS XML diff --git a/SqlServerVersions.sql b/SqlServerVersions.sql index cb1bb869a..3bb177a6a 100644 --- a/SqlServerVersions.sql +++ b/SqlServerVersions.sql @@ -41,10 +41,13 @@ DELETE FROM dbo.SqlServerVersions; INSERT INTO dbo.SqlServerVersions (MajorVersionNumber, MinorVersionNumber, Branch, [Url], ReleaseDate, MainstreamSupportEndDate, ExtendedSupportEndDate, MajorVersionName, MinorVersionName) VALUES - /*2022*/ + /*2025*/ + (17, 925, 'RC1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-09-17', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC1'), + (17, 900, 'RC0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-08-20', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC0'), (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.1'), (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.0'), /*2022*/ + (16, 4215, 'CU21', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate21', '2025-09-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 21'), (16, 4205, 'CU20', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate20', '2025-07-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 20'), (16, 4200, 'CU19 GDR', 'https://support.microsoft.com/en-us/help/5058721', '2025-07-08', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19 GDR'), (16, 4195, 'CU19', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate19', '2025-05-19', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19'), @@ -72,6 +75,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, 4445, 'CU32 GDR', 'https://support.microsoft.com/kb/5065222', '2025-09-09', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32 GDR'), + (15, 4440, 'CU32 GDR', 'https://support.microsoft.com/kb/5063757', '2025-08-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32 GDR'), (15, 4435, 'CU32 GDR', 'https://support.microsoft.com/kb/5058722', '2025-07-08', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 32 GDR'), (15, 4430, '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, 4420, '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'), @@ -113,6 +118,8 @@ VALUES (15, 2070, 'GDR', 'https://support.microsoft.com/en-us/help/4517790', '2019-11-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'RTM GDR '), (15, 2000, 'RTM ', '', '2019-11-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'RTM '), /*2017*/ + (14, 3505, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5065225', '2025-09-09', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3500, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5063759', '2025-08-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3495, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5058714', '2025-07-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3485, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5046858', '2024-11-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), (14, 3480, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5046061', '2024-10-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), @@ -163,6 +170,9 @@ VALUES (13, 7024, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5021128', '2023-02-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7016, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5015371', '2022-06-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), (13, 7000, 'SP3 Azure Feature Pack', 'https://support.microsoft.com/en-us/help/5014242', '2022-05-19', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack'), + (13, 6470, 'SP3 GDR', 'https://support.microsoft.com/kb/5065226', '2025-09-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6465, 'SP3 GDR', 'https://support.microsoft.com/kb/5063762', '2025-08-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6460, 'SP3 GDR', 'https://support.microsoft.com/kb/5058718', '2025-07-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), (13, 6455, 'SP3 GDR', 'https://support.microsoft.com/kb/5046855', '2024-11-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), (13, 6450, 'SP3 GDR', 'https://support.microsoft.com/kb/5046063', '2024-10-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), (13, 6445, 'SP3 GDR', 'https://support.microsoft.com/kb/5042207', '2024-09-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), diff --git a/sp_Blitz.sql b/sp_Blitz.sql index b0d360120..6bd44a704 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -39,7 +39,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzAnalysis.sql b/sp_BlitzAnalysis.sql index b30dedd22..09f11f49e 100644 --- a/sp_BlitzAnalysis.sql +++ b/sp_BlitzAnalysis.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzBackups.sql b/sp_BlitzBackups.sql index 822d79f53..dafec335f 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.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index 7d4175fdd..0df4e8a4e 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -283,7 +283,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 912452296..f5dda196e 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.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 7821b1d22..57e2760cb 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -50,7 +50,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index bd280b174..bd4e75da7 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.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF @VersionCheckMode = 1 BEGIN diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index 0267ff3cf..a9b61f89d 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.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_DatabaseRestore.sql b/sp_DatabaseRestore.sql index f4b5cb858..1a087860f 100755 --- a/sp_DatabaseRestore.sql +++ b/sp_DatabaseRestore.sql @@ -58,7 +58,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.25', @VersionDate = '20250704'; +SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index 5f81ee422..eca2b7a98 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -37,7 +37,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.25', @VersionDate = '20250704'; + SELECT @Version = '8.26', @VersionDate = '20251002'; IF(@VersionCheckMode = 1) BEGIN From 409baed6d1d464bbd2a859c98549d8d29db76fc3 Mon Sep 17 00:00:00 2001 From: Ben <60398078+bwiggin10@users.noreply.github.com> Date: Wed, 8 Oct 2025 13:28:00 -0400 Subject: [PATCH 52/76] Fix database object-related trace flag check wording This adds additional context to the checks for Trace Flags 834, 7745 and 7752 that have messages dependent on @CheckUserDatabasesObjects = 1 (Or that and @BringThePain = 1 for database counts over 50). So when you just ran sp_Blitz, you could end up with Trace Flag 7745 telling you that no databases have query store enabled even if you do (Because the check @QueryStoreInUse couldn't run) --- sp_Blitz.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 6bd44a704..102cbdc2f 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -8562,6 +8562,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT CASE WHEN [T].[TraceFlag] = '652' THEN '652 enabled globally, which disables pre-fetching during index scans. This is usually a very bad idea.' WHEN [T].[TraceFlag] = '661' THEN '661 enabled globally, which disables ghost record removal, causing the database to grow in size. This is usually a very bad idea.' WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN '834 is enabled globally, but you also have columnstore indexes. That combination is not recommended by Microsoft.' + WHEN [T].[TraceFlag] = '834' AND @CheckUserDatabaseObjects = 0 THEN '834 is enabled globally, but @CheckUserDatabaseObjects was set to 0, so we skipped checking if any databases have columnstore indexes. That combination is not recommended by Microsoft.' WHEN [T].[TraceFlag] = '1117' THEN '1117 enabled globally, which grows all files in a filegroup at the same time.' WHEN [T].[TraceFlag] = '1118' THEN '1118 enabled globally, which tries to reduce SGAM waits.' WHEN [T].[TraceFlag] = '1211' THEN '1211 enabled globally, which disables lock escalation when you least expect it. This is usually a very bad idea.' @@ -8575,10 +8576,12 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT WHEN [T].[TraceFlag] = '3226' THEN '3226 enabled globally, which keeps the event log clean by not reporting successful backups.' WHEN [T].[TraceFlag] = '3505' THEN '3505 enabled globally, which disables Checkpoints. This is usually a very bad idea.' WHEN [T].[TraceFlag] = '4199' THEN '4199 enabled globally, which enables non-default Query Optimizer fixes, changing query plans from the default behaviors.' + WHEN [T].[TraceFlag] = '7745' AND @CheckUserDatabaseObjects = 0 THEN '7745 enabled globally, which makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. This good idea loses you the non-flushed Query Store data. @CheckUserDatabaseObjects was set to 0, so we skipped checking if any databases have Query Store enabled.' WHEN [T].[TraceFlag] = '7745' AND @QueryStoreInUse = 1 THEN '7745 enabled globally, which makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. This good idea loses you the non-flushed Query Store data.' WHEN [T].[TraceFlag] = '7745' AND @ProductVersionMajor > 12 THEN '7745 enabled globally, which is for Query Store. None of your databases have Query Store enabled, so why do you have this turned on?' WHEN [T].[TraceFlag] = '7745' AND @ProductVersionMajor <= 12 THEN '7745 enabled globally, which is for Query Store. Query Store does not exist on your SQL Server version, so why do you have this turned on?' WHEN [T].[TraceFlag] = '7752' AND @ProductVersionMajor > 14 THEN '7752 enabled globally, which is for Query Store. However, it has no effect in your SQL Server version. Consider turning it off.' + WHEN [T].[TraceFlag] = '7752' AND @CheckUserDatabaseObjects = 0 THEN '7752 enabled globally, which stops queries needing to wait on Query Store loading up after database recovery. @CheckUserDatabaseObjects was set to 0, so we skipped checking if any databases have Query Store enabled.' WHEN [T].[TraceFlag] = '7752' AND @QueryStoreInUse = 1 THEN '7752 enabled globally, which stops queries needing to wait on Query Store loading up after database recovery.' WHEN [T].[TraceFlag] = '7752' AND @ProductVersionMajor > 12 THEN '7752 enabled globally, which is for Query Store. None of your databases have Query Store enabled, so why do you have this turned on?' WHEN [T].[TraceFlag] = '7752' AND @ProductVersionMajor <= 12 THEN '7752 enabled globally, which is for Query Store. Query Store does not exist on your SQL Server version, so why do you have this turned on?' From 44d3f8106f69af2a8c82e0026fc06adaf7a47995 Mon Sep 17 00:00:00 2001 From: ReeceGoding <67124261+ReeceGoding@users.noreply.github.com> Date: Fri, 10 Oct 2025 01:50:21 +0100 Subject: [PATCH 53/76] Made the sys.dm_os_ring_buffers CPU checks respect Linux always setting system_idle to 0. Disabled "High CPU Utilization - Not SQL" check on Linux. Closes #3710. --- sp_BlitzFirst.sql | 73 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index f5dda196e..f431cb7ac 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -145,7 +145,18 @@ DECLARE @StringToExecute NVARCHAR(MAX), @get_thread_time_ms NVARCHAR(MAX) = N'', @thread_time_ms FLOAT = 0, @logical_processors INT = 0, - @max_worker_threads INT = 0; + @max_worker_threads INT = 0, + @is_windows_operating_system BIT = 1; + +IF EXISTS +( + SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_os_host_info' +) +BEGIN + SELECT @is_windows_operating_system = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info; +END; /* Sanitize our inputs */ SELECT @@ -2399,19 +2410,28 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; END + /* Traditionally, we use 100 - SystemIdle here. + However, SystemIdle is always 0 on Linux. + So if we are on Linux, we use ProcessUtilization instead. + This is the approach found in + https://techcommunity.microsoft.com/blog/sqlserver/sql-server-cpu-usage-available-in-sys-dm-os-ring-buffers-dmv-starting-sql-server/825361 */ INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%.', 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%.', CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y - WHERE 100 - SystemIdle >= 50; + ) AS ShreddedCpuXml + ) AS OsCpu + WHERE CpuUsage >= 50; /* CPU Utilization - CheckID 23 */ IF (@Debug = 1) @@ -2423,7 +2443,8 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, WITH y AS ( - SELECT CONVERT(VARCHAR(5), 100 - ca.c.value('.', 'INT')) AS system_idle, + /* See earlier comments about SystemIdle on Linux. */ + SELECT CONVERT(VARCHAR(5), CASE WHEN @is_windows_operating_system = 1 THEN 100 - ca.c.value('.', 'INT') ELSE ca2.p.value('.', 'INT') END) AS cpu_usage, CONVERT(VARCHAR(30), rb.event_date) AS event_date, CONVERT(VARCHAR(8000), rb.record) AS record, event_date as event_date_raw @@ -2436,6 +2457,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, WHERE dorb.ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ) AS rb CROSS APPLY rb.record.nodes('/Record/SchedulerMonitorEvent/SystemHealth/SystemIdle') AS ca(c) + CROSS APPLY rb.record.nodes('/Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization') AS ca2(p) ) INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL, HowToStopIt) SELECT TOP 1 @@ -2443,12 +2465,12 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, 250, 'Server Info', 'CPU Utilization', - y.system_idle + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), - y.system_idle , + y.cpu_usage + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), + y.cpu_usage , 'https://www.brentozar.com/go/cpu', STUFF(( SELECT TOP 2147483647 CHAR(10) + CHAR(13) - + y2.system_idle + + y2.cpu_usage + '% ON ' + y2.event_date + ' Ring buffer details: ' @@ -2479,7 +2501,12 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, AND record LIKE '%%' ORDER BY timestamp DESC) AS rb ) AS y - WHERE 100 - (y.SQLUsage + y.SystemIdle) >= 25; + WHERE 100 - (y.SQLUsage + y.SystemIdle) >= 25 + /* SystemIdle is always 0 on Linux, as described earlier. + We therefore cannot distinguish between a totally idle Linux server and + a Linux server where SQL Server is being crushed by other CPU-heavy processes. + We therefore disable this check on Linux. */ + AND @is_windows_operating_system = 1; END; /* IF @Seconds < 30 */ @@ -3593,18 +3620,24 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + /* See earlier comments about SystemIdle on Linux. */ + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y - WHERE 100 - SystemIdle >= 50; + ) AS ShreddedCpuXml + ) AS OsCpu + WHERE CpuUsage >= 50; /* Server Performance - CPU Utilization CheckID 23 */ IF (@Debug = 1) @@ -3613,17 +3646,23 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 23, 250, 'Server Info', 'CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 23, 250, 'Server Info', 'CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + /* See earlier comments about SystemIdle on Linux. */ + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y; + ) AS ShreddedCpuXml + ) AS OsCpu; END; /* IF @Seconds >= 30 */ From f8c6c6d6cc07471fe2cbed91d71ece2896027d3f Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Sat, 18 Oct 2025 06:44:20 -0700 Subject: [PATCH 54/76] #3721 sp_Blitz TempDB RG Check to see if TempDB config will support RG config for SQL 2025. Closes #3721. --- Documentation/sp_Blitz_Checks_by_Priority.md | 5 +- sp_Blitz.sql | 60 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Documentation/sp_Blitz_Checks_by_Priority.md b/Documentation/sp_Blitz_Checks_by_Priority.md index d59eeb858..af9d6c669 100644 --- a/Documentation/sp_Blitz_Checks_by_Priority.md +++ b/Documentation/sp_Blitz_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 270. -If you want to add a new one, start at 271. +CURRENT HIGH CHECKID: 271. +If you want to add a new one, start at 272. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|-----------------------------|---------------------------------------------------------|------------------------------------------------------------------------|----------| @@ -131,6 +131,7 @@ If you want to add a new one, start at 271. | 170 | File Configuration | High VLF Count | https://www.BrentOzar.com/go/vlf | 69 | | 170 | File Configuration | Multiple Log Files on One Drive | https://www.BrentOzar.com/go/manylogs | 41 | | 170 | File Configuration | System Database on C Drive | https://www.BrentOzar.com/go/drivec | 24 | +| 170 | File Configuration | TempDB Governor Config Problem | https://www.BrentOzar.com/go/tempdbrg | 271 | | 170 | File Configuration | TempDB Has >16 Data Files | https://www.BrentOzar.com/go/tempdb | 175 | | 170 | File Configuration | TempDB Only Has 1 Data File | https://www.BrentOzar.com/go/tempdb | 40 | | 170 | File Configuration | TempDB Unevenly Sized Data Files | https://www.BrentOzar.com/go/tempdb | 183 | diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 102cbdc2f..f011e706c 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -6752,6 +6752,66 @@ IF @ProductVersionMajor >= 10 END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 271 ) + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_percent') + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_mb') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 271) WITH NOWAIT; + + IF EXISTS (SELECT * FROM sys.resource_governor_workload_groups + WHERE group_max_tempdb_data_percent <> 0 + AND group_max_tempdb_data_mb IS NULL) + BEGIN + DECLARE @TempDBfiles TABLE (config VARCHAR(50), data_files INT) + /* Valid configs */ + INSERT INTO @TempDBfiles + SELECT 'Fixed predictable growth' AS config, SUM(1) AS data_files + FROM master.sys.master_files + WHERE database_id = DB_ID('tempdb') + AND type = 0 /* data */ + AND max_size <> -1 /* only limited ones */ + AND growth <> 0 /* growth is set */ + HAVING SUM(1) > 0 + UNION ALL + SELECT 'Growth turned off' AS config, SUM(1) AS data_files + FROM master.sys.master_files + WHERE database_id = DB_ID('tempdb') + AND type = 0 /* data */ + AND max_size = -1 /* unlimited */ + AND growth = 0 + HAVING SUM(1) > 0; + + IF 1 <> (SELECT COUNT(*) FROM @TempDBfiles) + OR (SELECT SUM(data_files) FROM @TempDBfiles) <> + (SELECT SUM(1) + FROM master.sys.master_files + WHERE database_id = DB_ID('tempdb') + AND type = 0 /* data */) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + DatabaseName , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 271 AS CheckID, + 170 AS Priority, + 'tempdb', + 'File Configuration' AS FindingsGroup, + 'TempDB Governor Config Problem' AS Finding, + 'https://www.BrentOzar.com/go/tempdbrg' AS URL, + 'Resource Governor is configured to cap TempDB usage by percent, but the TempDB file configuration will not allow that to take effect.' AS details + END + END + END + IF @CheckUserDatabaseObjects = 1 BEGIN From 8f6dde2a5839b2ef7d189c785921863741fdd37b Mon Sep 17 00:00:00 2001 From: Chad Baldwin Date: Wed, 22 Oct 2025 22:30:21 -0700 Subject: [PATCH 55/76] DB_ID() does not support quoted identifiers, passing in as variable instead --- sp_BlitzIndex.sql | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 57e2760cb..625fca08c 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -1953,7 +1953,7 @@ WITH ON ty.user_type_id = co.user_type_id WHERE id_inner.index_handle = id.index_handle AND id_inner.object_id = id.object_id - AND id_inner.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + AND id_inner.database_id = DB_ID(@i_DatabaseName) AND cn_inner.IndexColumnType = cn.IndexColumnType FOR XML PATH('''') ), @@ -1991,7 +1991,7 @@ WITH ) x (n) CROSS APPLY n.nodes(''x'') node(v) )AS cn - WHERE id.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + WHERE id.database_id = DB_ID(@i_DatabaseName) GROUP BY id.index_handle, id.object_id, @@ -2137,7 +2137,7 @@ OPTION (RECOMPILE);'; END; SET @dsql = N' - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SELECT DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, s.name, fk_object.name AS foreign_key_name, @@ -2206,17 +2206,17 @@ OPTION (RECOMPILE);'; BEGIN SET @dsql = N' SELECT - DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, foreign_key_schema = s.name, foreign_key_name = fk.name, foreign_key_table = - OBJECT_NAME(fk.parent_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + OBJECT_NAME(fk.parent_object_id, DB_ID(@i_DatabaseName)), fk.parent_object_id, foreign_key_referenced_table = - OBJECT_NAME(fk.referenced_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + OBJECT_NAME(fk.referenced_object_id, DB_ID(@i_DatabaseName)), fk.referenced_object_id FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s @@ -2290,7 +2290,7 @@ OPTION (RECOMPILE);'; days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SELECT DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, obj.object_id, obj.name AS table_name, @@ -2385,7 +2385,7 @@ OPTION (RECOMPILE);'; last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SELECT DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, obj.object_id, obj.name AS table_name, @@ -2518,7 +2518,7 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Temporal Table Info',0,1) WITH NOWAIT; SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS database_name, - DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + DB_ID(@i_DatabaseName) AS [database_id], s.name AS schema_name, t.name AS table_name, oa.hsn as history_schema_name, @@ -2554,7 +2554,7 @@ OPTION (RECOMPILE);'; 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_object_id ) - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; END; SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], From 025108f5a7cb812706e9379810dd4304cbd73772 Mon Sep 17 00:00:00 2001 From: OZTPR Date: Tue, 28 Oct 2025 15:10:33 +0000 Subject: [PATCH 56/76] Update sp_BlitzBackups.sql Fix problem where Unicode Database Names get mangled in the sp_BlitzBackups output. --- sp_BlitzBackups.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_BlitzBackups.sql b/sp_BlitzBackups.sql index dafec335f..403199016 100755 --- a/sp_BlitzBackups.sql +++ b/sp_BlitzBackups.sql @@ -237,7 +237,7 @@ CREATE TABLE #Warnings Id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, CheckId INT, Priority INT, - DatabaseName VARCHAR(128), + DatabaseName NVARCHAR(128), Finding VARCHAR(256), Warning VARCHAR(8000) ); From 3897a21bbe859396a1707d2e343714fa860db0f8 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Sat, 1 Nov 2025 11:29:48 -0400 Subject: [PATCH 57/76] #3711 sp_BlitzLock duplicate rows Thanks ChatGPT! Closes #3711. --- sp_BlitzLock.sql | 88 ++++++++++++++++++------------------------------ 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index bd4e75da7..53e923556 100644 --- a/sp_BlitzLock.sql +++ b/sp_BlitzLock.sql @@ -2647,56 +2647,37 @@ BEGIN lock_types AS ( SELECT - database_name = - dp.database_name, + dp.database_name, dow.object_name, lock = CASE WHEN CHARINDEX(N':', dp.wait_resource) > 0 - THEN SUBSTRING - ( - dp.wait_resource, - 1, - CHARINDEX(N':', dp.wait_resource) - 1 - ) + THEN LEFT(dp.wait_resource, CHARINDEX(N':', dp.wait_resource) - 1) ELSE dp.wait_resource END, - lock_count = - CONVERT - ( - nvarchar(20), - COUNT_BIG(DISTINCT dp.event_date) - ) + lock_count = CONVERT(nvarchar(20), COUNT_BIG(DISTINCT dp.event_date)) FROM #deadlock_process AS dp JOIN #deadlock_owner_waiter AS dow - ON (dp.id = dow.owner_id - OR dp.victim_id = dow.waiter_id) - AND dp.event_date = dow.event_date - WHERE 1 = 1 - AND (dp.database_name = @DatabaseName OR @DatabaseName IS NULL) - AND (dp.event_date >= @StartDate OR @StartDate IS NULL) - AND (dp.event_date < @EndDate OR @EndDate IS NULL) - AND (dp.client_app = @AppName OR @AppName IS NULL) - AND (dp.host_name = @HostName OR @HostName IS NULL) - AND (dp.login_name = @LoginName OR @LoginName IS NULL) - AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) - AND dow.object_name IS NOT NULL + ON (dp.id = dow.owner_id OR dp.victim_id = dow.waiter_id) + AND dp.event_date = dow.event_date + WHERE (dp.database_name = @DatabaseName OR @DatabaseName IS NULL) + AND (dp.event_date >= @StartDate OR @StartDate IS NULL) + AND (dp.event_date < @EndDate OR @EndDate IS NULL) + AND (dp.client_app = @AppName OR @AppName IS NULL) + AND (dp.host_name = @HostName OR @HostName IS NULL) + AND (dp.login_name = @LoginName OR @LoginName IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.object_name IS NOT NULL GROUP BY dp.database_name, + dow.object_name, CASE WHEN CHARINDEX(N':', dp.wait_resource) > 0 - THEN SUBSTRING - ( - dp.wait_resource, - 1, - CHARINDEX(N':', dp.wait_resource) - 1 - ) + THEN LEFT(dp.wait_resource, CHARINDEX(N':', dp.wait_resource) - 1) ELSE dp.wait_resource - END, - dow.object_name + END ) - INSERT - #deadlock_findings WITH(TABLOCKX) + INSERT #deadlock_findings WITH (TABLOCKX) ( check_id, database_name, @@ -2706,36 +2687,33 @@ BEGIN sort_order ) SELECT - check_id = 7, + check_id = 7, lt.database_name, lt.object_name, finding_group = N'Types of locks by object', - finding = + finding = N'This object has had ' + - STUFF - ( + STUFF( ( SELECT - N', ' + - lt2.lock_count + - N' ' + - lt2.lock + N', ' + lt2.lock_count + N' ' + lt2.lock FROM lock_types AS lt2 WHERE lt2.database_name = lt.database_name - AND lt2.object_name = lt.object_name - FOR XML - PATH(N''), - TYPE - ).value(N'.[1]', N'nvarchar(MAX)'), - 1, - 1, - N'' + AND lt2.object_name = lt.object_name + FOR XML PATH(''), TYPE + ).value('.', 'nvarchar(max)'), + 1, 2, N'' ) + N' locks.', sort_order = - ROW_NUMBER() - OVER (ORDER BY CONVERT(bigint, lt.lock_count) DESC) + ROW_NUMBER() OVER ( + ORDER BY + MAX(CONVERT(bigint, lt.lock_count)) DESC + ) FROM lock_types AS lt - OPTION(RECOMPILE); + GROUP BY + lt.database_name, + lt.object_name + OPTION (RECOMPILE); RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; From 45cc09f37fe0053b858bc3f435586c507102f13b Mon Sep 17 00:00:00 2001 From: Yoni Sade <103036539+yoni-yad2@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:31:25 +0200 Subject: [PATCH 58/76] rollback reformatting for mentioned rows --- sp_Blitz.sql | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 1ba7e1a8f..c8125dd62 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -6778,12 +6778,7 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) - IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) - INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) - SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; END; /* Note that by using sp_MSforeachdb, we're running the query in all @@ -7048,12 +7043,11 @@ IF @ProductVersionMajor >= 10 ''Multiple Log Files on One Drive'', ''https://www.brentozar.com/go/manylogs'', (''The ['' + DB_NAME() + ''] database has multiple log files on the '' + LEFT(physical_name, 1) + '' drive. This is not a performance booster because log file access is sequential, not parallel.'') - FROM [?].sys.database_files - WHERE type_desc = ''LOG'' + FROM [?].sys.database_files WHERE type_desc = ''LOG'' AND ''?'' NOT IN (''rdsadmin'',''tempdb'') GROUP BY LEFT(physical_name, 1) - HAVING COUNT(*) > 1 AND SUM(size) < 268435456 - OPTION (RECOMPILE);'; + HAVING COUNT(*) > 1 + AND SUM(size) < 268435456 OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 From 5a2be78f79009ba646cb86eca342cdba1a6df4ea Mon Sep 17 00:00:00 2001 From: Yoni Sade <103036539+yoni-yad2@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:35:07 +0200 Subject: [PATCH 59/76] Update sp_Blitz.sql From a0ff59783d24baa1d4eb9d412611d4f2b6e94863 Mon Sep 17 00:00:00 2001 From: Yoni Sade <103036539+yoni-yad2@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:37:56 +0200 Subject: [PATCH 60/76] Update sp_Blitz.sql --- sp_Blitz.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index c8125dd62..bbebfa3a9 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -6778,7 +6778,7 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; END; /* Note that by using sp_MSforeachdb, we're running the query in all From 1f5cf043a5c807df93ff200a24640e41f56bd5c4 Mon Sep 17 00:00:00 2001 From: Yoni Sade <103036539+yoni-yad2@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:40:45 +0200 Subject: [PATCH 61/76] Update sp_Blitz.sql --- sp_Blitz.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index bbebfa3a9..9d72b82fd 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -6778,7 +6778,7 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; IF EXISTS (SELECT * FROM sys.tables WITH (NOLOCK) WHERE name = ''sysmergepublications'' ) IF EXISTS ( SELECT * FROM sysmergepublications WITH (NOLOCK) WHERE retention = 0) INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 99, DB_NAME(), 110, ''Performance'', ''Infinite merge replication metadata retention period'', ''https://www.brentozar.com/go/merge'', (''The ['' + DB_NAME() + ''] database has merge replication metadata retention period set to infinite - this can be the case of significant performance issues.'')'; END; /* Note that by using sp_MSforeachdb, we're running the query in all @@ -7044,10 +7044,10 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/manylogs'', (''The ['' + DB_NAME() + ''] database has multiple log files on the '' + LEFT(physical_name, 1) + '' drive. This is not a performance booster because log file access is sequential, not parallel.'') FROM [?].sys.database_files WHERE type_desc = ''LOG'' - AND ''?'' NOT IN (''rdsadmin'',''tempdb'') + AND ''?'' NOT IN (''rdsadmin'',''tempdb'') GROUP BY LEFT(physical_name, 1) - HAVING COUNT(*) > 1 - AND SUM(size) < 268435456 OPTION (RECOMPILE);'; + HAVING COUNT(*) > 1 + AND SUM(size) < 268435456 OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 From 6216da90d96824a8b4c35b3a4b3cf5aca7515880 Mon Sep 17 00:00:00 2001 From: ReeceGoding <67124261+ReeceGoding@users.noreply.github.com> Date: Fri, 7 Nov 2025 19:43:19 +0000 Subject: [PATCH 62/76] Added an ORDER BY to the temporal table check, so the Mode=4 output is less ugly when you have lots of them. Closes #3727. --- sp_BlitzIndex.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 625fca08c..6bb04e879 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -5650,6 +5650,7 @@ BEGIN 'N/A' AS index_usage_summary, 'N/A' AS index_size_summary FROM #TemporalTables AS t + ORDER BY t.database_name, t.schema_name, t.table_name OPTION ( RECOMPILE ); RAISERROR(N'check_id 121: Optimized For Sequential Keys.', 0,1) WITH NOWAIT; From f41fe51798c56322bfcd64f1d8d6338fa6c9c89b Mon Sep 17 00:00:00 2001 From: Vlad Drumea <48413726+VladDBA@users.noreply.github.com> Date: Sun, 9 Nov 2025 20:50:51 +0200 Subject: [PATCH 63/76] fix for #3729 - CheckId 271 --- sp_Blitz.sql | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index f011e706c..90c2abfc9 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -6754,17 +6754,21 @@ IF @ProductVersionMajor >= 10 IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 271 ) - AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_percent') - AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_mb') + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 271 ) + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_percent' + AND [object_id] = OBJECT_ID('sys.resource_governor_workload_groups')) + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_mb' + AND [object_id] = OBJECT_ID('sys.resource_governor_workload_groups')) BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 271) WITH NOWAIT; - IF EXISTS (SELECT * FROM sys.resource_governor_workload_groups - WHERE group_max_tempdb_data_percent <> 0 - AND group_max_tempdb_data_mb IS NULL) + SET @tsql = N'SELECT COUNT(1) FROM sys.resource_governor_workload_groups + WHERE group_max_tempdb_data_percent <> 0 + AND group_max_tempdb_data_mb IS NULL'; + EXEC @ExecRet = sp_executesql @tsql; + IF @ExecRet > 0 BEGIN DECLARE @TempDBfiles TABLE (config VARCHAR(50), data_files INT) /* Valid configs */ From 7cc07974776eb243552eaa2b6aad3706b9d95869 Mon Sep 17 00:00:00 2001 From: Vlad Drumea <48413726+VladDBA@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:04:05 +0200 Subject: [PATCH 64/76] changes for #3732 --- sp_Blitz.sql | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index f011e706c..ed37003af 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -3740,6 +3740,14 @@ AS IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 94) WITH NOWAIT; + ;WITH las_job_run AS ( + SELECT MAX(instance_id) AS instance_id, + job_id, COUNT_BIG(*) AS job_executions, + SUM(CASE WHEN run_status = 0 THEN 1 ELSE 0 END) AS failed_executions + FROM msdb.dbo.sysjobhistory + WHERE step_id = 0 + GROUP BY job_id + ) INSERT INTO #BlitzResults ( CheckID , Priority , @@ -3754,8 +3762,32 @@ AS 'Agent Jobs Without Failure Emails' AS Finding , 'https://www.brentozar.com/go/alerts' AS URL , 'The job ' + [name] - + ' has not been set up to notify an operator if it fails.' AS Details + + ' has not been set up to notify an operator if it fails.' + + CASE + WHEN jh.run_date IS NULL OR jh.run_time IS NULL OR jh.run_status IS NULL THEN '' + ELSE N' Executions: '+ CAST(ljr.job_executions AS VARCHAR(30)) + + CASE ljr.failed_executions + WHEN 0 THEN N'' + ELSE N' ('+CAST(ljr.failed_executions AS NVARCHAR(10)) + N' failed)' + END + + N' - last execution started on ' + + CAST(CONVERT(DATE,CAST(jh.run_date AS NVARCHAR(8)),113) AS NVARCHAR(10)) + + N', at ' + + STUFF(STUFF(RIGHT(N'000000' + CAST(jh.run_time AS varchar(6)),6),3,0,N':'),6,0,N':') + + N', with status "' + + CASE jh.run_status + WHEN 0 THEN N'Failed' + WHEN 1 THEN N'Succeeded' + WHEN 2 THEN N'Retry' + WHEN 3 THEN N'Canceled' + WHEN 4 THEN N'In Progress' + END +N'".' + END AS Details FROM msdb.[dbo].[sysjobs] j + LEFT JOIN las_job_run ljr + ON ljr.job_id = j.job_id + LEFT JOIN msdb.[dbo].[sysjobhistory] jh + ON ljr.job_id = jh.job_id AND ljr.instance_id = jh.instance_id WHERE j.enabled = 1 AND j.notify_email_operator_id = 0 AND j.notify_netsend_operator_id = 0 From e9809f04a770e717070acd3cd66dcd8cf37aaf9c Mon Sep 17 00:00:00 2001 From: Vlad Drumea <48413726+VladDBA@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:48:40 +0200 Subject: [PATCH 65/76] Changes for #3734 --- sp_BlitzWho.sql | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index a9b61f89d..64df0ca84 100644 --- a/sp_BlitzWho.sql +++ b/sp_BlitzWho.sql @@ -90,6 +90,7 @@ DECLARE @ProductVersion NVARCHAR(128) = CAST(SERVERPROPERTY('ProductVersion') A ,@ProductVersionMajor DECIMAL(10,2) ,@ProductVersionMinor DECIMAL(10,2) ,@Platform NVARCHAR(8) /* Azure or NonAzure are acceptable */ = (SELECT CASE WHEN @@VERSION LIKE '%Azure%' THEN N'Azure' ELSE N'NonAzure' END AS [Platform]) + ,@AzureSQLDB BIT = (SELECT CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END) ,@EnhanceFlag BIT = 0 ,@BlockingCheck NVARCHAR(MAX) ,@StringToSelect NVARCHAR(MAX) @@ -127,10 +128,10 @@ SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @Produ @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) SELECT - @OutputTableNameQueryStats_View = QUOTENAME(@OutputTableName + '_Deltas'), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName), + @OutputTableNameQueryStats_View = QUOTENAME(PARSENAME(@OutputTableName,1) + '_Deltas'), + @OutputDatabaseName = QUOTENAME(PARSENAME(@OutputDatabaseName,1)), + @OutputSchemaName = ISNULL(QUOTENAME(PARSENAME(@OutputSchemaName,1)),QUOTENAME(PARSENAME(@OutputTableName,2))), + @OutputTableName = QUOTENAME(PARSENAME(@OutputTableName,1)), @LineFeed = CHAR(13) + CHAR(10); IF @GetLiveQueryPlan IS NULL @@ -141,6 +142,20 @@ IF @GetLiveQueryPlan IS NULL SET @GetLiveQueryPlan = 0; END +IF @OutputTableName IS NOT NULL AND (@OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL) + BEGIN + IF @OutputDatabaseName IS NULL AND @AzureSQLDB = 1 + BEGIN + /* If we're in Azure SQL DB then use the current database */ + SET @OutputDatabaseName = QUOTENAME(DB_NAME()); + END; + IF @OutputSchemaName IS NULL AND @OutputDatabaseName = QUOTENAME(DB_NAME()) + BEGIN + /* If we're inserting records in the current database use the default schema */ + SET @OutputSchemaName = QUOTENAME(SCHEMA_NAME()); + END; + END; + IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL AND EXISTS ( SELECT * FROM sys.databases From 2663088a274c0ebc788d562ce606336f1785229c Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Mon, 10 Nov 2025 13:15:21 -0800 Subject: [PATCH 66/76] #3736 sp_BlitzIndex JSON Show JSON indexes in the output, but doesn't gather any real data on them yet. Working on #3736. --- sp_BlitzIndex.sql | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 6bb04e879..ca17092d0 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -329,6 +329,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL is_spatial BIT NOT NULL, is_NC_columnstore BIT NOT NULL, is_CX_columnstore BIT NOT NULL, + is_JSON BIT NOT NULL, is_in_memory_oltp BIT NOT NULL , is_disabled BIT NOT NULL , is_hypothetical BIT NOT NULL , @@ -370,6 +371,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL ELSE N'' END + CASE WHEN is_XML = 1 THEN N'[XML] ' ELSE N'' END + CASE WHEN is_spatial = 1 THEN N'[SPATIAL] ' ELSE N'' END + CASE WHEN is_NC_columnstore = 1 THEN N'[COLUMNSTORE] ' + ELSE N'' END + CASE WHEN is_json = 1 THEN N'[JSON] ' ELSE N'' END + CASE WHEN is_in_memory_oltp = 1 THEN N'[IN-MEMORY] ' ELSE N'' END + CASE WHEN is_disabled = 1 THEN N'[DISABLED] ' ELSE N'' END + CASE WHEN is_hypothetical = 1 THEN N'[HYPOTHETICAL] ' @@ -1414,6 +1416,7 @@ BEGIN TRY CASE when si.type = 4 THEN 1 ELSE 0 END AS is_spatial, CASE when si.type = 6 THEN 1 ELSE 0 END AS is_NC_columnstore, CASE when si.type = 5 then 1 else 0 end as is_CX_columnstore, + CASE when si.type = 9 then 1 else 0 end as is_JSON, CASE when si.data_space_id = 0 then 1 else 0 end as is_in_memory_oltp, si.is_disabled, si.is_hypothetical, @@ -1447,8 +1450,8 @@ BEGIN TRY LEFT JOIN sys.dm_db_index_usage_stats AS us WITH (NOLOCK) ON si.[object_id] = us.[object_id] AND si.index_id = us.index_id AND us.database_id = ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' - WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) - /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + + WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6, 9 ) + /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore, JSON */ ' + CASE WHEN @TableName IS NOT NULL THEN N' and so.name=' + QUOTENAME(@TableName,N'''') + N' ' ELSE N'' END + CASE WHEN ( @IncludeInactiveIndexes = 0 AND @Mode IN (0, 4) @@ -1476,7 +1479,7 @@ BEGIN TRY PRINT SUBSTRING(@dsql, 36000, 40000); END; INSERT #IndexSanity ( [database_id], [object_id], [index_id], [index_type], [database_name], [schema_name], [object_name], - index_name, is_indexed_view, is_unique, is_primary_key, is_unique_constraint, is_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, is_in_memory_oltp, + index_name, is_indexed_view, is_unique, is_primary_key, is_unique_constraint, is_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, is_JSON, is_in_memory_oltp, is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, [optimize_for_sequential_key], user_seeks, user_scans, user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, create_date, modify_date ) From 618fc8c3aafbfd0285b8ca49cbed4069a946c802 Mon Sep 17 00:00:00 2001 From: Vlad Drumea <48413726+VladDBA@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:19:58 +0200 Subject: [PATCH 67/76] Changes for #3739 --- sp_BlitzIndex.sql | 72 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 625fca08c..b1b0f22e6 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -16,7 +16,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex @ObjectName NVARCHAR(386) = NULL, /* 'dbname.schema.table' -- if you are lazy and want to fill in @DatabaseName, @SchemaName and @TableName, and since it's the first parameter can simply do: sp_BlitzIndex 'sch.table' */ @DatabaseName NVARCHAR(128) = NULL, /*Defaults to current DB if not specified*/ @SchemaName NVARCHAR(128) = NULL, /*Requires table_name as well.*/ - @TableName NVARCHAR(128) = NULL, /*Requires schema_name as well.*/ + @TableName NVARCHAR(261) = NULL, /*Requires schema_name as well.*/ @Mode TINYINT=0, /*0=Diagnose, 1=Summarize, 2=Index Usage Detail, 3=Missing Index Detail, 4=Diagnose Details*/ /*Note:@Mode doesn't matter if you're specifying schema_name and @TableName.*/ @Filter TINYINT = 0, /* 0=no filter (default). 1=No low-usage warnings for objects with 0 reads. 2=Only warn for objects >= 500MB */ @@ -33,7 +33,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex @OutputServerName NVARCHAR(256) = NULL , @OutputDatabaseName NVARCHAR(256) = NULL , @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(261) = NULL , @IncludeInactiveIndexes BIT = 0 /* Will skip indexes with no reads or writes */, @ShowAllMissingIndexRequests BIT = 0 /*Will make all missing index requests show up*/, @ShowPartitionRanges BIT = 0 /* Will add partition range values column to columnstore visualization */, @@ -134,12 +134,60 @@ DECLARE @PartitionCount INT; DECLARE @OptimizeForSequentialKey BIT = 0; DECLARE @ResumableIndexesDisappearAfter INT = 0; DECLARE @StringToExecute NVARCHAR(MAX); +DECLARE @AzureSQLDB BIT = (SELECT CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END); /* If user was lazy and just used @ObjectName with a fully qualified table name, then lets parse out the various parts */ SET @DatabaseName = COALESCE(@DatabaseName, PARSENAME(@ObjectName, 3)) /* 3 = Database name */ SET @SchemaName = COALESCE(@SchemaName, PARSENAME(@ObjectName, 2)) /* 2 = Schema name */ SET @TableName = COALESCE(@TableName, PARSENAME(@ObjectName, 1)) /* 1 = Table name */ +/* Handle already quoted input if it wasn't fully qualified*/ +SET @DatabaseName = PARSENAME(@DatabaseName,1); +SET @SchemaName = ISNULL(PARSENAME(@SchemaName,1),PARSENAME(@TableName,2)); +SET @TableName = PARSENAME(@TableName,1); + +/* If we're on Azure SQL DB let's cut people some slack */ +IF (@TableName IS NOT NULL AND @AzureSQLDB = 1 AND @DatabaseName IS NULL) + BEGIN + SET @DatabaseName = DB_NAME(); + END; + + +IF (@SchemaName IS NULL AND @TableName IS NOT NULL) + BEGIN + /* If the target is in the current database + and there's just one table or view with this name, then we can grab the schema from sys.objects*/ + IF ((SELECT COUNT(1) FROM [sys].[objects] + WHERE [name] = @TableName AND [type] IN ('U','V'))=1 + AND @TableName IS NOT NULL AND @DatabaseName = DB_NAME()) + BEGIN + SELECT @SchemaName = SCHEMA_NAME([schema_id]) + FROM [sys].[objects] + WHERE [name] = @TableName AND [type] IN ('U','V'); + END; + /* If the target isn't in the current database, then use dynamic T-SQL*/ + IF (@DatabaseName <> DB_NAME()) + BEGIN + /*first make sure only one row is returned from sys.objects*/ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = COUNT(1) FROM ' + QUOTENAME(@DatabaseName) + N'.[sys].[objects] + WHERE [name] = @TableName_IN AND [type] IN (''U'',''V'') OPTION (RECOMPILE);'; + SET @params = N'@TableName_IN NVARCHAR(128), @RowcountOUT BIGINT OUTPUT'; + EXEC sp_executesql @dsql, @params, @TableName_IN = @TableName, @RowcountOUT = @Rowcount OUTPUT; + + IF (@Rowcount = 1) + BEGIN + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @SchemaName_OUT = s.[name] + FROM ' + QUOTENAME(@DatabaseName) + N'.[sys].[objects] o + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.[sys].[schemas] s + ON o.[schema_id] = s.[schema_id] + WHERE o.[name] = @TableName_IN AND o.[type] IN (''U'',''V'') OPTION (RECOMPILE);'; + SET @params = N'@TableName_IN NVARCHAR(128), @SchemaName_OUT NVARCHAR(128) OUTPUT'; + EXEC sp_executesql @dsql, @params, @TableName_IN = @TableName, @SchemaName_OUT = @SchemaName OUTPUT; + END; + END; + END; /* Let's get @SortOrder set to lower case here for comparisons later */ SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); @@ -181,6 +229,26 @@ BEGIN RETURN; END; +/* Some prep-work for output object names before checking if they're ok or not */ +IF (@OutputTableName IS NOT NULL) +BEGIN + + /*Deal with potentially quoted object names*/ + SET @OutputDatabaseName = PARSENAME(@OutputDatabaseName,1); + SET @OutputSchemaName = ISNULL(PARSENAME(@OutputSchemaName,1),PARSENAME(@OutputTableName,2)); + SET @OutputTableName = PARSENAME(@OutputTableName,1); + + /* Running on Azure SQL DB or outputting to current database? */ + IF (@OutputDatabaseName IS NULL AND @AzureSQLDB = 1) + BEGIN + SET @OutputDatabaseName = DB_NAME(); + END; + IF (@OutputSchemaName IS NULL AND @OutputDatabaseName = DB_NAME()) + BEGIN + SET @OutputSchemaName = SCHEMA_NAME(); + END; +END; + IF(@OutputType = 'TABLE' AND NOT (@OutputTableName IS NULL AND @OutputSchemaName IS NULL AND @OutputDatabaseName IS NULL AND @OutputServerName IS NULL)) BEGIN RAISERROR(N'One or more output parameters specified in combination with TABLE output, changing to NONE output mode', 0,1) WITH NOWAIT; From 9284338f9a19733bf71858aaeb04210a8ed7ec73 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Mon, 17 Nov 2025 04:50:14 -0800 Subject: [PATCH 68/76] #3741 sp_Blitz optimized locking Warn if optimized locking is enabled but RCSI is not. Closes #3741. --- Documentation/sp_Blitz_Checks_by_Priority.md | 5 ++-- sp_Blitz.sql | 29 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/Documentation/sp_Blitz_Checks_by_Priority.md b/Documentation/sp_Blitz_Checks_by_Priority.md index af9d6c669..950d11b87 100644 --- a/Documentation/sp_Blitz_Checks_by_Priority.md +++ b/Documentation/sp_Blitz_Checks_by_Priority.md @@ -6,8 +6,8 @@ Before adding a new check, make sure to add a Github issue for it first, and hav If you want to change anything about a check - the priority, finding, URL, or ID - open a Github issue first. The relevant scripts have to be updated too. -CURRENT HIGH CHECKID: 271. -If you want to add a new one, start at 272. +CURRENT HIGH CHECKID: 272. +If you want to add a new one, start at 273. | Priority | FindingsGroup | Finding | URL | CheckID | |----------|-----------------------------|---------------------------------------------------------|------------------------------------------------------------------------|----------| @@ -87,6 +87,7 @@ If you want to add a new one, start at 272. | 100 | Performance | Many Plans for One Query | https://www.BrentOzar.com/go/parameterization | 160 | | 100 | Performance | Max Memory Set Too High | https://www.BrentOzar.com/go/max | 50 | | 100 | Performance | Memory Pressure Affecting Queries | https://www.BrentOzar.com/go/grants | 117 | +| 100 | Performance | Optimized Locking Not Fully Set Up | https://www.BrentOzar.com/go/optimizedlocking | 272 | | 100 | Performance | Partitioned database with non-aligned indexes | https://www.BrentOzar.com/go/aligned | 72 | | 100 | Performance | Repetitive Maintenance Tasks | https://ola.hallengren.com | 181 | | 100 | Performance | Resource Governor Enabled | https://www.BrentOzar.com/go/rg | 10 | diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 9f88d7ae9..928f486a2 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -4935,6 +4935,35 @@ AS CLOSE DatabaseDefaultsLoop; DEALLOCATE DatabaseDefaultsLoop; +/* CheckID 272 - Performance - Optimized Locking Not Fully Set Up */ +IF EXISTS (SELECT * FROM sys.all_columns WHERE name = 'is_optimized_locking_on' AND object_id = OBJECT_ID('sys.databases')) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 272 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 272) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] ) + + SELECT + 272 AS [CheckID] , + 100 AS [Priority] , + 'Performance' AS [FindingsGroup] , + 'Optimized Locking Not Fully Set Up' AS [Finding] , + name, + 'https://www.brentozar.com/go/optimizedlocking' AS [URL] , + 'RCSI should be enabled on this database to get the full benefits of optimized locking.' AS [Details] + FROM sys.databases + WHERE is_optimized_locking_on = 1 AND is_read_committed_snapshot_on = 0; + END; + /* Check if target recovery interval <> 60 */ IF @ProductVersionMajor >= 10 From b5d7b6bc248742aebc16e0ead8e9b5763978c775 Mon Sep 17 00:00:00 2001 From: Vlad Drumea <48413726+VladDBA@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:34:51 +0200 Subject: [PATCH 69/76] fix for #3742 --- sp_Blitz.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 90c2abfc9..6bb5dc9fa 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -6764,10 +6764,10 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 271) WITH NOWAIT; - SET @tsql = N'SELECT COUNT(1) FROM sys.resource_governor_workload_groups + SET @tsql = N'SELECT @ExecRet_Out = COUNT(1) FROM sys.resource_governor_workload_groups WHERE group_max_tempdb_data_percent <> 0 AND group_max_tempdb_data_mb IS NULL'; - EXEC @ExecRet = sp_executesql @tsql; + EXEC @ExecRet = sp_executesql @tsql, N'@ExecRet_Out INT OUTPUT', @ExecRet_Out = @ExecRet OUTPUT; IF @ExecRet > 0 BEGIN DECLARE @TempDBfiles TABLE (config VARCHAR(50), data_files INT) From f660651a949c3f514a71122e93692e7fc5ae7f57 Mon Sep 17 00:00:00 2001 From: Orestes Date: Tue, 18 Nov 2025 11:30:19 +0000 Subject: [PATCH 70/76] change lots of 4 x spaces into 1 x tabs --- sp_BlitzIndex.sql | 80 +++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 8a96797ff..c748f8e07 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -2208,48 +2208,48 @@ OPTION (RECOMPILE);'; END; SET @dsql = N' - SELECT DB_ID(@i_DatabaseName) AS [database_id], - @i_DatabaseName AS database_name, + SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, s.name, - fk_object.name AS foreign_key_name, - parent_object.[object_id] AS parent_object_id, - parent_object.name AS parent_object_name, - referenced_object.[object_id] AS referenced_object_id, - referenced_object.name AS referenced_object_name, - fk.is_disabled, - fk.is_not_trusted, - fk.is_not_for_replication, - parent.fk_columns, - referenced.fk_columns, - [update_referential_action_desc], - [delete_referential_action_desc] - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects fk_object ON fk.object_id=fk_object.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects parent_object ON fk.parent_object_id=parent_object.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects referenced_object ON fk.referenced_object_id=referenced_object.object_id + fk_object.name AS foreign_key_name, + parent_object.[object_id] AS parent_object_id, + parent_object.name AS parent_object_name, + referenced_object.[object_id] AS referenced_object_id, + referenced_object.name AS referenced_object_name, + fk.is_disabled, + fk.is_not_trusted, + fk.is_not_for_replication, + parent.fk_columns, + referenced.fk_columns, + [update_referential_action_desc], + [delete_referential_action_desc] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects fk_object ON fk.object_id=fk_object.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects parent_object ON fk.parent_object_id=parent_object.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects referenced_object ON fk.referenced_object_id=referenced_object.object_id JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON fk.schema_id=s.schema_id - CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_parent.name AS fk_columns - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_parent ON fkc.parent_object_id=c_parent.[object_id] - AND fkc.parent_column_id=c_parent.column_id - WHERE fk.parent_object_id=fkc.parent_object_id - AND fk.[object_id]=fkc.constraint_object_id - ORDER BY fkc.constraint_column_id - FOR XML PATH('''') , - TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''')/*This is how we remove the first comma*/ ) parent ( fk_columns ) - CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_referenced.name AS fk_columns - FROM ' + QUOTENAME(@DatabaseName) + N'.sys. foreign_key_columns fkc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_referenced ON fkc.referenced_object_id=c_referenced.[object_id] - AND fkc.referenced_column_id=c_referenced.column_id - WHERE fk.referenced_object_id=fkc.referenced_object_id - and fk.[object_id]=fkc.constraint_object_id - ORDER BY fkc.constraint_column_id /*order by col name, we don''t have anything better*/ - FOR XML PATH('''') , - TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''') ) referenced ( fk_columns ) - ' + CASE WHEN @ObjectID IS NOT NULL THEN - 'WHERE fk.parent_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' OR fk.referenced_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' - ELSE N' ' END + ' - ORDER BY parent_object_name, foreign_key_name + CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_parent.name AS fk_columns + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_parent ON fkc.parent_object_id=c_parent.[object_id] + AND fkc.parent_column_id=c_parent.column_id + WHERE fk.parent_object_id=fkc.parent_object_id + AND fk.[object_id]=fkc.constraint_object_id + ORDER BY fkc.constraint_column_id + FOR XML PATH('''') , + TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''')/*This is how we remove the first comma*/ ) parent ( fk_columns ) + CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_referenced.name AS fk_columns + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_referenced ON fkc.referenced_object_id=c_referenced.[object_id] + AND fkc.referenced_column_id=c_referenced.column_id + WHERE fk.referenced_object_id=fkc.referenced_object_id + and fk.[object_id]=fkc.constraint_object_id + ORDER BY fkc.constraint_column_id /*order by col name, we don''t have anything better*/ + FOR XML PATH('''') , + TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''') ) referenced ( fk_columns ) + ' + CASE WHEN @ObjectID IS NOT NULL THEN + 'WHERE fk.parent_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' OR fk.referenced_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' + ELSE N' ' END + ' + ORDER BY parent_object_name, foreign_key_name OPTION (RECOMPILE);'; IF @dsql IS NULL RAISERROR('@dsql is null',16,1); From 2b86ac2ff1c2f1d088531c66a88f9909c4267e90 Mon Sep 17 00:00:00 2001 From: Adrian Burla Date: Tue, 18 Nov 2025 15:48:32 +0200 Subject: [PATCH 71/76] Fix issue #3745 VSCode parsing a THROW in sp_BlitzFirst --- sp_BlitzFirst.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index f431cb7ac..00e1f3064 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -2587,7 +2587,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, N' this is likely due to an Index operation in Progress', -1; END ELSE - BEGIN + BEGIN; THROW; END END CATCH From 38509b9a29ebe5b87ac2241def7f771ed467a9c8 Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Sat, 22 Nov 2025 06:01:39 -0800 Subject: [PATCH 72/76] 2025-11-22 release prep Bumping version numbers and dates. --- Install-All-Scripts.sql | 603 +++++++++++++++++++++++++++++----------- Install-Azure.sql | 378 ++++++++++++++++--------- SqlServerVersions.sql | 10 +- sp_Blitz.sql | 2 +- sp_BlitzAnalysis.sql | 2 +- sp_BlitzBackups.sql | 2 +- sp_BlitzCache.sql | 2 +- sp_BlitzFirst.sql | 2 +- sp_BlitzIndex.sql | 2 +- sp_BlitzLock.sql | 2 +- sp_BlitzWho.sql | 2 +- sp_DatabaseRestore.sql | 2 +- sp_ineachdb.sql | 2 +- 13 files changed, 690 insertions(+), 321 deletions(-) diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index f1ec7b551..6f6f0b67b 100644 --- a/Install-All-Scripts.sql +++ b/Install-All-Scripts.sql @@ -39,7 +39,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -570,8 +570,7 @@ AS SELECT DB_NAME(d.database_id) FROM sys.databases AS d - WHERE (DB_NAME(d.database_id) LIKE 'rdsadmin%' - OR LOWER(d.name) IN ('dbatools', 'dbadmin', 'dbmaintenance')) + WHERE LOWER(d.name) IN ('dbatools', 'dbadmin', 'dbmaintenance', 'rdsadmin') OPTION(RECOMPILE); /*Skip checks for database where we don't have read permissions*/ @@ -2047,7 +2046,9 @@ AS ''Performance'' AS FindingsGroup, ''Server Triggers Enabled'' AS Finding, ''https://www.brentozar.com/go/logontriggers/'' AS URL, - (''Server Trigger ['' + [name] ++ ''] is enabled. Make sure you understand what that trigger is doing - the less work it does, the better.'') AS Details FROM sys.server_triggers WHERE is_disabled = 0 AND is_ms_shipped = 0 OPTION (RECOMPILE);'; + (''Server Trigger ['' + [name] ++ ''] is enabled. Make sure you understand what that trigger is doing - the less work it does, the better.'') AS Details + FROM sys.server_triggers + WHERE is_disabled = 0 AND is_ms_shipped = 0 AND name NOT LIKE ''rds^_%'' ESCAPE ''^'' OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; IF @Debug = 2 AND @StringToExecute IS NULL PRINT '@StringToExecute has gone NULL, for some reason.'; @@ -2706,7 +2707,8 @@ AS + '. Tables in the master database may not be restored in the event of a disaster.' ) AS Details FROM master.sys.tables WHERE is_ms_shipped = 0 - AND name NOT IN ('CommandLog','SqlServerVersions','$ndo$srvproperty'); + AND name NOT IN ('CommandLog','SqlServerVersions','$ndo$srvproperty') + AND name NOT LIKE 'rds^_%' ESCAPE '^'; /* That last one is the Dynamics NAV licensing table: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2426 */ END; @@ -3740,6 +3742,14 @@ AS IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 94) WITH NOWAIT; + ;WITH las_job_run AS ( + SELECT MAX(instance_id) AS instance_id, + job_id, COUNT_BIG(*) AS job_executions, + SUM(CASE WHEN run_status = 0 THEN 1 ELSE 0 END) AS failed_executions + FROM msdb.dbo.sysjobhistory + WHERE step_id = 0 + GROUP BY job_id + ) INSERT INTO #BlitzResults ( CheckID , Priority , @@ -3754,8 +3764,32 @@ AS 'Agent Jobs Without Failure Emails' AS Finding , 'https://www.brentozar.com/go/alerts' AS URL , 'The job ' + [name] - + ' has not been set up to notify an operator if it fails.' AS Details + + ' has not been set up to notify an operator if it fails.' + + CASE + WHEN jh.run_date IS NULL OR jh.run_time IS NULL OR jh.run_status IS NULL THEN '' + ELSE N' Executions: '+ CAST(ljr.job_executions AS VARCHAR(30)) + + CASE ljr.failed_executions + WHEN 0 THEN N'' + ELSE N' ('+CAST(ljr.failed_executions AS NVARCHAR(10)) + N' failed)' + END + + N' - last execution started on ' + + CAST(CONVERT(DATE,CAST(jh.run_date AS NVARCHAR(8)),113) AS NVARCHAR(10)) + + N', at ' + + STUFF(STUFF(RIGHT(N'000000' + CAST(jh.run_time AS varchar(6)),6),3,0,N':'),6,0,N':') + + N', with status "' + + CASE jh.run_status + WHEN 0 THEN N'Failed' + WHEN 1 THEN N'Succeeded' + WHEN 2 THEN N'Retry' + WHEN 3 THEN N'Canceled' + WHEN 4 THEN N'In Progress' + END +N'".' + END AS Details FROM msdb.[dbo].[sysjobs] j + LEFT JOIN las_job_run ljr + ON ljr.job_id = j.job_id + LEFT JOIN msdb.[dbo].[sysjobhistory] jh + ON ljr.job_id = jh.job_id AND ljr.instance_id = jh.instance_id WHERE j.enabled = 1 AND j.notify_email_operator_id = 0 AND j.notify_netsend_operator_id = 0 @@ -4883,12 +4917,12 @@ AS SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT ' + CAST(@CurrentCheckID AS NVARCHAR(200)) + ', d.[name], ' + CAST(@CurrentPriority AS NVARCHAR(200)) + ', ''Non-Default Database Config'', ''' + @CurrentFinding + ''',''' + @CurrentURL + ''',''' + COALESCE(@CurrentDetails, 'This database setting is not the default.') + ''' FROM sys.databases d - WHERE d.database_id > 4 AND d.state = 0 AND (d.[' + @CurrentName + '] NOT IN (0, 60) OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; + WHERE d.database_id > 4 AND DB_NAME(d.database_id) != ''rdsadmin'' AND d.state = 0 AND (d.[' + @CurrentName + '] NOT IN (0, 60) OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; ELSE SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT ' + CAST(@CurrentCheckID AS NVARCHAR(200)) + ', d.[name], ' + CAST(@CurrentPriority AS NVARCHAR(200)) + ', ''Non-Default Database Config'', ''' + @CurrentFinding + ''',''' + @CurrentURL + ''',''' + COALESCE(@CurrentDetails, 'This database setting is not the default.') + ''' FROM sys.databases d - WHERE d.database_id > 4 AND d.state = 0 AND (d.[' + @CurrentName + '] <> ' + @CurrentDefaultValue + ' OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; + WHERE d.database_id > 4 AND DB_NAME(d.database_id) != ''rdsadmin'' AND d.state = 0 AND (d.[' + @CurrentName + '] <> ' + @CurrentDefaultValue + ' OR d.[' + @CurrentName + '] IS NULL) OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; IF @Debug = 2 AND @StringToExecute IS NULL PRINT '@StringToExecute has gone NULL, for some reason.'; @@ -4901,6 +4935,35 @@ AS CLOSE DatabaseDefaultsLoop; DEALLOCATE DatabaseDefaultsLoop; +/* CheckID 272 - Performance - Optimized Locking Not Fully Set Up */ +IF EXISTS (SELECT * FROM sys.all_columns WHERE name = 'is_optimized_locking_on' AND object_id = OBJECT_ID('sys.databases')) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 272 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 272) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] ) + + SELECT + 272 AS [CheckID] , + 100 AS [Priority] , + 'Performance' AS [FindingsGroup] , + 'Optimized Locking Not Fully Set Up' AS [Finding] , + name, + 'https://www.brentozar.com/go/optimizedlocking' AS [URL] , + 'RCSI should be enabled on this database to get the full benefits of optimized locking.' AS [Details] + FROM sys.databases + WHERE is_optimized_locking_on = 1 AND is_read_committed_snapshot_on = 0; + END; + /* Check if target recovery interval <> 60 */ IF @ProductVersionMajor >= 10 @@ -6752,6 +6815,70 @@ IF @ProductVersionMajor >= 10 END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 271 ) + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_percent' + AND [object_id] = OBJECT_ID('sys.resource_governor_workload_groups')) + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'group_max_tempdb_data_mb' + AND [object_id] = OBJECT_ID('sys.resource_governor_workload_groups')) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 271) WITH NOWAIT; + + SET @tsql = N'SELECT @ExecRet_Out = COUNT(1) FROM sys.resource_governor_workload_groups + WHERE group_max_tempdb_data_percent <> 0 + AND group_max_tempdb_data_mb IS NULL'; + EXEC @ExecRet = sp_executesql @tsql, N'@ExecRet_Out INT OUTPUT', @ExecRet_Out = @ExecRet OUTPUT; + IF @ExecRet > 0 + BEGIN + DECLARE @TempDBfiles TABLE (config VARCHAR(50), data_files INT) + /* Valid configs */ + INSERT INTO @TempDBfiles + SELECT 'Fixed predictable growth' AS config, SUM(1) AS data_files + FROM master.sys.master_files + WHERE database_id = DB_ID('tempdb') + AND type = 0 /* data */ + AND max_size <> -1 /* only limited ones */ + AND growth <> 0 /* growth is set */ + HAVING SUM(1) > 0 + UNION ALL + SELECT 'Growth turned off' AS config, SUM(1) AS data_files + FROM master.sys.master_files + WHERE database_id = DB_ID('tempdb') + AND type = 0 /* data */ + AND max_size = -1 /* unlimited */ + AND growth = 0 + HAVING SUM(1) > 0; + + IF 1 <> (SELECT COUNT(*) FROM @TempDBfiles) + OR (SELECT SUM(data_files) FROM @TempDBfiles) <> + (SELECT SUM(1) + FROM master.sys.master_files + WHERE database_id = DB_ID('tempdb') + AND type = 0 /* data */) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + DatabaseName , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 271 AS CheckID, + 170 AS Priority, + 'tempdb', + 'File Configuration' AS FindingsGroup, + 'TempDB Governor Config Problem' AS Finding, + 'https://www.BrentOzar.com/go/tempdbrg' AS URL, + 'Resource Governor is configured to cap TempDB usage by percent, but the TempDB file configuration will not allow that to take effect.' AS details + END + END + END + IF @CheckUserDatabaseObjects = 1 BEGIN @@ -6813,7 +6940,7 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/querystore'', (''The new SQL Server 2016 Query Store feature has not been enabled on this database.'') FROM [?].sys.database_query_store_options WHERE desired_state = 0 - AND N''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; + AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -6845,6 +6972,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 AND wait_stats_capture_mode = 0 + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6876,6 +7004,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 AND actual_state <> 2 + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6907,6 +7036,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 AND desired_state <> actual_state + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6942,6 +7072,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options WHERE desired_state <> 0 /* No point in checking this if Query Store is off. */ AND query_capture_mode_desc <> ''AUTO'' + AND ''?'' != ''rdsadmin'' OPTION (RECOMPILE)'; END; @@ -6972,7 +7103,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/cleanup'', (''SQL 2016 RTM has a bug involving dumps that happen every time Query Store cleanup jobs run. This is fixed in CU1 and later: https://sqlserverupdates.com/sql-server-2016-updates/'') FROM sys.databases AS d - WHERE d.is_query_store_on = 1 OPTION (RECOMPILE);'; + WHERE d.is_query_store_on = 1 + AND d.name != ''rdsadmin'' + OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; IF @Debug = 2 AND @StringToExecute IS NULL PRINT '@StringToExecute has gone NULL, for some reason.'; @@ -7008,7 +7141,7 @@ IF @ProductVersionMajor >= 10 FROM [?].sys.database_query_store_options dqso join master.sys.databases D on D.name = N''?'' WHERE ((dqso.actual_state = 0 AND D.is_query_store_on = 1) OR (dqso.actual_state <> 0 AND D.is_query_store_on = 0)) - AND N''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; + AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -7036,7 +7169,7 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/manylogs'', (''The ['' + DB_NAME() + ''] database has multiple log files on the '' + LEFT(physical_name, 1) + '' drive. This is not a performance booster because log file access is sequential, not parallel.'') FROM [?].sys.database_files WHERE type_desc = ''LOG'' - AND N''?'' <> ''[tempdb]'' + AND ''?'' NOT IN (''rdsadmin'',''tempdb'') GROUP BY LEFT(physical_name, 1) HAVING COUNT(*) > 1 AND SUM(size) < 268435456 OPTION (RECOMPILE);'; @@ -7068,6 +7201,7 @@ IF @ProductVersionMajor >= 10 (''The ['' + DB_NAME() + ''] database has multiple data files in one filegroup, but they are not all set up to grow in identical amounts. This can lead to uneven file activity inside the filegroup.'') FROM [?].sys.database_files WHERE type_desc = ''ROWS'' + AND ''?'' != ''rdsadmin'' GROUP BY data_space_id HAVING COUNT(DISTINCT growth) > 1 OR COUNT(DISTINCT is_percent_growth) > 1 OPTION (RECOMPILE);'; END; @@ -7096,7 +7230,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/percentgrowth'' AS URL, ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' has grown to '' + CONVERT(NVARCHAR(20), CONVERT(NUMERIC(38, 2), (f.size / 128.) / 1024.)) + '' GB, and is using percent filegrowth settings. This can lead to slow performance during growths if Instant File Initialization is not enabled.'' FROM [?].sys.database_files f - WHERE is_percent_growth = 1 and size > 128000 OPTION (RECOMPILE);'; + WHERE is_percent_growth = 1 and size > 128000 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; /* addition by Henrik Staun Poulsen, Stovi Software */ @@ -7124,7 +7260,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/percentgrowth'' AS URL, ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' is using 1MB filegrowth settings, but it has grown to '' + CAST((CAST(f.size AS BIGINT) * 8 / 1000000) AS NVARCHAR(10)) + '' GB. Time to up the growth amount.'' FROM [?].sys.database_files f - WHERE is_percent_growth = 0 and growth=128 and size > 128000 OPTION (RECOMPILE);'; + WHERE is_percent_growth = 0 and growth=128 and size > 128000 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 @@ -7154,7 +7292,9 @@ IF @ProductVersionMajor >= 10 ''Enterprise Edition Features In Use'', ''https://www.brentozar.com/go/ee'', (''The ['' + DB_NAME() + ''] database is using '' + feature_name + ''. If this database is restored onto a Standard Edition server, the restore will fail on versions prior to 2016 SP1.'') - FROM [?].sys.dm_db_persisted_sku_features OPTION (RECOMPILE);'; + FROM [?].sys.dm_db_persisted_sku_features + WHERE ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; END; @@ -7212,8 +7352,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/repl'', (''['' + DB_NAME() + ''] has MSreplication_objects tables in it, indicating it is a replication subscriber.'') FROM [?].sys.tables - WHERE name = ''dbo.MSreplication_objects'' AND ''?'' <> ''master'' OPTION (RECOMPILE)'; - + WHERE name = ''dbo.MSreplication_objects'' + AND ''?'' NOT IN (''master'', ''rdsadmin'') + OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -7241,7 +7382,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/trig'', (''The ['' + DB_NAME() + ''] database has '' + CAST(SUM(1) AS NVARCHAR(50)) + '' triggers.'') FROM [?].sys.triggers t INNER JOIN [?].sys.objects o ON t.parent_id = o.object_id - INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id WHERE t.is_ms_shipped = 0 AND DB_NAME() != ''ReportServer'' + INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id + WHERE t.is_ms_shipped = 0 + AND ''?'' NOT IN (''rdsadmin'', ''ReportServer'') HAVING SUM(1) > 0 OPTION (RECOMPILE)'; END; @@ -7271,7 +7414,9 @@ IF @ProductVersionMajor >= 10 ''Plan Guides Failing'', ''https://www.brentozar.com/go/misguided'', (''The ['' + DB_NAME() + ''] database has plan guides that are no longer valid, so the queries involved may be failing silently.'') - FROM [?].sys.plan_guides g CROSS APPLY fn_validate_plan_guide(g.plan_guide_id) OPTION (RECOMPILE)'; + FROM [?].sys.plan_guides g CROSS APPLY fn_validate_plan_guide(g.plan_guide_id) + WHERE ''?'' != ''rdsadmin'' + OPTION (RECOMPILE)'; END; IF NOT EXISTS ( SELECT 1 @@ -7299,7 +7444,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/hypo'', (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is a leftover hypothetical index from the Index Tuning Wizard or Database Tuning Advisor. This index is not actually helping performance and should be removed.'') from [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id - WHERE i.is_hypothetical = 1 OPTION (RECOMPILE);'; + WHERE i.is_hypothetical = 1 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 @@ -7355,7 +7502,9 @@ IF @ProductVersionMajor >= 10 ''https://www.brentozar.com/go/trust'', (''The ['' + DB_NAME() + ''] database has foreign keys that were probably disabled, data was changed, and then the key was enabled again. Simply enabling the key is not enough for the optimizer to use this key - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') from [?].sys.foreign_keys i INNER JOIN [?].sys.objects o ON i.parent_object_id = o.object_id INNER JOIN [?].sys.schemas s ON o.schema_id = s.schema_id - WHERE i.is_not_trusted = 1 AND i.is_not_for_replication = 0 AND i.is_disabled = 0 AND N''?'' NOT IN (''master'', ''model'', ''msdb'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE);'; + WHERE i.is_not_trusted = 1 AND i.is_not_for_replication = 0 AND i.is_disabled = 0 AND ''?'' + NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''ReportServer'', ''ReportServerTempDB'') + OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 @@ -7844,8 +7993,10 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT INNER JOIN #DatabaseScopedConfigurationDefaults def1 ON dsc.configuration_id = def1.configuration_id LEFT OUTER JOIN #DatabaseScopedConfigurationDefaults def ON dsc.configuration_id = def.configuration_id AND (cast(dsc.value as nvarchar(100)) = cast(def.default_value as nvarchar(100)) OR dsc.value IS NULL) AND (dsc.value_for_secondary = def.default_value_for_secondary OR dsc.value_for_secondary IS NULL) LEFT OUTER JOIN #SkipChecks sk ON (sk.CheckID IS NULL OR def.CheckID = sk.CheckID) AND (sk.DatabaseName IS NULL OR sk.DatabaseName = DB_NAME()) - WHERE def.configuration_id IS NULL AND sk.CheckID IS NULL ORDER BY 1 - OPTION (RECOMPILE);'; + WHERE def.configuration_id IS NULL AND sk.CheckID IS NULL + AND ''?'' != ''rdsadmin'' + ORDER BY 1 + OPTION (RECOMPILE);'; END; /* Check 218 - Show me the dodgy SET Options */ @@ -7882,6 +8033,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT OR sm.uses_quoted_identifier <> 1 ) AND o.is_ms_shipped = 0 + AND ''?'' != ''rdsadmin'' HAVING COUNT(1) > 0;'; END; --of Check 218. @@ -7913,7 +8065,9 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT + CAST(iro.sql_text AS NVARCHAR(1000)) AS Details FROM sys.index_resumable_operations iro JOIN sys.objects o ON iro.[object_id] = o.[object_id] - WHERE iro.state <> 0;'; + WHERE iro.state <> 0 + AND ''?'' != ''rdsadmin'' + ;'; END; --of Check 225. --/* Check 220 - Statistics Without Histograms */ @@ -7947,7 +8101,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT -- WHERE o.is_ms_shipped = 0 AND o.type_desc = ''USER_TABLE'' -- AND h.object_id IS NULL -- AND 0 < (SELECT SUM(row_count) FROM sys.dm_db_partition_stats ps WHERE ps.object_id = o.object_id) - -- AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'') + -- AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'') -- HAVING COUNT(DISTINCT o.object_id) > 0;'; --END; --of Check 220. @@ -8562,6 +8716,7 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT CASE WHEN [T].[TraceFlag] = '652' THEN '652 enabled globally, which disables pre-fetching during index scans. This is usually a very bad idea.' WHEN [T].[TraceFlag] = '661' THEN '661 enabled globally, which disables ghost record removal, causing the database to grow in size. This is usually a very bad idea.' WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN '834 is enabled globally, but you also have columnstore indexes. That combination is not recommended by Microsoft.' + WHEN [T].[TraceFlag] = '834' AND @CheckUserDatabaseObjects = 0 THEN '834 is enabled globally, but @CheckUserDatabaseObjects was set to 0, so we skipped checking if any databases have columnstore indexes. That combination is not recommended by Microsoft.' WHEN [T].[TraceFlag] = '1117' THEN '1117 enabled globally, which grows all files in a filegroup at the same time.' WHEN [T].[TraceFlag] = '1118' THEN '1118 enabled globally, which tries to reduce SGAM waits.' WHEN [T].[TraceFlag] = '1211' THEN '1211 enabled globally, which disables lock escalation when you least expect it. This is usually a very bad idea.' @@ -8575,10 +8730,12 @@ EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITT WHEN [T].[TraceFlag] = '3226' THEN '3226 enabled globally, which keeps the event log clean by not reporting successful backups.' WHEN [T].[TraceFlag] = '3505' THEN '3505 enabled globally, which disables Checkpoints. This is usually a very bad idea.' WHEN [T].[TraceFlag] = '4199' THEN '4199 enabled globally, which enables non-default Query Optimizer fixes, changing query plans from the default behaviors.' + WHEN [T].[TraceFlag] = '7745' AND @CheckUserDatabaseObjects = 0 THEN '7745 enabled globally, which makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. This good idea loses you the non-flushed Query Store data. @CheckUserDatabaseObjects was set to 0, so we skipped checking if any databases have Query Store enabled.' WHEN [T].[TraceFlag] = '7745' AND @QueryStoreInUse = 1 THEN '7745 enabled globally, which makes shutdowns/failovers quicker by not waiting for Query Store to flush to disk. This good idea loses you the non-flushed Query Store data.' WHEN [T].[TraceFlag] = '7745' AND @ProductVersionMajor > 12 THEN '7745 enabled globally, which is for Query Store. None of your databases have Query Store enabled, so why do you have this turned on?' WHEN [T].[TraceFlag] = '7745' AND @ProductVersionMajor <= 12 THEN '7745 enabled globally, which is for Query Store. Query Store does not exist on your SQL Server version, so why do you have this turned on?' WHEN [T].[TraceFlag] = '7752' AND @ProductVersionMajor > 14 THEN '7752 enabled globally, which is for Query Store. However, it has no effect in your SQL Server version. Consider turning it off.' + WHEN [T].[TraceFlag] = '7752' AND @CheckUserDatabaseObjects = 0 THEN '7752 enabled globally, which stops queries needing to wait on Query Store loading up after database recovery. @CheckUserDatabaseObjects was set to 0, so we skipped checking if any databases have Query Store enabled.' WHEN [T].[TraceFlag] = '7752' AND @QueryStoreInUse = 1 THEN '7752 enabled globally, which stops queries needing to wait on Query Store loading up after database recovery.' WHEN [T].[TraceFlag] = '7752' AND @ProductVersionMajor > 12 THEN '7752 enabled globally, which is for Query Store. None of your databases have Query Store enabled, so why do you have this turned on?' WHEN [T].[TraceFlag] = '7752' AND @ProductVersionMajor <= 12 THEN '7752 enabled globally, which is for Query Store. Query Store does not exist on your SQL Server version, so why do you have this turned on?' @@ -10617,7 +10774,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -11495,7 +11652,7 @@ AS SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -11708,7 +11865,7 @@ CREATE TABLE #Warnings Id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, CheckId INT, Priority INT, - DatabaseName VARCHAR(128), + DatabaseName NVARCHAR(128), Finding VARCHAR(256), Warning VARCHAR(8000) ); @@ -13279,7 +13436,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -20632,7 +20789,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex @ObjectName NVARCHAR(386) = NULL, /* 'dbname.schema.table' -- if you are lazy and want to fill in @DatabaseName, @SchemaName and @TableName, and since it's the first parameter can simply do: sp_BlitzIndex 'sch.table' */ @DatabaseName NVARCHAR(128) = NULL, /*Defaults to current DB if not specified*/ @SchemaName NVARCHAR(128) = NULL, /*Requires table_name as well.*/ - @TableName NVARCHAR(128) = NULL, /*Requires schema_name as well.*/ + @TableName NVARCHAR(261) = NULL, /*Requires schema_name as well.*/ @Mode TINYINT=0, /*0=Diagnose, 1=Summarize, 2=Index Usage Detail, 3=Missing Index Detail, 4=Diagnose Details*/ /*Note:@Mode doesn't matter if you're specifying schema_name and @TableName.*/ @Filter TINYINT = 0, /* 0=no filter (default). 1=No low-usage warnings for objects with 0 reads. 2=Only warn for objects >= 500MB */ @@ -20649,7 +20806,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex @OutputServerName NVARCHAR(256) = NULL , @OutputDatabaseName NVARCHAR(256) = NULL , @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(261) = NULL , @IncludeInactiveIndexes BIT = 0 /* Will skip indexes with no reads or writes */, @ShowAllMissingIndexRequests BIT = 0 /*Will make all missing index requests show up*/, @ShowPartitionRanges BIT = 0 /* Will add partition range values column to columnstore visualization */, @@ -20666,7 +20823,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -20750,12 +20907,60 @@ DECLARE @PartitionCount INT; DECLARE @OptimizeForSequentialKey BIT = 0; DECLARE @ResumableIndexesDisappearAfter INT = 0; DECLARE @StringToExecute NVARCHAR(MAX); +DECLARE @AzureSQLDB BIT = (SELECT CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END); /* If user was lazy and just used @ObjectName with a fully qualified table name, then lets parse out the various parts */ SET @DatabaseName = COALESCE(@DatabaseName, PARSENAME(@ObjectName, 3)) /* 3 = Database name */ SET @SchemaName = COALESCE(@SchemaName, PARSENAME(@ObjectName, 2)) /* 2 = Schema name */ SET @TableName = COALESCE(@TableName, PARSENAME(@ObjectName, 1)) /* 1 = Table name */ +/* Handle already quoted input if it wasn't fully qualified*/ +SET @DatabaseName = PARSENAME(@DatabaseName,1); +SET @SchemaName = ISNULL(PARSENAME(@SchemaName,1),PARSENAME(@TableName,2)); +SET @TableName = PARSENAME(@TableName,1); + +/* If we're on Azure SQL DB let's cut people some slack */ +IF (@TableName IS NOT NULL AND @AzureSQLDB = 1 AND @DatabaseName IS NULL) + BEGIN + SET @DatabaseName = DB_NAME(); + END; + + +IF (@SchemaName IS NULL AND @TableName IS NOT NULL) + BEGIN + /* If the target is in the current database + and there's just one table or view with this name, then we can grab the schema from sys.objects*/ + IF ((SELECT COUNT(1) FROM [sys].[objects] + WHERE [name] = @TableName AND [type] IN ('U','V'))=1 + AND @TableName IS NOT NULL AND @DatabaseName = DB_NAME()) + BEGIN + SELECT @SchemaName = SCHEMA_NAME([schema_id]) + FROM [sys].[objects] + WHERE [name] = @TableName AND [type] IN ('U','V'); + END; + /* If the target isn't in the current database, then use dynamic T-SQL*/ + IF (@DatabaseName <> DB_NAME()) + BEGIN + /*first make sure only one row is returned from sys.objects*/ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = COUNT(1) FROM ' + QUOTENAME(@DatabaseName) + N'.[sys].[objects] + WHERE [name] = @TableName_IN AND [type] IN (''U'',''V'') OPTION (RECOMPILE);'; + SET @params = N'@TableName_IN NVARCHAR(128), @RowcountOUT BIGINT OUTPUT'; + EXEC sp_executesql @dsql, @params, @TableName_IN = @TableName, @RowcountOUT = @Rowcount OUTPUT; + + IF (@Rowcount = 1) + BEGIN + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @SchemaName_OUT = s.[name] + FROM ' + QUOTENAME(@DatabaseName) + N'.[sys].[objects] o + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.[sys].[schemas] s + ON o.[schema_id] = s.[schema_id] + WHERE o.[name] = @TableName_IN AND o.[type] IN (''U'',''V'') OPTION (RECOMPILE);'; + SET @params = N'@TableName_IN NVARCHAR(128), @SchemaName_OUT NVARCHAR(128) OUTPUT'; + EXEC sp_executesql @dsql, @params, @TableName_IN = @TableName, @SchemaName_OUT = @SchemaName OUTPUT; + END; + END; + END; /* Let's get @SortOrder set to lower case here for comparisons later */ SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); @@ -20797,6 +21002,26 @@ BEGIN RETURN; END; +/* Some prep-work for output object names before checking if they're ok or not */ +IF (@OutputTableName IS NOT NULL) +BEGIN + + /*Deal with potentially quoted object names*/ + SET @OutputDatabaseName = PARSENAME(@OutputDatabaseName,1); + SET @OutputSchemaName = ISNULL(PARSENAME(@OutputSchemaName,1),PARSENAME(@OutputTableName,2)); + SET @OutputTableName = PARSENAME(@OutputTableName,1); + + /* Running on Azure SQL DB or outputting to current database? */ + IF (@OutputDatabaseName IS NULL AND @AzureSQLDB = 1) + BEGIN + SET @OutputDatabaseName = DB_NAME(); + END; + IF (@OutputSchemaName IS NULL AND @OutputDatabaseName = DB_NAME()) + BEGIN + SET @OutputSchemaName = SCHEMA_NAME(); + END; +END; + IF(@OutputType = 'TABLE' AND NOT (@OutputTableName IS NULL AND @OutputSchemaName IS NULL AND @OutputDatabaseName IS NULL AND @OutputServerName IS NULL)) BEGIN RAISERROR(N'One or more output parameters specified in combination with TABLE output, changing to NONE output mode', 0,1) WITH NOWAIT; @@ -20945,6 +21170,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL is_spatial BIT NOT NULL, is_NC_columnstore BIT NOT NULL, is_CX_columnstore BIT NOT NULL, + is_JSON BIT NOT NULL, is_in_memory_oltp BIT NOT NULL , is_disabled BIT NOT NULL , is_hypothetical BIT NOT NULL , @@ -20986,6 +21212,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL ELSE N'' END + CASE WHEN is_XML = 1 THEN N'[XML] ' ELSE N'' END + CASE WHEN is_spatial = 1 THEN N'[SPATIAL] ' ELSE N'' END + CASE WHEN is_NC_columnstore = 1 THEN N'[COLUMNSTORE] ' + ELSE N'' END + CASE WHEN is_json = 1 THEN N'[JSON] ' ELSE N'' END + CASE WHEN is_in_memory_oltp = 1 THEN N'[IN-MEMORY] ' ELSE N'' END + CASE WHEN is_disabled = 1 THEN N'[DISABLED] ' ELSE N'' END + CASE WHEN is_hypothetical = 1 THEN N'[HYPOTHETICAL] ' @@ -22030,6 +22257,7 @@ BEGIN TRY CASE when si.type = 4 THEN 1 ELSE 0 END AS is_spatial, CASE when si.type = 6 THEN 1 ELSE 0 END AS is_NC_columnstore, CASE when si.type = 5 then 1 else 0 end as is_CX_columnstore, + CASE when si.type = 9 then 1 else 0 end as is_JSON, CASE when si.data_space_id = 0 then 1 else 0 end as is_in_memory_oltp, si.is_disabled, si.is_hypothetical, @@ -22063,8 +22291,8 @@ BEGIN TRY LEFT JOIN sys.dm_db_index_usage_stats AS us WITH (NOLOCK) ON si.[object_id] = us.[object_id] AND si.index_id = us.index_id AND us.database_id = ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' - WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) - /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + + WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6, 9 ) + /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore, JSON */ ' + CASE WHEN @TableName IS NOT NULL THEN N' and so.name=' + QUOTENAME(@TableName,N'''') + N' ' ELSE N'' END + CASE WHEN ( @IncludeInactiveIndexes = 0 AND @Mode IN (0, 4) @@ -22092,7 +22320,7 @@ BEGIN TRY PRINT SUBSTRING(@dsql, 36000, 40000); END; INSERT #IndexSanity ( [database_id], [object_id], [index_id], [index_type], [database_name], [schema_name], [object_name], - index_name, is_indexed_view, is_unique, is_primary_key, is_unique_constraint, is_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, is_in_memory_oltp, + index_name, is_indexed_view, is_unique, is_primary_key, is_unique_constraint, is_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, is_JSON, is_in_memory_oltp, is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, [optimize_for_sequential_key], user_seeks, user_scans, user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, create_date, modify_date ) @@ -22569,7 +22797,7 @@ WITH ON ty.user_type_id = co.user_type_id WHERE id_inner.index_handle = id.index_handle AND id_inner.object_id = id.object_id - AND id_inner.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + AND id_inner.database_id = DB_ID(@i_DatabaseName) AND cn_inner.IndexColumnType = cn.IndexColumnType FOR XML PATH('''') ), @@ -22607,7 +22835,7 @@ WITH ) x (n) CROSS APPLY n.nodes(''x'') node(v) )AS cn - WHERE id.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + WHERE id.database_id = DB_ID(@i_DatabaseName) GROUP BY id.index_handle, id.object_id, @@ -22753,48 +22981,48 @@ OPTION (RECOMPILE);'; END; SET @dsql = N' - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - @i_DatabaseName AS database_name, + SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, s.name, - fk_object.name AS foreign_key_name, - parent_object.[object_id] AS parent_object_id, - parent_object.name AS parent_object_name, - referenced_object.[object_id] AS referenced_object_id, - referenced_object.name AS referenced_object_name, - fk.is_disabled, - fk.is_not_trusted, - fk.is_not_for_replication, - parent.fk_columns, - referenced.fk_columns, - [update_referential_action_desc], - [delete_referential_action_desc] - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects fk_object ON fk.object_id=fk_object.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects parent_object ON fk.parent_object_id=parent_object.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects referenced_object ON fk.referenced_object_id=referenced_object.object_id + fk_object.name AS foreign_key_name, + parent_object.[object_id] AS parent_object_id, + parent_object.name AS parent_object_name, + referenced_object.[object_id] AS referenced_object_id, + referenced_object.name AS referenced_object_name, + fk.is_disabled, + fk.is_not_trusted, + fk.is_not_for_replication, + parent.fk_columns, + referenced.fk_columns, + [update_referential_action_desc], + [delete_referential_action_desc] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects fk_object ON fk.object_id=fk_object.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects parent_object ON fk.parent_object_id=parent_object.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects referenced_object ON fk.referenced_object_id=referenced_object.object_id JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON fk.schema_id=s.schema_id - CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_parent.name AS fk_columns - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_parent ON fkc.parent_object_id=c_parent.[object_id] - AND fkc.parent_column_id=c_parent.column_id - WHERE fk.parent_object_id=fkc.parent_object_id - AND fk.[object_id]=fkc.constraint_object_id - ORDER BY fkc.constraint_column_id - FOR XML PATH('''') , - TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''')/*This is how we remove the first comma*/ ) parent ( fk_columns ) - CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_referenced.name AS fk_columns - FROM ' + QUOTENAME(@DatabaseName) + N'.sys. foreign_key_columns fkc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_referenced ON fkc.referenced_object_id=c_referenced.[object_id] - AND fkc.referenced_column_id=c_referenced.column_id - WHERE fk.referenced_object_id=fkc.referenced_object_id - and fk.[object_id]=fkc.constraint_object_id - ORDER BY fkc.constraint_column_id /*order by col name, we don''t have anything better*/ - FOR XML PATH('''') , - TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''') ) referenced ( fk_columns ) - ' + CASE WHEN @ObjectID IS NOT NULL THEN - 'WHERE fk.parent_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' OR fk.referenced_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' - ELSE N' ' END + ' - ORDER BY parent_object_name, foreign_key_name + CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_parent.name AS fk_columns + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_parent ON fkc.parent_object_id=c_parent.[object_id] + AND fkc.parent_column_id=c_parent.column_id + WHERE fk.parent_object_id=fkc.parent_object_id + AND fk.[object_id]=fkc.constraint_object_id + ORDER BY fkc.constraint_column_id + FOR XML PATH('''') , + TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''')/*This is how we remove the first comma*/ ) parent ( fk_columns ) + CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_referenced.name AS fk_columns + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_referenced ON fkc.referenced_object_id=c_referenced.[object_id] + AND fkc.referenced_column_id=c_referenced.column_id + WHERE fk.referenced_object_id=fkc.referenced_object_id + and fk.[object_id]=fkc.constraint_object_id + ORDER BY fkc.constraint_column_id /*order by col name, we don''t have anything better*/ + FOR XML PATH('''') , + TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''') ) referenced ( fk_columns ) + ' + CASE WHEN @ObjectID IS NOT NULL THEN + 'WHERE fk.parent_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' OR fk.referenced_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' + ELSE N' ' END + ' + ORDER BY parent_object_name, foreign_key_name OPTION (RECOMPILE);'; IF @dsql IS NULL RAISERROR('@dsql is null',16,1); @@ -22822,17 +23050,17 @@ OPTION (RECOMPILE);'; BEGIN SET @dsql = N' SELECT - DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, foreign_key_schema = s.name, foreign_key_name = fk.name, foreign_key_table = - OBJECT_NAME(fk.parent_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + OBJECT_NAME(fk.parent_object_id, DB_ID(@i_DatabaseName)), fk.parent_object_id, foreign_key_referenced_table = - OBJECT_NAME(fk.referenced_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + OBJECT_NAME(fk.referenced_object_id, DB_ID(@i_DatabaseName)), fk.referenced_object_id FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s @@ -22906,7 +23134,7 @@ OPTION (RECOMPILE);'; days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SELECT DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, obj.object_id, obj.name AS table_name, @@ -23001,7 +23229,7 @@ OPTION (RECOMPILE);'; last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SELECT DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, obj.object_id, obj.name AS table_name, @@ -23134,7 +23362,7 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Temporal Table Info',0,1) WITH NOWAIT; SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS database_name, - DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + DB_ID(@i_DatabaseName) AS [database_id], s.name AS schema_name, t.name AS table_name, oa.hsn as history_schema_name, @@ -23170,7 +23398,7 @@ OPTION (RECOMPILE);'; 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_object_id ) - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; END; SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], @@ -26266,6 +26494,7 @@ BEGIN 'N/A' AS index_usage_summary, 'N/A' AS index_size_summary FROM #TemporalTables AS t + ORDER BY t.database_name, t.schema_name, t.table_name OPTION ( RECOMPILE ); RAISERROR(N'check_id 121: Optimized For Sequential Keys.', 0,1) WITH NOWAIT; @@ -27597,7 +27826,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF @VersionCheckMode = 1 BEGIN @@ -30202,56 +30431,37 @@ BEGIN lock_types AS ( SELECT - database_name = - dp.database_name, + dp.database_name, dow.object_name, lock = CASE WHEN CHARINDEX(N':', dp.wait_resource) > 0 - THEN SUBSTRING - ( - dp.wait_resource, - 1, - CHARINDEX(N':', dp.wait_resource) - 1 - ) + THEN LEFT(dp.wait_resource, CHARINDEX(N':', dp.wait_resource) - 1) ELSE dp.wait_resource END, - lock_count = - CONVERT - ( - nvarchar(20), - COUNT_BIG(DISTINCT dp.event_date) - ) + lock_count = CONVERT(nvarchar(20), COUNT_BIG(DISTINCT dp.event_date)) FROM #deadlock_process AS dp JOIN #deadlock_owner_waiter AS dow - ON (dp.id = dow.owner_id - OR dp.victim_id = dow.waiter_id) - AND dp.event_date = dow.event_date - WHERE 1 = 1 - AND (dp.database_name = @DatabaseName OR @DatabaseName IS NULL) - AND (dp.event_date >= @StartDate OR @StartDate IS NULL) - AND (dp.event_date < @EndDate OR @EndDate IS NULL) - AND (dp.client_app = @AppName OR @AppName IS NULL) - AND (dp.host_name = @HostName OR @HostName IS NULL) - AND (dp.login_name = @LoginName OR @LoginName IS NULL) - AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) - AND dow.object_name IS NOT NULL + ON (dp.id = dow.owner_id OR dp.victim_id = dow.waiter_id) + AND dp.event_date = dow.event_date + WHERE (dp.database_name = @DatabaseName OR @DatabaseName IS NULL) + AND (dp.event_date >= @StartDate OR @StartDate IS NULL) + AND (dp.event_date < @EndDate OR @EndDate IS NULL) + AND (dp.client_app = @AppName OR @AppName IS NULL) + AND (dp.host_name = @HostName OR @HostName IS NULL) + AND (dp.login_name = @LoginName OR @LoginName IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.object_name IS NOT NULL GROUP BY dp.database_name, + dow.object_name, CASE WHEN CHARINDEX(N':', dp.wait_resource) > 0 - THEN SUBSTRING - ( - dp.wait_resource, - 1, - CHARINDEX(N':', dp.wait_resource) - 1 - ) + THEN LEFT(dp.wait_resource, CHARINDEX(N':', dp.wait_resource) - 1) ELSE dp.wait_resource - END, - dow.object_name + END ) - INSERT - #deadlock_findings WITH(TABLOCKX) + INSERT #deadlock_findings WITH (TABLOCKX) ( check_id, database_name, @@ -30261,36 +30471,33 @@ BEGIN sort_order ) SELECT - check_id = 7, + check_id = 7, lt.database_name, lt.object_name, finding_group = N'Types of locks by object', - finding = + finding = N'This object has had ' + - STUFF - ( + STUFF( ( SELECT - N', ' + - lt2.lock_count + - N' ' + - lt2.lock + N', ' + lt2.lock_count + N' ' + lt2.lock FROM lock_types AS lt2 WHERE lt2.database_name = lt.database_name - AND lt2.object_name = lt.object_name - FOR XML - PATH(N''), - TYPE - ).value(N'.[1]', N'nvarchar(MAX)'), - 1, - 1, - N'' + AND lt2.object_name = lt.object_name + FOR XML PATH(''), TYPE + ).value('.', 'nvarchar(max)'), + 1, 2, N'' ) + N' locks.', sort_order = - ROW_NUMBER() - OVER (ORDER BY CONVERT(bigint, lt.lock_count) DESC) + ROW_NUMBER() OVER ( + ORDER BY + MAX(CONVERT(bigint, lt.lock_count)) DESC + ) FROM lock_types AS lt - OPTION(RECOMPILE); + GROUP BY + lt.database_name, + lt.object_name + OPTION (RECOMPILE); RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; @@ -32139,7 +32346,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -32196,6 +32403,7 @@ DECLARE @ProductVersion NVARCHAR(128) = CAST(SERVERPROPERTY('ProductVersion') A ,@ProductVersionMajor DECIMAL(10,2) ,@ProductVersionMinor DECIMAL(10,2) ,@Platform NVARCHAR(8) /* Azure or NonAzure are acceptable */ = (SELECT CASE WHEN @@VERSION LIKE '%Azure%' THEN N'Azure' ELSE N'NonAzure' END AS [Platform]) + ,@AzureSQLDB BIT = (SELECT CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END) ,@EnhanceFlag BIT = 0 ,@BlockingCheck NVARCHAR(MAX) ,@StringToSelect NVARCHAR(MAX) @@ -32233,10 +32441,10 @@ SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @Produ @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) SELECT - @OutputTableNameQueryStats_View = QUOTENAME(@OutputTableName + '_Deltas'), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName), + @OutputTableNameQueryStats_View = QUOTENAME(PARSENAME(@OutputTableName,1) + '_Deltas'), + @OutputDatabaseName = QUOTENAME(PARSENAME(@OutputDatabaseName,1)), + @OutputSchemaName = ISNULL(QUOTENAME(PARSENAME(@OutputSchemaName,1)),QUOTENAME(PARSENAME(@OutputTableName,2))), + @OutputTableName = QUOTENAME(PARSENAME(@OutputTableName,1)), @LineFeed = CHAR(13) + CHAR(10); IF @GetLiveQueryPlan IS NULL @@ -32247,6 +32455,20 @@ IF @GetLiveQueryPlan IS NULL SET @GetLiveQueryPlan = 0; END +IF @OutputTableName IS NOT NULL AND (@OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL) + BEGIN + IF @OutputDatabaseName IS NULL AND @AzureSQLDB = 1 + BEGIN + /* If we're in Azure SQL DB then use the current database */ + SET @OutputDatabaseName = QUOTENAME(DB_NAME()); + END; + IF @OutputSchemaName IS NULL AND @OutputDatabaseName = QUOTENAME(DB_NAME()) + BEGIN + /* If we're inserting records in the current database use the default schema */ + SET @OutputSchemaName = QUOTENAME(SCHEMA_NAME()); + END; + END; + IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL AND EXISTS ( SELECT * FROM sys.databases @@ -33560,7 +33782,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -35230,7 +35452,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -35610,11 +35832,13 @@ INSERT INTO dbo.SqlServerVersions (MajorVersionNumber, MinorVersionNumber, Branch, [Url], ReleaseDate, MainstreamSupportEndDate, ExtendedSupportEndDate, MajorVersionName, MinorVersionName) VALUES /*2025*/ - (17, 925, 'RC1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-09-17', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC1'), - (17, 900, 'RC0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-08-20', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC0'), - (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.1'), - (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.0'), + (17, 1000, 'RTM', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-11-18', '2031-01-06', '2036-01-06', 'SQL Server 2025', 'RTM'), + (17, 925, 'RC1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-09-17', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview RC1'), + (17, 900, 'RC0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-08-20', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview RC0'), + (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview CTP 2.1'), + (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview CTP 2.0'), /*2022*/ + (16, 4225, 'CU22', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate22', '2025-11-13', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 22'), (16, 4215, 'CU21', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate21', '2025-09-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 21'), (16, 4205, 'CU20', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate20', '2025-07-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 20'), (16, 4200, 'CU19 GDR', 'https://support.microsoft.com/en-us/help/5058721', '2025-07-08', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19 GDR'), @@ -36112,7 +36336,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -36210,7 +36434,18 @@ DECLARE @StringToExecute NVARCHAR(MAX), @get_thread_time_ms NVARCHAR(MAX) = N'', @thread_time_ms FLOAT = 0, @logical_processors INT = 0, - @max_worker_threads INT = 0; + @max_worker_threads INT = 0, + @is_windows_operating_system BIT = 1; + +IF EXISTS +( + SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_os_host_info' +) +BEGIN + SELECT @is_windows_operating_system = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info; +END; /* Sanitize our inputs */ SELECT @@ -38464,19 +38699,28 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; END + /* Traditionally, we use 100 - SystemIdle here. + However, SystemIdle is always 0 on Linux. + So if we are on Linux, we use ProcessUtilization instead. + This is the approach found in + https://techcommunity.microsoft.com/blog/sqlserver/sql-server-cpu-usage-available-in-sys-dm-os-ring-buffers-dmv-starting-sql-server/825361 */ INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%.', 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%.', CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y - WHERE 100 - SystemIdle >= 50; + ) AS ShreddedCpuXml + ) AS OsCpu + WHERE CpuUsage >= 50; /* CPU Utilization - CheckID 23 */ IF (@Debug = 1) @@ -38488,7 +38732,8 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, WITH y AS ( - SELECT CONVERT(VARCHAR(5), 100 - ca.c.value('.', 'INT')) AS system_idle, + /* See earlier comments about SystemIdle on Linux. */ + SELECT CONVERT(VARCHAR(5), CASE WHEN @is_windows_operating_system = 1 THEN 100 - ca.c.value('.', 'INT') ELSE ca2.p.value('.', 'INT') END) AS cpu_usage, CONVERT(VARCHAR(30), rb.event_date) AS event_date, CONVERT(VARCHAR(8000), rb.record) AS record, event_date as event_date_raw @@ -38501,6 +38746,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, WHERE dorb.ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ) AS rb CROSS APPLY rb.record.nodes('/Record/SchedulerMonitorEvent/SystemHealth/SystemIdle') AS ca(c) + CROSS APPLY rb.record.nodes('/Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization') AS ca2(p) ) INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL, HowToStopIt) SELECT TOP 1 @@ -38508,12 +38754,12 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, 250, 'Server Info', 'CPU Utilization', - y.system_idle + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), - y.system_idle , + y.cpu_usage + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), + y.cpu_usage , 'https://www.brentozar.com/go/cpu', STUFF(( SELECT TOP 2147483647 CHAR(10) + CHAR(13) - + y2.system_idle + + y2.cpu_usage + '% ON ' + y2.event_date + ' Ring buffer details: ' @@ -38544,7 +38790,12 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, AND record LIKE '%%' ORDER BY timestamp DESC) AS rb ) AS y - WHERE 100 - (y.SQLUsage + y.SystemIdle) >= 25; + WHERE 100 - (y.SQLUsage + y.SystemIdle) >= 25 + /* SystemIdle is always 0 on Linux, as described earlier. + We therefore cannot distinguish between a totally idle Linux server and + a Linux server where SQL Server is being crushed by other CPU-heavy processes. + We therefore disable this check on Linux. */ + AND @is_windows_operating_system = 1; END; /* IF @Seconds < 30 */ @@ -38625,7 +38876,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, N' this is likely due to an Index operation in Progress', -1; END ELSE - BEGIN + BEGIN; THROW; END END CATCH @@ -39658,18 +39909,24 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + /* See earlier comments about SystemIdle on Linux. */ + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y - WHERE 100 - SystemIdle >= 50; + ) AS ShreddedCpuXml + ) AS OsCpu + WHERE CpuUsage >= 50; /* Server Performance - CPU Utilization CheckID 23 */ IF (@Debug = 1) @@ -39678,17 +39935,23 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 23, 250, 'Server Info', 'CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 23, 250, 'Server Info', 'CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + /* See earlier comments about SystemIdle on Linux. */ + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y; + ) AS ShreddedCpuXml + ) AS OsCpu; END; /* IF @Seconds >= 30 */ diff --git a/Install-Azure.sql b/Install-Azure.sql index 5f0a15cdf..879137642 100644 --- a/Install-Azure.sql +++ b/Install-Azure.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -1174,7 +1174,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -8558,7 +8558,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -8656,7 +8656,18 @@ DECLARE @StringToExecute NVARCHAR(MAX), @get_thread_time_ms NVARCHAR(MAX) = N'', @thread_time_ms FLOAT = 0, @logical_processors INT = 0, - @max_worker_threads INT = 0; + @max_worker_threads INT = 0, + @is_windows_operating_system BIT = 1; + +IF EXISTS +( + SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_os_host_info' +) +BEGIN + SELECT @is_windows_operating_system = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info; +END; /* Sanitize our inputs */ SELECT @@ -10910,19 +10921,28 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; END + /* Traditionally, we use 100 - SystemIdle here. + However, SystemIdle is always 0 on Linux. + So if we are on Linux, we use ProcessUtilization instead. + This is the approach found in + https://techcommunity.microsoft.com/blog/sqlserver/sql-server-cpu-usage-available-in-sys-dm-os-ring-buffers-dmv-starting-sql-server/825361 */ INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%.', 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%.', CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y - WHERE 100 - SystemIdle >= 50; + ) AS ShreddedCpuXml + ) AS OsCpu + WHERE CpuUsage >= 50; /* CPU Utilization - CheckID 23 */ IF (@Debug = 1) @@ -10934,7 +10954,8 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, WITH y AS ( - SELECT CONVERT(VARCHAR(5), 100 - ca.c.value('.', 'INT')) AS system_idle, + /* See earlier comments about SystemIdle on Linux. */ + SELECT CONVERT(VARCHAR(5), CASE WHEN @is_windows_operating_system = 1 THEN 100 - ca.c.value('.', 'INT') ELSE ca2.p.value('.', 'INT') END) AS cpu_usage, CONVERT(VARCHAR(30), rb.event_date) AS event_date, CONVERT(VARCHAR(8000), rb.record) AS record, event_date as event_date_raw @@ -10947,6 +10968,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, WHERE dorb.ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ) AS rb CROSS APPLY rb.record.nodes('/Record/SchedulerMonitorEvent/SystemHealth/SystemIdle') AS ca(c) + CROSS APPLY rb.record.nodes('/Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization') AS ca2(p) ) INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL, HowToStopIt) SELECT TOP 1 @@ -10954,12 +10976,12 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, 250, 'Server Info', 'CPU Utilization', - y.system_idle + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), - y.system_idle , + y.cpu_usage + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), + y.cpu_usage , 'https://www.brentozar.com/go/cpu', STUFF(( SELECT TOP 2147483647 CHAR(10) + CHAR(13) - + y2.system_idle + + y2.cpu_usage + '% ON ' + y2.event_date + ' Ring buffer details: ' @@ -10990,7 +11012,12 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, AND record LIKE '%%' ORDER BY timestamp DESC) AS rb ) AS y - WHERE 100 - (y.SQLUsage + y.SystemIdle) >= 25; + WHERE 100 - (y.SQLUsage + y.SystemIdle) >= 25 + /* SystemIdle is always 0 on Linux, as described earlier. + We therefore cannot distinguish between a totally idle Linux server and + a Linux server where SQL Server is being crushed by other CPU-heavy processes. + We therefore disable this check on Linux. */ + AND @is_windows_operating_system = 1; END; /* IF @Seconds < 30 */ @@ -11071,7 +11098,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, N' this is likely due to an Index operation in Progress', -1; END ELSE - BEGIN + BEGIN; THROW; END END CATCH @@ -12104,18 +12131,24 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 24, 50, 'Server Performance', 'High CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + /* See earlier comments about SystemIdle on Linux. */ + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y - WHERE 100 - SystemIdle >= 50; + ) AS ShreddedCpuXml + ) AS OsCpu + WHERE CpuUsage >= 50; /* Server Performance - CPU Utilization CheckID 23 */ IF (@Debug = 1) @@ -12124,17 +12157,23 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 23, 250, 'Server Info', 'CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + SELECT 23, 250, 'Server Info', 'CPU Utilization', CAST(CpuUsage AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), CpuUsage, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + CASE WHEN @is_windows_operating_system = 1 THEN 100 - SystemIdle ELSE ProcessUtilization END AS CpuUsage FROM ( SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle, + /* See earlier comments about SystemIdle on Linux. */ + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS ProcessUtilization FROM ( SELECT TOP 1 CONVERT(XML, record) AS record FROM sys.dm_os_ring_buffers WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR' AND record LIKE '%%' ORDER BY timestamp DESC) AS rb - ) AS y; + ) AS ShreddedCpuXml + ) AS OsCpu; END; /* IF @Seconds >= 30 */ @@ -13651,7 +13690,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex @ObjectName NVARCHAR(386) = NULL, /* 'dbname.schema.table' -- if you are lazy and want to fill in @DatabaseName, @SchemaName and @TableName, and since it's the first parameter can simply do: sp_BlitzIndex 'sch.table' */ @DatabaseName NVARCHAR(128) = NULL, /*Defaults to current DB if not specified*/ @SchemaName NVARCHAR(128) = NULL, /*Requires table_name as well.*/ - @TableName NVARCHAR(128) = NULL, /*Requires schema_name as well.*/ + @TableName NVARCHAR(261) = NULL, /*Requires schema_name as well.*/ @Mode TINYINT=0, /*0=Diagnose, 1=Summarize, 2=Index Usage Detail, 3=Missing Index Detail, 4=Diagnose Details*/ /*Note:@Mode doesn't matter if you're specifying schema_name and @TableName.*/ @Filter TINYINT = 0, /* 0=no filter (default). 1=No low-usage warnings for objects with 0 reads. 2=Only warn for objects >= 500MB */ @@ -13668,7 +13707,7 @@ ALTER PROCEDURE dbo.sp_BlitzIndex @OutputServerName NVARCHAR(256) = NULL , @OutputDatabaseName NVARCHAR(256) = NULL , @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(261) = NULL , @IncludeInactiveIndexes BIT = 0 /* Will skip indexes with no reads or writes */, @ShowAllMissingIndexRequests BIT = 0 /*Will make all missing index requests show up*/, @ShowPartitionRanges BIT = 0 /* Will add partition range values column to columnstore visualization */, @@ -13685,7 +13724,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -13769,12 +13808,60 @@ DECLARE @PartitionCount INT; DECLARE @OptimizeForSequentialKey BIT = 0; DECLARE @ResumableIndexesDisappearAfter INT = 0; DECLARE @StringToExecute NVARCHAR(MAX); +DECLARE @AzureSQLDB BIT = (SELECT CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END); /* If user was lazy and just used @ObjectName with a fully qualified table name, then lets parse out the various parts */ SET @DatabaseName = COALESCE(@DatabaseName, PARSENAME(@ObjectName, 3)) /* 3 = Database name */ SET @SchemaName = COALESCE(@SchemaName, PARSENAME(@ObjectName, 2)) /* 2 = Schema name */ SET @TableName = COALESCE(@TableName, PARSENAME(@ObjectName, 1)) /* 1 = Table name */ +/* Handle already quoted input if it wasn't fully qualified*/ +SET @DatabaseName = PARSENAME(@DatabaseName,1); +SET @SchemaName = ISNULL(PARSENAME(@SchemaName,1),PARSENAME(@TableName,2)); +SET @TableName = PARSENAME(@TableName,1); + +/* If we're on Azure SQL DB let's cut people some slack */ +IF (@TableName IS NOT NULL AND @AzureSQLDB = 1 AND @DatabaseName IS NULL) + BEGIN + SET @DatabaseName = DB_NAME(); + END; + + +IF (@SchemaName IS NULL AND @TableName IS NOT NULL) + BEGIN + /* If the target is in the current database + and there's just one table or view with this name, then we can grab the schema from sys.objects*/ + IF ((SELECT COUNT(1) FROM [sys].[objects] + WHERE [name] = @TableName AND [type] IN ('U','V'))=1 + AND @TableName IS NOT NULL AND @DatabaseName = DB_NAME()) + BEGIN + SELECT @SchemaName = SCHEMA_NAME([schema_id]) + FROM [sys].[objects] + WHERE [name] = @TableName AND [type] IN ('U','V'); + END; + /* If the target isn't in the current database, then use dynamic T-SQL*/ + IF (@DatabaseName <> DB_NAME()) + BEGIN + /*first make sure only one row is returned from sys.objects*/ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = COUNT(1) FROM ' + QUOTENAME(@DatabaseName) + N'.[sys].[objects] + WHERE [name] = @TableName_IN AND [type] IN (''U'',''V'') OPTION (RECOMPILE);'; + SET @params = N'@TableName_IN NVARCHAR(128), @RowcountOUT BIGINT OUTPUT'; + EXEC sp_executesql @dsql, @params, @TableName_IN = @TableName, @RowcountOUT = @Rowcount OUTPUT; + + IF (@Rowcount = 1) + BEGIN + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @SchemaName_OUT = s.[name] + FROM ' + QUOTENAME(@DatabaseName) + N'.[sys].[objects] o + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.[sys].[schemas] s + ON o.[schema_id] = s.[schema_id] + WHERE o.[name] = @TableName_IN AND o.[type] IN (''U'',''V'') OPTION (RECOMPILE);'; + SET @params = N'@TableName_IN NVARCHAR(128), @SchemaName_OUT NVARCHAR(128) OUTPUT'; + EXEC sp_executesql @dsql, @params, @TableName_IN = @TableName, @SchemaName_OUT = @SchemaName OUTPUT; + END; + END; + END; /* Let's get @SortOrder set to lower case here for comparisons later */ SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); @@ -13816,6 +13903,26 @@ BEGIN RETURN; END; +/* Some prep-work for output object names before checking if they're ok or not */ +IF (@OutputTableName IS NOT NULL) +BEGIN + + /*Deal with potentially quoted object names*/ + SET @OutputDatabaseName = PARSENAME(@OutputDatabaseName,1); + SET @OutputSchemaName = ISNULL(PARSENAME(@OutputSchemaName,1),PARSENAME(@OutputTableName,2)); + SET @OutputTableName = PARSENAME(@OutputTableName,1); + + /* Running on Azure SQL DB or outputting to current database? */ + IF (@OutputDatabaseName IS NULL AND @AzureSQLDB = 1) + BEGIN + SET @OutputDatabaseName = DB_NAME(); + END; + IF (@OutputSchemaName IS NULL AND @OutputDatabaseName = DB_NAME()) + BEGIN + SET @OutputSchemaName = SCHEMA_NAME(); + END; +END; + IF(@OutputType = 'TABLE' AND NOT (@OutputTableName IS NULL AND @OutputSchemaName IS NULL AND @OutputDatabaseName IS NULL AND @OutputServerName IS NULL)) BEGIN RAISERROR(N'One or more output parameters specified in combination with TABLE output, changing to NONE output mode', 0,1) WITH NOWAIT; @@ -13964,6 +14071,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL is_spatial BIT NOT NULL, is_NC_columnstore BIT NOT NULL, is_CX_columnstore BIT NOT NULL, + is_JSON BIT NOT NULL, is_in_memory_oltp BIT NOT NULL , is_disabled BIT NOT NULL , is_hypothetical BIT NOT NULL , @@ -14005,6 +14113,7 @@ IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL ELSE N'' END + CASE WHEN is_XML = 1 THEN N'[XML] ' ELSE N'' END + CASE WHEN is_spatial = 1 THEN N'[SPATIAL] ' ELSE N'' END + CASE WHEN is_NC_columnstore = 1 THEN N'[COLUMNSTORE] ' + ELSE N'' END + CASE WHEN is_json = 1 THEN N'[JSON] ' ELSE N'' END + CASE WHEN is_in_memory_oltp = 1 THEN N'[IN-MEMORY] ' ELSE N'' END + CASE WHEN is_disabled = 1 THEN N'[DISABLED] ' ELSE N'' END + CASE WHEN is_hypothetical = 1 THEN N'[HYPOTHETICAL] ' @@ -15049,6 +15158,7 @@ BEGIN TRY CASE when si.type = 4 THEN 1 ELSE 0 END AS is_spatial, CASE when si.type = 6 THEN 1 ELSE 0 END AS is_NC_columnstore, CASE when si.type = 5 then 1 else 0 end as is_CX_columnstore, + CASE when si.type = 9 then 1 else 0 end as is_JSON, CASE when si.data_space_id = 0 then 1 else 0 end as is_in_memory_oltp, si.is_disabled, si.is_hypothetical, @@ -15082,8 +15192,8 @@ BEGIN TRY LEFT JOIN sys.dm_db_index_usage_stats AS us WITH (NOLOCK) ON si.[object_id] = us.[object_id] AND si.index_id = us.index_id AND us.database_id = ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' - WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) - /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + + WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6, 9 ) + /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore, JSON */ ' + CASE WHEN @TableName IS NOT NULL THEN N' and so.name=' + QUOTENAME(@TableName,N'''') + N' ' ELSE N'' END + CASE WHEN ( @IncludeInactiveIndexes = 0 AND @Mode IN (0, 4) @@ -15111,7 +15221,7 @@ BEGIN TRY PRINT SUBSTRING(@dsql, 36000, 40000); END; INSERT #IndexSanity ( [database_id], [object_id], [index_id], [index_type], [database_name], [schema_name], [object_name], - index_name, is_indexed_view, is_unique, is_primary_key, is_unique_constraint, is_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, is_in_memory_oltp, + index_name, is_indexed_view, is_unique, is_primary_key, is_unique_constraint, is_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, is_JSON, is_in_memory_oltp, is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, [optimize_for_sequential_key], user_seeks, user_scans, user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, create_date, modify_date ) @@ -15588,7 +15698,7 @@ WITH ON ty.user_type_id = co.user_type_id WHERE id_inner.index_handle = id.index_handle AND id_inner.object_id = id.object_id - AND id_inner.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + AND id_inner.database_id = DB_ID(@i_DatabaseName) AND cn_inner.IndexColumnType = cn.IndexColumnType FOR XML PATH('''') ), @@ -15626,7 +15736,7 @@ WITH ) x (n) CROSS APPLY n.nodes(''x'') node(v) )AS cn - WHERE id.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + WHERE id.database_id = DB_ID(@i_DatabaseName) GROUP BY id.index_handle, id.object_id, @@ -15772,48 +15882,48 @@ OPTION (RECOMPILE);'; END; SET @dsql = N' - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - @i_DatabaseName AS database_name, + SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, s.name, - fk_object.name AS foreign_key_name, - parent_object.[object_id] AS parent_object_id, - parent_object.name AS parent_object_name, - referenced_object.[object_id] AS referenced_object_id, - referenced_object.name AS referenced_object_name, - fk.is_disabled, - fk.is_not_trusted, - fk.is_not_for_replication, - parent.fk_columns, - referenced.fk_columns, - [update_referential_action_desc], - [delete_referential_action_desc] - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects fk_object ON fk.object_id=fk_object.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects parent_object ON fk.parent_object_id=parent_object.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects referenced_object ON fk.referenced_object_id=referenced_object.object_id + fk_object.name AS foreign_key_name, + parent_object.[object_id] AS parent_object_id, + parent_object.name AS parent_object_name, + referenced_object.[object_id] AS referenced_object_id, + referenced_object.name AS referenced_object_name, + fk.is_disabled, + fk.is_not_trusted, + fk.is_not_for_replication, + parent.fk_columns, + referenced.fk_columns, + [update_referential_action_desc], + [delete_referential_action_desc] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects fk_object ON fk.object_id=fk_object.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects parent_object ON fk.parent_object_id=parent_object.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects referenced_object ON fk.referenced_object_id=referenced_object.object_id JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON fk.schema_id=s.schema_id - CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_parent.name AS fk_columns - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_parent ON fkc.parent_object_id=c_parent.[object_id] - AND fkc.parent_column_id=c_parent.column_id - WHERE fk.parent_object_id=fkc.parent_object_id - AND fk.[object_id]=fkc.constraint_object_id - ORDER BY fkc.constraint_column_id - FOR XML PATH('''') , - TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''')/*This is how we remove the first comma*/ ) parent ( fk_columns ) - CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_referenced.name AS fk_columns - FROM ' + QUOTENAME(@DatabaseName) + N'.sys. foreign_key_columns fkc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_referenced ON fkc.referenced_object_id=c_referenced.[object_id] - AND fkc.referenced_column_id=c_referenced.column_id - WHERE fk.referenced_object_id=fkc.referenced_object_id - and fk.[object_id]=fkc.constraint_object_id - ORDER BY fkc.constraint_column_id /*order by col name, we don''t have anything better*/ - FOR XML PATH('''') , - TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''') ) referenced ( fk_columns ) - ' + CASE WHEN @ObjectID IS NOT NULL THEN - 'WHERE fk.parent_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' OR fk.referenced_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' - ELSE N' ' END + ' - ORDER BY parent_object_name, foreign_key_name + CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_parent.name AS fk_columns + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_parent ON fkc.parent_object_id=c_parent.[object_id] + AND fkc.parent_column_id=c_parent.column_id + WHERE fk.parent_object_id=fkc.parent_object_id + AND fk.[object_id]=fkc.constraint_object_id + ORDER BY fkc.constraint_column_id + FOR XML PATH('''') , + TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''')/*This is how we remove the first comma*/ ) parent ( fk_columns ) + CROSS APPLY ( SELECT STUFF( (SELECT N'', '' + c_referenced.name AS fk_columns + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c_referenced ON fkc.referenced_object_id=c_referenced.[object_id] + AND fkc.referenced_column_id=c_referenced.column_id + WHERE fk.referenced_object_id=fkc.referenced_object_id + and fk.[object_id]=fkc.constraint_object_id + ORDER BY fkc.constraint_column_id /*order by col name, we don''t have anything better*/ + FOR XML PATH('''') , + TYPE).value(''.'', ''nvarchar(max)''), 1, 1, '''') ) referenced ( fk_columns ) + ' + CASE WHEN @ObjectID IS NOT NULL THEN + 'WHERE fk.parent_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' OR fk.referenced_object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' + ELSE N' ' END + ' + ORDER BY parent_object_name, foreign_key_name OPTION (RECOMPILE);'; IF @dsql IS NULL RAISERROR('@dsql is null',16,1); @@ -15841,17 +15951,17 @@ OPTION (RECOMPILE);'; BEGIN SET @dsql = N' SELECT - DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, foreign_key_schema = s.name, foreign_key_name = fk.name, foreign_key_table = - OBJECT_NAME(fk.parent_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + OBJECT_NAME(fk.parent_object_id, DB_ID(@i_DatabaseName)), fk.parent_object_id, foreign_key_referenced_table = - OBJECT_NAME(fk.referenced_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + OBJECT_NAME(fk.referenced_object_id, DB_ID(@i_DatabaseName)), fk.referenced_object_id FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s @@ -15925,7 +16035,7 @@ OPTION (RECOMPILE);'; days_since_last_stats_update, rows, rows_sampled, percent_sampled, histogram_steps, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SELECT DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, obj.object_id, obj.name AS table_name, @@ -16020,7 +16130,7 @@ OPTION (RECOMPILE);'; last_statistics_update, days_since_last_stats_update, rows, modification_counter, percent_modifications, modifications_before_auto_update, index_type_desc, table_create_date, table_modify_date, no_recompute, has_filter, filter_definition, persisted_sample_percent, is_incremental) - SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SELECT DB_ID(@i_DatabaseName) AS [database_id], @i_DatabaseName AS database_name, obj.object_id, obj.name AS table_name, @@ -16153,7 +16263,7 @@ OPTION (RECOMPILE);'; BEGIN RAISERROR (N'Gathering Temporal Table Info',0,1) WITH NOWAIT; SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS database_name, - DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + DB_ID(@i_DatabaseName) AS [database_id], s.name AS schema_name, t.name AS table_name, oa.hsn as history_schema_name, @@ -16189,7 +16299,7 @@ OPTION (RECOMPILE);'; 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_object_id ) - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; END; SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], @@ -19285,6 +19395,7 @@ BEGIN 'N/A' AS index_usage_summary, 'N/A' AS index_size_summary FROM #TemporalTables AS t + ORDER BY t.database_name, t.schema_name, t.table_name OPTION ( RECOMPILE ); RAISERROR(N'check_id 121: Optimized For Sequential Keys.', 0,1) WITH NOWAIT; @@ -20616,7 +20727,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF @VersionCheckMode = 1 BEGIN @@ -23221,56 +23332,37 @@ BEGIN lock_types AS ( SELECT - database_name = - dp.database_name, + dp.database_name, dow.object_name, lock = CASE WHEN CHARINDEX(N':', dp.wait_resource) > 0 - THEN SUBSTRING - ( - dp.wait_resource, - 1, - CHARINDEX(N':', dp.wait_resource) - 1 - ) + THEN LEFT(dp.wait_resource, CHARINDEX(N':', dp.wait_resource) - 1) ELSE dp.wait_resource END, - lock_count = - CONVERT - ( - nvarchar(20), - COUNT_BIG(DISTINCT dp.event_date) - ) + lock_count = CONVERT(nvarchar(20), COUNT_BIG(DISTINCT dp.event_date)) FROM #deadlock_process AS dp JOIN #deadlock_owner_waiter AS dow - ON (dp.id = dow.owner_id - OR dp.victim_id = dow.waiter_id) - AND dp.event_date = dow.event_date - WHERE 1 = 1 - AND (dp.database_name = @DatabaseName OR @DatabaseName IS NULL) - AND (dp.event_date >= @StartDate OR @StartDate IS NULL) - AND (dp.event_date < @EndDate OR @EndDate IS NULL) - AND (dp.client_app = @AppName OR @AppName IS NULL) - AND (dp.host_name = @HostName OR @HostName IS NULL) - AND (dp.login_name = @LoginName OR @LoginName IS NULL) - AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) - AND dow.object_name IS NOT NULL + ON (dp.id = dow.owner_id OR dp.victim_id = dow.waiter_id) + AND dp.event_date = dow.event_date + WHERE (dp.database_name = @DatabaseName OR @DatabaseName IS NULL) + AND (dp.event_date >= @StartDate OR @StartDate IS NULL) + AND (dp.event_date < @EndDate OR @EndDate IS NULL) + AND (dp.client_app = @AppName OR @AppName IS NULL) + AND (dp.host_name = @HostName OR @HostName IS NULL) + AND (dp.login_name = @LoginName OR @LoginName IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.object_name IS NOT NULL GROUP BY dp.database_name, + dow.object_name, CASE WHEN CHARINDEX(N':', dp.wait_resource) > 0 - THEN SUBSTRING - ( - dp.wait_resource, - 1, - CHARINDEX(N':', dp.wait_resource) - 1 - ) + THEN LEFT(dp.wait_resource, CHARINDEX(N':', dp.wait_resource) - 1) ELSE dp.wait_resource - END, - dow.object_name + END ) - INSERT - #deadlock_findings WITH(TABLOCKX) + INSERT #deadlock_findings WITH (TABLOCKX) ( check_id, database_name, @@ -23280,36 +23372,33 @@ BEGIN sort_order ) SELECT - check_id = 7, + check_id = 7, lt.database_name, lt.object_name, finding_group = N'Types of locks by object', - finding = + finding = N'This object has had ' + - STUFF - ( + STUFF( ( SELECT - N', ' + - lt2.lock_count + - N' ' + - lt2.lock + N', ' + lt2.lock_count + N' ' + lt2.lock FROM lock_types AS lt2 WHERE lt2.database_name = lt.database_name - AND lt2.object_name = lt.object_name - FOR XML - PATH(N''), - TYPE - ).value(N'.[1]', N'nvarchar(MAX)'), - 1, - 1, - N'' + AND lt2.object_name = lt.object_name + FOR XML PATH(''), TYPE + ).value('.', 'nvarchar(max)'), + 1, 2, N'' ) + N' locks.', sort_order = - ROW_NUMBER() - OVER (ORDER BY CONVERT(bigint, lt.lock_count) DESC) + ROW_NUMBER() OVER ( + ORDER BY + MAX(CONVERT(bigint, lt.lock_count)) DESC + ) FROM lock_types AS lt - OPTION(RECOMPILE); + GROUP BY + lt.database_name, + lt.object_name + OPTION (RECOMPILE); RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; @@ -25158,7 +25247,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN @@ -25215,6 +25304,7 @@ DECLARE @ProductVersion NVARCHAR(128) = CAST(SERVERPROPERTY('ProductVersion') A ,@ProductVersionMajor DECIMAL(10,2) ,@ProductVersionMinor DECIMAL(10,2) ,@Platform NVARCHAR(8) /* Azure or NonAzure are acceptable */ = (SELECT CASE WHEN @@VERSION LIKE '%Azure%' THEN N'Azure' ELSE N'NonAzure' END AS [Platform]) + ,@AzureSQLDB BIT = (SELECT CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END) ,@EnhanceFlag BIT = 0 ,@BlockingCheck NVARCHAR(MAX) ,@StringToSelect NVARCHAR(MAX) @@ -25252,10 +25342,10 @@ SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @Produ @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) SELECT - @OutputTableNameQueryStats_View = QUOTENAME(@OutputTableName + '_Deltas'), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName), + @OutputTableNameQueryStats_View = QUOTENAME(PARSENAME(@OutputTableName,1) + '_Deltas'), + @OutputDatabaseName = QUOTENAME(PARSENAME(@OutputDatabaseName,1)), + @OutputSchemaName = ISNULL(QUOTENAME(PARSENAME(@OutputSchemaName,1)),QUOTENAME(PARSENAME(@OutputTableName,2))), + @OutputTableName = QUOTENAME(PARSENAME(@OutputTableName,1)), @LineFeed = CHAR(13) + CHAR(10); IF @GetLiveQueryPlan IS NULL @@ -25266,6 +25356,20 @@ IF @GetLiveQueryPlan IS NULL SET @GetLiveQueryPlan = 0; END +IF @OutputTableName IS NOT NULL AND (@OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL) + BEGIN + IF @OutputDatabaseName IS NULL AND @AzureSQLDB = 1 + BEGIN + /* If we're in Azure SQL DB then use the current database */ + SET @OutputDatabaseName = QUOTENAME(DB_NAME()); + END; + IF @OutputSchemaName IS NULL AND @OutputDatabaseName = QUOTENAME(DB_NAME()) + BEGIN + /* If we're inserting records in the current database use the default schema */ + SET @OutputSchemaName = QUOTENAME(SCHEMA_NAME()); + END; + END; + IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL AND EXISTS ( SELECT * FROM sys.databases diff --git a/SqlServerVersions.sql b/SqlServerVersions.sql index 3bb177a6a..64afc0b49 100644 --- a/SqlServerVersions.sql +++ b/SqlServerVersions.sql @@ -42,11 +42,13 @@ INSERT INTO dbo.SqlServerVersions (MajorVersionNumber, MinorVersionNumber, Branch, [Url], ReleaseDate, MainstreamSupportEndDate, ExtendedSupportEndDate, MajorVersionName, MinorVersionName) VALUES /*2025*/ - (17, 925, 'RC1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-09-17', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC1'), - (17, 900, 'RC0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-08-20', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview RC0'), - (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.1'), - (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-12-31', '2025-12-31', 'SQL Server 2025', 'Preview CTP 2.0'), + (17, 1000, 'RTM', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-11-18', '2031-01-06', '2036-01-06', 'SQL Server 2025', 'RTM'), + (17, 925, 'RC1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-09-17', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview RC1'), + (17, 900, 'RC0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-08-20', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview RC0'), + (17, 800, 'CTP 2.1', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-06-16', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview CTP 2.1'), + (17, 700, 'CTP 2.0', 'https://info.microsoft.com/ww-landing-sql-server-2025.html', '2025-05-19', '2025-11-18', '2025-11-18', 'SQL Server 2025', 'Preview CTP 2.0'), /*2022*/ + (16, 4225, 'CU22', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate22', '2025-11-13', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 22'), (16, 4215, 'CU21', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate21', '2025-09-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 21'), (16, 4205, 'CU20', 'https://learn.microsoft.com/en-us/troubleshoot/sql/releases/sqlserver-2022/cumulativeupdate20', '2025-07-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 20'), (16, 4200, 'CU19 GDR', 'https://support.microsoft.com/en-us/help/5058721', '2025-07-08', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 19 GDR'), diff --git a/sp_Blitz.sql b/sp_Blitz.sql index ca6eb2637..7874acf10 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -39,7 +39,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzAnalysis.sql b/sp_BlitzAnalysis.sql index 09f11f49e..195785974 100644 --- a/sp_BlitzAnalysis.sql +++ b/sp_BlitzAnalysis.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzBackups.sql b/sp_BlitzBackups.sql index 403199016..f285b62d2 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.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index 0df4e8a4e..f84e1ab29 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -283,7 +283,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 00e1f3064..815bc52af 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.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index c748f8e07..424845c19 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -50,7 +50,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index 53e923556..f07667e21 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.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF @VersionCheckMode = 1 BEGIN diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index 64df0ca84..fd53e3f3a 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.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_DatabaseRestore.sql b/sp_DatabaseRestore.sql index 1a087860f..8e026895e 100755 --- a/sp_DatabaseRestore.sql +++ b/sp_DatabaseRestore.sql @@ -58,7 +58,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.26', @VersionDate = '20251002'; +SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index eca2b7a98..7fe913524 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -37,7 +37,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.26', @VersionDate = '20251002'; + SELECT @Version = '8.27', @VersionDate = '20251122'; IF(@VersionCheckMode = 1) BEGIN From 63f9910332c60e1b11acd1d288dcc3beb39d6c3b Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Sat, 22 Nov 2025 06:48:57 -0800 Subject: [PATCH 73/76] Optimized locking check Turn it into dynamic SQL so that it works with earlier versions. --- Install-All-Scripts.sql | 13 ++++++++----- sp_Blitz.sql | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index 6f6f0b67b..b8483698b 100644 --- a/Install-All-Scripts.sql +++ b/Install-All-Scripts.sql @@ -4943,6 +4943,7 @@ IF EXISTS (SELECT * FROM sys.all_columns WHERE name = 'is_optimized_locking_on' BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 272) WITH NOWAIT; + SET @StringToExecute = N' INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4955,13 +4956,15 @@ IF EXISTS (SELECT * FROM sys.all_columns WHERE name = 'is_optimized_locking_on' SELECT 272 AS [CheckID] , 100 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Optimized Locking Not Fully Set Up' AS [Finding] , + ''Performance'' AS [FindingsGroup] , + ''Optimized Locking Not Fully Set Up'' AS [Finding] , name, - 'https://www.brentozar.com/go/optimizedlocking' AS [URL] , - 'RCSI should be enabled on this database to get the full benefits of optimized locking.' AS [Details] + ''https://www.brentozar.com/go/optimizedlocking'' AS [URL] , + ''RCSI should be enabled on this database to get the full benefits of optimized locking.'' AS [Details] FROM sys.databases - WHERE is_optimized_locking_on = 1 AND is_read_committed_snapshot_on = 0; + WHERE is_optimized_locking_on = 1 AND is_read_committed_snapshot_on = 0;' + + EXEC(@StringToExecute); END; /* Check if target recovery interval <> 60 */ diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 7874acf10..42b7a0a9b 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -4943,6 +4943,7 @@ IF EXISTS (SELECT * FROM sys.all_columns WHERE name = 'is_optimized_locking_on' BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 272) WITH NOWAIT; + SET @StringToExecute = N' INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4955,13 +4956,15 @@ IF EXISTS (SELECT * FROM sys.all_columns WHERE name = 'is_optimized_locking_on' SELECT 272 AS [CheckID] , 100 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Optimized Locking Not Fully Set Up' AS [Finding] , + ''Performance'' AS [FindingsGroup] , + ''Optimized Locking Not Fully Set Up'' AS [Finding] , name, - 'https://www.brentozar.com/go/optimizedlocking' AS [URL] , - 'RCSI should be enabled on this database to get the full benefits of optimized locking.' AS [Details] + ''https://www.brentozar.com/go/optimizedlocking'' AS [URL] , + ''RCSI should be enabled on this database to get the full benefits of optimized locking.'' AS [Details] FROM sys.databases - WHERE is_optimized_locking_on = 1 AND is_read_committed_snapshot_on = 0; + WHERE is_optimized_locking_on = 1 AND is_read_committed_snapshot_on = 0;' + + EXEC(@StringToExecute); END; /* Check if target recovery interval <> 60 */ From 050eaa83f5bf0f7d0504692ed5272190928f852a Mon Sep 17 00:00:00 2001 From: Vlad Drumea <48413726+VladDBA@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:25:53 +0200 Subject: [PATCH 74/76] quick fix for #3753 --- sp_BlitzIndex.sql | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 424845c19..f1cac4a17 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -141,10 +141,25 @@ SET @DatabaseName = COALESCE(@DatabaseName, PARSENAME(@ObjectName, 3)) /* 3 = Da SET @SchemaName = COALESCE(@SchemaName, PARSENAME(@ObjectName, 2)) /* 2 = Schema name */ SET @TableName = COALESCE(@TableName, PARSENAME(@ObjectName, 1)) /* 1 = Table name */ -/* Handle already quoted input if it wasn't fully qualified*/ -SET @DatabaseName = PARSENAME(@DatabaseName,1); -SET @SchemaName = ISNULL(PARSENAME(@SchemaName,1),PARSENAME(@TableName,2)); -SET @TableName = PARSENAME(@TableName,1); +/* Handle already quoted input if it wasn't fully qualified - only if @ObjectName is null*/ +IF (@ObjectName IS NULL) + BEGIN + SELECT @DatabaseName = CASE WHEN @DatabaseName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@DatabaseName,1) ELSE @DatabaseName + END, + @SchemaName = ISNULL( + CASE /*only apply parsename if the schema is actually quoted*/ + WHEN @SchemaName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@SchemaName,1) ELSE @SchemaName + END, + CASE /*if we already have @TableName in the form of [some.schema].[some.table]*/ + WHEN @TableName LIKE N'\[%\].\[%\]' ESCAPE N'\' THEN PARSENAME(@TableName,2) + /*I'm making an assumption here that people who use . in their naming conventions would have one in each object name*/ + WHEN LEN(@TableName)- LEN(REPLACE(@TableName,'.','')) = 1 THEN PARSENAME(@TableName,2) ELSE NULL + END), + @TableName = CASE + WHEN @TableName LIKE N'\[%\].\[%\]' ESCAPE N'\' OR @TableName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@TableName,1) + WHEN LEN(@TableName)- LEN(REPLACE(@TableName,'.','')) = 1 THEN PARSENAME(@TableName,1) ELSE @TableName + END; +END; /* If we're on Azure SQL DB let's cut people some slack */ IF (@TableName IS NOT NULL AND @AzureSQLDB = 1 AND @DatabaseName IS NULL) From 048bc34b23bf0c8e790e5212341a37bf97f8aa9c Mon Sep 17 00:00:00 2001 From: Brent Ozar Date: Mon, 24 Nov 2025 19:14:01 -0800 Subject: [PATCH 75/76] 2025-11-24 release Bumping version numbers and dates, building install scripts. --- Install-All-Scripts.sql | 43 +++++++++++++++++++++++++++-------------- Install-Azure.sql | 35 +++++++++++++++++++++++---------- sp_Blitz.sql | 2 +- sp_BlitzAnalysis.sql | 2 +- sp_BlitzBackups.sql | 2 +- sp_BlitzCache.sql | 2 +- sp_BlitzFirst.sql | 2 +- sp_BlitzIndex.sql | 2 +- sp_BlitzLock.sql | 2 +- sp_BlitzWho.sql | 2 +- sp_DatabaseRestore.sql | 2 +- sp_ineachdb.sql | 2 +- 12 files changed, 64 insertions(+), 34 deletions(-) diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index b8483698b..ae1570bc0 100644 --- a/Install-All-Scripts.sql +++ b/Install-All-Scripts.sql @@ -39,7 +39,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -10777,7 +10777,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN @@ -11655,7 +11655,7 @@ AS SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN @@ -13439,7 +13439,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -20826,7 +20826,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -20917,10 +20917,25 @@ SET @DatabaseName = COALESCE(@DatabaseName, PARSENAME(@ObjectName, 3)) /* 3 = Da SET @SchemaName = COALESCE(@SchemaName, PARSENAME(@ObjectName, 2)) /* 2 = Schema name */ SET @TableName = COALESCE(@TableName, PARSENAME(@ObjectName, 1)) /* 1 = Table name */ -/* Handle already quoted input if it wasn't fully qualified*/ -SET @DatabaseName = PARSENAME(@DatabaseName,1); -SET @SchemaName = ISNULL(PARSENAME(@SchemaName,1),PARSENAME(@TableName,2)); -SET @TableName = PARSENAME(@TableName,1); +/* Handle already quoted input if it wasn't fully qualified - only if @ObjectName is null*/ +IF (@ObjectName IS NULL) + BEGIN + SELECT @DatabaseName = CASE WHEN @DatabaseName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@DatabaseName,1) ELSE @DatabaseName + END, + @SchemaName = ISNULL( + CASE /*only apply parsename if the schema is actually quoted*/ + WHEN @SchemaName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@SchemaName,1) ELSE @SchemaName + END, + CASE /*if we already have @TableName in the form of [some.schema].[some.table]*/ + WHEN @TableName LIKE N'\[%\].\[%\]' ESCAPE N'\' THEN PARSENAME(@TableName,2) + /*I'm making an assumption here that people who use . in their naming conventions would have one in each object name*/ + WHEN LEN(@TableName)- LEN(REPLACE(@TableName,'.','')) = 1 THEN PARSENAME(@TableName,2) ELSE NULL + END), + @TableName = CASE + WHEN @TableName LIKE N'\[%\].\[%\]' ESCAPE N'\' OR @TableName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@TableName,1) + WHEN LEN(@TableName)- LEN(REPLACE(@TableName,'.','')) = 1 THEN PARSENAME(@TableName,1) ELSE @TableName + END; +END; /* If we're on Azure SQL DB let's cut people some slack */ IF (@TableName IS NOT NULL AND @AzureSQLDB = 1 AND @DatabaseName IS NULL) @@ -27829,7 +27844,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF @VersionCheckMode = 1 BEGIN @@ -32349,7 +32364,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN @@ -33785,7 +33800,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN @@ -35455,7 +35470,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN @@ -36339,7 +36354,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN diff --git a/Install-Azure.sql b/Install-Azure.sql index 879137642..01728ca32 100644 --- a/Install-Azure.sql +++ b/Install-Azure.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN @@ -1174,7 +1174,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -8558,7 +8558,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN @@ -13724,7 +13724,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) @@ -13815,10 +13815,25 @@ SET @DatabaseName = COALESCE(@DatabaseName, PARSENAME(@ObjectName, 3)) /* 3 = Da SET @SchemaName = COALESCE(@SchemaName, PARSENAME(@ObjectName, 2)) /* 2 = Schema name */ SET @TableName = COALESCE(@TableName, PARSENAME(@ObjectName, 1)) /* 1 = Table name */ -/* Handle already quoted input if it wasn't fully qualified*/ -SET @DatabaseName = PARSENAME(@DatabaseName,1); -SET @SchemaName = ISNULL(PARSENAME(@SchemaName,1),PARSENAME(@TableName,2)); -SET @TableName = PARSENAME(@TableName,1); +/* Handle already quoted input if it wasn't fully qualified - only if @ObjectName is null*/ +IF (@ObjectName IS NULL) + BEGIN + SELECT @DatabaseName = CASE WHEN @DatabaseName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@DatabaseName,1) ELSE @DatabaseName + END, + @SchemaName = ISNULL( + CASE /*only apply parsename if the schema is actually quoted*/ + WHEN @SchemaName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@SchemaName,1) ELSE @SchemaName + END, + CASE /*if we already have @TableName in the form of [some.schema].[some.table]*/ + WHEN @TableName LIKE N'\[%\].\[%\]' ESCAPE N'\' THEN PARSENAME(@TableName,2) + /*I'm making an assumption here that people who use . in their naming conventions would have one in each object name*/ + WHEN LEN(@TableName)- LEN(REPLACE(@TableName,'.','')) = 1 THEN PARSENAME(@TableName,2) ELSE NULL + END), + @TableName = CASE + WHEN @TableName LIKE N'\[%\].\[%\]' ESCAPE N'\' OR @TableName LIKE N'\[%\]' ESCAPE N'\' THEN PARSENAME(@TableName,1) + WHEN LEN(@TableName)- LEN(REPLACE(@TableName,'.','')) = 1 THEN PARSENAME(@TableName,1) ELSE @TableName + END; +END; /* If we're on Azure SQL DB let's cut people some slack */ IF (@TableName IS NOT NULL AND @AzureSQLDB = 1 AND @DatabaseName IS NULL) @@ -20727,7 +20742,7 @@ BEGIN SET XACT_ABORT OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF @VersionCheckMode = 1 BEGIN @@ -25247,7 +25262,7 @@ BEGIN SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_Blitz.sql b/sp_Blitz.sql index 42b7a0a9b..17527339a 100644 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -39,7 +39,7 @@ AS SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzAnalysis.sql b/sp_BlitzAnalysis.sql index 195785974..8e8418746 100644 --- a/sp_BlitzAnalysis.sql +++ b/sp_BlitzAnalysis.sql @@ -37,7 +37,7 @@ AS SET NOCOUNT ON; SET STATISTICS XML OFF; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzBackups.sql b/sp_BlitzBackups.sql index f285b62d2..8fb8d11a1 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.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index f84e1ab29..9e14ff769 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -283,7 +283,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 815bc52af..11c2f87df 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.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index f1cac4a17..4f83de5cf 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -50,7 +50,7 @@ SET NOCOUNT ON; SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index f07667e21..e3f33d894 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.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF @VersionCheckMode = 1 BEGIN diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index fd53e3f3a..74267c0d1 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.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_DatabaseRestore.sql b/sp_DatabaseRestore.sql index 8e026895e..5876f6ec6 100755 --- a/sp_DatabaseRestore.sql +++ b/sp_DatabaseRestore.sql @@ -58,7 +58,7 @@ SET STATISTICS XML OFF; /*Versioning details*/ -SELECT @Version = '8.27', @VersionDate = '20251122'; +SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql index 7fe913524..62bd155ed 100644 --- a/sp_ineachdb.sql +++ b/sp_ineachdb.sql @@ -37,7 +37,7 @@ BEGIN SET NOCOUNT ON; SET STATISTICS XML OFF; - SELECT @Version = '8.27', @VersionDate = '20251122'; + SELECT @Version = '8.28', @VersionDate = '20251124'; IF(@VersionCheckMode = 1) BEGIN From d0ef2fa28204c3817bc6f25c4e27e2052fcd6f11 Mon Sep 17 00:00:00 2001 From: anziob Date: Mon, 19 Jan 2026 08:25:24 +0200 Subject: [PATCH 76/76] Update Install-All-Scripts.sql saas-42104 Make confluence changes --- Install-All-Scripts.sql | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index ae1570bc0..c70a01210 100644 --- a/Install-All-Scripts.sql +++ b/Install-All-Scripts.sql @@ -36346,7 +36346,8 @@ ALTER PROCEDURE [dbo].[sp_BlitzFirst] @Debug BIT = 0, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, - @VersionCheckMode BIT = 0 + @VersionCheckMode BIT = 0, + @CheckStatisticsUpdatedRecently bit = 0 WITH EXECUTE AS CALLER, RECOMPILE AS BEGIN @@ -36830,7 +36831,7 @@ BEGIN /* We reuse this one by default rather than recreate it every time. */ CREATE TABLE ##WaitCategories ( - WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, + WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED with(ignore_dup_key=on), WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0 ); @@ -38817,6 +38818,8 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, END; /* IF @Seconds < 30 */ + if @CheckStatisticsUpdatedRecently =1 + begin /* Query Problems - Statistics Updated Recently - CheckID 44 */ IF (@Debug = 1) BEGIN @@ -38925,6 +38928,7 @@ If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, FOR XML PATH('')); END + end /* Server Performance - Azure Operation Ongoing - CheckID 53 */ IF (@Debug = 1)