|
| 1 | +<img src="https://erikdarling.com/wp-content/uploads/2025/08/darling-data-logo_RGB.jpg" width="300px" /> |
| 2 | + |
| 3 | +# sp_QueryStoreCleanup |
| 4 | + |
| 5 | +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. |
| 6 | + |
| 7 | +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`. |
| 8 | + |
| 9 | +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. |
| 10 | + |
| 11 | +Queries with forced plans are always protected from removal. |
| 12 | + |
| 13 | +## Parameters |
| 14 | + |
| 15 | +| parameter_name | data_type | description | valid_inputs | defaults | |
| 16 | +|---|---|---|---|---| |
| 17 | +| @database_name | sysname | the database to clean query store in | a database name with query store enabled | NULL; current database if NULL | |
| 18 | +| @cleanup_targets | varchar(100) | what to target for cleanup | all, system, maintenance (or maint), custom, none | all | |
| 19 | +| @custom_query_filter | nvarchar(1024) | custom LIKE pattern for query text filtering; also applied when @cleanup_targets = all | a valid LIKE pattern | NULL | |
| 20 | +| @dedupe_by | varchar(50) | deduplication strategy | all, query_hash, plan_hash, none | all | |
| 21 | +| @min_age_days | integer | only remove queries whose last execution is older than this many days | a positive integer | NULL; no age filter | |
| 22 | +| @report_only | bit | report what would be removed without removing | 0 or 1 | 0 | |
| 23 | +| @debug | bit | prints dynamic sql and diagnostics | 0 or 1 | 0 | |
| 24 | +| @help | bit | how you got here | 0 or 1 | 0 | |
| 25 | +| @version | varchar(30) | OUTPUT; for support | none; OUTPUT | none; OUTPUT | |
| 26 | +| @version_date | datetime | OUTPUT; for support | none; OUTPUT | none; OUTPUT | |
| 27 | + |
| 28 | +### Cleanup Targets |
| 29 | + |
| 30 | +The `@cleanup_targets` parameter controls which queries are identified by text pattern matching: |
| 31 | + |
| 32 | +| Value | What It Matches | |
| 33 | +|---|---| |
| 34 | +| `system` | Queries containing `FROM sys.%` | |
| 35 | +| `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`) | |
| 36 | +| `custom` | Uses your `@custom_query_filter` LIKE pattern | |
| 37 | +| `all` | system + maintenance combined; also applies `@custom_query_filter` if provided | |
| 38 | +| `none` | No text filtering; deduplication is purely hash-based across all queries | |
| 39 | + |
| 40 | +### Deduplication Strategy |
| 41 | + |
| 42 | +The `@dedupe_by` parameter controls how duplicates are identified after text filtering: |
| 43 | + |
| 44 | +| Value | Behavior | |
| 45 | +|---|---| |
| 46 | +| `query_hash` | Find queries with duplicate `query_hash` values (same query compiled multiple times) | |
| 47 | +| `plan_hash` | Find queries with duplicate `query_plan_hash` values (different queries producing identical plans) | |
| 48 | +| `all` | Both query_hash and plan_hash | |
| 49 | +| `none` | Skip hash deduplication entirely; send all text-matched queries directly to removal | |
| 50 | + |
| 51 | +**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. |
| 52 | + |
| 53 | +## Examples |
| 54 | + |
| 55 | +```sql |
| 56 | +-- Default: remove all system + maintenance duplicates from the current database |
| 57 | +EXECUTE dbo.sp_QueryStoreCleanup; |
| 58 | + |
| 59 | +-- Target a specific database |
| 60 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 61 | + @database_name = N'StackOverflow2013'; |
| 62 | + |
| 63 | +-- Report what would be removed without removing anything |
| 64 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 65 | + @database_name = N'YourDatabase', |
| 66 | + @report_only = 1; |
| 67 | + |
| 68 | +-- Only clean up system DMV queries |
| 69 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 70 | + @database_name = N'YourDatabase', |
| 71 | + @cleanup_targets = 'system'; |
| 72 | + |
| 73 | +-- Only clean up maintenance operations (index rebuilds, stats updates, DBCC, etc.) |
| 74 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 75 | + @database_name = N'YourDatabase', |
| 76 | + @cleanup_targets = 'maint'; |
| 77 | + |
| 78 | +-- Remove all text-matched queries without hash deduplication |
| 79 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 80 | + @database_name = N'YourDatabase', |
| 81 | + @cleanup_targets = 'system', |
| 82 | + @dedupe_by = 'none'; |
| 83 | + |
| 84 | +-- Use a custom text filter to find specific query patterns |
| 85 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 86 | + @database_name = N'YourDatabase', |
| 87 | + @cleanup_targets = 'custom', |
| 88 | + @custom_query_filter = N'%some_noisy_query%'; |
| 89 | + |
| 90 | +-- Only remove queries that haven't executed in the last 30 days |
| 91 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 92 | + @database_name = N'YourDatabase', |
| 93 | + @min_age_days = 30; |
| 94 | + |
| 95 | +-- Debug mode to see the generated dynamic SQL |
| 96 | +EXECUTE dbo.sp_QueryStoreCleanup |
| 97 | + @database_name = N'YourDatabase', |
| 98 | + @debug = 1; |
| 99 | +``` |
| 100 | + |
| 101 | +Copyright 2026 Darling Data, LLC |
| 102 | +Released under MIT license |
0 commit comments