From 2fdb798a52926216ef2025943d6dfab308a1d7e3 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 20:21:21 -0500 Subject: [PATCH 1/4] Update sp_QuickieStore.sql Exclude WAITFOR queries (often service broker related) from results. --- sp_QuickieStore/sp_QuickieStore.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/sp_QuickieStore/sp_QuickieStore.sql b/sp_QuickieStore/sp_QuickieStore.sql index ab7ad09b..48e5756f 100644 --- a/sp_QuickieStore/sp_QuickieStore.sql +++ b/sp_QuickieStore/sp_QuickieStore.sql @@ -5264,6 +5264,7 @@ WHERE NOT EXISTS AND qsqt.query_sql_text NOT LIKE N''%SELECT StatMan%'' AND qsqt.query_sql_text NOT LIKE N''DBCC%'' AND qsqt.query_sql_text NOT LIKE N''(@[_]msparam%'' + AND qsqt.query_sql_text NOT LIKE N''WAITFOR%'' ) OPTION(RECOMPILE);' + @nc10; From 6e717dd568e4c86780ce1e81141826feb87f4308 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:24:38 -0500 Subject: [PATCH 2/4] Add sp_QueryStoreCleanup: Query Store noise removal tool New stored procedure to identify and remove duplicate/noisy queries from Query Store. Targets system DMV queries, maintenance operations (index rebuilds, stats updates, DBCC, etc.), with flexible dedup strategies and safety features (forced plan protection, age filtering, report-only mode). Tested across 15 databases on SQL Server 2022 and 2025 with 772 successful removals. Excluded from Install-All due to destructive nature (removes queries). Co-Authored-By: Claude Opus 4.6 --- Install-All/Merge-All.ps1 | 2 +- Install-All/README.md | 2 + README.md | 30 + sp_QueryStoreCleanup/README.md | 102 ++ sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql | 953 ++++++++++++++++++ 5 files changed, 1088 insertions(+), 1 deletion(-) create mode 100644 sp_QueryStoreCleanup/README.md create mode 100644 sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql diff --git a/Install-All/Merge-All.ps1 b/Install-All/Merge-All.ps1 index 6750cda6..82fe3478 100644 --- a/Install-All/Merge-All.ps1 +++ b/Install-All/Merge-All.ps1 @@ -1,5 +1,5 @@ Get-ChildItem -Path ".." -Filter "sp_*" | -Where-Object { $_.FullName -notlike "*sp_WhoIsActive*" } | +Where-Object { $_.FullName -notlike "*sp_WhoIsActive*" -and $_.FullName -notlike "*sp_QueryStoreCleanup*" } | ForEach-Object { Get-ChildItem $_.FullName | Where-Object { $_.Name -like "sp_*" -and $_.Name -notlike "sp_Human Events Agent*" } } | ForEach-Object { Get-Content $_.FullName -Encoding UTF8 } | diff --git a/Install-All/README.md b/Install-All/README.md index 11e5ebf2..9a46cd8e 100644 --- a/Install-All/README.md +++ b/Install-All/README.md @@ -58,5 +58,7 @@ The script will: The WhoIsActive Logging procedures are not included in this file, as they have a different installation process and depend on Adam Machanic's sp_WhoIsActive. +sp_QueryStoreCleanup is also not included, as it is a destructive procedure that removes queries from Query Store. Install it separately from the [sp_QueryStoreCleanup](../sp_QueryStoreCleanup) directory. + Copyright 2026 Darling Data, LLC Released under MIT license \ No newline at end of file diff --git a/README.md b/README.md index 654167c5..e4f6bf5b 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - [sp_HealthParser](#health-parser): Pull all the performance-related data from the system health Extended Event - [sp_LogHunter](#log-hunter): Get all of the worst stuff out of your error log - [sp_IndexCleanup](#index-cleanup): Identify unused and duplicate indexes + - [sp_QueryStoreCleanup](#query-store-cleanup): Remove duplicate and noisy queries from Query Store ## Who are these scripts for? You need to troubleshoot performance problems with SQL Server, and you need to do it now. @@ -493,4 +494,33 @@ Current valid parameter details: [*Back to top*](#navigatory) +## Query Store Cleanup + +Query Store is great, but it collects a lot of noise. System DMV queries, index maintenance, statistics updates, and other background operations all pile up as duplicate entries, wasting space and making it harder to find the queries you actually care about. + +This procedure identifies and removes duplicate and noisy queries from Query Store in any database on your server. It uses text pattern matching and hash-based deduplication to find the junk, and removes it using `sp_query_store_remove_query`. + +By default, it targets system queries, maintenance operations, and removes all copies of duplicated query and plan hashes. You can customize what to target, how to deduplicate, and whether to just report or actually remove. + +Queries with forced plans are always protected from removal. + +**Note:** This procedure is not included in the Install-All script due to its destructive nature. Install it separately. + +Current valid parameter details: + +| parameter_name | data_type | description | valid_inputs | defaults | +|---|---|---|---|---| +| @database_name | sysname | the database to clean query store in | a database name with query store enabled | NULL; current database if NULL | +| @cleanup_targets | varchar(100) | what to target for cleanup | all, system, maintenance (or maint), custom, none | all | +| @custom_query_filter | nvarchar(1024) | custom LIKE pattern for query text filtering | a valid LIKE pattern | NULL | +| @dedupe_by | varchar(50) | deduplication strategy | all, query_hash, plan_hash, none | all | +| @min_age_days | integer | only remove queries not executed in this many days | a positive integer | NULL; no age filter | +| @report_only | bit | report what would be removed without removing | 0 or 1 | 0 | +| @help | bit | how you got here | 0 or 1 | 0 | +| @debug | bit | prints dynamic sql and diagnostics | 0 or 1 | 0 | +| @version | varchar | OUTPUT; for support | none; OUTPUT | none; OUTPUT | +| @version_date | datetime | OUTPUT; for support | none; OUTPUT | none; OUTPUT | + +[*Back to top*](#navigatory) + [licence badge]:https://img.shields.io/badge/license-MIT-blue.svg diff --git a/sp_QueryStoreCleanup/README.md b/sp_QueryStoreCleanup/README.md new file mode 100644 index 00000000..795a6612 --- /dev/null +++ b/sp_QueryStoreCleanup/README.md @@ -0,0 +1,102 @@ + + +# sp_QueryStoreCleanup + +Query Store is great, but it collects a lot of noise. System DMV queries, index maintenance, statistics updates, and other background operations all pile up as duplicate entries, wasting space and making it harder to find the queries you actually care about. + +This procedure identifies and removes duplicate and noisy queries from Query Store in any database on your server. It uses text pattern matching and hash-based deduplication to find the junk, and removes it using `sp_query_store_remove_query`. + +By default, it targets system queries (`FROM sys.%`), maintenance operations (index rebuilds, statistics updates, DBCC commands, etc.), and removes all copies of duplicated query and plan hashes. You can customize what to target, how to deduplicate, and whether to just report or actually remove. + +Queries with forced plans are always protected from removal. + +## Parameters + +| parameter_name | data_type | description | valid_inputs | defaults | +|---|---|---|---|---| +| @database_name | sysname | the database to clean query store in | a database name with query store enabled | NULL; current database if NULL | +| @cleanup_targets | varchar(100) | what to target for cleanup | all, system, maintenance (or maint), custom, none | all | +| @custom_query_filter | nvarchar(1024) | custom LIKE pattern for query text filtering; also applied when @cleanup_targets = all | a valid LIKE pattern | NULL | +| @dedupe_by | varchar(50) | deduplication strategy | all, query_hash, plan_hash, none | all | +| @min_age_days | integer | only remove queries whose last execution is older than this many days | a positive integer | NULL; no age filter | +| @report_only | bit | report what would be removed without removing | 0 or 1 | 0 | +| @debug | bit | prints dynamic sql and diagnostics | 0 or 1 | 0 | +| @help | bit | how you got here | 0 or 1 | 0 | +| @version | varchar(30) | OUTPUT; for support | none; OUTPUT | none; OUTPUT | +| @version_date | datetime | OUTPUT; for support | none; OUTPUT | none; OUTPUT | + +### Cleanup Targets + +The `@cleanup_targets` parameter controls which queries are identified by text pattern matching: + +| Value | What It Matches | +|---|---| +| `system` | Queries containing `FROM sys.%` | +| `maintenance` (or `maint`) | Index operations (`ALTER INDEX`, `CREATE INDEX`, `ALTER TABLE`), statistics operations (`UPDATE STATISTICS`, `CREATE STATISTICS`, `SELECT StatMan`), DBCC commands, and parameterized maintenance queries (`@_msparam`) | +| `custom` | Uses your `@custom_query_filter` LIKE pattern | +| `all` | system + maintenance combined; also applies `@custom_query_filter` if provided | +| `none` | No text filtering; deduplication is purely hash-based across all queries | + +### Deduplication Strategy + +The `@dedupe_by` parameter controls how duplicates are identified after text filtering: + +| Value | Behavior | +|---|---| +| `query_hash` | Find queries with duplicate `query_hash` values (same query compiled multiple times) | +| `plan_hash` | Find queries with duplicate `query_plan_hash` values (different queries producing identical plans) | +| `all` | Both query_hash and plan_hash | +| `none` | Skip hash deduplication entirely; send all text-matched queries directly to removal | + +**Note:** Hash deduplication removes all copies of duplicated hashes, not all-but-one. This is intentional, as the queries targeted are noise that will be recaptured by Query Store if they execute again. + +## Examples + +```sql +-- Default: remove all system + maintenance duplicates from the current database +EXECUTE dbo.sp_QueryStoreCleanup; + +-- Target a specific database +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'StackOverflow2013'; + +-- Report what would be removed without removing anything +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'YourDatabase', + @report_only = 1; + +-- Only clean up system DMV queries +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'YourDatabase', + @cleanup_targets = 'system'; + +-- Only clean up maintenance operations (index rebuilds, stats updates, DBCC, etc.) +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'YourDatabase', + @cleanup_targets = 'maint'; + +-- Remove all text-matched queries without hash deduplication +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'YourDatabase', + @cleanup_targets = 'system', + @dedupe_by = 'none'; + +-- Use a custom text filter to find specific query patterns +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'YourDatabase', + @cleanup_targets = 'custom', + @custom_query_filter = N'%some_noisy_query%'; + +-- Only remove queries that haven't executed in the last 30 days +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'YourDatabase', + @min_age_days = 30; + +-- Debug mode to see the generated dynamic SQL +EXECUTE dbo.sp_QueryStoreCleanup + @database_name = N'YourDatabase', + @debug = 1; +``` + +Copyright 2026 Darling Data, LLC +Released under MIT license diff --git a/sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql b/sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql new file mode 100644 index 00000000..8b9d215b --- /dev/null +++ b/sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql @@ -0,0 +1,953 @@ +SET ANSI_NULLS ON; +SET ANSI_PADDING ON; +SET ANSI_WARNINGS ON; +SET ARITHABORT ON; +SET CONCAT_NULL_YIELDS_NULL ON; +SET QUOTED_IDENTIFIER ON; +SET NUMERIC_ROUNDABORT OFF; +SET IMPLICIT_TRANSACTIONS OFF; +SET STATISTICS TIME, IO OFF; +GO + +/* +Copyright 2026 Darling Data, LLC +https://www.erikdarling.com/ + +For usage and licensing details, run: +EXECUTE dbo.sp_QueryStoreCleanup + @help = 1; + +For working through errors: +EXECUTE dbo.sp_QueryStoreCleanup + @debug = 1; + +For support, head over to GitHub: +https://code.erikdarling.com +*/ + +IF OBJECT_ID(N'dbo.sp_QueryStoreCleanup', N'P') IS NULL +BEGIN + EXECUTE (N'CREATE PROCEDURE dbo.sp_QueryStoreCleanup AS RETURN 138;'); +END; +GO + +ALTER PROCEDURE + dbo.sp_QueryStoreCleanup +( + @database_name sysname = NULL, /*database to clean; NULL = current database*/ + @cleanup_targets varchar(100) = 'all', /*what to target: all, system, maintenance, custom, none*/ + @custom_query_filter nvarchar(1024) = NULL, /*custom LIKE pattern when using custom target*/ + @dedupe_by varchar(50) = 'all', /*deduplication strategy: all, query_hash, plan_hash, none*/ + @min_age_days integer = NULL, /*only remove queries not executed in this many days*/ + @report_only bit = 0, /*1 = report what would be removed without removing*/ + @debug bit = 0, /*prints dynamic sql and diagnostics*/ + @help bit = 0, /*prints help information*/ + @version varchar(30) = NULL OUTPUT, /*OUTPUT; for support*/ + @version_date datetime = NULL OUTPUT /*OUTPUT; for support*/ +) +WITH RECOMPILE +AS +BEGIN + SET STATISTICS XML OFF; + SET NOCOUNT ON; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SELECT + @version = '1.0', + @version_date = '20260212'; + + /* + Help section + */ + IF @help = 1 + BEGIN + /* + Introduction + */ + SELECT + introduction = + 'hi, i''m sp_QueryStoreCleanup!' UNION ALL + SELECT 'i clean up duplicate and noisy queries from query store' UNION ALL + SELECT 'you can find me at https://code.erikdarling.com' UNION ALL + SELECT '' UNION ALL + SELECT 'for support, head over to github:' UNION ALL + SELECT 'https://code.erikdarling.com'; + + /* + Parameter descriptions + */ + SELECT + parameter_name = + ap.name, + data_type = + t.name, + description = + CASE + ap.name + WHEN N'@database_name' + THEN 'the database to clean query store in' + WHEN N'@cleanup_targets' + THEN 'what to target: all, system, maintenance (or maint), custom, none' + WHEN N'@custom_query_filter' + THEN 'custom LIKE pattern for query text filtering; also applied when @cleanup_targets = all' + WHEN N'@dedupe_by' + THEN 'deduplication strategy: all, query_hash, plan_hash, none. note: hash dedup removes ALL copies of duplicated hashes, not all-but-one' + WHEN N'@min_age_days' + THEN 'only remove queries whose last execution is older than this many days; NULL = no age filter' + WHEN N'@report_only' + THEN 'report what would be removed without removing' + WHEN N'@debug' + THEN 'prints dynamic sql and diagnostics' + WHEN N'@help' + THEN 'prints this help information' + WHEN N'@version' + THEN 'OUTPUT; for support' + WHEN N'@version_date' + THEN 'OUTPUT; for support' + END, + valid_inputs = + CASE + ap.name + WHEN N'@database_name' + THEN 'a database name with query store enabled' + WHEN N'@cleanup_targets' + THEN 'all, system, maintenance (or maint), custom, none, or comma-separated combination' + WHEN N'@custom_query_filter' + THEN 'any valid LIKE pattern, e.g. N''%some_text%''' + WHEN N'@dedupe_by' + THEN 'all, query_hash, plan_hash, none' + WHEN N'@min_age_days' + THEN 'any positive integer, e.g. 7, 30, 90' + WHEN N'@report_only' + THEN '0 or 1' + WHEN N'@debug' + THEN '0 or 1' + WHEN N'@help' + THEN '0 or 1' + WHEN N'@version' + THEN 'none; OUTPUT' + WHEN N'@version_date' + THEN 'none; OUTPUT' + END, + defaults = + CASE + ap.name + WHEN N'@database_name' + THEN 'NULL; current database name if NULL' + WHEN N'@cleanup_targets' + THEN 'all' + WHEN N'@custom_query_filter' + THEN 'NULL' + WHEN N'@dedupe_by' + THEN 'all' + WHEN N'@min_age_days' + THEN 'NULL; no age filter' + WHEN N'@report_only' + THEN '0' + WHEN N'@debug' + THEN '0' + WHEN N'@help' + THEN '0' + WHEN N'@version' + THEN 'none; OUTPUT' + WHEN N'@version_date' + THEN 'none; OUTPUT' + END + FROM sys.all_parameters AS ap + JOIN sys.all_objects AS o + ON ap.object_id = o.object_id + JOIN sys.types AS t + ON ap.system_type_id = t.system_type_id + AND ap.user_type_id = t.user_type_id + WHERE o.name = N'sp_QueryStoreCleanup' + OPTION(MAXDOP 1, RECOMPILE); + + /* + Example usage + */ + SELECT + example = + '/* default: clean all known noise from current database */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup;' UNION ALL + SELECT '' UNION ALL + SELECT '/* target a specific database */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'';' UNION ALL + SELECT '' UNION ALL + SELECT '/* report only mode */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @report_only = 1;' UNION ALL + SELECT '' UNION ALL + SELECT '/* clean only system DMV queries */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @cleanup_targets = ''system'';' UNION ALL + SELECT '' UNION ALL + SELECT '/* clean only maintenance noise (index rebuilds, stats updates, DBCC, etc.) */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @cleanup_targets = ''maintenance'';' UNION ALL + SELECT '' UNION ALL + SELECT '/* deduplicate all queries by query hash only, no text filtering */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @cleanup_targets = ''none'', @dedupe_by = ''query_hash'';' UNION ALL + SELECT '' UNION ALL + SELECT '/* custom text filter */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @cleanup_targets = ''custom'', @custom_query_filter = N''%my_noisy_query%'';' UNION ALL + SELECT '' UNION ALL + SELECT '/* combine targets */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @cleanup_targets = ''system,maint'';' UNION ALL + SELECT '' UNION ALL + SELECT '/* text-only removal, no deduplication required */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @cleanup_targets = ''system'', @dedupe_by = ''none'';' UNION ALL + SELECT '' UNION ALL + SELECT '/* only remove queries not executed in 30+ days */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @min_age_days = 30;' UNION ALL + SELECT '' UNION ALL + SELECT '/* emergency flush: remove all noise older than 7 days */' UNION ALL + SELECT 'EXECUTE dbo.sp_QueryStoreCleanup @database_name = N''YourDatabase'', @dedupe_by = ''none'', @min_age_days = 7;'; + + /* + MIT License + */ + RAISERROR(' +MIT License + +Copyright 2026 Darling Data, LLC +https://www.erikdarling.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +', 0, 1) WITH NOWAIT; + + RETURN; + END; /*End help section*/ + + /* + Variable declarations + */ + DECLARE + @sql nvarchar(max) = N'', + @database_name_quoted sysname = N'', + @actual_state integer = NULL, + @include_system bit = 0, + @include_maintenance bit = 0, + @include_custom bit = 0, + @no_text_filter bit = 0, + @dedupe_query_hash bit = 0, + @dedupe_plan_hash bit = 0, + @no_dedupe bit = 0, + @text_filter nvarchar(max) = N'', + @exists_clause nvarchar(max) = N'', + @removal_filters nvarchar(max) = N'', + @age_cutoff datetime = NULL, + @text_target_count bigint = 0, + @query_hash_dupe_count bigint = 0, + @plan_hash_dupe_count bigint = 0, + @removal_count bigint = 0, + @remove_sql nvarchar(max) = N'', + @error_message nvarchar(4000) = N'', + @c CURSOR, + @query_id bigint, + @current bigint = 0, + @total bigint = 0, + @removed bigint = 0, + @failed bigint = 0; + + /* + Default database to current + */ + IF @database_name IS NULL + BEGIN + SELECT + @database_name = DB_NAME(); + END; + + /* + Validate database exists + */ + IF DB_ID(@database_name) IS NULL + BEGIN + RAISERROR('Database %s does not exist.', 16, 1, @database_name) WITH NOWAIT; + RETURN; + END; + + SELECT + @database_name_quoted = QUOTENAME(@database_name); + + /* + Check Query Store is enabled + */ + SELECT + @sql = N' +SELECT + @actual_state = dqso.actual_state +FROM ' + @database_name_quoted + N'.sys.database_query_store_options AS dqso +OPTION(RECOMPILE);'; + + IF @debug = 1 + BEGIN + RAISERROR('/* Query Store check */', 0, 1) WITH NOWAIT; + PRINT @sql; + END; + + EXECUTE sys.sp_executesql + @sql, + N'@actual_state integer OUTPUT', + @actual_state OUTPUT; + + IF @actual_state IS NULL + OR @actual_state = 0 + BEGIN + RAISERROR('Query Store is not enabled for database %s.', 16, 1, @database_name) WITH NOWAIT; + RETURN; + END; + + /* + Parse @cleanup_targets + */ + SELECT + @cleanup_targets = LOWER(LTRIM(RTRIM(@cleanup_targets))); + + IF @cleanup_targets = 'all' + BEGIN + SELECT + @include_system = 1, + @include_maintenance = 1; + + IF @custom_query_filter IS NOT NULL + BEGIN + SELECT + @include_custom = 1; + END; + END; + ELSE IF @cleanup_targets = 'none' + BEGIN + SELECT + @no_text_filter = 1; + END; + ELSE + BEGIN + IF CHARINDEX('system', @cleanup_targets) > 0 + BEGIN + SELECT + @include_system = 1; + END; + + /* + CHARINDEX('maint', ...) matches maint, maintenance, etc. + */ + IF CHARINDEX('maint', @cleanup_targets) > 0 + BEGIN + SELECT + @include_maintenance = 1; + END; + + IF CHARINDEX('custom', @cleanup_targets) > 0 + BEGIN + SELECT + @include_custom = 1; + END; + END; + + /* + Validate custom filter + */ + IF @include_custom = 1 + AND @custom_query_filter IS NULL + BEGIN + RAISERROR('@custom_query_filter is required when @cleanup_targets includes ''custom''.', 16, 1) WITH NOWAIT; + RETURN; + END; + + /* + Validate at least one target is set + */ + IF @no_text_filter = 0 + AND @include_system = 0 + AND @include_maintenance = 0 + AND @include_custom = 0 + BEGIN + RAISERROR('No valid cleanup targets specified. Use all, system, maintenance, custom, or none.', 16, 1) WITH NOWAIT; + RETURN; + END; + + /* + Parse @dedupe_by + */ + SELECT + @dedupe_by = LOWER(LTRIM(RTRIM(@dedupe_by))); + + IF @dedupe_by = 'all' + BEGIN + SELECT + @dedupe_query_hash = 1, + @dedupe_plan_hash = 1; + END; + ELSE IF @dedupe_by = 'query_hash' + BEGIN + SELECT + @dedupe_query_hash = 1; + END; + ELSE IF @dedupe_by = 'plan_hash' + BEGIN + SELECT + @dedupe_plan_hash = 1; + END; + ELSE IF @dedupe_by = 'none' + BEGIN + SELECT + @no_dedupe = 1; + END; + ELSE + BEGIN + RAISERROR('@dedupe_by must be all, query_hash, plan_hash, or none. You passed: %s', 16, 1, @dedupe_by) WITH NOWAIT; + RETURN; + END; + + /* + Validate that @cleanup_targets and @dedupe_by aren't both none + That would remove every query in query store + */ + IF @no_text_filter = 1 + AND @no_dedupe = 1 + BEGIN + RAISERROR('@cleanup_targets = ''none'' and @dedupe_by = ''none'' would remove every query in query store. That''s probably not what you want.', 16, 1) WITH NOWAIT; + RETURN; + END; + + /* + Create temp tables + */ + CREATE TABLE + #text_targets + ( + query_text_id bigint NOT NULL PRIMARY KEY + ); + + CREATE TABLE + #query_hash_dupes + ( + query_hash binary(8) NOT NULL, + total_plans bigint NOT NULL + ); + + CREATE TABLE + #plan_hash_dupes + ( + query_plan_hash binary(8) NOT NULL, + total_plans bigint NOT NULL + ); + + CREATE TABLE + #removals + ( + query_id bigint NOT NULL PRIMARY KEY + ); + + /* + Step 1: Find text targets + */ + IF @no_text_filter = 0 + BEGIN + /* + Build text filter WHERE clause + Each condition is prefixed with newline + "OR " (7 chars) + so we can STUFF off the leading OR and prepend WHERE + */ + IF @include_system = 1 + BEGIN + SELECT + @text_filter += N' +OR qsqt.query_sql_text LIKE N''%FROM sys.%'''; + END; + + /* + Maintenance patterns from sp_QuickieStore: + index rebuilds, stats updates, DBCC, StatMan, maintenance plan params + */ + IF @include_maintenance = 1 + BEGIN + SELECT + @text_filter += N' +OR qsqt.query_sql_text LIKE N''ALTER INDEX%'' +OR qsqt.query_sql_text LIKE N''ALTER TABLE%'' +OR qsqt.query_sql_text LIKE N''CREATE%INDEX%'' +OR qsqt.query_sql_text LIKE N''CREATE STATISTICS%'' +OR qsqt.query_sql_text LIKE N''UPDATE STATISTICS%'' +OR qsqt.query_sql_text LIKE N''%SELECT StatMan%'' +OR qsqt.query_sql_text LIKE N''DBCC%'' +OR qsqt.query_sql_text LIKE N''(@[_]msparam%'''; + END; + + IF @include_custom = 1 + BEGIN + SELECT + @text_filter += N' +OR qsqt.query_sql_text LIKE @custom_query_filter'; + END; + + /* + Remove leading newline + "OR " (7 chars) and prepend WHERE + */ + SELECT + @text_filter = N'WHERE ' + STUFF(@text_filter, 1, 7, N''); + + SELECT + @sql = N' +INSERT + #text_targets +WITH + (TABLOCK) +( + query_text_id +) +SELECT + qsqt.query_text_id +FROM ' + @database_name_quoted + N'.sys.query_store_query_text AS qsqt +' + @text_filter + N' +OPTION(RECOMPILE);'; + + IF @debug = 1 + BEGIN + RAISERROR('/* Step 1: Find text targets */', 0, 1) WITH NOWAIT; + PRINT @sql; + END; + + EXECUTE sys.sp_executesql + @sql, + N'@custom_query_filter nvarchar(1024)', + @custom_query_filter; + + SELECT + @text_target_count = ROWCOUNT_BIG(); + + RAISERROR('Found %I64d query texts matching cleanup targets', 0, 1, @text_target_count) WITH NOWAIT; + + IF @debug = 1 + BEGIN + SELECT + tt.* + FROM #text_targets AS tt; + END; + + IF @text_target_count = 0 + BEGIN + RAISERROR('No matching query texts found. Exiting.', 0, 1) WITH NOWAIT; + RETURN; + END; + END; + + /* + Step 2: Find query_hash duplicates + */ + IF @dedupe_query_hash = 1 + BEGIN + SELECT + @sql = N' +INSERT + #query_hash_dupes +WITH + (TABLOCK) +( + query_hash, + total_plans +) +SELECT + qsq.query_hash, + total_plans = COUNT_BIG(*) +FROM ' + @database_name_quoted + N'.sys.query_store_runtime_stats AS qsrs +JOIN ' + @database_name_quoted + N'.sys.query_store_plan AS qsp + ON qsrs.plan_id = qsp.plan_id +JOIN ' + @database_name_quoted + N'.sys.query_store_query AS qsq + ON qsp.query_id = qsq.query_id +WHERE qsp.is_forced_plan = 0' + + CASE + WHEN @no_text_filter = 0 + THEN N' +AND EXISTS + ( + SELECT + 1/0 + FROM #text_targets AS tt + WHERE tt.query_text_id = qsq.query_text_id + )' + ELSE N'' + END + N' +GROUP BY + qsq.query_hash +HAVING + COUNT_BIG(*) > 1 +OPTION(RECOMPILE);'; + + IF @debug = 1 + BEGIN + RAISERROR('/* Step 2: Find query_hash duplicates */', 0, 1) WITH NOWAIT; + PRINT @sql; + END; + + EXECUTE sys.sp_executesql + @sql; + + SELECT + @query_hash_dupe_count = ROWCOUNT_BIG(); + + RAISERROR('Found %I64d duplicate query hashes', 0, 1, @query_hash_dupe_count) WITH NOWAIT; + + IF @debug = 1 + BEGIN + SELECT + qd.* + FROM #query_hash_dupes AS qd; + END; + END; + + /* + Step 3: Find plan_hash duplicates + */ + IF @dedupe_plan_hash = 1 + BEGIN + SELECT + @sql = N' +INSERT + #plan_hash_dupes +WITH + (TABLOCK) +( + query_plan_hash, + total_plans +) +SELECT + qsp.query_plan_hash, + total_plans = COUNT_BIG(*) +FROM ' + @database_name_quoted + N'.sys.query_store_runtime_stats AS qsrs +JOIN ' + @database_name_quoted + N'.sys.query_store_plan AS qsp + ON qsrs.plan_id = qsp.plan_id +JOIN ' + @database_name_quoted + N'.sys.query_store_query AS qsq + ON qsp.query_id = qsq.query_id +WHERE qsp.is_forced_plan = 0' + + CASE + WHEN @no_text_filter = 0 + THEN N' +AND EXISTS + ( + SELECT + 1/0 + FROM #text_targets AS tt + WHERE tt.query_text_id = qsq.query_text_id + )' + ELSE N'' + END + N' +GROUP BY + qsp.query_plan_hash +HAVING + COUNT_BIG(*) > 1 +OPTION(RECOMPILE);'; + + IF @debug = 1 + BEGIN + RAISERROR('/* Step 3: Find plan_hash duplicates */', 0, 1) WITH NOWAIT; + PRINT @sql; + END; + + EXECUTE sys.sp_executesql + @sql; + + SELECT + @plan_hash_dupe_count = ROWCOUNT_BIG(); + + RAISERROR('Found %I64d duplicate plan hashes', 0, 1, @plan_hash_dupe_count) WITH NOWAIT; + + IF @debug = 1 + BEGIN + SELECT + qd.* + FROM #plan_hash_dupes AS qd; + END; + END; + + /* + Check if any duplicates were found (skip when @no_dedupe = 1) + */ + IF @no_dedupe = 0 + AND @query_hash_dupe_count = 0 + AND @plan_hash_dupe_count = 0 + BEGIN + RAISERROR('No duplicates found. Exiting.', 0, 1) WITH NOWAIT; + RETURN; + END; + + /* + Build removal filters applied to both Step 4 paths: + forced plan protection + optional age filter + */ + SELECT + @removal_filters = N' +AND NOT EXISTS + ( + SELECT + 1/0 + FROM ' + @database_name_quoted + N'.sys.query_store_plan AS qsp_forced + WHERE qsp_forced.query_id = qsq.query_id + AND qsp_forced.is_forced_plan = 1 + )'; + + IF @min_age_days IS NOT NULL + BEGIN + SELECT + @age_cutoff = DATEADD(DAY, -@min_age_days, GETUTCDATE()); + + SELECT + @removal_filters += N' +AND NOT EXISTS + ( + SELECT + 1/0 + FROM ' + @database_name_quoted + N'.sys.query_store_plan AS qsp_age + WHERE qsp_age.query_id = qsq.query_id + AND qsp_age.last_execution_time > @age_cutoff + )'; + END; + + /* + Step 4: Build removal list + */ + IF @no_dedupe = 1 + BEGIN + /* + No deduplication: all text-matched queries go directly to removal + */ + SELECT + @sql = N' +INSERT + #removals +WITH + (TABLOCK) +( + query_id +) +SELECT DISTINCT + qsq.query_id +FROM ' + @database_name_quoted + N'.sys.query_store_query AS qsq +WHERE EXISTS + ( + SELECT + 1/0 + FROM #text_targets AS tt + WHERE tt.query_text_id = qsq.query_text_id + )' + @removal_filters + N' +OPTION(RECOMPILE);'; + END; + ELSE + BEGIN + /* + Build the EXISTS clause based on which strategies found results + */ + IF @dedupe_query_hash = 1 + AND @query_hash_dupe_count > 0 + BEGIN + SELECT + @exists_clause += N' + SELECT + 1/0 + FROM #query_hash_dupes AS qd + WHERE qd.query_hash = qsq.query_hash'; + END; + + IF @dedupe_plan_hash = 1 + AND @plan_hash_dupe_count > 0 + BEGIN + IF LEN(@exists_clause) > 0 + BEGIN + SELECT + @exists_clause += N' + + UNION ALL +'; + END; + + SELECT + @exists_clause += N' + SELECT + 1/0 + FROM #plan_hash_dupes AS qd + WHERE qd.query_plan_hash = qsp.query_plan_hash'; + END; + + SELECT + @sql = N' +INSERT + #removals +WITH + (TABLOCK) +( + query_id +) +SELECT DISTINCT + qsp.query_id +FROM ' + @database_name_quoted + N'.sys.query_store_plan AS qsp +JOIN ' + @database_name_quoted + N'.sys.query_store_query AS qsq + ON qsp.query_id = qsq.query_id +WHERE EXISTS + (' + @exists_clause + N' + )' + @removal_filters + N' +OPTION(RECOMPILE);'; + END; + + IF @debug = 1 + BEGIN + RAISERROR('/* Step 4: Build removal list */', 0, 1) WITH NOWAIT; + PRINT @sql; + END; + + EXECUTE sys.sp_executesql + @sql, + N'@age_cutoff datetime', + @age_cutoff; + + SELECT + @removal_count = ROWCOUNT_BIG(); + + RAISERROR('Found %I64d queries to remove', 0, 1, @removal_count) WITH NOWAIT; + + IF @debug = 1 + BEGIN + SELECT + r.* + FROM #removals AS r; + END; + + IF @removal_count = 0 + BEGIN + RAISERROR('No queries to remove. Exiting.', 0, 1) WITH NOWAIT; + RETURN; + END; + + /* + Step 5: Report or remove + */ + IF @report_only = 1 + BEGIN + /* + Report mode: show what would be removed + */ + SELECT + @sql = N' +SELECT + r.query_id, + qsq.query_hash, + qsp.query_plan_hash, + query_sql_text = + SUBSTRING + ( + qsqt.query_sql_text, + 1, + 200 + ) +FROM #removals AS r +JOIN ' + @database_name_quoted + N'.sys.query_store_query AS qsq + ON r.query_id = qsq.query_id +JOIN ' + @database_name_quoted + N'.sys.query_store_query_text AS qsqt + ON qsq.query_text_id = qsqt.query_text_id +CROSS APPLY +( + SELECT TOP (1) + qsp.query_plan_hash + FROM ' + @database_name_quoted + N'.sys.query_store_plan AS qsp + WHERE qsp.query_id = qsq.query_id + ORDER BY + qsp.last_execution_time DESC +) AS qsp +ORDER BY + r.query_id +OPTION(RECOMPILE);'; + + IF @debug = 1 + BEGIN + RAISERROR('/* Step 5: Report */', 0, 1) WITH NOWAIT; + PRINT @sql; + END; + + EXECUTE sys.sp_executesql + @sql; + + RAISERROR('%I64d queries would be removed (report only mode)', 0, 1, @removal_count) WITH NOWAIT; + RETURN; + END; + + /* + Removal mode: cursor through and remove each query + */ + SELECT + @total = @removal_count; + + SELECT + @remove_sql = + N'EXECUTE ' + + @database_name_quoted + + N'.sys.sp_query_store_remove_query @query_id = @query_id;'; + + IF @debug = 1 + BEGIN + RAISERROR('/* Remove SQL */', 0, 1) WITH NOWAIT; + PRINT @remove_sql; + END; + + SET @c = + CURSOR + LOCAL + DYNAMIC + READ_ONLY + FORWARD_ONLY + FOR + SELECT + r.query_id + FROM #removals AS r; + + OPEN @c; + + FETCH NEXT + FROM @c + INTO @query_id; + + WHILE @@FETCH_STATUS = 0 + BEGIN + SELECT + @current += 1; + + BEGIN TRY + EXECUTE sys.sp_executesql + @remove_sql, + N'@query_id bigint', + @query_id; + + SELECT + @removed += 1; + + RAISERROR('Query %I64d of %I64d: query_id %I64d removed', 0, 1, @current, @total, @query_id) WITH NOWAIT; + END TRY + BEGIN CATCH + SELECT + @failed += 1, + @error_message = ERROR_MESSAGE(); + + RAISERROR('Query %I64d of %I64d: query_id %I64d not removed (%s)', 0, 1, @current, @total, @query_id, @error_message) WITH NOWAIT; + END CATCH; + + FETCH NEXT + FROM @c + INTO @query_id; + END; + + RAISERROR('Finished: %I64d of %I64d removed (%I64d failed)', 0, 1, @removed, @total, @failed) WITH NOWAIT; + +END; +GO From 01644dcea9b924f29be10c4612ebfbfe854dafd0 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:22:30 -0500 Subject: [PATCH 3/4] sp_PressureDetector: filter out zero-rate perfmon counters Rows with cntr_value = 0 are now excluded at insert into the table variable, and rows where total_per_second rounds to 0 via integer division are filtered from the result set. Reduces noise when reviewing perfmon stats on long-uptime servers. Co-Authored-By: Claude Opus 4.6 --- sp_PressureDetector/sp_PressureDetector.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sp_PressureDetector/sp_PressureDetector.sql b/sp_PressureDetector/sp_PressureDetector.sql index 3fd35152..cfcf5c01 100644 --- a/sp_PressureDetector/sp_PressureDetector.sql +++ b/sp_PressureDetector/sp_PressureDetector.sql @@ -2056,7 +2056,8 @@ OPTION(MAXDOP 1, RECOMPILE);', p.cntr_value, p.cntr_type FROM p - WHERE p.object_name LIKE @prefix + WHERE p.cntr_value > 0 + AND p.object_name LIKE @prefix AND p.instance_name NOT IN ( N'internal', N'master', N'model', N'msdb', N'model_msdb', @@ -2125,7 +2126,7 @@ OPTION(MAXDOP 1, RECOMPILE);', p.total, p.total_per_second FROM p - WHERE p.cntr_value > 0 + WHERE p.total_per_second <> N'0' ORDER BY p.object_name, p.counter_name, From f6f0778be4311de10a8a6da7426b54b40388d666 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:59:28 -0500 Subject: [PATCH 4/4] Fix 27 logical bugs across all 10 stored procedures Bugs found via automated review, validated one-by-one against SQL2022: - sp_HealthParser: Fix XPath inputbuf extraction - sp_HumanEvents: Fix memory filter, CATCH cleanup, QUOTENAME schema/table - sp_HumanEventsBlockViewer: Fix table mode XML, DATEADD overflow guard - sp_LogHunter: Add archive > 0 guard - sp_IndexCleanup: Fix LEN trailing space (use DATALENGTH), fix t.max_length vs c.max_length for detecting (max) columns - sp_PerfCheck: Fix NULL @processors check, TOKENANDPERMUSERSTORE priority gap, wrap DMV queries in VIEW SERVER STATE guard - sp_QueryReproBuilder: Fix version gates from @new to @sql_2017 for columns available since SQL 2017 - sp_QueryStoreCleanup: Fix COUNT_BIG(*) false positives with COUNT(DISTINCT) (39% false positive rate eliminated) - sp_QuickieStore: Fix cursor FETCH NEXT/CONTINUE for @get_all_databases, remove compile memory 8x inflation, fix log bytes 95x deflation, remove duplicate TRUNCATEs, add @@DATEFIRST ELSE warning for @workdays - sp_PressureDetector: Add missing SET LOCK_TIMEOUT -1, fix DATEDIFF divide-by-zero, fix sampled avg_ms_per_wait delta calculation, fix @prefix NULL on Azure All 10 procedures installed and executed successfully on SQL2022. Co-Authored-By: Claude Opus 4.6 --- sp_HealthParser/sp_HealthParser.sql | 2 +- sp_HumanEvents/sp_HumanEvents.sql | 13 ++--- sp_HumanEvents/sp_HumanEventsBlockViewer.sql | 8 ++- sp_IndexCleanup/sp_IndexCleanup.sql | 28 +++++----- sp_LogHunter/sp_LogHunter.sql | 5 +- sp_PerfCheck/sp_PerfCheck.sql | 8 ++- sp_PressureDetector/sp_PressureDetector.sql | 52 ++++++++++++------ sp_QueryReproBuilder/sp_QueryReproBuilder.sql | 6 +-- sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql | 8 +-- sp_QuickieStore/sp_QuickieStore.sql | 53 +++++++++++-------- 10 files changed, 109 insertions(+), 74 deletions(-) diff --git a/sp_HealthParser/sp_HealthParser.sql b/sp_HealthParser/sp_HealthParser.sql index 92b0a10f..007ef9e9 100644 --- a/sp_HealthParser/sp_HealthParser.sql +++ b/sp_HealthParser/sp_HealthParser.sql @@ -5677,7 +5677,7 @@ AND ca.utc_timestamp < @end_date'; isolation_level = e.x.value('@isolationlevel', 'sysname'), clientoption1 = e.x.value('@clientoption1', 'bigint'), clientoption2 = e.x.value('@clientoption2', 'bigint'), - query_text_pre = e.x.value('(//process/inputbuf/text())[1]', 'nvarchar(max)'), + query_text_pre = e.x.value('(inputbuf/text())[1]', 'nvarchar(max)'), process_xml = e.x.query(N'.'), deadlock_resources = d.xml_deadlock_report.query('//deadlock/resource-list') FROM #deadlocks AS d diff --git a/sp_HumanEvents/sp_HumanEvents.sql b/sp_HumanEvents/sp_HumanEvents.sql index 47885d86..8a94ac8f 100644 --- a/sp_HumanEvents/sp_HumanEvents.sql +++ b/sp_HumanEvents/sp_HumanEvents.sql @@ -1544,7 +1544,8 @@ SET @session_filter_query_plans += ISNULL(@database_name_filter, N'') + ISNULL(@session_id_filter, N'') + ISNULL(@username_filter, N'') + - ISNULL(@object_name_filter, N'') + ISNULL(@object_name_filter, N'') + + ISNULL(@requested_memory_mb_filter, N'') ); /* Recompile can have almost everything except... duration */ @@ -3754,7 +3755,7 @@ BEGIN N'.' + QUOTENAME(hew.output_schema) + N'.' + - hew.output_table + QUOTENAME(hew.output_table) FROM #human_events_worker AS hew WHERE hew.id = @min_id AND hew.is_table_created = 0; @@ -4105,7 +4106,7 @@ END; N'.' + QUOTENAME(hew.output_schema) + N'.' + - hew.output_table, + QUOTENAME(hew.output_table), @date_filter = DATEADD ( @@ -4848,7 +4849,7 @@ BEGIN SELECT @i_cleanup_tables += N''DROP TABLE '' + - SCHEMA_NAME(s.schema_id) + + QUOTENAME(SCHEMA_NAME(s.schema_id)) + N''.'' + QUOTENAME(s.name) + ''; '' + @@ -4879,7 +4880,7 @@ BEGIN SELECT @i_cleanup_views += N''DROP VIEW '' + - SCHEMA_NAME(v.schema_id) + + QUOTENAME(SCHEMA_NAME(v.schema_id)) + N''.'' + QUOTENAME(v.name) + ''; '' + @@ -4912,7 +4913,7 @@ BEGIN CATCH /*Only try to drop a session if we're not outputting*/ IF (@output_database_name = N'' - AND @output_schema_name = N'') + AND @output_schema_name IN (N'', N'dbo')) BEGIN IF @debug = 1 BEGIN diff --git a/sp_HumanEvents/sp_HumanEventsBlockViewer.sql b/sp_HumanEvents/sp_HumanEventsBlockViewer.sql index b4f3f899..74c2fe63 100644 --- a/sp_HumanEvents/sp_HumanEventsBlockViewer.sql +++ b/sp_HumanEvents/sp_HumanEventsBlockViewer.sql @@ -1835,9 +1835,7 @@ BEGIN /* Build dynamic SQL to extract the XML */ SET @extract_sql = N' SELECT TOP (' + CONVERT(nvarchar(20), CASE WHEN @max_blocking_events > 0 THEN @max_blocking_events ELSE 2147483647 END) + N') - human_events_xml = ' + - QUOTENAME(@target_column) + - N' + human_events_xml = e.x.query(''.'') FROM ' + QUOTENAME(@target_database) + N'.' + @@ -3622,7 +3620,7 @@ BEGIN bigint, b.wait_time_ms ) - ) / 1000 + ) / 1000 % 86400 ), '19000101' ), @@ -3711,7 +3709,7 @@ BEGIN bigint, b.wait_time_ms ) - ) / 1000 + ) / 1000 % 86400 ), '19000101' ), diff --git a/sp_IndexCleanup/sp_IndexCleanup.sql b/sp_IndexCleanup/sp_IndexCleanup.sql index 1074c36e..89996019 100644 --- a/sp_IndexCleanup/sp_IndexCleanup.sql +++ b/sp_IndexCleanup/sp_IndexCleanup.sql @@ -1027,6 +1027,16 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ON #filtered_index_columns_analysis (database_id, schema_id, object_id, index_id); + CREATE TABLE + #merged_includes + ( + scope_hash varbinary(32) NOT NULL, + index_name sysname NOT NULL, + key_columns nvarchar(max) NOT NULL, + merged_includes nvarchar(max) NULL, + PRIMARY KEY (scope_hash, index_name) + ); + /* Parse @include_databases comma-separated list */ IF @get_all_databases = 1 AND @include_databases IS NOT NULL @@ -1135,10 +1145,10 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. OPTION(RECOMPILE); /* If we found any conflicts, raise an error */ - IF LEN(@conflict_list) > 0 + IF DATALENGTH(@conflict_list) > 0 BEGIN /* Remove trailing comma and space */ - SET @conflict_list = LEFT(@conflict_list, LEN(@conflict_list) - 2); + SET @conflict_list = LEFT(@conflict_list, DATALENGTH(@conflict_list) / 2 - 2); SET @error_msg = N'The following databases appear in both @include_databases and @exclude_databases, which creates ambiguity: ' + @@ -1380,6 +1390,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #check_constraints_analysis; TRUNCATE TABLE #filtered_index_columns_analysis; + TRUNCATE TABLE + #merged_includes; /*Validate searched objects per-database*/ IF @schema_name IS NOT NULL @@ -2178,7 +2190,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. WHERE c.system_type_id = t.system_type_id AND c.user_type_id = t.user_type_id AND t.name IN (N''varchar'', N''nvarchar'') - AND t.max_length = -1 + AND c.max_length = -1 ) THEN 1 ELSE 0 @@ -3374,16 +3386,6 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. OPTION(RECOMPILE); END; - CREATE TABLE - #merged_includes - ( - scope_hash bigint NOT NULL, - index_name sysname NOT NULL, - key_columns nvarchar(max) NOT NULL, - merged_includes nvarchar(max) NULL, - PRIMARY KEY (scope_hash, index_name) - ); - /* Gather all supersets that need include merging */ INSERT INTO #merged_includes diff --git a/sp_LogHunter/sp_LogHunter.sql b/sp_LogHunter/sp_LogHunter.sql index db4803ba..f988ee76 100644 --- a/sp_LogHunter/sp_LogHunter.sql +++ b/sp_LogHunter/sp_LogHunter.sql @@ -367,8 +367,9 @@ BEGIN DELETE e WITH(TABLOCKX) FROM #enum AS e - WHERE e.log_date < CONVERT(date, @start_date) - OR e.log_date > CONVERT(date, @end_date) + WHERE (e.log_date < CONVERT(date, @start_date) + OR e.log_date > CONVERT(date, @end_date)) + AND e.archive > 0 OPTION(RECOMPILE); END; diff --git a/sp_PerfCheck/sp_PerfCheck.sql b/sp_PerfCheck/sp_PerfCheck.sql index 307d5152..6a301ae0 100644 --- a/sp_PerfCheck/sp_PerfCheck.sql +++ b/sp_PerfCheck/sp_PerfCheck.sql @@ -990,6 +990,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. END; END; + IF @has_view_server_state = 1 + BEGIN /* Check for high number of deadlocks */ INSERT INTO #results @@ -1101,7 +1103,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. CASE WHEN CONVERT(decimal(10, 2), (domc.pages_kb / 1024.0 / 1024.0)) > 5 THEN 20 /* Very high priority >5GB */ - WHEN CONVERT(decimal(10, 2), (domc.pages_kb / 1024.0 / 1024.0)) BETWEEN 3 AND 5 + WHEN CONVERT(decimal(10, 2), (domc.pages_kb / 1024.0 / 1024.0)) BETWEEN 2 AND 5 THEN 30 /* High priority >2GB */ WHEN CONVERT(decimal(10, 2), (domc.pages_kb / 1024.0 / 1024.0)) BETWEEN 1 AND 2 THEN 40 /* Medium-high priority >1GB */ @@ -1130,6 +1132,7 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. osi.physical_memory_kb / 1024.0 / 1024.0 ) FROM sys.dm_os_sys_info AS osi; + END; /* Check if Lock Pages in Memory is enabled (on-prem and managed instances only) */ IF @azure_sql_db = 0 @@ -3306,7 +3309,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. END; /* Check for single data file */ - IF @tempdb_data_file_count = 1 + IF @tempdb_data_file_count = 1 + AND @processors IS NOT NULL BEGIN INSERT INTO #results diff --git a/sp_PressureDetector/sp_PressureDetector.sql b/sp_PressureDetector/sp_PressureDetector.sql index cfcf5c01..9b1621d7 100644 --- a/sp_PressureDetector/sp_PressureDetector.sql +++ b/sp_PressureDetector/sp_PressureDetector.sql @@ -441,17 +441,21 @@ OPTION(MAXDOP 1, RECOMPILE);', ELSE 0 END, @prefix sysname = + ISNULL ( - SELECT TOP (1) - SUBSTRING - ( - dopc.object_name, - 1, - CHARINDEX(N':', dopc.object_name) - ) - FROM sys.dm_os_performance_counters AS dopc - ) + - N'%', + ( + SELECT TOP (1) + SUBSTRING + ( + dopc.object_name, + 1, + CHARINDEX(N':', dopc.object_name) + ) + FROM sys.dm_os_performance_counters AS dopc + ) + + N'%', + N'%' + ), @memory_grant_cap xml, @cache_xml xml, @cache_sql nvarchar(max) = N'', @@ -1452,7 +1456,16 @@ OPTION(MAXDOP 1, RECOMPILE);', CONVERT ( decimal(38,1), - (w2.avg_ms_per_wait + w.avg_ms_per_wait) / 2 + ISNULL + ( + (w2.hours_wait_time - w.hours_wait_time) / + NULLIF + ( + 1. * (w2.waiting_tasks_count_n - w.waiting_tasks_count_n), + 0. + ), + 0. + ) ), percent_signal_waits = CONVERT @@ -2106,11 +2119,15 @@ OPTION(MAXDOP 1, RECOMPILE);', dopc.cntr_value / ISNULL ( - DATEDIFF + NULLIF ( - SECOND, - dopc.sample_time, - SYSDATETIME() + DATEDIFF + ( + SECOND, + dopc.sample_time, + SYSDATETIME() + ), + 0 ), 1 ), @@ -3932,7 +3949,10 @@ OPTION(MAXDOP 1, RECOMPILE);', THEN N' der.cpu_time DESC, der.parallel_worker_count DESC - OPTION(MAXDOP 1, RECOMPILE);' + OPTION(MAXDOP 1, RECOMPILE); + + SET LOCK_TIMEOUT -1; + ' ELSE N' der.cpu_time DESC OPTION(MAXDOP 1, RECOMPILE); diff --git a/sp_QueryReproBuilder/sp_QueryReproBuilder.sql b/sp_QueryReproBuilder/sp_QueryReproBuilder.sql index 00fe6e3a..c9ae76f7 100644 --- a/sp_QueryReproBuilder/sp_QueryReproBuilder.sql +++ b/sp_QueryReproBuilder/sp_QueryReproBuilder.sql @@ -1770,7 +1770,7 @@ SELECT MAX(qsrs_with_lasts.max_rowcount),'; /*Add SQL 2017+ columns*/ -IF @new = 1 +IF @sql_2017 = 1 BEGIN SELECT @sql += N' AVG((qsrs_with_lasts.avg_num_physical_io_reads * 8.) / 1024.), @@ -1911,7 +1911,7 @@ FROM ),'; /*Add SQL 2017+ windowing columns*/ -IF @new = 1 +IF @sql_2017 = 1 BEGIN SELECT @sql += N' partitioned_last_num_physical_io_reads = @@ -2210,7 +2210,7 @@ BEGIN qsp.is_optimized_plan_forcing_disabled, qsp.plan_type_desc'; END; -ELSE IF @new = 1 +ELSE IF @sql_2017 = 1 BEGIN SELECT @sql += N' qsp.plan_forcing_type_desc, diff --git a/sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql b/sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql index 8b9d215b..befc8c81 100644 --- a/sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql +++ b/sp_QueryStoreCleanup/sp_QueryStoreCleanup.sql @@ -563,7 +563,7 @@ WITH ) SELECT qsq.query_hash, - total_plans = COUNT_BIG(*) + total_plans = COUNT_BIG(DISTINCT qsq.query_id) FROM ' + @database_name_quoted + N'.sys.query_store_runtime_stats AS qsrs JOIN ' + @database_name_quoted + N'.sys.query_store_plan AS qsp ON qsrs.plan_id = qsp.plan_id @@ -585,7 +585,7 @@ AND EXISTS GROUP BY qsq.query_hash HAVING - COUNT_BIG(*) > 1 + COUNT_BIG(DISTINCT qsq.query_id) > 1 OPTION(RECOMPILE);'; IF @debug = 1 @@ -627,7 +627,7 @@ WITH ) SELECT qsp.query_plan_hash, - total_plans = COUNT_BIG(*) + total_plans = COUNT_BIG(DISTINCT qsp.plan_id) FROM ' + @database_name_quoted + N'.sys.query_store_runtime_stats AS qsrs JOIN ' + @database_name_quoted + N'.sys.query_store_plan AS qsp ON qsrs.plan_id = qsp.plan_id @@ -649,7 +649,7 @@ AND EXISTS GROUP BY qsp.query_plan_hash HAVING - COUNT_BIG(*) > 1 + COUNT_BIG(DISTINCT qsp.plan_id) > 1 OPTION(RECOMPILE);'; IF @debug = 1 diff --git a/sp_QuickieStore/sp_QuickieStore.sql b/sp_QuickieStore/sp_QuickieStore.sql index 48e5756f..7269cfc9 100644 --- a/sp_QuickieStore/sp_QuickieStore.sql +++ b/sp_QuickieStore/sp_QuickieStore.sql @@ -2476,18 +2476,6 @@ TRUNCATE TABLE TRUNCATE TABLE #ignore_sql_handles; -TRUNCATE TABLE - #only_queries_with_hints; - -TRUNCATE TABLE - #only_queries_with_feedback; - -TRUNCATE TABLE - #only_queries_with_variants; - -TRUNCATE TABLE - #forced_plans_failures; - TRUNCATE TABLE #query_hash_totals; @@ -2968,6 +2956,15 @@ BEGIN RETURN; END; END; + + IF @get_all_databases = 1 + BEGIN + FETCH NEXT + FROM @database_cursor + INTO @database_name; + + CONTINUE; + END; END; /* @@ -3078,6 +3075,15 @@ BEGIN RETURN; END; END; + + IF @get_all_databases = 1 + BEGIN + FETCH NEXT + FROM @database_cursor + INTO @database_name; + + CONTINUE; + END; END; /* @@ -3899,12 +3905,15 @@ BEGIN SELECT @where_clause += N'AND DATEPART(WEEKDAY, qsrs.last_execution_time) BETWEEN 1 AND 5' + @nc10; END;/*df 1*/ - - IF @df = 7 + ELSE IF @df = 7 BEGIN SELECT @where_clause += N'AND DATEPART(WEEKDAY, qsrs.last_execution_time) BETWEEN 2 AND 6' + @nc10; END;/*df 7*/ + ELSE + BEGIN + RAISERROR('Warning: @workdays filter does not support @@DATEFIRST = %i, weekday filter skipped', 10, 1, @df) WITH NOWAIT; + END; IF @work_start_utc IS NOT NULL AND @work_end_utc IS NOT NULL @@ -6549,10 +6558,10 @@ BEGIN MAX(((qsrs_with_lasts.partitioned_last_num_physical_io_reads * 8.) / 1024.)), MIN(((qsrs_with_lasts.min_num_physical_io_reads * 8.) / 1024.)), MAX(((qsrs_with_lasts.max_num_physical_io_reads * 8.) / 1024.)), - AVG((qsrs_with_lasts.avg_log_bytes_used / 100000000.)), - MAX((qsrs_with_lasts.partitioned_last_log_bytes_used / 100000000.)), - MIN((qsrs_with_lasts.min_log_bytes_used / 100000000.)), - MAX((qsrs_with_lasts.max_log_bytes_used / 100000000.)), + AVG((qsrs_with_lasts.avg_log_bytes_used / 1048576.)), + MAX((qsrs_with_lasts.partitioned_last_log_bytes_used / 1048576.)), + MIN((qsrs_with_lasts.min_log_bytes_used / 1048576.)), + MAX((qsrs_with_lasts.max_log_bytes_used / 1048576.)), AVG(((qsrs_with_lasts.avg_tempdb_space_used * 8) / 1024.)), MAX(((qsrs_with_lasts.partitioned_last_tempdb_space_used * 8) / 1024.)), MIN(((qsrs_with_lasts.min_tempdb_space_used * 8) / 1024.)), @@ -7185,9 +7194,9 @@ SELECT (qsq.last_optimize_duration / 1000.), (qsq.avg_optimize_cpu_time / 1000.), (qsq.last_optimize_cpu_time / 1000.), - ((qsq.avg_compile_memory_kb * 8) / 1024.), - ((qsq.last_compile_memory_kb * 8) / 1024.), - ((qsq.max_compile_memory_kb * 8) / 1024.), + (qsq.avg_compile_memory_kb / 1024.), + (qsq.last_compile_memory_kb / 1024.), + (qsq.max_compile_memory_kb / 1024.), qsq.is_clouddb_internal_query FROM #query_store_plan AS qsp CROSS APPLY @@ -7302,7 +7311,7 @@ BEGIN WHEN 1 THEN N', SUM(qsrs.count_executions * (qsrs.avg_num_physical_io_reads * 8)) / 1024., - SUM(qsrs.count_executions * qsrs.avg_log_bytes_used) / 100000000., + SUM(qsrs.count_executions * qsrs.avg_log_bytes_used) / 1048576., SUM(qsrs.count_executions * (qsrs.avg_tempdb_space_used * 8)) / 1024.' ELSE N', NULL,